From 3208a92e25ee8acca7fa78856dcd37aaeaef7bca Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 22 Jun 2025 15:08:21 +0200 Subject: [PATCH 001/116] dev: add makefile to help development --- makefile | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) create mode 100644 makefile diff --git a/makefile b/makefile new file mode 100644 index 0000000..565884a --- /dev/null +++ b/makefile @@ -0,0 +1,19 @@ + +test: + pytest --cov=src/pynurbs --cov-report=xml tests + python3-coverage report -m --fail-under 90 + python3-coverage html + +format: + isort src + isort tests + black src + black tests + flake8 src + pylint src + +docs: + sphinx-autobuild docs/ docs/_build/html + +html: + brave htmlcov/index.html \ No newline at end of file From db6d568c15fef92213cf23d1473b465c2bf10961 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 22 Jun 2025 15:11:36 +0200 Subject: [PATCH 002/116] test: set __init__ to include `src` folder --- tests/__init__.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tests/__init__.py b/tests/__init__.py index e69de29..04ddc64 100644 --- a/tests/__init__.py +++ b/tests/__init__.py @@ -0,0 +1,3 @@ +import sys + +sys.path.append("./src") From cccbaf4fe04f2063d353bc9904f291d572f05364 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 22 Jun 2025 15:12:00 +0200 Subject: [PATCH 003/116] feat: add polynomial class --- src/pynurbs/polynomial.py | 210 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 210 insertions(+) create mode 100644 src/pynurbs/polynomial.py diff --git a/src/pynurbs/polynomial.py b/src/pynurbs/polynomial.py new file mode 100644 index 0000000..7c20174 --- /dev/null +++ b/src/pynurbs/polynomial.py @@ -0,0 +1,210 @@ +""" +This file contains a class Polynomial that allows evaluating and +making operations with polynomials, like adding, multiplying, etc +""" + +from __future__ import annotations + +import math +from numbers import Real +from typing import Iterable, List, Union + + +class Polynomial: + """ + Defines a polynomial with coefficients + + p(x) = a0 + a1 * x + a2 * x^2 + ... + ap * x^p + + By receiving the coefficients + + coefs = [a0, a1, a2, ..., ap] + + This class allows evaluating, adding, multiplying, etc + + Example + ------- + >>> poly = Polynomial([3, 2]) + >>> poly(0) + 3 + >>> poly(1) + 5 + """ + + def __init__(self, coefs: Iterable[Real]): + coefs = tuple(coefs) + if len(coefs) == 0: + coefs = (0,) + self.__coefs = tuple(coefs) + + @property + def degree(self) -> int: + """ + Gives the degree of the polynomial + """ + return len(self.__coefs) - 1 + + def __iter__(self): + yield from self.__coefs + + def __getitem__(self, index): + return self.__coefs[index] + + def __neg__(self) -> Polynomial: + coefs = tuple(-coef for coef in self) + return self.__class__(coefs) + + def __add__(self, other: Union[Real, Polynomial]) -> Polynomial: + if isinstance(other, Polynomial): + 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 + else: + coefs = list(self) + coefs[0] += other + return self.__class__(coefs) + + def __mul__(self, other: Union[Real, Polynomial]) -> Polynomial: + if isinstance(other, Polynomial): + coefs = [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) + + def __truediv__(self, other: Real) -> Polynomial: + coefs = (coef / other for coef in self) + return self.__class__(coefs) + + def __pow__(self, other: int) -> Polynomial: + result = self + for _ in range(int(other) - 1): + result = result * self + return result + + def __sub__(self, other: Union[Real, Polynomial]) -> Polynomial: + return self.__add__(-other) + + def __rsub__(self, other: Real) -> Polynomial: + return (-self).__add__(other) + + def __radd__(self, other: Real) -> Polynomial: + return self.__add__(other) + + def __rmul__(self, other: Real) -> Polynomial: + return self.__mul__(other) + + def __call__(self, node: Real) -> Real: + return self.eval(node, 0) + + def __str__(self): + msgs: List[str] = [] + flag = False + for i, coef in enumerate(self): + if coef == 0: + continue + if coef < 0: + msg = "- " + elif flag: + msg = "+ " + else: + msg = "" + flag = True + coef = abs(coef) + if coef != 1 or i == 0: + msg += str(coef) + if i > 0: + if coef != 1: + msg += " * " + msg += "x" + if i > 1: + msg += f"^{i}" + msgs.append(msg) + return " ".join(msgs) + + def __repr__(self) -> str: + return str(self) + + def eval(self, node: Real, derivate: int = 0) -> Real: + """ + Evaluates the polynomial at given node + + Example + ------- + >>> poly = Polynomial([1, 2]) + >>> poly.eval(0) + 1 + >>> poly.eval(1) + 3 + >>> poly.eval(1, 1) + 2 + >>> poly.eval(1, 2) + 0 + """ + if not derivate: + coefs = self.__coefs + else: + coefs = tuple(self.derivate(derivate)) + if len(coefs) == 1: + return coefs[0] + result: Real = 0 * coefs[0] + for coef in coefs[::-1]: + result = node * result + coef + return result + + 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 * x + 5 * x^2 + >>> dpoly = poly.derivate() + >>> print(dpoly) + 2 + 10 * x + """ + coefs = ( + math.factorial(n + times) * coef / math.factorial(n) + for n, coef in enumerate(self[times:]) + ) + return self.__class__(coefs) + + +def scale(polynomial: Polynomial, amount: Real) -> Polynomial: + coefs = tuple(coef * amount**i for i, coef in enumerate(polynomial)) + return Polynomial(coefs) + + +def shift(polynomial: Polynomial, amount: Real) -> Polynomial: + """ + Transforms the polynomial p(x) into p(x-d) by + translating the curve by 'd' to the right. + + p(x) = a0 + a1 * x + ... + ap * x^p + p(x-d) = a0 + a1 * (x-d) + ... + ap * (x-d)^p + = b0 + b1 * x + ... + bp * x^p + + Example + ------- + >>> old_poly = Polynomial([0, 0, 0, 1]) + >>> print(old_poly) + x^3 + >>> new_poly = poly.shift(1) # transform to (x-1)^3 + >>> print(new_poly) + - 1 + 3 * x - 3 * x^2 + x^3 + """ + newcoefs = list(polynomial) + for i, coef in enumerate(polynomial): + for j in range(i): + binom = math.comb(i, j) + value = binom * (amount ** (i - j)) + if (i + j) % 2: + value *= -1 + newcoefs[j] += coef * value + return Polynomial(newcoefs) From 6a2c6e813e9721617c35ffb4b8a972795ca8edd3 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 22 Jun 2025 15:24:22 +0200 Subject: [PATCH 004/116] test: add test for polynomial --- tests/test_polynomial.py | 149 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 149 insertions(+) create mode 100644 tests/test_polynomial.py diff --git a/tests/test_polynomial.py b/tests/test_polynomial.py new file mode 100644 index 0000000..adb1967 --- /dev/null +++ b/tests/test_polynomial.py @@ -0,0 +1,149 @@ +import pytest + +from pynurbs.polynomial import Polynomial, scale, shift + + +@pytest.mark.order(1) +@pytest.mark.dependency() +def test_build(): + Polynomial([]) # p(x) = 0 + Polynomial([1]) # p(x) = 1 + Polynomial([1, 2]) # p(x) = 1 + 2 * x + Polynomial([1, 2, 3]) # p(x) = 1 + 2 * x + 3 * x^2 + Polynomial([1.0, 2, -3.0]) # p(x) = 1.0 + 2 * x - 3.0 * x^2 + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build"]) +def test_degree(): + poly = Polynomial([]) # p(x) = 0 + assert poly.degree == 0 + poly = Polynomial([1]) # p(x) = 1 + assert poly.degree == 0 + poly = Polynomial([1, 2]) # p(x) = 1 + 2 * x + assert poly.degree == 1 + poly = Polynomial([1, 2, 3]) # p(x) = 1 + 2 * x + 3 * x^2 + assert poly.degree == 2 + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_degree"]) +def test_evaluate(): + poly = Polynomial([]) # p(x) = 0 + assert poly.eval(0) == 0 + assert poly.eval(-1) == 0 + assert poly.eval(2) == 0 + poly = Polynomial([1]) # p(x) = 1 + assert poly.eval(0) == 1 + assert poly.eval(-1) == 1 + assert poly.eval(2) == 1 + poly = Polynomial([1, 2]) # p(x) = 1 + 2 * x + assert poly.eval(0) == 1 + assert poly.eval(-1) == 1 + 2 * (-1) + assert poly.eval(2) == 1 + 2 * (+2) + poly = Polynomial([1, 2, 3]) # p(x) = 1 + 2 * x + 3 * x^2 + assert poly.eval(0) == 1 + assert poly.eval(-1) == 1 + 2 * (-1) + 3 * (-1) * (-1) + assert poly.eval(2) == 1 + 2 * (+2) + 3 * (+2) * (+2) + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) +def test_add(): + """ + Function to test if the polynomials coefficients + are correctly computed + """ + import numpy as np + + ntests = 100 + maxdeg = 6 + tsample = np.linspace(-1, 1, 17) + for _ in range(ntests): + dega, degb = np.random.randint(0, maxdeg + 1, 2) + coefsa = np.random.uniform(-1, 1, dega + 1) + coefsb = np.random.uniform(-1, 1, degb + 1) + polya = Polynomial(coefsa) + polyb = Polynomial(coefsb) + polyc = polya + polyb + valuesa = polya(tsample) + valuesb = polyb(tsample) + valuesc = polyc(tsample) + + np.testing.assert_allclose(valuesa + valuesb, valuesc) + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) +def test_mul(): + """ + Function to test if the polynomials coefficients + are correctly computed + """ + import numpy as np + + ntests = 100 + maxdeg = 6 + tsample = np.linspace(-1, 1, 17) + for _ in range(ntests): + dega, degb = np.random.randint(0, maxdeg + 1, 2) + coefsa = np.random.uniform(-1, 1, dega + 1) + coefsb = np.random.uniform(-1, 1, degb + 1) + polya = Polynomial(coefsa) + polyb = Polynomial(coefsb) + polyc = polya * polyb + valuesa = polya(tsample) + valuesb = polyb(tsample) + valuesc = polyc(tsample) + + np.testing.assert_allclose(valuesa * valuesb, valuesc) + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] +) +def test_shift(): + """ + Function to test if the polynomials coefficients + are correctly computed + """ + import numpy as np + + ntests = 100 + maxdeg = 6 + tsample = np.linspace(-1, 1, 17) + for _ in range(ntests): + dega = np.random.randint(0, maxdeg + 1) + coefsa = np.random.uniform(-1, 1, dega + 1) + polya = Polynomial(coefsa) + polyb = shift(polya, 1) + valuesa = polya(tsample) + valuese = polyb(1 + tsample) + + np.testing.assert_allclose(valuese, valuesa) + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] +) +def test_scale(): + """ + Function to test if the polynomials coefficients + are correctly computed + """ + import numpy as np + + ntests = 100 + maxdeg = 6 + tsample = np.linspace(-1, 1, 17) + for _ in range(ntests): + dega = np.random.randint(0, maxdeg + 1) + coefsa = np.random.uniform(-1, 1, dega + 1) + polya = Polynomial(coefsa) + polyb = scale(polya, 2) + valuesa = polya(2 * tsample) + valuesb = polyb(tsample) + + np.testing.assert_allclose(valuesb, valuesa) From 79dbdcd97fa4f4c0a26b619b32f857190b58ed89 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 22 Jun 2025 15:27:11 +0200 Subject: [PATCH 005/116] docs: improve docs of `scale` and `shift` of polynomial --- src/pynurbs/polynomial.py | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/src/pynurbs/polynomial.py b/src/pynurbs/polynomial.py index 7c20174..0b20d77 100644 --- a/src/pynurbs/polynomial.py +++ b/src/pynurbs/polynomial.py @@ -177,6 +177,23 @@ def derivate(self, times: int = 1) -> Polynomial: def scale(polynomial: Polynomial, amount: Real) -> Polynomial: + """ + Transforms the polynomial p(x) into p(A*x) by + scaling the argument of the polynomial by 'A'. + + p(x) = a0 + a1 * x + ... + ap * x^p + p(A * x) = a0 + a1 * (A*x) + ... + ap * (A * x)^p + = b0 + b1 * x + ... + bp * x^p + + Example + ------- + >>> old_poly = Polynomial([0, 0, 0, 1]) + >>> print(old_poly) + x^3 + >>> new_poly = scale(poly, 1) # transform to (x-1)^3 + >>> print(new_poly) + - 1 + 3 * x - 3 * x^2 + x^3 + """ coefs = tuple(coef * amount**i for i, coef in enumerate(polynomial)) return Polynomial(coefs) @@ -184,7 +201,7 @@ def scale(polynomial: Polynomial, amount: Real) -> Polynomial: def shift(polynomial: Polynomial, amount: Real) -> Polynomial: """ Transforms the polynomial p(x) into p(x-d) by - translating the curve by 'd' to the right. + translating the polynomial by 'd' to the right. p(x) = a0 + a1 * x + ... + ap * x^p p(x-d) = a0 + a1 * (x-d) + ... + ap * (x-d)^p @@ -195,7 +212,7 @@ def shift(polynomial: Polynomial, amount: Real) -> Polynomial: >>> old_poly = Polynomial([0, 0, 0, 1]) >>> print(old_poly) x^3 - >>> new_poly = poly.shift(1) # transform to (x-1)^3 + >>> new_poly = shift(poly, 1) # transform to (x-1)^3 >>> print(new_poly) - 1 + 3 * x - 3 * x^2 + x^3 """ From f779c3fe66c733e76e33f3fe9409b9369572e4dd Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Mon, 23 Jun 2025 18:21:43 +0200 Subject: [PATCH 006/116] refac: move derivate function outside the class --- src/pynurbs/polynomial.py | 39 ++++++++++++++++++++------------------- 1 file changed, 20 insertions(+), 19 deletions(-) diff --git a/src/pynurbs/polynomial.py b/src/pynurbs/polynomial.py index 0b20d77..fa1397e 100644 --- a/src/pynurbs/polynomial.py +++ b/src/pynurbs/polynomial.py @@ -156,25 +156,6 @@ def eval(self, node: Real, derivate: int = 0) -> Real: result = node * result + coef return result - 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 * x + 5 * x^2 - >>> dpoly = poly.derivate() - >>> print(dpoly) - 2 + 10 * x - """ - coefs = ( - math.factorial(n + times) * coef / math.factorial(n) - for n, coef in enumerate(self[times:]) - ) - return self.__class__(coefs) - def scale(polynomial: Polynomial, amount: Real) -> Polynomial: """ @@ -225,3 +206,23 @@ def shift(polynomial: Polynomial, amount: Real) -> Polynomial: value *= -1 newcoefs[j] += coef * value return Polynomial(newcoefs) + + +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 * x + 5 * x^2 + >>> dpoly = poly.derivate() + >>> print(dpoly) + 2 + 10 * x + """ + coefs = ( + math.factorial(n + times) // math.factorial(n) * coef + for n, coef in enumerate(polynomial[times:]) + ) + return Polynomial(coefs) From 9059fd68561e9eec84ba0f4084ed96731e33de04 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Mon, 23 Jun 2025 18:25:41 +0200 Subject: [PATCH 007/116] test: improve test for polynomials --- tests/test_polynomial.py | 90 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 89 insertions(+), 1 deletion(-) diff --git a/tests/test_polynomial.py b/tests/test_polynomial.py index adb1967..4fb260f 100644 --- a/tests/test_polynomial.py +++ b/tests/test_polynomial.py @@ -1,6 +1,6 @@ import pytest -from pynurbs.polynomial import Polynomial, scale, shift +from pynurbs.polynomial import Polynomial, derivate, scale, shift @pytest.mark.order(1) @@ -47,6 +47,15 @@ def test_evaluate(): assert poly.eval(2) == 1 + 2 * (+2) + 3 * (+2) * (+2) +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) +def test_neg(): + polya = Polynomial([1, 2, 3, 4]) + polyb = Polynomial([-1, -2, -3, -4]) + + assert -polya == polyb + + @pytest.mark.order(1) @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_add(): @@ -72,6 +81,17 @@ def test_add(): np.testing.assert_allclose(valuesa + valuesb, valuesc) + for _ in range(ntests): + dega = np.random.randint(0, maxdeg + 1) + coefsa = np.random.uniform(-1, 1, dega + 1) + const = np.random.uniform(-1, 1, 1) + polya = Polynomial(coefsa) + polyb = polya + const + valuesa = polya(tsample) + valuesb = polyb(tsample) + + np.testing.assert_allclose(valuesa + const, valuesb) + @pytest.mark.order(1) @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) @@ -98,6 +118,55 @@ def test_mul(): np.testing.assert_allclose(valuesa * valuesb, valuesc) + for _ in range(ntests): + dega = np.random.randint(0, maxdeg + 1) + coefsa = np.random.uniform(-1, 1, dega + 1) + const = np.random.uniform(-1, 1) + polya = Polynomial(coefsa) + polyb = polya * const + valuesa = polya(tsample) + valuesb = polyb(tsample) + + np.testing.assert_allclose(valuesa * const, valuesb) + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) +def test_truediv(): + """ + Function to test if the polynomials coefficients + are correctly computed + """ + import numpy as np + + ntests = 100 + maxdeg = 6 + for _ in range(ntests): + dega = np.random.randint(0, maxdeg + 1) + coefsa = np.random.uniform(-1, 1, dega + 1) + divisor = np.random.randint(1, 10) + coefsb = [coef / divisor for coef in coefsa] + assert Polynomial(coefsa) / divisor == Polynomial(coefsb) + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] +) +def test_derivate(): + poly = Polynomial([0]) + assert derivate(poly, 1) == 0 + assert derivate(poly, 2) == 0 + + poly = Polynomial([3]) + assert derivate(poly, 1) == 0 + assert derivate(poly, 2) == 0 + + poly = Polynomial([1, 1, 1, 1, 1]) + assert derivate(poly, 1) == Polynomial([1, 2, 3, 4]) + assert derivate(poly, 2) == Polynomial([2, 6, 12]) + assert derivate(poly, 3) == Polynomial([6, 24]) + @pytest.mark.order(1) @pytest.mark.dependency( @@ -147,3 +216,22 @@ def test_scale(): valuesb = polyb(tsample) np.testing.assert_allclose(valuesb, valuesa) + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=[ + "test_build", + "test_degree", + "test_evaluate", + "test_neg", + "test_add", + "test_mul", + "test_truediv", + "test_derivate", + "test_shift", + "test_scale", + ] +) +def test_all(): + pass From 37cd4cdbd1cc7d3ad35712fcf018cc8f8a5ae036 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Mon, 23 Jun 2025 18:26:14 +0200 Subject: [PATCH 008/116] feat: add __eq__ function for polynomial --- src/pynurbs/polynomial.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pynurbs/polynomial.py b/src/pynurbs/polynomial.py index fa1397e..29397a3 100644 --- a/src/pynurbs/polynomial.py +++ b/src/pynurbs/polynomial.py @@ -44,6 +44,11 @@ def degree(self) -> int: """ return len(self.__coefs) - 1 + def __eq__(self, value: object) -> bool: + if isinstance(value, Polynomial): + return tuple(self) == tuple(value) + return self.degree == 0 and value == self[0] + def __iter__(self): yield from self.__coefs From 670bc71046ba8033ccc22322cb19b2dc92a87f9c Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Mon, 23 Jun 2025 19:18:02 +0200 Subject: [PATCH 009/116] test: improve polynomial tests --- tests/test_polynomial.py | 98 +++++++++++++++++++++++++++++++++++++++- 1 file changed, 97 insertions(+), 1 deletion(-) diff --git a/tests/test_polynomial.py b/tests/test_polynomial.py index 4fb260f..62f80ad 100644 --- a/tests/test_polynomial.py +++ b/tests/test_polynomial.py @@ -84,13 +84,56 @@ def test_add(): for _ in range(ntests): dega = np.random.randint(0, maxdeg + 1) coefsa = np.random.uniform(-1, 1, dega + 1) - const = np.random.uniform(-1, 1, 1) + const = np.random.uniform(-1, 1) polya = Polynomial(coefsa) polyb = polya + const + polyc = const + polya valuesa = polya(tsample) valuesb = polyb(tsample) + valuesc = polyc(tsample) np.testing.assert_allclose(valuesa + const, valuesb) + np.testing.assert_allclose(const + valuesa, valuesc) + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) +def test_sub(): + """ + Function to test if the polynomials coefficients + are correctly computed + """ + import numpy as np + + ntests = 100 + maxdeg = 6 + tsample = np.linspace(-1, 1, 17) + for _ in range(ntests): + dega, degb = np.random.randint(0, maxdeg + 1, 2) + coefsa = np.random.uniform(-1, 1, dega + 1) + coefsb = np.random.uniform(-1, 1, degb + 1) + polya = Polynomial(coefsa) + polyb = Polynomial(coefsb) + polyc = polya - polyb + valuesa = polya(tsample) + valuesb = polyb(tsample) + valuesc = polyc(tsample) + + np.testing.assert_allclose(valuesa - valuesb, valuesc) + + for _ in range(ntests): + dega = np.random.randint(0, maxdeg + 1) + coefsa = np.random.uniform(-1, 1, dega + 1) + const = np.random.uniform(-1, 1) + polya = Polynomial(coefsa) + polyb = polya - const + polyc = const - polya + valuesa = polya(tsample) + valuesb = polyb(tsample) + valuesc = polyc(tsample) + + np.testing.assert_allclose(valuesa - const, valuesb) + np.testing.assert_allclose(const - valuesa, valuesc) @pytest.mark.order(1) @@ -124,10 +167,13 @@ def test_mul(): const = np.random.uniform(-1, 1) polya = Polynomial(coefsa) polyb = polya * const + polyc = const * polya valuesa = polya(tsample) valuesb = polyb(tsample) + valuesc = polyc(tsample) np.testing.assert_allclose(valuesa * const, valuesb) + np.testing.assert_allclose(const * valuesa, valuesc) @pytest.mark.order(1) @@ -149,6 +195,15 @@ def test_truediv(): assert Polynomial(coefsa) / divisor == Polynomial(coefsb) +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) +def test_pow(): + poly = Polynomial([-1, 1]) + assert poly**2 == Polynomial([1, -2, 1]) + assert poly**3 == Polynomial([-1, 3, -3, 1]) + assert poly**4 == Polynomial([1, -4, 6, -4, 1]) + + @pytest.mark.order(1) @pytest.mark.dependency( depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] @@ -168,6 +223,33 @@ def test_derivate(): assert derivate(poly, 3) == Polynomial([6, 24]) +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=[ + "test_build", + "test_degree", + "test_evaluate", + "test_add", + "test_mul", + "test_derivate", + ] +) +def test_evaluate_derivate(): + import numpy as np + + ntests = 100 + maxdeg = 6 + tvalues = np.linspace(-1, 1, 129) + for _ in range(ntests): + dega = np.random.randint(0, maxdeg + 1) + coefsa = np.random.uniform(-1, 1, dega + 1) + polya = Polynomial(coefsa) + for times in range(dega + 1): + dpolya = derivate(polya, times) + for tval in tvalues: + assert polya.eval(tval, times) == dpolya.eval(tval, 0) + + @pytest.mark.order(1) @pytest.mark.dependency( depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] @@ -218,6 +300,18 @@ def test_scale(): np.testing.assert_allclose(valuesb, valuesa) +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build"]) +def test_print(): + poly = Polynomial([0]) + assert str(poly) == "0" + poly = Polynomial([1]) + assert str(poly) == "1" + poly = Polynomial([0, 1]) + assert str(poly) == "x" + repr(poly) + + @pytest.mark.order(1) @pytest.mark.dependency( depends=[ @@ -226,8 +320,10 @@ def test_scale(): "test_evaluate", "test_neg", "test_add", + "test_sub", "test_mul", "test_truediv", + "test_pow", "test_derivate", "test_shift", "test_scale", From 4a78776717417bf325859ebe0641aa4197539a63 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Mon, 23 Jun 2025 19:25:06 +0200 Subject: [PATCH 010/116] fix: evaluate and __str__ --- src/pynurbs/polynomial.py | 21 ++++++++++----------- 1 file changed, 10 insertions(+), 11 deletions(-) diff --git a/src/pynurbs/polynomial.py b/src/pynurbs/polynomial.py index 29397a3..9f143e7 100644 --- a/src/pynurbs/polynomial.py +++ b/src/pynurbs/polynomial.py @@ -56,8 +56,7 @@ def __getitem__(self, index): return self.__coefs[index] def __neg__(self) -> Polynomial: - coefs = tuple(-coef for coef in self) - return self.__class__(coefs) + return self.__class__(-coef for coef in self) def __add__(self, other: Union[Real, Polynomial]) -> Polynomial: if isinstance(other, Polynomial): @@ -107,6 +106,8 @@ def __call__(self, node: Real) -> Real: return self.eval(node, 0) def __str__(self): + if self.degree == 0: + return str(self[0]) msgs: List[str] = [] flag = False for i, coef in enumerate(self): @@ -134,7 +135,7 @@ def __str__(self): def __repr__(self) -> str: return str(self) - def eval(self, node: Real, derivate: int = 0) -> Real: + def eval(self, node: Real, times: int = 0) -> Real: """ Evaluates the polynomial at given node @@ -150,14 +151,12 @@ def eval(self, node: Real, derivate: int = 0) -> Real: >>> poly.eval(1, 2) 0 """ - if not derivate: - coefs = self.__coefs - else: - coefs = tuple(self.derivate(derivate)) - if len(coefs) == 1: - return coefs[0] - result: Real = 0 * coefs[0] - for coef in coefs[::-1]: + if times: + return derivate(self, times).eval(node, 0) + if self.degree == 0: + return self[0] + result: Real = 0 * self[0] + for coef in self[::-1]: result = node * result + coef return result From 44d85cd4acd81200668cc4f1e7d8a8ff5eceeaba Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 24 Jun 2025 20:55:39 +0200 Subject: [PATCH 011/116] feat: add piecewise polynomial function --- src/pynurbs/piecepoly.py | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) create mode 100644 src/pynurbs/piecepoly.py diff --git a/src/pynurbs/piecepoly.py b/src/pynurbs/piecepoly.py new file mode 100644 index 0000000..2bdbd7d --- /dev/null +++ b/src/pynurbs/piecepoly.py @@ -0,0 +1,37 @@ +""" +Defines the Piecewise Polynomial class +""" + +from numbers import Real +from typing import Iterable, Tuple + +from .polynomial import Polynomial + + +class PiecewisePolynomial: + """ + Defines a Polynomial piecewise function + """ + + def __init__(self, functions: Iterable[Polynomial], knots: Iterable[Real]) -> None: + self.__functions = tuple(functions) + self.__knots = tuple(knots) + + @property + def knots(self) -> Tuple[Real, ...]: + return self.__knots + + @property + def functions(self) -> Tuple[Polynomial, ...]: + return self.__functions + + def eval(self, node: Real, times: int = 0) -> Real: + nsegs = len(self.functions) + mask = self.knots[nsegs - 1] <= node + mask *= node < self.knots[nsegs] + result = mask * self.functions[-1].eval(node, times) + for i in range(nsegs - 1): + mask = self.knots[i] <= node + mask *= node < self.knots[i + 1] + result += mask * self.functions[i].eval(node, times) + return result From d1ffa5471bb794fda271481cb09c0e4d3dee99fb Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 27 Jun 2025 19:42:43 +0200 Subject: [PATCH 012/116] refactor: move math functions to a cmath.py file --- src/pynurbs/cmath.py | 508 ++++++++++++++++++++++++++ src/pynurbs/heavy.py | 506 +------------------------- tests/test_cmath.py | 787 ++++++++++++++++++++++++++++++++++++++++ tests/test_functions.py | 2 +- tests/test_heavy.py | 776 +-------------------------------------- 5 files changed, 1301 insertions(+), 1278 deletions(-) create mode 100644 src/pynurbs/cmath.py create mode 100644 tests/test_cmath.py diff --git a/src/pynurbs/cmath.py b/src/pynurbs/cmath.py new file mode 100644 index 0000000..528ee9b --- /dev/null +++ b/src/pynurbs/cmath.py @@ -0,0 +1,508 @@ +import math +from copy import deepcopy +from fractions import Fraction +from typing import Optional, Tuple, Union + +import numpy as np + + +class Math: + @staticmethod + def gcd(*numbers: Tuple[int]) -> int: + lenght = len(numbers) + if lenght == 1: + return abs(numbers[0]) + if lenght == 2: + x, y = numbers + else: + middle = lenght // 2 + x = Math.gcd(*numbers[:middle]) + y = Math.gcd(*numbers[middle:]) + while y: + x, y = y, x % y + return abs(x) + + @staticmethod + def lcm(*numbers: Tuple[int]) -> int: + lenght = len(numbers) + if lenght == 1: + return numbers[0] + if lenght == 2: + x, y = numbers + else: + middle = lenght // 2 + x = Math.lcm(*numbers[:middle]) + y = Math.lcm(*numbers[middle:]) + if x == 0 or y == 0: + return y if x == 0 else y + return x * y // Math.gcd(x, y) + + @staticmethod + def factorial(number: int) -> int: + if number < 2: + return 1 + prod = 1 + for i in range(2, number + 1): + prod *= i + return prod + + @staticmethod + def comb(upper: int, lower: int) -> int: + numerator = Math.factorial(upper) + denominator = Math.factorial(lower) + denominator *= Math.factorial(upper - lower) + return numerator // denominator + + +def number_type(number: Union[int, float, Fraction]): + """ + Returns the type of a number, if it's a integer, a float, fraction + It accepts tuple, lists and so on such: + [int, int, int] -> int + [int, Fraction, int] -> Fraction + [int, int, float] -> float + [Fraction, float, int] -> float + """ + try: + iter(number) + tipos = [] + for numb in number: + tipo = number_type(numb) + if tipo is float: + return float + tipos.append(tipo) + for tipo in tipos: + if tipo is Fraction: + return Fraction + return int + except TypeError: + if isinstance(number, (int, np.integer)): + return int + if isinstance(number, Fraction): + return Fraction + return float + + +def totuple(array): + """ + Convert recursively an array to tuples + """ + try: + return tuple(map(tuple, array)) + except TypeError: # Cannot iterate + return tuple(array) + + +def binom(n: int, i: int): + """ + Returns binomial (n, i) + """ + assert isinstance(n, int) + assert isinstance(i, int) + prod = 1 + if i <= 0 or i >= n: + return 1 + for j in range(i): + prod *= (n - j) / (i - j) + return int(prod) + + +class NodeSample: + __cheby = {1: (Fraction(1, 2),)} + __gauss = {1: (Fraction(1, 2),)} + + @staticmethod + def closed_linspace(npts: int, cls: Optional[type] = Fraction) -> Tuple[float]: + """Returns equally distributed nodes in [0, 1] + Include the extremities + + Example + ------------ + >>> NodeSample.closed_linspace(2) + (0, 1) + >>> NodeSample.closed_linspace(3) + (0, 1/2, 1) + >>> NodeSample.closed_linspace(4) + (0, 1/3, 2/3, 1) + >>> NodeSample.closed_linspace(5) + (0, 1/4, 2/4, 3/4, 1) + >>> NodeSample.closed_linspace(6) + (0, 1/5, 2/5, 3/5, 4/5, 1) + """ + assert isinstance(npts, int) + assert npts > 1 + nums = tuple(range(0, npts)) + nums = tuple(cls(num) / (npts - 1) for num in nums) + return nums + + @staticmethod + def open_linspace(npts: int, cls: Optional[type] = Fraction) -> Tuple[float]: + """Returns equally distributed nodes in (0, 1) + Exclude the extremities + + Example + ------------ + >>> NodeSample.open_linspace(1) + (1/2, ) + >>> NodeSample.open_linspace(2) + (1/4, 3/4) + >>> NodeSample.open_linspace(3) + (1/6, 3/6, 5/6) + >>> NodeSample.open_linspace(4) + (1/8, 3/8, 5/8, 7/8) + >>> NodeSample.open_linspace(5) + (1/10, 3/10, 5/10, 7/10, 9/10) + """ + assert isinstance(npts, int) + assert npts > 0 + nums = range(1, 2 * npts, 2) + nums = tuple(cls(num) / (2 * npts) for num in nums) + return nums + + @staticmethod + def chebyshev(npts: int) -> Tuple[float]: + """ + Returns chebyshev nodes in the space [0, 1] + `Chebyshev nodes `_ + + + >>> NodeSample.chebyshev(1) + (0.5,) + >>> NodeSample.chebyshev(2) + (0.146, 0.854) + >>> NodeSample.chebyshev(3) + (0.067, 0.5, 0.933) + >>> NodeSample.chebyshev(4) + (0.038, 0.309, 0.691, 0.962) + >>> NodeSample.chebyshev(5) + (0.024, 0.206, 0.5, 0.794, 0.976) + """ + assert isinstance(npts, int) + assert npts > 0 + if npts not in NodeSample.__cheby: + nums = NodeSample.open_linspace(npts) + nums = tuple(math.sin(0.5 * math.pi * num) ** 2 for num in nums) + NodeSample.__cheby[npts] = nums + return NodeSample.__cheby[npts] + + @staticmethod + def gauss_legendre(npts: int) -> Tuple[float]: + """ + Returns gauss legendre quadrature nodes in the space [0, 1] + `Gauss-Legendre quadrature `_ + + >>> NodeSample.gauss_legendre(1) + (0.5,) + >>> NodeSample.gauss_legendre(2) + (0.146, 0.854) + >>> NodeSample.gauss_legendre(3) + (0.067, 0.5, 0.933) + >>> NodeSample.gauss_legendre(4) + (0.038, 0.309, 0.691, 0.962) + >>> NodeSample.gauss_legendre(5) + (0.024, 0.206, 0.5, 0.794, 0.976) + """ + assert isinstance(npts, int) + assert npts > 0 + if npts not in NodeSample.__gauss: + nums, _ = np.polynomial.legendre.leggauss(npts) + nums = (1 + nums) / 2 + NodeSample.__gauss[npts] = tuple(nums) + return NodeSample.__gauss[npts] + + +class IntegratorArray: + __closed_newton = { + 2: (Fraction(1, 2), Fraction(1, 2)), + 3: (Fraction(1, 6), Fraction(2, 3), Fraction(1, 6)), + 4: (Fraction(1, 8), Fraction(3, 8), Fraction(3, 8), Fraction(1, 8)), + } + __open_newton = { + 1: (Fraction(1),), + 2: (Fraction(1, 2), Fraction(1, 2)), + 3: (Fraction(3, 8), Fraction(1, 4), Fraction(3, 8)), + } + __cheby = { + 1: (Fraction(1),), + 2: (Fraction(1, 2), Fraction(1, 2)), + 3: (Fraction(2, 9), Fraction(5, 9), Fraction(2, 9)), + } + __gauss = { + 1: (Fraction(1),), + 2: (Fraction(1, 2), Fraction(1, 2)), + 3: (Fraction(5, 18), Fraction(4, 9), Fraction(5, 18)), + } + + @staticmethod + def interpolate_bezier(nodes: Tuple[float]) -> Tuple[Tuple[float]]: + """Returns a matrix that interpolates a function at given nodes using bezier + + This function returns the inverse of matrix [M] which + interpolates a bezier curve C at the given nodes + C(u) = sum_{i=0}^{p} B_{i,p}(u) * P_{i} + B_{i,p}(u) = binom(p, i) * (1-u)^{p-i} * u^i + [M]_{i,k} = B_{i,p}(u_k) + [M] * [P] = [f(x_k)] + + Example + ------------ + >>> nodes = (0, 0.2, 1) + >>> IntegratorArray.interpolate_bezier(nodes) + ((1, -2, 0), (0, 25/8, 0), (0, -1/8, 1)) + >>> nodes = (0, 0.5, 1) + >>> IntegratorArray.interpolate_bezier(nodes) + ((1, -1/2, 0), (0, 2, 0), (0, -1/2, 1)) + + """ + assert isinstance(nodes, tuple) + for node in nodes: + float(node) + assert 0 <= node + assert node <= 1 + degree = len(nodes) - 1 + matrix_bezier = np.zeros((degree + 1, degree + 1), dtype="object") + for k, uk in enumerate(nodes): + for i in range(degree + 1): + matrix_bezier[i, k] = ( + Math.comb(degree, i) * (1 - uk) ** (degree - i) * (uk**i) + ) + matrix_bezier = totuple(matrix_bezier) + inverse = Linalg.invert(matrix_bezier) + inverse = tuple(map(tuple, inverse)) + return inverse + + @staticmethod + def bezier_integrator_array(nodes: Tuple[float]) -> Tuple[float]: + """Computes the weights to integrate at given nodes + + Given ``nodes`` the positions of ``n`` values of ``x_i``, + this function returns ``n`` values of ``w_i`` such + + int_{0}^{1} f(u) du = sum_{i=0}^{n-1} w_i * f(x_i) + + Example + ------------ + >>> nodes = (0, 0.2, 1) + >>> IntegratorArray.bezier_integrator_array(nodes) + (1/3, 1/3, 1/3) + >>> nodes = (0, 0.5, 1) + >>> IntegratorArray.bezier_integrator_array(nodes) + (1/3, 1/3, 1/3) + """ + matrix = IntegratorArray.interpolate_bezier(nodes) + array = [sum(line) / len(nodes) for line in matrix] + return tuple(array) + + @staticmethod + def closed_newton_cotes(npts: int) -> Tuple[Tuple[float]]: + """Returns the weight array for closed newton-cotes formula + in the interval [0, 1] + + Example + ------------ + >>> IntegratorArray.closed_newton_cotes(2) + (1/2, 1/2) + >>> IntegratorArray.closed_newton_cotes(3) + (1/6, 4/6, 1/6) + >>> IntegratorArray.closed_newton_cotes(4) + (1/8, 3/8, 3/8, 1/8) + >>> IntegratorArray.closed_newton_cotes(5) + (7/90, 16/45, 2/15, 16/45, 7/90) + """ + assert isinstance(npts, int) + assert npts > 1 + if npts not in IntegratorArray.__closed_newton: + nodes = NodeSample.closed_linspace(npts, Fraction) + weights = IntegratorArray.bezier_integrator_array(nodes) + IntegratorArray.__closed_newton[npts] = weights + return IntegratorArray.__closed_newton[npts] + + @staticmethod + def open_newton_cotes(npts: int) -> Tuple[Tuple[float]]: + """Returns the weight array for open newton-cotes formula + in the interval (0, 1) + + Example + ------------ + >>> IntegratorArray.open_newton_cotes(1) + (1, ) + >>> IntegratorArray.open_newton_cotes(2) + (1/2, 1/2) + >>> IntegratorArray.open_newton_cotes(3) + (3/8, 1/4, 3/8) + >>> IntegratorArray.open_newton_cotes(4) + (13/48, 11/48, 11/48, 13/48) + >>> IntegratorArray.open_newton_cotes(5) + (275/1152, 25/288, 67/192, 25/288, 275/1152) + + """ + assert isinstance(npts, int) + assert npts > 0 + if npts not in IntegratorArray.__open_newton: + nodes = NodeSample.open_linspace(npts, Fraction) + weights = IntegratorArray.bezier_integrator_array(nodes) + IntegratorArray.__open_newton[npts] = weights + return IntegratorArray.__open_newton[npts] + + @staticmethod + def chebyshev(npts: int) -> Tuple[float]: + """Returns the weight array for integrate at chebyshev nodes + + Example + ------------ + >>> IntegratorArray.chebyshev(1) + (1, ) + >>> IntegratorArray.chebyshev(2) + (1/2, 1/2) + >>> IntegratorArray.chebyshev(3) + (3/8, 1/4, 3/8) + >>> IntegratorArray.chebyshev(4) + (13/48, 11/48, 11/48, 13/48) + >>> IntegratorArray.chebyshev(5) + (275/1152, 25/288, 67/192, 25/288, 275/1152) + + """ + assert isinstance(npts, int) + assert 0 < npts + if npts not in IntegratorArray.__cheby: + nodes = NodeSample.chebyshev(npts) + weights = IntegratorArray.bezier_integrator_array(nodes) + IntegratorArray.__cheby[npts] = weights + return IntegratorArray.__cheby[npts] + + @staticmethod + def gauss_legendre(npts: int) -> Tuple[float]: + """Returns the weight array for integrate at gauss nodes + + Example + ------------ + >>> IntegratorArray.chebyshev(1) + (1, ) + >>> IntegratorArray.chebyshev(2) + (1/2, 1/2) + >>> IntegratorArray.chebyshev(3) + (3/8, 1/4, 3/8) + >>> IntegratorArray.chebyshev(4) + (13/48, 11/48, 11/48, 13/48) + >>> IntegratorArray.chebyshev(5) + (275/1152, 25/288, 67/192, 25/288, 275/1152) + + """ + assert isinstance(npts, int) + assert 0 < npts + if npts not in IntegratorArray.__gauss: + _, weights = np.polynomial.legendre.leggauss(npts) + IntegratorArray.__gauss[npts] = tuple(weights / 2) + return IntegratorArray.__gauss[npts] + + +class Linalg: + @staticmethod + def solve(matrix: Tuple[Tuple[float]], force: Tuple[Tuple[float]]): + numbtype = number_type((matrix, force)) + if numbtype not in (int, Fraction): + matrix = np.array(matrix, dtype="float64") + force = np.array(force, dtype="float64") + return totuple(np.linalg.solve(matrix, force)) + matrix = [[deepcopy(elem) for elem in line] for line in matrix] + inverse = Linalg.invert(matrix) + result = np.dot(inverse, force) + if numbtype is int: + all_int = True + for i, line in enumerate(result): + for j, elem in enumerate(line): + if elem.denominator == 1: + result[i, j] = int(elem) + else: + all_int = False + result = result.astype("int64") if all_int else result + return totuple(result) + + @staticmethod + def invert(matrix: Tuple[Tuple[float]]): + numbtype = number_type(matrix) + if numbtype not in (int, Fraction): + matrix = np.array(matrix, dtype="float64") + return totuple(np.linalg.inv(matrix)) + matrix = [[deepcopy(elem) for elem in line] for line in matrix] + denomins = [1] * len(matrix) + for i, line in enumerate(matrix): + lcm = Math.lcm(*[Fraction(elem).denominator for elem in line]) + denomins[i] *= lcm + for j, elem in enumerate(line): + line[j] = lcm * elem + matrix = tuple(tuple(int(elem) for elem in line) for line in matrix) + diagonal, inverse = Linalg.invert_integer_matrix(matrix) + inverse = np.array(inverse, dtype="object") + for i, diag in enumerate(diagonal): + for j, denom in enumerate(denomins): + inverse[i, j] = Fraction(denom * inverse[i, j], diag) + return inverse + + @staticmethod + def lstsq(matrix: Tuple[Tuple[float]]): + """ + Given a matrix A of shape (n, m), with n >= m + We want the best solution X for + [A] * [X] approx [B] + To do it, we first transform into a square matrix and solve: + [A]^T * [A] * [X] = [A]^T * [B] + This function in fact returns the matrix [M] such + [X] = [M] * [B] + [M] = (A^T * A)^{-1} * A^T + """ + matrix = np.array(matrix) + assert matrix.shape[0] >= matrix.shape[1] + if matrix.shape[0] == matrix.shape[1]: + ident = totuple(np.eye(len(matrix), dtype="object")) + return Linalg.solve(matrix, ident) + return Linalg.solve(matrix.T @ matrix, matrix.T) + + def invert_integer_matrix( + matrix: Tuple[Tuple[int]], + ) -> Tuple[Tuple[int], Tuple[Tuple[int]]]: + """ + Given a matrix A with integer entries, this function computes the + inverse of this matrix by gaussian elimination. + + # Input: + matrix: Tuple[Tuple[int]] + Square matrix A of size (m, m) of integer values + + # Output: + diagonal: Tuple[int] + The final diagonal D after gaussian elimination, with values d_i + inverse: Tuple[Tuple[int]] + The final inversed matrix M = diag(D) * A^{-1} + """ + side = len(matrix) + inverse = np.eye(side, dtype="object") + matrix = np.column_stack((matrix, inverse)) + + # Eliminate lower triangle + for k in range(side): + # Swap pivos + if matrix[k, k] == 0: + for i in range(k + 1, side): + if matrix[i, k] != 0: + matrix[[k, i]] = matrix[[i, k]] + break + # Eliminate lines bellow + if matrix[k, k] < 0: + matrix[k] *= -1 + for i in range(k + 1, side): + matrix[i] = matrix[i] * matrix[k, k] - matrix[k] * matrix[i, k] + gdcline = Math.gcd(*matrix[i]) + if gdcline != 1: + matrix[i] = matrix[i] // gdcline + + # Eliminate upper triangle + for k in range(side - 1, 0, -1): + for i in range(k - 1, -1, -1): + matrix[i] = matrix[i] * matrix[k, k] - matrix[k] * matrix[i, k] + gdcline = Math.gcd(*matrix[i]) + if gdcline != 1: + matrix[i] = matrix[i] // gdcline + diagonal = list(np.diag(matrix[:, :side])) + inverse = matrix[:, side:] + return totuple(diagonal), totuple(inverse) diff --git a/src/pynurbs/heavy.py b/src/pynurbs/heavy.py index e3aeef9..f50749a 100644 --- a/src/pynurbs/heavy.py +++ b/src/pynurbs/heavy.py @@ -6,13 +6,13 @@ from __future__ import annotations -import math -from copy import deepcopy from fractions import Fraction from typing import Optional, Tuple, Union import numpy as np +from .cmath import IntegratorArray, Linalg, NodeSample, number_type, totuple + class ImmutableKnotVector(tuple): @staticmethod @@ -254,83 +254,6 @@ def insert(self, nodes: Tuple[float]) -> ImmutableKnotVector: return self.__class__(vector, self.degree) -class Math: - @staticmethod - def gcd(*numbers: Tuple[int]) -> int: - lenght = len(numbers) - if lenght == 1: - return abs(numbers[0]) - if lenght == 2: - x, y = numbers - else: - middle = lenght // 2 - x = Math.gcd(*numbers[:middle]) - y = Math.gcd(*numbers[middle:]) - while y: - x, y = y, x % y - return abs(x) - - @staticmethod - def lcm(*numbers: Tuple[int]) -> int: - lenght = len(numbers) - if lenght == 1: - return numbers[0] - if lenght == 2: - x, y = numbers - else: - middle = lenght // 2 - x = Math.lcm(*numbers[:middle]) - y = Math.lcm(*numbers[middle:]) - if x == 0 or y == 0: - return y if x == 0 else y - return x * y // Math.gcd(x, y) - - @staticmethod - def factorial(number: int) -> int: - if number < 2: - return 1 - prod = 1 - for i in range(2, number + 1): - prod *= i - return prod - - @staticmethod - def comb(upper: int, lower: int) -> int: - numerator = Math.factorial(upper) - denominator = Math.factorial(lower) - denominator *= Math.factorial(upper - lower) - return numerator // denominator - - -def number_type(number: Union[int, float, Fraction]): - """ - Returns the type of a number, if it's a integer, a float, fraction - It accepts tuple, lists and so on such: - [int, int, int] -> int - [int, Fraction, int] -> Fraction - [int, int, float] -> float - [Fraction, float, int] -> float - """ - try: - iter(number) - tipos = [] - for numb in number: - tipo = number_type(numb) - if tipo is float: - return float - tipos.append(tipo) - for tipo in tipos: - if tipo is Fraction: - return Fraction - return int - except TypeError: - if isinstance(number, (int, np.integer)): - return int - if isinstance(number, Fraction): - return Fraction - return float - - def find_roots( knotvector: ImmutableKnotVector, ctrlvalues: Tuple[float] ) -> Tuple[float]: @@ -414,30 +337,6 @@ def find_roots( return tuple(sorted(filtered_roots)) -def totuple(array): - """ - Convert recursively an array to tuples - """ - try: - return tuple(map(tuple, array)) - except TypeError: # Cannot iterate - return tuple(array) - - -def binom(n: int, i: int): - """ - Returns binomial (n, i) - """ - assert isinstance(n, int) - assert isinstance(i, int) - prod = 1 - if i <= 0 or i >= n: - return 1 - for j in range(i): - prod *= (n - j) / (i - j) - return int(prod) - - def eval_spline_nodes( knotvector: ImmutableKnotVector, nodes: Tuple[float], degree: int ) -> Tuple[Tuple[float]]: @@ -513,118 +412,6 @@ def eval_rational_nodes( return totuple(rationalvals) -class Linalg: - @staticmethod - def solve(matrix: Tuple[Tuple[float]], force: Tuple[Tuple[float]]): - numbtype = number_type((matrix, force)) - if numbtype not in (int, Fraction): - matrix = np.array(matrix, dtype="float64") - force = np.array(force, dtype="float64") - return totuple(np.linalg.solve(matrix, force)) - matrix = [[deepcopy(elem) for elem in line] for line in matrix] - inverse = Linalg.invert(matrix) - result = np.dot(inverse, force) - if numbtype is int: - all_int = True - for i, line in enumerate(result): - for j, elem in enumerate(line): - if elem.denominator == 1: - result[i, j] = int(elem) - else: - all_int = False - result = result.astype("int64") if all_int else result - return totuple(result) - - @staticmethod - def invert(matrix: Tuple[Tuple[float]]): - numbtype = number_type(matrix) - if numbtype not in (int, Fraction): - matrix = np.array(matrix, dtype="float64") - return totuple(np.linalg.inv(matrix)) - matrix = [[deepcopy(elem) for elem in line] for line in matrix] - denomins = [1] * len(matrix) - for i, line in enumerate(matrix): - lcm = Math.lcm(*[Fraction(elem).denominator for elem in line]) - denomins[i] *= lcm - for j, elem in enumerate(line): - line[j] = lcm * elem - matrix = tuple(tuple(int(elem) for elem in line) for line in matrix) - diagonal, inverse = Linalg.invert_integer_matrix(matrix) - inverse = np.array(inverse, dtype="object") - for i, diag in enumerate(diagonal): - for j, denom in enumerate(denomins): - inverse[i, j] = Fraction(denom * inverse[i, j], diag) - return inverse - - @staticmethod - def lstsq(matrix: Tuple[Tuple[float]]): - """ - Given a matrix A of shape (n, m), with n >= m - We want the best solution X for - [A] * [X] approx [B] - To do it, we first transform into a square matrix and solve: - [A]^T * [A] * [X] = [A]^T * [B] - This function in fact returns the matrix [M] such - [X] = [M] * [B] - [M] = (A^T * A)^{-1} * A^T - """ - matrix = np.array(matrix) - assert matrix.shape[0] >= matrix.shape[1] - if matrix.shape[0] == matrix.shape[1]: - ident = totuple(np.eye(len(matrix), dtype="object")) - return Linalg.solve(matrix, ident) - return Linalg.solve(matrix.T @ matrix, matrix.T) - - def invert_integer_matrix( - matrix: Tuple[Tuple[int]], - ) -> Tuple[Tuple[int], Tuple[Tuple[int]]]: - """ - Given a matrix A with integer entries, this function computes the - inverse of this matrix by gaussian elimination. - - # Input: - matrix: Tuple[Tuple[int]] - Square matrix A of size (m, m) of integer values - - # Output: - diagonal: Tuple[int] - The final diagonal D after gaussian elimination, with values d_i - inverse: Tuple[Tuple[int]] - The final inversed matrix M = diag(D) * A^{-1} - """ - side = len(matrix) - inverse = np.eye(side, dtype="object") - matrix = np.column_stack((matrix, inverse)) - - # Eliminate lower triangle - for k in range(side): - # Swap pivos - if matrix[k, k] == 0: - for i in range(k + 1, side): - if matrix[i, k] != 0: - matrix[[k, i]] = matrix[[i, k]] - break - # Eliminate lines bellow - if matrix[k, k] < 0: - matrix[k] *= -1 - for i in range(k + 1, side): - matrix[i] = matrix[i] * matrix[k, k] - matrix[k] * matrix[i, k] - gdcline = Math.gcd(*matrix[i]) - if gdcline != 1: - matrix[i] = matrix[i] // gdcline - - # Eliminate upper triangle - for k in range(side - 1, 0, -1): - for i in range(k - 1, -1, -1): - matrix[i] = matrix[i] * matrix[k, k] - matrix[k] * matrix[i, k] - gdcline = Math.gcd(*matrix[i]) - if gdcline != 1: - matrix[i] = matrix[i] // gdcline - diagonal = list(np.diag(matrix[:, :side])) - inverse = matrix[:, side:] - return totuple(diagonal), totuple(inverse) - - class LeastSquare: """ Given two hypotetic curves C0 and C1, which are associated @@ -1391,292 +1178,3 @@ def derivate_rational_bezier( matrixleft = np.tensordot(np.transpose(matrixderi), matrixmult, axes=1) matrixrigh = matrixmult @ matrixderi return totuple(matrixleft - matrixrigh), totuple(matrixmult) - - -class NodeSample: - __cheby = {1: (Fraction(1, 2),)} - __gauss = {1: (Fraction(1, 2),)} - - @staticmethod - def closed_linspace(npts: int, cls: Optional[type] = Fraction) -> Tuple[float]: - """Returns equally distributed nodes in [0, 1] - Include the extremities - - Example - ------------ - >>> NodeSample.closed_linspace(2) - (0, 1) - >>> NodeSample.closed_linspace(3) - (0, 1/2, 1) - >>> NodeSample.closed_linspace(4) - (0, 1/3, 2/3, 1) - >>> NodeSample.closed_linspace(5) - (0, 1/4, 2/4, 3/4, 1) - >>> NodeSample.closed_linspace(6) - (0, 1/5, 2/5, 3/5, 4/5, 1) - """ - assert isinstance(npts, int) - assert npts > 1 - nums = tuple(range(0, npts)) - nums = tuple(cls(num) / (npts - 1) for num in nums) - return nums - - @staticmethod - def open_linspace(npts: int, cls: Optional[type] = Fraction) -> Tuple[float]: - """Returns equally distributed nodes in (0, 1) - Exclude the extremities - - Example - ------------ - >>> NodeSample.open_linspace(1) - (1/2, ) - >>> NodeSample.open_linspace(2) - (1/4, 3/4) - >>> NodeSample.open_linspace(3) - (1/6, 3/6, 5/6) - >>> NodeSample.open_linspace(4) - (1/8, 3/8, 5/8, 7/8) - >>> NodeSample.open_linspace(5) - (1/10, 3/10, 5/10, 7/10, 9/10) - """ - assert isinstance(npts, int) - assert npts > 0 - nums = range(1, 2 * npts, 2) - nums = tuple(cls(num) / (2 * npts) for num in nums) - return nums - - @staticmethod - def chebyshev(npts: int) -> Tuple[float]: - """ - Returns chebyshev nodes in the space [0, 1] - `Chebyshev nodes `_ - - - >>> NodeSample.chebyshev(1) - (0.5,) - >>> NodeSample.chebyshev(2) - (0.146, 0.854) - >>> NodeSample.chebyshev(3) - (0.067, 0.5, 0.933) - >>> NodeSample.chebyshev(4) - (0.038, 0.309, 0.691, 0.962) - >>> NodeSample.chebyshev(5) - (0.024, 0.206, 0.5, 0.794, 0.976) - """ - assert isinstance(npts, int) - assert npts > 0 - if npts not in NodeSample.__cheby: - nums = NodeSample.open_linspace(npts) - nums = tuple(math.sin(0.5 * math.pi * num) ** 2 for num in nums) - NodeSample.__cheby[npts] = nums - return NodeSample.__cheby[npts] - - @staticmethod - def gauss_legendre(npts: int) -> Tuple[float]: - """ - Returns gauss legendre quadrature nodes in the space [0, 1] - `Gauss-Legendre quadrature `_ - - >>> NodeSample.gauss_legendre(1) - (0.5,) - >>> NodeSample.gauss_legendre(2) - (0.146, 0.854) - >>> NodeSample.gauss_legendre(3) - (0.067, 0.5, 0.933) - >>> NodeSample.gauss_legendre(4) - (0.038, 0.309, 0.691, 0.962) - >>> NodeSample.gauss_legendre(5) - (0.024, 0.206, 0.5, 0.794, 0.976) - """ - assert isinstance(npts, int) - assert npts > 0 - if npts not in NodeSample.__gauss: - nums, _ = np.polynomial.legendre.leggauss(npts) - nums = (1 + nums) / 2 - NodeSample.__gauss[npts] = tuple(nums) - return NodeSample.__gauss[npts] - - -class IntegratorArray: - __closed_newton = { - 2: (Fraction(1, 2), Fraction(1, 2)), - 3: (Fraction(1, 6), Fraction(2, 3), Fraction(1, 6)), - 4: (Fraction(1, 8), Fraction(3, 8), Fraction(3, 8), Fraction(1, 8)), - } - __open_newton = { - 1: (Fraction(1),), - 2: (Fraction(1, 2), Fraction(1, 2)), - 3: (Fraction(3, 8), Fraction(1, 4), Fraction(3, 8)), - } - __cheby = { - 1: (Fraction(1),), - 2: (Fraction(1, 2), Fraction(1, 2)), - 3: (Fraction(2, 9), Fraction(5, 9), Fraction(2, 9)), - } - __gauss = { - 1: (Fraction(1),), - 2: (Fraction(1, 2), Fraction(1, 2)), - 3: (Fraction(5, 18), Fraction(4, 9), Fraction(5, 18)), - } - - @staticmethod - def interpolate_bezier(nodes: Tuple[float]) -> Tuple[Tuple[float]]: - """Returns a matrix that interpolates a function at given nodes using bezier - - This function returns the inverse of matrix [M] which - interpolates a bezier curve C at the given nodes - C(u) = sum_{i=0}^{p} B_{i,p}(u) * P_{i} - B_{i,p}(u) = binom(p, i) * (1-u)^{p-i} * u^i - [M]_{i,k} = B_{i,p}(u_k) - [M] * [P] = [f(x_k)] - - Example - ------------ - >>> nodes = (0, 0.2, 1) - >>> IntegratorArray.interpolate_bezier(nodes) - ((1, -2, 0), (0, 25/8, 0), (0, -1/8, 1)) - >>> nodes = (0, 0.5, 1) - >>> IntegratorArray.interpolate_bezier(nodes) - ((1, -1/2, 0), (0, 2, 0), (0, -1/2, 1)) - - """ - assert isinstance(nodes, tuple) - for node in nodes: - float(node) - assert 0 <= node - assert node <= 1 - degree = len(nodes) - 1 - matrix_bezier = np.zeros((degree + 1, degree + 1), dtype="object") - for k, uk in enumerate(nodes): - for i in range(degree + 1): - matrix_bezier[i, k] = ( - Math.comb(degree, i) * (1 - uk) ** (degree - i) * (uk**i) - ) - matrix_bezier = totuple(matrix_bezier) - inverse = Linalg.invert(matrix_bezier) - inverse = tuple(map(tuple, inverse)) - return inverse - - @staticmethod - def bezier_integrator_array(nodes: Tuple[float]) -> Tuple[float]: - """Computes the weights to integrate at given nodes - - Given ``nodes`` the positions of ``n`` values of ``x_i``, - this function returns ``n`` values of ``w_i`` such - - int_{0}^{1} f(u) du = sum_{i=0}^{n-1} w_i * f(x_i) - - Example - ------------ - >>> nodes = (0, 0.2, 1) - >>> IntegratorArray.bezier_integrator_array(nodes) - (1/3, 1/3, 1/3) - >>> nodes = (0, 0.5, 1) - >>> IntegratorArray.bezier_integrator_array(nodes) - (1/3, 1/3, 1/3) - """ - matrix = IntegratorArray.interpolate_bezier(nodes) - array = [sum(line) / len(nodes) for line in matrix] - return tuple(array) - - @staticmethod - def closed_newton_cotes(npts: int) -> Tuple[Tuple[float]]: - """Returns the weight array for closed newton-cotes formula - in the interval [0, 1] - - Example - ------------ - >>> IntegratorArray.closed_newton_cotes(2) - (1/2, 1/2) - >>> IntegratorArray.closed_newton_cotes(3) - (1/6, 4/6, 1/6) - >>> IntegratorArray.closed_newton_cotes(4) - (1/8, 3/8, 3/8, 1/8) - >>> IntegratorArray.closed_newton_cotes(5) - (7/90, 16/45, 2/15, 16/45, 7/90) - """ - assert isinstance(npts, int) - assert npts > 1 - if npts not in IntegratorArray.__closed_newton: - nodes = NodeSample.closed_linspace(npts, Fraction) - weights = IntegratorArray.bezier_integrator_array(nodes) - IntegratorArray.__closed_newton[npts] = weights - return IntegratorArray.__closed_newton[npts] - - @staticmethod - def open_newton_cotes(npts: int) -> Tuple[Tuple[float]]: - """Returns the weight array for open newton-cotes formula - in the interval (0, 1) - - Example - ------------ - >>> IntegratorArray.open_newton_cotes(1) - (1, ) - >>> IntegratorArray.open_newton_cotes(2) - (1/2, 1/2) - >>> IntegratorArray.open_newton_cotes(3) - (3/8, 1/4, 3/8) - >>> IntegratorArray.open_newton_cotes(4) - (13/48, 11/48, 11/48, 13/48) - >>> IntegratorArray.open_newton_cotes(5) - (275/1152, 25/288, 67/192, 25/288, 275/1152) - - """ - assert isinstance(npts, int) - assert npts > 0 - if npts not in IntegratorArray.__open_newton: - nodes = NodeSample.open_linspace(npts, Fraction) - weights = IntegratorArray.bezier_integrator_array(nodes) - IntegratorArray.__open_newton[npts] = weights - return IntegratorArray.__open_newton[npts] - - @staticmethod - def chebyshev(npts: int) -> Tuple[float]: - """Returns the weight array for integrate at chebyshev nodes - - Example - ------------ - >>> IntegratorArray.chebyshev(1) - (1, ) - >>> IntegratorArray.chebyshev(2) - (1/2, 1/2) - >>> IntegratorArray.chebyshev(3) - (3/8, 1/4, 3/8) - >>> IntegratorArray.chebyshev(4) - (13/48, 11/48, 11/48, 13/48) - >>> IntegratorArray.chebyshev(5) - (275/1152, 25/288, 67/192, 25/288, 275/1152) - - """ - assert isinstance(npts, int) - assert 0 < npts - if npts not in IntegratorArray.__cheby: - nodes = NodeSample.chebyshev(npts) - weights = IntegratorArray.bezier_integrator_array(nodes) - IntegratorArray.__cheby[npts] = weights - return IntegratorArray.__cheby[npts] - - @staticmethod - def gauss_legendre(npts: int) -> Tuple[float]: - """Returns the weight array for integrate at gauss nodes - - Example - ------------ - >>> IntegratorArray.chebyshev(1) - (1, ) - >>> IntegratorArray.chebyshev(2) - (1/2, 1/2) - >>> IntegratorArray.chebyshev(3) - (3/8, 1/4, 3/8) - >>> IntegratorArray.chebyshev(4) - (13/48, 11/48, 11/48, 13/48) - >>> IntegratorArray.chebyshev(5) - (275/1152, 25/288, 67/192, 25/288, 275/1152) - - """ - assert isinstance(npts, int) - assert 0 < npts - if npts not in IntegratorArray.__gauss: - _, weights = np.polynomial.legendre.leggauss(npts) - IntegratorArray.__gauss[npts] = tuple(weights / 2) - return IntegratorArray.__gauss[npts] diff --git a/tests/test_cmath.py b/tests/test_cmath.py new file mode 100644 index 0000000..c21fc19 --- /dev/null +++ b/tests/test_cmath.py @@ -0,0 +1,787 @@ +import math +from fractions import Fraction + +import numpy as np +import pytest + +from pynurbs.cmath import IntegratorArray, Linalg, Math, NodeSample + + +@pytest.mark.order(1) +@pytest.mark.dependency() +def test_begin(): + pass + + +class TestMath: + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["test_begin"]) + def test_begin(self): + pass + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestMath::test_begin"]) + def test_gcd(self): + assert Math.gcd(0) == 0 + assert Math.gcd(1) == 1 + assert Math.gcd(*[0] * 5) == 0 + assert Math.gcd(*[1] * 5) == 1 + assert Math.gcd(0, 1) == 1 + assert Math.gcd(0, 2) == 2 + assert Math.gcd(1, 1000) == 1 + assert Math.gcd(10, 1000) == 10 + assert Math.gcd(100, 1000) == 100 + assert Math.gcd(1000, 1000) == 1000 + assert Math.gcd(2, 3, 4) == 1 + assert Math.gcd(6, 9, 12) == 3 + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestMath::test_begin", "TestMath::test_gcd"]) + def test_lcm(self): + assert Math.lcm(0) == 0 + assert Math.lcm(1) == 1 + assert Math.lcm(*[0] * 5) == 0 + assert Math.lcm(*[1] * 5) == 1 + assert Math.lcm(0, 1) == 1 + assert Math.lcm(0, 2) == 2 + assert Math.lcm(1, 1000) == 1000 + assert Math.lcm(10, 1000) == 1000 + assert Math.lcm(100, 1000) == 1000 + assert Math.lcm(1000, 1000) == 1000 + assert Math.lcm(10000, 1000) == 10000 + assert Math.lcm(2, 3, 4) == 12 + assert Math.lcm(6, 9, 12) == 36 + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestMath::test_begin"]) + def test_comb(self): + assert Math.comb(1, 0) == 1 + assert Math.comb(1, 1) == 1 + assert Math.comb(2, 0) == 1 + assert Math.comb(2, 1) == 2 + assert Math.comb(2, 2) == 1 + assert Math.comb(3, 0) == 1 + assert Math.comb(3, 1) == 3 + assert Math.comb(3, 2) == 3 + assert Math.comb(3, 3) == 1 + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "TestMath::test_begin", + "TestMath::test_gcd", + "TestMath::test_lcm", + "TestMath::test_comb", + ] + ) + def test_end(self): + pass + + +class TestLinalg: + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["test_begin", "TestMath::test_end"]) + def test_begin(self): + pass + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestLinalg::test_begin"]) + def test_invert_float(self): + identit = np.eye(4) + inverse = Linalg.invert(identit) + np.testing.assert_allclose(inverse, identit) + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=["TestLinalg::test_begin", "TestLinalg::test_invert_float"] + ) + def test_invert_integer(self): + matrix = ((1, 0), (0, 1)) + good = ((1, 0), (0, 1)) + test = Linalg.invert(matrix) + test = np.array(test, dtype="int16") + np.testing.assert_allclose(test, good) + + matrix = ((1, 1), (2, 3)) + good = ((3, -1), (-2, 1)) + test = Linalg.invert(matrix) + test = np.array(test, dtype="int16") + np.testing.assert_allclose(test, good) + + matrix = ((1, 1), (11, 12)) + good = ((12, -1), (-11, 1)) + test = Linalg.invert(matrix) + test = np.array(test, dtype="int16") + np.testing.assert_allclose(test, good) + + matrix = ( + (1, 1, 1, 1), + (11, 12, 13, 14), + (55, 66, 78, 91), + (165, 220, 286, 364), + ) + good = ( + (364, -78, 12, -1), + (-1001, 221, -35, 3), + (924, -209, 34, -3), + (-286, 66, -11, 1), + ) + test = Linalg.invert(matrix) + test = np.array(test, dtype="int64") + np.testing.assert_allclose(test, good) + + # Ericksen matrix + for side in range(1, 10): + matrix = np.zeros((side, side), dtype="int64") + for n in range(side, side + 10): + for i in range(side): + for j in range(side): + matrix[i, j] = Math.comb(n + j, i) + inverse = Linalg.invert(matrix) + inverse = np.array(inverse, dtype="int64") + np.testing.assert_allclose(np.dot(inverse, matrix), np.eye(side)) + np.testing.assert_allclose(np.dot(matrix, inverse), np.eye(side)) + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "TestLinalg::test_begin", + "TestLinalg::test_invert_float", + "TestLinalg::test_invert_integer", + ] + ) + def test_invert_fraction(self): + # Identity + for side in range(1, 10): + zero, one = Fraction(0), Fraction(1) + matrix = [ + [one if i == j else zero for j in range(side)] for i in range(side) + ] + test = Linalg.invert(matrix) + test = np.array(test, dtype="int64") + good = np.eye(side, dtype="int64") + np.testing.assert_allclose(test, good) + + # Ericksen matrix + for side in range(1, 10): + matrix = np.zeros((side, side), dtype="object") + for n in range(side, side + 10): + for i in range(side): + for j in range(side): + matrix[i, j] = Fraction(Math.comb(n + j, i)) + inverse = Linalg.invert(matrix) + inverse = np.array(inverse, dtype="int64") + matrix = np.array(matrix, dtype="int64") + np.testing.assert_allclose(np.dot(inverse, matrix), np.eye(side)) + np.testing.assert_allclose(np.dot(matrix, inverse), np.eye(side)) + + matrix = [[3, 4], [1, 2]] + inverse = Linalg.invert(matrix) + testident = np.dot(inverse, matrix) + testident = testident.astype("float64") + np.testing.assert_allclose(testident, np.eye(len(matrix))) + + matrix = [[1, 2], [2, 3]] + inverse = Linalg.invert(matrix) + testident = np.dot(inverse, matrix) + testident = testident.astype("float64") + np.testing.assert_allclose(testident, np.eye(len(matrix))) + + frac = Fraction + matrix = [[frac(1), frac(-2)], [frac(-1, 2), frac(3, 2)]] + inverse = Linalg.invert(matrix) + testident = np.dot(inverse, matrix) + testident = testident.astype("float64") + np.testing.assert_allclose(testident, np.eye(len(matrix))) + + size = 3 + while True: + floatmatrix = np.random.uniform(-1, 1, (size, size)) + fracmatrix = np.empty((size, size), dtype="object") + for i, line in enumerate(floatmatrix): + for j, elem in enumerate(line): + fracmatrix[i, j] = Fraction(elem).limit_denominator(10) + fracmatrix += np.transpose(fracmatrix) + if abs(np.linalg.det(np.array(fracmatrix, dtype="float64"))) > 1e-6: + break + invfracmatrix = Linalg.invert(fracmatrix) + product = fracmatrix @ invfracmatrix + product = np.array(product, dtype="float64") + np.testing.assert_allclose(product, np.eye(size)) + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=["TestLinalg::test_begin", "TestLinalg::test_invert_fraction"] + ) + def test_solve_float(self): + side, nsols = 4, 4 + matrix = np.eye(side) + force = np.random.rand(side, nsols) + solution = Linalg.solve(matrix, force) + np.testing.assert_allclose(np.dot(matrix, solution), force) + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=["TestLinalg::test_begin", "TestLinalg::test_solve_float"] + ) + def test_solve_integer(self): + side, nsols = 2, 4 + force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] + matrix = ((1, 0), (0, 1)) + solution = Linalg.solve(matrix, force) + mult = np.dot(matrix, solution) + np.testing.assert_allclose(mult, force) + + side, nsols = 2, 4 + force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] + matrix = ((1, 1), (2, 3)) + solution = Linalg.solve(matrix, force) + mult = np.dot(matrix, solution) + np.testing.assert_allclose(mult, force) + + side, nsols = 2, 4 + force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] + matrix = ((1, 1), (11, 12)) + solution = Linalg.solve(matrix, force) + mult = np.dot(matrix, solution) + np.testing.assert_allclose(mult, force) + + side, nsols = 4, 4 + force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] + matrix = ( + (1, 1, 1, 1), + (11, 12, 13, 14), + (55, 66, 78, 91), + (165, 220, 286, 364), + ) + solution = Linalg.solve(matrix, force) + mult = np.dot(matrix, solution) + np.testing.assert_allclose(mult, force) + + # Ericksen matrix + for side in range(1, 10): + nsols = side + 1 + force = np.random.randint(-10, 10, (side, nsols)) + matrix = np.zeros((side, side), dtype="int64") + for n in range(side, side + 10): + for i in range(side): + for j in range(side): + matrix[i, j] = Math.comb(n + j, i) + solution = Linalg.solve(matrix, force) + mult = np.dot(matrix, solution) + np.testing.assert_allclose(mult, force) + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "TestLinalg::test_begin", + "TestLinalg::test_solve_float", + "TestLinalg::test_solve_integer", + ] + ) + def test_solve_fraction(self): + # Identity + for side in range(1, 10): + zero, one = Fraction(0), Fraction(1) + matrix = [ + [one if i == j else zero for j in range(side)] for i in range(side) + ] + test = Linalg.invert(matrix) + test = np.array(test, dtype="int64") + good = np.eye(side, dtype="int64") + np.testing.assert_allclose(test, good) + + # Ericksen matrix + for side in range(1, 10): + matrix = np.zeros((side, side), dtype="object") + for n in range(side, side + 10): + for i in range(side): + for j in range(side): + matrix[i, j] = Fraction(Math.comb(n + j, i)) + inverse = Linalg.invert(matrix) + inverse = np.array(inverse, dtype="int64") + matrix = np.array(matrix, dtype="int64") + np.testing.assert_allclose(np.dot(inverse, matrix), np.eye(side)) + np.testing.assert_allclose(np.dot(matrix, inverse), np.eye(side)) + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "TestLinalg::test_begin", + "TestLinalg::test_solve_float", + "TestLinalg::test_solve_fraction", + ] + ) + def test_specific_case(self): + f = Fraction + B = [ + [f(1, 9), f(1, 12), f(5, 84), f(5, 126), f(1, 42), f(1, 84), f(17, 4235)], + [ + f(1, 36), + f(1, 21), + f(5, 84), + f(4, 63), + f(5, 84), + f(204, 4235), + f(653, 21780), + ], + [ + f(1, 252), + f(1, 84), + f(1, 42), + f(5, 126), + f(51, 847), + f(653, 7260), + f(493, 4356), + ], + ] + C = [ + [f(1, 5), f(1, 10), f(727, 21780)], + [f(1, 10), f(727, 5445), f(1117, 10890)], + [f(727, 21780), f(1117, 10890), f(1501, 7260)], + ] + B = np.array(B) + C = np.array(C) + + Cinv = Linalg.invert(C) + solution = np.dot(Cinv, B) + mult = np.dot(C, solution) + diff = np.array(mult - B, dtype="float64") + np.testing.assert_allclose(diff, np.zeros(B.shape)) + + solution = Linalg.solve(C, B) + mult = np.dot(C, solution) + diff = np.array(mult - B, dtype="float64") + np.testing.assert_allclose(diff, np.zeros(B.shape)) + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "TestLinalg::test_begin", + "TestLinalg::test_solve_float", + "TestLinalg::test_solve_fraction", + ] + ) + def test_specific_case2(self): + f = Fraction + matrix = [[0, -54, -5], [-9, -20, -3], [-25, -90, 216]] + good = [ + [f(2295, 55288), f(-6057, 55288), f(-31, 55288)], + [f(-2019, 110576), f(125, 110576), f(-45, 110576)], + [f(-155, 55288), f(-675, 55288), f(243, 55288)], + ] + prod = np.dot(matrix, np.array(good, dtype="float64")) + np.testing.assert_allclose(prod, np.eye(3), atol=1e-9) + test = Linalg.invert(matrix) + diff = np.array(good - test, dtype="float64") + np.testing.assert_array_equal(diff, np.zeros(diff.shape)) + + matrix = [ + [f(0, 1), f(-3, 4), f(-5, 72)], + [f(-3, 4), f(-5, 3), f(-1, 4)], + [f(-5, 72), f(-1, 4), f(3, 5)], + ] + inverse = Linalg.invert(matrix) + prod = np.dot(matrix, inverse).astype("float64") + np.testing.assert_allclose(prod, np.eye(3)) + prod = np.dot(inverse, matrix).astype("float64") + np.testing.assert_allclose(prod, np.eye(3)) + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "TestLinalg::test_begin", + "TestLinalg::test_invert_float", + "TestLinalg::test_invert_integer", + "TestLinalg::test_invert_fraction", + "TestLinalg::test_solve_float", + "TestLinalg::test_solve_integer", + "TestLinalg::test_solve_fraction", + "TestLinalg::test_specific_case", + ] + ) + def test_end(self): + pass + + +class TestNodeSample: + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=["test_begin", "TestMath::test_end", "TestLinalg::test_end"] + ) + def test_begin(self): + pass + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) + def test_closed_linspace(self): + nodes = NodeSample.closed_linspace(2) + good = (0, 1) + assert nodes == good + + nodes = NodeSample.closed_linspace(3) + good = (0, 1 / 2, 1) + assert nodes == good + + nodes = NodeSample.closed_linspace(4) + nodes = np.array(nodes, dtype="float64") + good = (0, 1 / 3, 2 / 3, 1) + np.testing.assert_allclose(nodes, good) + + nodes = NodeSample.closed_linspace(5) + good = (0, 1 / 4, 2 / 4, 3 / 4, 1) + assert nodes == good + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) + def test_open_linspace(self): + nodes = NodeSample.open_linspace(1) + good = (1 / 2,) + assert nodes == good + + nodes = NodeSample.open_linspace(2) + good = (1 / 4, 3 / 4) + assert nodes == good + + nodes = NodeSample.open_linspace(3) + nodes = np.array(nodes, dtype="float64") + good = (1 / 6, 3 / 6, 5 / 6) + np.testing.assert_allclose(nodes, good) + + nodes = NodeSample.open_linspace(4) + good = (1 / 8, 3 / 8, 5 / 8, 7 / 8) + assert nodes == good + + nodes = NodeSample.open_linspace(5) + nodes = np.array(nodes, dtype="float64") + good = (1 / 10, 3 / 10, 5 / 10, 7 / 10, 9 / 10) + np.testing.assert_allclose(nodes, good) + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) + def test_chebyshev(self): + nodes = NodeSample.chebyshev(1) + assert nodes == (1 / 2,) + + nodes = NodeSample.chebyshev(2) + left = (2 - np.sqrt(2)) / 4 + right = (2 + np.sqrt(2)) / 4 + good = (left, right) + np.testing.assert_allclose(nodes, good) + + nodes = NodeSample.chebyshev(3) + left = (2 - np.sqrt(3)) / 4 + right = (2 + np.sqrt(3)) / 4 + good = (left, 1 / 2, right) + np.testing.assert_allclose(nodes, good) + + nodes = NodeSample.chebyshev(4) + good = np.sin(np.pi * np.array([1 / 16, 3 / 16, 5 / 16, 7 / 16])) ** 2 + np.testing.assert_allclose(nodes, good) + + nodes = NodeSample.chebyshev(5) + good = np.sin(np.pi * np.array([1 / 20, 3 / 20, 5 / 20, 7 / 20, 9 / 20])) ** 2 + np.testing.assert_allclose(nodes, good) + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) + def test_gauss_legendre(self): + nodes = NodeSample.gauss_legendre(1) + assert nodes == (1 / 2,) + + nodes = NodeSample.gauss_legendre(2) + minor = 1 / np.sqrt(3) + good = [(1 - minor) / 2, (1 + minor) / 2] + np.testing.assert_allclose(nodes, good) + + nodes = NodeSample.gauss_legendre(3) + minor = np.sqrt(3 / 5) + good = [(1 - minor) / 2, 1 / 2, (1 + minor) / 2] + np.testing.assert_allclose(nodes, good) + + nodes = NodeSample.gauss_legendre(4) + minor = np.sqrt(3 / 7 + 2 * np.sqrt(6 / 5) / 7) + middl = np.sqrt(3 / 7 - 2 * np.sqrt(6 / 5) / 7) + good = [(1 - minor) / 2, (1 - middl) / 2, (1 + middl) / 2, (1 + minor) / 2] + np.testing.assert_allclose(nodes, good) + + nodes = NodeSample.gauss_legendre(5) + minor = np.sqrt(5 + 2 * np.sqrt(10 / 7)) / 3 + middl = np.sqrt(5 - 2 * np.sqrt(10 / 7)) / 3 + good = [ + (1 - minor) / 2, + (1 - middl) / 2, + 1 / 2, + (1 + middl) / 2, + (1 + minor) / 2, + ] + np.testing.assert_allclose(nodes, good) + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "TestNodeSample::test_begin", + "TestNodeSample::test_closed_linspace", + "TestNodeSample::test_open_linspace", + "TestNodeSample::test_chebyshev", + "TestNodeSample::test_gauss_legendre", + ] + ) + def test_end(self): + pass + + +class TestUnidimentionIntegral: + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "test_begin", + "TestMath::test_end", + "TestLinalg::test_end", + "TestNodeSample::test_end", + ] + ) + def test_begin(self): + pass + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) + def test_closed_newton_cotes(self): + a, b = Fraction(3), Fraction(5) + for degree in range(0, 7): + npts = max(2, degree + 1) # Number integration points + numers = np.random.randint(-5, 5, degree + 1) + denoms = np.random.randint(2, 8, degree + 1) + coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] + good = sum( + ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) + for i, ci in enumerate(coefs) + ) + + nodes = NodeSample.closed_linspace(npts) + weights = IntegratorArray.closed_newton_cotes(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + + assert test == good + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) + def test_open_newton_cotes(self): + a, b = Fraction(3), Fraction(5) + for degree in range(0, 7): + npts = degree + 1 # Number integration points + numers = np.random.randint(-5, 5, degree + 1) + denoms = np.random.randint(2, 8, degree + 1) + coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] + good = sum( + ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) + for i, ci in enumerate(coefs) + ) + + nodes = NodeSample.open_linspace(npts) + weights = IntegratorArray.open_newton_cotes(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + + assert test == good + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) + def test_chebyshev(self): + a, b = Fraction(3), Fraction(7) + for degree in range(0, 7): + npts = degree + 1 # Number integration points + numers = np.random.randint(-5, 5, degree + 1) + denoms = np.random.randint(2, 8, degree + 1) + coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] + good = sum( + ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) + for i, ci in enumerate(coefs) + ) + + nodes = NodeSample.chebyshev(npts) + weights = IntegratorArray.chebyshev(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + + assert abs(test - good) < 1e-9 + + @pytest.mark.order(1) + @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) + def test_gauss_legendre(self): + a, b = Fraction(3), Fraction(7) + for degree in range(0, 7): + npts = degree + 1 # Number integration points + numers = np.random.randint(-5, 5, degree + 1) + denoms = np.random.randint(2, 8, degree + 1) + coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] + good = sum( + ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) + for i, ci in enumerate(coefs) + ) + + nodes = NodeSample.gauss_legendre(npts) + weights = IntegratorArray.gauss_legendre(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + + assert abs(test - good) < 1e-9 + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "TestUnidimentionIntegral::test_begin", + "TestUnidimentionIntegral::test_closed_newton_cotes", + "TestUnidimentionIntegral::test_open_newton_cotes", + "TestUnidimentionIntegral::test_chebyshev", + "TestUnidimentionIntegral::test_gauss_legendre", + ] + ) + def test_exact_integral_fraction(self): + a, b = Fraction(3), Fraction(7) + for degree in range(0, 7): + npts = 1 + 2 * math.floor(degree / 2) # Number integration points + numers = np.random.randint(-5, 5, degree + 1) + denoms = np.random.randint(2, 8, degree + 1) + coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] + good = sum( + ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) + for i, ci in enumerate(coefs) + ) + + if npts > 1: + nodes = NodeSample.open_linspace(npts) + weights = IntegratorArray.open_newton_cotes(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + assert test == good + + nodes = NodeSample.open_linspace(npts) + weights = IntegratorArray.open_newton_cotes(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + assert test == good + + nodes = NodeSample.chebyshev(npts) + weights = IntegratorArray.chebyshev(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + assert abs(test - good) < 1e-9 + + nodes = NodeSample.gauss_legendre(npts) + weights = IntegratorArray.gauss_legendre(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + assert abs(test - good) < 1e-9 + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "TestUnidimentionIntegral::test_begin", + "TestUnidimentionIntegral::test_closed_newton_cotes", + "TestUnidimentionIntegral::test_open_newton_cotes", + "TestUnidimentionIntegral::test_chebyshev", + "TestUnidimentionIntegral::test_gauss_legendre", + "TestUnidimentionIntegral::test_exact_integral_fraction", + ] + ) + def test_approx_integral(self): + a, b = Fraction(3), Fraction(7) + for degree in range(0, 7): + npts = 1 + 2 * math.floor(degree / 2) # Number integration points + coefs = np.random.uniform(-5, 6, degree + 1) + good = sum( + ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) + for i, ci in enumerate(coefs) + ) + + if npts > 1: + nodes = NodeSample.open_linspace(npts) + weights = IntegratorArray.open_newton_cotes(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + assert abs(test - good) < 1e-9 + + nodes = NodeSample.open_linspace(npts) + weights = IntegratorArray.open_newton_cotes(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + assert abs(test - good) < 1e-9 + + nodes = NodeSample.chebyshev(npts) + weights = IntegratorArray.chebyshev(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + assert abs(test - good) < 1e-9 + + nodes = NodeSample.gauss_legendre(npts) + weights = IntegratorArray.gauss_legendre(npts) + nodes = tuple(a + (b - a) * node for node in nodes) + funcvals = tuple( + sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + ) + test = (b - a) * np.inner(weights, funcvals) + assert abs(test - good) < 1e-9 + + @pytest.mark.order(1) + @pytest.mark.dependency( + depends=[ + "TestUnidimentionIntegral::test_begin", + "TestUnidimentionIntegral::test_closed_newton_cotes", + "TestUnidimentionIntegral::test_open_newton_cotes", + "TestUnidimentionIntegral::test_chebyshev", + "TestUnidimentionIntegral::test_gauss_legendre", + "TestUnidimentionIntegral::test_exact_integral_fraction", + "TestUnidimentionIntegral::test_approx_integral", + ] + ) + def test_end(self): + pass + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=[ + "test_begin", + "TestMath::test_end", + "TestLinalg::test_end", + "TestNodeSample::test_end", + "TestUnidimentionIntegral::test_end", + ] +) +def test_end(): + pass diff --git a/tests/test_functions.py b/tests/test_functions.py index a550317..b07a9ff 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -4,7 +4,7 @@ import pytest from pynurbs import Function -from pynurbs.heavy import binom +from pynurbs.cmath import binom from pynurbs.knotspace import GeneratorKnotVector diff --git a/tests/test_heavy.py b/tests/test_heavy.py index d8f530c..8454d6a 100644 --- a/tests/test_heavy.py +++ b/tests/test_heavy.py @@ -1,787 +1,20 @@ -import math -from fractions import Fraction - import numpy as np import pytest -from pynurbs.heavy import IntegratorArray, LeastSquare, Linalg, Math, NodeSample +from pynurbs.heavy import LeastSquare @pytest.mark.order(1) -@pytest.mark.dependency() +@pytest.mark.dependency(depends=["tests/test_cmath.py::test_end"], scope="session") def test_begin(): pass -class TestMath: - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["test_begin"]) - def test_begin(self): - pass - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestMath::test_begin"]) - def test_gcd(self): - assert Math.gcd(0) == 0 - assert Math.gcd(1) == 1 - assert Math.gcd(*[0] * 5) == 0 - assert Math.gcd(*[1] * 5) == 1 - assert Math.gcd(0, 1) == 1 - assert Math.gcd(0, 2) == 2 - assert Math.gcd(1, 1000) == 1 - assert Math.gcd(10, 1000) == 10 - assert Math.gcd(100, 1000) == 100 - assert Math.gcd(1000, 1000) == 1000 - assert Math.gcd(2, 3, 4) == 1 - assert Math.gcd(6, 9, 12) == 3 - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestMath::test_begin", "TestMath::test_gcd"]) - def test_lcm(self): - assert Math.lcm(0) == 0 - assert Math.lcm(1) == 1 - assert Math.lcm(*[0] * 5) == 0 - assert Math.lcm(*[1] * 5) == 1 - assert Math.lcm(0, 1) == 1 - assert Math.lcm(0, 2) == 2 - assert Math.lcm(1, 1000) == 1000 - assert Math.lcm(10, 1000) == 1000 - assert Math.lcm(100, 1000) == 1000 - assert Math.lcm(1000, 1000) == 1000 - assert Math.lcm(10000, 1000) == 10000 - assert Math.lcm(2, 3, 4) == 12 - assert Math.lcm(6, 9, 12) == 36 - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestMath::test_begin"]) - def test_comb(self): - assert Math.comb(1, 0) == 1 - assert Math.comb(1, 1) == 1 - assert Math.comb(2, 0) == 1 - assert Math.comb(2, 1) == 2 - assert Math.comb(2, 2) == 1 - assert Math.comb(3, 0) == 1 - assert Math.comb(3, 1) == 3 - assert Math.comb(3, 2) == 3 - assert Math.comb(3, 3) == 1 - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestMath::test_begin", - "TestMath::test_gcd", - "TestMath::test_lcm", - "TestMath::test_comb", - ] - ) - def test_end(self): - pass - - -class TestLinalg: - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["test_begin", "TestMath::test_end"]) - def test_begin(self): - pass - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestLinalg::test_begin"]) - def test_invert_float(self): - identit = np.eye(4) - inverse = Linalg.invert(identit) - np.testing.assert_allclose(inverse, identit) - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=["TestLinalg::test_begin", "TestLinalg::test_invert_float"] - ) - def test_invert_integer(self): - matrix = ((1, 0), (0, 1)) - good = ((1, 0), (0, 1)) - test = Linalg.invert(matrix) - test = np.array(test, dtype="int16") - np.testing.assert_allclose(test, good) - - matrix = ((1, 1), (2, 3)) - good = ((3, -1), (-2, 1)) - test = Linalg.invert(matrix) - test = np.array(test, dtype="int16") - np.testing.assert_allclose(test, good) - - matrix = ((1, 1), (11, 12)) - good = ((12, -1), (-11, 1)) - test = Linalg.invert(matrix) - test = np.array(test, dtype="int16") - np.testing.assert_allclose(test, good) - - matrix = ( - (1, 1, 1, 1), - (11, 12, 13, 14), - (55, 66, 78, 91), - (165, 220, 286, 364), - ) - good = ( - (364, -78, 12, -1), - (-1001, 221, -35, 3), - (924, -209, 34, -3), - (-286, 66, -11, 1), - ) - test = Linalg.invert(matrix) - test = np.array(test, dtype="int64") - np.testing.assert_allclose(test, good) - - # Ericksen matrix - for side in range(1, 10): - matrix = np.zeros((side, side), dtype="int64") - for n in range(side, side + 10): - for i in range(side): - for j in range(side): - matrix[i, j] = Math.comb(n + j, i) - inverse = Linalg.invert(matrix) - inverse = np.array(inverse, dtype="int64") - np.testing.assert_allclose(np.dot(inverse, matrix), np.eye(side)) - np.testing.assert_allclose(np.dot(matrix, inverse), np.eye(side)) - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestLinalg::test_begin", - "TestLinalg::test_invert_float", - "TestLinalg::test_invert_integer", - ] - ) - def test_invert_fraction(self): - # Identity - for side in range(1, 10): - zero, one = Fraction(0), Fraction(1) - matrix = [ - [one if i == j else zero for j in range(side)] for i in range(side) - ] - test = Linalg.invert(matrix) - test = np.array(test, dtype="int64") - good = np.eye(side, dtype="int64") - np.testing.assert_allclose(test, good) - - # Ericksen matrix - for side in range(1, 10): - matrix = np.zeros((side, side), dtype="object") - for n in range(side, side + 10): - for i in range(side): - for j in range(side): - matrix[i, j] = Fraction(Math.comb(n + j, i)) - inverse = Linalg.invert(matrix) - inverse = np.array(inverse, dtype="int64") - matrix = np.array(matrix, dtype="int64") - np.testing.assert_allclose(np.dot(inverse, matrix), np.eye(side)) - np.testing.assert_allclose(np.dot(matrix, inverse), np.eye(side)) - - matrix = [[3, 4], [1, 2]] - inverse = Linalg.invert(matrix) - testident = np.dot(inverse, matrix) - testident = testident.astype("float64") - np.testing.assert_allclose(testident, np.eye(len(matrix))) - - matrix = [[1, 2], [2, 3]] - inverse = Linalg.invert(matrix) - testident = np.dot(inverse, matrix) - testident = testident.astype("float64") - np.testing.assert_allclose(testident, np.eye(len(matrix))) - - frac = Fraction - matrix = [[frac(1), frac(-2)], [frac(-1, 2), frac(3, 2)]] - inverse = Linalg.invert(matrix) - testident = np.dot(inverse, matrix) - testident = testident.astype("float64") - np.testing.assert_allclose(testident, np.eye(len(matrix))) - - size = 3 - while True: - floatmatrix = np.random.uniform(-1, 1, (size, size)) - fracmatrix = np.empty((size, size), dtype="object") - for i, line in enumerate(floatmatrix): - for j, elem in enumerate(line): - fracmatrix[i, j] = Fraction(elem).limit_denominator(10) - fracmatrix += np.transpose(fracmatrix) - if abs(np.linalg.det(np.array(fracmatrix, dtype="float64"))) > 1e-6: - break - invfracmatrix = Linalg.invert(fracmatrix) - product = fracmatrix @ invfracmatrix - product = np.array(product, dtype="float64") - np.testing.assert_allclose(product, np.eye(size)) - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=["TestLinalg::test_begin", "TestLinalg::test_invert_fraction"] - ) - def test_solve_float(self): - side, nsols = 4, 4 - matrix = np.eye(side) - force = np.random.rand(side, nsols) - solution = Linalg.solve(matrix, force) - np.testing.assert_allclose(np.dot(matrix, solution), force) - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=["TestLinalg::test_begin", "TestLinalg::test_solve_float"] - ) - def test_solve_integer(self): - side, nsols = 2, 4 - force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] - matrix = ((1, 0), (0, 1)) - solution = Linalg.solve(matrix, force) - mult = np.dot(matrix, solution) - np.testing.assert_allclose(mult, force) - - side, nsols = 2, 4 - force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] - matrix = ((1, 1), (2, 3)) - solution = Linalg.solve(matrix, force) - mult = np.dot(matrix, solution) - np.testing.assert_allclose(mult, force) - - side, nsols = 2, 4 - force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] - matrix = ((1, 1), (11, 12)) - solution = Linalg.solve(matrix, force) - mult = np.dot(matrix, solution) - np.testing.assert_allclose(mult, force) - - side, nsols = 4, 4 - force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] - matrix = ( - (1, 1, 1, 1), - (11, 12, 13, 14), - (55, 66, 78, 91), - (165, 220, 286, 364), - ) - solution = Linalg.solve(matrix, force) - mult = np.dot(matrix, solution) - np.testing.assert_allclose(mult, force) - - # Ericksen matrix - for side in range(1, 10): - nsols = side + 1 - force = np.random.randint(-10, 10, (side, nsols)) - matrix = np.zeros((side, side), dtype="int64") - for n in range(side, side + 10): - for i in range(side): - for j in range(side): - matrix[i, j] = Math.comb(n + j, i) - solution = Linalg.solve(matrix, force) - mult = np.dot(matrix, solution) - np.testing.assert_allclose(mult, force) - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestLinalg::test_begin", - "TestLinalg::test_solve_float", - "TestLinalg::test_solve_integer", - ] - ) - def test_solve_fraction(self): - # Identity - for side in range(1, 10): - zero, one = Fraction(0), Fraction(1) - matrix = [ - [one if i == j else zero for j in range(side)] for i in range(side) - ] - test = Linalg.invert(matrix) - test = np.array(test, dtype="int64") - good = np.eye(side, dtype="int64") - np.testing.assert_allclose(test, good) - - # Ericksen matrix - for side in range(1, 10): - matrix = np.zeros((side, side), dtype="object") - for n in range(side, side + 10): - for i in range(side): - for j in range(side): - matrix[i, j] = Fraction(Math.comb(n + j, i)) - inverse = Linalg.invert(matrix) - inverse = np.array(inverse, dtype="int64") - matrix = np.array(matrix, dtype="int64") - np.testing.assert_allclose(np.dot(inverse, matrix), np.eye(side)) - np.testing.assert_allclose(np.dot(matrix, inverse), np.eye(side)) - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestLinalg::test_begin", - "TestLinalg::test_solve_float", - "TestLinalg::test_solve_fraction", - ] - ) - def test_specific_case(self): - f = Fraction - B = [ - [f(1, 9), f(1, 12), f(5, 84), f(5, 126), f(1, 42), f(1, 84), f(17, 4235)], - [ - f(1, 36), - f(1, 21), - f(5, 84), - f(4, 63), - f(5, 84), - f(204, 4235), - f(653, 21780), - ], - [ - f(1, 252), - f(1, 84), - f(1, 42), - f(5, 126), - f(51, 847), - f(653, 7260), - f(493, 4356), - ], - ] - C = [ - [f(1, 5), f(1, 10), f(727, 21780)], - [f(1, 10), f(727, 5445), f(1117, 10890)], - [f(727, 21780), f(1117, 10890), f(1501, 7260)], - ] - B = np.array(B) - C = np.array(C) - - Cinv = Linalg.invert(C) - solution = np.dot(Cinv, B) - mult = np.dot(C, solution) - diff = np.array(mult - B, dtype="float64") - np.testing.assert_allclose(diff, np.zeros(B.shape)) - - solution = Linalg.solve(C, B) - mult = np.dot(C, solution) - diff = np.array(mult - B, dtype="float64") - np.testing.assert_allclose(diff, np.zeros(B.shape)) - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestLinalg::test_begin", - "TestLinalg::test_solve_float", - "TestLinalg::test_solve_fraction", - ] - ) - def test_specific_case2(self): - f = Fraction - matrix = [[0, -54, -5], [-9, -20, -3], [-25, -90, 216]] - good = [ - [f(2295, 55288), f(-6057, 55288), f(-31, 55288)], - [f(-2019, 110576), f(125, 110576), f(-45, 110576)], - [f(-155, 55288), f(-675, 55288), f(243, 55288)], - ] - prod = np.dot(matrix, np.array(good, dtype="float64")) - np.testing.assert_allclose(prod, np.eye(3), atol=1e-9) - test = Linalg.invert(matrix) - diff = np.array(good - test, dtype="float64") - np.testing.assert_array_equal(diff, np.zeros(diff.shape)) - - matrix = [ - [f(0, 1), f(-3, 4), f(-5, 72)], - [f(-3, 4), f(-5, 3), f(-1, 4)], - [f(-5, 72), f(-1, 4), f(3, 5)], - ] - inverse = Linalg.invert(matrix) - prod = np.dot(matrix, inverse).astype("float64") - np.testing.assert_allclose(prod, np.eye(3)) - prod = np.dot(inverse, matrix).astype("float64") - np.testing.assert_allclose(prod, np.eye(3)) - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestLinalg::test_begin", - "TestLinalg::test_invert_float", - "TestLinalg::test_invert_integer", - "TestLinalg::test_invert_fraction", - "TestLinalg::test_solve_float", - "TestLinalg::test_solve_integer", - "TestLinalg::test_solve_fraction", - "TestLinalg::test_specific_case", - ] - ) - def test_end(self): - pass - - -class TestNodeSample: - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=["test_begin", "TestMath::test_end", "TestLinalg::test_end"] - ) - def test_begin(self): - pass - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) - def test_closed_linspace(self): - nodes = NodeSample.closed_linspace(2) - good = (0, 1) - assert nodes == good - - nodes = NodeSample.closed_linspace(3) - good = (0, 1 / 2, 1) - assert nodes == good - - nodes = NodeSample.closed_linspace(4) - nodes = np.array(nodes, dtype="float64") - good = (0, 1 / 3, 2 / 3, 1) - np.testing.assert_allclose(nodes, good) - - nodes = NodeSample.closed_linspace(5) - good = (0, 1 / 4, 2 / 4, 3 / 4, 1) - assert nodes == good - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) - def test_open_linspace(self): - nodes = NodeSample.open_linspace(1) - good = (1 / 2,) - assert nodes == good - - nodes = NodeSample.open_linspace(2) - good = (1 / 4, 3 / 4) - assert nodes == good - - nodes = NodeSample.open_linspace(3) - nodes = np.array(nodes, dtype="float64") - good = (1 / 6, 3 / 6, 5 / 6) - np.testing.assert_allclose(nodes, good) - - nodes = NodeSample.open_linspace(4) - good = (1 / 8, 3 / 8, 5 / 8, 7 / 8) - assert nodes == good - - nodes = NodeSample.open_linspace(5) - nodes = np.array(nodes, dtype="float64") - good = (1 / 10, 3 / 10, 5 / 10, 7 / 10, 9 / 10) - np.testing.assert_allclose(nodes, good) - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) - def test_chebyshev(self): - nodes = NodeSample.chebyshev(1) - assert nodes == (1 / 2,) - - nodes = NodeSample.chebyshev(2) - left = (2 - np.sqrt(2)) / 4 - right = (2 + np.sqrt(2)) / 4 - good = (left, right) - np.testing.assert_allclose(nodes, good) - - nodes = NodeSample.chebyshev(3) - left = (2 - np.sqrt(3)) / 4 - right = (2 + np.sqrt(3)) / 4 - good = (left, 1 / 2, right) - np.testing.assert_allclose(nodes, good) - - nodes = NodeSample.chebyshev(4) - good = np.sin(np.pi * np.array([1 / 16, 3 / 16, 5 / 16, 7 / 16])) ** 2 - np.testing.assert_allclose(nodes, good) - - nodes = NodeSample.chebyshev(5) - good = np.sin(np.pi * np.array([1 / 20, 3 / 20, 5 / 20, 7 / 20, 9 / 20])) ** 2 - np.testing.assert_allclose(nodes, good) - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) - def test_gauss_legendre(self): - nodes = NodeSample.gauss_legendre(1) - assert nodes == (1 / 2,) - - nodes = NodeSample.gauss_legendre(2) - minor = 1 / np.sqrt(3) - good = [(1 - minor) / 2, (1 + minor) / 2] - np.testing.assert_allclose(nodes, good) - - nodes = NodeSample.gauss_legendre(3) - minor = np.sqrt(3 / 5) - good = [(1 - minor) / 2, 1 / 2, (1 + minor) / 2] - np.testing.assert_allclose(nodes, good) - - nodes = NodeSample.gauss_legendre(4) - minor = np.sqrt(3 / 7 + 2 * np.sqrt(6 / 5) / 7) - middl = np.sqrt(3 / 7 - 2 * np.sqrt(6 / 5) / 7) - good = [(1 - minor) / 2, (1 - middl) / 2, (1 + middl) / 2, (1 + minor) / 2] - np.testing.assert_allclose(nodes, good) - - nodes = NodeSample.gauss_legendre(5) - minor = np.sqrt(5 + 2 * np.sqrt(10 / 7)) / 3 - middl = np.sqrt(5 - 2 * np.sqrt(10 / 7)) / 3 - good = [ - (1 - minor) / 2, - (1 - middl) / 2, - 1 / 2, - (1 + middl) / 2, - (1 + minor) / 2, - ] - np.testing.assert_allclose(nodes, good) - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestNodeSample::test_begin", - "TestNodeSample::test_closed_linspace", - "TestNodeSample::test_open_linspace", - "TestNodeSample::test_chebyshev", - "TestNodeSample::test_gauss_legendre", - ] - ) - def test_end(self): - pass - - -class TestUnidimentionIntegral: - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "test_begin", - "TestMath::test_end", - "TestLinalg::test_end", - "TestNodeSample::test_end", - ] - ) - def test_begin(self): - pass - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) - def test_closed_newton_cotes(self): - a, b = Fraction(3), Fraction(5) - for degree in range(0, 7): - npts = max(2, degree + 1) # Number integration points - numers = np.random.randint(-5, 5, degree + 1) - denoms = np.random.randint(2, 8, degree + 1) - coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] - good = sum( - ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) - for i, ci in enumerate(coefs) - ) - - nodes = NodeSample.closed_linspace(npts) - weights = IntegratorArray.closed_newton_cotes(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - - assert test == good - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) - def test_open_newton_cotes(self): - a, b = Fraction(3), Fraction(5) - for degree in range(0, 7): - npts = degree + 1 # Number integration points - numers = np.random.randint(-5, 5, degree + 1) - denoms = np.random.randint(2, 8, degree + 1) - coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] - good = sum( - ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) - for i, ci in enumerate(coefs) - ) - - nodes = NodeSample.open_linspace(npts) - weights = IntegratorArray.open_newton_cotes(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - - assert test == good - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) - def test_chebyshev(self): - a, b = Fraction(3), Fraction(7) - for degree in range(0, 7): - npts = degree + 1 # Number integration points - numers = np.random.randint(-5, 5, degree + 1) - denoms = np.random.randint(2, 8, degree + 1) - coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] - good = sum( - ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) - for i, ci in enumerate(coefs) - ) - - nodes = NodeSample.chebyshev(npts) - weights = IntegratorArray.chebyshev(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - - assert abs(test - good) < 1e-9 - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) - def test_gauss_legendre(self): - a, b = Fraction(3), Fraction(7) - for degree in range(0, 7): - npts = degree + 1 # Number integration points - numers = np.random.randint(-5, 5, degree + 1) - denoms = np.random.randint(2, 8, degree + 1) - coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] - good = sum( - ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) - for i, ci in enumerate(coefs) - ) - - nodes = NodeSample.gauss_legendre(npts) - weights = IntegratorArray.gauss_legendre(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - - assert abs(test - good) < 1e-9 - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestUnidimentionIntegral::test_begin", - "TestUnidimentionIntegral::test_closed_newton_cotes", - "TestUnidimentionIntegral::test_open_newton_cotes", - "TestUnidimentionIntegral::test_chebyshev", - "TestUnidimentionIntegral::test_gauss_legendre", - ] - ) - def test_exact_integral_fraction(self): - a, b = Fraction(3), Fraction(7) - for degree in range(0, 7): - npts = 1 + 2 * math.floor(degree / 2) # Number integration points - numers = np.random.randint(-5, 5, degree + 1) - denoms = np.random.randint(2, 8, degree + 1) - coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] - good = sum( - ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) - for i, ci in enumerate(coefs) - ) - - if npts > 1: - nodes = NodeSample.open_linspace(npts) - weights = IntegratorArray.open_newton_cotes(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - assert test == good - - nodes = NodeSample.open_linspace(npts) - weights = IntegratorArray.open_newton_cotes(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - assert test == good - - nodes = NodeSample.chebyshev(npts) - weights = IntegratorArray.chebyshev(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - assert abs(test - good) < 1e-9 - - nodes = NodeSample.gauss_legendre(npts) - weights = IntegratorArray.gauss_legendre(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - assert abs(test - good) < 1e-9 - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestUnidimentionIntegral::test_begin", - "TestUnidimentionIntegral::test_closed_newton_cotes", - "TestUnidimentionIntegral::test_open_newton_cotes", - "TestUnidimentionIntegral::test_chebyshev", - "TestUnidimentionIntegral::test_gauss_legendre", - "TestUnidimentionIntegral::test_exact_integral_fraction", - ] - ) - def test_approx_integral(self): - a, b = Fraction(3), Fraction(7) - for degree in range(0, 7): - npts = 1 + 2 * math.floor(degree / 2) # Number integration points - coefs = np.random.uniform(-5, 6, degree + 1) - good = sum( - ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) - for i, ci in enumerate(coefs) - ) - - if npts > 1: - nodes = NodeSample.open_linspace(npts) - weights = IntegratorArray.open_newton_cotes(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - assert abs(test - good) < 1e-9 - - nodes = NodeSample.open_linspace(npts) - weights = IntegratorArray.open_newton_cotes(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - assert abs(test - good) < 1e-9 - - nodes = NodeSample.chebyshev(npts) - weights = IntegratorArray.chebyshev(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - assert abs(test - good) < 1e-9 - - nodes = NodeSample.gauss_legendre(npts) - weights = IntegratorArray.gauss_legendre(npts) - nodes = tuple(a + (b - a) * node for node in nodes) - funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes - ) - test = (b - a) * np.inner(weights, funcvals) - assert abs(test - good) < 1e-9 - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestUnidimentionIntegral::test_begin", - "TestUnidimentionIntegral::test_closed_newton_cotes", - "TestUnidimentionIntegral::test_open_newton_cotes", - "TestUnidimentionIntegral::test_chebyshev", - "TestUnidimentionIntegral::test_gauss_legendre", - "TestUnidimentionIntegral::test_exact_integral_fraction", - "TestUnidimentionIntegral::test_approx_integral", - ] - ) - def test_end(self): - pass - - class TestLeastSquare: @pytest.mark.order(1) @pytest.mark.dependency( depends=[ "test_begin", - "TestMath::test_end", - "TestLinalg::test_end", - "TestNodeSample::test_end", - "TestUnidimentionIntegral::test_end", ] ) def test_begin(self): @@ -838,10 +71,7 @@ def test_end(self): @pytest.mark.order(1) @pytest.mark.dependency( depends=[ - "TestMath::test_end", - "TestLinalg::test_end", - "TestNodeSample::test_end", - "TestUnidimentionIntegral::test_end", + "test_begin", "TestLeastSquare::test_end", ] ) From 1072d7c9c4802f7ad0115350471238f4a8f47fe8 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 27 Jun 2025 19:44:40 +0200 Subject: [PATCH 013/116] refactor: move ImmutableKnotVector into knotspace.py --- src/pynurbs/heavy.py | 241 +------------------------------------- src/pynurbs/knotspace.py | 242 ++++++++++++++++++++++++++++++++++++++- 2 files changed, 242 insertions(+), 241 deletions(-) diff --git a/src/pynurbs/heavy.py b/src/pynurbs/heavy.py index f50749a..5d58cce 100644 --- a/src/pynurbs/heavy.py +++ b/src/pynurbs/heavy.py @@ -12,246 +12,7 @@ import numpy as np from .cmath import IntegratorArray, Linalg, NodeSample, number_type, totuple - - -class ImmutableKnotVector(tuple): - @staticmethod - def __get_unique(vector: Tuple[float]): - unique = [] - for node in vector: - for knot in unique: - if abs(node - knot) < 1e-6: - break - else: - unique.append(node) - unique.sort() - return tuple(unique) - - @staticmethod - def __is_valid(vector: Tuple[float], degree: Union[int, None]): - try: - for knot in vector: - float(knot) - except TypeError: - return False - lenght = len(vector) - if lenght < 2: - return False - for i in range(lenght - 1): - if not vector[i] <= vector[i + 1]: - return False - if degree is None: - degree = 0 - while vector[degree] == vector[degree + 1]: - degree += 1 - npts = lenght - degree - 1 - if not degree < npts: - return False - knots = ImmutableKnotVector.__get_unique(vector[degree : npts + 1]) - for knot in knots: - mult = vector.count(knot) - if mult > degree + 1: - return False - if vector.count(vector[degree]) != vector.count(vector[npts]): - return False - return True - - def __new__(cls, knotvector: Tuple[float], degree: Optional[int] = None): - if isinstance(knotvector, ImmutableKnotVector): - return knotvector - try: - knotvector = tuple(knotvector) - except TypeError: - raise ValueError - if not cls.__is_valid(knotvector, degree): - msg = f"Invalid knot vector (deg {degree}): {knotvector}" - raise ValueError(msg) - if degree is None: - degree = 0 - while knotvector[degree] == knotvector[degree + 1]: - degree += 1 - instance = super(ImmutableKnotVector, cls).__new__(cls, tuple(knotvector)) - instance._ImmutableKnotVector__degree = degree - instance._ImmutableKnotVector__npts = len(knotvector) - degree - 1 - return instance - - def __or__(self, other: ImmutableKnotVector) -> ImmutableKnotVector: - other = ImmutableKnotVector(other) - if self.limits != other.limits: - raise ValueError - all_knots = list(self.knots) + list(other.knots) - all_knots = ImmutableKnotVector.__get_unique(all_knots) - all_mults = [0] * len(all_knots) - for vector in [self, other]: - for knot in vector: - index = all_knots.index(knot) - mult = vector.mult(knot) - if mult > all_mults[index]: - all_mults[index] = mult - final_vector = [] - for knot, mult in zip(all_knots, all_mults): - final_vector += [knot] * mult - final_vector = tuple(sorted(final_vector)) - return ImmutableKnotVector(final_vector) - - def __and__(self, other: ImmutableKnotVector) -> ImmutableKnotVector: - other = ImmutableKnotVector(other) - if self.limits != other.limits: - raise ValueError - all_knots = tuple(sorted(set(self.knots) & set(other.knots))) - all_mults = [float("inf")] * len(all_knots) - for vector in [self, other]: - for knot in vector: - if knot not in all_knots: - continue - index = all_knots.index(knot) - mult = vector.mult(knot) - if mult < all_mults[index]: - all_mults[index] = mult - final_vector = [] - for knot, mult in zip(all_knots, all_mults): - final_vector += [knot] * mult - return ImmutableKnotVector(sorted(final_vector)) - - def __add__(self, other): - raise ValueError - - def __sub__(self, other): - raise ValueError - - @property - def degree(self) -> int: - return self.__degree - - @property - def npts(self) -> int: - return self.__npts - - @property - def knots(self) -> Tuple[float]: - vector = self[self.degree : self.npts + 1] - return ImmutableKnotVector.__get_unique(vector) - - @property - def limits(self) -> Tuple[float]: - return (self[self.degree], self[self.npts]) - - def __span_single(self, node: float) -> int: - if node == self[self.npts]: # Special case - return self.npts - 1 - low, high = self.degree, self.npts + 1 # Do binary search - mid = (low + high) // 2 - while True: - if node < self[mid]: - high = mid - else: - low = mid - mid = (low + high) // 2 - if self[mid] <= node < self[mid + 1]: - return mid - - def __mult_single(self, node: Tuple[float]) -> Tuple[int]: - return sum(abs(node - knot) < 1e-9 for knot in self) - - def __valid_single(self, node: float) -> bool: - try: - float(node) # Verify if it's a number - except TypeError: - return False - umin, umax = self.limits - if node < umin or umax < node: - return False - return True - - def span(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: - if not self.valid(nodes): - raise ValueError - try: - return tuple(map(self.span, nodes)) - except TypeError: - return self.__span_single(nodes) - - def mult(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: - if not self.valid(nodes): - raise ValueError - try: - return tuple(map(self.mult, nodes)) - except TypeError: - return self.__mult_single(nodes) - - def valid(self, nodes: Tuple[float]) -> bool: - if isinstance(nodes, str): - return False - try: - for node in nodes: - if not self.valid(node): - return False - return True - except TypeError: - return self.__valid_single(nodes) - - def split(self, nodes: Tuple[float]) -> Tuple[ImmutableKnotVector]: - """ - It splits the knotvector at nodes. - You may put initial and final values, but they are ignored. - Example: - >> U = [0, 0, 0.5, 1, 1] - >> split(U, [0.5]) - [[0, 0, 0.5, 0.5], - [0.5, 0.5, 1, 1]] - >> split(U, [0.25]) - [[0, 0, 0.25, 0.25], - [0.25, 0.25, 0.5, 1, 1]] - >> split(U, [0, 0.25, 0.75]) - [[0, 0, 0.25, 0.25], - [0.25, 0.25, 0.5, 0.75, 0.75], - [0.75, 0.75, 1, 1]] - """ - if not self.valid(nodes): - raise ValueError - nodes = set(nodes) - if len(nodes) == 0: - return (self,) - nodes = tuple(sorted(nodes | set(self.limits))) - vector = np.array(self) - - retorno = [] - for a, b in zip(nodes[:-1], nodes[1:]): - middle = list(vector[(a < vector) * (vector < b)]) - newknotvect = (self.degree + 1) * [a] + middle + (self.degree + 1) * [b] - newknotvect = ImmutableKnotVector(newknotvect) - retorno.append(newknotvect) - return tuple(retorno) - - def increase(self, times: int) -> ImmutableKnotVector: - """Degree increase""" - vector = sorted(list(self) + times * list(self.knots)) - return self.__class__(vector, self.degree + times) - - def decrease(self, times: int) -> ImmutableKnotVector: - """Degree decrease""" - vector = list(self) - knots = self.knots[1:-1] - for _ in range(times): - vector.pop(0) - vector.pop(-1) - for node in knots: - vector.remove(node) - vector = sorted(vector) - return self.__class__(vector, self.degree - times) - - def remove(self, nodes: Tuple[float]) -> ImmutableKnotVector: - """Remove knots""" - vector = list(self) - for node in nodes: - vector.remove(node) - vector = sorted(vector) - return self.__class__(vector, self.degree) - - def insert(self, nodes: Tuple[float]) -> ImmutableKnotVector: - """Insert knots""" - vector = sorted(list(self) + list(nodes)) - return self.__class__(vector, self.degree) +from .knotspace import ImmutableKnotVector def find_roots( diff --git a/src/pynurbs/knotspace.py b/src/pynurbs/knotspace.py index aaf4910..af4a9ed 100644 --- a/src/pynurbs/knotspace.py +++ b/src/pynurbs/knotspace.py @@ -4,6 +4,7 @@ For example, the valid limits to evaluate curve, the polynomial degree, continuity and smoothness """ + from __future__ import annotations from copy import deepcopy @@ -12,7 +13,246 @@ import numpy as np from pynurbs.__classes__ import Intface_KnotVector -from pynurbs.heavy import ImmutableKnotVector + + +class ImmutableKnotVector(tuple): + @staticmethod + def __get_unique(vector: Tuple[float]): + unique = [] + for node in vector: + for knot in unique: + if abs(node - knot) < 1e-6: + break + else: + unique.append(node) + unique.sort() + return tuple(unique) + + @staticmethod + def __is_valid(vector: Tuple[float], degree: Union[int, None]): + try: + for knot in vector: + float(knot) + except TypeError: + return False + lenght = len(vector) + if lenght < 2: + return False + for i in range(lenght - 1): + if not vector[i] <= vector[i + 1]: + return False + if degree is None: + degree = 0 + while vector[degree] == vector[degree + 1]: + degree += 1 + npts = lenght - degree - 1 + if not degree < npts: + return False + knots = ImmutableKnotVector.__get_unique(vector[degree : npts + 1]) + for knot in knots: + mult = vector.count(knot) + if mult > degree + 1: + return False + if vector.count(vector[degree]) != vector.count(vector[npts]): + return False + return True + + def __new__(cls, knotvector: Tuple[float], degree: Optional[int] = None): + if isinstance(knotvector, ImmutableKnotVector): + return knotvector + try: + knotvector = tuple(knotvector) + except TypeError: + raise ValueError + if not cls.__is_valid(knotvector, degree): + msg = f"Invalid knot vector (deg {degree}): {knotvector}" + raise ValueError(msg) + if degree is None: + degree = 0 + while knotvector[degree] == knotvector[degree + 1]: + degree += 1 + instance = super(ImmutableKnotVector, cls).__new__(cls, tuple(knotvector)) + instance._ImmutableKnotVector__degree = degree + instance._ImmutableKnotVector__npts = len(knotvector) - degree - 1 + return instance + + def __or__(self, other: ImmutableKnotVector) -> ImmutableKnotVector: + other = ImmutableKnotVector(other) + if self.limits != other.limits: + raise ValueError + all_knots = list(self.knots) + list(other.knots) + all_knots = ImmutableKnotVector.__get_unique(all_knots) + all_mults = [0] * len(all_knots) + for vector in [self, other]: + for knot in vector: + index = all_knots.index(knot) + mult = vector.mult(knot) + if mult > all_mults[index]: + all_mults[index] = mult + final_vector = [] + for knot, mult in zip(all_knots, all_mults): + final_vector += [knot] * mult + final_vector = tuple(sorted(final_vector)) + return ImmutableKnotVector(final_vector) + + def __and__(self, other: ImmutableKnotVector) -> ImmutableKnotVector: + other = ImmutableKnotVector(other) + if self.limits != other.limits: + raise ValueError + all_knots = tuple(sorted(set(self.knots) & set(other.knots))) + all_mults = [float("inf")] * len(all_knots) + for vector in [self, other]: + for knot in vector: + if knot not in all_knots: + continue + index = all_knots.index(knot) + mult = vector.mult(knot) + if mult < all_mults[index]: + all_mults[index] = mult + final_vector = [] + for knot, mult in zip(all_knots, all_mults): + final_vector += [knot] * mult + return ImmutableKnotVector(sorted(final_vector)) + + def __add__(self, other): + raise ValueError + + def __sub__(self, other): + raise ValueError + + @property + def degree(self) -> int: + return self.__degree + + @property + def npts(self) -> int: + return self.__npts + + @property + def knots(self) -> Tuple[float]: + vector = self[self.degree : self.npts + 1] + return ImmutableKnotVector.__get_unique(vector) + + @property + def limits(self) -> Tuple[float]: + return (self[self.degree], self[self.npts]) + + def __span_single(self, node: float) -> int: + if node == self[self.npts]: # Special case + return self.npts - 1 + low, high = self.degree, self.npts + 1 # Do binary search + mid = (low + high) // 2 + while True: + if node < self[mid]: + high = mid + else: + low = mid + mid = (low + high) // 2 + if self[mid] <= node < self[mid + 1]: + return mid + + def __mult_single(self, node: Tuple[float]) -> Tuple[int]: + return sum(abs(node - knot) < 1e-9 for knot in self) + + def __valid_single(self, node: float) -> bool: + try: + float(node) # Verify if it's a number + except TypeError: + return False + umin, umax = self.limits + if node < umin or umax < node: + return False + return True + + def span(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: + if not self.valid(nodes): + raise ValueError + try: + return tuple(map(self.span, nodes)) + except TypeError: + return self.__span_single(nodes) + + def mult(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: + if not self.valid(nodes): + raise ValueError + try: + return tuple(map(self.mult, nodes)) + except TypeError: + return self.__mult_single(nodes) + + def valid(self, nodes: Tuple[float]) -> bool: + if isinstance(nodes, str): + return False + try: + for node in nodes: + if not self.valid(node): + return False + return True + except TypeError: + return self.__valid_single(nodes) + + def split(self, nodes: Tuple[float]) -> Tuple[ImmutableKnotVector]: + """ + It splits the knotvector at nodes. + You may put initial and final values, but they are ignored. + Example: + >> U = [0, 0, 0.5, 1, 1] + >> split(U, [0.5]) + [[0, 0, 0.5, 0.5], + [0.5, 0.5, 1, 1]] + >> split(U, [0.25]) + [[0, 0, 0.25, 0.25], + [0.25, 0.25, 0.5, 1, 1]] + >> split(U, [0, 0.25, 0.75]) + [[0, 0, 0.25, 0.25], + [0.25, 0.25, 0.5, 0.75, 0.75], + [0.75, 0.75, 1, 1]] + """ + if not self.valid(nodes): + raise ValueError + nodes = set(nodes) + if len(nodes) == 0: + return (self,) + nodes = tuple(sorted(nodes | set(self.limits))) + vector = np.array(self) + + retorno = [] + for a, b in zip(nodes[:-1], nodes[1:]): + middle = list(vector[(a < vector) * (vector < b)]) + newknotvect = (self.degree + 1) * [a] + middle + (self.degree + 1) * [b] + newknotvect = ImmutableKnotVector(newknotvect) + retorno.append(newknotvect) + return tuple(retorno) + + def increase(self, times: int) -> ImmutableKnotVector: + """Degree increase""" + vector = sorted(list(self) + times * list(self.knots)) + return self.__class__(vector, self.degree + times) + + def decrease(self, times: int) -> ImmutableKnotVector: + """Degree decrease""" + vector = list(self) + knots = self.knots[1:-1] + for _ in range(times): + vector.pop(0) + vector.pop(-1) + for node in knots: + vector.remove(node) + vector = sorted(vector) + return self.__class__(vector, self.degree - times) + + def remove(self, nodes: Tuple[float]) -> ImmutableKnotVector: + """Remove knots""" + vector = list(self) + for node in nodes: + vector.remove(node) + vector = sorted(vector) + return self.__class__(vector, self.degree) + + def insert(self, nodes: Tuple[float]) -> ImmutableKnotVector: + """Insert knots""" + vector = sorted(list(self) + list(nodes)) + return self.__class__(vector, self.degree) class KnotVector(Intface_KnotVector): From 4616de94f7467521a6a423bec5a78ea0be3d80c9 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 27 Jun 2025 19:52:20 +0200 Subject: [PATCH 014/116] fix: remove possibility of receive empty tuple as input of polynomial --- src/pynurbs/polynomial.py | 4 +++- tests/test_polynomial.py | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/src/pynurbs/polynomial.py b/src/pynurbs/polynomial.py index 9f143e7..c9b0978 100644 --- a/src/pynurbs/polynomial.py +++ b/src/pynurbs/polynomial.py @@ -34,7 +34,7 @@ class Polynomial: def __init__(self, coefs: Iterable[Real]): coefs = tuple(coefs) if len(coefs) == 0: - coefs = (0,) + raise ValueError("Cannot receive an empty tuple") self.__coefs = tuple(coefs) @property @@ -225,6 +225,8 @@ def derivate(polynomial: Polynomial, times: int = 1) -> Polynomial: >>> print(dpoly) 2 + 10 * x """ + if polynomial.degree < times: + return Polynomial([0 * polynomial[0]]) coefs = ( math.factorial(n + times) // math.factorial(n) * coef for n, coef in enumerate(polynomial[times:]) diff --git a/tests/test_polynomial.py b/tests/test_polynomial.py index 62f80ad..9f9baa5 100644 --- a/tests/test_polynomial.py +++ b/tests/test_polynomial.py @@ -6,7 +6,7 @@ @pytest.mark.order(1) @pytest.mark.dependency() def test_build(): - Polynomial([]) # p(x) = 0 + Polynomial([0]) # p(x) = 0 Polynomial([1]) # p(x) = 1 Polynomial([1, 2]) # p(x) = 1 + 2 * x Polynomial([1, 2, 3]) # p(x) = 1 + 2 * x + 3 * x^2 @@ -16,7 +16,7 @@ def test_build(): @pytest.mark.order(1) @pytest.mark.dependency(depends=["test_build"]) def test_degree(): - poly = Polynomial([]) # p(x) = 0 + poly = Polynomial([0]) # p(x) = 0 assert poly.degree == 0 poly = Polynomial([1]) # p(x) = 1 assert poly.degree == 0 @@ -29,7 +29,7 @@ def test_degree(): @pytest.mark.order(1) @pytest.mark.dependency(depends=["test_build", "test_degree"]) def test_evaluate(): - poly = Polynomial([]) # p(x) = 0 + poly = Polynomial([0]) # p(x) = 0 assert poly.eval(0) == 0 assert poly.eval(-1) == 0 assert poly.eval(2) == 0 From 7bb06efefe9629516708c24fca033c194b4b6ec8 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 27 Jun 2025 20:17:10 +0200 Subject: [PATCH 015/116] refactor: move evaluation of splines into basisfunction.py --- src/pynurbs/basisfunction.py | 100 +++++++++++++++++++++++ src/pynurbs/heavy.py | 148 ----------------------------------- 2 files changed, 100 insertions(+), 148 deletions(-) create mode 100644 src/pynurbs/basisfunction.py diff --git a/src/pynurbs/basisfunction.py b/src/pynurbs/basisfunction.py new file mode 100644 index 0000000..123f658 --- /dev/null +++ b/src/pynurbs/basisfunction.py @@ -0,0 +1,100 @@ +from numbers import Real +from typing import Tuple, Union + +import numpy as np + +from .cmath import totuple +from .knotspace import ImmutableKnotVector +from .piecepoly import PiecewisePolynomial +from .polynomial import Polynomial + + +def spectral_matrix( + knotvector: ImmutableKnotVector, reqdegree: int +) -> Tuple[Tuple[Tuple[Real, ...], ...], ...]: + """ + Given a knotvector, it has properties like + - number of points: npts + - polynomial degree: degree + - knots: A list of non-repeted knots + - spans: The span of each knot + This function returns a matrix of size + (m) x (j+1) x (j+1) + which + - m is the number of segments: len(knots)-1 + - j is the requested degree + """ + knotvector = ImmutableKnotVector(knotvector) + if not isinstance(reqdegree, int): + raise TypeError("reqdegree must be integer") + if reqdegree < 0 or knotvector.degree < reqdegree: + msg = f"reqdegree must be in [0, {knotvector.degree}]" + raise ValueError(msg) + knots = knotvector.knots + spans = knotvector.span(knots) + j = reqdegree + + ninter = len(knots) - 1 + matrix = [[[0 * knots[0]] * (j + 1)] * (j + 1)] * ninter + matrix = np.array(matrix, dtype="object") + if j == 0: + matrix.fill(1) + return matrix + matrix_less1 = spectral_matrix(knotvector, j - 1) + matrix_less1 = np.array(matrix_less1).tolist() + for y in range(j): + for z, sz in enumerate(spans[:-1]): + i = y + sz - j + 1 + denom = knotvector[i + j] - knotvector[i] + for k in range(j): + matrix_less1[z][y][k] /= denom + + a0 = knots[z] - knotvector[i] + a1 = knots[z + 1] - knots[z] + b0 = knotvector[i + j] - knots[z] + b1 = knots[z] - knots[z + 1] + for k in range(j): + matrix[z][y][k] += b0 * matrix_less1[z][y][k] + matrix[z][y][k + 1] += b1 * matrix_less1[z][y][k] + matrix[z][y + 1][k] += a0 * matrix_less1[z][y][k] + matrix[z][y + 1][k + 1] += a1 * matrix_less1[z][y][k] + return totuple(matrix) + + +class ImmutableBasisFunction: + + def __init__( + self, knotvector: ImmutableKnotVector, degree: Union[int, None] = None + ): + degree = degree or knotvector.degree + self.__matrix = tuple( + tuple(tuple(Polynomial(coefs) for coefs in all_coefs)) + for all_coefs in spectral_matrix(knotvector, degree) + ) + self.__knotvector = knotvector + + @property + def knots(self) -> Tuple[Real, ...]: + return self.__knotvector.knots + + def __getitem__(self, index: int) -> PiecewisePolynomial: + functions = self.__matrix[index] + return PiecewisePolynomial(functions, self.knots) + + def eval(self, node: Real, times: int = 0) -> Tuple[Real, ...]: + + npts = self.__knotvector.npts + knots = self.__knotvector.knots + spans = self.__knotvector.span(knots) + degree = self.__knotvector.degree + result = [0] * npts + + span = self.__knotvector.span(node) + ind = spans.index(span) + shifnode = node - knots[ind] + shifnode /= knots[ind + 1] - knots[ind] + for y in range(self.__knotvector.degree + 1): + i = y + span - degree + polynomial = self.__matrix[ind][y] + result[i] = polynomial.eval(shifnode, times) + return tuple(result) diff --git a/src/pynurbs/heavy.py b/src/pynurbs/heavy.py index 5d58cce..027a4ba 100644 --- a/src/pynurbs/heavy.py +++ b/src/pynurbs/heavy.py @@ -98,81 +98,6 @@ def find_roots( return tuple(sorted(filtered_roots)) -def eval_spline_nodes( - knotvector: ImmutableKnotVector, nodes: Tuple[float], degree: int -) -> Tuple[Tuple[float]]: - """ - Returns a matrix M of which M_{ij} = N_{i,degree}(node_j) - M.shape = (npts, len(nodes)) - """ - knotvector = ImmutableKnotVector(knotvector) - if not knotvector.valid(nodes): - msg = f"Invalid nodes {nodes} in knotvector {knotvector}" - raise ValueError(msg) - if not isinstance(degree, int): - msg = f"Degree must be int, received {degree}" - print(msg) - raise ValueError(msg) - if degree < 0 or knotvector.degree < degree: - msg = f"Degree {degree} must be in the interval [0, {knotvector.degree}]" - print(msg) - raise ValueError(msg) - - npts = knotvector.npts - knots = knotvector.knots - spans = knotvector.span(knots) - result = np.zeros((npts, len(nodes)), dtype="object") - matrix3d = BasisFunction.speval_matrix(knotvector, degree) - matrix3d = np.array(matrix3d) - for j, node in enumerate(nodes): - span = knotvector.span(node) - ind = spans.index(span) - shifnode = node - knots[ind] - shifnode /= knots[ind + 1] - knots[ind] - for y in range(degree + 1): - i = y + span - degree - coefs = matrix3d[ind, y] - value = BasisFunction.horner_method(coefs, shifnode) - result[i, j] = value - return totuple(result) - - -def eval_rational_nodes( - knotvector: ImmutableKnotVector, - weights: Tuple[float], - nodes: Tuple[float], - degree: int, -) -> Tuple[Tuple[float]]: - """ - Returns a matrix M of which M_{ij} = N_{i,p}(node_j) - M.shape = (len(weights), len(nodes)) - """ - try: - knotvector = ImmutableKnotVector(knotvector) - weights = tuple(weights) - nodes = tuple(nodes) - except (ValueError, TypeError): - msg = "Invalid inputs: \n" - msg += "knotvector = %s\n" % str(knotvector) - msg += "weights = %s\n" % str(weights) - msg += "nodes = %s\n" % str(nodes) - raise ValueError(msg) - if not isinstance(degree, int): - msg = f"Degree must be int, received {degree}" - raise ValueError(msg) - if degree < 0 or knotvector.degree < degree: - msg = f"Degree {degree} must be in the interval [0, {knotvector.degree}]" - raise ValueError(msg) - - rationalvals = eval_spline_nodes(knotvector, nodes, degree) - rationalvals = np.array(rationalvals) - for j, node in enumerate(nodes): - denom = np.inner(rationalvals[:, j], weights) - for i, weight in enumerate(weights): - rationalvals[i, j] *= weight / denom - return totuple(rationalvals) - - class LeastSquare: """ Given two hypotetic curves C0 and C1, which are associated @@ -346,79 +271,6 @@ def func2func( return totuple(T), totuple(E) -class BasisFunction: - @staticmethod - def horner_method(coefs: Tuple[float], value: float) -> float: - """ - Horner method is a efficient method of computing polynomials - Let's say you have a polynomial - P(x) = a_0 + a_1 * x + ... + a_n * x^n - A way to compute P(x_0) is - P(x_0) = a_0 + a_1 * x_0 + ... + a_n * x_0^n - But a more efficient way is to use - P(x_0) = ((...((x_0 * a_n + a_{n-1})*x_0)...)*x_0 + a_1)*x_0 + a_0 - - Input: - coefs : Tuple[float] = (a_0, a_1, ..., a_n) - value : float = x_0 - """ - soma = 0 - for ck in coefs[::-1]: - soma *= value - soma += ck - return soma - - @staticmethod - def speval_matrix(knotvector: ImmutableKnotVector, reqdegree: int): - """ - Given a knotvector, it has properties like - - number of points: npts - - polynomial degree: degree - - knots: A list of non-repeted knots - - spans: The span of each knot - This function returns a matrix of size - (m) x (j+1) x (j+1) - which - - m is the number of segments: len(knots)-1 - - j is the requested degree - """ - knotvector = ImmutableKnotVector(knotvector) - if not isinstance(reqdegree, int): - raise TypeError("reqdegree must be integer") - if reqdegree < 0 or knotvector.degree < reqdegree: - msg = f"reqdegree must be in [0, {knotvector.degree}]" - raise ValueError(msg) - knots = knotvector.knots - spans = knotvector.span(knots) - j = reqdegree - - ninter = len(knots) - 1 - matrix = [[[0 * knots[0]] * (j + 1)] * (j + 1)] * ninter - matrix = np.array(matrix, dtype="object") - if j == 0: - matrix.fill(1) - return matrix - matrix_less1 = BasisFunction.speval_matrix(knotvector, j - 1) - matrix_less1 = np.array(matrix_less1).tolist() - for y in range(j): - for z, sz in enumerate(spans[:-1]): - i = y + sz - j + 1 - denom = knotvector[i + j] - knotvector[i] - for k in range(j): - matrix_less1[z][y][k] /= denom - - a0 = knots[z] - knotvector[i] - a1 = knots[z + 1] - knots[z] - b0 = knotvector[i + j] - knots[z] - b1 = knots[z] - knots[z + 1] - for k in range(j): - matrix[z][y][k] += b0 * matrix_less1[z][y][k] - matrix[z][y][k + 1] += b1 * matrix_less1[z][y][k] - matrix[z][y + 1][k] += a0 * matrix_less1[z][y][k] - matrix[z][y + 1][k + 1] += a1 * matrix_less1[z][y][k] - return totuple(matrix) - - class Operations: """ Contains algorithms to From d341228ee508863e53f633d99e284ec79f2f8c05 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 27 Jun 2025 20:30:56 +0200 Subject: [PATCH 016/116] refactor: move core functions and classes into core folder --- src/pynurbs/core/__init__.py | 3 + src/pynurbs/{ => core}/basisfunction.py | 4 +- src/pynurbs/core/knotvector.py | 243 +++++++++++++++++++++++ src/pynurbs/{ => core}/piecepoly.py | 0 src/pynurbs/{ => core}/polynomial.py | 0 src/pynurbs/knotspace.py | 240 +---------------------- tests/core/__init__.py | 3 + tests/core/test_basis_function.py | 0 tests/core/test_knotvector.py | 246 ++++++++++++++++++++++++ tests/{ => core}/test_polynomial.py | 2 +- 10 files changed, 499 insertions(+), 242 deletions(-) create mode 100644 src/pynurbs/core/__init__.py rename src/pynurbs/{ => core}/basisfunction.py (97%) create mode 100644 src/pynurbs/core/knotvector.py rename src/pynurbs/{ => core}/piecepoly.py (100%) rename src/pynurbs/{ => core}/polynomial.py (100%) create mode 100644 tests/core/__init__.py create mode 100644 tests/core/test_basis_function.py create mode 100644 tests/core/test_knotvector.py rename tests/{ => core}/test_polynomial.py (99%) diff --git a/src/pynurbs/core/__init__.py b/src/pynurbs/core/__init__.py new file mode 100644 index 0000000..55f36c1 --- /dev/null +++ b/src/pynurbs/core/__init__.py @@ -0,0 +1,3 @@ +from .basisfunction import ImmutableBasisFunction +from .knotvector import ImmutableKnotVector +from .polynomial import Polynomial diff --git a/src/pynurbs/basisfunction.py b/src/pynurbs/core/basisfunction.py similarity index 97% rename from src/pynurbs/basisfunction.py rename to src/pynurbs/core/basisfunction.py index 123f658..859f1f3 100644 --- a/src/pynurbs/basisfunction.py +++ b/src/pynurbs/core/basisfunction.py @@ -3,8 +3,8 @@ import numpy as np -from .cmath import totuple -from .knotspace import ImmutableKnotVector +from ..cmath import totuple +from .knotvector import ImmutableKnotVector from .piecepoly import PiecewisePolynomial from .polynomial import Polynomial diff --git a/src/pynurbs/core/knotvector.py b/src/pynurbs/core/knotvector.py new file mode 100644 index 0000000..b457967 --- /dev/null +++ b/src/pynurbs/core/knotvector.py @@ -0,0 +1,243 @@ +from __future__ import annotations + +from typing import Optional, Tuple, Union + + +class ImmutableKnotVector(tuple): + @staticmethod + def __get_unique(vector: Tuple[float]): + unique = [] + for node in vector: + for knot in unique: + if abs(node - knot) < 1e-6: + break + else: + unique.append(node) + unique.sort() + return tuple(unique) + + @staticmethod + def __is_valid(vector: Tuple[float], degree: Union[int, None]): + try: + for knot in vector: + float(knot) + except TypeError: + return False + lenght = len(vector) + if lenght < 2: + return False + for i in range(lenght - 1): + if not vector[i] <= vector[i + 1]: + return False + if degree is None: + degree = 0 + while vector[degree] == vector[degree + 1]: + degree += 1 + npts = lenght - degree - 1 + if not degree < npts: + return False + knots = ImmutableKnotVector.__get_unique(vector[degree : npts + 1]) + for knot in knots: + mult = vector.count(knot) + if mult > degree + 1: + return False + if vector.count(vector[degree]) != vector.count(vector[npts]): + return False + return True + + def __new__(cls, knotvector: Tuple[float], degree: Optional[int] = None): + if isinstance(knotvector, ImmutableKnotVector): + return knotvector + try: + knotvector = tuple(knotvector) + except TypeError: + raise ValueError + if not cls.__is_valid(knotvector, degree): + msg = f"Invalid knot vector (deg {degree}): {knotvector}" + raise ValueError(msg) + if degree is None: + degree = 0 + while knotvector[degree] == knotvector[degree + 1]: + degree += 1 + instance = super(ImmutableKnotVector, cls).__new__(cls, tuple(knotvector)) + instance._ImmutableKnotVector__degree = degree + instance._ImmutableKnotVector__npts = len(knotvector) - degree - 1 + return instance + + def __or__(self, other: ImmutableKnotVector) -> ImmutableKnotVector: + other = ImmutableKnotVector(other) + if self.limits != other.limits: + raise ValueError + all_knots = list(self.knots) + list(other.knots) + all_knots = ImmutableKnotVector.__get_unique(all_knots) + all_mults = [0] * len(all_knots) + for vector in [self, other]: + for knot in vector: + index = all_knots.index(knot) + mult = vector.mult(knot) + if mult > all_mults[index]: + all_mults[index] = mult + final_vector = [] + for knot, mult in zip(all_knots, all_mults): + final_vector += [knot] * mult + final_vector = tuple(sorted(final_vector)) + return ImmutableKnotVector(final_vector) + + def __and__(self, other: ImmutableKnotVector) -> ImmutableKnotVector: + other = ImmutableKnotVector(other) + if self.limits != other.limits: + raise ValueError + all_knots = tuple(sorted(set(self.knots) & set(other.knots))) + all_mults = [float("inf")] * len(all_knots) + for vector in [self, other]: + for knot in vector: + if knot not in all_knots: + continue + index = all_knots.index(knot) + mult = vector.mult(knot) + if mult < all_mults[index]: + all_mults[index] = mult + final_vector = [] + for knot, mult in zip(all_knots, all_mults): + final_vector += [knot] * mult + return ImmutableKnotVector(sorted(final_vector)) + + def __add__(self, other): + raise ValueError + + def __sub__(self, other): + raise ValueError + + @property + def degree(self) -> int: + return self.__degree + + @property + def npts(self) -> int: + return self.__npts + + @property + def knots(self) -> Tuple[float]: + vector = self[self.degree : self.npts + 1] + return ImmutableKnotVector.__get_unique(vector) + + @property + def limits(self) -> Tuple[float]: + return (self[self.degree], self[self.npts]) + + def __span_single(self, node: float) -> int: + if node == self[self.npts]: # Special case + return self.npts - 1 + low, high = self.degree, self.npts + 1 # Do binary search + mid = (low + high) // 2 + while True: + if node < self[mid]: + high = mid + else: + low = mid + mid = (low + high) // 2 + if self[mid] <= node < self[mid + 1]: + return mid + + def __mult_single(self, node: Tuple[float]) -> Tuple[int]: + return sum(abs(node - knot) < 1e-9 for knot in self) + + def __valid_single(self, node: float) -> bool: + try: + float(node) # Verify if it's a number + except TypeError: + return False + umin, umax = self.limits + if node < umin or umax < node: + return False + return True + + def span(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: + if not self.valid(nodes): + raise ValueError + try: + return tuple(map(self.span, nodes)) + except TypeError: + return self.__span_single(nodes) + + def mult(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: + if not self.valid(nodes): + raise ValueError + try: + return tuple(map(self.mult, nodes)) + except TypeError: + return self.__mult_single(nodes) + + def valid(self, nodes: Tuple[float]) -> bool: + if isinstance(nodes, str): + return False + try: + for node in nodes: + if not self.valid(node): + return False + return True + except TypeError: + return self.__valid_single(nodes) + + def split(self, nodes: Tuple[float]) -> Tuple[ImmutableKnotVector]: + """ + It splits the knotvector at nodes. + You may put initial and final values, but they are ignored. + Example: + >> U = [0, 0, 0.5, 1, 1] + >> split(U, [0.5]) + [[0, 0, 0.5, 0.5], + [0.5, 0.5, 1, 1]] + >> split(U, [0.25]) + [[0, 0, 0.25, 0.25], + [0.25, 0.25, 0.5, 1, 1]] + >> split(U, [0, 0.25, 0.75]) + [[0, 0, 0.25, 0.25], + [0.25, 0.25, 0.5, 0.75, 0.75], + [0.75, 0.75, 1, 1]] + """ + if not self.valid(nodes): + raise ValueError + nodes = set(nodes) + if len(nodes) == 0: + return (self,) + nodes = tuple(sorted(nodes | set(self.limits))) + vector = np.array(self) + + retorno = [] + for a, b in zip(nodes[:-1], nodes[1:]): + middle = list(vector[(a < vector) * (vector < b)]) + newknotvect = (self.degree + 1) * [a] + middle + (self.degree + 1) * [b] + newknotvect = ImmutableKnotVector(newknotvect) + retorno.append(newknotvect) + return tuple(retorno) + + def increase(self, times: int) -> ImmutableKnotVector: + """Degree increase""" + vector = sorted(list(self) + times * list(self.knots)) + return self.__class__(vector, self.degree + times) + + def decrease(self, times: int) -> ImmutableKnotVector: + """Degree decrease""" + vector = list(self) + knots = self.knots[1:-1] + for _ in range(times): + vector.pop(0) + vector.pop(-1) + for node in knots: + vector.remove(node) + vector = sorted(vector) + return self.__class__(vector, self.degree - times) + + def remove(self, nodes: Tuple[float]) -> ImmutableKnotVector: + """Remove knots""" + vector = list(self) + for node in nodes: + vector.remove(node) + vector = sorted(vector) + return self.__class__(vector, self.degree) + + def insert(self, nodes: Tuple[float]) -> ImmutableKnotVector: + """Insert knots""" + vector = sorted(list(self) + list(nodes)) + return self.__class__(vector, self.degree) diff --git a/src/pynurbs/piecepoly.py b/src/pynurbs/core/piecepoly.py similarity index 100% rename from src/pynurbs/piecepoly.py rename to src/pynurbs/core/piecepoly.py diff --git a/src/pynurbs/polynomial.py b/src/pynurbs/core/polynomial.py similarity index 100% rename from src/pynurbs/polynomial.py rename to src/pynurbs/core/polynomial.py diff --git a/src/pynurbs/knotspace.py b/src/pynurbs/knotspace.py index af4a9ed..b8f0c99 100644 --- a/src/pynurbs/knotspace.py +++ b/src/pynurbs/knotspace.py @@ -14,245 +14,7 @@ from pynurbs.__classes__ import Intface_KnotVector - -class ImmutableKnotVector(tuple): - @staticmethod - def __get_unique(vector: Tuple[float]): - unique = [] - for node in vector: - for knot in unique: - if abs(node - knot) < 1e-6: - break - else: - unique.append(node) - unique.sort() - return tuple(unique) - - @staticmethod - def __is_valid(vector: Tuple[float], degree: Union[int, None]): - try: - for knot in vector: - float(knot) - except TypeError: - return False - lenght = len(vector) - if lenght < 2: - return False - for i in range(lenght - 1): - if not vector[i] <= vector[i + 1]: - return False - if degree is None: - degree = 0 - while vector[degree] == vector[degree + 1]: - degree += 1 - npts = lenght - degree - 1 - if not degree < npts: - return False - knots = ImmutableKnotVector.__get_unique(vector[degree : npts + 1]) - for knot in knots: - mult = vector.count(knot) - if mult > degree + 1: - return False - if vector.count(vector[degree]) != vector.count(vector[npts]): - return False - return True - - def __new__(cls, knotvector: Tuple[float], degree: Optional[int] = None): - if isinstance(knotvector, ImmutableKnotVector): - return knotvector - try: - knotvector = tuple(knotvector) - except TypeError: - raise ValueError - if not cls.__is_valid(knotvector, degree): - msg = f"Invalid knot vector (deg {degree}): {knotvector}" - raise ValueError(msg) - if degree is None: - degree = 0 - while knotvector[degree] == knotvector[degree + 1]: - degree += 1 - instance = super(ImmutableKnotVector, cls).__new__(cls, tuple(knotvector)) - instance._ImmutableKnotVector__degree = degree - instance._ImmutableKnotVector__npts = len(knotvector) - degree - 1 - return instance - - def __or__(self, other: ImmutableKnotVector) -> ImmutableKnotVector: - other = ImmutableKnotVector(other) - if self.limits != other.limits: - raise ValueError - all_knots = list(self.knots) + list(other.knots) - all_knots = ImmutableKnotVector.__get_unique(all_knots) - all_mults = [0] * len(all_knots) - for vector in [self, other]: - for knot in vector: - index = all_knots.index(knot) - mult = vector.mult(knot) - if mult > all_mults[index]: - all_mults[index] = mult - final_vector = [] - for knot, mult in zip(all_knots, all_mults): - final_vector += [knot] * mult - final_vector = tuple(sorted(final_vector)) - return ImmutableKnotVector(final_vector) - - def __and__(self, other: ImmutableKnotVector) -> ImmutableKnotVector: - other = ImmutableKnotVector(other) - if self.limits != other.limits: - raise ValueError - all_knots = tuple(sorted(set(self.knots) & set(other.knots))) - all_mults = [float("inf")] * len(all_knots) - for vector in [self, other]: - for knot in vector: - if knot not in all_knots: - continue - index = all_knots.index(knot) - mult = vector.mult(knot) - if mult < all_mults[index]: - all_mults[index] = mult - final_vector = [] - for knot, mult in zip(all_knots, all_mults): - final_vector += [knot] * mult - return ImmutableKnotVector(sorted(final_vector)) - - def __add__(self, other): - raise ValueError - - def __sub__(self, other): - raise ValueError - - @property - def degree(self) -> int: - return self.__degree - - @property - def npts(self) -> int: - return self.__npts - - @property - def knots(self) -> Tuple[float]: - vector = self[self.degree : self.npts + 1] - return ImmutableKnotVector.__get_unique(vector) - - @property - def limits(self) -> Tuple[float]: - return (self[self.degree], self[self.npts]) - - def __span_single(self, node: float) -> int: - if node == self[self.npts]: # Special case - return self.npts - 1 - low, high = self.degree, self.npts + 1 # Do binary search - mid = (low + high) // 2 - while True: - if node < self[mid]: - high = mid - else: - low = mid - mid = (low + high) // 2 - if self[mid] <= node < self[mid + 1]: - return mid - - def __mult_single(self, node: Tuple[float]) -> Tuple[int]: - return sum(abs(node - knot) < 1e-9 for knot in self) - - def __valid_single(self, node: float) -> bool: - try: - float(node) # Verify if it's a number - except TypeError: - return False - umin, umax = self.limits - if node < umin or umax < node: - return False - return True - - def span(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: - if not self.valid(nodes): - raise ValueError - try: - return tuple(map(self.span, nodes)) - except TypeError: - return self.__span_single(nodes) - - def mult(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: - if not self.valid(nodes): - raise ValueError - try: - return tuple(map(self.mult, nodes)) - except TypeError: - return self.__mult_single(nodes) - - def valid(self, nodes: Tuple[float]) -> bool: - if isinstance(nodes, str): - return False - try: - for node in nodes: - if not self.valid(node): - return False - return True - except TypeError: - return self.__valid_single(nodes) - - def split(self, nodes: Tuple[float]) -> Tuple[ImmutableKnotVector]: - """ - It splits the knotvector at nodes. - You may put initial and final values, but they are ignored. - Example: - >> U = [0, 0, 0.5, 1, 1] - >> split(U, [0.5]) - [[0, 0, 0.5, 0.5], - [0.5, 0.5, 1, 1]] - >> split(U, [0.25]) - [[0, 0, 0.25, 0.25], - [0.25, 0.25, 0.5, 1, 1]] - >> split(U, [0, 0.25, 0.75]) - [[0, 0, 0.25, 0.25], - [0.25, 0.25, 0.5, 0.75, 0.75], - [0.75, 0.75, 1, 1]] - """ - if not self.valid(nodes): - raise ValueError - nodes = set(nodes) - if len(nodes) == 0: - return (self,) - nodes = tuple(sorted(nodes | set(self.limits))) - vector = np.array(self) - - retorno = [] - for a, b in zip(nodes[:-1], nodes[1:]): - middle = list(vector[(a < vector) * (vector < b)]) - newknotvect = (self.degree + 1) * [a] + middle + (self.degree + 1) * [b] - newknotvect = ImmutableKnotVector(newknotvect) - retorno.append(newknotvect) - return tuple(retorno) - - def increase(self, times: int) -> ImmutableKnotVector: - """Degree increase""" - vector = sorted(list(self) + times * list(self.knots)) - return self.__class__(vector, self.degree + times) - - def decrease(self, times: int) -> ImmutableKnotVector: - """Degree decrease""" - vector = list(self) - knots = self.knots[1:-1] - for _ in range(times): - vector.pop(0) - vector.pop(-1) - for node in knots: - vector.remove(node) - vector = sorted(vector) - return self.__class__(vector, self.degree - times) - - def remove(self, nodes: Tuple[float]) -> ImmutableKnotVector: - """Remove knots""" - vector = list(self) - for node in nodes: - vector.remove(node) - vector = sorted(vector) - return self.__class__(vector, self.degree) - - def insert(self, nodes: Tuple[float]) -> ImmutableKnotVector: - """Insert knots""" - vector = sorted(list(self) + list(nodes)) - return self.__class__(vector, self.degree) +from .core import ImmutableKnotVector class KnotVector(Intface_KnotVector): diff --git a/tests/core/__init__.py b/tests/core/__init__.py new file mode 100644 index 0000000..04ddc64 --- /dev/null +++ b/tests/core/__init__.py @@ -0,0 +1,3 @@ +import sys + +sys.path.append("./src") diff --git a/tests/core/test_basis_function.py b/tests/core/test_basis_function.py new file mode 100644 index 0000000..e69de29 diff --git a/tests/core/test_knotvector.py b/tests/core/test_knotvector.py new file mode 100644 index 0000000..a230763 --- /dev/null +++ b/tests/core/test_knotvector.py @@ -0,0 +1,246 @@ +import numpy as np +import pytest + +from pynurbs.core import ImmutableKnotVector + + +@pytest.mark.order(1) +@pytest.mark.dependency() +def test_begin(): + pass + + +@pytest.mark.order(1) +@pytest.mark.timeout(2) +@pytest.mark.dependency(depends=["test_begin"]) +def test_Creation(): + """ + Tests if creates a ImmutableKnotVector correctly + """ + ImmutableKnotVector([0, 1]) + ImmutableKnotVector([0, 0, 1, 1]) + ImmutableKnotVector([0, 0, 0, 1, 1, 1]) + ImmutableKnotVector([0, 0, 0, 0, 1, 1, 1, 1]) + ImmutableKnotVector([0, 0, 0, 0, 0.5, 1, 1, 1, 1]) + + ImmutableKnotVector([0, 0.5, 1]) + ImmutableKnotVector([0, 0, 0.5, 0.5, 1, 1]) + ImmutableKnotVector([0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1]) + + ImmutableKnotVector([0, 4]) + ImmutableKnotVector([-4, 0]) + ImmutableKnotVector([0, 0, 4, 4]) + ImmutableKnotVector([-4, -4, 0, 0]) + + ImmutableKnotVector([0, 0, 0.25, 0.5, 0.75, 1, 1]) + ImmutableKnotVector([0, 0, 0.5, 0.5, 1, 1]) + ImmutableKnotVector([0.0, 0.0, 0.25, 0.5, 0.75, 1.0, 1.0]) + ImmutableKnotVector([0.0, 0.0, 0.5, 0.5, 1.0, 1.0]) + + +@pytest.mark.order(1) +@pytest.mark.timeout(2) +@pytest.mark.dependency(depends=["test_Creation"]) +def test_FailCreation(): + """ + Test some invalid creation cases, which should raise error + """ + with pytest.raises(ValueError): + ImmutableKnotVector(-1) + with pytest.raises(ValueError): + ImmutableKnotVector({1: 1}) + with pytest.raises(ValueError): + ImmutableKnotVector(["asd", {1.1: 1}]) + + with pytest.raises(ValueError): + ImmutableKnotVector([0, 0, 0, 1, 1]) + with pytest.raises(ValueError): + ImmutableKnotVector([0, 0, 1, 1, 1]) + with pytest.raises(ValueError): + ImmutableKnotVector([0, 0, 0, 0, 1, 1, 1]) + with pytest.raises(ValueError): + ImmutableKnotVector([0, 0, 0, 1, 1, 1, 1]) + with pytest.raises(ValueError): + ImmutableKnotVector([0, 0, 0.7, 0.2, 1, 1]) + with pytest.raises(ValueError): + ImmutableKnotVector([[0, 0, 0.2, 0.7, 1, 1], [0, 0, 0.2, 0.7, 1, 1]]) + with pytest.raises(ValueError): + ImmutableKnotVector([[0, 0, 0.7, 0.2, 1, 1], [0, 0, 0.7, 0.2, 1, 1]]) + + # Internal multiplicity error + with pytest.raises(ValueError): + ImmutableKnotVector([0, 0, 0, 0.5, 0.5, 0.5, 0.5, 1, 1, 1]) + with pytest.raises(ValueError): + ImmutableKnotVector([0, 0, 0.5, 0.5, 0.5, 0.5, 1, 1]) + + +@pytest.mark.order(1) +@pytest.mark.timeout(2) +@pytest.mark.dependency(depends=["test_Creation", "test_FailCreation"]) +def test_ValuesDegree(): + V = ImmutableKnotVector([0, 0, 1, 1]) + assert V.degree == 1 + V = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) + assert V.degree == 2 + V = ImmutableKnotVector([0, 0, 0, 0, 1, 1, 1, 1]) + assert V.degree == 3 + + V = ImmutableKnotVector([0, 0, 0.5, 1, 1]) + assert V.degree == 1 + V = ImmutableKnotVector([0, 0, 0.2, 0.6, 1, 1]) + assert V.degree == 1 + V = ImmutableKnotVector([0, 0, 0, 0.5, 1, 1, 1]) + assert V.degree == 2 + V = ImmutableKnotVector([0, 0, 0, 0.2, 0.6, 1, 1, 1]) + assert V.degree == 2 + V = ImmutableKnotVector([0, 0, 0, 0, 0.5, 1, 1, 1, 1]) + assert V.degree == 3 + V = ImmutableKnotVector([0, 0, 0, 0, 0.2, 0.6, 1, 1, 1, 1]) + assert V.degree == 3 + + +@pytest.mark.order(1) +@pytest.mark.timeout(2) +@pytest.mark.dependency(depends=["test_Creation", "test_FailCreation"]) +def test_ValuesNumberPoints(): + V = ImmutableKnotVector([0, 0, 1, 1]) + assert V.npts == 2 + V = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) + assert V.npts == 3 + V = ImmutableKnotVector([0, 0, 0, 0, 1, 1, 1, 1]) + assert V.npts == 4 + V = ImmutableKnotVector([0, 0, 0, 0, 0, 1, 1, 1, 1, 1]) + assert V.npts == 5 + + V = ImmutableKnotVector([0, 0, 0.5, 1, 1]) + assert V.npts == 3 + V = ImmutableKnotVector([0, 0, 0.2, 0.6, 1, 1]) + assert V.npts == 4 + V = ImmutableKnotVector([0, 0, 0, 0.5, 1, 1, 1]) + assert V.npts == 4 + V = ImmutableKnotVector([0, 0, 0, 0.2, 0.6, 1, 1, 1]) + assert V.npts == 5 + V = ImmutableKnotVector([0, 0, 0, 0, 0.5, 1, 1, 1, 1]) + assert V.npts == 5 + V = ImmutableKnotVector([0, 0, 0, 0, 0.2, 0.6, 1, 1, 1, 1]) + assert V.npts == 6 + + +@pytest.mark.order(1) +@pytest.mark.timeout(2) +@pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) +def test_findspans_single(): + U = ImmutableKnotVector([0, 0, 0.2, 0.4, 0.5, 0.6, 0.8, 1, 1]) + assert U.degree == 1 + assert U.npts == 7 + assert U.span(0) == 1 + assert U.span(0.1) == 1 + assert U.span(0.2) == 2 + assert U.span(0.3) == 2 + assert U.span(0.4) == 3 + assert U.span(0.5) == 4 + assert U.span(0.6) == 5 + assert U.span(0.7) == 5 + assert U.span(0.8) == 6 + assert U.span(0.9) == 6 + assert U.span(1.0) == 6 + + with pytest.raises(ValueError): + U.span(-0.1) # Outside interval + with pytest.raises(ValueError): + U.span(1.1) # Outside interval + with pytest.raises(ValueError): + U.span("asd") # Not a number + + +@pytest.mark.order(1) +@pytest.mark.timeout(2) +@pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) +def test_findmult_single(): + U = ImmutableKnotVector([0, 0, 0.2, 0.4, 0.5, 0.6, 0.8, 1, 1]) + assert U.degree == 1 + assert U.npts == 7 + assert U.mult(0) == 2 + assert U.mult(0.1) == 0 + assert U.mult(0.2) == 1 + assert U.mult(0.3) == 0 + assert U.mult(0.4) == 1 + assert U.mult(0.5) == 1 + assert U.mult(0.6) == 1 + assert U.mult(0.7) == 0 + assert U.mult(0.8) == 1 + assert U.mult(0.9) == 0 + assert U.mult(1.0) == 2 + + with pytest.raises(ValueError): + U.mult(-0.1) # Outside interval + with pytest.raises(ValueError): + U.mult(1.1) # Outside interval + with pytest.raises(ValueError): + U.mult("asd") # Not a number + + +@pytest.mark.order(1) +@pytest.mark.timeout(2) +@pytest.mark.dependency(depends=["test_findspans_single"]) +def test_findspans_array(): + U = ImmutableKnotVector([0, 0, 0.2, 0.4, 0.5, 0.6, 0.8, 1, 1]) + array = np.linspace(0, 1, 11) # (0, 0.1, 0.2, ..., 0.9, 1.0) + suposedspans = U.span(array) + correctspans = [1, 1, 2, 2, 3, 4, 5, 5, 6, 6, 6] + assert U.degree == 1 + assert U.npts == 7 + np.testing.assert_equal(suposedspans, correctspans) + + +@pytest.mark.order(1) +@pytest.mark.timeout(2) +@pytest.mark.dependency(depends=["test_findmult_single"]) +def test_findmult_array(): + U = ImmutableKnotVector([0, 0, 0.2, 0.4, 0.5, 0.6, 0.8, 1, 1]) + array = np.linspace(0, 1, 11) # (0, 0.1, 0.2, ..., 0.9, 1.0) + suposedmults = U.mult(array) + correctmults = [2, 0, 1, 0, 1, 1, 1, 0, 1, 0, 2] + assert U.degree == 1 + assert U.npts == 7 + np.testing.assert_equal(suposedmults, correctmults) + + +@pytest.mark.order(1) +@pytest.mark.timeout(4) +@pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) +def test_CompareImmutableKnotVector(): + U1 = ImmutableKnotVector([0, 0, 1, 1]) + U2 = ImmutableKnotVector([0, 0, 1, 1]) + assert U1 == U2 + assert U1 == (0, 0, 1, 1) + + U3 = ImmutableKnotVector([0, 0, 0.5, 1, 1]) + assert U1 != U3 + + assert U1 != 0 + assert U1 != "asad" + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_Creation", + "test_FailCreation", + "test_ValuesDegree", + "test_ValuesNumberPoints", + "test_findspans_single", + "test_findmult_single", + "test_findspans_array", + "test_findmult_array", + "test_compare_ImmutableKnotVectors_fail", + "test_insert_knot_remove", + "test_degree_change", + "test_or_and", + "test_others", + "test_fractions", + ] +) +def test_end(): + pass diff --git a/tests/test_polynomial.py b/tests/core/test_polynomial.py similarity index 99% rename from tests/test_polynomial.py rename to tests/core/test_polynomial.py index 9f9baa5..cc1c2dc 100644 --- a/tests/test_polynomial.py +++ b/tests/core/test_polynomial.py @@ -1,6 +1,6 @@ import pytest -from pynurbs.polynomial import Polynomial, derivate, scale, shift +from pynurbs.core.polynomial import Polynomial, derivate, scale, shift @pytest.mark.order(1) From 007c77e6f993bbe8508e13eae4aaec8003e173c9 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 27 Jun 2025 21:41:41 +0200 Subject: [PATCH 017/116] fix: evaluation of core basis functions --- src/pynurbs/core/basisfunction.py | 18 +- tests/core/test_basis_function.py | 315 ++++++++++++++++++++++++++++++ tests/core/test_knotvector.py | 6 - 3 files changed, 332 insertions(+), 7 deletions(-) diff --git a/src/pynurbs/core/basisfunction.py b/src/pynurbs/core/basisfunction.py index 859f1f3..f25ef43 100644 --- a/src/pynurbs/core/basisfunction.py +++ b/src/pynurbs/core/basisfunction.py @@ -24,7 +24,8 @@ def spectral_matrix( - m is the number of segments: len(knots)-1 - j is the requested degree """ - knotvector = ImmutableKnotVector(knotvector) + if not isinstance(knotvector, ImmutableKnotVector): + raise TypeError if not isinstance(reqdegree, int): raise TypeError("reqdegree must be integer") if reqdegree < 0 or knotvector.degree < reqdegree: @@ -66,13 +67,25 @@ class ImmutableBasisFunction: def __init__( self, knotvector: ImmutableKnotVector, degree: Union[int, None] = None ): + if not isinstance(knotvector, ImmutableKnotVector): + raise TypeError degree = degree or knotvector.degree self.__matrix = tuple( tuple(tuple(Polynomial(coefs) for coefs in all_coefs)) for all_coefs in spectral_matrix(knotvector, degree) ) + self.__degree = degree + self.__npts = knotvector.npts self.__knotvector = knotvector + @property + def degree(self) -> int: + return self.__degree + + @property + def npts(self) -> int: + return self.__npts + @property def knots(self) -> Tuple[Real, ...]: return self.__knotvector.knots @@ -98,3 +111,6 @@ def eval(self, node: Real, times: int = 0) -> Tuple[Real, ...]: polynomial = self.__matrix[ind][y] result[i] = polynomial.eval(shifnode, times) return tuple(result) + + def __call__(self, node: Real) -> Tuple[Real, ...]: + return self.eval(node, 0) diff --git a/tests/core/test_basis_function.py b/tests/core/test_basis_function.py index e69de29..64c0e9e 100644 --- a/tests/core/test_basis_function.py +++ b/tests/core/test_basis_function.py @@ -0,0 +1,315 @@ +from copy import copy +from fractions import Fraction + +import numpy as np +import pytest + +from pynurbs.core.basisfunction import ImmutableBasisFunction +from pynurbs.core.knotvector import ImmutableKnotVector +from pynurbs.knotspace import GeneratorKnotVector + + +def binom(n: int, i: int): + """ + Returns binomial (n, i) + """ + assert isinstance(n, int) + assert isinstance(i, int) + prod = 1 + if i <= 0 or i >= n: + return 1 + for j in range(i): + prod *= (n - j) / (i - j) + return int(prod) + + +@pytest.mark.order(3) +@pytest.mark.dependency( + depends=[ + "tests/core/test_knotvector.py::test_end", + "tests/core/test_polynomial.py::test_all", + ], + scope="session", +) +def test_begin(): + pass + + +class TestBezier: + @pytest.mark.order(3) + @pytest.mark.dependency(depends=["test_begin"]) + def test_begin(self): + pass + + @pytest.mark.order(3) + @pytest.mark.timeout(1) + @pytest.mark.dependency(depends=["TestBezier::test_begin"]) + def test_creation(self): + knotvector = ImmutableKnotVector([0, 0, 1, 1]) + bezier = ImmutableBasisFunction(knotvector) + assert callable(bezier) + assert bezier.degree == 1 + assert bezier.npts == 2 + + knotvector = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) + bezier = ImmutableBasisFunction(knotvector) + assert callable(bezier) + assert bezier.degree == 2 + assert bezier.npts == 3 + + knotvector = ImmutableKnotVector([0, 0, 0, 0, 1, 1, 1, 1]) + bezier = ImmutableBasisFunction(knotvector) + assert callable(bezier) + assert bezier.degree == 3 + assert bezier.npts == 4 + + for degree in range(0, 8): + npts = degree + 1 + knotvector = ImmutableKnotVector([0] * npts + [1] * npts) + bezier = ImmutableBasisFunction(knotvector) + assert callable(bezier) + assert bezier.degree == degree + assert bezier.npts == npts + + @pytest.mark.order(3) + @pytest.mark.timeout(5) + @pytest.mark.dependency(depends=["TestBezier::test_creation"]) + def test_sum_equal_to_1(self): + knotvector = ImmutableKnotVector([0, 0, 1, 1]) + bezier = ImmutableBasisFunction(knotvector) + assert sum(bezier(0.00)) == 1 + assert sum(bezier(0.25)) == 1 + assert sum(bezier(0.50)) == 1 + assert sum(bezier(0.75)) == 1 + assert sum(bezier(1.00)) == 1 + + knotvector = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) + bezier = ImmutableBasisFunction(knotvector) + assert sum(bezier(0.00)) == 1 + assert sum(bezier(0.25)) == 1 + assert sum(bezier(0.50)) == 1 + assert sum(bezier(0.75)) == 1 + assert sum(bezier(1.00)) == 1 + + knotvector = ImmutableKnotVector([0, 0, 0, 0, 1, 1, 1, 1]) + bezier = ImmutableBasisFunction(knotvector) + assert sum(bezier(0.00)) == 1 + assert sum(bezier(0.25)) == 1 + assert sum(bezier(0.50)) == 1 + assert sum(bezier(0.75)) == 1 + assert sum(bezier(1.00)) == 1 + + divisions = 32 + for degree in range(0, 6): + npts = degree + 1 + knotvector = ImmutableKnotVector([0] * npts + [1] * npts) + bezier = ImmutableBasisFunction(knotvector) + assert bezier.degree == degree + assert bezier.npts == npts + + for i in range(divisions + 1): + results = bezier(i / divisions) + assert len(results) == npts + assert all(result >= 0 for result in results) + assert sum(results) == 1 + + @pytest.mark.order(3) + @pytest.mark.timeout(5) + @pytest.mark.dependency( + depends=[ + "TestBezier::test_creation", + "TestBezier::test_sum_equal_to_1", + ] + ) + def test_single_values(self): + # degree = 1, npts = 2 + knotvector = ImmutableKnotVector([0, 0, 1, 1]) + bezier = ImmutableBasisFunction(knotvector) + assert bezier(0) == (1, 0) + assert bezier(0.5) == (0.5, 0.5) + assert bezier(1) == (0, 1) + + # degree = 2, npts = 3 + knotvector = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) + bezier = ImmutableBasisFunction(knotvector) + assert bezier(0) == (1, 0, 0) + assert bezier(0.5) == (0.25, 0.5, 0.25) + assert bezier(1) == (0, 0, 1) + + # degree = 3, npts = 3 + knotvector = ImmutableKnotVector([0, 0, 0, 0, 1, 1, 1, 1]) + bezier = ImmutableBasisFunction(knotvector) + assert bezier(0) == (1, 0, 0, 0) + assert bezier(0.25) == (27 / 64, 27 / 64, 9 / 64, 1 / 64) + assert bezier(0.5) == (1 / 8, 3 / 8, 3 / 8, 1 / 8) + assert bezier(0.75) == (1 / 64, 9 / 64, 27 / 64, 27 / 64) + assert bezier(1) == (0, 0, 0, 1) + + divisions = 8 + for degree in range(0, 6): + npts = degree + 1 + knotvector = [Fraction(0)] * npts + [Fraction(1)] * npts + knotvector = ImmutableKnotVector(knotvector) + bezier = ImmutableBasisFunction(knotvector) + for j in range(divisions + 1): + node = Fraction(j, divisions) + minu = 1 - node + goods = ( + binom(degree, i) * minu ** (degree - i) * node**i + for i in range(degree + 1) + ) + assert bezier(node) == tuple(goods) + + @pytest.mark.order(3) + @pytest.mark.dependency( + depends=[ + "TestBezier::test_begin", + "TestBezier::test_creation", + "TestBezier::test_sum_equal_to_1", + "TestBezier::test_single_values", + ] + ) + def test_all(self): + pass + + +class TestSpline: + @pytest.mark.order(3) + @pytest.mark.dependency(depends=["TestBezier::test_all"]) + def test_begin(self): + pass + + @pytest.mark.order(3) + @pytest.mark.timeout(1) + @pytest.mark.dependency(depends=["TestSpline::test_begin"]) + def test_creation(self): + knotvector = ImmutableKnotVector([0, 0, 1, 1]) + spline = ImmutableBasisFunction(knotvector) + assert callable(spline) + assert spline.degree == 1 + assert spline.npts == 2 + + knotvector = ImmutableKnotVector([0, 0, 0.5, 1, 1]) + spline = ImmutableBasisFunction(knotvector) + assert callable(spline) + assert spline.degree == 1 + assert spline.npts == 3 + + knotvector = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) + spline = ImmutableBasisFunction(knotvector) + assert callable(spline) + assert spline.degree == 2 + assert spline.npts == 3 + + knotvector = ImmutableKnotVector([0, 0, 0, 0.5, 1, 1, 1]) + spline = ImmutableBasisFunction(knotvector) + assert callable(spline) + assert spline.degree == 2 + assert spline.npts == 4 + + @pytest.mark.order(3) + @pytest.mark.timeout(5) + @pytest.mark.dependency(depends=["TestSpline::test_creation"]) + def test_tablevalues_degree1npts3(self): + knotvector = ImmutableKnotVector([0, 0, 0.5, 1, 1]) + spline = ImmutableBasisFunction(knotvector) + assert spline.degree == 1 + assert spline.npts == 3 + + nodes_test = np.linspace(0, 1, 11) + + matrix_good = [ + [1.0, 0.0, 0.0], + [0.8, 0.2, 0.0], + [0.6, 0.4, 0.0], + [0.4, 0.6, 0.0], + [0.2, 0.8, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.8, 0.2], + [0.0, 0.6, 0.4], + [0.0, 0.4, 0.6], + [0.0, 0.2, 0.8], + [0.0, 0.0, 1.0], + ] + for node, good in zip(nodes_test, matrix_good): + np.testing.assert_allclose(spline(node), good) + + @pytest.mark.order(3) + @pytest.mark.timeout(5) + @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree1npts3"]) + def test_tablevalues_degree2npts4(self): + knotvector = [0, 0, 0, 0.5, 1, 1, 1] + knotvector = ImmutableKnotVector(knotvector) + spline = ImmutableBasisFunction(knotvector) + assert spline.degree == 2 + assert spline.npts == 4 + nodes_test = np.linspace(0, 1, 11) + + matrix_good = [ + [1.0, 0.0, 0.0, 0.0], + [0.64, 0.34, 0.02, 0.0], + [0.36, 0.56, 0.08, 0.0], + [0.16, 0.66, 0.18, 0.0], + [0.04, 0.64, 0.32, 0.0], + [0.0, 0.5, 0.5, 0.0], + [0.0, 0.32, 0.64, 0.04], + [0.0, 0.18, 0.66, 0.16], + [0.0, 0.08, 0.56, 0.36], + [0.0, 0.02, 0.34, 0.64], + [0.0, 0.0, 0.0, 1.0], + ] + + for node, good in zip(nodes_test, matrix_good): + np.testing.assert_allclose(spline(node), good) + + @pytest.mark.order(3) + @pytest.mark.timeout(5) + @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree2npts4"]) + def test_tablevalues_degree3npts5(self): + knotvector = [0, 0, 0, 0, 0.5, 1, 1, 1, 1] + knotvector = ImmutableKnotVector(knotvector) + spline = ImmutableBasisFunction(knotvector) + assert spline.degree == 3 + assert spline.npts == 5 + nodes_test = np.linspace(0, 1, 11) + + matrix_good = [ + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.512, 0.434, 0.052, 0.002, 0.0], + [0.216, 0.592, 0.176, 0.016, 0.0], + [0.064, 0.558, 0.324, 0.054, 0.0], + [0.008, 0.416, 0.448, 0.128, 0.0], + [0.0, 0.25, 0.5, 0.25, 0.0], + [0.0, 0.128, 0.448, 0.416, 0.008], + [0.0, 0.054, 0.324, 0.558, 0.064], + [0.0, 0.016, 0.176, 0.592, 0.216], + [0.0, 0.002, 0.052, 0.434, 0.512], + [0.0, 0.0, 0.0, 0.0, 1.0], + ] + for node, good in zip(nodes_test, matrix_good): + np.testing.assert_allclose(spline(node), good) + + @pytest.mark.order(3) + @pytest.mark.dependency( + depends=[ + "TestSpline::test_begin", + "TestSpline::test_creation", + "TestSpline::test_tablevalues_degree1npts3", + "TestSpline::test_tablevalues_degree2npts4", + "TestSpline::test_tablevalues_degree3npts5", + ] + ) + def test_all(self): + pass + + +@pytest.mark.order(3) +@pytest.mark.dependency( + depends=[ + "test_begin", + "TestBezier::test_all", + "TestSpline::test_all", + ] +) +def test_all(): + pass diff --git a/tests/core/test_knotvector.py b/tests/core/test_knotvector.py index a230763..0343978 100644 --- a/tests/core/test_knotvector.py +++ b/tests/core/test_knotvector.py @@ -234,12 +234,6 @@ def test_CompareImmutableKnotVector(): "test_findmult_single", "test_findspans_array", "test_findmult_array", - "test_compare_ImmutableKnotVectors_fail", - "test_insert_knot_remove", - "test_degree_change", - "test_or_and", - "test_others", - "test_fractions", ] ) def test_end(): From 4df3f0294b7e7743b765ed95fa97e4e81445cf3f Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 27 Jun 2025 22:17:06 +0200 Subject: [PATCH 018/116] fix: evaluation of spline and rational splines values --- src/pynurbs/core/knotvector.py | 4 +++- src/pynurbs/curves.py | 15 ++++++------ src/pynurbs/functions.py | 4 ++-- src/pynurbs/heavy.py | 44 +++++++++++++++++++++++++++------- 4 files changed, 47 insertions(+), 20 deletions(-) diff --git a/src/pynurbs/core/knotvector.py b/src/pynurbs/core/knotvector.py index b457967..80f182d 100644 --- a/src/pynurbs/core/knotvector.py +++ b/src/pynurbs/core/knotvector.py @@ -2,6 +2,8 @@ from typing import Optional, Tuple, Union +import numpy as np + class ImmutableKnotVector(tuple): @staticmethod @@ -202,7 +204,7 @@ def split(self, nodes: Tuple[float]) -> Tuple[ImmutableKnotVector]: if len(nodes) == 0: return (self,) nodes = tuple(sorted(nodes | set(self.limits))) - vector = np.array(self) + vector = np.array(tuple(self)) retorno = [] for a, b in zip(nodes[:-1], nodes[1:]): diff --git a/src/pynurbs/curves.py b/src/pynurbs/curves.py index 4907728..a9bd049 100644 --- a/src/pynurbs/curves.py +++ b/src/pynurbs/curves.py @@ -10,6 +10,8 @@ from pynurbs.__classes__ import Intface_BaseCurve from pynurbs.knotspace import KnotVector +from .core.basisfunction import ImmutableBasisFunction + def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: """ @@ -598,14 +600,11 @@ def __eval(self, nodes: Tuple[float]) -> Tuple[Any]: """ vector = self.knotvector.internal nodes = tuple(nodes) - degree = int(self.knotvector.degree) - if self.weights is None: - eval = heavy.eval_spline_nodes - matrix = eval(vector, nodes, degree) - else: - eval = heavy.eval_rational_nodes - weights = tuple(self.weights) - matrix = eval(vector, weights, nodes, degree) + basis = ImmutableBasisFunction(vector) + matrix = np.transpose(tuple(map(basis, nodes))) + if self.weights is not None: + denominators = 1 / np.dot(self.weights, matrix) + matrix = np.einsum("j,ij,i->ij", denominators, matrix, self.weights) result = np.moveaxis(matrix, 0, -1) @ self.ctrlpoints return tuple(result) diff --git a/src/pynurbs/functions.py b/src/pynurbs/functions.py index 3750282..0a1daac 100644 --- a/src/pynurbs/functions.py +++ b/src/pynurbs/functions.py @@ -5,8 +5,8 @@ import numpy as np -from pynurbs import heavy from pynurbs.__classes__ import Intface_BaseFunction, Intface_Evaluator +from pynurbs.core.basisfunction import spectral_matrix from pynurbs.knotspace import KnotVector @@ -180,7 +180,7 @@ def __init__(self, func: BaseFunction, i: Union[int, slice], j: int): self.__weights = func.weights self.__first_index = i self.__second_index = j - self.__matrix = heavy.BasisFunction.speval_matrix(vector.internal, j) + self.__matrix = spectral_matrix(vector.internal, j) self.__knots = vector.knots self.__spans = vector.span(vector.knots) diff --git a/src/pynurbs/heavy.py b/src/pynurbs/heavy.py index 027a4ba..317750e 100644 --- a/src/pynurbs/heavy.py +++ b/src/pynurbs/heavy.py @@ -12,7 +12,8 @@ import numpy as np from .cmath import IntegratorArray, Linalg, NodeSample, number_type, totuple -from .knotspace import ImmutableKnotVector +from .core.basisfunction import ImmutableBasisFunction +from .core.knotvector import ImmutableKnotVector def find_roots( @@ -98,6 +99,33 @@ def find_roots( return tuple(sorted(filtered_roots)) +def eval_spline_nodes( + knotvector: ImmutableKnotVector, nodes: Tuple[float], degree: int +) -> Tuple[Tuple[float]]: + """ + Returns a matrix M of which M_{ij} = N_{i,degree}(node_j) + M.shape = (npts, len(nodes)) + """ + knotvector = ImmutableKnotVector(knotvector) + basis = ImmutableBasisFunction(knotvector, degree) + return np.transpose(tuple(map(basis, nodes))) + + +def eval_rational_nodes( + knotvector: ImmutableKnotVector, + weights: Tuple[float], + nodes: Tuple[float], + degree: int, +) -> Tuple[Tuple[float]]: + """ + Returns a matrix M of which M_{ij} = N_{i,p}(node_j) + M.shape = (len(weights), len(nodes)) + """ + matrix = eval_spline_nodes(knotvector, nodes, degree) + denominators = 1 / np.dot(weights, matrix) + return np.einsum("j,ij,i->ij", denominators, matrix, weights) + + class LeastSquare: """ Given two hypotetic curves C0 and C1, which are associated @@ -140,14 +168,12 @@ def fit_function( [P] = [M] * [f(nodes)] """ knotvector = ImmutableKnotVector(knotvector) - npts = knotvector.npts - degree = knotvector.degree - assert len(nodes) >= npts - if weights is None: - funcvals = eval_spline_nodes(knotvector, nodes, degree) - else: - funcvals = eval_rational_nodes(knotvector, weights, nodes, degree) - return Linalg.lstsq(np.transpose(funcvals)) + basis = ImmutableBasisFunction(knotvector) + matrix = np.transpose(tuple(map(basis, nodes))) + if weights is not None: + denominators = 1 / np.dot(weights, matrix) + matrix = np.einsum("j,ij->ij", denominators, matrix) + return Linalg.lstsq(np.transpose(matrix)) @staticmethod def spline2spline( From d8e71c91e0f17773dc58d8f6be3f1166edcd3b02 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 27 Jun 2025 23:53:38 +0200 Subject: [PATCH 019/116] feat: implement division of polynomials --- src/pynurbs/core/polynomial.py | 3 +- src/pynurbs/core/roots.py | 34 ++++++++++++++++++++ tests/core/test_roots.py | 57 ++++++++++++++++++++++++++++++++++ 3 files changed, 93 insertions(+), 1 deletion(-) create mode 100644 src/pynurbs/core/roots.py create mode 100644 tests/core/test_roots.py diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index c9b0978..9146eab 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -35,7 +35,8 @@ def __init__(self, coefs: Iterable[Real]): coefs = tuple(coefs) if len(coefs) == 0: raise ValueError("Cannot receive an empty tuple") - self.__coefs = tuple(coefs) + degree = max((i for i, v in enumerate(coefs) if v), default=0) + self.__coefs = tuple(coefs[: degree + 1]) @property def degree(self) -> int: diff --git a/src/pynurbs/core/roots.py b/src/pynurbs/core/roots.py new file mode 100644 index 0000000..46c434f --- /dev/null +++ b/src/pynurbs/core/roots.py @@ -0,0 +1,34 @@ +""" +Finds the roots of polynomials +""" + +from typing import Tuple + +from .polynomial import Polynomial + + +def division(poly: Polynomial, doly: Polynomial) -> Tuple[Polynomial, Polynomial]: + """ + Given the polynomials poly and doly, finds qoly and roly such: + + poly = doly * qoly + roly + + with: + * degree(qoly) = degree(poly) - degree(doly) + * degree(roly) < degree(doly) + """ + if not isinstance(poly, Polynomial) or not isinstance(doly, Polynomial): + raise TypeError + if doly.degree > poly.degree: + return Polynomial([0]), poly + if doly.degree == 0: + return Polynomial([coef / doly[0] for coef in poly]), Polynomial([0]) + qoly = Polynomial([0]) + roly = Polynomial(poly) + index = poly.degree + while index >= doly.degree: + const = roly[index] / doly[doly.degree] + qoly += Polynomial([0] * (index - doly.degree) + [const]) + roly = poly - doly * qoly + index -= 1 + return qoly, Polynomial(roly[: doly.degree]) diff --git a/tests/core/test_roots.py b/tests/core/test_roots.py new file mode 100644 index 0000000..0362810 --- /dev/null +++ b/tests/core/test_roots.py @@ -0,0 +1,57 @@ +import pytest + +from pynurbs.core.polynomial import Polynomial +from pynurbs.core.roots import division + + +@pytest.mark.order(3) +@pytest.mark.dependency( + depends=[ + "tests/core/test_polynomial.py::test_all", + ], + scope="session", +) +def test_division(): + poly = Polynomial([0, 1]) + doly = Polynomial([1]) + + qoly, roly = division(poly, doly) + assert qoly == poly + assert roly == 0 + + qoly, roly = division(doly, poly) + assert qoly == 0 + assert roly == 1 + + all_numerators = ( + (3,), + (9,), + (0, 1), + (0, -2), + (-6, 8, -1, 2), + (-2, 5, -3, 5, 5), + (7, 3, -5, -8, 3), + (7, -9, 2, 0, -2, -5), + (-1, -9, 10), + ) + polys = tuple(map(Polynomial, all_numerators)) + + all_denominators = ( + (7, 10, -3), + (2, -7, -10), + (-9, -7, -3), + (10, -6, 9, 7), + (-1, -8, -10, 6), + (-1, -10, 5, 9, 8), + (9, -1, -2, 7, 5), + (2, 6, -3, -7, -5), + (0, 8, 10, 1, -4, -5), + (5, -10, 4, 7, 2, -9), + ) + dolys = tuple(map(Polynomial, all_denominators)) + + for poly in polys: + for doly in dolys: + qoly, roly = division(poly, doly) + diff = doly * qoly + roly - poly + assert all(abs(coef) < 1e-9 for coef in diff) From 8f09ae59419130834c0694431082f2620671bf53 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 28 Jun 2025 09:32:04 +0200 Subject: [PATCH 020/116] feat: add integration of polynomials --- src/pynurbs/core/polynomial.py | 20 +++++++++++++++++++- tests/core/test_polynomial.py | 18 +++++++++++++++++- 2 files changed, 36 insertions(+), 2 deletions(-) diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 9146eab..033503f 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -7,7 +7,7 @@ import math from numbers import Real -from typing import Iterable, List, Union +from typing import Iterable, List, Tuple, Union class Polynomial: @@ -233,3 +233,21 @@ def derivate(polynomial: Polynomial, times: int = 1) -> Polynomial: for n, coef in enumerate(polynomial[times:]) ) return Polynomial(coefs) + + +def integrate(polynomial: Polynomial, domain: Tuple[Real, Real]) -> Real: + """ + Computes the definite integral of a polynomial + + Example + ------- + >>> poly = Polynomial([1, 2, 5]) + >>> print(poly) + 1 + 2 * x + 5 * x^2 + >>> integrate(poly, (-2, 1)) + 15 + """ + return sum( + coef * (domain[1] ** (n + 1) - domain[0] ** (n + 1)) / (n + 1) + for n, coef in enumerate(polynomial) + ) diff --git a/tests/core/test_polynomial.py b/tests/core/test_polynomial.py index cc1c2dc..675ba16 100644 --- a/tests/core/test_polynomial.py +++ b/tests/core/test_polynomial.py @@ -1,6 +1,6 @@ import pytest -from pynurbs.core.polynomial import Polynomial, derivate, scale, shift +from pynurbs.core.polynomial import Polynomial, derivate, integrate, scale, shift @pytest.mark.order(1) @@ -223,6 +223,21 @@ def test_derivate(): assert derivate(poly, 3) == Polynomial([6, 24]) +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] +) +def test_integrate(): + poly = Polynomial([1]) + assert integrate(poly, (-3, 4)) == 7 + + poly = Polynomial([1, -2]) + assert integrate(poly, (-3, 3)) == 6 + + poly = Polynomial([1, 2, 5]) + assert integrate(poly, (-2, 1)) == 15 + + @pytest.mark.order(1) @pytest.mark.dependency( depends=[ @@ -325,6 +340,7 @@ def test_print(): "test_truediv", "test_pow", "test_derivate", + "test_integrate", "test_shift", "test_scale", ] From 0381aa6829f29c91df3df0630677be4135e8cdc7 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 28 Jun 2025 11:22:34 +0200 Subject: [PATCH 021/116] refactor: move operations on knotvector to `core/operations.py` file --- src/pynurbs/core/knotvector.py | 101 ------------------ src/pynurbs/core/operations.py | 185 +++++++++++++++++++++++++++++++++ src/pynurbs/curves.py | 14 ++- src/pynurbs/heavy.py | 35 ++++--- src/pynurbs/knotspace.py | 23 ++-- tests/core/test_operations.py | 64 ++++++++++++ 6 files changed, 296 insertions(+), 126 deletions(-) create mode 100644 src/pynurbs/core/operations.py create mode 100644 tests/core/test_operations.py diff --git a/src/pynurbs/core/knotvector.py b/src/pynurbs/core/knotvector.py index 80f182d..453b779 100644 --- a/src/pynurbs/core/knotvector.py +++ b/src/pynurbs/core/knotvector.py @@ -66,44 +66,6 @@ def __new__(cls, knotvector: Tuple[float], degree: Optional[int] = None): instance._ImmutableKnotVector__npts = len(knotvector) - degree - 1 return instance - def __or__(self, other: ImmutableKnotVector) -> ImmutableKnotVector: - other = ImmutableKnotVector(other) - if self.limits != other.limits: - raise ValueError - all_knots = list(self.knots) + list(other.knots) - all_knots = ImmutableKnotVector.__get_unique(all_knots) - all_mults = [0] * len(all_knots) - for vector in [self, other]: - for knot in vector: - index = all_knots.index(knot) - mult = vector.mult(knot) - if mult > all_mults[index]: - all_mults[index] = mult - final_vector = [] - for knot, mult in zip(all_knots, all_mults): - final_vector += [knot] * mult - final_vector = tuple(sorted(final_vector)) - return ImmutableKnotVector(final_vector) - - def __and__(self, other: ImmutableKnotVector) -> ImmutableKnotVector: - other = ImmutableKnotVector(other) - if self.limits != other.limits: - raise ValueError - all_knots = tuple(sorted(set(self.knots) & set(other.knots))) - all_mults = [float("inf")] * len(all_knots) - for vector in [self, other]: - for knot in vector: - if knot not in all_knots: - continue - index = all_knots.index(knot) - mult = vector.mult(knot) - if mult < all_mults[index]: - all_mults[index] = mult - final_vector = [] - for knot, mult in zip(all_knots, all_mults): - final_vector += [knot] * mult - return ImmutableKnotVector(sorted(final_vector)) - def __add__(self, other): raise ValueError @@ -180,66 +142,3 @@ def valid(self, nodes: Tuple[float]) -> bool: return True except TypeError: return self.__valid_single(nodes) - - def split(self, nodes: Tuple[float]) -> Tuple[ImmutableKnotVector]: - """ - It splits the knotvector at nodes. - You may put initial and final values, but they are ignored. - Example: - >> U = [0, 0, 0.5, 1, 1] - >> split(U, [0.5]) - [[0, 0, 0.5, 0.5], - [0.5, 0.5, 1, 1]] - >> split(U, [0.25]) - [[0, 0, 0.25, 0.25], - [0.25, 0.25, 0.5, 1, 1]] - >> split(U, [0, 0.25, 0.75]) - [[0, 0, 0.25, 0.25], - [0.25, 0.25, 0.5, 0.75, 0.75], - [0.75, 0.75, 1, 1]] - """ - if not self.valid(nodes): - raise ValueError - nodes = set(nodes) - if len(nodes) == 0: - return (self,) - nodes = tuple(sorted(nodes | set(self.limits))) - vector = np.array(tuple(self)) - - retorno = [] - for a, b in zip(nodes[:-1], nodes[1:]): - middle = list(vector[(a < vector) * (vector < b)]) - newknotvect = (self.degree + 1) * [a] + middle + (self.degree + 1) * [b] - newknotvect = ImmutableKnotVector(newknotvect) - retorno.append(newknotvect) - return tuple(retorno) - - def increase(self, times: int) -> ImmutableKnotVector: - """Degree increase""" - vector = sorted(list(self) + times * list(self.knots)) - return self.__class__(vector, self.degree + times) - - def decrease(self, times: int) -> ImmutableKnotVector: - """Degree decrease""" - vector = list(self) - knots = self.knots[1:-1] - for _ in range(times): - vector.pop(0) - vector.pop(-1) - for node in knots: - vector.remove(node) - vector = sorted(vector) - return self.__class__(vector, self.degree - times) - - def remove(self, nodes: Tuple[float]) -> ImmutableKnotVector: - """Remove knots""" - vector = list(self) - for node in nodes: - vector.remove(node) - vector = sorted(vector) - return self.__class__(vector, self.degree) - - def insert(self, nodes: Tuple[float]) -> ImmutableKnotVector: - """Insert knots""" - vector = sorted(list(self) + list(nodes)) - return self.__class__(vector, self.degree) diff --git a/src/pynurbs/core/operations.py b/src/pynurbs/core/operations.py new file mode 100644 index 0000000..5016003 --- /dev/null +++ b/src/pynurbs/core/operations.py @@ -0,0 +1,185 @@ +from numbers import Real +from typing import Iterable + +from .knotvector import ImmutableKnotVector + + +def insert_knots( + knotvector: ImmutableKnotVector, nodes: Iterable[Real] +) -> ImmutableKnotVector: + """ + Insert the given nodes into the knotvector + + Example + ------- + >>> knotvector = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) + >>> insert_knots(knotvector, [0.5, 0.5]) + (0, 0, 0, 0.5, 0.5, 1, 1, 1) + """ + if not isinstance(knotvector, ImmutableKnotVector): + raise TypeError + nodes = list(nodes) + if len(nodes) == 0: + return knotvector + new_knots = sorted(list(knotvector) + list(nodes)) + return ImmutableKnotVector(new_knots, knotvector.degree) + + +def remove_knots( + knotvector: ImmutableKnotVector, nodes: Iterable[Real] +) -> ImmutableKnotVector: + """ + Remove the given nodes from the knotvector + + Example + ------- + >>> knotvector = ImmutableKnotVector([0, 0, 0, 0.5, 0.5, 1, 1, 1]) + >>> remove_knots(knotvector, [0.5]) + (0, 0, 0, 0.5, 1, 1, 1) + """ + if not isinstance(knotvector, ImmutableKnotVector): + raise TypeError + nodes = list(nodes) + new_knots = list(knotvector) + if len(nodes) == 0: + return knotvector + for node in nodes: + new_knots.remove(node) + return ImmutableKnotVector(new_knots, knotvector.degree) + + +def increase_degree(knotvector: ImmutableKnotVector, times: int) -> ImmutableKnotVector: + """ + Increases the degree of the given knotvector + + Example + ------- + >>> knotvector = ImmutableKnotVector([0, 0, 0, 0.5, 1, 1, 1]) + >>> increase_degree(knotvector, 1) + (0, 0, 0, 0, 0.5, 0.5, 1, 1, 1, 1) + >>> increase_degree(knotvector, 2) + (0, 0, 0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1) + """ + if not isinstance(knotvector, ImmutableKnotVector): + raise TypeError + if times < 0: + raise ValueError + if times == 0: + return knotvector + new_knots = sorted(list(knotvector) + times * list(knotvector.knots)) + return ImmutableKnotVector(new_knots, knotvector.degree + times) + + +def decrease_degree(knotvector: ImmutableKnotVector, times: int) -> ImmutableKnotVector: + """ + Decreases the degree of the given knotvector + + Example + ------- + >>> vector = [0, 0, 0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1] + >>> knotvector = ImmutableKnotVector(vector) + >>> decrease_degree(knotvector, 1) + (0, 0, 0, 0, 0.5, 0.5, 1, 1, 1, 1) + >>> decrease_degree(knotvector, 2) + (0, 0, 0, 0.5, 1, 1, 1) + >>> decrease_degree(knotvector, 3) + (0, 0, 1, 1) + """ + if not isinstance(knotvector, ImmutableKnotVector): + raise TypeError + if times < 0: + raise ValueError + if times == 0: + return knotvector + knots_to_remove = times * list(knotvector.knots) + new_knots = list(knotvector) + for knot in knots_to_remove: + new_knots.remove(knot) + final = ImmutableKnotVector(new_knots, knotvector.degree - times) + return final + + +def split_knotvector( + knotvector: ImmutableKnotVector, nodes: Iterable[Real] +) -> Iterable[ImmutableKnotVector]: + """ + Splits the given knotvector in the given nodes + + Example + ------- + >>> vector = [0, 0, 0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 1, 1] + >>> knotvector = ImmutableKnotVector(vector) + >>> split_knotvector(knotvector, [0.3, 0.7]) + [(0, 0, 0, 0, 0, 0.3, 0.3, 0.3, 0.3, 0.3] + (0.3, 0.3, 0.3, 0.3, 0.3, 0.5, 0.5, 0.5, 0.7, 0.7, 0.7, 0.7, 0.7], + (0.7, 0.7, 0.7, 0.7, 0.7, 1, 1, 1, 1, 1])] + """ + if not isinstance(knotvector, ImmutableKnotVector): + raise TypeError + degree = knotvector.degree + nodes = sorted(set(nodes) | {knotvector.knots[0], knotvector.knots[-1]}) + for a, b in zip(nodes[:-1], nodes[1:]): + middle = list(knot for knot in knotvector if (a < knot < b)) + newknotvect = (degree + 1) * [a] + middle + (degree + 1) * [b] + yield ImmutableKnotVector(newknotvect) + + +def union_knotvectors( + knotvectors: Iterable[ImmutableKnotVector], +) -> ImmutableKnotVector: + """ + Computes the union of the given knotvectors + """ + knotvectors = tuple(knotvectors) + if not all(isinstance(vec, ImmutableKnotVector) for vec in knotvectors): + raise TypeError + left, right = knotvectors[0].knots[0], knotvectors[0].knots[-1] + if any(vec.knots[0] != left or vec.knots[-1] != right for vec in knotvectors): + raise ValueError + maxdeg = max(vec.degree for vec in knotvectors) + internals = {} + for knotvector in knotvectors: + if knotvector.degree < maxdeg: + knotvector = increase_degree(knotvector, maxdeg - knotvector.degree) + for knot in knotvector.knots[1:-1]: + if knot not in internals: + internals[knot] = 0 + internals[knot] = max(internals[knot], knotvector.mult(knot)) + final = [left] * (maxdeg + 1) + for knot in sorted(internals.keys()): + final += internals[knot] * [knot] + final += [right] * (maxdeg + 1) + return ImmutableKnotVector(final, maxdeg) + + +def intersect_knotvectors( + knotvectors: Iterable[ImmutableKnotVector], +) -> ImmutableKnotVector: + """ + Computes the intersections of the given knotvectors + """ + knotvectors = tuple(knotvectors) + if not all(isinstance(vec, ImmutableKnotVector) for vec in knotvectors): + raise TypeError + left, right = knotvectors[0].knots[0], knotvectors[0].knots[-1] + if any(vec.knots[0] != left or vec.knots[-1] != right for vec in knotvectors): + raise ValueError + mindeg = min(vec.degree for vec in knotvectors) + internals = {} + for knotvector in knotvectors: + if knotvector.degree > mindeg: + knotvector = decrease_degree(knotvector, knotvector.degree - mindeg) + for knot in knotvector.knots[1:-1]: + if knot not in internals: + internals[knot] = knotvector.mult(knot) + for knotvector in knotvectors: + if knotvector.degree > mindeg: + knotvector = decrease_degree(knotvector, knotvector.degree - mindeg) + for knot in internals.keys(): + internals[knot] = min(internals[knot], knotvector.mult(knot)) + final = [left] * (mindeg + 1) + for knot in sorted(internals.keys()): + + final += internals[knot] * [knot] + final += [right] * (mindeg + 1) + return ImmutableKnotVector(final, mindeg) diff --git a/src/pynurbs/curves.py b/src/pynurbs/curves.py index a9bd049..5c69ca6 100644 --- a/src/pynurbs/curves.py +++ b/src/pynurbs/curves.py @@ -11,6 +11,12 @@ from pynurbs.knotspace import KnotVector from .core.basisfunction import ImmutableBasisFunction +from .core.operations import ( + decrease_degree, + increase_degree, + insert_knots, + remove_knots, +) def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: @@ -670,7 +676,7 @@ def knot_insert(self, nodes: Tuple[float]) -> None: """ nodes = tuple(nodes) oldvector = self.knotvector.internal - newvector = oldvector.insert(nodes) + newvector = insert_knots(oldvector, nodes) if self.ctrlpoints is None and self.weights is None: self.knotvector = newvector matrix = heavy.Operations.knot_insert(oldvector, nodes) @@ -706,7 +712,7 @@ def knot_remove(self, nodes: Tuple[float], tolerance: float = 1e-9) -> None: """ old_vector = self.knotvector.internal - new_vector = old_vector.remove(nodes) + new_vector = remove_knots(old_vector, nodes) knots = new_vector.knots if new_vector.degree != 0 else None self.update(new_vector, tolerance, knots) @@ -786,7 +792,7 @@ def degree_increase(self, times: Optional[int] = 1): if not isinstance(times, int) or times <= 0: raise ValueError old_vector = self.knotvector.internal - new_vector = old_vector.increase(times) + new_vector = increase_degree(old_vector, times) matrix = heavy.Operations.degree_increase(old_vector, times) self.apply(new_vector, matrix) @@ -824,7 +830,7 @@ def degree_decrease( float(tolerance) assert tolerance >= 0 old_vector = self.knotvector.internal - new_vector = old_vector.decrease(times) + new_vector = decrease_degree(old_vector, times) knots = new_vector.knots if new_vector.degree != 0 else None self.update(new_vector, tolerance, knots) diff --git a/src/pynurbs/heavy.py b/src/pynurbs/heavy.py index 317750e..8ba3a4f 100644 --- a/src/pynurbs/heavy.py +++ b/src/pynurbs/heavy.py @@ -1,19 +1,26 @@ """ This module contains very low level functions that can be easily change to another language such as C/C++ (further may be). -They are 'heavy' functions that are called many times and don't require any special package +They are 'heavy' functions that are called many times and don't require any special package Most of these functions works only with integers, floats and tuples. """ from __future__ import annotations from fractions import Fraction -from typing import Optional, Tuple, Union +from typing import Tuple, Union import numpy as np from .cmath import IntegratorArray, Linalg, NodeSample, number_type, totuple from .core.basisfunction import ImmutableBasisFunction from .core.knotvector import ImmutableKnotVector +from .core.operations import ( + increase_degree, + insert_knots, + remove_knots, + split_knotvector, + union_knotvectors, +) def find_roots( @@ -338,9 +345,9 @@ def split_curve(knotvector: ImmutableKnotVector, nodes: Tuple[float]): for node in nodes: mult = knotvector.mult(node) manynodes += [node] * (degree + 1 - mult) - bigvector = knotvector.insert(manynodes) + bigvector = insert_knots(knotvector, manynodes) bigmatrix = Operations.knot_insert(knotvector, manynodes) - newvectors = bigvector.split(nodes) + newvectors = split_knotvector(bigvector, nodes) matrices = [] for newvector in newvectors: umin = newvector.limits[0] @@ -416,7 +423,7 @@ def one_knot_insert( for _ in range(times): incmatrix = Operations.one_knot_insert_once(knotvector, node) matrix = incmatrix @ matrix - knotvector = knotvector.insert([node]) + knotvector = insert_knots(knotvector, [node]) return totuple(matrix) def knot_insert(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix2D": @@ -449,7 +456,7 @@ def knot_insert(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix times = nodes.count(node) incmatrix = Operations.one_knot_insert(knotvector, node, times) matrix = incmatrix @ matrix - knotvector = knotvector.insert(times * [node]) + knotvector = insert_knots(knotvector, times * [node]) return totuple(matrix) def knot_remove(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix2D": @@ -458,7 +465,7 @@ def knot_remove(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix if not knotvector.valid(nodes): msg = f"Invalid nodes {nodes} in knotvector {knotvector}" raise ValueError(msg) - newknotvector = knotvector.remove(nodes) + newknotvector = remove_knots(knotvector, nodes) matrix, _ = LeastSquare.spline2spline(knotvector, newknotvector) return totuple(matrix) @@ -499,7 +506,7 @@ def degree_increase_bezier( for i in range(times): elevateonce = Operations.degree_increase_bezier_once(knotvector) matrix = elevateonce @ matrix - knotvector = knotvector.increase(1) + knotvector = increase_degree(knotvector, 1) return totuple(matrix) def degree_increase(knotvector: ImmutableKnotVector, times: int) -> "Matrix2D": @@ -521,7 +528,7 @@ def degree_increase(knotvector: ImmutableKnotVector, times: int) -> "Matrix2D": if degree + 1 == npts: return Operations.degree_increase_bezier(knotvector, times) nodes = knotvector.knots - newvectors = knotvector.split(nodes) + newvectors = split_knotvector(knotvector, nodes) matrices = Operations.split_curve(knotvector, nodes) bigmatrix = [] @@ -537,8 +544,8 @@ def degree_increase(knotvector: ImmutableKnotVector, times: int) -> "Matrix2D": for node in nodes: mult = knotvector.mult(node) insertednodes += (degree + 1 - mult) * [node] - bigvector = knotvector.insert(insertednodes) - incbigvector = bigvector.increase(times) + bigvector = insert_knots(knotvector, insertednodes) + incbigvector = increase_degree(bigvector, times) removematrix = Operations.knot_remove(incbigvector, insertednodes) bigmatrix = np.array(bigmatrix) @@ -566,10 +573,10 @@ def matrix_transformation( degreea = knotvectora.degree degreeb = knotvectorb.degree - knotsa = knotvectora.knots assert degreea <= degreeb matrix_deginc = Operations.degree_increase(knotvectora, degreeb - degreea) - knotvectora = knotvectora.increase(degreeb - degreea) + if degreea < degreeb: + knotvectora = increase_degree(knotvectora, degreeb - degreea) nodes2ins = [] for knot in knotvectorb.knots: @@ -653,7 +660,7 @@ def add_spline_curve( knotvectorb = ImmutableKnotVector(knotvectorb) assert knotvectora.limits == knotvectorb.limits - knotvectorc = knotvectora | knotvectorb + knotvectorc = union_knotvectors([knotvectora, knotvectorb]) matrixa = Operations.matrix_transformation(knotvectora, knotvectorc) matrixb = Operations.matrix_transformation(knotvectorb, knotvectorc) return totuple(matrixa), totuple(matrixb) diff --git a/src/pynurbs/knotspace.py b/src/pynurbs/knotspace.py index b8f0c99..e188846 100644 --- a/src/pynurbs/knotspace.py +++ b/src/pynurbs/knotspace.py @@ -15,6 +15,15 @@ from pynurbs.__classes__ import Intface_KnotVector from .core import ImmutableKnotVector +from .core.operations import ( + decrease_degree, + increase_degree, + insert_knots, + intersect_knotvectors, + remove_knots, + split_knotvector, + union_knotvectors, +) class KnotVector(Intface_KnotVector): @@ -77,11 +86,11 @@ def __itruediv__(self, other: float): return self.scale(1 / other) def __ior__(self, other: KnotVector) -> KnotVector: - self.internal |= other + self.internal = union_knotvectors([self.internal, other.internal]) return self def __iand__(self, other: KnotVector) -> KnotVector: - self.internal &= other + self.internal = intersect_knotvectors([self.internal, other.internal]) return self def __add__(self, other: Union[float, Tuple[float]]): @@ -399,7 +408,7 @@ def insert(self, nodes: Tuple[float]) -> KnotVector: (0, 0, 1, 2, 2, 3, 3) """ - self.internal = self.internal.insert(nodes) + self.internal = insert_knots(self.internal, nodes) return self def remove(self, nodes: Tuple[float]) -> KnotVector: @@ -425,15 +434,15 @@ def remove(self, nodes: Tuple[float]) -> KnotVector: (0, 0, 3, 3) """ - self.internal = self.internal.remove(nodes) + self.internal = remove_knots(self.internal, nodes) return self def increase(self, times: int) -> KnotVector: - self.internal = self.internal.increase(times) + self.internal = increase_degree(self.internal, times) return self def decrease(self, times: int) -> KnotVector: - self.internal = self.internal.decrease(times) + self.internal = decrease_degree(self.internal, times) return self def span(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: @@ -534,7 +543,7 @@ def split(self, nodes: Tuple[float]) -> Tuple[KnotVector]: ((0, 0, 0.5, 0.5), (0.5, 0.5, 1, 1)) """ - vectors = self.internal.split(nodes) + vectors = split_knotvector(self.internal, nodes) return tuple(map(self.__class__, vectors)) diff --git a/tests/core/test_operations.py b/tests/core/test_operations.py new file mode 100644 index 0000000..ab35143 --- /dev/null +++ b/tests/core/test_operations.py @@ -0,0 +1,64 @@ +import pytest + +from pynurbs.core.knotvector import ImmutableKnotVector +from pynurbs.core.operations import ( + decrease_degree, + increase_degree, + insert_knots, + remove_knots, +) + + +@pytest.mark.order(3) +@pytest.mark.dependency( + depends=[ + "tests/core/test_knotvector.py::test_end", + ], + scope="session", +) +def test_begin(): + pass + + +@pytest.mark.order(3) +@pytest.mark.dependency(depends=["test_begin"]) +def test_insert_knots(): + knotvector = ImmutableKnotVector([0, 0, 1, 2, 2]) + assert insert_knots(knotvector, [0.5, 1.5]) == (0, 0, 0.5, 1, 1.5, 2, 2) + + +@pytest.mark.order(3) +@pytest.mark.dependency(depends=["test_begin"]) +def test_remove_knots(): + knotvector = ImmutableKnotVector([0, 0, 1, 2, 2]) + assert remove_knots(knotvector, [1]) == (0, 0, 2, 2) + + +@pytest.mark.order(3) +@pytest.mark.dependency(depends=["test_begin"]) +def test_increase_degree(): + knotvector = ImmutableKnotVector([0, 0, 1, 2, 2]) + assert increase_degree(knotvector, 1) == (0, 0, 0, 1, 1, 2, 2, 2) + assert increase_degree(knotvector, 2) == (0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2) + + +@pytest.mark.order(3) +@pytest.mark.dependency(depends=["test_begin"]) +def test_decrease_degree(): + knotvector = ImmutableKnotVector([0, 0, 0, 0, 1, 1, 2, 2, 2, 2]) + assert decrease_degree(knotvector, 1) == (0, 0, 0, 1, 2, 2, 2) + assert decrease_degree(knotvector, 2) == (0, 0, 2, 2) + + +@pytest.mark.order(3) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_insert_knots", + "test_remove_knots", + "test_increase_degree", + "test_decrease_degree", + ] +) +def test_all(): + pass From 16a0e9b2cef53d4483246406c69de0402c0a0481 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 28 Jun 2025 11:39:06 +0200 Subject: [PATCH 022/116] feat: implement finding roots of polynomials --- src/pynurbs/core/roots.py | 23 +++++++++++++++++++++++ tests/core/test_roots.py | 26 +++++++++++++++++++++++++- 2 files changed, 48 insertions(+), 1 deletion(-) diff --git a/src/pynurbs/core/roots.py b/src/pynurbs/core/roots.py index 46c434f..f860a1f 100644 --- a/src/pynurbs/core/roots.py +++ b/src/pynurbs/core/roots.py @@ -2,8 +2,12 @@ Finds the roots of polynomials """ +from fractions import Fraction +from numbers import Real from typing import Tuple +import numpy as np + from .polynomial import Polynomial @@ -32,3 +36,22 @@ def division(poly: Polynomial, doly: Polynomial) -> Tuple[Polynomial, Polynomial roly = poly - doly * qoly index -= 1 return qoly, Polynomial(roly[: doly.degree]) + + +def roots(poly: Polynomial) -> Tuple[Real, ...]: + """ + Finds the real roots of the given polynomial + + Example + ------- + >>> x = Polynomial([0, 1]) + >>> roots(x**2 + 3*x + 2) + (-2, -1) + >>> roots(x**3 - 6*x**2 + 11*x - 6) + (1, 2, 3) + """ + values = sorted(np.roots(tuple(poly)[::-1])) + for i, value in enumerate(values): + if abs(round(1440 * value, 0) - 1440 * value) < 1e-6: + values[i] = Fraction(round(1440 * value), 1440) + return tuple(values) diff --git a/tests/core/test_roots.py b/tests/core/test_roots.py index 0362810..7f2f65a 100644 --- a/tests/core/test_roots.py +++ b/tests/core/test_roots.py @@ -1,7 +1,7 @@ import pytest from pynurbs.core.polynomial import Polynomial -from pynurbs.core.roots import division +from pynurbs.core.roots import division, roots @pytest.mark.order(3) @@ -11,6 +11,12 @@ ], scope="session", ) +def test_begin(): + pass + + +@pytest.mark.order(3) +@pytest.mark.dependency(depends=["test_begin"]) def test_division(): poly = Polynomial([0, 1]) doly = Polynomial([1]) @@ -55,3 +61,21 @@ def test_division(): qoly, roly = division(poly, doly) diff = doly * qoly + roly - poly assert all(abs(coef) < 1e-9 for coef in diff) + + +@pytest.mark.order(3) +@pytest.mark.dependency(depends=["test_begin"]) +def test_roots(): + x = Polynomial([0, 1]) + values = roots(x**2 + 3 * x + 2) + print(values) + assert values == (-2, -1) + values = roots(x**3 - 6 * x**2 + 11 * x - 6) + print(values) + assert values == (1, 2, 3) + + +@pytest.mark.order(3) +@pytest.mark.dependency(depends=["test_division", "test_roots"]) +def test_all(): + pass From 5b88b614c93f925fd3c84ef37810f4acb871da4c Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 28 Jun 2025 12:21:03 +0200 Subject: [PATCH 023/116] dev: remove ValueError and TypeError from coverage --- .coveragerc | 2 ++ 1 file changed, 2 insertions(+) diff --git a/.coveragerc b/.coveragerc index 433e8f0..0993d3f 100644 --- a/.coveragerc +++ b/.coveragerc @@ -8,3 +8,5 @@ exclude_lines = # Don't complain if tests don't hit defensive assertion code: raise NotImplementedError + raise ValueError + raise TypeError From f5a0ec2ee9f306b95483b8932549eeca028c505f Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 29 Jun 2025 12:37:21 +0200 Subject: [PATCH 024/116] feat: add vectorize decorator to treat arrays of parameters --- src/pynurbs/core/tools.py | 58 ++++++++++++ src/pynurbs/functions.py | 67 +++----------- tests/test_functions.py | 183 ++++++++++++++++++++++++++------------ 3 files changed, 196 insertions(+), 112 deletions(-) create mode 100644 src/pynurbs/core/tools.py diff --git a/src/pynurbs/core/tools.py b/src/pynurbs/core/tools.py new file mode 100644 index 0000000..6f44138 --- /dev/null +++ b/src/pynurbs/core/tools.py @@ -0,0 +1,58 @@ +""" +File that stores usual functions and decorators used in the package +""" + +import types +from functools import wraps + +import numpy as np + + +# Creates a decorator to vectorize functions that receives floats +# Or an array of floats depending on the dimension +def vectorize(position: int = 0, dimension: int = 0): + """ + Decorator to vectorize functions that gives the same type of container + as received from input. Meaning: tuple -> tuple, list -> list, ... + + The dimension parameter is to decide the quantity of floats per call + * dimension = 0 -> float + * dimension = 1 -> [float] + * dimension = 2 -> [float, float] + ... + """ + + def decorator(func): + conversion = { + types.GeneratorType: tuple, # No conversion + range: tuple, # No conversion + } + + @wraps(func) + def wrapper(*args, **kwargs): + param = args[position] + if dimension == 0: + try: + float(param) + return func(*args, **kwargs) + except TypeError: + result = ( + func(*args[:position], p, *args[position + 1 :], **kwargs) + for p in param + ) + result = tuple(result) + for key, tipo in conversion.items(): + if isinstance(param, key): + if tipo is not None: + result = tipo(result) + return result + if isinstance(param, np.ndarray): + result = np.array(result, dtype=param.dtype) + else: + result = param.__class__(result) + return result + raise NotImplementedError + + return wrapper + + return decorator diff --git a/src/pynurbs/functions.py b/src/pynurbs/functions.py index 0a1daac..2265e5e 100644 --- a/src/pynurbs/functions.py +++ b/src/pynurbs/functions.py @@ -9,6 +9,8 @@ from pynurbs.core.basisfunction import spectral_matrix from pynurbs.knotspace import KnotVector +from .core.tools import vectorize + class BaseFunction(Intface_BaseFunction): def __init__(self, knotvector: KnotVector): @@ -146,7 +148,9 @@ def degree(self, value: int): @knotvector.setter def knotvector(self, value: KnotVector): - self.__knotvector = KnotVector(value) + if not isinstance(value, KnotVector): + value = KnotVector(value) + self.__knotvector = value @weights.setter def weights(self, value: Tuple[float]): @@ -201,65 +205,20 @@ def __compute_vector_spline(self, node: float, span: int) -> np.ndarray: result[i] += self.__matrix[z][y][k] return result - def __compute_vector(self, node: float, span: int) -> np.ndarray: - """ - Given a 'u' float, it returns the vector with all BasicFunctions: - compute_vector(u, span) = [F_{0j}(u), F_{1j}(u), ..., F_{npts-1,j}(u)] - """ - result = self.__compute_vector_spline(node, span) - if self.__weights is None: - return result - return self.__weights * result / np.inner(self.__weights, result) - - def __compute_matrix( - self, nodes: Tuple[float], spans: Tuple[int] - ) -> Tuple[Tuple[float]]: - """ - Receives an 1D array of nodes, and returns a 2D array. - nodes.shape = (len(nodes), ) - result.shape = (npts, len(nodes)) - """ - nodes = tuple(nodes) - npts = self.__knotvector.npts - matrix = np.empty((npts, len(nodes)), dtype="object") - for j, (nodej, spanj) in enumerate(zip(nodes, spans)): - values = self.__compute_vector(nodej, spanj) - for i in range(npts): - matrix[i][j] = values[i] - matrix = matrix.tolist() - for i, line in enumerate(matrix): - matrix[i] = tuple(line) - return tuple(matrix) - - def __eval(self, nodes: Tuple[float]) -> Tuple[Tuple[float]]: - """ - Private and unprotected method of eval - """ - nodes = tuple(nodes) - spans = self.__knotvector.span(nodes) - matrix = self.__compute_matrix(nodes, spans) - return matrix - - def eval( - self, nodes: Union[float, Tuple[float]] - ) -> Union[float, Tuple[float], Tuple[Tuple[float]]]: + @vectorize(1, 0) + def eval(self, node: float) -> Union[float, Tuple[float]]: """ If i is integer, u is float -> float If i is integer, u is Tuple[float], ndim = k -> np.ndarray, ndim = k If i is slice, u is float -> Tuple[float] if i is slice, u is Tuple[float], ndim = k -> Tuple[Tuple[float]], ndim = k+1 """ - singlenode = True - try: - iter(nodes) - singlenode = False - except TypeError: - nodes = (nodes,) - matrix = self.__eval(nodes) - if singlenode: - matrix = tuple([ri[0] for ri in matrix]) - result = matrix[self.__first_index] - return result + span = self.__knotvector.span(node) + result = self.__compute_vector_spline(node, span) + if self.__weights is not None: + result *= self.__weights + result *= 1 / sum(result) + return result[self.__first_index] def __call__( self, nodes: Union[float, Tuple[float]] diff --git a/tests/test_functions.py b/tests/test_functions.py index b07a9ff..62fa78b 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -109,7 +109,7 @@ def test_shape_calls(self): assert values.shape == (npts,) matrix = bezier[:, j](nodes_test) matrix = np.array(matrix, dtype="float64") - assert matrix.shape == (npts, npts_sample) + assert matrix.shape == (npts_sample, npts) @pytest.mark.order(3) @pytest.mark.timeout(5) @@ -127,10 +127,10 @@ def test_sum_equal_to_1(self): for j in range(degree + 1): matrix = bezier[:, j](nodes_test) matrix = np.array(matrix, dtype="float64") - assert matrix.shape == (npts, npts_sample) + assert matrix.shape == (npts_sample, npts) assert np.all(matrix >= 0) for k in range(npts_sample): - assert abs(np.sum(matrix[:, k]) - 1) < 1e-9 + assert abs(np.sum(matrix[k]) - 1) < 1e-9 @pytest.mark.order(3) @pytest.mark.timeout(5) @@ -230,11 +230,11 @@ def test_tablevalues_degree1(self): nodes_test = np.linspace(0, 1, 11) matrix_test = bezier[:, 0](nodes_test) - matrix_good = [[0] * 11, [1] * 11] + matrix_good = np.transpose([[0] * 11, [1] * 11]) np.testing.assert_allclose(matrix_test, matrix_good) matrix_test = bezier[:, 1](nodes_test) - matrix_good = [np.linspace(1, 0, 11), np.linspace(0, 1, 11)] + matrix_good = np.transpose([1 - nodes_test, nodes_test]) np.testing.assert_allclose(matrix_test, matrix_good) @pytest.mark.order(3) @@ -252,21 +252,27 @@ def test_tablevalues_degree2(self): nodes_test = np.linspace(0, 1, 11) matrix_test = bezier[:, 0](nodes_test) - matrix_good = np.array([[0] * 11, [0] * 11, [1] * 11]) + matrix_good = np.transpose([[0] * 11, [0] * 11, [1] * 11]) np.testing.assert_allclose(matrix_test, matrix_good) matrix_test = bezier[:, 1](nodes_test) - matrix_good = [[0] * 11, np.linspace(1, 0, 11), np.linspace(0, 1, 11)] + matrix_good = np.transpose([[0] * 11, 1 - nodes_test, nodes_test]) np.testing.assert_allclose(matrix_test, matrix_good) matrix_test = bezier[:, 2](nodes_test) - matrix_good = np.array( - [ - [1.0, 0.81, 0.64, 0.49, 0.36, 0.25, 0.16, 0.09, 0.04, 0.01, 0.0], - [0.0, 0.18, 0.32, 0.42, 0.48, 0.50, 0.48, 0.42, 0.32, 0.18, 0.0], - [0.0, 0.01, 0.04, 0.09, 0.16, 0.25, 0.36, 0.49, 0.64, 0.81, 1.0], - ] - ) + matrix_good = [ + [1.0, 0.0, 0.0], + [0.81, 0.18, 0.01], + [0.64, 0.32, 0.04], + [0.49, 0.42, 0.09], + [0.36, 0.48, 0.16], + [0.25, 0.5, 0.25], + [0.16, 0.48, 0.36], + [0.09, 0.42, 0.49], + [0.04, 0.32, 0.64], + [0.01, 0.18, 0.81], + [0.0, 0.0, 1.0], + ] np.testing.assert_allclose(matrix_test, matrix_good) @pytest.mark.order(3) @@ -286,11 +292,11 @@ def test_tablevalues_random_degree(self): nodestest = np.linspace(0, 1, 11) matrix_test = bezier[:, degree](nodestest) - matrix_good = np.zeros((degree + 1, len(nodestest))) + matrix_good = np.zeros((len(nodestest), degree + 1)) for i, node in enumerate(nodestest): for j in range(degree + 1): value = binom(degree, j) * (1 - node) ** (degree - j) * node**j - matrix_good[j, i] = value + matrix_good[i, j] = value np.testing.assert_allclose(matrix_test, matrix_good) @pytest.mark.order(3) @@ -310,11 +316,11 @@ def test_shifted_scaled_bezier(self): nodesgood = np.linspace(0, 1, 11) nodestest = np.linspace(knotvector[0], knotvector[-1], 11) matrix_test = bezier[:, degree](nodestest) - matrix_good = np.zeros((degree + 1, len(nodestest))) + matrix_good = np.zeros((len(nodestest), degree + 1)) for i, node in enumerate(nodesgood): for j in range(degree + 1): value = binom(degree, j) * (1 - node) ** (degree - j) * node**j - matrix_good[j, i] = value + matrix_good[i, j] = value np.testing.assert_allclose(matrix_test, matrix_good) @pytest.mark.order(3) @@ -423,17 +429,33 @@ def test_tablevalues_degree1npts3(self): matrix_test = spline[:, 0](nodes_test) matrix_good = [ - [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0], - [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 1, 0], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], + [0, 0, 1], ] np.testing.assert_allclose(matrix_test, matrix_good) matrix_test = spline[:, 1](nodes_test) matrix_good = [ - [1.0, 0.8, 0.6, 0.4, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 0.8, 0.6, 0.4, 0.2, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0], + [1.0, 0.0, 0.0], + [0.8, 0.2, 0.0], + [0.6, 0.4, 0.0], + [0.4, 0.6, 0.0], + [0.2, 0.8, 0.0], + [0.0, 1.0, 0.0], + [0.0, 0.8, 0.2], + [0.0, 0.6, 0.4], + [0.0, 0.4, 0.6], + [0.0, 0.2, 0.8], + [0.0, 0.0, 1.0], ] np.testing.assert_allclose(matrix_test, matrix_good) @@ -448,28 +470,49 @@ def test_tablevalues_degree2npts4(self): matrix_test = spline[:, 0](nodes_test) matrix_good = [ - [0] * 11, - [0] * 11, - [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 1, 0], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], + [0, 0, 0, 1], ] np.testing.assert_allclose(matrix_test, matrix_good) matrix_test = spline[:, 1](nodes_test) matrix_good = [ - [0] * 11, - [1.0, 0.8, 0.6, 0.4, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 0.8, 0.6, 0.4, 0.2, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0], + [0.0, 1.0, 0.0, 0.0], + [0.0, 0.8, 0.2, 0.0], + [0.0, 0.6, 0.4, 0.0], + [0.0, 0.4, 0.6, 0.0], + [0.0, 0.2, 0.8, 0.0], + [0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.8, 0.2], + [0.0, 0.0, 0.6, 0.4], + [0.0, 0.0, 0.4, 0.6], + [0.0, 0.0, 0.2, 0.8], + [0.0, 0.0, 0.0, 1.0], ] np.testing.assert_allclose(matrix_test, matrix_good) matrix_test = spline[:, 2](nodes_test) matrix_good = [ - [1, 0.64, 0.36, 0.16, 0.04, 0.0, 0.00, 0.00, 0.00, 0.00, 0], - [0, 0.34, 0.56, 0.66, 0.64, 0.5, 0.32, 0.18, 0.08, 0.02, 0], - [0, 0.02, 0.08, 0.18, 0.32, 0.5, 0.64, 0.66, 0.56, 0.34, 0], - [0, 0.00, 0.00, 0.00, 0.00, 0.0, 0.04, 0.16, 0.36, 0.64, 1], + [1.0, 0.0, 0.0, 0.0], + [0.64, 0.34, 0.02, 0.0], + [0.36, 0.56, 0.08, 0.0], + [0.16, 0.66, 0.18, 0.0], + [0.04, 0.64, 0.32, 0.0], + [0.0, 0.5, 0.5, 0.0], + [0.0, 0.32, 0.64, 0.04], + [0.0, 0.18, 0.66, 0.16], + [0.0, 0.08, 0.56, 0.36], + [0.0, 0.02, 0.34, 0.64], + [0.0, 0.0, 0.0, 1.0], ] np.testing.assert_allclose(matrix_test, matrix_good) @@ -485,41 +528,65 @@ def test_tablevalues_degree3npts5(self): matrix_test = spline[:, 0](nodes_test) matrix_good = [ - [0] * 11, - [0] * 11, - [0] * 11, - [1, 1, 1, 1, 1, 0, 0, 0, 0, 0, 0], - [0, 0, 0, 0, 0, 1, 1, 1, 1, 1, 1], + [0, 0, 0, 1, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 1, 0], + [0, 0, 0, 0, 1], + [0, 0, 0, 0, 1], + [0, 0, 0, 0, 1], + [0, 0, 0, 0, 1], + [0, 0, 0, 0, 1], + [0, 0, 0, 0, 1], ] np.testing.assert_allclose(matrix_test, matrix_good) matrix_test = spline[:, 1](nodes_test) matrix_good = [ - [0] * 11, - [0] * 11, - [1.0, 0.8, 0.6, 0.4, 0.2, 0.0, 0.0, 0.0, 0.0, 0.0, 0.0], - [0.0, 0.2, 0.4, 0.6, 0.8, 1.0, 0.8, 0.6, 0.4, 0.2, 0.0], - [0.0, 0.0, 0.0, 0.0, 0.0, 0.0, 0.2, 0.4, 0.6, 0.8, 1.0], + [0.0, 0.0, 1.0, 0.0, 0.0], + [0.0, 0.0, 0.8, 0.2, 0.0], + [0.0, 0.0, 0.6, 0.4, 0.0], + [0.0, 0.0, 0.4, 0.6, 0.0], + [0.0, 0.0, 0.2, 0.8, 0.0], + [0.0, 0.0, 0.0, 1.0, 0.0], + [0.0, 0.0, 0.0, 0.8, 0.2], + [0.0, 0.0, 0.0, 0.6, 0.4], + [0.0, 0.0, 0.0, 0.4, 0.6], + [0.0, 0.0, 0.0, 0.2, 0.8], + [0.0, 0.0, 0.0, 0.0, 1.0], ] np.testing.assert_allclose(matrix_test, matrix_good) matrix_test = spline[:, 2](nodes_test) matrix_good = [ - [0] * 11, - [1, 0.64, 0.36, 0.16, 0.04, 0.0, 0.00, 0.00, 0.00, 0.00, 0], - [0, 0.34, 0.56, 0.66, 0.64, 0.5, 0.32, 0.18, 0.08, 0.02, 0], - [0, 0.02, 0.08, 0.18, 0.32, 0.5, 0.64, 0.66, 0.56, 0.34, 0], - [0, 0.00, 0.00, 0.00, 0.00, 0.0, 0.04, 0.16, 0.36, 0.64, 1], + [0.0, 1.0, 0.0, 0.0, 0.0], + [0.0, 0.64, 0.34, 0.02, 0.0], + [0.0, 0.36, 0.56, 0.08, 0.0], + [0.0, 0.16, 0.66, 0.18, 0.0], + [0.0, 0.04, 0.64, 0.32, 0.0], + [0.0, 0.0, 0.5, 0.5, 0.0], + [0.0, 0.0, 0.32, 0.64, 0.04], + [0.0, 0.0, 0.18, 0.66, 0.16], + [0.0, 0.0, 0.08, 0.56, 0.36], + [0.0, 0.0, 0.02, 0.34, 0.64], + [0.0, 0.0, 0.0, 0.0, 1.0], ] np.testing.assert_allclose(matrix_test, matrix_good) matrix_test = spline[:, 3](nodes_test) matrix_good = [ - [1, 0.512, 0.216, 0.064, 0.008, 0.000, 0.000, 0.000, 0.000, 0.000, 0], - [0, 0.434, 0.592, 0.558, 0.416, 0.250, 0.128, 0.054, 0.016, 0.002, 0], - [0, 0.052, 0.176, 0.324, 0.448, 0.500, 0.448, 0.324, 0.176, 0.052, 0], - [0, 0.002, 0.016, 0.054, 0.128, 0.250, 0.416, 0.558, 0.592, 0.434, 0], - [0, 0.000, 0.000, 0.000, 0.000, 0.000, 0.008, 0.064, 0.216, 0.512, 1], + [1.0, 0.0, 0.0, 0.0, 0.0], + [0.512, 0.434, 0.052, 0.002, 0.0], + [0.216, 0.592, 0.176, 0.016, 0.0], + [0.064, 0.558, 0.324, 0.054, 0.0], + [0.008, 0.416, 0.448, 0.128, 0.0], + [0.0, 0.25, 0.5, 0.25, 0.0], + [0.0, 0.128, 0.448, 0.416, 0.008], + [0.0, 0.054, 0.324, 0.558, 0.064], + [0.0, 0.016, 0.176, 0.592, 0.216], + [0.0, 0.002, 0.052, 0.434, 0.512], + [0.0, 0.0, 0.0, 0.0, 1.0], ] np.testing.assert_allclose(matrix_test, matrix_good) @@ -659,7 +726,7 @@ def test_quarter_circle_standard(self): 2 * nodes_sample * (1 - nodes_sample), 2 * nodes_sample**2, ] - good_matrix = np.array(good_matrix) / (1 + nodes_sample**2) + good_matrix = np.transpose(good_matrix / (1 + nodes_sample**2)) test_matrix = rational(nodes_sample) np.testing.assert_allclose(test_matrix, good_matrix) @@ -680,7 +747,7 @@ def test_quarter_circle_symmetric(self): ] denomin = 2 * (1 - 2 * nodes_sample + 2 * nodes_sample**2) denomin += 2 * np.sqrt(2) * nodes_sample * (1 - nodes_sample) - good_matrix = np.array(good_matrix) / denomin + good_matrix = np.transpose(good_matrix / denomin) test_matrix = rational(nodes_sample) np.testing.assert_allclose(test_matrix, good_matrix) From 04c55bf35cd18b21baaeecb16b02b1023295a27c Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 29 Jun 2025 12:45:48 +0200 Subject: [PATCH 025/116] fix: evaluation of basis function when knotvector.degree != degree --- src/pynurbs/core/basisfunction.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pynurbs/core/basisfunction.py b/src/pynurbs/core/basisfunction.py index f25ef43..bb7294e 100644 --- a/src/pynurbs/core/basisfunction.py +++ b/src/pynurbs/core/basisfunction.py @@ -69,7 +69,7 @@ def __init__( ): if not isinstance(knotvector, ImmutableKnotVector): raise TypeError - degree = degree or knotvector.degree + degree = degree if degree is not None else knotvector.degree self.__matrix = tuple( tuple(tuple(Polynomial(coefs) for coefs in all_coefs)) for all_coefs in spectral_matrix(knotvector, degree) @@ -99,14 +99,14 @@ def eval(self, node: Real, times: int = 0) -> Tuple[Real, ...]: npts = self.__knotvector.npts knots = self.__knotvector.knots spans = self.__knotvector.span(knots) - degree = self.__knotvector.degree + degree = self.__degree result = [0] * npts span = self.__knotvector.span(node) ind = spans.index(span) shifnode = node - knots[ind] shifnode /= knots[ind + 1] - knots[ind] - for y in range(self.__knotvector.degree + 1): + for y in range(self.__degree + 1): i = y + span - degree polynomial = self.__matrix[ind][y] result[i] = polynomial.eval(shifnode, times) From 2bfebd3ceb44e4b5dfdcb878392e7c812cd44fce Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 29 Jun 2025 12:46:22 +0200 Subject: [PATCH 026/116] refactor: use class that evaluates the basis functions --- src/pynurbs/functions.py | 27 +++------------------------ 1 file changed, 3 insertions(+), 24 deletions(-) diff --git a/src/pynurbs/functions.py b/src/pynurbs/functions.py index 2265e5e..a551a2b 100644 --- a/src/pynurbs/functions.py +++ b/src/pynurbs/functions.py @@ -6,7 +6,7 @@ import numpy as np from pynurbs.__classes__ import Intface_BaseFunction, Intface_Evaluator -from pynurbs.core.basisfunction import spectral_matrix +from pynurbs.core.basisfunction import ImmutableBasisFunction from pynurbs.knotspace import KnotVector from .core.tools import vectorize @@ -180,30 +180,10 @@ def __deepcopy__(self, memo) -> BaseFunction: class FunctionEvaluator(Intface_Evaluator): def __init__(self, func: BaseFunction, i: Union[int, slice], j: int): vector = func.knotvector - self.__knotvector = vector self.__weights = func.weights self.__first_index = i - self.__second_index = j - self.__matrix = spectral_matrix(vector.internal, j) - self.__knots = vector.knots - self.__spans = vector.span(vector.knots) + self.__basis = ImmutableBasisFunction(vector.internal, j) - def __compute_vector_spline(self, node: float, span: int) -> np.ndarray: - """ - Given a 'u' float, it returns the vector with all Spline Basis Functions: - compute_vector(u, span) = [N_{0j}(u), N_{1j}(u), ..., N_{npts-1,j}(u)] - """ - npts = self.__knotvector.npts - result = [0 * node] * npts - z = self.__spans.index(span) - denom = self.__knots[z + 1] - self.__knots[z] - shifnode = (node - self.__knots[z]) / denom - for y in range(self.__second_index + 1): - i = y + span - self.__second_index - for k in range(self.__second_index, -1, -1): - result[i] *= shifnode - result[i] += self.__matrix[z][y][k] - return result @vectorize(1, 0) def eval(self, node: float) -> Union[float, Tuple[float]]: @@ -213,8 +193,7 @@ def eval(self, node: float) -> Union[float, Tuple[float]]: If i is slice, u is float -> Tuple[float] if i is slice, u is Tuple[float], ndim = k -> Tuple[Tuple[float]], ndim = k+1 """ - span = self.__knotvector.span(node) - result = self.__compute_vector_spline(node, span) + result = self.__basis.eval(node) if self.__weights is not None: result *= self.__weights result *= 1 / sum(result) From 11ff4027d144e160570faa4922101808be2b38f9 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 29 Jun 2025 12:49:41 +0200 Subject: [PATCH 027/116] refactor: move `custom_math` to `core` --- src/pynurbs/core/basisfunction.py | 2 +- src/pynurbs/{cmath.py => core/custom_math.py} | 0 src/pynurbs/heavy.py | 2 +- tests/{test_cmath.py => test_custom_math.py} | 2 +- tests/test_functions.py | 2 +- tests/test_heavy.py | 4 +++- 6 files changed, 7 insertions(+), 5 deletions(-) rename src/pynurbs/{cmath.py => core/custom_math.py} (100%) rename tests/{test_cmath.py => test_custom_math.py} (99%) diff --git a/src/pynurbs/core/basisfunction.py b/src/pynurbs/core/basisfunction.py index bb7294e..a09fbb0 100644 --- a/src/pynurbs/core/basisfunction.py +++ b/src/pynurbs/core/basisfunction.py @@ -3,7 +3,7 @@ import numpy as np -from ..cmath import totuple +from .custom_math import totuple from .knotvector import ImmutableKnotVector from .piecepoly import PiecewisePolynomial from .polynomial import Polynomial diff --git a/src/pynurbs/cmath.py b/src/pynurbs/core/custom_math.py similarity index 100% rename from src/pynurbs/cmath.py rename to src/pynurbs/core/custom_math.py diff --git a/src/pynurbs/heavy.py b/src/pynurbs/heavy.py index 8ba3a4f..1f019fc 100644 --- a/src/pynurbs/heavy.py +++ b/src/pynurbs/heavy.py @@ -11,7 +11,7 @@ import numpy as np -from .cmath import IntegratorArray, Linalg, NodeSample, number_type, totuple +from .core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple from .core.basisfunction import ImmutableBasisFunction from .core.knotvector import ImmutableKnotVector from .core.operations import ( diff --git a/tests/test_cmath.py b/tests/test_custom_math.py similarity index 99% rename from tests/test_cmath.py rename to tests/test_custom_math.py index c21fc19..0dc9be9 100644 --- a/tests/test_cmath.py +++ b/tests/test_custom_math.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from pynurbs.cmath import IntegratorArray, Linalg, Math, NodeSample +from pynurbs.core.custom_math import IntegratorArray, Linalg, Math, NodeSample @pytest.mark.order(1) diff --git a/tests/test_functions.py b/tests/test_functions.py index 62fa78b..8302d04 100644 --- a/tests/test_functions.py +++ b/tests/test_functions.py @@ -4,7 +4,7 @@ import pytest from pynurbs import Function -from pynurbs.cmath import binom +from pynurbs.core.custom_math import binom from pynurbs.knotspace import GeneratorKnotVector diff --git a/tests/test_heavy.py b/tests/test_heavy.py index 8454d6a..dd30d61 100644 --- a/tests/test_heavy.py +++ b/tests/test_heavy.py @@ -5,7 +5,9 @@ @pytest.mark.order(1) -@pytest.mark.dependency(depends=["tests/test_cmath.py::test_end"], scope="session") +@pytest.mark.dependency( + depends=["tests/test_custom_math.py::test_end"], scope="session" +) def test_begin(): pass From 1d98ae63f83302cee0e46de727778e8f353fb0f3 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 29 Jun 2025 13:08:39 +0200 Subject: [PATCH 028/116] del: remove meaningless interface classes --- src/pynurbs/__classes__.py | 114 ------------------------------------- src/pynurbs/curves.py | 3 +- src/pynurbs/functions.py | 29 +++++----- src/pynurbs/heavy.py | 2 +- src/pynurbs/knotspace.py | 4 +- 5 files changed, 16 insertions(+), 136 deletions(-) delete mode 100644 src/pynurbs/__classes__.py diff --git a/src/pynurbs/__classes__.py b/src/pynurbs/__classes__.py deleted file mode 100644 index dd2340c..0000000 --- a/src/pynurbs/__classes__.py +++ /dev/null @@ -1,114 +0,0 @@ -import abc -from typing import Tuple, Union - -import numpy as np - - -class Intface_KnotVector(abc.ABC): - @abc.abstractproperty - def degree(self) -> int: - raise NotImplementedError - - @abc.abstractproperty - def npts(self) -> int: - raise NotImplementedError - - @abc.abstractproperty - def knots(self) -> Tuple[float]: - raise NotImplementedError - - @abc.abstractmethod - def __iter__(self): - raise NotImplementedError - - @abc.abstractmethod - def __eq__(self, obj: object) -> bool: - raise NotImplementedError - - @abc.abstractmethod - def shift(self, value: float): - raise NotImplementedError - - @abc.abstractmethod - def span(self, nodes: Union[float, np.ndarray]) -> Union[int, np.ndarray]: - raise NotImplementedError - - @abc.abstractmethod - def mult(self, nodes: Union[float, np.ndarray]) -> Union[int, np.ndarray]: - raise NotImplementedError - - -class Intface_Evaluator(abc.ABC): - @abc.abstractproperty - def eval(self) -> Union[int, slice]: - raise NotImplementedError - - @abc.abstractmethod - def __call__(self, u: np.ndarray) -> np.ndarray: - raise NotImplementedError - - -class Intface_BaseFunction_BaseCurve(abc.ABC): - @abc.abstractmethod - def __eq__(self, obj: object) -> bool: - raise NotImplementedError - - @abc.abstractmethod - def __ne__(self, obj: object) -> bool: - raise NotImplementedError - - @abc.abstractproperty - def knotvector(self) -> Intface_KnotVector: - raise NotImplementedError - - @abc.abstractproperty - def degree(self) -> int: - raise NotImplementedError - - @abc.abstractproperty - def npts(self) -> int: - raise NotImplementedError - - @abc.abstractproperty - def knots(self) -> Tuple[float]: - raise NotImplementedError - - @abc.abstractproperty - def weights(self) -> Tuple[float]: - raise NotImplementedError - - -class Intface_BaseFunction(Intface_BaseFunction_BaseCurve): - @abc.abstractmethod - def __init__(self, knotvector: Intface_KnotVector): - raise NotImplementedError - - @abc.abstractmethod - def eval(self, nodes: Union[float, np.ndarray]) -> Union[float, np.ndarray]: - raise NotImplementedError - - @abc.abstractmethod - def __getitem__(self, index) -> Intface_Evaluator: - raise NotImplementedError - - -class Intface_BaseCurve(Intface_BaseFunction_BaseCurve): - @abc.abstractmethod - def __init__(self, knotvector: Intface_KnotVector, ctrlpoints: np.ndarray): - raise NotImplementedError - - @abc.abstractproperty - def ctrlpoints(self) -> np.ndarray: - raise NotImplementedError - - @abc.abstractmethod - def knot_clean(self): - raise NotImplementedError - - @abc.abstractmethod - def degree_clean(self): - raise NotImplementedError - - @abc.abstractmethod - def eval(self): - raise NotImplementedError diff --git a/src/pynurbs/curves.py b/src/pynurbs/curves.py index 5c69ca6..15db37e 100644 --- a/src/pynurbs/curves.py +++ b/src/pynurbs/curves.py @@ -7,7 +7,6 @@ import numpy as np from pynurbs import heavy -from pynurbs.__classes__ import Intface_BaseCurve from pynurbs.knotspace import KnotVector from .core.basisfunction import ImmutableBasisFunction @@ -36,7 +35,7 @@ def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: return abs(object) -class BaseCurve(Intface_BaseCurve): +class BaseCurve: def __init__(self, knotvector: KnotVector): self.__ctrlpoints = None self.__weights = None diff --git a/src/pynurbs/functions.py b/src/pynurbs/functions.py index a551a2b..56381a3 100644 --- a/src/pynurbs/functions.py +++ b/src/pynurbs/functions.py @@ -5,21 +5,20 @@ import numpy as np -from pynurbs.__classes__ import Intface_BaseFunction, Intface_Evaluator from pynurbs.core.basisfunction import ImmutableBasisFunction from pynurbs.knotspace import KnotVector from .core.tools import vectorize -class BaseFunction(Intface_BaseFunction): +class BaseFunction: def __init__(self, knotvector: KnotVector): self.knotvector = knotvector self.weights = None - def __eq__(self, other: Intface_BaseFunction) -> bool: - if not isinstance(other, Intface_BaseFunction): - return False + def __eq__(self, other: BaseFunction) -> bool: + if not isinstance(other, BaseFunction): + return NotImplemented if self.knotvector != other.knotvector: return False weightleft = self.weights @@ -28,9 +27,6 @@ def __eq__(self, other: Intface_BaseFunction) -> bool: weightrigh = np.ones(self.npts) if weightrigh is None else weightrigh return np.all(weightleft == weightrigh) - def __ne__(self, other: Intface_BaseFunction) -> bool: - return not self.__eq__(other) - def __call__(self, nodes: Union[float, np.ndarray]) -> Union[float, np.ndarray]: return self.eval(nodes) @@ -177,14 +173,13 @@ def __deepcopy__(self, memo) -> BaseFunction: return newfunc -class FunctionEvaluator(Intface_Evaluator): +class FunctionEvaluator: def __init__(self, func: BaseFunction, i: Union[int, slice], j: int): vector = func.knotvector self.__weights = func.weights self.__first_index = i self.__basis = ImmutableBasisFunction(vector.internal, j) - @vectorize(1, 0) def eval(self, node: float) -> Union[float, Tuple[float]]: """ @@ -199,10 +194,9 @@ def eval(self, node: float) -> Union[float, Tuple[float]]: result *= 1 / sum(result) return result[self.__first_index] - def __call__( - self, nodes: Union[float, Tuple[float]] - ) -> Union[float, Tuple[float], Tuple[Tuple[float]]]: - return self.eval(nodes) + @vectorize(1, 0) + def __call__(self, node: float) -> Union[float, Tuple[float]]: + return self.eval(node) class IndexableFunction(BaseFunction): @@ -242,8 +236,11 @@ def __getitem__(self, index) -> FunctionEvaluator: def eval(self, nodes: Union[float, np.ndarray]) -> Union[float, np.ndarray]: """Evaluate the given nodes""" - evaluator = self[:, self.degree] - return evaluator(nodes) + return self[:, self.degree](nodes) + + @vectorize(1, 0) + def __call__(self, node: float) -> Union[float, Tuple[float]]: + return self.eval(node) class Function(IndexableFunction): diff --git a/src/pynurbs/heavy.py b/src/pynurbs/heavy.py index 1f019fc..e803587 100644 --- a/src/pynurbs/heavy.py +++ b/src/pynurbs/heavy.py @@ -11,8 +11,8 @@ import numpy as np -from .core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple from .core.basisfunction import ImmutableBasisFunction +from .core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple from .core.knotvector import ImmutableKnotVector from .core.operations import ( increase_degree, diff --git a/src/pynurbs/knotspace.py b/src/pynurbs/knotspace.py index e188846..3bc28e9 100644 --- a/src/pynurbs/knotspace.py +++ b/src/pynurbs/knotspace.py @@ -12,8 +12,6 @@ import numpy as np -from pynurbs.__classes__ import Intface_KnotVector - from .core import ImmutableKnotVector from .core.operations import ( decrease_degree, @@ -26,7 +24,7 @@ ) -class KnotVector(Intface_KnotVector): +class KnotVector: """Creates a KnotVector instance Examples From 1996a39416a227344ee0149e71b1897267697801 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 29 Jun 2025 16:32:55 +0200 Subject: [PATCH 029/116] feat: add immutable manifold to evaluate curves --- src/pynurbs/core/manifold.py | 111 +++++++++++++++++++++++++++++++++++ 1 file changed, 111 insertions(+) create mode 100644 src/pynurbs/core/manifold.py diff --git a/src/pynurbs/core/manifold.py b/src/pynurbs/core/manifold.py new file mode 100644 index 0000000..20a46d2 --- /dev/null +++ b/src/pynurbs/core/manifold.py @@ -0,0 +1,111 @@ +from numbers import Real +from typing import Any, Generic, Iterable, Tuple, Union + +import numpy as np + +from .basisfunction import ImmutableBasisFunction + + +def permutations(numbers: Tuple[int, ...]) -> Iterable[Tuple[int, ...]]: + """ + Computes the permutations of the numbers + + Example + ------- + >>> permutations([2]) + [(0, ), (1, )] + >>> permutations([2, 3]) + [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)] + """ + if len(numbers) > 1: + for index in range(numbers[0]): + for permu in permutations(numbers[1:]): + yield (index,) + permu + else: + for index in range(numbers[0]): + yield (index,) + + +class Container(Generic[Any]): + + @staticmethod + def __find_ndim(ctrlpoints: Any) -> int: + ndim = 0 + try: + while True: + iter(ctrlpoints) + ndim += 1 + ctrlpoints = ctrlpoints[0] + except Exception: + return ndim + + @staticmethod + def __find_shape(ctrlpoints: Any, ndim: int) -> Tuple[int, ...]: + shape = [0] * ndim + for i in range(ndim): + shape[i] = len(ctrlpoints) + ctrlpoints = ctrlpoints[0] + return tuple(shape) + + def __init__(self, ctrlpoints: Any, ndim: Union[None, int] = None): + if ndim is None: + ndim = Container.__find_ndim(ctrlpoints) + self.__shape = Container.__find_shape(ctrlpoints, ndim) + self.__ctrlpoints = ctrlpoints + + @property + def ndim(self) -> int: + return len(self.shape) + + @property + def shape(self) -> Tuple[int, ...]: + return self.__shape + + def __getitem__(self, indexs: Tuple[int, ...]) -> Any: + ctrlpoint = self.__ctrlpoints + for index in indexs: + ctrlpoint = ctrlpoint[index] + return ctrlpoint + + +class ImmuntableManifold: + """ + f(x1, x2, ..., xn) + """ + + def __init__( + self, allbasis: Iterable[ImmutableBasisFunction], ctrlpoints: Container[Any] + ): + + allbasis = tuple(allbasis) + if not all(isinstance(fun, ImmutableBasisFunction) for fun in allbasis): + raise TypeError + if isinstance(ctrlpoints, Container): + if ctrlpoints.ndim != len(allbasis): + raise ValueError + else: + ctrlpoints = Container(ctrlpoints, len(allbasis)) + self.__allbasis = allbasis + self.__ctrlpoints = ctrlpoints + + @property + def ndim(self) -> int: + return len(self.__allbasis) + + @property + def shape(self) -> Tuple[int, ...]: + return tuple(basis.npts for basis in self.__allbasis) + + def __call__(self, node: Tuple[Real, ...]) -> Any: + node = tuple(node) + if len(node) != self.ndim: + raise ValueError + result = 0 * self.__ctrlpoints[*(0,) * self.ndim] + basivalues = [basis.eval(nodei) for nodei, basis in zip(node, self.__allbasis)] + for indexs in permutations(self.shape): + scalar = 1 + for i, index in enumerate(indexs): + scalar *= basivalues[i][index] + if scalar: + result += scalar * self.__ctrlpoints[*indexs] + return result From 3ba187a06df8964de01632aac6fdaea0148c8aa0 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Mon, 30 Jun 2025 22:11:07 +0200 Subject: [PATCH 030/116] refactor: remove eval function to use __call__ --- src/pynurbs/core/basisfunction.py | 8 ++--- src/pynurbs/core/manifold.py | 2 +- src/pynurbs/core/piecepoly.py | 4 +-- src/pynurbs/core/polynomial.py | 32 ++++--------------- src/pynurbs/functions.py | 23 ++------------ tests/core/test_polynomial.py | 52 +++++++------------------------ 6 files changed, 26 insertions(+), 95 deletions(-) diff --git a/src/pynurbs/core/basisfunction.py b/src/pynurbs/core/basisfunction.py index a09fbb0..986ac42 100644 --- a/src/pynurbs/core/basisfunction.py +++ b/src/pynurbs/core/basisfunction.py @@ -94,8 +94,7 @@ def __getitem__(self, index: int) -> PiecewisePolynomial: functions = self.__matrix[index] return PiecewisePolynomial(functions, self.knots) - def eval(self, node: Real, times: int = 0) -> Tuple[Real, ...]: - + def __call__(self, node: Real) -> Tuple[Real, ...]: npts = self.__knotvector.npts knots = self.__knotvector.knots spans = self.__knotvector.span(knots) @@ -109,8 +108,5 @@ def eval(self, node: Real, times: int = 0) -> Tuple[Real, ...]: for y in range(self.__degree + 1): i = y + span - degree polynomial = self.__matrix[ind][y] - result[i] = polynomial.eval(shifnode, times) + result[i] = polynomial(shifnode) return tuple(result) - - def __call__(self, node: Real) -> Tuple[Real, ...]: - return self.eval(node, 0) diff --git a/src/pynurbs/core/manifold.py b/src/pynurbs/core/manifold.py index 20a46d2..44d2c7e 100644 --- a/src/pynurbs/core/manifold.py +++ b/src/pynurbs/core/manifold.py @@ -101,7 +101,7 @@ def __call__(self, node: Tuple[Real, ...]) -> Any: if len(node) != self.ndim: raise ValueError result = 0 * self.__ctrlpoints[*(0,) * self.ndim] - basivalues = [basis.eval(nodei) for nodei, basis in zip(node, self.__allbasis)] + basivalues = [basis(nodei) for nodei, basis in zip(node, self.__allbasis)] for indexs in permutations(self.shape): scalar = 1 for i, index in enumerate(indexs): diff --git a/src/pynurbs/core/piecepoly.py b/src/pynurbs/core/piecepoly.py index 2bdbd7d..fdaeeee 100644 --- a/src/pynurbs/core/piecepoly.py +++ b/src/pynurbs/core/piecepoly.py @@ -29,9 +29,9 @@ def eval(self, node: Real, times: int = 0) -> Real: nsegs = len(self.functions) mask = self.knots[nsegs - 1] <= node mask *= node < self.knots[nsegs] - result = mask * self.functions[-1].eval(node, times) + result = mask * self.functions[-1](node, times) for i in range(nsegs - 1): mask = self.knots[i] <= node mask *= node < self.knots[i + 1] - result += mask * self.functions[i].eval(node, times) + result += mask * self.functions[i](node, times) return result diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 033503f..9df8b8b 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -104,7 +104,12 @@ def __rmul__(self, other: Real) -> Polynomial: return self.__mul__(other) def __call__(self, node: Real) -> Real: - return self.eval(node, 0) + if self.degree == 0: + return self[0] + result: Real = 0 * self[0] + for coef in self[::-1]: + result = node * result + coef + return result def __str__(self): if self.degree == 0: @@ -136,31 +141,6 @@ def __str__(self): def __repr__(self) -> str: return str(self) - def eval(self, node: Real, times: int = 0) -> Real: - """ - Evaluates the polynomial at given node - - Example - ------- - >>> poly = Polynomial([1, 2]) - >>> poly.eval(0) - 1 - >>> poly.eval(1) - 3 - >>> poly.eval(1, 1) - 2 - >>> poly.eval(1, 2) - 0 - """ - if times: - return derivate(self, times).eval(node, 0) - if self.degree == 0: - return self[0] - result: Real = 0 * self[0] - for coef in self[::-1]: - result = node * result + coef - return result - def scale(polynomial: Polynomial, amount: Real) -> Polynomial: """ diff --git a/src/pynurbs/functions.py b/src/pynurbs/functions.py index 56381a3..100945b 100644 --- a/src/pynurbs/functions.py +++ b/src/pynurbs/functions.py @@ -27,9 +27,6 @@ def __eq__(self, other: BaseFunction) -> bool: weightrigh = np.ones(self.npts) if weightrigh is None else weightrigh return np.all(weightleft == weightrigh) - def __call__(self, nodes: Union[float, np.ndarray]) -> Union[float, np.ndarray]: - return self.eval(nodes) - @property def knotvector(self) -> KnotVector: """The knotvector of the current basis function @@ -181,23 +178,13 @@ def __init__(self, func: BaseFunction, i: Union[int, slice], j: int): self.__basis = ImmutableBasisFunction(vector.internal, j) @vectorize(1, 0) - def eval(self, node: float) -> Union[float, Tuple[float]]: - """ - If i is integer, u is float -> float - If i is integer, u is Tuple[float], ndim = k -> np.ndarray, ndim = k - If i is slice, u is float -> Tuple[float] - if i is slice, u is Tuple[float], ndim = k -> Tuple[Tuple[float]], ndim = k+1 - """ - result = self.__basis.eval(node) + def __call__(self, node: float) -> Union[float, Tuple[float]]: + result = self.__basis(node) if self.__weights is not None: result *= self.__weights result *= 1 / sum(result) return result[self.__first_index] - @vectorize(1, 0) - def __call__(self, node: float) -> Union[float, Tuple[float]]: - return self.eval(node) - class IndexableFunction(BaseFunction): """ @@ -234,13 +221,9 @@ def __getitem__(self, index) -> FunctionEvaluator: self.__valid_second_index(j) return FunctionEvaluator(self, i, j) - def eval(self, nodes: Union[float, np.ndarray]) -> Union[float, np.ndarray]: - """Evaluate the given nodes""" - return self[:, self.degree](nodes) - @vectorize(1, 0) def __call__(self, node: float) -> Union[float, Tuple[float]]: - return self.eval(node) + return self[:, self.degree](node) class Function(IndexableFunction): diff --git a/tests/core/test_polynomial.py b/tests/core/test_polynomial.py index 675ba16..b84734a 100644 --- a/tests/core/test_polynomial.py +++ b/tests/core/test_polynomial.py @@ -30,21 +30,21 @@ def test_degree(): @pytest.mark.dependency(depends=["test_build", "test_degree"]) def test_evaluate(): poly = Polynomial([0]) # p(x) = 0 - assert poly.eval(0) == 0 - assert poly.eval(-1) == 0 - assert poly.eval(2) == 0 + assert poly(0) == 0 + assert poly(-1) == 0 + assert poly(2) == 0 poly = Polynomial([1]) # p(x) = 1 - assert poly.eval(0) == 1 - assert poly.eval(-1) == 1 - assert poly.eval(2) == 1 + assert poly(0) == 1 + assert poly(-1) == 1 + assert poly(2) == 1 poly = Polynomial([1, 2]) # p(x) = 1 + 2 * x - assert poly.eval(0) == 1 - assert poly.eval(-1) == 1 + 2 * (-1) - assert poly.eval(2) == 1 + 2 * (+2) + assert poly(0) == 1 + assert poly(-1) == 1 + 2 * (-1) + assert poly(2) == 1 + 2 * (+2) poly = Polynomial([1, 2, 3]) # p(x) = 1 + 2 * x + 3 * x^2 - assert poly.eval(0) == 1 - assert poly.eval(-1) == 1 + 2 * (-1) + 3 * (-1) * (-1) - assert poly.eval(2) == 1 + 2 * (+2) + 3 * (+2) * (+2) + assert poly(0) == 1 + assert poly(-1) == 1 + 2 * (-1) + 3 * (-1) * (-1) + assert poly(2) == 1 + 2 * (+2) + 3 * (+2) * (+2) @pytest.mark.order(1) @@ -238,33 +238,6 @@ def test_integrate(): assert integrate(poly, (-2, 1)) == 15 -@pytest.mark.order(1) -@pytest.mark.dependency( - depends=[ - "test_build", - "test_degree", - "test_evaluate", - "test_add", - "test_mul", - "test_derivate", - ] -) -def test_evaluate_derivate(): - import numpy as np - - ntests = 100 - maxdeg = 6 - tvalues = np.linspace(-1, 1, 129) - for _ in range(ntests): - dega = np.random.randint(0, maxdeg + 1) - coefsa = np.random.uniform(-1, 1, dega + 1) - polya = Polynomial(coefsa) - for times in range(dega + 1): - dpolya = derivate(polya, times) - for tval in tvalues: - assert polya.eval(tval, times) == dpolya.eval(tval, 0) - - @pytest.mark.order(1) @pytest.mark.dependency( depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] @@ -339,7 +312,6 @@ def test_print(): "test_mul", "test_truediv", "test_pow", - "test_derivate", "test_integrate", "test_shift", "test_scale", From 927a062e72277ba24219ad5846b0c51fa83596cc Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Mon, 30 Jun 2025 22:36:22 +0200 Subject: [PATCH 031/116] feat: implement __add__, __sub__, __mul__ on piecewise --- src/pynurbs/core/piecepoly.py | 116 ++++++++++++++++++++++++++++++---- 1 file changed, 105 insertions(+), 11 deletions(-) diff --git a/src/pynurbs/core/piecepoly.py b/src/pynurbs/core/piecepoly.py index fdaeeee..cf81b07 100644 --- a/src/pynurbs/core/piecepoly.py +++ b/src/pynurbs/core/piecepoly.py @@ -2,10 +2,47 @@ Defines the Piecewise Polynomial class """ +from __future__ import annotations + from numbers import Real -from typing import Iterable, Tuple +from typing import Iterable, Tuple, Union from .polynomial import Polynomial +from .tools import vectorize + + +def find_span(node: Real, knots: Tuple[Real, ...]): + """ + Finds the span of the given node + + Example + ------- + >>> knots = [0, 1, 3, 4] + >>> find_span(-1, knots) + -1 + >>> find_span(0, knots) + 0 + >>> find_span(0.5, knots) + 0 + >>> find_span(1, knots) + 1 + >>> find_span(2, knots) + 1 + >>> find_span(3, knots) + 2 + >>> find_span(4, knots) + 2 + >>> find_span(5, knots) + 3 + """ + if node < knots[0]: + return -1 + if knots[-1] < node: + return len(knots) - 1 + for i, knot in enumerate(knots[:-1]): + if knot <= node: + return i + return len(knots) - 2 class PiecewisePolynomial: @@ -25,13 +62,70 @@ def knots(self) -> Tuple[Real, ...]: def functions(self) -> Tuple[Polynomial, ...]: return self.__functions - def eval(self, node: Real, times: int = 0) -> Real: - nsegs = len(self.functions) - mask = self.knots[nsegs - 1] <= node - mask *= node < self.knots[nsegs] - result = mask * self.functions[-1](node, times) - for i in range(nsegs - 1): - mask = self.knots[i] <= node - mask *= node < self.knots[i + 1] - result += mask * self.functions[i](node, times) - return result + @vectorize(1, 0) + def __call__(self, node: Real) -> Real: + span = find_span(node, self.knots) + function = self.functions[span] + return function(node) + + def __neg__(self) -> Polynomial: + return self.__class__((-func for func in self.functions), self.knots) + + def __add__( + self, other: Union[Real, Polynomial, PiecewisePolynomial] + ) -> PiecewisePolynomial: + if not isinstance(other, PiecewisePolynomial): + return self.__class__((func + other for func in self.functions), self.knots) + allknots = sorted(set(self.knots) | set(other.knots)) + functions = [None] * (len(allknots) - 1) + for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): + midknot = (knota + knotb) / 2 + spana = find_span(midknot, self.knots) + spanb = find_span(midknot, self.knots) + functions[i] = self.functions[spana] + other.functions[spanb] + return self.__class__(functions, allknots) + + def __mul__( + self, other: Union[Real, Polynomial, PiecewisePolynomial] + ) -> PiecewisePolynomial: + if not isinstance(other, PiecewisePolynomial): + return self.__class__((func + other for func in self.functions), self.knots) + allknots = sorted(set(self.knots) | set(other.knots)) + functions = [None] * (len(allknots) - 1) + for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): + midknot = (knota + knotb) / 2 + spana = find_span(midknot, self.knots) + spanb = find_span(midknot, self.knots) + functions[i] = self.functions[spana] * other.functions[spanb] + return self.__class__(functions, allknots) + + def __matmul__( + self, other: Union[Real, Polynomial, PiecewisePolynomial] + ) -> PiecewisePolynomial: + if not isinstance(other, PiecewisePolynomial): + return self.__class__((func + other for func in self.functions), self.knots) + allknots = sorted(set(self.knots) | set(other.knots)) + functions = [None] * (len(allknots) - 1) + for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): + midknot = (knota + knotb) / 2 + spana = find_span(midknot, self.knots) + spanb = find_span(midknot, self.knots) + functions[i] = self.functions[spana] @ other.functions[spanb] + return self.__class__(functions, allknots) + + def __sub__( + self, other: Union[Real, Polynomial, PiecewisePolynomial] + ) -> PiecewisePolynomial: + return self.__add__(-other) + + def __rsub__(self, other: Real) -> PiecewisePolynomial: + return (-self).__add__(other) + + def __radd__(self, other: Real) -> PiecewisePolynomial: + return self.__add__(other) + + def __rmul__(self, other: Real) -> PiecewisePolynomial: + return self.__mul__(other) + + def __rmatmul__(self, other: Real) -> PiecewisePolynomial: + return self.__matmul__(other) From c0aa51b22c643665c70940cae8774cfd429452b0 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Mon, 30 Jun 2025 22:38:02 +0200 Subject: [PATCH 032/116] feat: add __matmul__ for Polynomial --- src/pynurbs/core/polynomial.py | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 9df8b8b..25e37e8 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -81,6 +81,16 @@ def __mul__(self, other: Union[Real, Polynomial]) -> Polynomial: coefs = tuple(other * coef for coef in self) return self.__class__(coefs) + def __matmul__(self, other: Union[Real, Polynomial]) -> Polynomial: + if isinstance(other, Polynomial): + coefs = [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) + def __truediv__(self, other: Real) -> Polynomial: coefs = (coef / other for coef in self) return self.__class__(coefs) @@ -103,6 +113,9 @@ def __radd__(self, other: Real) -> Polynomial: def __rmul__(self, other: Real) -> Polynomial: return self.__mul__(other) + def __rmatmul__(self, other: Real) -> Polynomial: + return self.__matmul__(other) + def __call__(self, node: Real) -> Real: if self.degree == 0: return self[0] From f70593e35aa8b0e269bd56a23bf75988b03bedf7 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 21:27:04 +0200 Subject: [PATCH 033/116] fix: Polynomial receives numpy array as argument --- src/pynurbs/core/polynomial.py | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 25e37e8..1b2fcde 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -35,7 +35,10 @@ def __init__(self, coefs: Iterable[Real]): 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), default=0) + if isinstance(coefs[0], Real): + degree = max((i for i, v in enumerate(coefs) if v), default=0) + else: + degree = len(coefs) - 1 self.__coefs = tuple(coefs[: degree + 1]) @property @@ -128,6 +131,13 @@ def __str__(self): if self.degree == 0: return str(self[0]) msgs: List[str] = [] + if not isinstance(self[0], Real): + msgs.append(f"({self[0]})") + if self.degree > 0: + msgs.append(f"({self[1]}) * x") + for i, coef in enumerate(self[2:]): + msgs.append(f"({coef}) * x^{i+2}") + return " + ".join(msgs) flag = False for i, coef in enumerate(self): if coef == 0: From 34fcd71d43a3dc30cba2d50e583d4d27d66c5c01 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 21:27:33 +0200 Subject: [PATCH 034/116] fix: operations __add__, __mul__ and __matmul__ for piecewise --- src/pynurbs/core/piecepoly.py | 34 +++++++++++++++++++++++++++------- 1 file changed, 27 insertions(+), 7 deletions(-) diff --git a/src/pynurbs/core/piecepoly.py b/src/pynurbs/core/piecepoly.py index cf81b07..ef93b53 100644 --- a/src/pynurbs/core/piecepoly.py +++ b/src/pynurbs/core/piecepoly.py @@ -39,8 +39,8 @@ def find_span(node: Real, knots: Tuple[Real, ...]): return -1 if knots[-1] < node: return len(knots) - 1 - for i, knot in enumerate(knots[:-1]): - if knot <= node: + for i, knot in enumerate(knots[1:]): + if node < knot: return i return len(knots) - 2 @@ -51,8 +51,12 @@ class PiecewisePolynomial: """ def __init__(self, functions: Iterable[Polynomial], knots: Iterable[Real]) -> None: - self.__functions = tuple(functions) - self.__knots = tuple(knots) + functions = tuple(functions) + knots = tuple(knots) + if len(knots) != 1 + len(functions): + raise ValueError(f"{len(knots)} != 1 + {len(functions)}") + self.__functions = functions + self.__knots = knots @property def knots(self) -> Tuple[Real, ...]: @@ -62,6 +66,22 @@ def knots(self) -> Tuple[Real, ...]: def functions(self) -> Tuple[Polynomial, ...]: return self.__functions + def __str__(self) -> str: + msgs = [] + for i, functioni in enumerate(self.functions): + knota, knotb = self.knots[i], self.knots[i + 1] + if i + 1 != len(self.functions): + msgs.append(f"[{knota}, {knotb}): {functioni}") + return " ".join(msgs) + + def __repr__(self) -> str: + msgs = [] + for i, functioni in enumerate(self.functions): + knota, knotb = self.knots[i], self.knots[i + 1] + if i + 1 != len(self.functions): + msgs.append(f"[{knota}, {knotb}): {repr(functioni)}") + return " ".join(msgs) + @vectorize(1, 0) def __call__(self, node: Real) -> Real: span = find_span(node, self.knots) @@ -81,7 +101,7 @@ def __add__( for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): midknot = (knota + knotb) / 2 spana = find_span(midknot, self.knots) - spanb = find_span(midknot, self.knots) + spanb = find_span(midknot, other.knots) functions[i] = self.functions[spana] + other.functions[spanb] return self.__class__(functions, allknots) @@ -95,7 +115,7 @@ def __mul__( for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): midknot = (knota + knotb) / 2 spana = find_span(midknot, self.knots) - spanb = find_span(midknot, self.knots) + spanb = find_span(midknot, other.knots) functions[i] = self.functions[spana] * other.functions[spanb] return self.__class__(functions, allknots) @@ -109,7 +129,7 @@ def __matmul__( for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): midknot = (knota + knotb) / 2 spana = find_span(midknot, self.knots) - spanb = find_span(midknot, self.knots) + spanb = find_span(midknot, other.knots) functions[i] = self.functions[spana] @ other.functions[spanb] return self.__class__(functions, allknots) From 11aab7f0e76fd24cd4086b142215ff73d9b89a7d Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 21:27:42 +0200 Subject: [PATCH 035/116] test: add tests for piecewise functions --- tests/core/test_piecewise.py | 201 +++++++++++++++++++++++++++++++++++ 1 file changed, 201 insertions(+) create mode 100644 tests/core/test_piecewise.py diff --git a/tests/core/test_piecewise.py b/tests/core/test_piecewise.py new file mode 100644 index 0000000..7d3f895 --- /dev/null +++ b/tests/core/test_piecewise.py @@ -0,0 +1,201 @@ +from fractions import Fraction +from numbers import Integral, Real +from typing import Tuple + +import numpy as np +import pytest + +from pynurbs.core.piecepoly import PiecewisePolynomial, Polynomial, find_span + + +def get_random_knots( + start: Real, end: Real, nsegs: int, cls: type = float +) -> Tuple[Real, ...]: + """ + Computes the (nsegs+1) knots that are in the interval [start, end] + These knots are randomly distributed in + """ + nodes = np.cumsum(np.random.randint(1, 17, nsegs)) + if cls is int: + cls = Fraction + nodes = [cls(0)] + list(map(cls, nodes)) + nodes = [start + node * (end - start) / nodes[-1] for node in nodes] + return tuple(nodes) + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=[ + "tests/core/test_knotvector.py::test_end", + ], + scope="session", +) +def test_begin(): + pass + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_begin"]) +def test_find_span(): + knots = [0, 1, 2, 3, 4] + assert find_span(0, knots) == 0 + assert find_span(1, knots) == 1 + assert find_span(2, knots) == 2 + assert find_span(3, knots) == 3 + assert find_span(4, knots) == 3 + + assert find_span(0.5, knots) == 0 + assert find_span(1.5, knots) == 1 + assert find_span(2.5, knots) == 2 + assert find_span(3.5, knots) == 3 + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_begin"]) +def test_build(): + x = Polynomial([0, 1]) + polys = [x, 1 - x, x * x, 3 - x * x * x] + PiecewisePolynomial(polys, range(1 + len(polys))) + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_find_span"]) +def test_evaluate(): + x = Polynomial([0, 1]) + polys = [x, 1 - x, x * x, 3 - x * x * x] + piece = PiecewisePolynomial(polys, range(1 + len(polys))) + + assert piece(0) == 0 + assert piece(0.5) == 0.5 + assert piece(1) == 0 + assert piece(1.5) == -0.5 + assert piece(2) == 4 + assert piece(2.5) == 6.25 + assert piece(3) == -24 + assert piece(4) == -61 + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build"]) +def test_compare(): + x = Polynomial([0, 1]) + polysa = [x, 1 - x, x * x, 3 - x * x * x] + polysb = [-x, x - 1, -x * x, x * x * x - 3] + piecea = PiecewisePolynomial(polysa, range(1 + len(polysa))) + pieceb = PiecewisePolynomial(polysb, range(1 + len(polysb))) + + assert piecea == piecea + assert pieceb == pieceb + assert piecea != pieceb + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_evaluate"]) +def test_add(): + nsegs, degree = 6, 4 + + knotsa = get_random_knots(0, 1, nsegs) + coefsa = np.random.randint(-10, 11, (nsegs, degree + 1)) + piecea = PiecewisePolynomial(map(Polynomial, coefsa), knotsa) + + knotsb = get_random_knots(0, 1, nsegs) + coefsb = np.random.randint(-10, 11, (nsegs, degree + 1)) + pieceb = PiecewisePolynomial(map(Polynomial, coefsb), knotsb) + + piecec = piecea + pieceb + + for x in np.linspace(0, 1, 129): + assert abs(piecea(x) + pieceb(x) - piecec(x)) < 1e-9 + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_evaluate"]) +def test_neg(): + nsegs, degree = 6, 4 + knots = get_random_knots(0, 10, nsegs) + coefs = np.random.randint(-10, 11, (nsegs, degree + 1)) + piecea = PiecewisePolynomial(map(Polynomial, coefs), knots) + pieceb = -piecea + + for x in np.linspace(knots[0], knots[-1], 129): + assert pieceb(x) == -piecea(x) + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["test_build", "test_evaluate", "test_add", "test_neg"]) +def test_sub(): + nsegs, degree = 6, 4 + + knotsa = get_random_knots(0, 1, nsegs) + coefsa = np.random.randint(-10, 11, (nsegs, degree + 1)) + piecea = PiecewisePolynomial(map(Polynomial, coefsa), knotsa) + + knotsb = get_random_knots(0, 1, nsegs) + coefsb = np.random.randint(-10, 11, (nsegs, degree + 1)) + pieceb = PiecewisePolynomial(map(Polynomial, coefsb), knotsb) + + piecec = piecea - pieceb + pieced = piecea + (-pieceb) + + for x in np.linspace(0, 1, 129): + assert abs(piecea(x) - pieceb(x) - piecec(x)) < 1e-9 + assert abs(piecea(x) - pieceb(x) - pieced(x)) < 1e-9 + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=["test_build", "test_evaluate", "test_neg", "test_add", "test_sub"] +) +def test_mul(): + nsegs, degree = 6, 4 + + knotsa = get_random_knots(0, 1, nsegs) + coefsa = np.random.randint(-10, 11, (nsegs, degree + 1)) + piecea = PiecewisePolynomial(map(Polynomial, coefsa), knotsa) + + knotsb = get_random_knots(0, 1, nsegs) + coefsb = np.random.randint(-10, 11, (nsegs, degree + 1)) + pieceb = PiecewisePolynomial(map(Polynomial, coefsb), knotsb) + + piecec = piecea * pieceb + + for x in np.linspace(0, 1, 129): + assert abs(piecea(x) * pieceb(x) - piecec(x)) < 1e-9 + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=["test_build", "test_neg", "test_add", "test_sub", "test_mul"] +) +def test_matmul(): + nsegs, degree = 6, 4 + + knotsa = get_random_knots(0, 1, nsegs) + coefsa = np.random.randint(-10, 11, (nsegs, degree + 1, 3)) + piecea = PiecewisePolynomial(map(Polynomial, coefsa), knotsa) + + knotsb = get_random_knots(0, 1, nsegs) + coefsb = np.random.randint(-10, 11, (nsegs, degree + 1, 3)) + pieceb = PiecewisePolynomial(map(Polynomial, coefsb), knotsb) + + piecec = piecea @ pieceb + + for x in np.linspace(0, 1, 129): + assert abs(piecea(x) @ pieceb(x) - piecec(x)) < 1e-9 + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_build", + "test_evaluate", + "test_neg", + "test_add", + "test_sub", + "test_mul", + "test_matmul", + ] +) +def test_all(): + pass From 46f12ec7fb9cef8966d399d9361eb5b49f39a470 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 21:31:33 +0200 Subject: [PATCH 036/116] fix: raise ValueError when find_span outside interval --- src/pynurbs/core/piecepoly.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/src/pynurbs/core/piecepoly.py b/src/pynurbs/core/piecepoly.py index ef93b53..9f8a9cc 100644 --- a/src/pynurbs/core/piecepoly.py +++ b/src/pynurbs/core/piecepoly.py @@ -35,10 +35,8 @@ def find_span(node: Real, knots: Tuple[Real, ...]): >>> find_span(5, knots) 3 """ - if node < knots[0]: - return -1 - if knots[-1] < node: - return len(knots) - 1 + if node < knots[0] or knots[-1] < node: + raise ValueError(f"Node not inside [{knots[0]}, {knots[-1]}]") for i, knot in enumerate(knots[1:]): if node < knot: return i From 0c9c5239e90d5b8738a7fd3a10e598c2962927bf Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 21:46:25 +0200 Subject: [PATCH 037/116] fix: __matmul__ function for polynomials --- src/pynurbs/core/polynomial.py | 15 ++++++++------- 1 file changed, 8 insertions(+), 7 deletions(-) diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 1b2fcde..936e308 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -85,13 +85,14 @@ def __mul__(self, other: Union[Real, Polynomial]) -> Polynomial: return self.__class__(coefs) def __matmul__(self, other: Union[Real, Polynomial]) -> Polynomial: - if isinstance(other, Polynomial): - coefs = [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) + if not isinstance(other, Polynomial): + newcoefs = tuple(coef @ other for coef in self) + print(newcoefs) + return self.__class__(newcoefs) + coefs = [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) def __truediv__(self, other: Real) -> Polynomial: From d45601b210c0376499b8aa0349d6f83419ec2819 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 21:46:59 +0200 Subject: [PATCH 038/116] fix: __matmul__ and __mul__ operation for Piecewise --- src/pynurbs/core/piecepoly.py | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/src/pynurbs/core/piecepoly.py b/src/pynurbs/core/piecepoly.py index 9f8a9cc..ca614e8 100644 --- a/src/pynurbs/core/piecepoly.py +++ b/src/pynurbs/core/piecepoly.py @@ -107,7 +107,7 @@ def __mul__( self, other: Union[Real, Polynomial, PiecewisePolynomial] ) -> PiecewisePolynomial: if not isinstance(other, PiecewisePolynomial): - return self.__class__((func + other for func in self.functions), self.knots) + return self.__class__((func * other for func in self.functions), self.knots) allknots = sorted(set(self.knots) | set(other.knots)) functions = [None] * (len(allknots) - 1) for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): @@ -121,7 +121,7 @@ def __matmul__( self, other: Union[Real, Polynomial, PiecewisePolynomial] ) -> PiecewisePolynomial: if not isinstance(other, PiecewisePolynomial): - return self.__class__((func + other for func in self.functions), self.knots) + return self.__class__((func @ other for func in self.functions), self.knots) allknots = sorted(set(self.knots) | set(other.knots)) functions = [None] * (len(allknots) - 1) for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): @@ -144,6 +144,3 @@ def __radd__(self, other: Real) -> PiecewisePolynomial: def __rmul__(self, other: Real) -> PiecewisePolynomial: return self.__mul__(other) - - def __rmatmul__(self, other: Real) -> PiecewisePolynomial: - return self.__matmul__(other) From 947b0fe45fa9e1027b102ed1fba4541cc66e4fde Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 21:47:55 +0200 Subject: [PATCH 039/116] test: add piecewise operations with scalars --- tests/core/test_piecewise.py | 52 ++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) diff --git a/tests/core/test_piecewise.py b/tests/core/test_piecewise.py index 7d3f895..c196205 100644 --- a/tests/core/test_piecewise.py +++ b/tests/core/test_piecewise.py @@ -184,6 +184,57 @@ def test_matmul(): assert abs(piecea(x) @ pieceb(x) - piecec(x)) < 1e-9 +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=[ + "test_build", + "test_neg", + "test_add", + "test_sub", + "test_mul", + "test_matmul", + ] +) +def test_scalar_operation(): + nsegs, degree = 6, 4 + + knotsa = get_random_knots(0, 1, nsegs) + nodes = np.linspace(0, 1, 129) + + for _ in range(10): # number of tests + const = np.random.randint(-10, 11) + coefsa = np.random.randint(-10, 11, (nsegs, degree + 1)) + piecea = PiecewisePolynomial(map(Polynomial, coefsa), knotsa) + + pieceb = piecea + const + piecec = const + piecea + for node in nodes: + assert abs(piecea(node) + const - pieceb(node)) < 1e-9 + assert abs(const + piecea(node) - piecec(node)) < 1e-9 + + pieceb = piecea - const + piecec = const - piecea + for node in nodes: + assert abs(piecea(node) - const - pieceb(node)) < 1e-9 + assert abs(const - piecea(node) - piecec(node)) < 1e-9 + + pieceb = piecea * const + piecec = const * piecea + for node in nodes: + assert abs(piecea(node) * const - pieceb(node)) < 1e-9 + assert abs(const * piecea(node) - piecec(node)) < 1e-9 + + ndim = 3 + for _ in range(10): # number of tests + const = np.random.randint(-10, 11, (ndim,)) + coefsa = np.random.randint(-10, 11, (nsegs, degree + 1, ndim)) + piecea = PiecewisePolynomial(map(Polynomial, coefsa), knotsa) + + pieceb = piecea @ const + for node in nodes: + assert abs(piecea(node) @ const - pieceb(node)) < 1e-9 + + @pytest.mark.order(1) @pytest.mark.dependency( depends=[ @@ -195,6 +246,7 @@ def test_matmul(): "test_sub", "test_mul", "test_matmul", + "test_scalar_operation", ] ) def test_all(): From 3dc9f6266f7df70055b22777899a11dbf1d5b108 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 21:53:08 +0200 Subject: [PATCH 040/116] fix: __str__ and __repr__ for piecewise polynomial --- src/pynurbs/core/piecepoly.py | 8 ++++++-- tests/core/test_piecewise.py | 16 ++++++++++++++++ 2 files changed, 22 insertions(+), 2 deletions(-) diff --git a/src/pynurbs/core/piecepoly.py b/src/pynurbs/core/piecepoly.py index ca614e8..9368283 100644 --- a/src/pynurbs/core/piecepoly.py +++ b/src/pynurbs/core/piecepoly.py @@ -70,7 +70,9 @@ def __str__(self) -> str: knota, knotb = self.knots[i], self.knots[i + 1] if i + 1 != len(self.functions): msgs.append(f"[{knota}, {knotb}): {functioni}") - return " ".join(msgs) + else: + msgs.append(f"[{knota}, {knotb}]: {functioni}") + return "{" + ", ".join(msgs) + "}" def __repr__(self) -> str: msgs = [] @@ -78,7 +80,9 @@ def __repr__(self) -> str: knota, knotb = self.knots[i], self.knots[i + 1] if i + 1 != len(self.functions): msgs.append(f"[{knota}, {knotb}): {repr(functioni)}") - return " ".join(msgs) + else: + msgs.append(f"[{knota}, {knotb}]: {repr(functioni)}") + return "{" + ", ".join(msgs) + "}" @vectorize(1, 0) def __call__(self, node: Real) -> Real: diff --git a/tests/core/test_piecewise.py b/tests/core/test_piecewise.py index c196205..9646f14 100644 --- a/tests/core/test_piecewise.py +++ b/tests/core/test_piecewise.py @@ -235,6 +235,21 @@ def test_scalar_operation(): assert abs(piecea(node) @ const - pieceb(node)) < 1e-9 +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=[ + "test_build", + ] +) +def test_print(): + x = Polynomial([0, 1]) + knots = [0, 1, 2] + polys = [x, 1 - x] + piecewise = PiecewisePolynomial(polys, knots) + assert str(piecewise) == r"{[0, 1): x, [1, 2]: 1 - x}" + repr(piecewise) + + @pytest.mark.order(1) @pytest.mark.dependency( depends=[ @@ -247,6 +262,7 @@ def test_scalar_operation(): "test_mul", "test_matmul", "test_scalar_operation", + "test_print", ] ) def test_all(): From 027fc9aa94cfb6258c52774214a748e3394e0224 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 21:57:43 +0200 Subject: [PATCH 041/116] refactor: move operations of knotvectors to a separated folder --- src/pynurbs/{core/operations.py => operations/knotvector.py} | 2 +- .../{core/test_operations.py => operations/test_knotvector.py} | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) rename src/pynurbs/{core/operations.py => operations/knotvector.py} (99%) rename tests/{core/test_operations.py => operations/test_knotvector.py} (97%) diff --git a/src/pynurbs/core/operations.py b/src/pynurbs/operations/knotvector.py similarity index 99% rename from src/pynurbs/core/operations.py rename to src/pynurbs/operations/knotvector.py index 5016003..b191057 100644 --- a/src/pynurbs/core/operations.py +++ b/src/pynurbs/operations/knotvector.py @@ -1,7 +1,7 @@ from numbers import Real from typing import Iterable -from .knotvector import ImmutableKnotVector +from ..core.knotvector import ImmutableKnotVector def insert_knots( diff --git a/tests/core/test_operations.py b/tests/operations/test_knotvector.py similarity index 97% rename from tests/core/test_operations.py rename to tests/operations/test_knotvector.py index ab35143..3f6146c 100644 --- a/tests/core/test_operations.py +++ b/tests/operations/test_knotvector.py @@ -1,7 +1,7 @@ import pytest from pynurbs.core.knotvector import ImmutableKnotVector -from pynurbs.core.operations import ( +from pynurbs.operations.knotvector import ( decrease_degree, increase_degree, insert_knots, From 75d1acb78c743171416a93da9785b74c4417e19b Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:00:18 +0200 Subject: [PATCH 042/116] test: move custom_math to core folder --- tests/{ => core}/test_custom_math.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{ => core}/test_custom_math.py (100%) diff --git a/tests/test_custom_math.py b/tests/core/test_custom_math.py similarity index 100% rename from tests/test_custom_math.py rename to tests/core/test_custom_math.py From 89a0056b075636b5656fa7654973417674408818 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:06:33 +0200 Subject: [PATCH 043/116] dev: add __init__ file on operations and tests --- src/pynurbs/operations/__init__.py | 1 + tests/operations/__init__.py | 3 +++ 2 files changed, 4 insertions(+) create mode 100644 src/pynurbs/operations/__init__.py create mode 100644 tests/operations/__init__.py diff --git a/src/pynurbs/operations/__init__.py b/src/pynurbs/operations/__init__.py new file mode 100644 index 0000000..3e2e643 --- /dev/null +++ b/src/pynurbs/operations/__init__.py @@ -0,0 +1 @@ +from .knotvector import insert_knots, increase_degree, remove_knots, decrease_degree diff --git a/tests/operations/__init__.py b/tests/operations/__init__.py new file mode 100644 index 0000000..04ddc64 --- /dev/null +++ b/tests/operations/__init__.py @@ -0,0 +1,3 @@ +import sys + +sys.path.append("./src") From 7abc89dd19149f2f3b99a2677ebcd662a295082d Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:07:12 +0200 Subject: [PATCH 044/116] fix: import due to move core.operations to operations.knotvector --- src/pynurbs/curves.py | 2 +- src/pynurbs/heavy.py | 2 +- src/pynurbs/knotspace.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/pynurbs/curves.py b/src/pynurbs/curves.py index 15db37e..3a2d50d 100644 --- a/src/pynurbs/curves.py +++ b/src/pynurbs/curves.py @@ -10,7 +10,7 @@ from pynurbs.knotspace import KnotVector from .core.basisfunction import ImmutableBasisFunction -from .core.operations import ( +from .operations.knotvector import ( decrease_degree, increase_degree, insert_knots, diff --git a/src/pynurbs/heavy.py b/src/pynurbs/heavy.py index e803587..23aee82 100644 --- a/src/pynurbs/heavy.py +++ b/src/pynurbs/heavy.py @@ -14,7 +14,7 @@ from .core.basisfunction import ImmutableBasisFunction from .core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple from .core.knotvector import ImmutableKnotVector -from .core.operations import ( +from .operations.knotvector import ( increase_degree, insert_knots, remove_knots, diff --git a/src/pynurbs/knotspace.py b/src/pynurbs/knotspace.py index 3bc28e9..7c3890a 100644 --- a/src/pynurbs/knotspace.py +++ b/src/pynurbs/knotspace.py @@ -13,7 +13,7 @@ import numpy as np from .core import ImmutableKnotVector -from .core.operations import ( +from .operations.knotvector import ( decrease_degree, increase_degree, insert_knots, From 2f54e5c0c42ad138560a33d4b43a462a226468d0 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:13:50 +0200 Subject: [PATCH 045/116] refactor: move roots.py and tools.py to operations folder --- src/pynurbs/core/piecepoly.py | 2 -- src/pynurbs/functions.py | 2 +- src/pynurbs/operations/__init__.py | 2 +- src/pynurbs/{core => operations}/roots.py | 2 +- src/pynurbs/{core => operations}/tools.py | 0 tests/{core => operations}/test_roots.py | 2 +- 6 files changed, 4 insertions(+), 6 deletions(-) rename src/pynurbs/{core => operations}/roots.py (97%) rename src/pynurbs/{core => operations}/tools.py (100%) rename tests/{core => operations}/test_roots.py (97%) diff --git a/src/pynurbs/core/piecepoly.py b/src/pynurbs/core/piecepoly.py index 9368283..0e81bd4 100644 --- a/src/pynurbs/core/piecepoly.py +++ b/src/pynurbs/core/piecepoly.py @@ -8,7 +8,6 @@ from typing import Iterable, Tuple, Union from .polynomial import Polynomial -from .tools import vectorize def find_span(node: Real, knots: Tuple[Real, ...]): @@ -84,7 +83,6 @@ def __repr__(self) -> str: msgs.append(f"[{knota}, {knotb}]: {repr(functioni)}") return "{" + ", ".join(msgs) + "}" - @vectorize(1, 0) def __call__(self, node: Real) -> Real: span = find_span(node, self.knots) function = self.functions[span] diff --git a/src/pynurbs/functions.py b/src/pynurbs/functions.py index 100945b..fad8f7a 100644 --- a/src/pynurbs/functions.py +++ b/src/pynurbs/functions.py @@ -8,7 +8,7 @@ from pynurbs.core.basisfunction import ImmutableBasisFunction from pynurbs.knotspace import KnotVector -from .core.tools import vectorize +from .operations.tools import vectorize class BaseFunction: diff --git a/src/pynurbs/operations/__init__.py b/src/pynurbs/operations/__init__.py index 3e2e643..56c5b9d 100644 --- a/src/pynurbs/operations/__init__.py +++ b/src/pynurbs/operations/__init__.py @@ -1 +1 @@ -from .knotvector import insert_knots, increase_degree, remove_knots, decrease_degree +from .knotvector import decrease_degree, increase_degree, insert_knots, remove_knots diff --git a/src/pynurbs/core/roots.py b/src/pynurbs/operations/roots.py similarity index 97% rename from src/pynurbs/core/roots.py rename to src/pynurbs/operations/roots.py index f860a1f..48a6ae4 100644 --- a/src/pynurbs/core/roots.py +++ b/src/pynurbs/operations/roots.py @@ -8,7 +8,7 @@ import numpy as np -from .polynomial import Polynomial +from ..core.polynomial import Polynomial def division(poly: Polynomial, doly: Polynomial) -> Tuple[Polynomial, Polynomial]: diff --git a/src/pynurbs/core/tools.py b/src/pynurbs/operations/tools.py similarity index 100% rename from src/pynurbs/core/tools.py rename to src/pynurbs/operations/tools.py diff --git a/tests/core/test_roots.py b/tests/operations/test_roots.py similarity index 97% rename from tests/core/test_roots.py rename to tests/operations/test_roots.py index 7f2f65a..900d41f 100644 --- a/tests/core/test_roots.py +++ b/tests/operations/test_roots.py @@ -1,7 +1,7 @@ import pytest from pynurbs.core.polynomial import Polynomial -from pynurbs.core.roots import division, roots +from pynurbs.operations.roots import division, roots @pytest.mark.order(3) From 6d0dbd3f368531854e9ad22018334ffced7a3259 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:32:27 +0200 Subject: [PATCH 046/116] refactor: move `knotspace`, `functions` and `curves` to submodule --- src/pynurbs/__init__.py | 4 +--- src/pynurbs/advanced.py | 6 +++--- src/pynurbs/calculus.py | 6 +++--- src/pynurbs/responsive/__init__.py | 3 +++ src/pynurbs/{ => responsive}/curves.py | 6 +++--- src/pynurbs/{ => responsive}/functions.py | 4 ++-- src/pynurbs/{ => responsive}/knotspace.py | 4 ++-- tests/core/test_basis_function.py | 2 -- tests/responsive/__init__.py | 3 +++ tests/{ => responsive}/test_beziercurve.py | 4 ++-- tests/{ => responsive}/test_functions.py | 4 ++-- tests/{ => responsive}/test_knotspace.py | 2 +- tests/{operations => responsive}/test_knotvector.py | 0 tests/{ => responsive}/test_rationalcurve.py | 4 ++-- tests/{ => responsive}/test_splinecurve.py | 4 ++-- tests/test_advanced.py | 2 +- tests/test_calculus.py | 4 ++-- tests/test_customstruc.py | 6 ++---- tests/test_fitting.py | 4 ++-- 19 files changed, 36 insertions(+), 36 deletions(-) create mode 100644 src/pynurbs/responsive/__init__.py rename src/pynurbs/{ => responsive}/curves.py (99%) rename src/pynurbs/{ => responsive}/functions.py (98%) rename src/pynurbs/{ => responsive}/knotspace.py (99%) create mode 100644 tests/responsive/__init__.py rename tests/{ => responsive}/test_beziercurve.py (99%) rename tests/{ => responsive}/test_functions.py (99%) rename tests/{ => responsive}/test_knotspace.py (99%) rename tests/{operations => responsive}/test_knotvector.py (100%) rename tests/{ => responsive}/test_rationalcurve.py (99%) rename tests/{ => responsive}/test_splinecurve.py (99%) diff --git a/src/pynurbs/__init__.py b/src/pynurbs/__init__.py index 9f69103..e6a87b5 100644 --- a/src/pynurbs/__init__.py +++ b/src/pynurbs/__init__.py @@ -1,8 +1,6 @@ from .advanced import Intersection, Projection from .calculus import Derivate, Integrate -from .curves import Curve -from .functions import Function -from .knotspace import GeneratorKnotVector, KnotVector +from .responsive import Curve, Function, GeneratorKnotVector, KnotVector __version__ = "1.1.0" diff --git a/src/pynurbs/advanced.py b/src/pynurbs/advanced.py index 86098b5..ca69591 100644 --- a/src/pynurbs/advanced.py +++ b/src/pynurbs/advanced.py @@ -7,9 +7,9 @@ import numpy as np -from pynurbs import heavy -from pynurbs.calculus import Derivate -from pynurbs.curves import Curve +from . import heavy +from .calculus import Derivate +from .responsive.curves import Curve class Projection: diff --git a/src/pynurbs/calculus.py b/src/pynurbs/calculus.py index 5cb8d59..44d5239 100644 --- a/src/pynurbs/calculus.py +++ b/src/pynurbs/calculus.py @@ -3,9 +3,9 @@ import numpy as np -from pynurbs import heavy -from pynurbs.curves import Curve -from pynurbs.knotspace import KnotVector +from . import heavy +from .responsive.curves import Curve +from .responsive.knotspace import KnotVector class Derivate: diff --git a/src/pynurbs/responsive/__init__.py b/src/pynurbs/responsive/__init__.py new file mode 100644 index 0000000..08f097f --- /dev/null +++ b/src/pynurbs/responsive/__init__.py @@ -0,0 +1,3 @@ +from .curves import Curve +from .functions import Function +from .knotspace import GeneratorKnotVector, KnotVector diff --git a/src/pynurbs/curves.py b/src/pynurbs/responsive/curves.py similarity index 99% rename from src/pynurbs/curves.py rename to src/pynurbs/responsive/curves.py index 3a2d50d..8b6e64e 100644 --- a/src/pynurbs/curves.py +++ b/src/pynurbs/responsive/curves.py @@ -7,15 +7,15 @@ import numpy as np from pynurbs import heavy -from pynurbs.knotspace import KnotVector -from .core.basisfunction import ImmutableBasisFunction -from .operations.knotvector import ( +from ..core.basisfunction import ImmutableBasisFunction +from ..operations.knotvector import ( decrease_degree, increase_degree, insert_knots, remove_knots, ) +from .knotspace import KnotVector def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: diff --git a/src/pynurbs/functions.py b/src/pynurbs/responsive/functions.py similarity index 98% rename from src/pynurbs/functions.py rename to src/pynurbs/responsive/functions.py index fad8f7a..2ec6b2f 100644 --- a/src/pynurbs/functions.py +++ b/src/pynurbs/responsive/functions.py @@ -6,9 +6,9 @@ import numpy as np from pynurbs.core.basisfunction import ImmutableBasisFunction -from pynurbs.knotspace import KnotVector -from .operations.tools import vectorize +from ..operations.tools import vectorize +from .knotspace import KnotVector class BaseFunction: diff --git a/src/pynurbs/knotspace.py b/src/pynurbs/responsive/knotspace.py similarity index 99% rename from src/pynurbs/knotspace.py rename to src/pynurbs/responsive/knotspace.py index 7c3890a..ef98650 100644 --- a/src/pynurbs/knotspace.py +++ b/src/pynurbs/responsive/knotspace.py @@ -12,8 +12,8 @@ import numpy as np -from .core import ImmutableKnotVector -from .operations.knotvector import ( +from ..core import ImmutableKnotVector +from ..operations.knotvector import ( decrease_degree, increase_degree, insert_knots, diff --git a/tests/core/test_basis_function.py b/tests/core/test_basis_function.py index 64c0e9e..bef88d6 100644 --- a/tests/core/test_basis_function.py +++ b/tests/core/test_basis_function.py @@ -1,4 +1,3 @@ -from copy import copy from fractions import Fraction import numpy as np @@ -6,7 +5,6 @@ from pynurbs.core.basisfunction import ImmutableBasisFunction from pynurbs.core.knotvector import ImmutableKnotVector -from pynurbs.knotspace import GeneratorKnotVector def binom(n: int, i: int): diff --git a/tests/responsive/__init__.py b/tests/responsive/__init__.py new file mode 100644 index 0000000..04ddc64 --- /dev/null +++ b/tests/responsive/__init__.py @@ -0,0 +1,3 @@ +import sys + +sys.path.append("./src") diff --git a/tests/test_beziercurve.py b/tests/responsive/test_beziercurve.py similarity index 99% rename from tests/test_beziercurve.py rename to tests/responsive/test_beziercurve.py index 1adf406..b4a2dcb 100644 --- a/tests/test_beziercurve.py +++ b/tests/responsive/test_beziercurve.py @@ -6,8 +6,8 @@ import numpy as np import pytest -from pynurbs.curves import Curve -from pynurbs.knotspace import GeneratorKnotVector, KnotVector +from pynurbs.responsive.curves import Curve +from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector @pytest.mark.order(4) diff --git a/tests/test_functions.py b/tests/responsive/test_functions.py similarity index 99% rename from tests/test_functions.py rename to tests/responsive/test_functions.py index 8302d04..3b67b02 100644 --- a/tests/test_functions.py +++ b/tests/responsive/test_functions.py @@ -3,9 +3,9 @@ import numpy as np import pytest -from pynurbs import Function from pynurbs.core.custom_math import binom -from pynurbs.knotspace import GeneratorKnotVector +from pynurbs.responsive.functions import Function +from pynurbs.responsive.knotspace import GeneratorKnotVector @pytest.mark.order(3) diff --git a/tests/test_knotspace.py b/tests/responsive/test_knotspace.py similarity index 99% rename from tests/test_knotspace.py rename to tests/responsive/test_knotspace.py index f6c6d4b..dfa79c0 100644 --- a/tests/test_knotspace.py +++ b/tests/responsive/test_knotspace.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from pynurbs import GeneratorKnotVector, KnotVector +from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector @pytest.mark.order(2) diff --git a/tests/operations/test_knotvector.py b/tests/responsive/test_knotvector.py similarity index 100% rename from tests/operations/test_knotvector.py rename to tests/responsive/test_knotvector.py diff --git a/tests/test_rationalcurve.py b/tests/responsive/test_rationalcurve.py similarity index 99% rename from tests/test_rationalcurve.py rename to tests/responsive/test_rationalcurve.py index ad479a9..88aa1ff 100644 --- a/tests/test_rationalcurve.py +++ b/tests/responsive/test_rationalcurve.py @@ -5,8 +5,8 @@ import numpy as np import pytest -from pynurbs.curves import Curve -from pynurbs.knotspace import GeneratorKnotVector +from pynurbs.responsive.curves import Curve +from pynurbs.responsive.knotspace import GeneratorKnotVector @pytest.mark.order(6) diff --git a/tests/test_splinecurve.py b/tests/responsive/test_splinecurve.py similarity index 99% rename from tests/test_splinecurve.py rename to tests/responsive/test_splinecurve.py index 194c0d0..cc54e19 100644 --- a/tests/test_splinecurve.py +++ b/tests/responsive/test_splinecurve.py @@ -3,8 +3,8 @@ import numpy as np import pytest -from pynurbs.curves import Curve -from pynurbs.knotspace import GeneratorKnotVector, KnotVector +from pynurbs.responsive.curves import Curve +from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector @pytest.mark.order(5) diff --git a/tests/test_advanced.py b/tests/test_advanced.py index c1dbf1a..67b912c 100644 --- a/tests/test_advanced.py +++ b/tests/test_advanced.py @@ -7,7 +7,7 @@ import pytest from pynurbs.advanced import Intersection, Projection -from pynurbs.curves import Curve +from pynurbs.responsive.curves import Curve @pytest.mark.order(8) diff --git a/tests/test_calculus.py b/tests/test_calculus.py index 8d399f2..a45dc4b 100644 --- a/tests/test_calculus.py +++ b/tests/test_calculus.py @@ -9,8 +9,8 @@ import pytest from pynurbs.calculus import Derivate, Integrate -from pynurbs.curves import Curve -from pynurbs.knotspace import GeneratorKnotVector, KnotVector +from pynurbs.responsive.curves import Curve +from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector @pytest.mark.order(7) diff --git a/tests/test_customstruc.py b/tests/test_customstruc.py index 091e87f..9dde2b6 100644 --- a/tests/test_customstruc.py +++ b/tests/test_customstruc.py @@ -14,10 +14,8 @@ import numpy as np import pytest -from pynurbs import calculus -from pynurbs.curves import Curve -from pynurbs.functions import Function -from pynurbs.knotspace import GeneratorKnotVector, KnotVector +from pynurbs.responsive.functions import Function +from pynurbs.responsive.knotspace import KnotVector class CustomFloat: diff --git a/tests/test_fitting.py b/tests/test_fitting.py index 7783a28..f292cb9 100644 --- a/tests/test_fitting.py +++ b/tests/test_fitting.py @@ -1,8 +1,8 @@ import numpy as np import pytest -from pynurbs.curves import Curve -from pynurbs.knotspace import GeneratorKnotVector +from pynurbs.responsive.curves import Curve +from pynurbs.responsive.knotspace import GeneratorKnotVector @pytest.mark.order(7) From c80ed09d7e974243363a395739732ca4eb4e5d9b Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:39:16 +0200 Subject: [PATCH 047/116] refactor: move advanced, calculus and heavy to submodule --- src/pynurbs/__init__.py | 4 ++-- src/pynurbs/{ => operations}/advanced.py | 2 +- src/pynurbs/{ => operations}/calculus.py | 4 ++-- src/pynurbs/{ => operations}/heavy.py | 8 ++++---- src/pynurbs/responsive/curves.py | 3 +-- tests/{ => operations}/test_advanced.py | 2 +- tests/{ => operations}/test_calculus.py | 2 +- tests/{ => operations}/test_customstruc.py | 1 - tests/{ => operations}/test_fitting.py | 0 tests/{ => operations}/test_heavy.py | 2 +- 10 files changed, 13 insertions(+), 15 deletions(-) rename src/pynurbs/{ => operations}/advanced.py (99%) rename src/pynurbs/{ => operations}/calculus.py (99%) rename src/pynurbs/{ => operations}/heavy.py (99%) rename tests/{ => operations}/test_advanced.py (99%) rename tests/{ => operations}/test_calculus.py (99%) rename tests/{ => operations}/test_customstruc.py (99%) rename tests/{ => operations}/test_fitting.py (100%) rename tests/{ => operations}/test_heavy.py (97%) diff --git a/src/pynurbs/__init__.py b/src/pynurbs/__init__.py index e6a87b5..ab32605 100644 --- a/src/pynurbs/__init__.py +++ b/src/pynurbs/__init__.py @@ -1,5 +1,5 @@ -from .advanced import Intersection, Projection -from .calculus import Derivate, Integrate +from .operations.advanced import Intersection, Projection +from .operations.calculus import Derivate, Integrate from .responsive import Curve, Function, GeneratorKnotVector, KnotVector __version__ = "1.1.0" diff --git a/src/pynurbs/advanced.py b/src/pynurbs/operations/advanced.py similarity index 99% rename from src/pynurbs/advanced.py rename to src/pynurbs/operations/advanced.py index ca69591..aa69667 100644 --- a/src/pynurbs/advanced.py +++ b/src/pynurbs/operations/advanced.py @@ -7,9 +7,9 @@ import numpy as np +from ..responsive.curves import Curve from . import heavy from .calculus import Derivate -from .responsive.curves import Curve class Projection: diff --git a/src/pynurbs/calculus.py b/src/pynurbs/operations/calculus.py similarity index 99% rename from src/pynurbs/calculus.py rename to src/pynurbs/operations/calculus.py index 44d5239..ee199dc 100644 --- a/src/pynurbs/calculus.py +++ b/src/pynurbs/operations/calculus.py @@ -3,9 +3,9 @@ import numpy as np +from ..responsive.curves import Curve +from ..responsive.knotspace import KnotVector from . import heavy -from .responsive.curves import Curve -from .responsive.knotspace import KnotVector class Derivate: diff --git a/src/pynurbs/heavy.py b/src/pynurbs/operations/heavy.py similarity index 99% rename from src/pynurbs/heavy.py rename to src/pynurbs/operations/heavy.py index 23aee82..8b006c0 100644 --- a/src/pynurbs/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -11,10 +11,10 @@ import numpy as np -from .core.basisfunction import ImmutableBasisFunction -from .core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple -from .core.knotvector import ImmutableKnotVector -from .operations.knotvector import ( +from ..core.basisfunction import ImmutableBasisFunction +from ..core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple +from ..core.knotvector import ImmutableKnotVector +from ..operations.knotvector import ( increase_degree, insert_knots, remove_knots, diff --git a/src/pynurbs/responsive/curves.py b/src/pynurbs/responsive/curves.py index 8b6e64e..17588a3 100644 --- a/src/pynurbs/responsive/curves.py +++ b/src/pynurbs/responsive/curves.py @@ -6,9 +6,8 @@ import numpy as np -from pynurbs import heavy - from ..core.basisfunction import ImmutableBasisFunction +from ..operations import heavy from ..operations.knotvector import ( decrease_degree, increase_degree, diff --git a/tests/test_advanced.py b/tests/operations/test_advanced.py similarity index 99% rename from tests/test_advanced.py rename to tests/operations/test_advanced.py index 67b912c..6ff318d 100644 --- a/tests/test_advanced.py +++ b/tests/operations/test_advanced.py @@ -6,7 +6,7 @@ import numpy as np import pytest -from pynurbs.advanced import Intersection, Projection +from pynurbs.operations.advanced import Intersection, Projection from pynurbs.responsive.curves import Curve diff --git a/tests/test_calculus.py b/tests/operations/test_calculus.py similarity index 99% rename from tests/test_calculus.py rename to tests/operations/test_calculus.py index a45dc4b..8c723ba 100644 --- a/tests/test_calculus.py +++ b/tests/operations/test_calculus.py @@ -8,7 +8,7 @@ import numpy as np import pytest -from pynurbs.calculus import Derivate, Integrate +from pynurbs.operations.calculus import Derivate, Integrate from pynurbs.responsive.curves import Curve from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector diff --git a/tests/test_customstruc.py b/tests/operations/test_customstruc.py similarity index 99% rename from tests/test_customstruc.py rename to tests/operations/test_customstruc.py index 9dde2b6..cb8efaf 100644 --- a/tests/test_customstruc.py +++ b/tests/operations/test_customstruc.py @@ -11,7 +11,6 @@ We expect all final computations returns CustomFloats """ -import numpy as np import pytest from pynurbs.responsive.functions import Function diff --git a/tests/test_fitting.py b/tests/operations/test_fitting.py similarity index 100% rename from tests/test_fitting.py rename to tests/operations/test_fitting.py diff --git a/tests/test_heavy.py b/tests/operations/test_heavy.py similarity index 97% rename from tests/test_heavy.py rename to tests/operations/test_heavy.py index dd30d61..aac01bc 100644 --- a/tests/test_heavy.py +++ b/tests/operations/test_heavy.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from pynurbs.heavy import LeastSquare +from pynurbs.operations.heavy import LeastSquare @pytest.mark.order(1) From dcd022a326b84db8e933fae3212e8313bcc12091 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:44:52 +0200 Subject: [PATCH 048/116] refactor: separate least_square from heavy --- src/pynurbs/operations/heavy.py | 171 ------------------------ src/pynurbs/operations/least_square.py | 178 +++++++++++++++++++++++++ tests/operations/test_heavy.py | 81 ----------- tests/operations/test_least_square.py | 62 +++++++++ 4 files changed, 240 insertions(+), 252 deletions(-) create mode 100644 src/pynurbs/operations/least_square.py delete mode 100644 tests/operations/test_heavy.py create mode 100644 tests/operations/test_least_square.py diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index 8b006c0..663ae57 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -133,177 +133,6 @@ def eval_rational_nodes( return np.einsum("j,ij,i->ij", denominators, matrix, weights) -class LeastSquare: - """ - Given two hypotetic curves C0 and C1, which are associated - with knotvectors U and V, and control points P and Q. - C0(u) = sum_{i=0}^{n-1} N_{i}(u) * P_{i} - C1(u) = sum_{i=0}^{m-1} M_{i}(u) * Q_{i} - Then, this class has functions to return [T] and [E] such - [Q] = [T] * [P] - error = [P]^T * [E] * [P] - Then, C1 keeps near to C1 by using galerkin projections. - - They minimizes the integral - int_{a}^{b} abs(C0(u) - C1(u))^2 du - The way it does it by using the norm of inner product: - abs(X) = sqrt(< X, X >) - Then finally it finds the matrix [A] and [B] - [C] * [Q] = [B] * [P] - [A]_{ij} = int_{0}^{1} < Ni(u), Nj(u) > du - [B]_{ij} = int_{0}^{1} < Mi(u), Nj(u) > du - [C]_{ij} = int_{0}^{1} < Mi(u), Mj(u) > du - --> [T] = [C]^{-1} * [B]^T - --> [E] = [A] - [B] * [T] - """ - - @staticmethod - def fit_function( - knotvector: ImmutableKnotVector, - nodes: Tuple[float], - weights: Union[Tuple[float], None], - ) -> Tuple[Tuple[float]]: - """ - Let C(u) be a curve C(u) of base functions F of given knot vector - C(u) = sum_i F_i(u) * P_i - it's wanted to fit a C(u) into the curve f(u) - - To do it, we do least squares by minimizing - J(P) = sum_j abs(C(nodej)-f(nodej)) - - This function returns a matrix M such - [P] = [M] * [f(nodes)] - """ - knotvector = ImmutableKnotVector(knotvector) - basis = ImmutableBasisFunction(knotvector) - matrix = np.transpose(tuple(map(basis, nodes))) - if weights is not None: - denominators = 1 / np.dot(weights, matrix) - matrix = np.einsum("j,ij->ij", denominators, matrix) - return Linalg.lstsq(np.transpose(matrix)) - - @staticmethod - def spline2spline( - oldknotvector: ImmutableKnotVector, - newknotvector: ImmutableKnotVector, - fit_nodes: Tuple[float] = None, - ) -> Tuple["Matrix2D"]: - """ - Given two bspline curves A(u) and B(u), this - function returns a matrix [M] such - [Q] = [M] * [P] - A(u) = sum_i N_i(u) * P_i - B(u) = sum_i N_i(u) * Q_i - """ - oldknotvector = ImmutableKnotVector(oldknotvector) - newknotvector = ImmutableKnotVector(newknotvector) - oldnpts = oldknotvector.npts - newnpts = newknotvector.npts - oldweights = [Fraction(1) for i in range(oldnpts)] - newweights = [Fraction(1) for i in range(newnpts)] - result = LeastSquare.func2func( - oldknotvector, oldweights, newknotvector, newweights, fit_nodes - ) - return totuple(result) - - @staticmethod - def func2func( - oldknotvector: ImmutableKnotVector, - oldweights: Tuple[float], - newknotvector: ImmutableKnotVector, - newweights: Tuple[float], - fit_nodes: Tuple[float] = None, - ) -> Tuple[np.ndarray]: - """ - Given two rational bspline curves A(u) and B(u), this - function returns a matrix [M] such - [Q] = [M] * [P] - A(u) = sum_i R_i(u) * P_i - B(u) = sum_i R_i(u) * Q_i - """ - oldknotvector = ImmutableKnotVector(oldknotvector) - newknotvector = ImmutableKnotVector(newknotvector) - for val in oldweights: - float(val) - for val in newweights: - float(val) - - olddegree = oldknotvector.degree - oldnpts = oldknotvector.npts - oldknots = oldknotvector.knots - - newdegree = newknotvector.degree - newnpts = newknotvector.npts - newknots = newknotvector.knots - - oldknotvector = tuple( - Fraction(node) if isinstance(node, int) else node for node in oldknotvector - ) - newknotvector = tuple( - Fraction(node) if isinstance(node, int) else node for node in newknotvector - ) - oldknotvector = ImmutableKnotVector(oldknotvector, olddegree) - newknotvector = ImmutableKnotVector(newknotvector, newdegree) - - if fit_nodes and len(fit_nodes) > newnpts: - raise NotImplementedError - allknots = list(set(oldknots + newknots)) - allknots.sort() - - numbtype = number_type(allknots) - numbtype = Fraction if (numbtype is int) else numbtype - nptsinteg = olddegree + newdegree + 3 # Number integration points - if numbtype is Fraction: - nodes0to1 = NodeSample.closed_linspace(nptsinteg) - integrator = IntegratorArray.closed_newton_cotes(nptsinteg) - else: - nodes0to1 = NodeSample.chebyshev(nptsinteg) - integrator = IntegratorArray.chebyshev(nptsinteg) - nodes0to1 = np.array(nodes0to1) - integrator = np.array(integrator, dtype=numbtype) - - FF = np.zeros((oldnpts, oldnpts), dtype=numbtype) # F*F - GF = np.zeros((newnpts, oldnpts), dtype=numbtype) # F*G - GG = np.zeros((newnpts, newnpts), dtype=numbtype) # G*G - for start, end in zip(allknots[:-1], allknots[1:]): - nodes = start + (end - start) * nodes0to1 - # Integral of the functions in the interval [a, b] - Fvalues = eval_rational_nodes( - oldknotvector, oldweights, tuple(nodes), olddegree - ) - Gvalues = eval_rational_nodes( - newknotvector, newweights, tuple(nodes), newdegree - ) - Fvalues = np.array(Fvalues, dtype=numbtype) - Gvalues = np.array(Gvalues, dtype=numbtype) - for k, integ in enumerate(integrator): - FF += integ * np.tensordot(Fvalues[:, k], Fvalues[:, k], axes=0) - GF += integ * np.tensordot(Gvalues[:, k], Fvalues[:, k], axes=0) - GG += integ * np.tensordot(Gvalues[:, k], Gvalues[:, k], axes=0) - - GGinv = Linalg.invert(GG) - if fit_nodes is None: - T = np.dot(GGinv, GF) - E = FF - np.dot(GF.T, T) - return totuple(T), totuple(E) - fit_nodes = tuple( - Fraction(node) if isinstance(node, int) else node for node in fit_nodes - ) - F = eval_rational_nodes(oldknotvector, oldweights, tuple(fit_nodes), olddegree) - G = eval_rational_nodes(newknotvector, newweights, tuple(fit_nodes), newdegree) - F = np.array(F, dtype="object").T - GT = np.array(G, dtype="object") - G = np.transpose(GT) - LL = np.dot(G, np.dot(GGinv, GT)) - LLinv = Linalg.invert(LL) - LG = np.dot(LLinv, np.dot(G, GGinv)) - QG = GGinv - np.dot(GGinv, np.dot(GT, LG)) - QF = np.dot(GGinv, np.dot(GT, LLinv)) - T = np.dot(QG, GF) + np.dot(QF, F) - E = (FF - 2 * np.dot(T.T, GF) + np.dot(T.T, np.dot(GG, T))) / 2 - return totuple(T), totuple(E) - - class Operations: """ Contains algorithms to diff --git a/src/pynurbs/operations/least_square.py b/src/pynurbs/operations/least_square.py new file mode 100644 index 0000000..c1556e6 --- /dev/null +++ b/src/pynurbs/operations/least_square.py @@ -0,0 +1,178 @@ +""" +Given two hypotetic curves C0 and C1, which are associated +with knotvectors U and V, and control points P and Q. + C0(u) = sum_{i=0}^{n-1} N_{i}(u) * P_{i} + C1(u) = sum_{i=0}^{m-1} M_{i}(u) * Q_{i} +Then, this class has functions to return [T] and [E] such + [Q] = [T] * [P] + error = [P]^T * [E] * [P] +Then, C1 keeps near to C1 by using galerkin projections. + +They minimizes the integral + int_{a}^{b} abs(C0(u) - C1(u))^2 du +The way it does it by using the norm of inner product: + abs(X) = sqrt(< X, X >) +Then finally it finds the matrix [A] and [B] + [C] * [Q] = [B] * [P] + [A]_{ij} = int_{0}^{1} < Ni(u), Nj(u) > du + [B]_{ij} = int_{0}^{1} < Mi(u), Nj(u) > du + [C]_{ij} = int_{0}^{1} < Mi(u), Mj(u) > du + --> [T] = [C]^{-1} * [B]^T + --> [E] = [A] - [B] * [T] +""" + +from __future__ import annotations + +from fractions import Fraction +from typing import Tuple, Union + +import numpy as np + +from ..core.basisfunction import ImmutableBasisFunction +from ..core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple +from ..core.knotvector import ImmutableKnotVector +from .heavy import eval_rational_nodes + + +def fit_function( + knotvector: ImmutableKnotVector, + nodes: Tuple[float], + weights: Union[Tuple[float], None], +) -> Tuple[Tuple[float]]: + """ + Let C(u) be a curve C(u) of base functions F of given knot vector + C(u) = sum_i F_i(u) * P_i + it's wanted to fit a C(u) into the curve f(u) + + To do it, we do least squares by minimizing + J(P) = sum_j abs(C(nodej)-f(nodej)) + + This function returns a matrix M such + [P] = [M] * [f(nodes)] + """ + knotvector = ImmutableKnotVector(knotvector) + basis = ImmutableBasisFunction(knotvector) + matrix = np.transpose(tuple(map(basis, nodes))) + if weights is not None: + denominators = 1 / np.dot(weights, matrix) + matrix = np.einsum("j,ij->ij", denominators, matrix) + return Linalg.lstsq(np.transpose(matrix)) + + +def spline2spline( + oldknotvector: ImmutableKnotVector, + newknotvector: ImmutableKnotVector, + fit_nodes: Tuple[float] = None, +) -> Tuple["Matrix2D"]: + """ + Given two bspline curves A(u) and B(u), this + function returns a matrix [M] such + [Q] = [M] * [P] + A(u) = sum_i N_i(u) * P_i + B(u) = sum_i N_i(u) * Q_i + """ + oldknotvector = ImmutableKnotVector(oldknotvector) + newknotvector = ImmutableKnotVector(newknotvector) + oldnpts = oldknotvector.npts + newnpts = newknotvector.npts + oldweights = [Fraction(1) for i in range(oldnpts)] + newweights = [Fraction(1) for i in range(newnpts)] + result = func2func(oldknotvector, oldweights, newknotvector, newweights, fit_nodes) + return totuple(result) + + +def func2func( + oldknotvector: ImmutableKnotVector, + oldweights: Tuple[float], + newknotvector: ImmutableKnotVector, + newweights: Tuple[float], + fit_nodes: Tuple[float] = None, +) -> Tuple[np.ndarray]: + """ + Given two rational bspline curves A(u) and B(u), this + function returns a matrix [M] such + [Q] = [M] * [P] + A(u) = sum_i R_i(u) * P_i + B(u) = sum_i R_i(u) * Q_i + """ + oldknotvector = ImmutableKnotVector(oldknotvector) + newknotvector = ImmutableKnotVector(newknotvector) + for val in oldweights: + float(val) + for val in newweights: + float(val) + + olddegree = oldknotvector.degree + oldnpts = oldknotvector.npts + oldknots = oldknotvector.knots + + newdegree = newknotvector.degree + newnpts = newknotvector.npts + newknots = newknotvector.knots + + oldknotvector = tuple( + Fraction(node) if isinstance(node, int) else node for node in oldknotvector + ) + newknotvector = tuple( + Fraction(node) if isinstance(node, int) else node for node in newknotvector + ) + oldknotvector = ImmutableKnotVector(oldknotvector, olddegree) + newknotvector = ImmutableKnotVector(newknotvector, newdegree) + + if fit_nodes and len(fit_nodes) > newnpts: + raise NotImplementedError + allknots = list(set(oldknots + newknots)) + allknots.sort() + + numbtype = number_type(allknots) + numbtype = Fraction if (numbtype is int) else numbtype + nptsinteg = olddegree + newdegree + 3 # Number integration points + if numbtype is Fraction: + nodes0to1 = NodeSample.closed_linspace(nptsinteg) + integrator = IntegratorArray.closed_newton_cotes(nptsinteg) + else: + nodes0to1 = NodeSample.chebyshev(nptsinteg) + integrator = IntegratorArray.chebyshev(nptsinteg) + nodes0to1 = np.array(nodes0to1) + integrator = np.array(integrator, dtype=numbtype) + + FF = np.zeros((oldnpts, oldnpts), dtype=numbtype) # F*F + GF = np.zeros((newnpts, oldnpts), dtype=numbtype) # F*G + GG = np.zeros((newnpts, newnpts), dtype=numbtype) # G*G + for start, end in zip(allknots[:-1], allknots[1:]): + nodes = start + (end - start) * nodes0to1 + # Integral of the functions in the interval [a, b] + Fvalues = eval_rational_nodes( + oldknotvector, oldweights, tuple(nodes), olddegree + ) + Gvalues = eval_rational_nodes( + newknotvector, newweights, tuple(nodes), newdegree + ) + Fvalues = np.array(Fvalues, dtype=numbtype) + Gvalues = np.array(Gvalues, dtype=numbtype) + for k, integ in enumerate(integrator): + FF += integ * np.tensordot(Fvalues[:, k], Fvalues[:, k], axes=0) + GF += integ * np.tensordot(Gvalues[:, k], Fvalues[:, k], axes=0) + GG += integ * np.tensordot(Gvalues[:, k], Gvalues[:, k], axes=0) + + GGinv = Linalg.invert(GG) + if fit_nodes is None: + T = np.dot(GGinv, GF) + E = FF - np.dot(GF.T, T) + return totuple(T), totuple(E) + fit_nodes = tuple( + Fraction(node) if isinstance(node, int) else node for node in fit_nodes + ) + F = eval_rational_nodes(oldknotvector, oldweights, tuple(fit_nodes), olddegree) + G = eval_rational_nodes(newknotvector, newweights, tuple(fit_nodes), newdegree) + F = np.array(F, dtype="object").T + GT = np.array(G, dtype="object") + G = np.transpose(GT) + LL = np.dot(G, np.dot(GGinv, GT)) + LLinv = Linalg.invert(LL) + LG = np.dot(LLinv, np.dot(G, GGinv)) + QG = GGinv - np.dot(GGinv, np.dot(GT, LG)) + QF = np.dot(GGinv, np.dot(GT, LLinv)) + T = np.dot(QG, GF) + np.dot(QF, F) + E = (FF - 2 * np.dot(T.T, GF) + np.dot(T.T, np.dot(GG, T))) / 2 + return totuple(T), totuple(E) diff --git a/tests/operations/test_heavy.py b/tests/operations/test_heavy.py deleted file mode 100644 index aac01bc..0000000 --- a/tests/operations/test_heavy.py +++ /dev/null @@ -1,81 +0,0 @@ -import numpy as np -import pytest - -from pynurbs.operations.heavy import LeastSquare - - -@pytest.mark.order(1) -@pytest.mark.dependency( - depends=["tests/test_custom_math.py::test_end"], scope="session" -) -def test_begin(): - pass - - -class TestLeastSquare: - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "test_begin", - ] - ) - def test_begin(self): - pass - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestLeastSquare::test_begin"]) - def test_leastsquarespline_identity(self): - U0 = [0, 0, 1, 1] - U1 = [0, 0, 1, 1] - T, E = LeastSquare.spline2spline(U0, U1) - np.testing.assert_almost_equal(T, np.eye(2)) - assert np.all(np.abs(E) < 1e-9) - - U0 = [0, 0, 0, 1, 1, 1] - U1 = [0, 0, 0, 1, 1, 1] - T, E = LeastSquare.spline2spline(U0, U1) - np.testing.assert_almost_equal(T, np.eye(3)) - assert np.all(np.abs(E) < 1e-9) - - U0 = [0, 0, 0, 0.5, 1, 1, 1] - U1 = [0, 0, 0, 0.5, 1, 1, 1] - T, E = LeastSquare.spline2spline(U0, U1) - np.testing.assert_almost_equal(T, np.eye(4)) - assert np.all(np.abs(E) < 1e-9) - - @pytest.mark.order(1) - @pytest.mark.dependency(depends=["TestLeastSquare::test_begin"]) - def test_leastsquarespline_eval_error(self): - # knot insertion - U0 = [0, 0, 0, 1, 1, 1] - U1 = [0, 0, 0, 0.5, 1, 1, 1] - _, E = LeastSquare.spline2spline(U0, U1) - assert np.all(np.abs(E) < 1e-9) - - # degree elevate - U0 = [0, 0, 1, 1] - U1 = [0, 0, 0, 1, 1, 1] - _, E = LeastSquare.spline2spline(U0, U1) - assert np.all(np.abs(E) < 1e-9) - - @pytest.mark.order(1) - @pytest.mark.dependency( - depends=[ - "TestLeastSquare::test_begin", - "TestLeastSquare::test_leastsquarespline_identity", - "TestLeastSquare::test_leastsquarespline_eval_error", - ] - ) - def test_end(self): - pass - - -@pytest.mark.order(1) -@pytest.mark.dependency( - depends=[ - "test_begin", - "TestLeastSquare::test_end", - ] -) -def test_end(): - pass diff --git a/tests/operations/test_least_square.py b/tests/operations/test_least_square.py new file mode 100644 index 0000000..12ae862 --- /dev/null +++ b/tests/operations/test_least_square.py @@ -0,0 +1,62 @@ +import numpy as np +import pytest + +from pynurbs.operations.least_square import spline2spline + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=["tests/test_custom_math.py::test_end"], scope="session" +) +def test_begin(): + pass + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["TestLeastSquare::test_begin"]) +def test_leastsquarespline_identity(): + U0 = [0, 0, 1, 1] + U1 = [0, 0, 1, 1] + T, E = spline2spline(U0, U1) + np.testing.assert_almost_equal(T, np.eye(2)) + assert np.all(np.abs(E) < 1e-9) + + U0 = [0, 0, 0, 1, 1, 1] + U1 = [0, 0, 0, 1, 1, 1] + T, E = spline2spline(U0, U1) + np.testing.assert_almost_equal(T, np.eye(3)) + assert np.all(np.abs(E) < 1e-9) + + U0 = [0, 0, 0, 0.5, 1, 1, 1] + U1 = [0, 0, 0, 0.5, 1, 1, 1] + T, E = spline2spline(U0, U1) + np.testing.assert_almost_equal(T, np.eye(4)) + assert np.all(np.abs(E) < 1e-9) + + +@pytest.mark.order(1) +@pytest.mark.dependency(depends=["TestLeastSquare::test_begin"]) +def test_leastsquarespline_eval_error(): + # knot insertion + U0 = [0, 0, 0, 1, 1, 1] + U1 = [0, 0, 0, 0.5, 1, 1, 1] + _, E = spline2spline(U0, U1) + assert np.all(np.abs(E) < 1e-9) + + # degree elevate + U0 = [0, 0, 1, 1] + U1 = [0, 0, 0, 1, 1, 1] + _, E = spline2spline(U0, U1) + assert np.all(np.abs(E) < 1e-9) + + +@pytest.mark.order(1) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_leastsquarespline_identity", + "test_leastsquarespline_eval_error", + ] +) +def test_end(): + pass From e2903d785023bdb21bfff485c2715e7e3704fa1e Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:47:49 +0200 Subject: [PATCH 049/116] test: change test order for responsive --- tests/responsive/test_beziercurve.py | 90 +++++++++---------- tests/responsive/test_functions.py | 80 ++++++++--------- tests/responsive/test_knotspace.py | 66 +++++++------- tests/responsive/test_rationalcurve.py | 68 +++++++-------- tests/responsive/test_splinecurve.py | 116 ++++++++++++------------- 5 files changed, 210 insertions(+), 210 deletions(-) diff --git a/tests/responsive/test_beziercurve.py b/tests/responsive/test_beziercurve.py index b4a2dcb..d477277 100644 --- a/tests/responsive/test_beziercurve.py +++ b/tests/responsive/test_beziercurve.py @@ -10,7 +10,7 @@ from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector -@pytest.mark.order(4) +@pytest.mark.order(33) @pytest.mark.dependency( depends=[ "tests/test_knotspace.py::test_end", @@ -23,12 +23,12 @@ def test_begin(): class TestInitCurve: - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestInitCurve::test_begin"]) def test_build_scalar(self): @@ -37,7 +37,7 @@ def test_build_scalar(self): ctrlpoints = np.random.uniform(-1, 1, npts) Curve(knotvector, ctrlpoints) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestInitCurve::test_begin"]) def test_build_vectorial(self): @@ -47,7 +47,7 @@ def test_build_vectorial(self): ctrlpoints = np.random.uniform(-1, 1, (npts, ndim)) Curve(knotvector, ctrlpoints) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestInitCurve::test_build_scalar"]) def test_failbuild(self): @@ -63,7 +63,7 @@ def test_failbuild(self): with pytest.raises(TypeError): Curve(knotvector, 1) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestInitCurve::test_build_scalar"]) def test_attributes(self): degree, npts = 3, 4 @@ -78,7 +78,7 @@ def test_attributes(self): assert hasattr(curve, "knotvector") assert hasattr(curve, "knots") - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestInitCurve::test_build_scalar"]) def test_functions(self): degree, npts = 3, 4 @@ -97,7 +97,7 @@ def test_functions(self): assert hasattr(curve, "__str__") assert callable(curve) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestInitCurve::test_attributes"]) def test_atributesgood(self): degree, npts = 3, 4 @@ -110,7 +110,7 @@ def test_atributesgood(self): np.testing.assert_allclose(curve.ctrlpoints, ctrlpoints) assert curve.knots == knotvector.knots - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency(depends=["TestInitCurve::test_build_vectorial"]) def test_compare_two_curves(self): @@ -130,7 +130,7 @@ def test_compare_two_curves(self): assert C1 == C3 assert C1 != C4 - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency(depends=["TestInitCurve::test_build_scalar"]) def test_print(self): @@ -140,7 +140,7 @@ def test_print(self): bezier.ctrlpoints = [2, 4, 3, 1] str(bezier) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency( depends=[ "TestInitCurve::test_begin", @@ -157,12 +157,12 @@ def test_end(self): class TestCompare: - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestInitCurve::test_end"]) def test_begin(self): pass - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestCompare::test_begin"]) def test_knotvector(self): degree, npts = 3, 4 @@ -187,7 +187,7 @@ def test_knotvector(self): curve1 = Curve(knotvector1, ctrlpoints) assert curve0 != curve1 - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestCompare::test_begin"]) def test_controlpoints(self): npts = 7 @@ -199,7 +199,7 @@ def test_controlpoints(self): curve1 = Curve(knotvector1, ctrlpoints1) assert curve0 != curve1 - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestCompare::test_begin"]) def test_userentry(self): npts = 7 @@ -212,7 +212,7 @@ def test_userentry(self): assert curve != ctrlpoints assert curve != [] - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency( depends=[ "TestCompare::test_begin", @@ -225,14 +225,14 @@ def test_end(self): class TestCallShape: - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency( depends=["TestCompare::test_end", "TestInitCurve::test_end"] ) def test_begin(self): pass - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency(depends=["TestCallShape::test_begin"]) def test_build_scalar(self): @@ -242,7 +242,7 @@ def test_build_scalar(self): ctrlpoints = np.random.uniform(-1, 1, npts) Curve(knotvector, ctrlpoints) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency(depends=["TestCallShape::test_begin"]) def test_build_vectorial(self): @@ -253,7 +253,7 @@ def test_build_vectorial(self): ctrlpoints = np.random.uniform(-1, 1, (npts, ndim)) Curve(knotvector, ctrlpoints) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -273,7 +273,7 @@ def test_callscal_scalpts(self): curvevalues = curve(tparam) assert type(curvevalues) == type(ctrlpoints[0]) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -295,7 +295,7 @@ def test_callscal_vectpts(self, ntests=1): assert type(curvevalues) == type(ctrlpoints[0]) assert type(curvevalues[0]) == type(ctrlpoints[0][0]) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -320,7 +320,7 @@ def test_callvect_scalpts(self): assert len(Cval) == nsample assert type(Cval[0]) == type(ctrlpoints[0]) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -347,7 +347,7 @@ def test_callvect_vectpts(self): assert type(curvevalues[0]) == type(ctrlpoints[0]) assert np.array(curvevalues).shape == (nsample, ndim) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -363,7 +363,7 @@ def test_end(self): class TestDegreeOperations: - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -374,7 +374,7 @@ class TestDegreeOperations: def test_begin(self): pass - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -393,7 +393,7 @@ def test_increase_once_degree1(self): correctctrlpoints = matrix @ ctrlpoints np.testing.assert_allclose(curve.ctrlpoints, correctctrlpoints) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -413,7 +413,7 @@ def test_increase_once_degree2(self): correctctrlpoints = matrix @ ctrlpoints np.testing.assert_allclose(curve.ctrlpoints, correctctrlpoints) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -440,7 +440,7 @@ def test_increase_once_degree3(self): Pgood = matrix @ ctrlpoints np.testing.assert_allclose(curve.ctrlpoints, Pgood) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -471,7 +471,7 @@ def test_increase_4times_degree2(self): correctctrlpoints = matrix @ ctrlpoints np.testing.assert_allclose(curve.ctrlpoints, correctctrlpoints) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -494,7 +494,7 @@ def test_increase_random(self): np.testing.assert_allclose(curve.ctrlpoints[0], ctrlpoints[0]) np.testing.assert_allclose(curve.ctrlpoints[-1], ctrlpoints[-1]) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -514,7 +514,7 @@ def test_decrease_random(self): np.testing.assert_allclose(curve.ctrlpoints[0], ctrlpoints[0]) np.testing.assert_allclose(curve.ctrlpoints[-1], ctrlpoints[-1]) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -536,7 +536,7 @@ def test_increase_decrease_random(self): assert curve.degree == degree np.testing.assert_allclose(curve.ctrlpoints, curve.ctrlpoints) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -555,7 +555,7 @@ def test_clean(self): curve.degree_clean() assert curve.degree == degree - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=["TestDegreeOperations::test_begin", "TestDegreeOperations::test_clean"] @@ -583,7 +583,7 @@ def test_fails(self): with pytest.raises(ValueError): curve.degree_increase(0) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency( depends=[ "TestDegreeOperations::test_begin", @@ -596,7 +596,7 @@ def test_end(self): class TestAddSubMulDiv: - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency( depends=[ "TestCompare::test_end", @@ -607,7 +607,7 @@ class TestAddSubMulDiv: def test_begin(self): pass - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestAddSubMulDiv::test_begin"]) def test_addsub_curves(self): """ @@ -638,7 +638,7 @@ def test_addsub_curves(self): np.testing.assert_allclose(points0 - points1, ptssub0) np.testing.assert_allclose(points1 - points0, ptssub1) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestAddSubMulDiv::test_addsub_curves"]) def test_muldiv_curves(self): """ @@ -681,7 +681,7 @@ def test_muldiv_curves(self): np.testing.assert_allclose(points0 / points1, ptsdiv0) np.testing.assert_allclose(points1 / points0, ptsdiv1) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestAddSubMulDiv::test_begin"]) def test_addsub_scalar(self): """ @@ -710,7 +710,7 @@ def test_addsub_scalar(self): np.testing.assert_allclose(points - c, ptssub0) np.testing.assert_allclose(d - points, ptssub1) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency(depends=["TestAddSubMulDiv::test_begin"]) def test_muldiv_scalar(self): """ @@ -736,7 +736,7 @@ def test_muldiv_scalar(self): np.testing.assert_allclose(b * points, ptsmul1) np.testing.assert_allclose(points / c, ptsdiv0) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency( depends=[ "TestAddSubMulDiv::test_begin", @@ -777,7 +777,7 @@ def test_matmul_scalar(self): good = [pt @ scalar for pt in points] np.testing.assert_allclose(test, good) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency( depends=[ "TestAddSubMulDiv::test_begin", @@ -826,7 +826,7 @@ def test_matmul_curves(self): good = [pt1 @ pt0 for pt0, pt1 in zip(points0, points1)] np.testing.assert_allclose(test, good) - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency( depends=[ "TestAddSubMulDiv::test_begin", @@ -907,7 +907,7 @@ def test_fails(self): with pytest.raises(ValueError): curve0 @ curve1 - @pytest.mark.order(4) + @pytest.mark.order(33) @pytest.mark.dependency( depends=[ "TestAddSubMulDiv::test_begin", @@ -924,7 +924,7 @@ def test_end(self): pass -@pytest.mark.order(4) +@pytest.mark.order(33) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/responsive/test_functions.py b/tests/responsive/test_functions.py index 3b67b02..17237cb 100644 --- a/tests/responsive/test_functions.py +++ b/tests/responsive/test_functions.py @@ -8,19 +8,19 @@ from pynurbs.responsive.knotspace import GeneratorKnotVector -@pytest.mark.order(3) +@pytest.mark.order(32) @pytest.mark.dependency(depends=["tests/test_knotspace.py::test_end"], scope="session") def test_begin(): pass class TestBezier: - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestBezier::test_begin"]) def test_creation(self): @@ -33,7 +33,7 @@ def test_creation(self): assert bezier.degree == 2 assert bezier.npts == 3 - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestBezier::test_creation"]) def test_random_creation(self): @@ -44,7 +44,7 @@ def test_random_creation(self): assert bezier.degree == degree assert bezier.npts == degree + 1 - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_random_creation"]) def test_evalfuncs_degree1(self): @@ -59,7 +59,7 @@ def test_evalfuncs_degree1(self): assert callable(bezier[:, 1]) assert callable(bezier[:]) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_random_creation"]) def test_evalfuncs_degree2(self): @@ -80,7 +80,7 @@ def test_evalfuncs_degree2(self): assert callable(bezier[:, 2]) assert callable(bezier[:]) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency( depends=[ @@ -111,7 +111,7 @@ def test_shape_calls(self): matrix = np.array(matrix, dtype="float64") assert matrix.shape == (npts_sample, npts) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_shape_calls"]) def test_sum_equal_to_1(self): @@ -132,7 +132,7 @@ def test_sum_equal_to_1(self): for k in range(npts_sample): assert abs(np.sum(matrix[k]) - 1) < 1e-9 - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_shape_calls"]) def test_standard_index(self): @@ -152,7 +152,7 @@ def test_standard_index(self): np.testing.assert_allclose(matrix_dire, matrix_degr) np.testing.assert_allclose(matrix_none, matrix_degr) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency( depends=[ @@ -176,7 +176,7 @@ def test_singlevalues_degree1(self): assert bezier[1, 1](0.5) == 0.5 assert bezier[1, 1](1.0) == 1 - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency( depends=[ @@ -215,7 +215,7 @@ def test_singlevalues_degree2(self): assert bezier[2, 2](0.5) == 0.25 assert bezier[2, 2](1.0) == 1 - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency( depends=[ @@ -237,7 +237,7 @@ def test_tablevalues_degree1(self): matrix_good = np.transpose([1 - nodes_test, nodes_test]) np.testing.assert_allclose(matrix_test, matrix_good) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency( depends=[ @@ -275,7 +275,7 @@ def test_tablevalues_degree2(self): ] np.testing.assert_allclose(matrix_test, matrix_good) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency( depends=[ @@ -299,7 +299,7 @@ def test_tablevalues_random_degree(self): matrix_good[i, j] = value np.testing.assert_allclose(matrix_test, matrix_good) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_tablevalues_random_degree"]) def test_shifted_scaled_bezier(self): @@ -323,7 +323,7 @@ def test_shifted_scaled_bezier(self): matrix_good[i, j] = value np.testing.assert_allclose(matrix_test, matrix_good) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_shifted_scaled_bezier"]) def test_degree_operations(self): @@ -338,7 +338,7 @@ def test_degree_operations(self): assert bezier.degree == 1 assert bezier.npts == 2 - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.dependency( depends=[ "TestBezier::test_begin", @@ -363,12 +363,12 @@ def test_end(self): class TestSpline: - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.dependency(depends=["TestBezier::test_end"]) def test_begin(self): pass - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestSpline::test_begin"]) def test_creation(self): @@ -389,7 +389,7 @@ def test_creation(self): assert spline.degree == 2 assert spline.npts == 4 - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestSpline::test_creation"]) def test_random_creation(self): @@ -401,7 +401,7 @@ def test_random_creation(self): assert spline.degree == degree assert spline.npts == npts - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestSpline::test_random_creation"]) def test_evalfuncs_degree1npts3(self): @@ -418,7 +418,7 @@ def test_evalfuncs_degree1npts3(self): assert callable(spline[:, 1]) assert callable(spline[:]) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_evalfuncs_degree1npts3"]) def test_tablevalues_degree1npts3(self): @@ -459,7 +459,7 @@ def test_tablevalues_degree1npts3(self): ] np.testing.assert_allclose(matrix_test, matrix_good) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree1npts3"]) def test_tablevalues_degree2npts4(self): @@ -516,7 +516,7 @@ def test_tablevalues_degree2npts4(self): ] np.testing.assert_allclose(matrix_test, matrix_good) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree2npts4"]) def test_tablevalues_degree3npts5(self): @@ -590,7 +590,7 @@ def test_tablevalues_degree3npts5(self): ] np.testing.assert_allclose(matrix_test, matrix_good) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.dependency( depends=[ "TestSpline::test_tablevalues_degree3npts5", @@ -629,7 +629,7 @@ def test_degree_operation(self): assert spline.npts == 9 assert spline.knotvector == [0, 0, 0, 0, 1, 1, 1, 2, 2, 3, 3, 3, 3] - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.dependency( depends=[ "TestSpline::test_begin", @@ -647,12 +647,12 @@ def test_end(self): class TestRational: - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.dependency(depends=["TestSpline::test_end"]) def test_begin(self): pass - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestRational::test_begin"]) def test_creation(self): @@ -665,7 +665,7 @@ def test_creation(self): assert rational.degree == degree assert rational.npts == npts - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestRational::test_creation"]) def test_fail_creation(self): @@ -679,7 +679,7 @@ def test_fail_creation(self): with pytest.raises(ValueError): rational.weights = -1 * np.ones(npts) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestRational::test_begin"]) def test_compare_spline(self): @@ -698,7 +698,7 @@ def test_compare_spline(self): assert id(rat_copy) != id(rational) assert rat_copy == rational - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestRational::test_begin"]) def test_values_rational_equal_spline(self): @@ -711,7 +711,7 @@ def test_values_rational_equal_spline(self): for node in nodes_sample: assert np.all(rational(node) == spline(node)) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestRational::test_begin"]) def test_quarter_circle_standard(self): @@ -730,7 +730,7 @@ def test_quarter_circle_standard(self): test_matrix = rational(nodes_sample) np.testing.assert_allclose(test_matrix, good_matrix) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestRational::test_quarter_circle_standard"]) def test_quarter_circle_symmetric(self): @@ -751,7 +751,7 @@ def test_quarter_circle_symmetric(self): test_matrix = rational(nodes_sample) np.testing.assert_allclose(test_matrix, good_matrix) - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.dependency( depends=[ "TestRational::test_begin", @@ -768,7 +768,7 @@ def test_end(self): class TestOthers: - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(1) @pytest.mark.dependency( depends=[ @@ -794,7 +794,7 @@ def test_print(self): spline.__repr__() rational.__repr__() - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_creation"]) def test_specific_cases(self): @@ -806,7 +806,7 @@ def test_specific_cases(self): assert bezier.knots[0] == 0 assert bezier.knots[1] == 1 - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_creation"]) def test_fail_getitem_index(self): @@ -822,7 +822,7 @@ def test_fail_getitem_index(self): with pytest.raises(TypeError): bezier[0, "asd"] - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_creation"]) def test_fractions(self): @@ -839,7 +839,7 @@ def test_fractions(self): assert type(bezier[0](0.5)) is float assert type(bezier[1](0.5)) is float - @pytest.mark.order(3) + @pytest.mark.order(32) @pytest.mark.dependency( depends=[ "TestOthers::test_print", @@ -852,7 +852,7 @@ def test_end(self): pass -@pytest.mark.order(3) +@pytest.mark.order(32) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/responsive/test_knotspace.py b/tests/responsive/test_knotspace.py index dfa79c0..8adff36 100644 --- a/tests/responsive/test_knotspace.py +++ b/tests/responsive/test_knotspace.py @@ -7,13 +7,13 @@ from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.dependency(depends=["tests/test_heavy.py::test_end"], scope="session") def test_begin(): pass -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_begin"]) def test_Creation(): @@ -41,7 +41,7 @@ def test_Creation(): KnotVector([0.0, 0.0, 0.5, 0.5, 1.0, 1.0]) -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_Creation"]) def test_FailCreation(): @@ -77,7 +77,7 @@ def test_FailCreation(): KnotVector([0, 0, 0.5, 0.5, 0.5, 0.5, 1, 1]) -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_Creation", "test_FailCreation"]) def test_ValuesDegree(): @@ -102,7 +102,7 @@ def test_ValuesDegree(): assert V.degree == 3 -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_Creation", "test_FailCreation"]) def test_ValuesNumberPoints(): @@ -129,7 +129,7 @@ def test_ValuesNumberPoints(): assert V.npts == 6 -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) def test_findspans_single(): @@ -156,7 +156,7 @@ def test_findspans_single(): U.span("asd") # Not a number -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) def test_findmult_single(): @@ -183,7 +183,7 @@ def test_findmult_single(): U.mult("asd") # Not a number -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_findspans_single"]) def test_findspans_array(): @@ -196,7 +196,7 @@ def test_findspans_array(): np.testing.assert_equal(suposedspans, correctspans) -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_findmult_single"]) def test_findmult_array(): @@ -209,7 +209,7 @@ def test_findmult_array(): np.testing.assert_equal(suposedmults, correctmults) -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) def test_CompareKnotvector(): @@ -226,13 +226,13 @@ def test_CompareKnotvector(): class TestOperations: - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_CompareKnotvector"]) def test_begin(self): pass - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["TestOperations::test_begin"]) def test_scale(self): @@ -285,7 +285,7 @@ def test_scale(self): assert U == U3 assert U != U4 - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["TestOperations::test_begin"]) def test_shift(self): @@ -311,7 +311,7 @@ def test_shift(self): U -= 1 assert U == U1 - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["TestOperations::test_begin"]) def test_normalize(self): @@ -323,7 +323,7 @@ def test_normalize(self): knotvector.normalize() assert knotvector == [0, 0, 0.5, 1, 1] - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["TestOperations::test_begin"]) def test_convert(self): @@ -344,7 +344,7 @@ def test_convert(self): assert isinstance(knot, Fraction) assert knotvector == [0, 0, 0.5, 1, 1] - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency( depends=[ @@ -360,7 +360,7 @@ def test_fails(self): with pytest.raises(ValueError): knotvector.convert(int) - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency( depends=[ @@ -377,7 +377,7 @@ def test_end(self): class TestGenerator: - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency( depends=["test_CompareKnotvector", "TestOperations::test_end"] @@ -385,7 +385,7 @@ class TestGenerator: def test_begin(self): pass - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["TestGenerator::test_begin"]) def test_bezier(self): @@ -416,7 +416,7 @@ def test_bezier(self): for knot in Utest: assert isinstance(knot, int) - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency( depends=["TestGenerator::test_begin", "TestGenerator::test_bezier"] @@ -460,7 +460,7 @@ def test_integer(self): for knot in Utest: assert isinstance(knot, int) - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency( depends=[ @@ -500,7 +500,7 @@ def test_uniform(self): assert Utest.npts == npts assert Utest.degree == degree - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency( depends=[ @@ -520,7 +520,7 @@ def test_random(self): assert knotvect.npts == npts assert knotvect.degree == degree - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency( depends=[ @@ -560,7 +560,7 @@ def test_weighted(self): assert isinstance(Utest, KnotVector) assert Utest == Ugood - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency( depends=[ @@ -612,7 +612,7 @@ def test_clstype(self): for knot in knotvector: assert isinstance(knot, cls) - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(2) @pytest.mark.dependency( depends=[ @@ -655,7 +655,7 @@ def test_fails(self): with pytest.raises(AssertionError): GeneratorKnotVector.random(degree=2.0, npts=3) - @pytest.mark.order(2) + @pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency( depends=[ @@ -673,7 +673,7 @@ def test_end(self): pass -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["TestGenerator::test_end"]) def test_compare_knotvectors_fail(): @@ -700,7 +700,7 @@ def test_compare_knotvectors_fail(): assert U4 != U5 -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["TestGenerator::test_end"]) def test_insert_knot_remove(): @@ -782,7 +782,7 @@ def test_insert_knot_remove(): U0 -= [0] # Take out one extremity -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["TestGenerator::test_end"]) def test_degree_change(): @@ -821,7 +821,7 @@ def test_degree_change(): assert U == [0, 0, 0, 0, 2, 2, 2, 2] -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["TestGenerator::test_end"]) def test_or_and(): @@ -854,7 +854,7 @@ def test_or_and(): U1 & U2 -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency( depends=[ @@ -882,7 +882,7 @@ def test_others(): knotvect.__repr__() -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.timeout(4) @pytest.mark.dependency( depends=[ @@ -914,7 +914,7 @@ def test_fractions(): assert type(knot) is frac -@pytest.mark.order(2) +@pytest.mark.order(30) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/responsive/test_rationalcurve.py b/tests/responsive/test_rationalcurve.py index 88aa1ff..f8e6dd2 100644 --- a/tests/responsive/test_rationalcurve.py +++ b/tests/responsive/test_rationalcurve.py @@ -9,7 +9,7 @@ from pynurbs.responsive.knotspace import GeneratorKnotVector -@pytest.mark.order(6) +@pytest.mark.order(35) @pytest.mark.dependency( depends=[ "tests/test_knotspace.py::test_end", @@ -24,12 +24,12 @@ def test_begin(): class TestBuild: - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestBuild::test_begin"]) def test_failbuild(self): @@ -42,7 +42,7 @@ def test_failbuild(self): with pytest.raises(ValueError): curve.weights = "asd" - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(15) @pytest.mark.dependency(depends=["TestBuild::test_failbuild"]) def test_print(self): @@ -54,7 +54,7 @@ def test_print(self): rational.weights = (2, 3, 1, 4) str(rational) - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency( depends=[ "TestBuild::test_begin", @@ -67,12 +67,12 @@ def test_end(self): class TestAddSubMulDiv: - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency(depends=["TestBuild::test_end"]) def test_begin(self): pass - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestAddSubMulDiv::test_begin"]) def test_bezier_known(self): @@ -88,7 +88,7 @@ def test_bezier_known(self): assert divatob.weights == (1, 1, 2) assert divatob.ctrlpoints == (0, 1, 1) - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.skip( reason="Standard fraction fails due to lack of precision. sympy.Rational works" @@ -135,7 +135,7 @@ def test_random_bezier_fractions(self): assert abs(adivb(ui) - (ai / bi)) < 1e-9 assert abs(bdiva(ui) - (bi / ai)) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=["TestAddSubMulDiv::test_begin", "TestAddSubMulDiv::test_bezier_known"] @@ -173,7 +173,7 @@ def test_random_bezier_float64(self): assert abs(adivb(ui) - (ai / bi)) < 1e-9 assert abs(bdiva(ui) - (bi / ai)) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=["TestAddSubMulDiv::test_begin", "TestAddSubMulDiv::test_bezier_known"] @@ -200,7 +200,7 @@ def test_others(self): assert id(invinverse) != id(curve) assert invinverse == curve - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=["TestAddSubMulDiv::test_begin", "TestAddSubMulDiv::test_bezier_known"] @@ -217,7 +217,7 @@ def test_zero_division(self): with pytest.raises(ValueError): curve.weights = [1, -4, 5, -1] - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency( depends=[ "TestAddSubMulDiv::test_begin", @@ -232,12 +232,12 @@ def test_end(self): class TestCircle: - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency(depends=["TestAddSubMulDiv::test_end"]) def test_begin(self): pass - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestCircle::test_begin"]) def test_quarter_circle_standard(self): @@ -254,7 +254,7 @@ def test_quarter_circle_standard(self): dist2 = sum(point**2) assert abs(dist2 - 1) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestCircle::test_quarter_circle_standard"]) def test_quarter_circle_symmetric(self): @@ -271,7 +271,7 @@ def test_quarter_circle_symmetric(self): dist2 = sum(point**2) assert abs(dist2 - 1) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.dependency( depends=[ @@ -293,7 +293,7 @@ def test_half_circle(self): dist2 = sum(point**2) assert abs(dist2 - 1) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.dependency( depends=[ @@ -315,7 +315,7 @@ def test_full_circle(self): dist2 = sum(point**2) assert abs(dist2 - 1) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency( depends=[ "TestCircle::test_begin", @@ -330,12 +330,12 @@ def test_end(self): class TestRandomInsertKnot: - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency(depends=["TestCircle::test_end"]) def test_begin(self): pass - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["TestRandomInsertKnot::test_begin"]) def test_none_weights_fraction(self): @@ -362,7 +362,7 @@ def test_none_weights_fraction(self): diff = oldpt - newpt assert float(diff**2) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -396,7 +396,7 @@ def test_unitary_weights_fraction(self): diff = oldpt - newpt assert float(diff**2) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -431,7 +431,7 @@ def test_const_weights_fraction(self): diff = oldpt - newpt assert float(diff**2) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -468,7 +468,7 @@ def test_random_weights_fraction(self): diff = oldpt - newpt assert float(diff**2) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency( depends=[ "TestRandomInsertKnot::test_begin", @@ -483,12 +483,12 @@ def test_end(self): class TestInsKnotCircle: - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency(depends=["TestRandomInsertKnot::test_end"]) def test_begin(self): pass - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestInsKnotCircle::test_begin"]) def test_quarter_circle_standard(self): @@ -512,7 +512,7 @@ def test_quarter_circle_standard(self): distsquare = sum(diff**2) assert float(distsquare) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestInsKnotCircle::test_quarter_circle_standard"]) def test_quarter_circle_symmetric(self): @@ -532,7 +532,7 @@ def test_quarter_circle_symmetric(self): for oldpt, newpt in zip(points_old, points_new): assert abs(np.linalg.norm(oldpt - newpt)) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.dependency( depends=[ @@ -557,7 +557,7 @@ def test_half_circle(self): for oldpt, newpt in zip(points_old, points_new): assert abs(np.linalg.norm(oldpt - newpt)) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.dependency( depends=[ @@ -582,7 +582,7 @@ def test_full_circle(self): for oldpt, newpt in zip(points_old, points_new): assert abs(np.linalg.norm(oldpt - newpt)) < 1e-9 - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency( depends=[ "TestInsKnotCircle::test_begin", @@ -597,12 +597,12 @@ def test_end(self): class TestCleanRational: - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency(depends=["TestRandomInsertKnot::test_end"]) def test_begin(self): pass - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.timeout(1) @pytest.mark.skip(reason="Needs correction") @pytest.mark.dependency(depends=["TestInsKnotCircle::test_begin"]) @@ -645,7 +645,7 @@ def test_divpolybezier(self): assert test_curve == good_curve - @pytest.mark.order(6) + @pytest.mark.order(35) @pytest.mark.dependency( depends=[ "TestInsKnotCircle::test_begin", @@ -659,7 +659,7 @@ def test_end(self): pass -@pytest.mark.order(6) +@pytest.mark.order(35) @pytest.mark.dependency( depends=["test_begin", "TestCircle::test_end", "TestInsKnotCircle::test_end"] ) diff --git a/tests/responsive/test_splinecurve.py b/tests/responsive/test_splinecurve.py index cc54e19..f279bea 100644 --- a/tests/responsive/test_splinecurve.py +++ b/tests/responsive/test_splinecurve.py @@ -7,7 +7,7 @@ from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector -@pytest.mark.order(5) +@pytest.mark.order(34) @pytest.mark.dependency( depends=[ "tests/test_knotspace.py::test_end", @@ -21,12 +21,12 @@ def test_begin(): class TestInitCurve: - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestInitCurve::test_begin"]) def test_build_scalar(self): @@ -35,7 +35,7 @@ def test_build_scalar(self): ctrlpoints = np.random.uniform(-1, 1, npts) Curve(knotvector, ctrlpoints) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestInitCurve::test_begin"]) def test_build_vectorial(self): @@ -45,7 +45,7 @@ def test_build_vectorial(self): ctrlpoints = np.random.uniform(-1, 1, (npts, ndim)) Curve(knotvector, ctrlpoints) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestInitCurve::test_build_scalar"]) def test_failbuild(self): @@ -61,7 +61,7 @@ def test_failbuild(self): with pytest.raises(TypeError): Curve(knotvector, 1) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency(depends=["TestInitCurve::test_build_scalar"]) def test_attributes(self): degree, npts = 3, 9 @@ -76,7 +76,7 @@ def test_attributes(self): assert hasattr(curve, "knotvector") assert hasattr(curve, "knots") - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency(depends=["TestInitCurve::test_build_scalar"]) def test_functions(self): degree, npts = 3, 9 @@ -94,7 +94,7 @@ def test_functions(self): assert hasattr(curve, "knot_clean") assert callable(curve) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency(depends=["TestInitCurve::test_attributes"]) def test_atributesgood(self): degree, npts = 3, 9 @@ -107,7 +107,7 @@ def test_atributesgood(self): np.testing.assert_allclose(curve.ctrlpoints, ctrlpoints) assert curve.knots == knotvector.knots - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency(depends=["TestInitCurve::test_build_vectorial"]) def test_compare_two_curves(self): @@ -127,7 +127,7 @@ def test_compare_two_curves(self): assert C1 == C3 assert C1 != C4 - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency(depends=["TestInitCurve::test_build_scalar"]) def test_print(self): @@ -137,7 +137,7 @@ def test_print(self): spline.ctrlpoints = [2, 4, 3, 1] str(spline) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=[ "TestInitCurve::test_begin", @@ -154,12 +154,12 @@ def test_end(self): class TestCompare: - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency(depends=["TestInitCurve::test_end"]) def test_begin(self): pass - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency(depends=["TestCompare::test_begin"]) def test_knotvector(self): degree, npts = 3, 7 @@ -184,7 +184,7 @@ def test_knotvector(self): curve1 = Curve(knotvector1, ctrlpoints) assert curve0 != curve1 - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency(depends=["TestCompare::test_begin"]) def test_controlpoints(self): npts = 7 @@ -196,7 +196,7 @@ def test_controlpoints(self): curve1 = Curve(knotvector1, ctrlpoints1) assert curve0 != curve1 - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency(depends=["TestCompare::test_begin"]) def test_userentry(self): npts = 7 @@ -209,7 +209,7 @@ def test_userentry(self): assert curve != ctrlpoints assert curve != [] - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=[ "TestCompare::test_begin", @@ -222,14 +222,14 @@ def test_end(self): class TestCallShape: - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=["TestCompare::test_end", "TestInitCurve::test_end"] ) def test_begin(self): pass - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency(depends=["TestCallShape::test_begin"]) def test_build_scalar(self): @@ -239,7 +239,7 @@ def test_build_scalar(self): ctrlpoints = np.random.uniform(-1, 1, npts) Curve(knotvector, ctrlpoints) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency(depends=["TestCallShape::test_begin"]) def test_build_vectorial(self): @@ -250,7 +250,7 @@ def test_build_vectorial(self): ctrlpoints = np.random.uniform(-1, 1, (npts, ndim)) Curve(knotvector, ctrlpoints) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -269,7 +269,7 @@ def test_callscal_scalpts(self): curvevalues = curve(tparam) assert type(curvevalues) == type(ctrlpoints[0]) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -291,7 +291,7 @@ def test_callscal_vectpts(self, ntests=1): assert type(curvevalues) == type(ctrlpoints[0]) assert type(curvevalues[0]) == type(ctrlpoints[0][0]) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -315,7 +315,7 @@ def test_callvect_scalpts(self): assert len(Cval) == nsample assert type(Cval[0]) == type(ctrlpoints[0]) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -341,7 +341,7 @@ def test_callvect_vectpts(self): assert type(curvevalues[0]) == type(ctrlpoints[0]) assert np.array(curvevalues).shape == (nsample, ndim) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -357,14 +357,14 @@ def test_end(self): class TestSumSubtract: - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=["TestCompare::test_end", "TestCallShape::test_end"] ) def test_begin(self): pass - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency(depends=["TestSumSubtract::test_begin"]) def test_sumsub_failknotvector(self): """ @@ -385,7 +385,7 @@ def test_sumsub_failknotvector(self): with pytest.raises(ValueError): C1 - C2 - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency(depends=["TestSumSubtract::test_begin"]) def test_sumsub_scalar(self): @@ -405,7 +405,7 @@ def test_sumsub_scalar(self): assert (C1 + C2) == Cadd assert (C1 - C2) == Csub - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -428,7 +428,7 @@ def test_sumsub_vector(self): assert (C1 + C2) == Cs assert (C1 - C2) == Cd - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency(depends=["TestSumSubtract::test_begin"]) def test_somefails(self): knotvector = GeneratorKnotVector.random(3, 7) @@ -437,7 +437,7 @@ def test_somefails(self): with pytest.raises(TypeError): curve + "asd" - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=[ "TestSumSubtract::test_begin", @@ -452,14 +452,14 @@ def test_end(self): class TestKnotOperations: - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=["TestCompare::test_end", "TestCallShape::test_end"] ) def test_begin(self): pass - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -497,7 +497,7 @@ def test_insert_known_case(self): Cinse = Curve(Uinse, Q) assert Corig == Cinse - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -529,7 +529,7 @@ def test_remove_known_case(self): Cinse = Curve(Uinse, Q) assert Corig == Cinse - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -553,7 +553,7 @@ def test_insert_remove_once_random(self): assert curve == Curve(knotvector, ctrlpoints) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -571,7 +571,7 @@ def test_knotclean(self): curve.knot_clean() assert curve.knotvector == U - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -592,7 +592,7 @@ def test_knotclean_random(self): curve.knot_clean() assert curve.knotvector == knotvector - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -625,7 +625,7 @@ def test_somefails(self): curve = Curve(knotvector) curve.knot_insert([0.2, 0.8]) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=[ "TestKnotOperations::test_begin", @@ -642,7 +642,7 @@ def test_end(self): class TestSplitUnite: - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=[ "TestCompare::test_end", @@ -652,7 +652,7 @@ class TestSplitUnite: def test_begin(self): pass - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -678,7 +678,7 @@ def test_split_number_curves(self): assert len(C.split([0.25, 0.75])) == 3 assert C == Curve(U, P) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -697,7 +697,7 @@ def test_split_matchboundary(self): assert min(curves[1].knotvector) == 0.5 assert max(curves[1].knotvector) == 1 - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -722,7 +722,7 @@ def test_splitrand_matchboundary(self): for i, knot in enumerate(knots[1:-1]): np.all(curves[i](knot) == curves[i + 1](knot)) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -744,7 +744,7 @@ def test_split_knowncase1(self): assert len(curves) == 1 assert curves[0] == curve_original - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -765,7 +765,7 @@ def test_split_knowncase2(self): assert curves[0].knotvector == [0, 0, 0, 0.5, 0.5, 0.5] assert curves[1].knotvector == [0.5, 0.5, 0.5, 1, 1, 1] - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -792,7 +792,7 @@ def test_unite_knowncase(self): assert curves[0] == curve0 assert curves[1] == curve1 - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=[ "TestSplitUnite::test_begin", @@ -816,7 +816,7 @@ def test_somefails(self): with pytest.raises(ValueError): curve1 | curve0 - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=[ "TestSplitUnite::test_begin", @@ -834,7 +834,7 @@ def test_end(self): class TestDegreeOperations: - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -845,7 +845,7 @@ class TestDegreeOperations: def test_begin(self): pass - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["TestDegreeOperations::test_begin"]) def test_increase_decrease_random(self): @@ -861,7 +861,7 @@ def test_increase_decrease_random(self): assert curve.degree == degree np.testing.assert_allclose(curve.ctrlpoints, curve.ctrlpoints) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=[ @@ -881,7 +881,7 @@ def test_clean(self): assert curve.degree == degree np.testing.assert_allclose(curve.ctrlpoints, ctrlpoints) - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency( depends=["TestDegreeOperations::test_begin", "TestDegreeOperations::test_clean"] @@ -901,7 +901,7 @@ def test_fails(self): with pytest.raises(ValueError): curve.degree = "asd" - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=[ "TestDegreeOperations::test_begin", @@ -914,7 +914,7 @@ def test_end(self): class TestOthers: - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(15) @pytest.mark.dependency( depends=[ @@ -930,7 +930,7 @@ class TestOthers: def test_begin(self): pass - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["TestOthers::test_begin"]) def test_others(self): @@ -957,7 +957,7 @@ def test_others(self): curve.knot_remove([1.5]) curve.knot_remove([1.5], None) # No tolerance - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["TestOthers::test_begin"]) def test_fractions(self): @@ -992,7 +992,7 @@ def test_fractions(self): assert type(tval) is frac assert tval == gval - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency(depends=["TestOthers::test_begin"]) def test_fraction_function(self): @@ -1004,7 +1004,7 @@ def test_fraction_function(self): newcurve = num / den assert curve == newcurve - @pytest.mark.order(5) + @pytest.mark.order(34) @pytest.mark.dependency( depends=[ "TestOthers::test_begin", @@ -1017,7 +1017,7 @@ def test_end(self): pass -@pytest.mark.order(5) +@pytest.mark.order(34) @pytest.mark.dependency( depends=[ "test_begin", From 047637dce29c05abe9cdc97548f1cb4dc129a1f6 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:48:11 +0200 Subject: [PATCH 050/116] test: move test of operations on knotvector to operations folder --- tests/{responsive => operations}/test_knotvector.py | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename tests/{responsive => operations}/test_knotvector.py (100%) diff --git a/tests/responsive/test_knotvector.py b/tests/operations/test_knotvector.py similarity index 100% rename from tests/responsive/test_knotvector.py rename to tests/operations/test_knotvector.py From 5cc6cf593d500eafb2cbb0004742b8c188ed24ca Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:51:47 +0200 Subject: [PATCH 051/116] test: change order of core tests --- tests/core/test_basis_function.py | 26 ++++++------- tests/core/test_custom_math.py | 62 +++++++++++++++---------------- tests/core/test_knotvector.py | 22 +++++------ tests/core/test_piecewise.py | 26 ++++++------- tests/core/test_polynomial.py | 30 +++++++-------- 5 files changed, 83 insertions(+), 83 deletions(-) diff --git a/tests/core/test_basis_function.py b/tests/core/test_basis_function.py index bef88d6..cf0b960 100644 --- a/tests/core/test_basis_function.py +++ b/tests/core/test_basis_function.py @@ -21,7 +21,7 @@ def binom(n: int, i: int): return int(prod) -@pytest.mark.order(3) +@pytest.mark.order(13) @pytest.mark.dependency( depends=[ "tests/core/test_knotvector.py::test_end", @@ -34,12 +34,12 @@ def test_begin(): class TestBezier: - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestBezier::test_begin"]) def test_creation(self): @@ -69,7 +69,7 @@ def test_creation(self): assert bezier.degree == degree assert bezier.npts == npts - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_creation"]) def test_sum_equal_to_1(self): @@ -111,7 +111,7 @@ def test_sum_equal_to_1(self): assert all(result >= 0 for result in results) assert sum(results) == 1 - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.timeout(5) @pytest.mark.dependency( depends=[ @@ -158,7 +158,7 @@ def test_single_values(self): ) assert bezier(node) == tuple(goods) - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.dependency( depends=[ "TestBezier::test_begin", @@ -172,12 +172,12 @@ def test_all(self): class TestSpline: - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.dependency(depends=["TestBezier::test_all"]) def test_begin(self): pass - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestSpline::test_begin"]) def test_creation(self): @@ -205,7 +205,7 @@ def test_creation(self): assert spline.degree == 2 assert spline.npts == 4 - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_creation"]) def test_tablevalues_degree1npts3(self): @@ -232,7 +232,7 @@ def test_tablevalues_degree1npts3(self): for node, good in zip(nodes_test, matrix_good): np.testing.assert_allclose(spline(node), good) - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree1npts3"]) def test_tablevalues_degree2npts4(self): @@ -260,7 +260,7 @@ def test_tablevalues_degree2npts4(self): for node, good in zip(nodes_test, matrix_good): np.testing.assert_allclose(spline(node), good) - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree2npts4"]) def test_tablevalues_degree3npts5(self): @@ -287,7 +287,7 @@ def test_tablevalues_degree3npts5(self): for node, good in zip(nodes_test, matrix_good): np.testing.assert_allclose(spline(node), good) - @pytest.mark.order(3) + @pytest.mark.order(13) @pytest.mark.dependency( depends=[ "TestSpline::test_begin", @@ -301,7 +301,7 @@ def test_all(self): pass -@pytest.mark.order(3) +@pytest.mark.order(13) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/core/test_custom_math.py b/tests/core/test_custom_math.py index 0dc9be9..11f35bb 100644 --- a/tests/core/test_custom_math.py +++ b/tests/core/test_custom_math.py @@ -7,19 +7,19 @@ from pynurbs.core.custom_math import IntegratorArray, Linalg, Math, NodeSample -@pytest.mark.order(1) +@pytest.mark.order(11) @pytest.mark.dependency() def test_begin(): pass class TestMath: - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestMath::test_begin"]) def test_gcd(self): assert Math.gcd(0) == 0 @@ -35,7 +35,7 @@ def test_gcd(self): assert Math.gcd(2, 3, 4) == 1 assert Math.gcd(6, 9, 12) == 3 - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestMath::test_begin", "TestMath::test_gcd"]) def test_lcm(self): assert Math.lcm(0) == 0 @@ -52,7 +52,7 @@ def test_lcm(self): assert Math.lcm(2, 3, 4) == 12 assert Math.lcm(6, 9, 12) == 36 - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestMath::test_begin"]) def test_comb(self): assert Math.comb(1, 0) == 1 @@ -65,7 +65,7 @@ def test_comb(self): assert Math.comb(3, 2) == 3 assert Math.comb(3, 3) == 1 - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "TestMath::test_begin", @@ -79,19 +79,19 @@ def test_end(self): class TestLinalg: - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["test_begin", "TestMath::test_end"]) def test_begin(self): pass - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestLinalg::test_begin"]) def test_invert_float(self): identit = np.eye(4) inverse = Linalg.invert(identit) np.testing.assert_allclose(inverse, identit) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=["TestLinalg::test_begin", "TestLinalg::test_invert_float"] ) @@ -142,7 +142,7 @@ def test_invert_integer(self): np.testing.assert_allclose(np.dot(inverse, matrix), np.eye(side)) np.testing.assert_allclose(np.dot(matrix, inverse), np.eye(side)) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "TestLinalg::test_begin", @@ -209,7 +209,7 @@ def test_invert_fraction(self): product = np.array(product, dtype="float64") np.testing.assert_allclose(product, np.eye(size)) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=["TestLinalg::test_begin", "TestLinalg::test_invert_fraction"] ) @@ -220,7 +220,7 @@ def test_solve_float(self): solution = Linalg.solve(matrix, force) np.testing.assert_allclose(np.dot(matrix, solution), force) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=["TestLinalg::test_begin", "TestLinalg::test_solve_float"] ) @@ -271,7 +271,7 @@ def test_solve_integer(self): mult = np.dot(matrix, solution) np.testing.assert_allclose(mult, force) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "TestLinalg::test_begin", @@ -304,7 +304,7 @@ def test_solve_fraction(self): np.testing.assert_allclose(np.dot(inverse, matrix), np.eye(side)) np.testing.assert_allclose(np.dot(matrix, inverse), np.eye(side)) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "TestLinalg::test_begin", @@ -354,7 +354,7 @@ def test_specific_case(self): diff = np.array(mult - B, dtype="float64") np.testing.assert_allclose(diff, np.zeros(B.shape)) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "TestLinalg::test_begin", @@ -387,7 +387,7 @@ def test_specific_case2(self): prod = np.dot(inverse, matrix).astype("float64") np.testing.assert_allclose(prod, np.eye(3)) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "TestLinalg::test_begin", @@ -405,14 +405,14 @@ def test_end(self): class TestNodeSample: - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=["test_begin", "TestMath::test_end", "TestLinalg::test_end"] ) def test_begin(self): pass - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) def test_closed_linspace(self): nodes = NodeSample.closed_linspace(2) @@ -432,7 +432,7 @@ def test_closed_linspace(self): good = (0, 1 / 4, 2 / 4, 3 / 4, 1) assert nodes == good - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) def test_open_linspace(self): nodes = NodeSample.open_linspace(1) @@ -457,7 +457,7 @@ def test_open_linspace(self): good = (1 / 10, 3 / 10, 5 / 10, 7 / 10, 9 / 10) np.testing.assert_allclose(nodes, good) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) def test_chebyshev(self): nodes = NodeSample.chebyshev(1) @@ -483,7 +483,7 @@ def test_chebyshev(self): good = np.sin(np.pi * np.array([1 / 20, 3 / 20, 5 / 20, 7 / 20, 9 / 20])) ** 2 np.testing.assert_allclose(nodes, good) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestNodeSample::test_begin"]) def test_gauss_legendre(self): nodes = NodeSample.gauss_legendre(1) @@ -517,7 +517,7 @@ def test_gauss_legendre(self): ] np.testing.assert_allclose(nodes, good) - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "TestNodeSample::test_begin", @@ -532,7 +532,7 @@ def test_end(self): class TestUnidimentionIntegral: - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "test_begin", @@ -544,7 +544,7 @@ class TestUnidimentionIntegral: def test_begin(self): pass - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) def test_closed_newton_cotes(self): a, b = Fraction(3), Fraction(5) @@ -568,7 +568,7 @@ def test_closed_newton_cotes(self): assert test == good - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) def test_open_newton_cotes(self): a, b = Fraction(3), Fraction(5) @@ -592,7 +592,7 @@ def test_open_newton_cotes(self): assert test == good - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) def test_chebyshev(self): a, b = Fraction(3), Fraction(7) @@ -616,7 +616,7 @@ def test_chebyshev(self): assert abs(test - good) < 1e-9 - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestUnidimentionIntegral::test_begin"]) def test_gauss_legendre(self): a, b = Fraction(3), Fraction(7) @@ -640,7 +640,7 @@ def test_gauss_legendre(self): assert abs(test - good) < 1e-9 - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "TestUnidimentionIntegral::test_begin", @@ -699,7 +699,7 @@ def test_exact_integral_fraction(self): test = (b - a) * np.inner(weights, funcvals) assert abs(test - good) < 1e-9 - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "TestUnidimentionIntegral::test_begin", @@ -757,7 +757,7 @@ def test_approx_integral(self): test = (b - a) * np.inner(weights, funcvals) assert abs(test - good) < 1e-9 - @pytest.mark.order(1) + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ "TestUnidimentionIntegral::test_begin", @@ -773,7 +773,7 @@ def test_end(self): pass -@pytest.mark.order(1) +@pytest.mark.order(11) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/core/test_knotvector.py b/tests/core/test_knotvector.py index 0343978..8fa1715 100644 --- a/tests/core/test_knotvector.py +++ b/tests/core/test_knotvector.py @@ -4,13 +4,13 @@ from pynurbs.core import ImmutableKnotVector -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency() def test_begin(): pass -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_begin"]) def test_Creation(): @@ -38,7 +38,7 @@ def test_Creation(): ImmutableKnotVector([0.0, 0.0, 0.5, 0.5, 1.0, 1.0]) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_Creation"]) def test_FailCreation(): @@ -74,7 +74,7 @@ def test_FailCreation(): ImmutableKnotVector([0, 0, 0.5, 0.5, 0.5, 0.5, 1, 1]) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_Creation", "test_FailCreation"]) def test_ValuesDegree(): @@ -99,7 +99,7 @@ def test_ValuesDegree(): assert V.degree == 3 -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_Creation", "test_FailCreation"]) def test_ValuesNumberPoints(): @@ -126,7 +126,7 @@ def test_ValuesNumberPoints(): assert V.npts == 6 -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) def test_findspans_single(): @@ -153,7 +153,7 @@ def test_findspans_single(): U.span("asd") # Not a number -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) def test_findmult_single(): @@ -180,7 +180,7 @@ def test_findmult_single(): U.mult("asd") # Not a number -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_findspans_single"]) def test_findspans_array(): @@ -193,7 +193,7 @@ def test_findspans_array(): np.testing.assert_equal(suposedspans, correctspans) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.timeout(2) @pytest.mark.dependency(depends=["test_findmult_single"]) def test_findmult_array(): @@ -206,7 +206,7 @@ def test_findmult_array(): np.testing.assert_equal(suposedmults, correctmults) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) def test_CompareImmutableKnotVector(): @@ -222,7 +222,7 @@ def test_CompareImmutableKnotVector(): assert U1 != "asad" -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/core/test_piecewise.py b/tests/core/test_piecewise.py index 9646f14..898c645 100644 --- a/tests/core/test_piecewise.py +++ b/tests/core/test_piecewise.py @@ -23,7 +23,7 @@ def get_random_knots( return tuple(nodes) -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency( depends=[ "tests/core/test_knotvector.py::test_end", @@ -34,7 +34,7 @@ def test_begin(): pass -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency(depends=["test_begin"]) def test_find_span(): knots = [0, 1, 2, 3, 4] @@ -50,7 +50,7 @@ def test_find_span(): assert find_span(3.5, knots) == 3 -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency(depends=["test_begin"]) def test_build(): x = Polynomial([0, 1]) @@ -58,7 +58,7 @@ def test_build(): PiecewisePolynomial(polys, range(1 + len(polys))) -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency(depends=["test_build", "test_find_span"]) def test_evaluate(): x = Polynomial([0, 1]) @@ -75,7 +75,7 @@ def test_evaluate(): assert piece(4) == -61 -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency(depends=["test_build"]) def test_compare(): x = Polynomial([0, 1]) @@ -89,7 +89,7 @@ def test_compare(): assert piecea != pieceb -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency(depends=["test_build", "test_evaluate"]) def test_add(): nsegs, degree = 6, 4 @@ -108,7 +108,7 @@ def test_add(): assert abs(piecea(x) + pieceb(x) - piecec(x)) < 1e-9 -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency(depends=["test_build", "test_evaluate"]) def test_neg(): nsegs, degree = 6, 4 @@ -121,7 +121,7 @@ def test_neg(): assert pieceb(x) == -piecea(x) -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency(depends=["test_build", "test_evaluate", "test_add", "test_neg"]) def test_sub(): nsegs, degree = 6, 4 @@ -142,7 +142,7 @@ def test_sub(): assert abs(piecea(x) - pieceb(x) - pieced(x)) < 1e-9 -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency( depends=["test_build", "test_evaluate", "test_neg", "test_add", "test_sub"] ) @@ -163,7 +163,7 @@ def test_mul(): assert abs(piecea(x) * pieceb(x) - piecec(x)) < 1e-9 -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency( depends=["test_build", "test_neg", "test_add", "test_sub", "test_mul"] ) @@ -184,7 +184,7 @@ def test_matmul(): assert abs(piecea(x) @ pieceb(x) - piecec(x)) < 1e-9 -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency( depends=[ "test_build", @@ -235,7 +235,7 @@ def test_scalar_operation(): assert abs(piecea(node) @ const - pieceb(node)) < 1e-9 -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency( depends=[ "test_build", @@ -250,7 +250,7 @@ def test_print(): repr(piecewise) -@pytest.mark.order(1) +@pytest.mark.order(13) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/core/test_polynomial.py b/tests/core/test_polynomial.py index b84734a..c540713 100644 --- a/tests/core/test_polynomial.py +++ b/tests/core/test_polynomial.py @@ -3,7 +3,7 @@ from pynurbs.core.polynomial import Polynomial, derivate, integrate, scale, shift -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency() def test_build(): Polynomial([0]) # p(x) = 0 @@ -13,7 +13,7 @@ def test_build(): Polynomial([1.0, 2, -3.0]) # p(x) = 1.0 + 2 * x - 3.0 * x^2 -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency(depends=["test_build"]) def test_degree(): poly = Polynomial([0]) # p(x) = 0 @@ -26,7 +26,7 @@ def test_degree(): assert poly.degree == 2 -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency(depends=["test_build", "test_degree"]) def test_evaluate(): poly = Polynomial([0]) # p(x) = 0 @@ -47,7 +47,7 @@ def test_evaluate(): assert poly(2) == 1 + 2 * (+2) + 3 * (+2) * (+2) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_neg(): polya = Polynomial([1, 2, 3, 4]) @@ -56,7 +56,7 @@ def test_neg(): assert -polya == polyb -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_add(): """ @@ -96,7 +96,7 @@ def test_add(): np.testing.assert_allclose(const + valuesa, valuesc) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_sub(): """ @@ -136,7 +136,7 @@ def test_sub(): np.testing.assert_allclose(const - valuesa, valuesc) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_mul(): """ @@ -176,7 +176,7 @@ def test_mul(): np.testing.assert_allclose(const * valuesa, valuesc) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_truediv(): """ @@ -195,7 +195,7 @@ def test_truediv(): assert Polynomial(coefsa) / divisor == Polynomial(coefsb) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_pow(): poly = Polynomial([-1, 1]) @@ -204,7 +204,7 @@ def test_pow(): assert poly**4 == Polynomial([1, -4, 6, -4, 1]) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency( depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] ) @@ -223,7 +223,7 @@ def test_derivate(): assert derivate(poly, 3) == Polynomial([6, 24]) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency( depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] ) @@ -238,7 +238,7 @@ def test_integrate(): assert integrate(poly, (-2, 1)) == 15 -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency( depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] ) @@ -263,7 +263,7 @@ def test_shift(): np.testing.assert_allclose(valuese, valuesa) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency( depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] ) @@ -288,7 +288,7 @@ def test_scale(): np.testing.assert_allclose(valuesb, valuesa) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency(depends=["test_build"]) def test_print(): poly = Polynomial([0]) @@ -300,7 +300,7 @@ def test_print(): repr(poly) -@pytest.mark.order(1) +@pytest.mark.order(12) @pytest.mark.dependency( depends=[ "test_build", From 8012844e7ba11bf146729f99058a9eaf1ed0d74b Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:53:54 +0200 Subject: [PATCH 052/116] test: change order of operations tests --- tests/operations/test_advanced.py | 22 +++++++------- tests/operations/test_calculus.py | 42 +++++++++++++-------------- tests/operations/test_customstruc.py | 18 ++++++------ tests/operations/test_fitting.py | 34 +++++++++++----------- tests/operations/test_knotvector.py | 12 ++++---- tests/operations/test_least_square.py | 8 ++--- tests/operations/test_roots.py | 8 ++--- 7 files changed, 72 insertions(+), 72 deletions(-) diff --git a/tests/operations/test_advanced.py b/tests/operations/test_advanced.py index 6ff318d..9fb984f 100644 --- a/tests/operations/test_advanced.py +++ b/tests/operations/test_advanced.py @@ -10,7 +10,7 @@ from pynurbs.responsive.curves import Curve -@pytest.mark.order(8) +@pytest.mark.order(42) @pytest.mark.dependency( depends=[ "tests/test_knotspace.py::test_end", @@ -26,12 +26,12 @@ def test_begin(): class TestProjection: - @pytest.mark.order(8) + @pytest.mark.order(42) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(8) + @pytest.mark.order(42) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["TestProjection::test_begin"]) def test_point_on_curve(self): @@ -69,7 +69,7 @@ def test_point_on_curve(self): np.testing.assert_allclose(project((1, 2)), (4,)) np.testing.assert_allclose(project((1, 0)), (1.5, 2.5)) - @pytest.mark.order(8) + @pytest.mark.order(42) @pytest.mark.dependency( depends=["TestProjection::test_begin", "TestProjection::test_point_on_curve"] ) @@ -78,12 +78,12 @@ def test_end(self): class TestIntersection: - @pytest.mark.order(8) + @pytest.mark.order(42) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(8) + @pytest.mark.order(42) @pytest.mark.timeout(4) @pytest.mark.dependency(depends=["TestIntersection::test_begin"]) def test_bcurve_and_bcurve(self): @@ -103,7 +103,7 @@ def test_bcurve_and_bcurve(self): assert len(inters) == 1 np.testing.assert_allclose(inters[0], (0.5, 0.5)) - @pytest.mark.order(8) + @pytest.mark.order(42) @pytest.mark.timeout(50) @pytest.mark.dependency( depends=[ @@ -136,7 +136,7 @@ def test_quarter_circles(self): root = 1 / np.sqrt(3) np.testing.assert_allclose(inters[0], (root, root)) - @pytest.mark.order(8) + @pytest.mark.order(42) @pytest.mark.timeout(50) @pytest.mark.dependency( depends=[ @@ -158,7 +158,7 @@ def test_half_circles(self): root = (np.sqrt(3) - 1) / 2 np.testing.assert_allclose(inters[0], (root, root)) - @pytest.mark.order(8) + @pytest.mark.order(42) @pytest.mark.timeout(50) @pytest.mark.dependency( depends=[ @@ -194,7 +194,7 @@ def test_circle_and_circle(self): distance = np.abs(pointa - pointb) assert np.all(distance < 1e-9) - @pytest.mark.order(8) + @pytest.mark.order(42) @pytest.mark.dependency( depends=[ "TestIntersection::test_begin", @@ -208,7 +208,7 @@ def test_end(self): pass -@pytest.mark.order(8) +@pytest.mark.order(42) @pytest.mark.dependency( depends=["test_begin", "TestProjection::test_end", "TestIntersection::test_end"] ) diff --git a/tests/operations/test_calculus.py b/tests/operations/test_calculus.py index 8c723ba..ecd8cc9 100644 --- a/tests/operations/test_calculus.py +++ b/tests/operations/test_calculus.py @@ -13,7 +13,7 @@ from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector -@pytest.mark.order(7) +@pytest.mark.order(41) @pytest.mark.dependency( depends=[ "tests/test_knotspace.py::test_end", @@ -29,12 +29,12 @@ def test_begin(): class TestDerivBezier: - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestDerivBezier::test_begin"]) def test_bezier_degree1(self): curve = Curve(GeneratorKnotVector.bezier(1)) @@ -47,7 +47,7 @@ def test_bezier_degree1(self): assert test_curve == good_curve - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestDerivBezier::test_bezier_degree1"]) def test_bezier_degree2(self): curve = Curve(GeneratorKnotVector.bezier(2)) @@ -61,7 +61,7 @@ def test_bezier_degree2(self): assert test_curve == good_curve - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestDerivBezier::test_bezier_degree2"]) def test_bezier_degree3(self): curve = Curve(GeneratorKnotVector.bezier(3)) @@ -79,7 +79,7 @@ def test_bezier_degree3(self): assert test_curve == good_curve - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestDerivBezier::test_bezier_degree2"]) def test_random_degree(self): for degree in range(1, 7): @@ -98,7 +98,7 @@ def test_random_degree(self): assert test_curve == good_curve - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestDerivBezier::test_begin", @@ -113,12 +113,12 @@ def test_end(self): class TestNumericalDeriv: - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestNumericalDeriv::test_begin"]) def test_bezier(self): deltau = 1e-6 @@ -134,7 +134,7 @@ def test_bezier(self): dnumer = (curve(node + deltau) - curve(node - deltau)) / (2 * deltau) assert np.abs(dcurve(node) - dnumer) < 1e-6 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=["TestNumericalDeriv::test_begin", "TestNumericalDeriv::test_bezier"] ) @@ -158,7 +158,7 @@ def test_spline(self): ) assert np.abs(dcurve(node) - dnumer) < 1e-6 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestNumericalDeriv::test_begin", @@ -183,7 +183,7 @@ def test_rationalbezier(self): dnumer /= 2 * deltau assert np.abs(dcurve(node) - dnumer) < 1e-6 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestNumericalDeriv::test_begin", @@ -212,7 +212,7 @@ def test_rationalspline(self): dnumer /= 2 * deltau assert np.abs(dcurve(node) - dnumer) < 1e-6 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestNumericalDeriv::test_begin", @@ -226,7 +226,7 @@ def test_end(self): pass -@pytest.mark.order(7) +@pytest.mark.order(41) @pytest.mark.dependency(depends=["TestDerivBezier::test_end"]) def test_derivate_integers_knotvector(): knotvector = [0, 0, 1, 2, 3, 4, 5, 5] @@ -242,7 +242,7 @@ def test_derivate_integers_knotvector(): assert np.abs((points[i + 1] - points[i]) - dcurve(node)) < 1e-9 -@pytest.mark.order(7) +@pytest.mark.order(41) @pytest.mark.dependency(depends=["TestDerivBezier::test_end"]) def test_example31page94nurbsbook(): # Example 3.1 at page 94 of Nurbs book @@ -264,12 +264,12 @@ def test_example31page94nurbsbook(): class TestIntegBezier: - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestIntegBezier::test_begin"]) def test_scalar_integral(self): curve = Curve(GeneratorKnotVector.bezier(1)) @@ -293,7 +293,7 @@ def test_scalar_integral(self): good = sum(points) / 4 assert abs(test - good) < 1e-9 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestIntegBezier::test_begin"]) def test_lenght_integral(self): curve = Curve(GeneratorKnotVector.bezier(1)) @@ -316,7 +316,7 @@ def test_lenght_integral(self): assert abs(test - good) < 1e-9 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestIntegBezier::test_begin"]) def test_winding_number(self): knotvector = GeneratorKnotVector.uniform(1, 5) @@ -366,7 +366,7 @@ def test_winding_number(self): test = Integrate.function(knotvector, function, "open-newton-cotes") assert abs(test - 2 * np.pi) < 1 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestIntegBezier::test_begin", @@ -379,7 +379,7 @@ def test_end(self): pass -@pytest.mark.order(7) +@pytest.mark.order(41) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/operations/test_customstruc.py b/tests/operations/test_customstruc.py index cb8efaf..544f1a6 100644 --- a/tests/operations/test_customstruc.py +++ b/tests/operations/test_customstruc.py @@ -109,7 +109,7 @@ def __rmul__(self, other: CustomFloat): return self.__class__(other * self.internal) -@pytest.mark.order(7) +@pytest.mark.order(41) @pytest.mark.dependency( depends=[ "tests/test_knotspace.py::test_end", @@ -124,7 +124,7 @@ def test_begin(): pass -@pytest.mark.order(7) +@pytest.mark.order(41) @pytest.mark.dependency(depends=["test_begin"]) def test_custom_float(): a = CustomFloat(1) @@ -153,12 +153,12 @@ def test_custom_float(): class TestKnotVector: - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["test_begin", "test_custom_float"]) def test_begin(self): pass - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestKnotVector::test_begin"]) def test_creation(self): @@ -170,7 +170,7 @@ def test_creation(self): assert type(vector[3]) is CustomFloat tuple(vector) - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=["TestKnotVector::test_begin", "TestKnotVector::test_creation"] ) @@ -179,12 +179,12 @@ def test_end(self): class TestBasisFunctions: - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["test_begin", "TestKnotVector::test_end"]) def test_begin(self): pass - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.timeout(1) # @pytest.mark.skip(reason="Needs correction") @pytest.mark.dependency(depends=["TestBasisFunctions::test_begin"]) @@ -195,7 +195,7 @@ def test_creation(self): assert type(N[0](a)) is CustomFloat assert type(N[0](b)) is CustomFloat - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=["TestBasisFunctions::test_begin", "TestBasisFunctions::test_creation"] ) @@ -203,7 +203,7 @@ def test_end(self): pass -@pytest.mark.order(7) +@pytest.mark.order(41) @pytest.mark.dependency( depends=["test_begin", "TestKnotVector::test_end", "TestBasisFunctions::test_end"] ) diff --git a/tests/operations/test_fitting.py b/tests/operations/test_fitting.py index f292cb9..02e9620 100644 --- a/tests/operations/test_fitting.py +++ b/tests/operations/test_fitting.py @@ -5,7 +5,7 @@ from pynurbs.responsive.knotspace import GeneratorKnotVector -@pytest.mark.order(7) +@pytest.mark.order(41) @pytest.mark.dependency( depends=[ "tests/test_knotspace.py::test_end", @@ -21,12 +21,12 @@ def test_begin(): class TestCurve: - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestCurve::test_begin"]) def test_constant(self): vector = GeneratorKnotVector.bezier(0) @@ -39,7 +39,7 @@ def test_constant(self): test_curve.fit(good_curve) assert test_curve == good_curve - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=["TestCurve::test_begin", "TestCurve::test_constant"] ) @@ -55,7 +55,7 @@ def test_overdefined_spline(self): test_curve.fit(good_curve) assert test_curve == good_curve - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestCurve::test_begin", @@ -76,7 +76,7 @@ def test_overdefined_rational(self): test_curve.fit(good_curve) assert test_curve == good_curve - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestCurve::test_begin", @@ -90,12 +90,12 @@ def test_end(self): class TestFunction: - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestFunction::test_begin"]) def test_constant(self): constval = np.random.uniform(-1, 1) @@ -107,7 +107,7 @@ def test_constant(self): for point in test_curve.ctrlpoints: assert np.abs(point - constval) < 1e-9 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=["TestFunction::test_begin", "TestFunction::test_constant"] ) @@ -123,7 +123,7 @@ def test_overdefined_spline(self): for ui in usample: assert np.abs(test_curve(ui) - function(ui)) < 1e-9 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestFunction::test_begin", @@ -144,7 +144,7 @@ def test_overdefined_rational(self): for ui in usample: assert np.abs(test_curve(ui) - function(ui)) < 1e-9 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestFunction::test_begin", @@ -158,13 +158,13 @@ def test_end(self): class TestPoints: - @pytest.mark.order(7) + @pytest.mark.order(41) # @pytest.mark.skip(reason="Needs implementation") @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency(depends=["TestPoints::test_begin"]) def test_constant(self): usample = np.linspace(0, 1, 9) @@ -177,7 +177,7 @@ def test_constant(self): for point in test_curve.ctrlpoints: assert np.abs(point - constval) < 1e-9 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=["TestPoints::test_begin", "TestPoints::test_constant"] ) @@ -195,7 +195,7 @@ def test_overdefined_spline(self): for ui, valui in zip(usample, values): assert np.abs(test_curve(ui) - valui) < 1e-9 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestPoints::test_begin", @@ -218,7 +218,7 @@ def test_overdefined_rational(self): for ui, valui in zip(usample, values): assert np.abs(test_curve(ui) - valui) < 1e-9 - @pytest.mark.order(7) + @pytest.mark.order(41) @pytest.mark.dependency( depends=[ "TestPoints::test_begin", @@ -231,7 +231,7 @@ def test_end(self): pass -@pytest.mark.order(7) +@pytest.mark.order(41) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/operations/test_knotvector.py b/tests/operations/test_knotvector.py index 3f6146c..3d1cce2 100644 --- a/tests/operations/test_knotvector.py +++ b/tests/operations/test_knotvector.py @@ -9,7 +9,7 @@ ) -@pytest.mark.order(3) +@pytest.mark.order(21) @pytest.mark.dependency( depends=[ "tests/core/test_knotvector.py::test_end", @@ -20,21 +20,21 @@ def test_begin(): pass -@pytest.mark.order(3) +@pytest.mark.order(21) @pytest.mark.dependency(depends=["test_begin"]) def test_insert_knots(): knotvector = ImmutableKnotVector([0, 0, 1, 2, 2]) assert insert_knots(knotvector, [0.5, 1.5]) == (0, 0, 0.5, 1, 1.5, 2, 2) -@pytest.mark.order(3) +@pytest.mark.order(21) @pytest.mark.dependency(depends=["test_begin"]) def test_remove_knots(): knotvector = ImmutableKnotVector([0, 0, 1, 2, 2]) assert remove_knots(knotvector, [1]) == (0, 0, 2, 2) -@pytest.mark.order(3) +@pytest.mark.order(21) @pytest.mark.dependency(depends=["test_begin"]) def test_increase_degree(): knotvector = ImmutableKnotVector([0, 0, 1, 2, 2]) @@ -42,7 +42,7 @@ def test_increase_degree(): assert increase_degree(knotvector, 2) == (0, 0, 0, 0, 1, 1, 1, 2, 2, 2, 2) -@pytest.mark.order(3) +@pytest.mark.order(21) @pytest.mark.dependency(depends=["test_begin"]) def test_decrease_degree(): knotvector = ImmutableKnotVector([0, 0, 0, 0, 1, 1, 2, 2, 2, 2]) @@ -50,7 +50,7 @@ def test_decrease_degree(): assert decrease_degree(knotvector, 2) == (0, 0, 2, 2) -@pytest.mark.order(3) +@pytest.mark.order(21) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/operations/test_least_square.py b/tests/operations/test_least_square.py index 12ae862..3d062ac 100644 --- a/tests/operations/test_least_square.py +++ b/tests/operations/test_least_square.py @@ -4,7 +4,7 @@ from pynurbs.operations.least_square import spline2spline -@pytest.mark.order(1) +@pytest.mark.order(21) @pytest.mark.dependency( depends=["tests/test_custom_math.py::test_end"], scope="session" ) @@ -12,7 +12,7 @@ def test_begin(): pass -@pytest.mark.order(1) +@pytest.mark.order(21) @pytest.mark.dependency(depends=["TestLeastSquare::test_begin"]) def test_leastsquarespline_identity(): U0 = [0, 0, 1, 1] @@ -34,7 +34,7 @@ def test_leastsquarespline_identity(): assert np.all(np.abs(E) < 1e-9) -@pytest.mark.order(1) +@pytest.mark.order(21) @pytest.mark.dependency(depends=["TestLeastSquare::test_begin"]) def test_leastsquarespline_eval_error(): # knot insertion @@ -50,7 +50,7 @@ def test_leastsquarespline_eval_error(): assert np.all(np.abs(E) < 1e-9) -@pytest.mark.order(1) +@pytest.mark.order(21) @pytest.mark.dependency( depends=[ "test_begin", diff --git a/tests/operations/test_roots.py b/tests/operations/test_roots.py index 900d41f..c622fc9 100644 --- a/tests/operations/test_roots.py +++ b/tests/operations/test_roots.py @@ -4,7 +4,7 @@ from pynurbs.operations.roots import division, roots -@pytest.mark.order(3) +@pytest.mark.order(21) @pytest.mark.dependency( depends=[ "tests/core/test_polynomial.py::test_all", @@ -15,7 +15,7 @@ def test_begin(): pass -@pytest.mark.order(3) +@pytest.mark.order(21) @pytest.mark.dependency(depends=["test_begin"]) def test_division(): poly = Polynomial([0, 1]) @@ -63,7 +63,7 @@ def test_division(): assert all(abs(coef) < 1e-9 for coef in diff) -@pytest.mark.order(3) +@pytest.mark.order(21) @pytest.mark.dependency(depends=["test_begin"]) def test_roots(): x = Polynomial([0, 1]) @@ -75,7 +75,7 @@ def test_roots(): assert values == (1, 2, 3) -@pytest.mark.order(3) +@pytest.mark.order(21) @pytest.mark.dependency(depends=["test_division", "test_roots"]) def test_all(): pass From a5499001bb7550c191c75a61f437991f804e0213 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 22:54:36 +0200 Subject: [PATCH 053/116] test: fix test least square to remove Class name --- tests/operations/test_least_square.py | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/tests/operations/test_least_square.py b/tests/operations/test_least_square.py index 3d062ac..5387738 100644 --- a/tests/operations/test_least_square.py +++ b/tests/operations/test_least_square.py @@ -6,14 +6,14 @@ @pytest.mark.order(21) @pytest.mark.dependency( - depends=["tests/test_custom_math.py::test_end"], scope="session" + depends=["tests/core/test_custom_math.py::test_end"], scope="session" ) def test_begin(): pass @pytest.mark.order(21) -@pytest.mark.dependency(depends=["TestLeastSquare::test_begin"]) +@pytest.mark.dependency(depends=["test_begin"]) def test_leastsquarespline_identity(): U0 = [0, 0, 1, 1] U1 = [0, 0, 1, 1] @@ -35,7 +35,7 @@ def test_leastsquarespline_identity(): @pytest.mark.order(21) -@pytest.mark.dependency(depends=["TestLeastSquare::test_begin"]) +@pytest.mark.dependency(depends=["test_begin"]) def test_leastsquarespline_eval_error(): # knot insertion U0 = [0, 0, 0, 1, 1, 1] From 893a24a100a0c583a08374871c716c9224020ab6 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 23:16:33 +0200 Subject: [PATCH 054/116] test: fix depedency of tests --- tests/responsive/test_beziercurve.py | 4 ++-- tests/responsive/test_functions.py | 8 +++++++- tests/responsive/test_knotspace.py | 4 +++- 3 files changed, 12 insertions(+), 4 deletions(-) diff --git a/tests/responsive/test_beziercurve.py b/tests/responsive/test_beziercurve.py index d477277..69de434 100644 --- a/tests/responsive/test_beziercurve.py +++ b/tests/responsive/test_beziercurve.py @@ -13,8 +13,8 @@ @pytest.mark.order(33) @pytest.mark.dependency( depends=[ - "tests/test_knotspace.py::test_end", - "tests/test_functions.py::test_end", + "tests/responsive/test_knotspace.py::test_end", + "tests/responsive/test_functions.py::test_end", ], scope="session", ) diff --git a/tests/responsive/test_functions.py b/tests/responsive/test_functions.py index 17237cb..dd431cf 100644 --- a/tests/responsive/test_functions.py +++ b/tests/responsive/test_functions.py @@ -9,7 +9,13 @@ @pytest.mark.order(32) -@pytest.mark.dependency(depends=["tests/test_knotspace.py::test_end"], scope="session") +@pytest.mark.dependency( + depends=[ + "tests/core/test_basis_function.py::test_all", + "tests/responsive/test_knotspace.py::test_end", + ], + scope="session", +) def test_begin(): pass diff --git a/tests/responsive/test_knotspace.py b/tests/responsive/test_knotspace.py index 8adff36..98c4efb 100644 --- a/tests/responsive/test_knotspace.py +++ b/tests/responsive/test_knotspace.py @@ -8,7 +8,9 @@ @pytest.mark.order(30) -@pytest.mark.dependency(depends=["tests/test_heavy.py::test_end"], scope="session") +@pytest.mark.dependency( + depends=["tests/core/test_knotvector.py::test_end"], scope="session" +) def test_begin(): pass From 51b260fb81010a1b82945ab074ce150ff677e628 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 23:16:52 +0200 Subject: [PATCH 055/116] fix: import of least_square functions on curves.py --- src/pynurbs/responsive/curves.py | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/pynurbs/responsive/curves.py b/src/pynurbs/responsive/curves.py index 17588a3..6ce039a 100644 --- a/src/pynurbs/responsive/curves.py +++ b/src/pynurbs/responsive/curves.py @@ -14,6 +14,7 @@ insert_knots, remove_knots, ) +from ..operations.least_square import func2func, spline2spline from .knotspace import KnotVector @@ -992,12 +993,12 @@ def fit_curve(self, other: Curve, nodes: Tuple[float] = None) -> float: assert isinstance(other, self.__class__) vectora, vectorb = tuple(self.knotvector), tuple(other.knotvector) if self.weights is None and other.weights is None: - lstsq = heavy.LeastSquare.spline2spline + lstsq = spline2spline transmat, materror = lstsq(vectorb, vectora, nodes) else: weightsa = self.weights if self.weights else [1] * self.npts weightsb = other.weights if other.weights else [1] * other.npts - lstsq = heavy.LeastSquare.func2func + lstsq = func2func transmat, materror = lstsq(vectorb, weightsb, vectora, weightsa, nodes) transmat = np.array(transmat) ctrlpoints = np.dot(transmat, other.ctrlpoints) From 701fd28f650122d78cdf2870f62acf6d104c1336 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 23:22:04 +0200 Subject: [PATCH 056/116] refactor: move eval_spline_nodes and eval_rational_nodes to least_square --- src/pynurbs/operations/heavy.py | 36 +++----------------------- src/pynurbs/operations/least_square.py | 28 +++++++++++++++++++- 2 files changed, 31 insertions(+), 33 deletions(-) diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index 663ae57..66919d6 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -6,13 +6,11 @@ from __future__ import annotations -from fractions import Fraction -from typing import Tuple, Union +from typing import Tuple import numpy as np -from ..core.basisfunction import ImmutableBasisFunction -from ..core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple +from ..core.custom_math import Linalg, NodeSample, totuple from ..core.knotvector import ImmutableKnotVector from ..operations.knotvector import ( increase_degree, @@ -21,6 +19,7 @@ split_knotvector, union_knotvectors, ) +from .least_square import eval_spline_nodes, spline2spline def find_roots( @@ -106,33 +105,6 @@ def find_roots( return tuple(sorted(filtered_roots)) -def eval_spline_nodes( - knotvector: ImmutableKnotVector, nodes: Tuple[float], degree: int -) -> Tuple[Tuple[float]]: - """ - Returns a matrix M of which M_{ij} = N_{i,degree}(node_j) - M.shape = (npts, len(nodes)) - """ - knotvector = ImmutableKnotVector(knotvector) - basis = ImmutableBasisFunction(knotvector, degree) - return np.transpose(tuple(map(basis, nodes))) - - -def eval_rational_nodes( - knotvector: ImmutableKnotVector, - weights: Tuple[float], - nodes: Tuple[float], - degree: int, -) -> Tuple[Tuple[float]]: - """ - Returns a matrix M of which M_{ij} = N_{i,p}(node_j) - M.shape = (len(weights), len(nodes)) - """ - matrix = eval_spline_nodes(knotvector, nodes, degree) - denominators = 1 / np.dot(weights, matrix) - return np.einsum("j,ij,i->ij", denominators, matrix, weights) - - class Operations: """ Contains algorithms to @@ -295,7 +267,7 @@ def knot_remove(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix msg = f"Invalid nodes {nodes} in knotvector {knotvector}" raise ValueError(msg) newknotvector = remove_knots(knotvector, nodes) - matrix, _ = LeastSquare.spline2spline(knotvector, newknotvector) + matrix, _ = spline2spline(knotvector, newknotvector) return totuple(matrix) def degree_increase_bezier_once(knotvector: ImmutableKnotVector) -> "Matrix2D": diff --git a/src/pynurbs/operations/least_square.py b/src/pynurbs/operations/least_square.py index c1556e6..0bdcf78 100644 --- a/src/pynurbs/operations/least_square.py +++ b/src/pynurbs/operations/least_square.py @@ -31,7 +31,33 @@ from ..core.basisfunction import ImmutableBasisFunction from ..core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple from ..core.knotvector import ImmutableKnotVector -from .heavy import eval_rational_nodes + + +def eval_spline_nodes( + knotvector: ImmutableKnotVector, nodes: Tuple[float], degree: int +) -> Tuple[Tuple[float]]: + """ + Returns a matrix M of which M_{ij} = N_{i,degree}(node_j) + M.shape = (npts, len(nodes)) + """ + knotvector = ImmutableKnotVector(knotvector) + basis = ImmutableBasisFunction(knotvector, degree) + return np.transpose(tuple(map(basis, nodes))) + + +def eval_rational_nodes( + knotvector: ImmutableKnotVector, + weights: Tuple[float], + nodes: Tuple[float], + degree: int, +) -> Tuple[Tuple[float]]: + """ + Returns a matrix M of which M_{ij} = N_{i,p}(node_j) + M.shape = (len(weights), len(nodes)) + """ + matrix = eval_spline_nodes(knotvector, nodes, degree) + denominators = 1 / np.dot(weights, matrix) + return np.einsum("j,ij,i->ij", denominators, matrix, weights) def fit_function( From 71a28f2a189c9bb2799bcd534691edc4a10df22d Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 23:25:45 +0200 Subject: [PATCH 057/116] fix: relative import of functions --- src/pynurbs/responsive/curves.py | 12 +++++------- 1 file changed, 5 insertions(+), 7 deletions(-) diff --git a/src/pynurbs/responsive/curves.py b/src/pynurbs/responsive/curves.py index 6ce039a..57185dd 100644 --- a/src/pynurbs/responsive/curves.py +++ b/src/pynurbs/responsive/curves.py @@ -7,6 +7,7 @@ import numpy as np from ..core.basisfunction import ImmutableBasisFunction +from ..core.custom_math import number_type from ..operations import heavy from ..operations.knotvector import ( decrease_degree, @@ -14,7 +15,7 @@ insert_knots, remove_knots, ) -from ..operations.least_square import func2func, spline2spline +from ..operations.least_square import fit_function, func2func, spline2spline from .knotspace import KnotVector @@ -902,9 +903,7 @@ def clean(self, tolerance: float = 1e-9): knotvector = tuple(self.knotvector) weights = tuple(self.weights) ctrlpoints = tuple(self.ctrlpoints) - mattrans, materror = heavy.LeastSquare.func2func( - knotvector, weights, knotvector, [1] * self.npts - ) + mattrans, materror = func2func(knotvector, weights, knotvector, [1] * self.npts) error = np.dot(np.moveaxis(ctrlpoints, 0, -1), np.dot(materror, ctrlpoints)) error = np.max(abs(error)) error = max(error, np.dot(weights, np.dot(materror, weights))) @@ -1054,7 +1053,7 @@ def fit_function(self, function: Callable, nodes: Tuple[float] = None) -> None: knots = self.knotvector.knots npts_each = 1 + int(np.ceil(self.degree * self.npts / (len(knots) - 1))) nodes = [] - numbtype = heavy.number_type(knots) + numbtype = number_type(knots) if numbtype in (float, np.floating): funcnodes = heavy.NodeSample.chebyshev else: @@ -1102,7 +1101,6 @@ def fit_points(self, points: Tuple[Any], nodes: Tuple[float] = None) -> None: """ assert len(points) >= self.npts - fitfunc = heavy.LeastSquare.fit_function if nodes is None: umin, umax = self.knotvector.limits if isinstance(umin, (int, Fraction)): @@ -1114,7 +1112,7 @@ def fit_points(self, points: Tuple[Any], nodes: Tuple[float] = None) -> None: knotvector = tuple(self.knotvector) nodes = tuple(nodes) weights = None if self.weights is None else tuple(self.weights) - matrix = fitfunc(knotvector, nodes, weights) + matrix = fit_function(knotvector, nodes, weights) ctrlpoints = np.dot(matrix, points) self.ctrlpoints = tuple(ctrlpoints) From 9e307c97f978dcc14dcb760313185dcb6a9e8326 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 1 Jul 2025 23:26:00 +0200 Subject: [PATCH 058/116] tests: fix dependency on tests --- tests/operations/test_calculus.py | 10 +++++----- tests/operations/test_customstruc.py | 10 +++++----- tests/operations/test_fitting.py | 10 +++++----- tests/responsive/test_rationalcurve.py | 8 ++++---- tests/responsive/test_splinecurve.py | 6 +++--- 5 files changed, 22 insertions(+), 22 deletions(-) diff --git a/tests/operations/test_calculus.py b/tests/operations/test_calculus.py index ecd8cc9..7aa4e73 100644 --- a/tests/operations/test_calculus.py +++ b/tests/operations/test_calculus.py @@ -16,11 +16,11 @@ @pytest.mark.order(41) @pytest.mark.dependency( depends=[ - "tests/test_knotspace.py::test_end", - "tests/test_functions.py::test_end", - "tests/test_beziercurve.py::test_end", - "tests/test_splinecurve.py::test_end", - "tests/test_rationalcurve.py::test_end", + "tests/responsive/test_knotspace.py::test_end", + "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_beziercurve.py::test_end", + "tests/responsive/test_splinecurve.py::test_end", + "tests/responsive/test_rationalcurve.py::test_end", ], scope="session", ) diff --git a/tests/operations/test_customstruc.py b/tests/operations/test_customstruc.py index 544f1a6..8d31c8a 100644 --- a/tests/operations/test_customstruc.py +++ b/tests/operations/test_customstruc.py @@ -112,11 +112,11 @@ def __rmul__(self, other: CustomFloat): @pytest.mark.order(41) @pytest.mark.dependency( depends=[ - "tests/test_knotspace.py::test_end", - "tests/test_functions.py::test_end", - "tests/test_beziercurve.py::test_end", - "tests/test_splinecurve.py::test_end", - "tests/test_rationalcurve.py::test_end", + "tests/responsive/test_knotspace.py::test_end", + "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_beziercurve.py::test_end", + "tests/responsive/test_splinecurve.py::test_end", + "tests/responsive/test_rationalcurve.py::test_end", ], scope="session", ) diff --git a/tests/operations/test_fitting.py b/tests/operations/test_fitting.py index 02e9620..685e39f 100644 --- a/tests/operations/test_fitting.py +++ b/tests/operations/test_fitting.py @@ -8,11 +8,11 @@ @pytest.mark.order(41) @pytest.mark.dependency( depends=[ - "tests/test_knotspace.py::test_end", - "tests/test_functions.py::test_end", - "tests/test_beziercurve.py::test_end", - "tests/test_splinecurve.py::test_end", - "tests/test_rationalcurve.py::test_end", + "tests/responsive/test_knotspace.py::test_end", + "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_beziercurve.py::test_end", + "tests/responsive/test_splinecurve.py::test_end", + "tests/responsive/test_rationalcurve.py::test_end", ], scope="session", ) diff --git a/tests/responsive/test_rationalcurve.py b/tests/responsive/test_rationalcurve.py index f8e6dd2..216ab7a 100644 --- a/tests/responsive/test_rationalcurve.py +++ b/tests/responsive/test_rationalcurve.py @@ -12,10 +12,10 @@ @pytest.mark.order(35) @pytest.mark.dependency( depends=[ - "tests/test_knotspace.py::test_end", - "tests/test_functions.py::test_end", - "tests/test_beziercurve.py::test_end", - "tests/test_splinecurve.py::test_end", + "tests/responsive/test_knotspace.py::test_end", + "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_beziercurve.py::test_end", + "tests/responsive/test_splinecurve.py::test_end", ], scope="session", ) diff --git a/tests/responsive/test_splinecurve.py b/tests/responsive/test_splinecurve.py index f279bea..b0ba833 100644 --- a/tests/responsive/test_splinecurve.py +++ b/tests/responsive/test_splinecurve.py @@ -10,9 +10,9 @@ @pytest.mark.order(34) @pytest.mark.dependency( depends=[ - "tests/test_knotspace.py::test_end", - "tests/test_functions.py::test_end", - "tests/test_beziercurve.py::test_end", + "tests/responsive/test_knotspace.py::test_end", + "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_beziercurve.py::test_end", ], scope="session", ) From 4c4d8f1e445856e3f6153f1cb76e59d9bf1e39c4 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 19:35:09 +0200 Subject: [PATCH 059/116] refactor: rename basis functions to spline basis --- src/pynurbs/core/__init__.py | 2 +- src/pynurbs/core/manifold.py | 6 +-- .../{basisfunction.py => spline_basis.py} | 2 +- src/pynurbs/operations/least_square.py | 6 +-- src/pynurbs/responsive/curves.py | 4 +- src/pynurbs/responsive/functions.py | 4 +- ...basis_function.py => test_spline_basis.py} | 40 +++++++++---------- tests/responsive/test_functions.py | 2 +- 8 files changed, 33 insertions(+), 33 deletions(-) rename src/pynurbs/core/{basisfunction.py => spline_basis.py} (99%) rename tests/core/{test_basis_function.py => test_spline_basis.py} (89%) diff --git a/src/pynurbs/core/__init__.py b/src/pynurbs/core/__init__.py index 55f36c1..5f638b1 100644 --- a/src/pynurbs/core/__init__.py +++ b/src/pynurbs/core/__init__.py @@ -1,3 +1,3 @@ -from .basisfunction import ImmutableBasisFunction from .knotvector import ImmutableKnotVector from .polynomial import Polynomial +from .spline_basis import ImmutableSplineBasis diff --git a/src/pynurbs/core/manifold.py b/src/pynurbs/core/manifold.py index 44d2c7e..81592ca 100644 --- a/src/pynurbs/core/manifold.py +++ b/src/pynurbs/core/manifold.py @@ -3,7 +3,7 @@ import numpy as np -from .basisfunction import ImmutableBasisFunction +from .spline_basis import ImmutableSplineBasis def permutations(numbers: Tuple[int, ...]) -> Iterable[Tuple[int, ...]]: @@ -74,11 +74,11 @@ class ImmuntableManifold: """ def __init__( - self, allbasis: Iterable[ImmutableBasisFunction], ctrlpoints: Container[Any] + self, allbasis: Iterable[ImmutableSplineBasis], ctrlpoints: Container[Any] ): allbasis = tuple(allbasis) - if not all(isinstance(fun, ImmutableBasisFunction) for fun in allbasis): + if not all(isinstance(fun, ImmutableSplineBasis) for fun in allbasis): raise TypeError if isinstance(ctrlpoints, Container): if ctrlpoints.ndim != len(allbasis): diff --git a/src/pynurbs/core/basisfunction.py b/src/pynurbs/core/spline_basis.py similarity index 99% rename from src/pynurbs/core/basisfunction.py rename to src/pynurbs/core/spline_basis.py index 986ac42..224f4f0 100644 --- a/src/pynurbs/core/basisfunction.py +++ b/src/pynurbs/core/spline_basis.py @@ -62,7 +62,7 @@ def spectral_matrix( return totuple(matrix) -class ImmutableBasisFunction: +class ImmutableSplineBasis: def __init__( self, knotvector: ImmutableKnotVector, degree: Union[int, None] = None diff --git a/src/pynurbs/operations/least_square.py b/src/pynurbs/operations/least_square.py index 0bdcf78..872bc91 100644 --- a/src/pynurbs/operations/least_square.py +++ b/src/pynurbs/operations/least_square.py @@ -28,9 +28,9 @@ import numpy as np -from ..core.basisfunction import ImmutableBasisFunction from ..core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple from ..core.knotvector import ImmutableKnotVector +from ..core.spline_basis import ImmutableSplineBasis def eval_spline_nodes( @@ -41,7 +41,7 @@ def eval_spline_nodes( M.shape = (npts, len(nodes)) """ knotvector = ImmutableKnotVector(knotvector) - basis = ImmutableBasisFunction(knotvector, degree) + basis = ImmutableSplineBasis(knotvector, degree) return np.transpose(tuple(map(basis, nodes))) @@ -77,7 +77,7 @@ def fit_function( [P] = [M] * [f(nodes)] """ knotvector = ImmutableKnotVector(knotvector) - basis = ImmutableBasisFunction(knotvector) + basis = ImmutableSplineBasis(knotvector) matrix = np.transpose(tuple(map(basis, nodes))) if weights is not None: denominators = 1 / np.dot(weights, matrix) diff --git a/src/pynurbs/responsive/curves.py b/src/pynurbs/responsive/curves.py index 57185dd..35ec3dc 100644 --- a/src/pynurbs/responsive/curves.py +++ b/src/pynurbs/responsive/curves.py @@ -6,8 +6,8 @@ import numpy as np -from ..core.basisfunction import ImmutableBasisFunction from ..core.custom_math import number_type +from ..core.spline_basis import ImmutableSplineBasis from ..operations import heavy from ..operations.knotvector import ( decrease_degree, @@ -606,7 +606,7 @@ def __eval(self, nodes: Tuple[float]) -> Tuple[Any]: """ vector = self.knotvector.internal nodes = tuple(nodes) - basis = ImmutableBasisFunction(vector) + basis = ImmutableSplineBasis(vector) matrix = np.transpose(tuple(map(basis, nodes))) if self.weights is not None: denominators = 1 / np.dot(self.weights, matrix) diff --git a/src/pynurbs/responsive/functions.py b/src/pynurbs/responsive/functions.py index 2ec6b2f..372f3c1 100644 --- a/src/pynurbs/responsive/functions.py +++ b/src/pynurbs/responsive/functions.py @@ -5,7 +5,7 @@ import numpy as np -from pynurbs.core.basisfunction import ImmutableBasisFunction +from pynurbs.core.spline_basis import ImmutableSplineBasis from ..operations.tools import vectorize from .knotspace import KnotVector @@ -175,7 +175,7 @@ def __init__(self, func: BaseFunction, i: Union[int, slice], j: int): vector = func.knotvector self.__weights = func.weights self.__first_index = i - self.__basis = ImmutableBasisFunction(vector.internal, j) + self.__basis = ImmutableSplineBasis(vector.internal, j) @vectorize(1, 0) def __call__(self, node: float) -> Union[float, Tuple[float]]: diff --git a/tests/core/test_basis_function.py b/tests/core/test_spline_basis.py similarity index 89% rename from tests/core/test_basis_function.py rename to tests/core/test_spline_basis.py index cf0b960..c915d0c 100644 --- a/tests/core/test_basis_function.py +++ b/tests/core/test_spline_basis.py @@ -3,8 +3,8 @@ import numpy as np import pytest -from pynurbs.core.basisfunction import ImmutableBasisFunction from pynurbs.core.knotvector import ImmutableKnotVector +from pynurbs.core.spline_basis import ImmutableSplineBasis def binom(n: int, i: int): @@ -44,19 +44,19 @@ def test_begin(self): @pytest.mark.dependency(depends=["TestBezier::test_begin"]) def test_creation(self): knotvector = ImmutableKnotVector([0, 0, 1, 1]) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert callable(bezier) assert bezier.degree == 1 assert bezier.npts == 2 knotvector = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert callable(bezier) assert bezier.degree == 2 assert bezier.npts == 3 knotvector = ImmutableKnotVector([0, 0, 0, 0, 1, 1, 1, 1]) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert callable(bezier) assert bezier.degree == 3 assert bezier.npts == 4 @@ -64,7 +64,7 @@ def test_creation(self): for degree in range(0, 8): npts = degree + 1 knotvector = ImmutableKnotVector([0] * npts + [1] * npts) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert callable(bezier) assert bezier.degree == degree assert bezier.npts == npts @@ -74,7 +74,7 @@ def test_creation(self): @pytest.mark.dependency(depends=["TestBezier::test_creation"]) def test_sum_equal_to_1(self): knotvector = ImmutableKnotVector([0, 0, 1, 1]) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert sum(bezier(0.00)) == 1 assert sum(bezier(0.25)) == 1 assert sum(bezier(0.50)) == 1 @@ -82,7 +82,7 @@ def test_sum_equal_to_1(self): assert sum(bezier(1.00)) == 1 knotvector = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert sum(bezier(0.00)) == 1 assert sum(bezier(0.25)) == 1 assert sum(bezier(0.50)) == 1 @@ -90,7 +90,7 @@ def test_sum_equal_to_1(self): assert sum(bezier(1.00)) == 1 knotvector = ImmutableKnotVector([0, 0, 0, 0, 1, 1, 1, 1]) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert sum(bezier(0.00)) == 1 assert sum(bezier(0.25)) == 1 assert sum(bezier(0.50)) == 1 @@ -101,7 +101,7 @@ def test_sum_equal_to_1(self): for degree in range(0, 6): npts = degree + 1 knotvector = ImmutableKnotVector([0] * npts + [1] * npts) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert bezier.degree == degree assert bezier.npts == npts @@ -122,21 +122,21 @@ def test_sum_equal_to_1(self): def test_single_values(self): # degree = 1, npts = 2 knotvector = ImmutableKnotVector([0, 0, 1, 1]) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert bezier(0) == (1, 0) assert bezier(0.5) == (0.5, 0.5) assert bezier(1) == (0, 1) # degree = 2, npts = 3 knotvector = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert bezier(0) == (1, 0, 0) assert bezier(0.5) == (0.25, 0.5, 0.25) assert bezier(1) == (0, 0, 1) # degree = 3, npts = 3 knotvector = ImmutableKnotVector([0, 0, 0, 0, 1, 1, 1, 1]) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) assert bezier(0) == (1, 0, 0, 0) assert bezier(0.25) == (27 / 64, 27 / 64, 9 / 64, 1 / 64) assert bezier(0.5) == (1 / 8, 3 / 8, 3 / 8, 1 / 8) @@ -148,7 +148,7 @@ def test_single_values(self): npts = degree + 1 knotvector = [Fraction(0)] * npts + [Fraction(1)] * npts knotvector = ImmutableKnotVector(knotvector) - bezier = ImmutableBasisFunction(knotvector) + bezier = ImmutableSplineBasis(knotvector) for j in range(divisions + 1): node = Fraction(j, divisions) minu = 1 - node @@ -182,25 +182,25 @@ def test_begin(self): @pytest.mark.dependency(depends=["TestSpline::test_begin"]) def test_creation(self): knotvector = ImmutableKnotVector([0, 0, 1, 1]) - spline = ImmutableBasisFunction(knotvector) + spline = ImmutableSplineBasis(knotvector) assert callable(spline) assert spline.degree == 1 assert spline.npts == 2 knotvector = ImmutableKnotVector([0, 0, 0.5, 1, 1]) - spline = ImmutableBasisFunction(knotvector) + spline = ImmutableSplineBasis(knotvector) assert callable(spline) assert spline.degree == 1 assert spline.npts == 3 knotvector = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) - spline = ImmutableBasisFunction(knotvector) + spline = ImmutableSplineBasis(knotvector) assert callable(spline) assert spline.degree == 2 assert spline.npts == 3 knotvector = ImmutableKnotVector([0, 0, 0, 0.5, 1, 1, 1]) - spline = ImmutableBasisFunction(knotvector) + spline = ImmutableSplineBasis(knotvector) assert callable(spline) assert spline.degree == 2 assert spline.npts == 4 @@ -210,7 +210,7 @@ def test_creation(self): @pytest.mark.dependency(depends=["TestSpline::test_creation"]) def test_tablevalues_degree1npts3(self): knotvector = ImmutableKnotVector([0, 0, 0.5, 1, 1]) - spline = ImmutableBasisFunction(knotvector) + spline = ImmutableSplineBasis(knotvector) assert spline.degree == 1 assert spline.npts == 3 @@ -238,7 +238,7 @@ def test_tablevalues_degree1npts3(self): def test_tablevalues_degree2npts4(self): knotvector = [0, 0, 0, 0.5, 1, 1, 1] knotvector = ImmutableKnotVector(knotvector) - spline = ImmutableBasisFunction(knotvector) + spline = ImmutableSplineBasis(knotvector) assert spline.degree == 2 assert spline.npts == 4 nodes_test = np.linspace(0, 1, 11) @@ -266,7 +266,7 @@ def test_tablevalues_degree2npts4(self): def test_tablevalues_degree3npts5(self): knotvector = [0, 0, 0, 0, 0.5, 1, 1, 1, 1] knotvector = ImmutableKnotVector(knotvector) - spline = ImmutableBasisFunction(knotvector) + spline = ImmutableSplineBasis(knotvector) assert spline.degree == 3 assert spline.npts == 5 nodes_test = np.linspace(0, 1, 11) diff --git a/tests/responsive/test_functions.py b/tests/responsive/test_functions.py index dd431cf..2b3ef98 100644 --- a/tests/responsive/test_functions.py +++ b/tests/responsive/test_functions.py @@ -11,7 +11,7 @@ @pytest.mark.order(32) @pytest.mark.dependency( depends=[ - "tests/core/test_basis_function.py::test_all", + "tests/core/test_spline_basis.py::test_all", "tests/responsive/test_knotspace.py::test_end", ], scope="session", From 874a419c355603274f6dbe65418f95f6010dd21a Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 20:28:04 +0200 Subject: [PATCH 060/116] refactor: make ImmutableKnotVector not inherit from tuple --- src/pynurbs/core/knotvector.py | 171 ++++++++++++------------------- src/pynurbs/core/spline_basis.py | 4 +- tests/core/test_knotvector.py | 10 +- 3 files changed, 69 insertions(+), 116 deletions(-) diff --git a/src/pynurbs/core/knotvector.py b/src/pynurbs/core/knotvector.py index 453b779..a7a4b4d 100644 --- a/src/pynurbs/core/knotvector.py +++ b/src/pynurbs/core/knotvector.py @@ -1,76 +1,47 @@ from __future__ import annotations -from typing import Optional, Tuple, Union +from collections import Counter +from numbers import Real +from typing import Iterable, Tuple, Union -import numpy as np +def find_degree(vector: Tuple[Real, ...]) -> int: + return max(Counter(vector).values()) - 1 -class ImmutableKnotVector(tuple): - @staticmethod - def __get_unique(vector: Tuple[float]): - unique = [] - for node in vector: - for knot in unique: - if abs(node - knot) < 1e-6: - break - else: - unique.append(node) - unique.sort() - return tuple(unique) - @staticmethod - def __is_valid(vector: Tuple[float], degree: Union[int, None]): - try: - for knot in vector: - float(knot) - except TypeError: - return False - lenght = len(vector) - if lenght < 2: - return False - for i in range(lenght - 1): - if not vector[i] <= vector[i + 1]: - return False - if degree is None: - degree = 0 - while vector[degree] == vector[degree + 1]: - degree += 1 - npts = lenght - degree - 1 - if not degree < npts: - return False - knots = ImmutableKnotVector.__get_unique(vector[degree : npts + 1]) - for knot in knots: - mult = vector.count(knot) - if mult > degree + 1: - return False - if vector.count(vector[degree]) != vector.count(vector[npts]): - return False - return True - - def __new__(cls, knotvector: Tuple[float], degree: Optional[int] = None): - if isinstance(knotvector, ImmutableKnotVector): - return knotvector - try: - knotvector = tuple(knotvector) - except TypeError: - raise ValueError - if not cls.__is_valid(knotvector, degree): - msg = f"Invalid knot vector (deg {degree}): {knotvector}" - raise ValueError(msg) - if degree is None: - degree = 0 - while knotvector[degree] == knotvector[degree + 1]: - degree += 1 - instance = super(ImmutableKnotVector, cls).__new__(cls, tuple(knotvector)) - instance._ImmutableKnotVector__degree = degree - instance._ImmutableKnotVector__npts = len(knotvector) - degree - 1 - return instance +def is_sorted(vector: Tuple[Real, ...]) -> bool: + return all(val <= vector[i + 1] for i, val in enumerate(vector[:-1])) - def __add__(self, other): - raise ValueError - def __sub__(self, other): - raise ValueError +class ImmutableKnotVector: + + def __init__(self, vector: Iterable[Real], degree: Union[None, int] = None): + try: + vector = tuple(vector) + except Exception: + raise ValueError(f"Wrong argument: '{vector}'") + if not all(isinstance(val, Real) for val in vector): + raise ValueError(f"Cannot create KnotVector with {vector}") + if not is_sorted(vector): + raise ValueError(f"Cannot create KnotVector with {vector}") + if degree is None: + degree = find_degree(vector) + elif int(degree) < find_degree(vector): + raise ValueError(f"Cannot create KnotVector with {vector}") + npts = len(vector) - degree - 1 + if degree >= npts: + raise ValueError(f"Cannot have {degree} <= {npts}") + knots = tuple(sorted(set(vector[degree : npts + 1]))) + if len(knots) < 2: + raise ValueError(f"Cannot create KnotVector with {vector}") + if vector[degree] == vector[degree + 1]: + raise ValueError(f"Cannot create KnotVector with {vector}") + if degree != 0 and vector[npts - 1] == vector[npts]: + raise ValueError(f"Cannot create KnotVector with {vector}") + self.__degree = degree + self.__npts = npts + self.__knots = knots + self.__vector = vector @property def degree(self) -> int: @@ -81,15 +52,32 @@ def npts(self) -> int: return self.__npts @property - def knots(self) -> Tuple[float]: - vector = self[self.degree : self.npts + 1] - return ImmutableKnotVector.__get_unique(vector) + def knots(self) -> Tuple[Real, ...]: + return self.__knots @property - def limits(self) -> Tuple[float]: + def limits(self) -> Tuple[Real, Real]: return (self[self.degree], self[self.npts]) - def __span_single(self, node: float) -> int: + def __getitem__(self, index): + return self.__vector[index] + + def __len__(self) -> int: + return len(self.__vector) + + def __eq__(self, other: object) -> bool: + if isinstance(other, ImmutableKnotVector): + return self.degree == other.degree and tuple(self) == tuple(other) + try: + return tuple(self) == tuple(other) + except Exception: + return NotImplemented + + def span(self, node: Real) -> int: + if not isinstance(node, Real): + raise ValueError(f"Node '{node}' must be Real instance") + if node < self[self.degree] or self[self.npts] < node: + raise ValueError(f"Node {node} outside [{self.knots[0], self.knots[-1]}]") if node == self[self.npts]: # Special case return self.npts - 1 low, high = self.degree, self.npts + 1 # Do binary search @@ -103,42 +91,9 @@ def __span_single(self, node: float) -> int: if self[mid] <= node < self[mid + 1]: return mid - def __mult_single(self, node: Tuple[float]) -> Tuple[int]: + def mult(self, node: Real) -> int: + if not isinstance(node, Real): + raise ValueError(f"Node '{node}' must be Real instance") + if node < self[self.degree] or self[self.npts] < node: + raise ValueError(f"Node {node} outside [{self.knots[0], self.knots[-1]}]") return sum(abs(node - knot) < 1e-9 for knot in self) - - def __valid_single(self, node: float) -> bool: - try: - float(node) # Verify if it's a number - except TypeError: - return False - umin, umax = self.limits - if node < umin or umax < node: - return False - return True - - def span(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: - if not self.valid(nodes): - raise ValueError - try: - return tuple(map(self.span, nodes)) - except TypeError: - return self.__span_single(nodes) - - def mult(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: - if not self.valid(nodes): - raise ValueError - try: - return tuple(map(self.mult, nodes)) - except TypeError: - return self.__mult_single(nodes) - - def valid(self, nodes: Tuple[float]) -> bool: - if isinstance(nodes, str): - return False - try: - for node in nodes: - if not self.valid(node): - return False - return True - except TypeError: - return self.__valid_single(nodes) diff --git a/src/pynurbs/core/spline_basis.py b/src/pynurbs/core/spline_basis.py index 224f4f0..86b6d86 100644 --- a/src/pynurbs/core/spline_basis.py +++ b/src/pynurbs/core/spline_basis.py @@ -32,7 +32,7 @@ def spectral_matrix( msg = f"reqdegree must be in [0, {knotvector.degree}]" raise ValueError(msg) knots = knotvector.knots - spans = knotvector.span(knots) + spans = tuple(map(knotvector.span, knots)) j = reqdegree ninter = len(knots) - 1 @@ -97,7 +97,7 @@ def __getitem__(self, index: int) -> PiecewisePolynomial: def __call__(self, node: Real) -> Tuple[Real, ...]: npts = self.__knotvector.npts knots = self.__knotvector.knots - spans = self.__knotvector.span(knots) + spans = tuple(map(self.__knotvector.span, knots)) degree = self.__degree result = [0] * npts diff --git a/tests/core/test_knotvector.py b/tests/core/test_knotvector.py index 8fa1715..4d7fe1e 100644 --- a/tests/core/test_knotvector.py +++ b/tests/core/test_knotvector.py @@ -186,11 +186,10 @@ def test_findmult_single(): def test_findspans_array(): U = ImmutableKnotVector([0, 0, 0.2, 0.4, 0.5, 0.6, 0.8, 1, 1]) array = np.linspace(0, 1, 11) # (0, 0.1, 0.2, ..., 0.9, 1.0) - suposedspans = U.span(array) - correctspans = [1, 1, 2, 2, 3, 4, 5, 5, 6, 6, 6] + correctspans = (1, 1, 2, 2, 3, 4, 5, 5, 6, 6, 6) assert U.degree == 1 assert U.npts == 7 - np.testing.assert_equal(suposedspans, correctspans) + assert tuple(map(U.span, array)) == correctspans @pytest.mark.order(12) @@ -199,11 +198,10 @@ def test_findspans_array(): def test_findmult_array(): U = ImmutableKnotVector([0, 0, 0.2, 0.4, 0.5, 0.6, 0.8, 1, 1]) array = np.linspace(0, 1, 11) # (0, 0.1, 0.2, ..., 0.9, 1.0) - suposedmults = U.mult(array) - correctmults = [2, 0, 1, 0, 1, 1, 1, 0, 1, 0, 2] + correctmults = (2, 0, 1, 0, 1, 1, 1, 0, 1, 0, 2) assert U.degree == 1 assert U.npts == 7 - np.testing.assert_equal(suposedmults, correctmults) + assert tuple(map(U.mult, array)) == correctmults @pytest.mark.order(12) From abe358dc6e79a5db5ab73ee31f56676e28fa8c41 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 20:56:48 +0200 Subject: [PATCH 061/116] refactor: KnotVector due to recent changes --- src/pynurbs/responsive/knotspace.py | 90 +++++++++++++++-------------- tests/responsive/test_knotspace.py | 15 +++-- 2 files changed, 53 insertions(+), 52 deletions(-) diff --git a/src/pynurbs/responsive/knotspace.py b/src/pynurbs/responsive/knotspace.py index ef98650..bc42325 100644 --- a/src/pynurbs/responsive/knotspace.py +++ b/src/pynurbs/responsive/knotspace.py @@ -8,7 +8,8 @@ from __future__ import annotations from copy import deepcopy -from typing import Optional, Tuple, Union +from numbers import Real +from typing import Iterable, Optional, Tuple, Union import numpy as np @@ -22,6 +23,7 @@ split_knotvector, union_knotvectors, ) +from ..operations.tools import vectorize class KnotVector: @@ -39,25 +41,22 @@ class KnotVector: """ - def __new__(cls, vector: Tuple[float], degree: Optional[int] = None): - if isinstance(vector, cls): - return vector - instance = super(KnotVector, cls).__new__(cls) - if not isinstance(vector, ImmutableKnotVector): - vector = ImmutableKnotVector(vector, degree) - instance.internal = vector - return instance + def __init__(self, vector: Iterable[Real], degree: Optional[int] = None): + if isinstance(vector, KnotVector): + self.__internal = vector.internal + elif isinstance(vector, ImmutableKnotVector): + self.__internal = vector + else: + self.__internal = ImmutableKnotVector(vector, degree) def __str__(self) -> str: return "(" + ", ".join(map(str, self)) + ")" def __repr__(self) -> str: - return str(self) - return f"KV({self.degree}, {self.npts})" + return str(self.internal) def __iter__(self): - for item in self.internal: - yield item + yield from self.internal def __getitem__(self, index: int): return self.internal[index] @@ -71,7 +70,7 @@ def __iadd__(self, other: float) -> KnotVector: except TypeError: return self.insert(other) - def __isub__(self, other: Union[float, Tuple[float]]): + def __isub__(self, other: Union[float, Tuple[Real, ...]]): try: return self.shift(-other) except TypeError: @@ -91,26 +90,26 @@ def __iand__(self, other: KnotVector) -> KnotVector: self.internal = intersect_knotvectors([self.internal, other.internal]) return self - def __add__(self, other: Union[float, Tuple[float]]): + def __add__(self, other: Union[float, Tuple[Real, ...]]): """Shifts all the knots by same given amout""" return deepcopy(self).__iadd__(other) - def __sub__(self, other: Union[float, Tuple[float]]): + def __sub__(self, other: Union[Real, Tuple[Real, ...]]): return deepcopy(self).__isub__(other) - def __mul__(self, other: float): + def __mul__(self, other: Real): return deepcopy(self).__imul__(other) - def __rmul__(self, other: float): + def __rmul__(self, other: Real): return deepcopy(self).__imul__(other) - def __truediv__(self, other: float): + def __truediv__(self, other: Real): return deepcopy(self).__itruediv__(other) - def __or__(self, other: float): + def __or__(self, other: Real): return deepcopy(self).__ior__(other) - def __and__(self, other: float): + def __and__(self, other: Real): return deepcopy(self).__iand__(other) def __eq__(self, other: object): @@ -197,11 +196,11 @@ def npts(self) -> int: return self.internal.npts @property - def knots(self) -> Tuple[float]: + def knots(self) -> Tuple[Real, ...]: """Non-repeted knots :getter: Non-repeted knots - :type: tuple[float] + :type: tuple[Real] Example use ----------- @@ -221,11 +220,11 @@ def knots(self) -> Tuple[float]: return self.internal.knots @property - def limits(self) -> Tuple[float]: + def limits(self) -> Tuple[Real, Real]: """The knotvector limits :getter: Returns the tuple [Umin, Umax] - :type: tuple[float] + :type: tuple[Real] Example use @@ -251,16 +250,16 @@ def degree(self, value: int): self.increase(diff) @internal.setter - def internal(self, vector: Tuple[float]): + def internal(self, vector: Tuple[Real, ...]): if not isinstance(vector, ImmutableKnotVector): vector = ImmutableKnotVector(vector) self.__internal = vector - def shift(self, value: float) -> KnotVector: + def shift(self, value: Real) -> KnotVector: """Add ``value`` to each knot :param value: The amount to shift every knot - :type value: float + :type value: Real :raises TypeError: If ``value`` is not a number :return: The same instance :rtype: KnotVector @@ -286,11 +285,11 @@ def shift(self, value: float) -> KnotVector: self.internal = ImmutableKnotVector(vector) return self - def scale(self, value: float) -> KnotVector: + def scale(self, value: Real) -> KnotVector: """Multiplies every knot by amount ``value`` :param value: The amount to scale every knot - :type value: float + :type value: Real :raises TypeError: If ``value`` is not a number :raises AssertionError: If ``value`` is not positive :return: The same instance @@ -316,7 +315,7 @@ def scale(self, value: float) -> KnotVector: self.internal = ImmutableKnotVector(knoti * value for knoti in self) return self - def convert(self, cls: type, tolerance: Optional[float] = 1e-9) -> KnotVector: + def convert(self, cls: type, tolerance: Union[None, Real] = 1e-9) -> KnotVector: """Convert the knots from current type to given type. If ``tolerance`` is too small, it raises a ValueError cause cannot convert. @@ -324,7 +323,7 @@ def convert(self, cls: type, tolerance: Optional[float] = 1e-9) -> KnotVector: :param cls: The class to convert the knots :type cls: type :param tolerance: The tolerance to check if each node is very far from other - :type tolerance: float + :type tolerance: Real :raises ValueError: If cannot convert all knots to given type for given tolerance :return: The same instance :rtype: KnotVector @@ -383,11 +382,11 @@ def normalize(self) -> KnotVector: self.scale(1 / self[-1]) return self - def insert(self, nodes: Tuple[float]) -> KnotVector: + def insert(self, nodes: Tuple[Real, ...]) -> KnotVector: """Insert given nodes inside knotvector :param nodes: The nodes to be inserted - :type nodes: tuple[float] + :type nodes: tuple[Real] :raises ValueError: If cannot insert knots :return: The same instance :rtype: KnotVector @@ -409,11 +408,11 @@ def insert(self, nodes: Tuple[float]) -> KnotVector: self.internal = insert_knots(self.internal, nodes) return self - def remove(self, nodes: Tuple[float]) -> KnotVector: + def remove(self, nodes: Tuple[Real, ...]) -> KnotVector: """Remove given nodes inside knotvector :param nodes: The nodes to be remove - :type nodes: tuple[float] + :type nodes: tuple[Real] :raises ValueError: If cannot remove knots :return: The same instance :rtype: KnotVector @@ -443,7 +442,8 @@ def decrease(self, times: int) -> KnotVector: self.internal = decrease_degree(self.internal, times) return self - def span(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: + @vectorize(1, 0) + def span(self, node: Real) -> int: """Finds the index position of a ``node`` such ``knotvector[span] <= node < knotvector[span+1]`` @@ -471,9 +471,10 @@ def span(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: >>> knotvector.span([0, 0.5, 1, 1.5, 2]) (1, 1, 2, 2, 2) """ - return self.internal.span(nodes) + return self.internal.span(node) - def mult(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: + @vectorize(1, 0) + def mult(self, node: Real) -> int: """Counts how many times a node is inside the knotvector If ``nodes`` is a vector of numbers, it returns a list of mult @@ -500,9 +501,10 @@ def mult(self, nodes: Union[float, Tuple[float]]) -> Union[int, Tuple[int]]: >>> knotvector.mult([0, 0.5, 1, 1.2, 1.8, 2]) (2, 0, 1, 0, 0, 2) """ - return self.internal.mult(nodes) + return self.internal.mult(node) - def valid(self, nodes: Tuple[float]) -> bool: + @vectorize(1, 0) + def valid(self, node: Real) -> bool: """Tells if all given nodes are valid :param nodes: The list of nodes @@ -521,9 +523,9 @@ def valid(self, nodes: Tuple[float]) -> bool: >>> knotvector.valid([-1, 0.5, 1]) False """ - return self.internal.valid(nodes) + return self.knots[0] <= node <= self.knots[-1] - def split(self, nodes: Tuple[float]) -> Tuple[KnotVector]: + def split(self, nodes: Tuple[Real, ...]) -> Tuple[KnotVector]: """Split the knot vector at given nodes :param nodes: The list of nodes @@ -691,7 +693,7 @@ def random(degree: int, npts: int, cls: Optional[type] = float) -> KnotVector: return knotvector @staticmethod - def weight(degree: int, weights: Tuple[float]) -> KnotVector: + def weight(degree: int, weights: Tuple[Real, ...]) -> KnotVector: """Creates a knotvector of degree ``degree`` based on given ``weights`` vector. diff --git a/tests/responsive/test_knotspace.py b/tests/responsive/test_knotspace.py index 98c4efb..f5d5a7b 100644 --- a/tests/responsive/test_knotspace.py +++ b/tests/responsive/test_knotspace.py @@ -865,18 +865,17 @@ def test_or_and(): ] ) def test_others(): - knotvect = [0, 0.2, 0.4, 0.4, 0.8, 1] - with pytest.raises(ValueError): - KnotVector(knotvect) + KnotVector([0, 0.2, 0.4, 0.4, 0.8, 1]) + knotvect = [0, 0, 0.5, 1, 1] knotvect = KnotVector(knotvect) knotvect = KnotVector(knotvect) - newvect = knotvect + 1 - newvect = knotvect - 1 - newvect = knotvect * 2 - newvect = knotvect / 2 - newvect = 2 * knotvect + knotvect + 1 + knotvect - 1 + knotvect * 2 + knotvect / 2 + 2 * knotvect np.testing.assert_allclose(knotvect.knots, [0, 0.5, 1]) From ed73fbe9e1b031388612019299665d2ec0b4df6a Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 21:17:55 +0200 Subject: [PATCH 062/116] refactor: Function due to last changes --- src/pynurbs/responsive/functions.py | 40 +++++++++++++++-------------- tests/responsive/test_functions.py | 2 +- 2 files changed, 22 insertions(+), 20 deletions(-) diff --git a/src/pynurbs/responsive/functions.py b/src/pynurbs/responsive/functions.py index 372f3c1..3cca109 100644 --- a/src/pynurbs/responsive/functions.py +++ b/src/pynurbs/responsive/functions.py @@ -1,6 +1,7 @@ from __future__ import annotations from copy import copy +from numbers import Real from typing import Tuple, Union import numpy as np @@ -12,9 +13,11 @@ class BaseFunction: - def __init__(self, knotvector: KnotVector): + def __init__( + self, knotvector: KnotVector, weights: Union[None, Tuple[Real, ...]] = None + ): self.knotvector = knotvector - self.weights = None + self.weights = weights def __eq__(self, other: BaseFunction) -> bool: if not isinstance(other, BaseFunction): @@ -90,7 +93,7 @@ def npts(self) -> int: return self.knotvector.npts @property - def knots(self) -> Tuple[float]: + def knots(self) -> Tuple[Real, ...]: """The knots of the knotvector :getter: knot of the knotvector @@ -110,7 +113,7 @@ def knots(self) -> Tuple[float]: return self.knotvector.knots @property - def weights(self) -> Union[Tuple[float], None]: + def weights(self) -> Union[None, Tuple[Real, ...]]: """Weights of the current function. If it's ``None``, it means the basis function is not rational @@ -136,28 +139,26 @@ def weights(self) -> Union[Tuple[float], None]: @degree.setter def degree(self, value: int): - value = int(value) self.knotvector.degree = value @knotvector.setter def knotvector(self, value: KnotVector): if not isinstance(value, KnotVector): value = KnotVector(value) + self.__basis = ImmutableSplineBasis(value.internal) self.__knotvector = value @weights.setter - def weights(self, value: Tuple[float]): - if value is None: + def weights(self, weights: Union[None, Tuple[Real, ...]]): + if weights is None: self.__weights = None return - value = np.array(value, dtype="object") - if not np.all(value > 0): - error_msg = "All weights must be positive!" - raise ValueError(error_msg) - if value.shape != (self.npts,): - error_msg = f"Weights shape invalid! {value.shape} != ({self.npts})" - raise ValueError(error_msg) - self.__weights = value + weights = tuple(weights) + if len(weights) != self.npts: + raise ValueError(f"Weights must have len {self.npts} != {len(weights)}") + if not all(float(weight) > 0 for weight in weights): + raise ValueError("All weights must be positive!") + self.__weights = weights def __copy__(self) -> BaseFunction: return self.__deepcopy__(None) @@ -179,11 +180,12 @@ def __init__(self, func: BaseFunction, i: Union[int, slice], j: int): @vectorize(1, 0) def __call__(self, node: float) -> Union[float, Tuple[float]]: - result = self.__basis(node) + results = self.__basis(node) if self.__weights is not None: - result *= self.__weights - result *= 1 / sum(result) - return result[self.__first_index] + results = tuple(v * w for v, w in zip(results, self.__weights)) + inverse = 1 / sum(results) + results = tuple(inverse * value for value in results) + return results[self.__first_index] class IndexableFunction(BaseFunction): diff --git a/tests/responsive/test_functions.py b/tests/responsive/test_functions.py index 2b3ef98..234d04b 100644 --- a/tests/responsive/test_functions.py +++ b/tests/responsive/test_functions.py @@ -678,7 +678,7 @@ def test_fail_creation(self): degree, npts = 3, 7 knotvector = GeneratorKnotVector.random(degree, npts) rational = Function(knotvector) - with pytest.raises(ValueError): + with pytest.raises(TypeError): rational.weights = 1 with pytest.raises(ValueError): rational.weights = 1 * np.ones(degree) From 45aaa7a42aff46135cea791fdc93b92ee24fb40b Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 21:30:38 +0200 Subject: [PATCH 063/116] feat: add __str__ and __repr__ for knotvector --- src/pynurbs/core/knotvector.py | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/pynurbs/core/knotvector.py b/src/pynurbs/core/knotvector.py index a7a4b4d..61ab0f1 100644 --- a/src/pynurbs/core/knotvector.py +++ b/src/pynurbs/core/knotvector.py @@ -59,6 +59,12 @@ def knots(self) -> Tuple[Real, ...]: def limits(self) -> Tuple[Real, Real]: return (self[self.degree], self[self.npts]) + def __str__(self) -> str: + return "(" + ", ".join(map(str, self)) + ")" + + def __repr__(self) -> str: + return "(" + ", ".join(map(repr, self)) + ")" + def __getitem__(self, index): return self.__vector[index] From 97440e08171900e945ea2da1ff905dfd26f7884d Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 21:38:09 +0200 Subject: [PATCH 064/116] fix: call of knotvector operations --- src/pynurbs/operations/heavy.py | 63 ++++++++++++++++----------------- 1 file changed, 30 insertions(+), 33 deletions(-) diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index 66919d6..eccf553 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -12,13 +12,7 @@ from ..core.custom_math import Linalg, NodeSample, totuple from ..core.knotvector import ImmutableKnotVector -from ..operations.knotvector import ( - increase_degree, - insert_knots, - remove_knots, - split_knotvector, - union_knotvectors, -) +from ..operations import knotvector as opekv from .least_square import eval_spline_nodes, spline2spline @@ -135,9 +129,11 @@ def split_curve(knotvector: ImmutableKnotVector, nodes: Tuple[float]): - Repeted nodes are ignored, [0.5, 0.5] is the same as [0.5] """ knotvector = ImmutableKnotVector(knotvector) - if not knotvector.valid(nodes): - msg = f"Invalid nodes {nodes} in knotvector {knotvector}" - raise ValueError(msg) + nodes = tuple( + node + for node in nodes + if knotvector.knots[0] <= node <= knotvector.knots[-1] + ) degree = knotvector.degree nodes = set(nodes) # Remove repeted nodes nodes -= set([knotvector[0], knotvector[-1]]) # Take out extremities @@ -146,9 +142,9 @@ def split_curve(knotvector: ImmutableKnotVector, nodes: Tuple[float]): for node in nodes: mult = knotvector.mult(node) manynodes += [node] * (degree + 1 - mult) - bigvector = insert_knots(knotvector, manynodes) + bigvector = opekv.insert_knots(knotvector, manynodes) bigmatrix = Operations.knot_insert(knotvector, manynodes) - newvectors = split_knotvector(bigvector, nodes) + newvectors = opekv.split_knotvector(bigvector, nodes) matrices = [] for newvector in newvectors: umin = newvector.limits[0] @@ -174,9 +170,8 @@ def one_knot_insert_once( [Q] = [T] @ [P] """ knotvector = ImmutableKnotVector(knotvector) - if not knotvector.valid(node): - msg = f"Invalid nodes {node} in knotvector {knotvector}" - raise ValueError(msg) + if not (knotvector.knots[0] <= node <= knotvector.knots[-1]): + raise ValueError(f"Invalid nodes {node} in knotvector {knotvector}") oldnpts = knotvector.npts degree = knotvector.degree @@ -210,9 +205,8 @@ def one_knot_insert( [Q] = [T] @ [P] """ knotvector = ImmutableKnotVector(knotvector) - if not knotvector.valid(node): - msg = f"Invalid node {node} in knotvector {knotvector}" - raise ValueError(msg) + if not (knotvector.knots[0] <= node <= knotvector.knots[-1]): + raise ValueError(f"Invalid node {node} in knotvector {knotvector}") if not isinstance(times, int): msg = f"Times must be an int, not {times}" raise TypeError(msg) @@ -224,7 +218,7 @@ def one_knot_insert( for _ in range(times): incmatrix = Operations.one_knot_insert_once(knotvector, node) matrix = incmatrix @ matrix - knotvector = insert_knots(knotvector, [node]) + knotvector = opekv.insert_knots(knotvector, [node]) return totuple(matrix) def knot_insert(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix2D": @@ -244,9 +238,11 @@ def knot_insert(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix """ knotvector = ImmutableKnotVector(knotvector) - if not knotvector.valid(nodes): - msg = f"Invalid nodes {nodes} in knotvector {knotvector}" - raise ValueError(msg) + + if not all( + knotvector.knots[0] <= node <= knotvector.knots[-1] for node in nodes + ): + raise ValueError(f"Invalid nodes {nodes} in knotvector {knotvector}") nodes = tuple(nodes) setnodes = tuple(sorted(set(nodes) - set([knotvector[0], knotvector[-1]]))) oldnpts = knotvector.npts @@ -257,16 +253,17 @@ def knot_insert(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix times = nodes.count(node) incmatrix = Operations.one_knot_insert(knotvector, node, times) matrix = incmatrix @ matrix - knotvector = insert_knots(knotvector, times * [node]) + knotvector = opekv.insert_knots(knotvector, times * [node]) return totuple(matrix) def knot_remove(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix2D": """ """ knotvector = ImmutableKnotVector(knotvector) - if not knotvector.valid(nodes): - msg = f"Invalid nodes {nodes} in knotvector {knotvector}" - raise ValueError(msg) - newknotvector = remove_knots(knotvector, nodes) + if not all( + knotvector.knots[0] <= node <= knotvector.knots[-1] for node in nodes + ): + raise ValueError(f"Invalid nodes {nodes} in knotvector {knotvector}") + newknotvector = opekv.remove_knots(knotvector, nodes) matrix, _ = spline2spline(knotvector, newknotvector) return totuple(matrix) @@ -307,7 +304,7 @@ def degree_increase_bezier( for i in range(times): elevateonce = Operations.degree_increase_bezier_once(knotvector) matrix = elevateonce @ matrix - knotvector = increase_degree(knotvector, 1) + knotvector = opekv.increase_degree(knotvector, 1) return totuple(matrix) def degree_increase(knotvector: ImmutableKnotVector, times: int) -> "Matrix2D": @@ -329,7 +326,7 @@ def degree_increase(knotvector: ImmutableKnotVector, times: int) -> "Matrix2D": if degree + 1 == npts: return Operations.degree_increase_bezier(knotvector, times) nodes = knotvector.knots - newvectors = split_knotvector(knotvector, nodes) + newvectors = opekv.split_knotvector(knotvector, nodes) matrices = Operations.split_curve(knotvector, nodes) bigmatrix = [] @@ -345,8 +342,8 @@ def degree_increase(knotvector: ImmutableKnotVector, times: int) -> "Matrix2D": for node in nodes: mult = knotvector.mult(node) insertednodes += (degree + 1 - mult) * [node] - bigvector = insert_knots(knotvector, insertednodes) - incbigvector = increase_degree(bigvector, times) + bigvector = opekv.insert_knots(knotvector, insertednodes) + incbigvector = opekv.increase_degree(bigvector, times) removematrix = Operations.knot_remove(incbigvector, insertednodes) bigmatrix = np.array(bigmatrix) @@ -377,7 +374,7 @@ def matrix_transformation( assert degreea <= degreeb matrix_deginc = Operations.degree_increase(knotvectora, degreeb - degreea) if degreea < degreeb: - knotvectora = increase_degree(knotvectora, degreeb - degreea) + knotvectora = opekv.increase_degree(knotvectora, degreeb - degreea) nodes2ins = [] for knot in knotvectorb.knots: @@ -461,7 +458,7 @@ def add_spline_curve( knotvectorb = ImmutableKnotVector(knotvectorb) assert knotvectora.limits == knotvectorb.limits - knotvectorc = union_knotvectors([knotvectora, knotvectorb]) + knotvectorc = opekv.union_knotvectors([knotvectora, knotvectorb]) matrixa = Operations.matrix_transformation(knotvectora, knotvectorc) matrixb = Operations.matrix_transformation(knotvectorb, knotvectorc) return totuple(matrixa), totuple(matrixb) From cc2bebbdbf2446c429c83ef5d04829521bd19037 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 21:39:14 +0200 Subject: [PATCH 065/116] fix: check if received parameter is already KnotVector --- src/pynurbs/responsive/curves.py | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/pynurbs/responsive/curves.py b/src/pynurbs/responsive/curves.py index 35ec3dc..d42085c 100644 --- a/src/pynurbs/responsive/curves.py +++ b/src/pynurbs/responsive/curves.py @@ -40,7 +40,9 @@ class BaseCurve: def __init__(self, knotvector: KnotVector): self.__ctrlpoints = None self.__weights = None - self.__knotvector = KnotVector(knotvector) + if not isinstance(knotvector, KnotVector): + knotvector = KnotVector(knotvector) + self.__knotvector = knotvector def __call__(self, nodes: np.ndarray) -> np.ndarray: return self.eval(nodes) @@ -367,7 +369,8 @@ def ctrlpoints(self): @knotvector.setter def knotvector(self, value: KnotVector): - value = KnotVector(value) + if not isinstance(value, KnotVector): + value = KnotVector(value) self.update(value) @degree.setter @@ -505,7 +508,8 @@ def update( >>> curve.update([0, 0, 1, 1], nodes = (0, 1)) # Remove knot [0.5] """ - newknotvector = KnotVector(newknotvector) + if not isinstance(newknotvector, KnotVector): + newknotvector = KnotVector(newknotvector) if newknotvector == self.knotvector: return if self.ctrlpoints is None: @@ -543,7 +547,8 @@ def apply(self, newknotvector: KnotVector, matrix: Tuple[Tuple[float]]): ControlPoints = [4, 0, 0] """ - newknotvector = KnotVector(newknotvector) + if not isinstance(newknotvector, KnotVector): + newknotvector = KnotVector(newknotvector) oldctrlpoints = self.ctrlpoints oldweights = self.weights if oldctrlpoints is None and oldweights is None: From 8aa74816f1b8b9a3e53538ae1758e66a03aa2865 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 21:44:21 +0200 Subject: [PATCH 066/116] fix: reference to IntegratorArray and NodeSample --- src/pynurbs/operations/calculus.py | 49 +++++++++++++++--------------- 1 file changed, 25 insertions(+), 24 deletions(-) diff --git a/src/pynurbs/operations/calculus.py b/src/pynurbs/operations/calculus.py index ee199dc..13923a8 100644 --- a/src/pynurbs/operations/calculus.py +++ b/src/pynurbs/operations/calculus.py @@ -3,6 +3,7 @@ import numpy as np +from ..core.custom_math import IntegratorArray, NodeSample from ..responsive.curves import Curve from ..responsive.knotspace import KnotVector from . import heavy @@ -137,16 +138,16 @@ def scalar( """ nodes_functs = { - "closed-newton-cotes": heavy.NodeSample.closed_linspace, - "open-newton-cotes": heavy.NodeSample.open_linspace, - "chebyshev": heavy.NodeSample.chebyshev, - "gauss-legendre": heavy.NodeSample.gauss_legendre, + "closed-newton-cotes": NodeSample.closed_linspace, + "open-newton-cotes": NodeSample.open_linspace, + "chebyshev": NodeSample.chebyshev, + "gauss-legendre": NodeSample.gauss_legendre, } array_functs = { - "closed-newton-cotes": heavy.IntegratorArray.closed_newton_cotes, - "open-newton-cotes": heavy.IntegratorArray.open_newton_cotes, - "chebyshev": heavy.IntegratorArray.chebyshev, - "gauss-legendre": heavy.IntegratorArray.gauss_legendre, + "closed-newton-cotes": IntegratorArray.closed_newton_cotes, + "open-newton-cotes": IntegratorArray.open_newton_cotes, + "chebyshev": IntegratorArray.chebyshev, + "gauss-legendre": IntegratorArray.gauss_legendre, } assert isinstance(curve, Curve) if function is None: @@ -241,16 +242,16 @@ def density( """ nodes_functs = { - "closed-newton-cotes": heavy.NodeSample.closed_linspace, - "open-newton-cotes": heavy.NodeSample.open_linspace, - "chebyshev": heavy.NodeSample.chebyshev, - "gauss-legendre": heavy.NodeSample.gauss_legendre, + "closed-newton-cotes": NodeSample.closed_linspace, + "open-newton-cotes": NodeSample.open_linspace, + "chebyshev": NodeSample.chebyshev, + "gauss-legendre": NodeSample.gauss_legendre, } array_functs = { - "closed-newton-cotes": heavy.IntegratorArray.closed_newton_cotes, - "open-newton-cotes": heavy.IntegratorArray.open_newton_cotes, - "chebyshev": heavy.IntegratorArray.chebyshev, - "gauss-legendre": heavy.IntegratorArray.gauss_legendre, + "closed-newton-cotes": IntegratorArray.closed_newton_cotes, + "open-newton-cotes": IntegratorArray.open_newton_cotes, + "chebyshev": IntegratorArray.chebyshev, + "gauss-legendre": IntegratorArray.gauss_legendre, } assert isinstance(curve, Curve) if function is None: @@ -316,16 +317,16 @@ def function( """ nodes_functs = { - "closed-newton-cotes": heavy.NodeSample.closed_linspace, - "open-newton-cotes": heavy.NodeSample.open_linspace, - "chebyshev": heavy.NodeSample.chebyshev, - "gauss-legendre": heavy.NodeSample.gauss_legendre, + "closed-newton-cotes": NodeSample.closed_linspace, + "open-newton-cotes": NodeSample.open_linspace, + "chebyshev": NodeSample.chebyshev, + "gauss-legendre": NodeSample.gauss_legendre, } array_functs = { - "closed-newton-cotes": heavy.IntegratorArray.closed_newton_cotes, - "open-newton-cotes": heavy.IntegratorArray.open_newton_cotes, - "chebyshev": heavy.IntegratorArray.chebyshev, - "gauss-legendre": heavy.IntegratorArray.gauss_legendre, + "closed-newton-cotes": IntegratorArray.closed_newton_cotes, + "open-newton-cotes": IntegratorArray.open_newton_cotes, + "chebyshev": IntegratorArray.chebyshev, + "gauss-legendre": IntegratorArray.gauss_legendre, } if method is not None: pass From bf95521aedfaee2ebf3c954aebbff8c7f3c0cd79 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 21:53:07 +0200 Subject: [PATCH 067/116] feat: define and use function that tells if it's a number --- src/pynurbs/core/custom_math.py | 18 +++++++++++++++++- src/pynurbs/core/knotvector.py | 8 +++++--- src/pynurbs/core/polynomial.py | 6 ++++-- 3 files changed, 26 insertions(+), 6 deletions(-) diff --git a/src/pynurbs/core/custom_math.py b/src/pynurbs/core/custom_math.py index 528ee9b..a923a01 100644 --- a/src/pynurbs/core/custom_math.py +++ b/src/pynurbs/core/custom_math.py @@ -1,7 +1,8 @@ import math from copy import deepcopy from fractions import Fraction -from typing import Optional, Tuple, Union +from numbers import Real +from typing import Any, Optional, Tuple, Union import numpy as np @@ -107,6 +108,21 @@ def binom(n: int, i: int): return int(prod) +def isnumber(obj: Any) -> bool: + """ + Tells if an object is a number + """ + if isinstance(obj, Real): + return True + if isinstance(obj, (str, tuple, list, set, dict)): + return False + try: + (1.0 * float(obj) + 0) / 4.0 + return True + except Exception: + return False + + class NodeSample: __cheby = {1: (Fraction(1, 2),)} __gauss = {1: (Fraction(1, 2),)} diff --git a/src/pynurbs/core/knotvector.py b/src/pynurbs/core/knotvector.py index 61ab0f1..9b37a74 100644 --- a/src/pynurbs/core/knotvector.py +++ b/src/pynurbs/core/knotvector.py @@ -4,6 +4,8 @@ from numbers import Real from typing import Iterable, Tuple, Union +from .custom_math import isnumber + def find_degree(vector: Tuple[Real, ...]) -> int: return max(Counter(vector).values()) - 1 @@ -20,7 +22,7 @@ def __init__(self, vector: Iterable[Real], degree: Union[None, int] = None): vector = tuple(vector) except Exception: raise ValueError(f"Wrong argument: '{vector}'") - if not all(isinstance(val, Real) for val in vector): + if not all(map(isnumber, vector)): raise ValueError(f"Cannot create KnotVector with {vector}") if not is_sorted(vector): raise ValueError(f"Cannot create KnotVector with {vector}") @@ -80,7 +82,7 @@ def __eq__(self, other: object) -> bool: return NotImplemented def span(self, node: Real) -> int: - if not isinstance(node, Real): + if not isnumber(node): raise ValueError(f"Node '{node}' must be Real instance") if node < self[self.degree] or self[self.npts] < node: raise ValueError(f"Node {node} outside [{self.knots[0], self.knots[-1]}]") @@ -98,7 +100,7 @@ def span(self, node: Real) -> int: return mid def mult(self, node: Real) -> int: - if not isinstance(node, Real): + if not isnumber(node): raise ValueError(f"Node '{node}' must be Real instance") if node < self[self.degree] or self[self.npts] < node: raise ValueError(f"Node {node} outside [{self.knots[0], self.knots[-1]}]") diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 936e308..961d0ec 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -9,6 +9,8 @@ from numbers import Real from typing import Iterable, List, Tuple, Union +from .custom_math import isnumber + class Polynomial: """ @@ -35,7 +37,7 @@ def __init__(self, coefs: Iterable[Real]): coefs = tuple(coefs) if len(coefs) == 0: raise ValueError("Cannot receive an empty tuple") - if isinstance(coefs[0], Real): + if isnumber(coefs[0]): degree = max((i for i, v in enumerate(coefs) if v), default=0) else: degree = len(coefs) - 1 @@ -132,7 +134,7 @@ def __str__(self): if self.degree == 0: return str(self[0]) msgs: List[str] = [] - if not isinstance(self[0], Real): + if not isnumber(self[0]): msgs.append(f"({self[0]})") if self.degree > 0: msgs.append(f"({self[1]}) * x") From 9bab3da3c9adead73594d85bf2be548bd566bce5 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 22:05:29 +0200 Subject: [PATCH 068/116] feat: add function that checks if object supports linear operations --- src/pynurbs/core/custom_math.py | 16 ++++++++++++++++ src/pynurbs/core/polynomial.py | 5 ++++- src/pynurbs/responsive/curves.py | 8 ++------ 3 files changed, 22 insertions(+), 7 deletions(-) diff --git a/src/pynurbs/core/custom_math.py b/src/pynurbs/core/custom_math.py index a923a01..74566ae 100644 --- a/src/pynurbs/core/custom_math.py +++ b/src/pynurbs/core/custom_math.py @@ -123,6 +123,22 @@ def isnumber(obj: Any) -> bool: return False +def supports_linear_operation(obj: Any) -> bool: + """ + Tells if an object suports a linear operations like + sum and multiplication by scalar + """ + if isinstance(obj, Real): + return True + if isinstance(obj, (str, tuple, list, set, dict)): + return False + try: + 0 * obj + 0.4 * obj + (-4) * obj + return True + except Exception: + return False + + class NodeSample: __cheby = {1: (Fraction(1, 2),)} __gauss = {1: (Fraction(1, 2),)} diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 961d0ec..0573e42 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -9,7 +9,7 @@ from numbers import Real from typing import Iterable, List, Tuple, Union -from .custom_math import isnumber +from .custom_math import isnumber, supports_linear_operation class Polynomial: @@ -41,6 +41,9 @@ def __init__(self, coefs: Iterable[Real]): degree = max((i for i, v in enumerate(coefs) if v), default=0) else: degree = len(coefs) - 1 + coefs = coefs[: degree + 1] + if not all(map(supports_linear_operation, coefs)): + raise ValueError self.__coefs = tuple(coefs[: degree + 1]) @property diff --git a/src/pynurbs/responsive/curves.py b/src/pynurbs/responsive/curves.py index d42085c..f28e675 100644 --- a/src/pynurbs/responsive/curves.py +++ b/src/pynurbs/responsive/curves.py @@ -409,12 +409,8 @@ def ctrlpoints(self, newpoints: np.ndarray): if newpoints is None: self.__ctrlpoints = None return - if isinstance(newpoints, str): - raise TypeError - try: - iter(newpoints) - except Exception: - raise TypeError + if not all(map(supports_linear_operation, newpoints)): + raise ValueError for point in newpoints: # Verify if operations are valid for each node for knot in self.knotvector.knots: knot * point From c83c2af1402da702e32bdfa3db83e5d85c3877d8 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 22:05:49 +0200 Subject: [PATCH 069/116] fix: use isnumber function when setting for weights --- src/pynurbs/operations/heavy.py | 5 ++++- src/pynurbs/responsive/curves.py | 14 ++++++-------- tests/responsive/test_beziercurve.py | 4 ++-- tests/responsive/test_rationalcurve.py | 2 +- tests/responsive/test_splinecurve.py | 4 ++-- 5 files changed, 15 insertions(+), 14 deletions(-) diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index eccf553..3f70a64 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -10,7 +10,7 @@ import numpy as np -from ..core.custom_math import Linalg, NodeSample, totuple +from ..core.custom_math import Linalg, NodeSample, isnumber, totuple from ..core.knotvector import ImmutableKnotVector from ..operations import knotvector as opekv from .least_square import eval_spline_nodes, spline2spline @@ -27,6 +27,9 @@ def find_roots( We do it by sampling """ + ctrlvalues = tuple(ctrlvalues) + if not all(map(isnumber, ctrlvalues)): + raise ValueError knotvector = ImmutableKnotVector(knotvector) assert isinstance(ctrlvalues, tuple) tolerance = 1e-8 diff --git a/src/pynurbs/responsive/curves.py b/src/pynurbs/responsive/curves.py index f28e675..6b3dcb2 100644 --- a/src/pynurbs/responsive/curves.py +++ b/src/pynurbs/responsive/curves.py @@ -6,7 +6,7 @@ import numpy as np -from ..core.custom_math import number_type +from ..core.custom_math import isnumber, number_type, supports_linear_operation from ..core.spline_basis import ImmutableSplineBasis from ..operations import heavy from ..operations.knotvector import ( @@ -389,13 +389,11 @@ def weights(self, value: Tuple[float]): if value is None: self.__weights = None return - try: - value = tuple(value) - for val in value: - float(val) - except TypeError: - msg = f"Weights must be a vector of floats, received {value}" - raise ValueError(msg) + if not all(map(isnumber, value)): + raise ValueError + if not all(number > 0 for number in value): + raise ValueError + # Verify if there's roots vector = tuple(self.knotvector) roots = heavy.find_roots(vector, value) diff --git a/tests/responsive/test_beziercurve.py b/tests/responsive/test_beziercurve.py index 69de434..79041be 100644 --- a/tests/responsive/test_beziercurve.py +++ b/tests/responsive/test_beziercurve.py @@ -56,9 +56,9 @@ def test_failbuild(self): ctrlpoints = np.random.uniform(-1, 1, npts + 1) with pytest.raises(ValueError): Curve(knotvector, ctrlpoints) - with pytest.raises(TypeError): + with pytest.raises(ValueError): Curve(knotvector, "asd") - with pytest.raises(TypeError): + with pytest.raises(ValueError): Curve(knotvector, "asdefghjk") with pytest.raises(TypeError): Curve(knotvector, 1) diff --git a/tests/responsive/test_rationalcurve.py b/tests/responsive/test_rationalcurve.py index 216ab7a..ae84c41 100644 --- a/tests/responsive/test_rationalcurve.py +++ b/tests/responsive/test_rationalcurve.py @@ -37,7 +37,7 @@ def test_failbuild(self): npts = np.random.randint(degree + 1, degree + 3) knotvector = GeneratorKnotVector.random(degree, npts) curve = Curve(knotvector) - with pytest.raises(ValueError): + with pytest.raises(TypeError): curve.weights = 1 with pytest.raises(ValueError): curve.weights = "asd" diff --git a/tests/responsive/test_splinecurve.py b/tests/responsive/test_splinecurve.py index b0ba833..9eb5c8f 100644 --- a/tests/responsive/test_splinecurve.py +++ b/tests/responsive/test_splinecurve.py @@ -54,9 +54,9 @@ def test_failbuild(self): ctrlpoints = np.random.uniform(-1, 1, npts + 1) with pytest.raises(ValueError): Curve(knotvector, ctrlpoints) - with pytest.raises(TypeError): + with pytest.raises(ValueError): Curve(knotvector, "asd") - with pytest.raises(TypeError): + with pytest.raises(ValueError): Curve(knotvector, "asdefghjk") with pytest.raises(TypeError): Curve(knotvector, 1) From 5c404a0a5db56b348d27e7398cf6b7236068f54d Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 22:08:03 +0200 Subject: [PATCH 070/116] style: insert message inside ValueError directly --- src/pynurbs/core/spline_basis.py | 3 +-- src/pynurbs/operations/heavy.py | 9 +++------ src/pynurbs/responsive/curves.py | 3 +-- 3 files changed, 5 insertions(+), 10 deletions(-) diff --git a/src/pynurbs/core/spline_basis.py b/src/pynurbs/core/spline_basis.py index 86b6d86..efd7d46 100644 --- a/src/pynurbs/core/spline_basis.py +++ b/src/pynurbs/core/spline_basis.py @@ -29,8 +29,7 @@ def spectral_matrix( if not isinstance(reqdegree, int): raise TypeError("reqdegree must be integer") if reqdegree < 0 or knotvector.degree < reqdegree: - msg = f"reqdegree must be in [0, {knotvector.degree}]" - raise ValueError(msg) + raise ValueError(f"reqdegree must be in [0, {knotvector.degree}]") knots = knotvector.knots spans = tuple(map(knotvector.span, knots)) j = reqdegree diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index 3f70a64..286a5fd 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -214,8 +214,7 @@ def one_knot_insert( msg = f"Times must be an int, not {times}" raise TypeError(msg) if times <= 0: - msg = f"Times must be positive! Received {times}" - raise ValueError(msg) + raise ValueError(f"Times must be positive! Received {times}") oldnpts = knotvector.npts matrix = np.eye(oldnpts, dtype="object") for _ in range(times): @@ -300,8 +299,7 @@ def degree_increase_bezier( msg = f"Times must be an int, not {times}" raise TypeError(msg) if times <= 0: - msg = f"Times must be positive! Received {times}" - raise ValueError(msg) + raise ValueError(f"Times must be positive! Received {times}") degree = knotvector.degree matrix = np.eye(degree + 1, dtype="object") for i in range(times): @@ -322,8 +320,7 @@ def degree_increase(knotvector: ImmutableKnotVector, times: int) -> "Matrix2D": if times == 0: return totuple(np.eye(knotvector.npts, dtype="object")) elif times < 0: - msg = f"Times must be >= 0! Received {times}" - raise ValueError(msg) + raise ValueError(f"Times must be >= 0! Received {times}") degree = knotvector.degree npts = knotvector.npts if degree + 1 == npts: diff --git a/src/pynurbs/responsive/curves.py b/src/pynurbs/responsive/curves.py index 6b3dcb2..3ec9484 100644 --- a/src/pynurbs/responsive/curves.py +++ b/src/pynurbs/responsive/curves.py @@ -649,8 +649,7 @@ def eval(self, nodes: Union[float, Tuple[float]]) -> Union[Any, Tuple[Any]]: nodes = (nodes,) onevalue = True if not self.knotvector.valid(nodes): - msg = f"Received invalid nodes to eval: {nodes}" - raise ValueError(msg) + raise ValueError(f"Received invalid nodes to eval: {nodes}") result = self.__eval(nodes) return result[0] if onevalue else result From 330273db718e10377c0843f3436d55baf85cf9ae Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 22:18:24 +0200 Subject: [PATCH 071/116] test: fix test dependency for advanced --- tests/operations/test_advanced.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/tests/operations/test_advanced.py b/tests/operations/test_advanced.py index 9fb984f..54ae658 100644 --- a/tests/operations/test_advanced.py +++ b/tests/operations/test_advanced.py @@ -13,11 +13,11 @@ @pytest.mark.order(42) @pytest.mark.dependency( depends=[ - "tests/test_knotspace.py::test_end", - "tests/test_functions.py::test_end", - "tests/test_beziercurve.py::test_end", - "tests/test_splinecurve.py::test_end", - "tests/test_calculus.py::test_end", + "tests/responsive/test_knotspace.py::test_end", + "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_beziercurve.py::test_end", + "tests/responsive/test_splinecurve.py::test_end", + "tests/operations/test_calculus.py::test_end", ], scope="session", ) From f186b4904682a8f7129fd70723825deb15935208 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Wed, 2 Jul 2025 22:20:34 +0200 Subject: [PATCH 072/116] docs: fix docstrings of functions due to \i and \m chars --- src/pynurbs/operations/calculus.py | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pynurbs/operations/calculus.py b/src/pynurbs/operations/calculus.py index 13923a8..d995cb1 100644 --- a/src/pynurbs/operations/calculus.py +++ b/src/pynurbs/operations/calculus.py @@ -114,7 +114,7 @@ def scalar( method: Optional[str] = None, nnodes: Optional[int] = None, ) -> float: - """Computes the integral I + r"""Computes the integral I If no ``function`` is given, it supposes that :math:`g(u)=1` @@ -183,7 +183,7 @@ def lenght( method: Optional[str] = None, nnodes: Optional[int] = None, ) -> float: - """Computes the integral I + r"""Computes the integral I The operation ``@`` is needed cause ``norm(curve(u)) = numpy.sqrt(curve(u) @ curve(u))`` @@ -216,7 +216,7 @@ def density( method: Optional[str] = None, nnodes: Optional[int] = None, ) -> float: - """Computes the integral I + r"""Computes the integral I The operation ``@`` is needed cause ``norm(curve(u)) = numpy.sqrt(curve(u) @ curve(u))`` @@ -288,7 +288,7 @@ def function( method: Optional[str] = None, nnodes: Optional[int] = None, ) -> float: - """Computes the integral I + r"""Computes the integral I .. math:: I = \int_{a}^{b} g ( u ) \ du From 7c5da6f46b990e58c5b88c269c1fe2ea37a55abb Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Thu, 3 Jul 2025 20:55:58 +0200 Subject: [PATCH 073/116] fix: construction of piecewise polynomial from spline basis --- src/pynurbs/core/spline_basis.py | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/pynurbs/core/spline_basis.py b/src/pynurbs/core/spline_basis.py index efd7d46..e8ea512 100644 --- a/src/pynurbs/core/spline_basis.py +++ b/src/pynurbs/core/spline_basis.py @@ -6,7 +6,7 @@ from .custom_math import totuple from .knotvector import ImmutableKnotVector from .piecepoly import PiecewisePolynomial -from .polynomial import Polynomial +from .polynomial import Polynomial, scale, shift def spectral_matrix( @@ -90,8 +90,12 @@ def knots(self) -> Tuple[Real, ...]: return self.__knotvector.knots def __getitem__(self, index: int) -> PiecewisePolynomial: - functions = self.__matrix[index] - return PiecewisePolynomial(functions, self.knots) + index = int(index) + basis = list(polys[index] for polys in self.__matrix) + for i, base in enumerate(basis): + knota, knotb = self.knots[i], self.knots[i + 1] + basis[i] = shift(scale(base, knotb - knota), knota) + return PiecewisePolynomial(basis, self.knots) def __call__(self, node: Real) -> Tuple[Real, ...]: npts = self.__knotvector.npts From 6b7c43c2d8cd9e4eb444467a268a316baa08a79d Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Thu, 3 Jul 2025 20:56:21 +0200 Subject: [PATCH 074/116] feat: add __eq__ comparator for Piecewise polynomial --- src/pynurbs/core/piecepoly.py | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/pynurbs/core/piecepoly.py b/src/pynurbs/core/piecepoly.py index 0e81bd4..30ada65 100644 --- a/src/pynurbs/core/piecepoly.py +++ b/src/pynurbs/core/piecepoly.py @@ -146,3 +146,8 @@ def __radd__(self, other: Real) -> PiecewisePolynomial: def __rmul__(self, other: Real) -> PiecewisePolynomial: return self.__mul__(other) + + def __eq__(self, other: PiecewisePolynomial) -> bool: + if not isinstance(other, PiecewisePolynomial): + return NotImplemented + return self.knots == other.knots and self.functions == other.functions From f8c0b8f27cbc5a16a212654260d27bd9a5283521 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Thu, 3 Jul 2025 21:03:28 +0200 Subject: [PATCH 075/116] feat: add __str__ for spline basis function --- src/pynurbs/core/spline_basis.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/pynurbs/core/spline_basis.py b/src/pynurbs/core/spline_basis.py index e8ea512..7fd748c 100644 --- a/src/pynurbs/core/spline_basis.py +++ b/src/pynurbs/core/spline_basis.py @@ -113,3 +113,6 @@ def __call__(self, node: Real) -> Tuple[Real, ...]: polynomial = self.__matrix[ind][y] result[i] = polynomial(shifnode) return tuple(result) + + def __str__(self): + return "{" + ", ".join(f"N{i}: {self[i]}" for i in range(self.npts)) + "}" From 07add0c4227b6f31f7400410eecccb47f1b2fb05 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Thu, 3 Jul 2025 21:04:37 +0200 Subject: [PATCH 076/116] fix: creation of piecewise polynomial from basis --- src/pynurbs/core/spline_basis.py | 14 +++++-- tests/core/test_spline_basis.py | 69 ++++++++++++++++++++++++++------ 2 files changed, 67 insertions(+), 16 deletions(-) diff --git a/src/pynurbs/core/spline_basis.py b/src/pynurbs/core/spline_basis.py index 7fd748c..640836c 100644 --- a/src/pynurbs/core/spline_basis.py +++ b/src/pynurbs/core/spline_basis.py @@ -91,10 +91,18 @@ def knots(self) -> Tuple[Real, ...]: def __getitem__(self, index: int) -> PiecewisePolynomial: index = int(index) - basis = list(polys[index] for polys in self.__matrix) - for i, base in enumerate(basis): + nsegs = len(self.knots) - 1 + basis = [Polynomial([0])] * nsegs + spans = tuple(map(self.__knotvector.span, self.knots)) + for i in range(nsegs): knota, knotb = self.knots[i], self.knots[i + 1] - basis[i] = shift(scale(base, knotb - knota), knota) + span = self.__knotvector.span(knota) + ind = spans.index(span) + y = index + self.degree - span + if 0 <= y <= self.degree: + poly = self.__matrix[ind][y] + poly = scale(poly, 1 / (knotb - knota)) + basis[i] = shift(poly, knota) return PiecewisePolynomial(basis, self.knots) def __call__(self, node: Real) -> Tuple[Real, ...]: diff --git a/tests/core/test_spline_basis.py b/tests/core/test_spline_basis.py index c915d0c..c536a43 100644 --- a/tests/core/test_spline_basis.py +++ b/tests/core/test_spline_basis.py @@ -4,6 +4,8 @@ import pytest from pynurbs.core.knotvector import ImmutableKnotVector +from pynurbs.core.piecepoly import PiecewisePolynomial +from pynurbs.core.polynomial import Polynomial from pynurbs.core.spline_basis import ImmutableSplineBasis @@ -21,11 +23,12 @@ def binom(n: int, i: int): return int(prod) -@pytest.mark.order(13) +@pytest.mark.order(14) @pytest.mark.dependency( depends=[ "tests/core/test_knotvector.py::test_end", "tests/core/test_polynomial.py::test_all", + "tests/core/test_piecewise.py::test_all", ], scope="session", ) @@ -34,12 +37,12 @@ def test_begin(): class TestBezier: - @pytest.mark.order(13) + @pytest.mark.order(14) @pytest.mark.dependency(depends=["test_begin"]) def test_begin(self): pass - @pytest.mark.order(13) + @pytest.mark.order(14) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestBezier::test_begin"]) def test_creation(self): @@ -69,7 +72,7 @@ def test_creation(self): assert bezier.degree == degree assert bezier.npts == npts - @pytest.mark.order(13) + @pytest.mark.order(14) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_creation"]) def test_sum_equal_to_1(self): @@ -111,7 +114,7 @@ def test_sum_equal_to_1(self): assert all(result >= 0 for result in results) assert sum(results) == 1 - @pytest.mark.order(13) + @pytest.mark.order(14) @pytest.mark.timeout(5) @pytest.mark.dependency( depends=[ @@ -158,13 +161,53 @@ def test_single_values(self): ) assert bezier(node) == tuple(goods) - @pytest.mark.order(13) + @pytest.mark.order(14) + @pytest.mark.timeout(5) + @pytest.mark.dependency( + depends=[ + "TestBezier::test_creation", + "TestBezier::test_sum_equal_to_1", + "TestBezier::test_single_values", + ] + ) + def test_piecewise_polynomial(self): + x = Polynomial((0, 1)) + + # Degree 0 + knotvector = ImmutableKnotVector([0, 1]) + bezier = ImmutableSplineBasis(knotvector) + assert bezier.degree == 0 + assert bezier.npts == 1 + assert bezier.knots == (0, 1) + assert bezier[0] == PiecewisePolynomial([1 + 0 * x], [0, 1]) + + # Degree 1 + knotvector = ImmutableKnotVector([0, 0, 1, 1]) + bezier = ImmutableSplineBasis(knotvector) + assert bezier.degree == 1 + assert bezier.npts == 2 + assert bezier.knots == (0, 1) + assert bezier[0] == PiecewisePolynomial([1 - x], [0, 1]) + assert bezier[1] == PiecewisePolynomial([x], [0, 1]) + + # Degree 2 + knotvector = ImmutableKnotVector([0, 0, 0, 1, 1, 1]) + bezier = ImmutableSplineBasis(knotvector) + assert bezier.degree == 2 + assert bezier.npts == 3 + assert bezier.knots == (0, 1) + assert bezier[0] == PiecewisePolynomial([(1 - x) ** 2], [0, 1]) + assert bezier[1] == PiecewisePolynomial([2 * x * (1 - x)], [0, 1]) + assert bezier[2] == PiecewisePolynomial([x**2], [0, 1]) + + @pytest.mark.order(14) @pytest.mark.dependency( depends=[ "TestBezier::test_begin", "TestBezier::test_creation", "TestBezier::test_sum_equal_to_1", "TestBezier::test_single_values", + "TestBezier::test_piecewise_polynomial", ] ) def test_all(self): @@ -172,12 +215,12 @@ def test_all(self): class TestSpline: - @pytest.mark.order(13) + @pytest.mark.order(14) @pytest.mark.dependency(depends=["TestBezier::test_all"]) def test_begin(self): pass - @pytest.mark.order(13) + @pytest.mark.order(14) @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestSpline::test_begin"]) def test_creation(self): @@ -205,7 +248,7 @@ def test_creation(self): assert spline.degree == 2 assert spline.npts == 4 - @pytest.mark.order(13) + @pytest.mark.order(14) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_creation"]) def test_tablevalues_degree1npts3(self): @@ -232,7 +275,7 @@ def test_tablevalues_degree1npts3(self): for node, good in zip(nodes_test, matrix_good): np.testing.assert_allclose(spline(node), good) - @pytest.mark.order(13) + @pytest.mark.order(14) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree1npts3"]) def test_tablevalues_degree2npts4(self): @@ -260,7 +303,7 @@ def test_tablevalues_degree2npts4(self): for node, good in zip(nodes_test, matrix_good): np.testing.assert_allclose(spline(node), good) - @pytest.mark.order(13) + @pytest.mark.order(14) @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree2npts4"]) def test_tablevalues_degree3npts5(self): @@ -287,7 +330,7 @@ def test_tablevalues_degree3npts5(self): for node, good in zip(nodes_test, matrix_good): np.testing.assert_allclose(spline(node), good) - @pytest.mark.order(13) + @pytest.mark.order(14) @pytest.mark.dependency( depends=[ "TestSpline::test_begin", @@ -301,7 +344,7 @@ def test_all(self): pass -@pytest.mark.order(13) +@pytest.mark.order(14) @pytest.mark.dependency( depends=[ "test_begin", From e4e3c21a0a7f8927790f7f75f7b525cfa4a20df8 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Thu, 3 Jul 2025 23:32:36 +0200 Subject: [PATCH 077/116] fix: isnumber function to accept numpy arrays --- src/pynurbs/core/custom_math.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/src/pynurbs/core/custom_math.py b/src/pynurbs/core/custom_math.py index 74566ae..86a325c 100644 --- a/src/pynurbs/core/custom_math.py +++ b/src/pynurbs/core/custom_math.py @@ -117,10 +117,13 @@ def isnumber(obj: Any) -> bool: if isinstance(obj, (str, tuple, list, set, dict)): return False try: - (1.0 * float(obj) + 0) / 4.0 - return True - except Exception: + iter(obj) return False + except TypeError: + try: + return bool((1.0 * float(obj) + 4) * 5.0 * 0.0 == 0) + except (TypeError, ValueError): + return False def supports_linear_operation(obj: Any) -> bool: From 9e814033f35fba3b9f2e9b1a76558c9d7d6d2029 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Thu, 3 Jul 2025 23:33:04 +0200 Subject: [PATCH 078/116] feat: Polynomial now accepts Real parameter --- src/pynurbs/core/polynomial.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 0573e42..7908718 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -34,7 +34,7 @@ class Polynomial: """ def __init__(self, coefs: Iterable[Real]): - coefs = tuple(coefs) + coefs = tuple(coefs) if not isnumber(coefs) else (coefs,) if len(coefs) == 0: raise ValueError("Cannot receive an empty tuple") if isnumber(coefs[0]): @@ -105,6 +105,8 @@ def __truediv__(self, other: Real) -> Polynomial: return self.__class__(coefs) def __pow__(self, other: int) -> Polynomial: + if other == 0: + return self.__class__([1 + 0 * sum(self)]) result = self for _ in range(int(other) - 1): result = result * self From 8d45deb80a64035cd9dfcff92649e3a06376cef6 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Thu, 3 Jul 2025 23:33:44 +0200 Subject: [PATCH 079/116] feat: transform function to Polynomial if it's not already in Piecewise --- src/pynurbs/core/piecepoly.py | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/pynurbs/core/piecepoly.py b/src/pynurbs/core/piecepoly.py index 30ada65..ce844e8 100644 --- a/src/pynurbs/core/piecepoly.py +++ b/src/pynurbs/core/piecepoly.py @@ -48,7 +48,9 @@ class PiecewisePolynomial: """ def __init__(self, functions: Iterable[Polynomial], knots: Iterable[Real]) -> None: - functions = tuple(functions) + functions = tuple( + f if isinstance(f, Polynomial) else Polynomial(f) for f in functions + ) knots = tuple(knots) if len(knots) != 1 + len(functions): raise ValueError(f"{len(knots)} != 1 + {len(functions)}") From 5d0c3ae76a93adf944338dfd8ef15c8b8070f1ed Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Thu, 3 Jul 2025 23:34:12 +0200 Subject: [PATCH 080/116] test: add tests to check the coefs of piecewise obtained from spline --- tests/core/test_spline_basis.py | 64 +++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/core/test_spline_basis.py b/tests/core/test_spline_basis.py index c536a43..6ff87a2 100644 --- a/tests/core/test_spline_basis.py +++ b/tests/core/test_spline_basis.py @@ -200,6 +200,19 @@ def test_piecewise_polynomial(self): assert bezier[1] == PiecewisePolynomial([2 * x * (1 - x)], [0, 1]) assert bezier[2] == PiecewisePolynomial([x**2], [0, 1]) + knots = 0, 1 + for degree in range(6): + vector = [knots[0]] * (degree + 1) + [knots[1]] * (degree + 1) + knotvector = ImmutableKnotVector(vector) + bezier = ImmutableSplineBasis(knotvector) + assert bezier.degree == degree + assert bezier.npts == degree + 1 + assert bezier.knots == knots + for j in range(degree + 1): + poly = (knots[1] - knots[0] - x) ** (degree - j) * x**j + good = PiecewisePolynomial([binom(degree, j) * poly], knots) + assert bezier[j] == good + @pytest.mark.order(14) @pytest.mark.dependency( depends=[ @@ -330,6 +343,56 @@ def test_tablevalues_degree3npts5(self): for node, good in zip(nodes_test, matrix_good): np.testing.assert_allclose(spline(node), good) + @pytest.mark.order(14) + @pytest.mark.timeout(5) + @pytest.mark.dependency( + depends=[ + "TestSpline::test_tablevalues_degree2npts4", + "TestSpline::test_tablevalues_degree3npts5", + ] + ) + def test_piecewise_polynomial(self): + x = Polynomial([0, 1]) + + knotvector = [0, 0, 1, 2, 3, 3] + knotvector = ImmutableKnotVector(knotvector) + spline = ImmutableSplineBasis(knotvector) + knots = (0, 1, 2, 3) + assert spline.degree == 1 + assert spline.npts == 4 + assert spline.knots == knots + + functions = [[1 - x, 0, 0], [x, 2 - x, 0], [0, x - 1, 3 - x]] + assert spline[0] == PiecewisePolynomial(functions[0], knots) + assert spline[1] == PiecewisePolynomial(functions[1], knots) + assert spline[2] == PiecewisePolynomial(functions[2], knots) + + knotvector = [0, 0, 0, 2, 2, 5, 7, 7, 7] + knotvector = map(Fraction, knotvector) + knotvector = ImmutableKnotVector(knotvector) + spline = ImmutableSplineBasis(knotvector) + knots = (0, 2, 5, 7) + assert spline.degree == 2 + assert spline.npts == 6 + assert spline.knots == knots + + one = Fraction(1, 1) + functions = [ + [1 - x + x * x * one / 4, 0, 0], + [x - x * x * one / 2, 0, 0], + [x * x * one / 4, (25 - 10 * x + x * x) * one / 9, 0], + [ + 0, + (-92 + 62 * x - 8 * x * x) * one / 45, + (49 - 14 * x + x * x) * one / 10, + ], + [0, (4 - 4 * x + x * x) * one / 15, (-203 + 78 * x - 7 * x * x) * one / 20], + [0, 0, (25 - 10 * x + x * x) * one / 4], + ] + for i, functs in enumerate(functions): + good = PiecewisePolynomial(functs, knots) + assert spline[i] == good + @pytest.mark.order(14) @pytest.mark.dependency( depends=[ @@ -338,6 +401,7 @@ def test_tablevalues_degree3npts5(self): "TestSpline::test_tablevalues_degree1npts3", "TestSpline::test_tablevalues_degree2npts4", "TestSpline::test_tablevalues_degree3npts5", + "TestSpline::test_piecewise_polynomial", ] ) def test_all(self): From 83273c7dcc51dea18f68ab913c2e564cd98522fb Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 4 Jul 2025 21:10:55 +0200 Subject: [PATCH 081/116] refactor: compare two basis functions --- src/pynurbs/responsive/functions.py | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/pynurbs/responsive/functions.py b/src/pynurbs/responsive/functions.py index 3cca109..8814bde 100644 --- a/src/pynurbs/responsive/functions.py +++ b/src/pynurbs/responsive/functions.py @@ -22,13 +22,7 @@ def __init__( def __eq__(self, other: BaseFunction) -> bool: if not isinstance(other, BaseFunction): return NotImplemented - if self.knotvector != other.knotvector: - return False - weightleft = self.weights - weightrigh = other.weights - weightleft = np.ones(self.npts) if self.weights is None else self.weights - weightrigh = np.ones(self.npts) if weightrigh is None else weightrigh - return np.all(weightleft == weightrigh) + return self.knotvector == other.knotvector and self.weights == other.weights @property def knotvector(self) -> KnotVector: From 380042eefadd735f658a8df7203e76abc8a7703b Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 4 Jul 2025 21:11:54 +0200 Subject: [PATCH 082/116] style: minor corrections of formating --- src/pynurbs/responsive/functions.py | 16 +++++++--------- 1 file changed, 7 insertions(+), 9 deletions(-) diff --git a/src/pynurbs/responsive/functions.py b/src/pynurbs/responsive/functions.py index 8814bde..7ff0c2d 100644 --- a/src/pynurbs/responsive/functions.py +++ b/src/pynurbs/responsive/functions.py @@ -2,9 +2,7 @@ from copy import copy from numbers import Real -from typing import Tuple, Union - -import numpy as np +from typing import Iterable, Tuple, Union from pynurbs.core.spline_basis import ImmutableSplineBasis @@ -14,7 +12,7 @@ class BaseFunction: def __init__( - self, knotvector: KnotVector, weights: Union[None, Tuple[Real, ...]] = None + self, knotvector: KnotVector, weights: Union[None, Iterable[Real]] = None ): self.knotvector = knotvector self.weights = weights @@ -136,11 +134,10 @@ def degree(self, value: int): self.knotvector.degree = value @knotvector.setter - def knotvector(self, value: KnotVector): - if not isinstance(value, KnotVector): - value = KnotVector(value) - self.__basis = ImmutableSplineBasis(value.internal) - self.__knotvector = value + def knotvector(self, vector: KnotVector): + if not isinstance(vector, KnotVector): + vector = KnotVector(vector) + self.__knotvector = vector @weights.setter def weights(self, weights: Union[None, Tuple[Real, ...]]): @@ -152,6 +149,7 @@ def weights(self, weights: Union[None, Tuple[Real, ...]]): raise ValueError(f"Weights must have len {self.npts} != {len(weights)}") if not all(float(weight) > 0 for weight in weights): raise ValueError("All weights must be positive!") + # Still needs to check if there are no roots self.__weights = weights def __copy__(self) -> BaseFunction: From 40cfa334830fccfa543667c05f428a5c9d16e83d Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 4 Jul 2025 21:15:31 +0200 Subject: [PATCH 083/116] refactor!: rename class 'Function' to 'BasisFunctions' --- src/pynurbs/__init__.py | 2 +- src/pynurbs/responsive/__init__.py | 2 +- .../{functions.py => basis_functions.py} | 18 ++-- tests/core/test_polynomial.py | 12 +-- tests/operations/test_advanced.py | 2 +- tests/operations/test_calculus.py | 2 +- tests/operations/test_customstruc.py | 6 +- tests/operations/test_fitting.py | 2 +- ...t_functions.py => test_basis_functions.py} | 87 ++++++++++--------- tests/responsive/test_beziercurve.py | 2 +- tests/responsive/test_rationalcurve.py | 2 +- tests/responsive/test_splinecurve.py | 2 +- 12 files changed, 70 insertions(+), 69 deletions(-) rename src/pynurbs/responsive/{functions.py => basis_functions.py} (94%) rename tests/responsive/{test_functions.py => test_basis_functions.py} (92%) diff --git a/src/pynurbs/__init__.py b/src/pynurbs/__init__.py index ab32605..bc49191 100644 --- a/src/pynurbs/__init__.py +++ b/src/pynurbs/__init__.py @@ -1,6 +1,6 @@ from .operations.advanced import Intersection, Projection from .operations.calculus import Derivate, Integrate -from .responsive import Curve, Function, GeneratorKnotVector, KnotVector +from .responsive import BasisFunctions, Curve, GeneratorKnotVector, KnotVector __version__ = "1.1.0" diff --git a/src/pynurbs/responsive/__init__.py b/src/pynurbs/responsive/__init__.py index 08f097f..24746a7 100644 --- a/src/pynurbs/responsive/__init__.py +++ b/src/pynurbs/responsive/__init__.py @@ -1,3 +1,3 @@ from .curves import Curve -from .functions import Function +from .basis_functions import BasisFunctions from .knotspace import GeneratorKnotVector, KnotVector diff --git a/src/pynurbs/responsive/functions.py b/src/pynurbs/responsive/basis_functions.py similarity index 94% rename from src/pynurbs/responsive/functions.py rename to src/pynurbs/responsive/basis_functions.py index 7ff0c2d..07444e0 100644 --- a/src/pynurbs/responsive/functions.py +++ b/src/pynurbs/responsive/basis_functions.py @@ -35,7 +35,7 @@ def knotvector(self) -> KnotVector: >>> from pynurbs import KnotVector >>> knotvector = KnotVector([0, 0, 2, 3, 3]) - >>> basis = Function(knotvector) + >>> basis = BasisFunctions(knotvector) >>> basis.knotvector (0, 0, 2, 3, 3) >>> type(basis.knotvector) @@ -57,7 +57,7 @@ def degree(self) -> int: >>> from pynurbs import KnotVector >>> knotvector = KnotVector([0, 0, 2, 3, 3]) - >>> basis = Function(knotvector) + >>> basis = BasisFunctions(knotvector) >>> basis.degree 1 @@ -77,7 +77,7 @@ def npts(self) -> int: >>> from pynurbs import KnotVector >>> knotvector = KnotVector([0, 0, 2, 3, 3]) - >>> basis = Function(knotvector) + >>> basis = BasisFunctions(knotvector) >>> basis.npts 3 @@ -97,7 +97,7 @@ def knots(self) -> Tuple[Real, ...]: >>> from pynurbs import KnotVector >>> knotvector = KnotVector([0, 0, 2, 3, 3]) - >>> basis = Function(knotvector) + >>> basis = BasisFunctions(knotvector) >>> basis.knots (0, 2, 3) @@ -118,7 +118,7 @@ def weights(self) -> Union[None, Tuple[Real, ...]]: >>> from pynurbs import KnotVector >>> knotvector = KnotVector([0, 0, 2, 3, 3]) - >>> basis = Function(knotvector) + >>> basis = BasisFunctions(knotvector) >>> basis.weights None @@ -220,16 +220,16 @@ def __call__(self, node: float) -> Union[float, Tuple[float]]: return self[:, self.degree](node) -class Function(IndexableFunction): - """Basis Function class, to evaluate functions +class BasisFunctions(IndexableFunction): + """Basis BasisFunctions class, to evaluate functions Example use ----------- >>> import numpy as np - >>> from pynurbs import Function + >>> from pynurbs import BasisFunctions >>> knotvector = [0, 0, 1, 1] - >>> basis = Function(knotvector) + >>> basis = BasisFunctions(knotvector) >>> basis.degree 1 >>> basis.npts diff --git a/tests/core/test_polynomial.py b/tests/core/test_polynomial.py index c540713..50387cb 100644 --- a/tests/core/test_polynomial.py +++ b/tests/core/test_polynomial.py @@ -60,7 +60,7 @@ def test_neg(): @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_add(): """ - Function to test if the polynomials coefficients + BasisFunctions to test if the polynomials coefficients are correctly computed """ import numpy as np @@ -100,7 +100,7 @@ def test_add(): @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_sub(): """ - Function to test if the polynomials coefficients + BasisFunctions to test if the polynomials coefficients are correctly computed """ import numpy as np @@ -140,7 +140,7 @@ def test_sub(): @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_mul(): """ - Function to test if the polynomials coefficients + BasisFunctions to test if the polynomials coefficients are correctly computed """ import numpy as np @@ -180,7 +180,7 @@ def test_mul(): @pytest.mark.dependency(depends=["test_build", "test_degree", "test_evaluate"]) def test_truediv(): """ - Function to test if the polynomials coefficients + BasisFunctions to test if the polynomials coefficients are correctly computed """ import numpy as np @@ -244,7 +244,7 @@ def test_integrate(): ) def test_shift(): """ - Function to test if the polynomials coefficients + BasisFunctions to test if the polynomials coefficients are correctly computed """ import numpy as np @@ -269,7 +269,7 @@ def test_shift(): ) def test_scale(): """ - Function to test if the polynomials coefficients + BasisFunctions to test if the polynomials coefficients are correctly computed """ import numpy as np diff --git a/tests/operations/test_advanced.py b/tests/operations/test_advanced.py index 54ae658..8dc96df 100644 --- a/tests/operations/test_advanced.py +++ b/tests/operations/test_advanced.py @@ -14,7 +14,7 @@ @pytest.mark.dependency( depends=[ "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", "tests/responsive/test_splinecurve.py::test_end", "tests/operations/test_calculus.py::test_end", diff --git a/tests/operations/test_calculus.py b/tests/operations/test_calculus.py index 7aa4e73..52b2174 100644 --- a/tests/operations/test_calculus.py +++ b/tests/operations/test_calculus.py @@ -17,7 +17,7 @@ @pytest.mark.dependency( depends=[ "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", "tests/responsive/test_splinecurve.py::test_end", "tests/responsive/test_rationalcurve.py::test_end", diff --git a/tests/operations/test_customstruc.py b/tests/operations/test_customstruc.py index 8d31c8a..cb93170 100644 --- a/tests/operations/test_customstruc.py +++ b/tests/operations/test_customstruc.py @@ -13,7 +13,7 @@ import pytest -from pynurbs.responsive.functions import Function +from pynurbs.responsive.basis_functions import BasisFunctions from pynurbs.responsive.knotspace import KnotVector @@ -113,7 +113,7 @@ def __rmul__(self, other: CustomFloat): @pytest.mark.dependency( depends=[ "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", "tests/responsive/test_splinecurve.py::test_end", "tests/responsive/test_rationalcurve.py::test_end", @@ -191,7 +191,7 @@ def test_begin(self): def test_creation(self): a, b = CustomFloat(0), CustomFloat(1) vector = KnotVector([a, a, b, b]) - N = Function(vector) + N = BasisFunctions(vector) assert type(N[0](a)) is CustomFloat assert type(N[0](b)) is CustomFloat diff --git a/tests/operations/test_fitting.py b/tests/operations/test_fitting.py index 685e39f..970e70b 100644 --- a/tests/operations/test_fitting.py +++ b/tests/operations/test_fitting.py @@ -9,7 +9,7 @@ @pytest.mark.dependency( depends=[ "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", "tests/responsive/test_splinecurve.py::test_end", "tests/responsive/test_rationalcurve.py::test_end", diff --git a/tests/responsive/test_functions.py b/tests/responsive/test_basis_functions.py similarity index 92% rename from tests/responsive/test_functions.py rename to tests/responsive/test_basis_functions.py index 234d04b..8a69791 100644 --- a/tests/responsive/test_functions.py +++ b/tests/responsive/test_basis_functions.py @@ -4,7 +4,7 @@ import pytest from pynurbs.core.custom_math import binom -from pynurbs.responsive.functions import Function +from pynurbs.responsive.basis_functions import BasisFunctions from pynurbs.responsive.knotspace import GeneratorKnotVector @@ -30,11 +30,11 @@ def test_begin(self): @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestBezier::test_begin"]) def test_creation(self): - bezier = Function([0, 0, 1, 1]) + bezier = BasisFunctions([0, 0, 1, 1]) assert callable(bezier) assert bezier.degree == 1 assert bezier.npts == 2 - bezier = Function([0, 0, 0, 1, 1, 1]) + bezier = BasisFunctions([0, 0, 0, 1, 1, 1]) assert callable(bezier) assert bezier.degree == 2 assert bezier.npts == 3 @@ -45,7 +45,7 @@ def test_creation(self): def test_random_creation(self): for degree in range(1, 6): knotvector = GeneratorKnotVector.bezier(degree) - bezier = Function(knotvector) + bezier = BasisFunctions(knotvector) assert callable(bezier) assert bezier.degree == degree assert bezier.npts == degree + 1 @@ -54,7 +54,7 @@ def test_random_creation(self): @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_random_creation"]) def test_evalfuncs_degree1(self): - bezier = Function([0, 0, 1, 1]) + bezier = BasisFunctions([0, 0, 1, 1]) assert bezier.degree == 1 assert bezier.npts == 2 assert callable(bezier[0, 0]) @@ -69,7 +69,7 @@ def test_evalfuncs_degree1(self): @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_random_creation"]) def test_evalfuncs_degree2(self): - bezier = Function([0, 0, 0, 1, 1, 1]) + bezier = BasisFunctions([0, 0, 0, 1, 1, 1]) assert bezier.degree == 2 assert bezier.npts == 3 assert callable(bezier[0, 0]) @@ -98,7 +98,7 @@ def test_shape_calls(self): for degree in range(1, 6): npts = degree + 1 vector = GeneratorKnotVector.bezier(degree) - bezier = Function(vector) + bezier = BasisFunctions(vector) assert bezier.degree == degree assert bezier.npts == npts @@ -124,7 +124,7 @@ def test_sum_equal_to_1(self): for degree in range(1, 6): npts = degree + 1 vector = GeneratorKnotVector.bezier(degree) - bezier = Function(vector) + bezier = BasisFunctions(vector) assert bezier.degree == degree assert bezier.npts == npts @@ -145,7 +145,7 @@ def test_standard_index(self): for degree in range(1, 6): npts = degree + 1 vector = GeneratorKnotVector.bezier(degree) - bezier = Function(vector) + bezier = BasisFunctions(vector) assert bezier.degree == degree assert bezier.npts == npts @@ -168,7 +168,7 @@ def test_standard_index(self): ) def test_singlevalues_degree1(self): knotvector = [0, 0, 1, 1] # degree = 1, npts = 2 - bezier = Function(knotvector) + bezier = BasisFunctions(knotvector) assert bezier[0, 0](0.0) == 0 assert bezier[0, 0](0.5) == 0 assert bezier[0, 0](1.0) == 0 @@ -192,7 +192,7 @@ def test_singlevalues_degree1(self): ) def test_singlevalues_degree2(self): knotvector = [0, 0, 0, 1, 1, 1] # degree = 2, npts = 3 - bezier = Function(knotvector) + bezier = BasisFunctions(knotvector) assert bezier[0, 0](0.0) == 0 assert bezier[0, 0](0.5) == 0 assert bezier[0, 0](1.0) == 0 @@ -230,7 +230,7 @@ def test_singlevalues_degree2(self): ] ) def test_tablevalues_degree1(self): - bezier = Function([0, 0, 1, 1]) + bezier = BasisFunctions([0, 0, 1, 1]) assert bezier.degree == 1 assert bezier.npts == 2 nodes_test = np.linspace(0, 1, 11) @@ -252,7 +252,7 @@ def test_tablevalues_degree1(self): ] ) def test_tablevalues_degree2(self): - bezier = Function([0, 0, 0, 1, 1, 1]) + bezier = BasisFunctions([0, 0, 0, 1, 1, 1]) assert bezier.degree == 2 assert bezier.npts == 3 nodes_test = np.linspace(0, 1, 11) @@ -292,7 +292,7 @@ def test_tablevalues_degree2(self): def test_tablevalues_random_degree(self): for degree in range(1, 6): knotvector = GeneratorKnotVector.bezier(degree) - bezier = Function(knotvector) + bezier = BasisFunctions(knotvector) assert bezier.degree == degree assert bezier.npts == degree + 1 @@ -315,7 +315,7 @@ def test_shifted_scaled_bezier(self): scaleval = np.exp(np.random.uniform(-1, 1)) # knotvector.shift(shiftval) # knotvector.scale(scaleval) - bezier = Function(knotvector) + bezier = BasisFunctions(knotvector) assert bezier.degree == degree assert bezier.npts == degree + 1 @@ -334,7 +334,7 @@ def test_shifted_scaled_bezier(self): @pytest.mark.dependency(depends=["TestBezier::test_shifted_scaled_bezier"]) def test_degree_operations(self): knotvector = GeneratorKnotVector.bezier(3) - bezier = Function(knotvector) + bezier = BasisFunctions(knotvector) assert bezier.degree == 3 assert bezier.npts == 4 bezier.degree = 2 @@ -378,19 +378,19 @@ def test_begin(self): @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestSpline::test_begin"]) def test_creation(self): - spline = Function([0, 0, 1, 1]) + spline = BasisFunctions([0, 0, 1, 1]) assert callable(spline) assert spline.degree == 1 assert spline.npts == 2 - spline = Function([0, 0, 0.5, 1, 1]) + spline = BasisFunctions([0, 0, 0.5, 1, 1]) assert callable(spline) assert spline.degree == 1 assert spline.npts == 3 - spline = Function([0, 0, 0, 1, 1, 1]) + spline = BasisFunctions([0, 0, 0, 1, 1, 1]) assert callable(spline) assert spline.degree == 2 assert spline.npts == 3 - spline = Function([0, 0, 0, 0.5, 1, 1, 1]) + spline = BasisFunctions([0, 0, 0, 0.5, 1, 1, 1]) assert callable(spline) assert spline.degree == 2 assert spline.npts == 4 @@ -402,7 +402,7 @@ def test_random_creation(self): for degree in range(1, 6): npts = np.random.randint(degree + 1, degree + 9) knotvector = GeneratorKnotVector.random(degree, npts) - spline = Function(knotvector) + spline = BasisFunctions(knotvector) assert callable(spline) assert spline.degree == degree assert spline.npts == npts @@ -411,7 +411,7 @@ def test_random_creation(self): @pytest.mark.timeout(1) @pytest.mark.dependency(depends=["TestSpline::test_random_creation"]) def test_evalfuncs_degree1npts3(self): - spline = Function([0, 0, 0.5, 1, 1]) + spline = BasisFunctions([0, 0, 0.5, 1, 1]) assert spline.degree == 1 assert spline.npts == 3 assert callable(spline[0, 0]) @@ -428,7 +428,7 @@ def test_evalfuncs_degree1npts3(self): @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_evalfuncs_degree1npts3"]) def test_tablevalues_degree1npts3(self): - spline = Function([0, 0, 0.5, 1, 1]) + spline = BasisFunctions([0, 0, 0.5, 1, 1]) assert spline.degree == 1 assert spline.npts == 3 nodes_test = np.linspace(0, 1, 11) @@ -469,7 +469,7 @@ def test_tablevalues_degree1npts3(self): @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree1npts3"]) def test_tablevalues_degree2npts4(self): - spline = Function([0, 0, 0, 0.5, 1, 1, 1]) + spline = BasisFunctions([0, 0, 0, 0.5, 1, 1, 1]) assert spline.degree == 2 assert spline.npts == 4 nodes_test = np.linspace(0, 1, 11) @@ -527,7 +527,7 @@ def test_tablevalues_degree2npts4(self): @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree2npts4"]) def test_tablevalues_degree3npts5(self): knotvector = [0, 0, 0, 0, 0.5, 1, 1, 1, 1] - spline = Function(knotvector) + spline = BasisFunctions(knotvector) assert spline.degree == 3 assert spline.npts == 5 nodes_test = np.linspace(0, 1, 11) @@ -603,7 +603,7 @@ def test_tablevalues_degree3npts5(self): ] ) def test_degree_operation(self): - spline = Function([0, 0, 0, 0, 1, 2, 2, 2, 2]) + spline = BasisFunctions([0, 0, 0, 0, 1, 2, 2, 2, 2]) assert spline.degree == 3 assert spline.npts == 5 spline.degree = 2 @@ -611,7 +611,7 @@ def test_degree_operation(self): assert spline.npts == 3 assert spline.knotvector == [0, 0, 0, 2, 2, 2] - spline = Function([0, 0, 0, 1, 2, 3, 3, 3]) + spline = BasisFunctions([0, 0, 0, 1, 2, 3, 3, 3]) assert spline.degree == 2 assert spline.npts == 5 spline.degree -= 1 @@ -619,7 +619,7 @@ def test_degree_operation(self): assert spline.npts == 2 assert spline.knotvector == [0, 0, 3, 3] - spline = Function([0, 0, 0, 1, 1, 2, 2, 2]) + spline = BasisFunctions([0, 0, 0, 1, 1, 2, 2, 2]) assert spline.degree == 2 assert spline.npts == 5 spline.degree -= 1 @@ -627,7 +627,7 @@ def test_degree_operation(self): assert spline.npts == 3 assert spline.knotvector == [0, 0, 1, 2, 2] - spline = Function([0, 0, 0, 1, 1, 2, 3, 3, 3]) + spline = BasisFunctions([0, 0, 0, 1, 1, 2, 3, 3, 3]) assert spline.degree == 2 assert spline.npts == 6 spline.degree += 1 @@ -665,7 +665,7 @@ def test_creation(self): for degree in range(1, 6): npts = np.random.randint(degree + 1, degree + 9) knotvector = GeneratorKnotVector.random(degree, npts) - rational = Function(knotvector) + rational = BasisFunctions(knotvector) rational.weights = np.random.uniform(0.1, 1, npts) assert callable(rational) assert rational.degree == degree @@ -677,7 +677,7 @@ def test_creation(self): def test_fail_creation(self): degree, npts = 3, 7 knotvector = GeneratorKnotVector.random(degree, npts) - rational = Function(knotvector) + rational = BasisFunctions(knotvector) with pytest.raises(TypeError): rational.weights = 1 with pytest.raises(ValueError): @@ -690,13 +690,14 @@ def test_fail_creation(self): @pytest.mark.dependency(depends=["TestRational::test_begin"]) def test_compare_spline(self): knotvector = [0, 0, 0, 1, 2, 2, 2] - spline = Function(knotvector) + spline = BasisFunctions(knotvector) rational = copy(spline) rational.weights = np.ones(rational.npts) - assert rational == spline + assert rational != spline - rational = Function([0, 0, 0, 2, 4, 4, 4]) + vector = [0, 0, 0, 2, 4, 4, 4] + rational = BasisFunctions(vector) rational.weights = np.ones(rational.npts) assert rational != spline @@ -709,7 +710,7 @@ def test_compare_spline(self): @pytest.mark.dependency(depends=["TestRational::test_begin"]) def test_values_rational_equal_spline(self): knotvector = [0, 0, 0, 1, 2, 2, 2] - spline = Function(knotvector) + spline = BasisFunctions(knotvector) rational = copy(spline) rational.weights = np.ones(rational.npts) @@ -722,7 +723,7 @@ def test_values_rational_equal_spline(self): @pytest.mark.dependency(depends=["TestRational::test_begin"]) def test_quarter_circle_standard(self): knotvector = [0, 0, 0, 1, 1, 1] - rational = Function(knotvector) + rational = BasisFunctions(knotvector) weights = [1, 1, 2] rational.weights = weights @@ -741,7 +742,7 @@ def test_quarter_circle_standard(self): @pytest.mark.dependency(depends=["TestRational::test_quarter_circle_standard"]) def test_quarter_circle_symmetric(self): knotvector = [0, 0, 0, 1, 1, 1] - rational = Function(knotvector) + rational = BasisFunctions(knotvector) weights = [2, np.sqrt(2), 2] rational.weights = weights @@ -788,9 +789,9 @@ def test_print(self): vector_spline = GeneratorKnotVector.uniform(3, 5) vector_rational = copy(vector_spline) weights = np.random.uniform(1, 2, 5) - bezier = Function(vector_bezier) - spline = Function(vector_spline) - rational = Function(vector_rational) + bezier = BasisFunctions(vector_bezier) + spline = BasisFunctions(vector_spline) + rational = BasisFunctions(vector_rational) rational.weights = weights bezier.__str__() @@ -804,7 +805,7 @@ def test_print(self): @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_creation"]) def test_specific_cases(self): - bezier = Function([0, 0, 1, 1]) + bezier = BasisFunctions([0, 0, 1, 1]) assert bezier != 1 assert bezier != "Asd" assert bezier != [0, 0, 1, 1] @@ -816,7 +817,7 @@ def test_specific_cases(self): @pytest.mark.timeout(5) @pytest.mark.dependency(depends=["TestBezier::test_creation"]) def test_fail_getitem_index(self): - bezier = Function([0, 0, 1, 1]) + bezier = BasisFunctions([0, 0, 1, 1]) with pytest.raises(IndexError): bezier[0, -1] with pytest.raises(IndexError): @@ -834,7 +835,7 @@ def test_fail_getitem_index(self): def test_fractions(self): from fractions import Fraction as frac - bezier = Function([frac(0), frac(0), frac(1), frac(1)]) + bezier = BasisFunctions([frac(0), frac(0), frac(1), frac(1)]) assert type(bezier[0](0)) is frac assert type(bezier[1](0)) is frac diff --git a/tests/responsive/test_beziercurve.py b/tests/responsive/test_beziercurve.py index 79041be..2c258f2 100644 --- a/tests/responsive/test_beziercurve.py +++ b/tests/responsive/test_beziercurve.py @@ -14,7 +14,7 @@ @pytest.mark.dependency( depends=[ "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_basis_functions.py::test_end", ], scope="session", ) diff --git a/tests/responsive/test_rationalcurve.py b/tests/responsive/test_rationalcurve.py index ae84c41..ded9c9b 100644 --- a/tests/responsive/test_rationalcurve.py +++ b/tests/responsive/test_rationalcurve.py @@ -13,7 +13,7 @@ @pytest.mark.dependency( depends=[ "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", "tests/responsive/test_splinecurve.py::test_end", ], diff --git a/tests/responsive/test_splinecurve.py b/tests/responsive/test_splinecurve.py index 9eb5c8f..0b00679 100644 --- a/tests/responsive/test_splinecurve.py +++ b/tests/responsive/test_splinecurve.py @@ -11,7 +11,7 @@ @pytest.mark.dependency( depends=[ "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_functions.py::test_end", + "tests/responsive/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", ], scope="session", From 1a41e7e7182544926060effe86f10f794159c5bf Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 4 Jul 2025 21:38:13 +0200 Subject: [PATCH 084/116] refactor: move KnotVector and BasisFunction one folder up --- src/pynurbs/__init__.py | 3 ++- src/pynurbs/{responsive => }/basis_functions.py | 2 +- src/pynurbs/{responsive => }/knotspace.py | 6 +++--- src/pynurbs/operations/calculus.py | 2 +- src/pynurbs/responsive/__init__.py | 2 -- src/pynurbs/responsive/curves.py | 2 +- tests/operations/test_advanced.py | 4 ++-- tests/operations/test_calculus.py | 6 +++--- tests/operations/test_customstruc.py | 8 ++++---- tests/operations/test_fitting.py | 6 +++--- tests/responsive/test_beziercurve.py | 6 +++--- tests/responsive/test_rationalcurve.py | 6 +++--- tests/responsive/test_splinecurve.py | 6 +++--- tests/{responsive => }/test_basis_functions.py | 6 +++--- tests/{responsive => }/test_knotspace.py | 2 +- 15 files changed, 33 insertions(+), 34 deletions(-) rename src/pynurbs/{responsive => }/basis_functions.py (99%) rename src/pynurbs/{responsive => }/knotspace.py (99%) rename tests/{responsive => }/test_basis_functions.py (99%) rename tests/{responsive => }/test_knotspace.py (99%) diff --git a/src/pynurbs/__init__.py b/src/pynurbs/__init__.py index bc49191..9387db0 100644 --- a/src/pynurbs/__init__.py +++ b/src/pynurbs/__init__.py @@ -1,6 +1,7 @@ +from .basis_functions import BasisFunctions +from .knotspace import GeneratorKnotVector, KnotVector from .operations.advanced import Intersection, Projection from .operations.calculus import Derivate, Integrate -from .responsive import BasisFunctions, Curve, GeneratorKnotVector, KnotVector __version__ = "1.1.0" diff --git a/src/pynurbs/responsive/basis_functions.py b/src/pynurbs/basis_functions.py similarity index 99% rename from src/pynurbs/responsive/basis_functions.py rename to src/pynurbs/basis_functions.py index 07444e0..4400259 100644 --- a/src/pynurbs/responsive/basis_functions.py +++ b/src/pynurbs/basis_functions.py @@ -6,8 +6,8 @@ from pynurbs.core.spline_basis import ImmutableSplineBasis -from ..operations.tools import vectorize from .knotspace import KnotVector +from .operations.tools import vectorize class BaseFunction: diff --git a/src/pynurbs/responsive/knotspace.py b/src/pynurbs/knotspace.py similarity index 99% rename from src/pynurbs/responsive/knotspace.py rename to src/pynurbs/knotspace.py index bc42325..dcdbd3f 100644 --- a/src/pynurbs/responsive/knotspace.py +++ b/src/pynurbs/knotspace.py @@ -13,8 +13,8 @@ import numpy as np -from ..core import ImmutableKnotVector -from ..operations.knotvector import ( +from .core import ImmutableKnotVector +from .operations.knotvector import ( decrease_degree, increase_degree, insert_knots, @@ -23,7 +23,7 @@ split_knotvector, union_knotvectors, ) -from ..operations.tools import vectorize +from .operations.tools import vectorize class KnotVector: diff --git a/src/pynurbs/operations/calculus.py b/src/pynurbs/operations/calculus.py index d995cb1..ea28ce2 100644 --- a/src/pynurbs/operations/calculus.py +++ b/src/pynurbs/operations/calculus.py @@ -4,8 +4,8 @@ import numpy as np from ..core.custom_math import IntegratorArray, NodeSample +from ..knotspace import KnotVector from ..responsive.curves import Curve -from ..responsive.knotspace import KnotVector from . import heavy diff --git a/src/pynurbs/responsive/__init__.py b/src/pynurbs/responsive/__init__.py index 24746a7..2a792da 100644 --- a/src/pynurbs/responsive/__init__.py +++ b/src/pynurbs/responsive/__init__.py @@ -1,3 +1 @@ from .curves import Curve -from .basis_functions import BasisFunctions -from .knotspace import GeneratorKnotVector, KnotVector diff --git a/src/pynurbs/responsive/curves.py b/src/pynurbs/responsive/curves.py index 3ec9484..450cee6 100644 --- a/src/pynurbs/responsive/curves.py +++ b/src/pynurbs/responsive/curves.py @@ -8,6 +8,7 @@ from ..core.custom_math import isnumber, number_type, supports_linear_operation from ..core.spline_basis import ImmutableSplineBasis +from ..knotspace import KnotVector from ..operations import heavy from ..operations.knotvector import ( decrease_degree, @@ -16,7 +17,6 @@ remove_knots, ) from ..operations.least_square import fit_function, func2func, spline2spline -from .knotspace import KnotVector def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: diff --git a/tests/operations/test_advanced.py b/tests/operations/test_advanced.py index 8dc96df..9427443 100644 --- a/tests/operations/test_advanced.py +++ b/tests/operations/test_advanced.py @@ -13,8 +13,8 @@ @pytest.mark.order(42) @pytest.mark.dependency( depends=[ - "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_basis_functions.py::test_end", + "tests/test_knotspace.py::test_end", + "tests/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", "tests/responsive/test_splinecurve.py::test_end", "tests/operations/test_calculus.py::test_end", diff --git a/tests/operations/test_calculus.py b/tests/operations/test_calculus.py index 52b2174..55eec48 100644 --- a/tests/operations/test_calculus.py +++ b/tests/operations/test_calculus.py @@ -8,16 +8,16 @@ import numpy as np import pytest +from pynurbs.knotspace import GeneratorKnotVector, KnotVector from pynurbs.operations.calculus import Derivate, Integrate from pynurbs.responsive.curves import Curve -from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector @pytest.mark.order(41) @pytest.mark.dependency( depends=[ - "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_basis_functions.py::test_end", + "tests/test_knotspace.py::test_end", + "tests/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", "tests/responsive/test_splinecurve.py::test_end", "tests/responsive/test_rationalcurve.py::test_end", diff --git a/tests/operations/test_customstruc.py b/tests/operations/test_customstruc.py index cb93170..5dfa246 100644 --- a/tests/operations/test_customstruc.py +++ b/tests/operations/test_customstruc.py @@ -13,8 +13,8 @@ import pytest -from pynurbs.responsive.basis_functions import BasisFunctions -from pynurbs.responsive.knotspace import KnotVector +from pynurbs.basis_functions import BasisFunctions +from pynurbs.knotspace import KnotVector class CustomFloat: @@ -112,8 +112,8 @@ def __rmul__(self, other: CustomFloat): @pytest.mark.order(41) @pytest.mark.dependency( depends=[ - "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_basis_functions.py::test_end", + "tests/test_knotspace.py::test_end", + "tests/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", "tests/responsive/test_splinecurve.py::test_end", "tests/responsive/test_rationalcurve.py::test_end", diff --git a/tests/operations/test_fitting.py b/tests/operations/test_fitting.py index 970e70b..c771606 100644 --- a/tests/operations/test_fitting.py +++ b/tests/operations/test_fitting.py @@ -1,15 +1,15 @@ import numpy as np import pytest +from pynurbs.knotspace import GeneratorKnotVector from pynurbs.responsive.curves import Curve -from pynurbs.responsive.knotspace import GeneratorKnotVector @pytest.mark.order(41) @pytest.mark.dependency( depends=[ - "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_basis_functions.py::test_end", + "tests/test_knotspace.py::test_end", + "tests/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", "tests/responsive/test_splinecurve.py::test_end", "tests/responsive/test_rationalcurve.py::test_end", diff --git a/tests/responsive/test_beziercurve.py b/tests/responsive/test_beziercurve.py index 2c258f2..019682a 100644 --- a/tests/responsive/test_beziercurve.py +++ b/tests/responsive/test_beziercurve.py @@ -6,15 +6,15 @@ import numpy as np import pytest +from pynurbs.knotspace import GeneratorKnotVector, KnotVector from pynurbs.responsive.curves import Curve -from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector @pytest.mark.order(33) @pytest.mark.dependency( depends=[ - "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_basis_functions.py::test_end", + "tests/test_knotspace.py::test_end", + "tests/test_basis_functions.py::test_end", ], scope="session", ) diff --git a/tests/responsive/test_rationalcurve.py b/tests/responsive/test_rationalcurve.py index ded9c9b..d82f7ee 100644 --- a/tests/responsive/test_rationalcurve.py +++ b/tests/responsive/test_rationalcurve.py @@ -5,15 +5,15 @@ import numpy as np import pytest +from pynurbs.knotspace import GeneratorKnotVector from pynurbs.responsive.curves import Curve -from pynurbs.responsive.knotspace import GeneratorKnotVector @pytest.mark.order(35) @pytest.mark.dependency( depends=[ - "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_basis_functions.py::test_end", + "tests/test_knotspace.py::test_end", + "tests/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", "tests/responsive/test_splinecurve.py::test_end", ], diff --git a/tests/responsive/test_splinecurve.py b/tests/responsive/test_splinecurve.py index 0b00679..9d9d1df 100644 --- a/tests/responsive/test_splinecurve.py +++ b/tests/responsive/test_splinecurve.py @@ -3,15 +3,15 @@ import numpy as np import pytest +from pynurbs.knotspace import GeneratorKnotVector, KnotVector from pynurbs.responsive.curves import Curve -from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector @pytest.mark.order(34) @pytest.mark.dependency( depends=[ - "tests/responsive/test_knotspace.py::test_end", - "tests/responsive/test_basis_functions.py::test_end", + "tests/test_knotspace.py::test_end", + "tests/test_basis_functions.py::test_end", "tests/responsive/test_beziercurve.py::test_end", ], scope="session", diff --git a/tests/responsive/test_basis_functions.py b/tests/test_basis_functions.py similarity index 99% rename from tests/responsive/test_basis_functions.py rename to tests/test_basis_functions.py index 8a69791..77fb61d 100644 --- a/tests/responsive/test_basis_functions.py +++ b/tests/test_basis_functions.py @@ -3,16 +3,16 @@ import numpy as np import pytest +from pynurbs.basis_functions import BasisFunctions from pynurbs.core.custom_math import binom -from pynurbs.responsive.basis_functions import BasisFunctions -from pynurbs.responsive.knotspace import GeneratorKnotVector +from pynurbs.knotspace import GeneratorKnotVector @pytest.mark.order(32) @pytest.mark.dependency( depends=[ "tests/core/test_spline_basis.py::test_all", - "tests/responsive/test_knotspace.py::test_end", + "tests/test_knotspace.py::test_end", ], scope="session", ) diff --git a/tests/responsive/test_knotspace.py b/tests/test_knotspace.py similarity index 99% rename from tests/responsive/test_knotspace.py rename to tests/test_knotspace.py index f5d5a7b..e50c0b3 100644 --- a/tests/responsive/test_knotspace.py +++ b/tests/test_knotspace.py @@ -4,7 +4,7 @@ import numpy as np import pytest -from pynurbs.responsive.knotspace import GeneratorKnotVector, KnotVector +from pynurbs.knotspace import GeneratorKnotVector, KnotVector @pytest.mark.order(30) From b8f6a8b5d16a0bcc3ab035afa5c646b32fa3f2fd Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 4 Jul 2025 21:42:27 +0200 Subject: [PATCH 085/116] refactor: rename files to put all curves in one place --- src/pynurbs/{responsive => curves}/__init__.py | 0 src/pynurbs/{responsive => curves}/curves.py | 0 src/pynurbs/operations/advanced.py | 2 +- src/pynurbs/operations/calculus.py | 2 +- tests/{responsive => curves}/__init__.py | 0 .../test_beziercurve.py => curves/test_bezier.py} | 2 +- .../test_rationalcurve.py => curves/test_rational.py} | 6 +++--- .../test_splinecurve.py => curves/test_spline.py} | 4 ++-- tests/operations/test_advanced.py | 6 +++--- tests/operations/test_calculus.py | 8 ++++---- tests/operations/test_customstruc.py | 6 +++--- tests/operations/test_fitting.py | 8 ++++---- 12 files changed, 22 insertions(+), 22 deletions(-) rename src/pynurbs/{responsive => curves}/__init__.py (100%) rename src/pynurbs/{responsive => curves}/curves.py (100%) rename tests/{responsive => curves}/__init__.py (100%) rename tests/{responsive/test_beziercurve.py => curves/test_bezier.py} (99%) rename tests/{responsive/test_rationalcurve.py => curves/test_rational.py} (99%) rename tests/{responsive/test_splinecurve.py => curves/test_spline.py} (99%) diff --git a/src/pynurbs/responsive/__init__.py b/src/pynurbs/curves/__init__.py similarity index 100% rename from src/pynurbs/responsive/__init__.py rename to src/pynurbs/curves/__init__.py diff --git a/src/pynurbs/responsive/curves.py b/src/pynurbs/curves/curves.py similarity index 100% rename from src/pynurbs/responsive/curves.py rename to src/pynurbs/curves/curves.py diff --git a/src/pynurbs/operations/advanced.py b/src/pynurbs/operations/advanced.py index aa69667..92d20c6 100644 --- a/src/pynurbs/operations/advanced.py +++ b/src/pynurbs/operations/advanced.py @@ -7,7 +7,7 @@ import numpy as np -from ..responsive.curves import Curve +from ..curves.curves import Curve from . import heavy from .calculus import Derivate diff --git a/src/pynurbs/operations/calculus.py b/src/pynurbs/operations/calculus.py index ea28ce2..ab98013 100644 --- a/src/pynurbs/operations/calculus.py +++ b/src/pynurbs/operations/calculus.py @@ -4,8 +4,8 @@ import numpy as np from ..core.custom_math import IntegratorArray, NodeSample +from ..curves.curves import Curve from ..knotspace import KnotVector -from ..responsive.curves import Curve from . import heavy diff --git a/tests/responsive/__init__.py b/tests/curves/__init__.py similarity index 100% rename from tests/responsive/__init__.py rename to tests/curves/__init__.py diff --git a/tests/responsive/test_beziercurve.py b/tests/curves/test_bezier.py similarity index 99% rename from tests/responsive/test_beziercurve.py rename to tests/curves/test_bezier.py index 019682a..4c5edc9 100644 --- a/tests/responsive/test_beziercurve.py +++ b/tests/curves/test_bezier.py @@ -6,8 +6,8 @@ import numpy as np import pytest +from pynurbs.curves.curves import Curve from pynurbs.knotspace import GeneratorKnotVector, KnotVector -from pynurbs.responsive.curves import Curve @pytest.mark.order(33) diff --git a/tests/responsive/test_rationalcurve.py b/tests/curves/test_rational.py similarity index 99% rename from tests/responsive/test_rationalcurve.py rename to tests/curves/test_rational.py index d82f7ee..684795e 100644 --- a/tests/responsive/test_rationalcurve.py +++ b/tests/curves/test_rational.py @@ -5,8 +5,8 @@ import numpy as np import pytest +from pynurbs.curves.curves import Curve from pynurbs.knotspace import GeneratorKnotVector -from pynurbs.responsive.curves import Curve @pytest.mark.order(35) @@ -14,8 +14,8 @@ depends=[ "tests/test_knotspace.py::test_end", "tests/test_basis_functions.py::test_end", - "tests/responsive/test_beziercurve.py::test_end", - "tests/responsive/test_splinecurve.py::test_end", + "tests/curves/test_bezier.py::test_end", + "tests/curves/test_spline.py::test_end", ], scope="session", ) diff --git a/tests/responsive/test_splinecurve.py b/tests/curves/test_spline.py similarity index 99% rename from tests/responsive/test_splinecurve.py rename to tests/curves/test_spline.py index 9d9d1df..5b19721 100644 --- a/tests/responsive/test_splinecurve.py +++ b/tests/curves/test_spline.py @@ -3,8 +3,8 @@ import numpy as np import pytest +from pynurbs.curves.curves import Curve from pynurbs.knotspace import GeneratorKnotVector, KnotVector -from pynurbs.responsive.curves import Curve @pytest.mark.order(34) @@ -12,7 +12,7 @@ depends=[ "tests/test_knotspace.py::test_end", "tests/test_basis_functions.py::test_end", - "tests/responsive/test_beziercurve.py::test_end", + "tests/curves/test_bezier.py::test_end", ], scope="session", ) diff --git a/tests/operations/test_advanced.py b/tests/operations/test_advanced.py index 9427443..48ef1bd 100644 --- a/tests/operations/test_advanced.py +++ b/tests/operations/test_advanced.py @@ -6,8 +6,8 @@ import numpy as np import pytest +from pynurbs.curves.curves import Curve from pynurbs.operations.advanced import Intersection, Projection -from pynurbs.responsive.curves import Curve @pytest.mark.order(42) @@ -15,8 +15,8 @@ depends=[ "tests/test_knotspace.py::test_end", "tests/test_basis_functions.py::test_end", - "tests/responsive/test_beziercurve.py::test_end", - "tests/responsive/test_splinecurve.py::test_end", + "tests/curves/test_bezier.py::test_end", + "tests/curves/test_spline.py::test_end", "tests/operations/test_calculus.py::test_end", ], scope="session", diff --git a/tests/operations/test_calculus.py b/tests/operations/test_calculus.py index 55eec48..19f37e8 100644 --- a/tests/operations/test_calculus.py +++ b/tests/operations/test_calculus.py @@ -8,9 +8,9 @@ import numpy as np import pytest +from pynurbs.curves.curves import Curve from pynurbs.knotspace import GeneratorKnotVector, KnotVector from pynurbs.operations.calculus import Derivate, Integrate -from pynurbs.responsive.curves import Curve @pytest.mark.order(41) @@ -18,9 +18,9 @@ depends=[ "tests/test_knotspace.py::test_end", "tests/test_basis_functions.py::test_end", - "tests/responsive/test_beziercurve.py::test_end", - "tests/responsive/test_splinecurve.py::test_end", - "tests/responsive/test_rationalcurve.py::test_end", + "tests/curves/test_bezier.py::test_end", + "tests/curves/test_spline.py::test_end", + "tests/curves/test_rational.py::test_end", ], scope="session", ) diff --git a/tests/operations/test_customstruc.py b/tests/operations/test_customstruc.py index 5dfa246..cfb5251 100644 --- a/tests/operations/test_customstruc.py +++ b/tests/operations/test_customstruc.py @@ -114,9 +114,9 @@ def __rmul__(self, other: CustomFloat): depends=[ "tests/test_knotspace.py::test_end", "tests/test_basis_functions.py::test_end", - "tests/responsive/test_beziercurve.py::test_end", - "tests/responsive/test_splinecurve.py::test_end", - "tests/responsive/test_rationalcurve.py::test_end", + "tests/curves/test_bezier.py::test_end", + "tests/curves/test_spline.py::test_end", + "tests/curves/test_rational.py::test_end", ], scope="session", ) diff --git a/tests/operations/test_fitting.py b/tests/operations/test_fitting.py index c771606..5100e0d 100644 --- a/tests/operations/test_fitting.py +++ b/tests/operations/test_fitting.py @@ -1,8 +1,8 @@ import numpy as np import pytest +from pynurbs.curves.curves import Curve from pynurbs.knotspace import GeneratorKnotVector -from pynurbs.responsive.curves import Curve @pytest.mark.order(41) @@ -10,9 +10,9 @@ depends=[ "tests/test_knotspace.py::test_end", "tests/test_basis_functions.py::test_end", - "tests/responsive/test_beziercurve.py::test_end", - "tests/responsive/test_splinecurve.py::test_end", - "tests/responsive/test_rationalcurve.py::test_end", + "tests/curves/test_bezier.py::test_end", + "tests/curves/test_spline.py::test_end", + "tests/curves/test_rational.py::test_end", ], scope="session", ) From ec4879f3efe287202b03e066c0aefa3bcc777b02 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 4 Jul 2025 21:49:50 +0200 Subject: [PATCH 086/116] refactor: divide curves file into two --- src/pynurbs/curves/base.py | 562 +++++++++++++++++++++++++++++++++++ src/pynurbs/curves/curves.py | 556 +--------------------------------- 2 files changed, 564 insertions(+), 554 deletions(-) create mode 100644 src/pynurbs/curves/base.py diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py new file mode 100644 index 0000000..22e1d3f --- /dev/null +++ b/src/pynurbs/curves/base.py @@ -0,0 +1,562 @@ +from __future__ import annotations + +from copy import copy +from typing import Optional, Tuple, Union + +import numpy as np + +from ..core.custom_math import isnumber, supports_linear_operation +from ..knotspace import KnotVector +from ..operations import heavy + + +def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: + """ + Computes recursively a norm of an object. + If L = 0, it means infinity norm + If L = 1, it means abs norm + If L = 2, it means euclidean norm + """ + try: + soma = 0 + for item in object: + norma = norm(item, L) + soma = max(soma, norma) if L == 0 else soma + norma**L + return soma if L == 0 else soma ** (1 / L) + except TypeError: + return abs(object) + + +class BaseCurve: + def __init__(self, knotvector: KnotVector): + self.__ctrlpoints = None + self.__weights = None + if not isinstance(knotvector, KnotVector): + knotvector = KnotVector(knotvector) + self.__knotvector = knotvector + + def __call__(self, nodes: np.ndarray) -> np.ndarray: + return self.eval(nodes) + + def __eq__(self, other: object) -> bool: + if type(self) is not type(other): + return False + if self.knotvector[0] != other.knotvector[0]: + return False + if self.knotvector[-1] != other.knotvector[-1]: + return False + if (self.ctrlpoints is None) ^ (other.ctrlpoints is None): + return False + newknotvec = self.knotvector | other.knotvector + selfcopy = copy(self) + selfcopy.knotvector = newknotvec + othercopy = copy(other) + othercopy.knotvector = newknotvec + for poi, qoi in zip(self.ctrlpoints, othercopy.ctrlpoints): + if norm(poi - qoi) > 1e-9: + return False + return True + + def __ne__(self, obj: object): + return not self.__eq__(obj) + + def __neg__(self): + if self.ctrlpoints is None: + raise ValueError + newcurve = copy(self) + newctrlpoints = [-1 * ctrlpt for ctrlpt in newcurve.ctrlpoints] + newcurve.ctrlpoints = newctrlpoints + return newcurve + + def __add__(self, other: object): + if self.ctrlpoints is None: + raise ValueError + if not isinstance(other, self.__class__): + copied = copy(self) + copied.ctrlpoints = [other + point for point in self.ctrlpoints] + return copied + if self.knotvector.limits != other.knotvector.limits: + raise ValueError + if self.weights is None and other.weights is None: + vecta, vectb = tuple(self.knotvector), tuple(other.knotvector) + matra, matrb = heavy.MathOperations.add_spline_curve(vecta, vectb) + curve = Curve(self.knotvector | other.knotvector) + ctrlpoints = np.array(matra) @ self.ctrlpoints + ctrlpoints += np.array(matrb) @ other.ctrlpoints + curve.ctrlpoints = ctrlpoints + return curve + numa, dena = self.fraction() + numb, denb = other.fraction() + return (numa * denb + numb * dena) / (dena * denb) + + def __radd__(self, other: object): + return self.__add__(other) + + def __sub__(self, other: object): + return self + (-other) + + def __rsub__(self, other: object): + return other + (-self) + + def __mul__(self, other: object): + if self.ctrlpoints is None: + raise ValueError + if not isinstance(other, self.__class__): + copied = copy(self) + copied.ctrlpoints = [point * other for point in copied.ctrlpoints] + return copied + if self.knotvector.limits != other.knotvector.limits: + raise ValueError + if self.weights is None and other.weights is None: + vecta, vectb = tuple(self.knotvector), tuple(other.knotvector) + vectmul = heavy.MathOperations.knotvector_mul(vecta, vectb) + matrix3d = heavy.MathOperations.mul_spline_curve(vecta, vectb) + ctrlpoints = np.tensordot( + np.moveaxis(self.ctrlpoints, 0, -1), matrix3d, axes=1 + ) + ctrlpoints = ctrlpoints @ other.ctrlpoints + curve = Curve(vectmul, ctrlpoints) + return curve + numa, dena = self.fraction() + numb, denb = other.fraction() + return (numa * numb) / (dena * denb) + + def __rmul__(self, other: object): + if self.ctrlpoints is None: + raise ValueError + assert not isinstance(other, self.__class__) + copied = copy(self) + copied.ctrlpoints = [other * point for point in copied.ctrlpoints] + return copied + + def __matmul__(self, other: object): + if self.ctrlpoints is None: + raise ValueError + if not isinstance(other, self.__class__): + copied = copy(self) + copied.ctrlpoints = [point @ other for point in copied.ctrlpoints] + return copied + if self.knotvector.limits != other.knotvector.limits: + raise ValueError + if self.weights is None and other.weights is None: + vecta, vectb = tuple(self.knotvector), tuple(other.knotvector) + vectmul = heavy.MathOperations.knotvector_mul(vecta, vectb) + matrix3d = heavy.MathOperations.mul_spline_curve(vecta, vectb) + matrix2d = [ + [pt0 @ pt1 for pt0 in self.ctrlpoints] for pt1 in other.ctrlpoints + ] + matrix3d = np.array(matrix3d) + matrix2d = np.array(matrix2d) + newctrlpts = [0] * matrix3d.shape[1] + for i in range(matrix3d.shape[1]): + newctrlpt = np.tensordot(matrix3d[:, i, :], matrix2d, axes=2) + newctrlpts[i] = newctrlpt + ctrlpoints = newctrlpts + curve = Curve(vectmul, ctrlpoints) + return curve + numa, dena = self.fraction() + numb, denb = other.fraction() + return (numa @ numb) / (dena * denb) + + def __rmatmul__(self, other: object): + if self.ctrlpoints is None: + raise ValueError + assert not isinstance(other, self.__class__) + copied = copy(self) + copied.ctrlpoints = [other @ point for point in copied.ctrlpoints] + return copied + + def __truediv__(self, other: object): + if self.ctrlpoints is None: + raise ValueError + if not isinstance(other, self.__class__): + copied = copy(self) + copied.ctrlpoints = [point / other for point in copied.ctrlpoints] + return copied + if self.knotvector.limits != other.knotvector.limits: + raise ValueError + if self.weights is None and other.weights is None: + copyse = copy(self) + copyot = copy(other) + vectora, vectorb = tuple(copyse.knotvector), tuple(copyot.knotvector) + vectorc = tuple(copyse.knotvector | copyot.knotvector) + transctrlpts = heavy.Operations.matrix_transformation(vectora, vectorc) + transweights = heavy.Operations.matrix_transformation(vectorb, vectorc) + weights = np.dot(transweights, copyot.ctrlpoints) + ctrlpts = np.dot(transctrlpts, copyse.ctrlpoints) + ctrlpts = [pti / wi for pti, wi in zip(ctrlpts, weights)] + return self.__class__(vectorc, ctrlpts, weights) + + numa, dena = self.fraction() + numb, denb = other.fraction() + return (numa * denb) / (dena * numb) + + def __rtruediv__(self, other: object): + """ + Example: 1/curve + """ + if self.ctrlpoints is None: + raise ValueError + assert not isinstance(other, self.__class__) + for point in self.ctrlpoints: + float(point) + if self.weights is None: + newcurve = self.__class__(tuple(self.knotvector)) + newcurve.weights = [copy(point) for point in self.ctrlpoints] + newcurve.ctrlpoints = [1 / w for w in newcurve.weights] + return newcurve + num, den = self.fraction() + frac = den / num + return other * frac + + def __or__(self, other: object): + umaxleft = self.knotvector[-1] + uminright = other.knotvector[0] + if umaxleft != uminright: + error_msg = f"max(Uleft) = {umaxleft} != {uminright} = min(Uright)" + raise ValueError(error_msg) + othercopy = copy(other) + selfcopy = copy(self) + maxdegree = max(self.degree, other.degree) + selfcopy.degree = maxdegree + othercopy.degree = maxdegree + npts0 = selfcopy.npts + npts1 = othercopy.npts + newknotvector = [0] * (maxdegree + npts0 + npts1 + 1) + newknotvector[:npts0] = selfcopy.knotvector[:npts0] + newknotvector[npts0:] = othercopy.knotvector[1:] + newknotvector = KnotVector(newknotvector) + newctrlpoints = [0] * (npts0 + npts1 - 1) + newctrlpoints[:npts0] = selfcopy.ctrlpoints[:npts0] + newctrlpoints[npts0:] = othercopy.ctrlpoints[1:] + newcurve = self.__class__(newknotvector, newctrlpoints) + newcurve.knot_clean([umaxleft]) + return newcurve + + @property + def knotvector(self): + """Knot Vector + + :getter: Returns the knotvector of the curve + :setter: Sets the knotvector of the curve + :type: KnotVector + + Example use + ----------- + + >>> from pynurbs import Curve + >>> curve = Curve([0., 0., 1., 1.]) + >>> curve.knotvector + (0., 0., 1., 1.) + >>> curve.knotvector = [0, 0, 0, 1, 1, 1] + >>> curve.knotvector + (0, 0, 0, 1, 1, 1) + + """ + + return self.__knotvector + + @property + def degree(self): + """Polynomial degree of curve + + :getter: Returns the degree of the curve + :setter: Sets the degree of the curve, by increasing or decreasing + :type: int + + Example use + ----------- + + >>> from pynurbs import Curve + >>> curve = Curve([0., 0., 1., 1.], [1, 2]) + >>> curve.degree = 3 # From 1 to 3 + >>> print(curve.ctrlpoints) + (1.0, 1.33, 1.67, 2.0) + >>> curve.degree -= 1 + >>> print(curve.ctrlpoints) + (1.0, 1.5, 2.0) + + """ + + return self.knotvector.degree + + @property + def npts(self): + """Number of control points + + :getter: Returns the number of control points of the curve + :type: int + + Example use + ----------- + + >>> from pynurbs import Curve + >>> curve = Curve([0., 0., 1., 1.], [1, 2]) + >>> curve.npts + 2 + >>> curve = Curve([0., 0., 0.5, 1., 1.], [1, 2, 1]) + >>> curve.npts + 3 + + """ + return self.knotvector.npts + + @property + def knots(self): + return self.knotvector.knots + + @property + def weights(self): + """Weights of rational curve + + If weights is None, the curve is a spline + + :getter: Returns the weights of curve + :setter: Sets the weights for a rational curve + :type: None | tuple[float] + + Example use + ----------- + + >>> from pynurbs import Curve + >>> curve = Curve([0., 0., 1., 1.], [1, 2]) + >>> curve.weights = [1, 2] + (1, 2) + >>> curve = Curve([0., 0., 0.5, 1., 1.], [1, 2, 1]) + >>> curve.weights = [1, 2, 1] + >>> curve.weights + (1, 2, 1) + + """ + if self.__weights is None: + return None + return tuple(self.__weights) + + @property + def ctrlpoints(self): + """Control points of the curve + + :getter: Returns the control points of the curve + :setter: Sets the control points of the curve + :type: None | tuple[Any] + + Example use + ----------- + + >>> from pynurbs import Curve + >>> curve = Curve([0., 0., 1., 1.]) + >>> curve.ctrlpoints = [1, 2] + >>> curve.ctrlpoints + (1, 2) + >>> curve = Curve([0., 0., 0.5, 1., 1.]) + >>> curve.ctrlpoints = [1, 2, 1] + >>> curve.ctrlpoints + (1, 2, 1) + + """ + if self.__ctrlpoints is None: + return None + return tuple(self.__ctrlpoints) + + @knotvector.setter + def knotvector(self, value: KnotVector): + if not isinstance(value, KnotVector): + value = KnotVector(value) + self.update(value) + + @degree.setter + def degree(self, value: int): + if not isinstance(value, int) or value < 0: + raise ValueError(f"Cannot set degree {value}") + times = value - self.degree + if times == 0: + return + if times > 0: + return self.degree_increase(times) + return self.degree_decrease(-times) + + @weights.setter + def weights(self, value: Tuple[float]): + if value is None: + self.__weights = None + return + if not all(map(isnumber, value)): + raise ValueError + if not all(number > 0 for number in value): + raise ValueError + + # Verify if there's roots + vector = tuple(self.knotvector) + roots = heavy.find_roots(vector, value) + if roots: + error_msg = f"Zero division at nodes {roots}" + raise ValueError(error_msg) + self.__weights = tuple(value) + + @ctrlpoints.setter + def ctrlpoints(self, newpoints: np.ndarray): + if newpoints is None: + self.__ctrlpoints = None + return + if not all(map(supports_linear_operation, newpoints)): + raise ValueError + for point in newpoints: # Verify if operations are valid for each node + for knot in self.knotvector.knots: + knot * point + for otherpoint in newpoints: + point + otherpoint # Verify if we can sum every point, same type + + if len(newpoints) != self.npts: + error_msg = f"The number of control points ({len(newpoints)}) must be " + error_msg += f"the same as npts of KnotVector ({self.knotvector.npts})\n" + error_msg += f" knotvector.npts = {self.npts}" + error_msg += f" len(ctrlpoints) = {len(newpoints)}" + raise ValueError(error_msg) + + self.__ctrlpoints = tuple(newpoints) + + def __copy__(self) -> Curve: + return self.__deepcopy__(None) + + def __deepcopy__(self, memo) -> Curve: + knotvector = copy(self.knotvector) + curve = self.__class__(knotvector) + if self.ctrlpoints is not None: + curve.ctrlpoints = [copy(point) for point in self.ctrlpoints] + if self.weights is not None: + curve.weights = [copy(weight) for weight in self.weights] + return curve + + def fraction(self) -> Tuple[BaseCurve]: + """Returns the current curve ``C`` in the form ``A``/``B`` + + ``A`` and ``B`` are bsplines of same degree as ``C`` + and same number of points. + + If ``C`` is already a bspline, then ``B = 1`` + + :return: The pair ``(A, B)`` + :rtype: tuple[curve] + + Example use + ----------- + + >>> from pynurbs import Curve + >>> curve = Curve([0, 0, 0.5, 1, 1]) + >>> curve.ctrlpoints = [2, 4, 2] + >>> curve.weights = [1, 3, 2] + >>> A, B = curve.fraction() + >>> print(A) + Spline curve of degree 1 and 3 control points + KnotVector = (0, 0, 0.5, 1, 1) + ControlPoints = [2, 12, 4] + >>> print(B) + Spline curve of degree 1 and 3 control points + KnotVector = (0, 0, 0.5, 1, 1) + ControlPoints = [1, 3, 2] + + """ + if self.weights is None: + numerator = copy(self) + return numerator, 1 + ctrlpoints = [copy(point) for point in self.ctrlpoints] + numerator = self.__class__(copy(self.knotvector)) + denominator = self.__class__(copy(self.knotvector)) + numerator.ctrlpoints = [wi * pt for wi, pt in zip(self.weights, ctrlpoints)] + denominator.ctrlpoints = self.weights + return numerator, denominator + + def update( + self, + newknotvector: KnotVector, + tolerance: Optional[float] = 1e-9, + nodes: Optional[tuple[float]] = None, + ): + """Update the knotvector to newknotvector + + This function compute the new control points and + new weights such the new curve is near the old curve + + If the error is bigger than tolerance, then raises a ValueError + + If ``nodes`` are given, the new curve will fit on these ``nodes`` + + Example use + ----------- + + >>> from pynurbs import Curve + >>> curve = Curve([0, 0, 1, 1], [1, 2]) + >>> curve.update([0, 0, 0.5, 1, 1]) # Insert knot [0.5] + >>> curve = Curve([0, 0, 1, 1], [1, 2]) + >>> curve.update([0, 0, 0, 1, 1, 1]) # Degree elevate + >>> curve = Curve([0, 0, 0.5, 1, 1], [1, 2, 1]) + >>> curve.update([0, 0, 1, 1], nodes = (0, 1)) # Remove knot [0.5] + + """ + if not isinstance(newknotvector, KnotVector): + newknotvector = KnotVector(newknotvector) + if newknotvector == self.knotvector: + return + if self.ctrlpoints is None: + self.__knotvector = newknotvector + return + if self.knotvector.limits != newknotvector.limits: + raise ValueError + temp_curve = self.__class__(newknotvector) + error = temp_curve.fit_curve(self, nodes) + if tolerance and error > tolerance: + error_msg = "Cannot update knotvector cause error is " + error_msg += f" {float(error):.2e} > {tolerance}" + raise ValueError(error_msg) + self.__knotvector = newknotvector + self.ctrlpoints = temp_curve.ctrlpoints + self.weights = temp_curve.weights + + def apply(self, newknotvector: KnotVector, matrix: Tuple[Tuple[float]]): + """Applies the linear transformation for every control point + + new ctrlpoints = matrix @ old ctrlpoints + new weights = matrix @ old weights + + Example use + ----------- + + >>> from pynurbs import Curve + >>> curve = Curve([0, 0, 0.5, 1, 1]) + >>> curve.ctrlpoints = [2, 4, 2] + >>> matrix = [(0, 1, 0), (-1, 0, 1), (2, -1, 0)] + >>> curve.apply(matrix) + >>> print(curve) + Spline curve of degree 1 and 3 control points + KnotVector = (0, 0, 0.5, 1, 1) + ControlPoints = [4, 0, 0] + + """ + if not isinstance(newknotvector, KnotVector): + newknotvector = KnotVector(newknotvector) + oldctrlpoints = self.ctrlpoints + oldweights = self.weights + if oldctrlpoints is None and oldweights is None: + self.knotvector = newknotvector + return + self.ctrlpoints = None + self.weights = None + self.knotvector = newknotvector + if oldweights is None: + self.ctrlpoints = np.dot(matrix, oldctrlpoints) + return + newweights = np.dot(matrix, oldweights) + self.weights = newweights + + if oldctrlpoints is not None: + oldctrlpoints = list(oldctrlpoints) + for i, weight in enumerate(oldweights): + oldctrlpoints[i] *= weight + newctrlpoints = [] + for i, line in enumerate(matrix): + newctrlpoints.append(0 * oldctrlpoints[0]) + for j, point in enumerate(oldctrlpoints): + newpoint = line[j] * point + newpoint /= self.weights[i] + newctrlpoints[i] += newpoint + self.ctrlpoints = newctrlpoints diff --git a/src/pynurbs/curves/curves.py b/src/pynurbs/curves/curves.py index 450cee6..59fdcd7 100644 --- a/src/pynurbs/curves/curves.py +++ b/src/pynurbs/curves/curves.py @@ -1,12 +1,11 @@ from __future__ import annotations -from copy import copy from fractions import Fraction from typing import Any, Callable, Optional, Tuple, Union import numpy as np -from ..core.custom_math import isnumber, number_type, supports_linear_operation +from ..core.custom_math import number_type from ..core.spline_basis import ImmutableSplineBasis from ..knotspace import KnotVector from ..operations import heavy @@ -17,558 +16,7 @@ remove_knots, ) from ..operations.least_square import fit_function, func2func, spline2spline - - -def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: - """ - Computes recursively a norm of an object. - If L = 0, it means infinity norm - If L = 1, it means abs norm - If L = 2, it means euclidean norm - """ - try: - soma = 0 - for item in object: - norma = norm(item, L) - soma = max(soma, norma) if L == 0 else soma + norma**L - return soma if L == 0 else soma ** (1 / L) - except TypeError: - return abs(object) - - -class BaseCurve: - def __init__(self, knotvector: KnotVector): - self.__ctrlpoints = None - self.__weights = None - if not isinstance(knotvector, KnotVector): - knotvector = KnotVector(knotvector) - self.__knotvector = knotvector - - def __call__(self, nodes: np.ndarray) -> np.ndarray: - return self.eval(nodes) - - def __eq__(self, other: object) -> bool: - if type(self) is not type(other): - return False - if self.knotvector[0] != other.knotvector[0]: - return False - if self.knotvector[-1] != other.knotvector[-1]: - return False - if (self.ctrlpoints is None) ^ (other.ctrlpoints is None): - return False - newknotvec = self.knotvector | other.knotvector - selfcopy = copy(self) - selfcopy.knotvector = newknotvec - othercopy = copy(other) - othercopy.knotvector = newknotvec - for poi, qoi in zip(self.ctrlpoints, othercopy.ctrlpoints): - if norm(poi - qoi) > 1e-9: - return False - return True - - def __ne__(self, obj: object): - return not self.__eq__(obj) - - def __neg__(self): - if self.ctrlpoints is None: - raise ValueError - newcurve = copy(self) - newctrlpoints = [-1 * ctrlpt for ctrlpt in newcurve.ctrlpoints] - newcurve.ctrlpoints = newctrlpoints - return newcurve - - def __add__(self, other: object): - if self.ctrlpoints is None: - raise ValueError - if not isinstance(other, self.__class__): - copied = copy(self) - copied.ctrlpoints = [other + point for point in self.ctrlpoints] - return copied - if self.knotvector.limits != other.knotvector.limits: - raise ValueError - if self.weights is None and other.weights is None: - vecta, vectb = tuple(self.knotvector), tuple(other.knotvector) - matra, matrb = heavy.MathOperations.add_spline_curve(vecta, vectb) - curve = Curve(self.knotvector | other.knotvector) - ctrlpoints = np.array(matra) @ self.ctrlpoints - ctrlpoints += np.array(matrb) @ other.ctrlpoints - curve.ctrlpoints = ctrlpoints - return curve - numa, dena = self.fraction() - numb, denb = other.fraction() - return (numa * denb + numb * dena) / (dena * denb) - - def __radd__(self, other: object): - return self.__add__(other) - - def __sub__(self, other: object): - return self + (-other) - - def __rsub__(self, other: object): - return other + (-self) - - def __mul__(self, other: object): - if self.ctrlpoints is None: - raise ValueError - if not isinstance(other, self.__class__): - copied = copy(self) - copied.ctrlpoints = [point * other for point in copied.ctrlpoints] - return copied - if self.knotvector.limits != other.knotvector.limits: - raise ValueError - if self.weights is None and other.weights is None: - vecta, vectb = tuple(self.knotvector), tuple(other.knotvector) - vectmul = heavy.MathOperations.knotvector_mul(vecta, vectb) - matrix3d = heavy.MathOperations.mul_spline_curve(vecta, vectb) - ctrlpoints = np.tensordot( - np.moveaxis(self.ctrlpoints, 0, -1), matrix3d, axes=1 - ) - ctrlpoints = ctrlpoints @ other.ctrlpoints - curve = Curve(vectmul, ctrlpoints) - return curve - numa, dena = self.fraction() - numb, denb = other.fraction() - return (numa * numb) / (dena * denb) - - def __rmul__(self, other: object): - if self.ctrlpoints is None: - raise ValueError - assert not isinstance(other, self.__class__) - copied = copy(self) - copied.ctrlpoints = [other * point for point in copied.ctrlpoints] - return copied - - def __matmul__(self, other: object): - if self.ctrlpoints is None: - raise ValueError - if not isinstance(other, self.__class__): - copied = copy(self) - copied.ctrlpoints = [point @ other for point in copied.ctrlpoints] - return copied - if self.knotvector.limits != other.knotvector.limits: - raise ValueError - if self.weights is None and other.weights is None: - vecta, vectb = tuple(self.knotvector), tuple(other.knotvector) - vectmul = heavy.MathOperations.knotvector_mul(vecta, vectb) - matrix3d = heavy.MathOperations.mul_spline_curve(vecta, vectb) - matrix2d = [ - [pt0 @ pt1 for pt0 in self.ctrlpoints] for pt1 in other.ctrlpoints - ] - matrix3d = np.array(matrix3d) - matrix2d = np.array(matrix2d) - newctrlpts = [0] * matrix3d.shape[1] - for i in range(matrix3d.shape[1]): - newctrlpt = np.tensordot(matrix3d[:, i, :], matrix2d, axes=2) - newctrlpts[i] = newctrlpt - ctrlpoints = newctrlpts - curve = Curve(vectmul, ctrlpoints) - return curve - numa, dena = self.fraction() - numb, denb = other.fraction() - return (numa @ numb) / (dena * denb) - - def __rmatmul__(self, other: object): - if self.ctrlpoints is None: - raise ValueError - assert not isinstance(other, self.__class__) - copied = copy(self) - copied.ctrlpoints = [other @ point for point in copied.ctrlpoints] - return copied - - def __truediv__(self, other: object): - if self.ctrlpoints is None: - raise ValueError - if not isinstance(other, self.__class__): - copied = copy(self) - copied.ctrlpoints = [point / other for point in copied.ctrlpoints] - return copied - if self.knotvector.limits != other.knotvector.limits: - raise ValueError - if self.weights is None and other.weights is None: - copyse = copy(self) - copyot = copy(other) - vectora, vectorb = tuple(copyse.knotvector), tuple(copyot.knotvector) - vectorc = tuple(copyse.knotvector | copyot.knotvector) - transctrlpts = heavy.Operations.matrix_transformation(vectora, vectorc) - transweights = heavy.Operations.matrix_transformation(vectorb, vectorc) - weights = np.dot(transweights, copyot.ctrlpoints) - ctrlpts = np.dot(transctrlpts, copyse.ctrlpoints) - ctrlpts = [pti / wi for pti, wi in zip(ctrlpts, weights)] - return self.__class__(vectorc, ctrlpts, weights) - - numa, dena = self.fraction() - numb, denb = other.fraction() - return (numa * denb) / (dena * numb) - - def __rtruediv__(self, other: object): - """ - Example: 1/curve - """ - if self.ctrlpoints is None: - raise ValueError - assert not isinstance(other, self.__class__) - for point in self.ctrlpoints: - float(point) - if self.weights is None: - newcurve = self.__class__(tuple(self.knotvector)) - newcurve.weights = [copy(point) for point in self.ctrlpoints] - newcurve.ctrlpoints = [1 / w for w in newcurve.weights] - return newcurve - num, den = self.fraction() - frac = den / num - return other * frac - - def __or__(self, other: object): - umaxleft = self.knotvector[-1] - uminright = other.knotvector[0] - if umaxleft != uminright: - error_msg = f"max(Uleft) = {umaxleft} != {uminright} = min(Uright)" - raise ValueError(error_msg) - othercopy = copy(other) - selfcopy = copy(self) - maxdegree = max(self.degree, other.degree) - selfcopy.degree = maxdegree - othercopy.degree = maxdegree - npts0 = selfcopy.npts - npts1 = othercopy.npts - newknotvector = [0] * (maxdegree + npts0 + npts1 + 1) - newknotvector[:npts0] = selfcopy.knotvector[:npts0] - newknotvector[npts0:] = othercopy.knotvector[1:] - newknotvector = KnotVector(newknotvector) - newctrlpoints = [0] * (npts0 + npts1 - 1) - newctrlpoints[:npts0] = selfcopy.ctrlpoints[:npts0] - newctrlpoints[npts0:] = othercopy.ctrlpoints[1:] - newcurve = self.__class__(newknotvector, newctrlpoints) - newcurve.knot_clean([umaxleft]) - return newcurve - - @property - def knotvector(self): - """Knot Vector - - :getter: Returns the knotvector of the curve - :setter: Sets the knotvector of the curve - :type: KnotVector - - Example use - ----------- - - >>> from pynurbs import Curve - >>> curve = Curve([0., 0., 1., 1.]) - >>> curve.knotvector - (0., 0., 1., 1.) - >>> curve.knotvector = [0, 0, 0, 1, 1, 1] - >>> curve.knotvector - (0, 0, 0, 1, 1, 1) - - """ - - return self.__knotvector - - @property - def degree(self): - """Polynomial degree of curve - - :getter: Returns the degree of the curve - :setter: Sets the degree of the curve, by increasing or decreasing - :type: int - - Example use - ----------- - - >>> from pynurbs import Curve - >>> curve = Curve([0., 0., 1., 1.], [1, 2]) - >>> curve.degree = 3 # From 1 to 3 - >>> print(curve.ctrlpoints) - (1.0, 1.33, 1.67, 2.0) - >>> curve.degree -= 1 - >>> print(curve.ctrlpoints) - (1.0, 1.5, 2.0) - - """ - - return self.knotvector.degree - - @property - def npts(self): - """Number of control points - - :getter: Returns the number of control points of the curve - :type: int - - Example use - ----------- - - >>> from pynurbs import Curve - >>> curve = Curve([0., 0., 1., 1.], [1, 2]) - >>> curve.npts - 2 - >>> curve = Curve([0., 0., 0.5, 1., 1.], [1, 2, 1]) - >>> curve.npts - 3 - - """ - return self.knotvector.npts - - @property - def knots(self): - return self.knotvector.knots - - @property - def weights(self): - """Weights of rational curve - - If weights is None, the curve is a spline - - :getter: Returns the weights of curve - :setter: Sets the weights for a rational curve - :type: None | tuple[float] - - Example use - ----------- - - >>> from pynurbs import Curve - >>> curve = Curve([0., 0., 1., 1.], [1, 2]) - >>> curve.weights = [1, 2] - (1, 2) - >>> curve = Curve([0., 0., 0.5, 1., 1.], [1, 2, 1]) - >>> curve.weights = [1, 2, 1] - >>> curve.weights - (1, 2, 1) - - """ - if self.__weights is None: - return None - return tuple(self.__weights) - - @property - def ctrlpoints(self): - """Control points of the curve - - :getter: Returns the control points of the curve - :setter: Sets the control points of the curve - :type: None | tuple[Any] - - Example use - ----------- - - >>> from pynurbs import Curve - >>> curve = Curve([0., 0., 1., 1.]) - >>> curve.ctrlpoints = [1, 2] - >>> curve.ctrlpoints - (1, 2) - >>> curve = Curve([0., 0., 0.5, 1., 1.]) - >>> curve.ctrlpoints = [1, 2, 1] - >>> curve.ctrlpoints - (1, 2, 1) - - """ - if self.__ctrlpoints is None: - return None - return tuple(self.__ctrlpoints) - - @knotvector.setter - def knotvector(self, value: KnotVector): - if not isinstance(value, KnotVector): - value = KnotVector(value) - self.update(value) - - @degree.setter - def degree(self, value: int): - if not isinstance(value, int) or value < 0: - raise ValueError(f"Cannot set degree {value}") - times = value - self.degree - if times == 0: - return - if times > 0: - return self.degree_increase(times) - return self.degree_decrease(-times) - - @weights.setter - def weights(self, value: Tuple[float]): - if value is None: - self.__weights = None - return - if not all(map(isnumber, value)): - raise ValueError - if not all(number > 0 for number in value): - raise ValueError - - # Verify if there's roots - vector = tuple(self.knotvector) - roots = heavy.find_roots(vector, value) - if roots: - error_msg = f"Zero division at nodes {roots}" - raise ValueError(error_msg) - self.__weights = tuple(value) - - @ctrlpoints.setter - def ctrlpoints(self, newpoints: np.ndarray): - if newpoints is None: - self.__ctrlpoints = None - return - if not all(map(supports_linear_operation, newpoints)): - raise ValueError - for point in newpoints: # Verify if operations are valid for each node - for knot in self.knotvector.knots: - knot * point - for otherpoint in newpoints: - point + otherpoint # Verify if we can sum every point, same type - - if len(newpoints) != self.npts: - error_msg = f"The number of control points ({len(newpoints)}) must be " - error_msg += f"the same as npts of KnotVector ({self.knotvector.npts})\n" - error_msg += f" knotvector.npts = {self.npts}" - error_msg += f" len(ctrlpoints) = {len(newpoints)}" - raise ValueError(error_msg) - - self.__ctrlpoints = tuple(newpoints) - - def __copy__(self) -> Curve: - return self.__deepcopy__(None) - - def __deepcopy__(self, memo) -> Curve: - knotvector = copy(self.knotvector) - curve = self.__class__(knotvector) - if self.ctrlpoints is not None: - curve.ctrlpoints = [copy(point) for point in self.ctrlpoints] - if self.weights is not None: - curve.weights = [copy(weight) for weight in self.weights] - return curve - - def fraction(self) -> Tuple[BaseCurve]: - """Returns the current curve ``C`` in the form ``A``/``B`` - - ``A`` and ``B`` are bsplines of same degree as ``C`` - and same number of points. - - If ``C`` is already a bspline, then ``B = 1`` - - :return: The pair ``(A, B)`` - :rtype: tuple[curve] - - Example use - ----------- - - >>> from pynurbs import Curve - >>> curve = Curve([0, 0, 0.5, 1, 1]) - >>> curve.ctrlpoints = [2, 4, 2] - >>> curve.weights = [1, 3, 2] - >>> A, B = curve.fraction() - >>> print(A) - Spline curve of degree 1 and 3 control points - KnotVector = (0, 0, 0.5, 1, 1) - ControlPoints = [2, 12, 4] - >>> print(B) - Spline curve of degree 1 and 3 control points - KnotVector = (0, 0, 0.5, 1, 1) - ControlPoints = [1, 3, 2] - - """ - if self.weights is None: - numerator = copy(self) - return numerator, 1 - ctrlpoints = [copy(point) for point in self.ctrlpoints] - numerator = self.__class__(copy(self.knotvector)) - denominator = self.__class__(copy(self.knotvector)) - numerator.ctrlpoints = [wi * pt for wi, pt in zip(self.weights, ctrlpoints)] - denominator.ctrlpoints = self.weights - return numerator, denominator - - def update( - self, - newknotvector: KnotVector, - tolerance: Optional[float] = 1e-9, - nodes: Optional[tuple[float]] = None, - ): - """Update the knotvector to newknotvector - - This function compute the new control points and - new weights such the new curve is near the old curve - - If the error is bigger than tolerance, then raises a ValueError - - If ``nodes`` are given, the new curve will fit on these ``nodes`` - - Example use - ----------- - - >>> from pynurbs import Curve - >>> curve = Curve([0, 0, 1, 1], [1, 2]) - >>> curve.update([0, 0, 0.5, 1, 1]) # Insert knot [0.5] - >>> curve = Curve([0, 0, 1, 1], [1, 2]) - >>> curve.update([0, 0, 0, 1, 1, 1]) # Degree elevate - >>> curve = Curve([0, 0, 0.5, 1, 1], [1, 2, 1]) - >>> curve.update([0, 0, 1, 1], nodes = (0, 1)) # Remove knot [0.5] - - """ - if not isinstance(newknotvector, KnotVector): - newknotvector = KnotVector(newknotvector) - if newknotvector == self.knotvector: - return - if self.ctrlpoints is None: - self.__knotvector = newknotvector - return - if self.knotvector.limits != newknotvector.limits: - raise ValueError - temp_curve = self.__class__(newknotvector) - error = temp_curve.fit_curve(self, nodes) - if tolerance and error > tolerance: - error_msg = "Cannot update knotvector cause error is " - error_msg += f" {float(error):.2e} > {tolerance}" - raise ValueError(error_msg) - self.__knotvector = newknotvector - self.ctrlpoints = temp_curve.ctrlpoints - self.weights = temp_curve.weights - - def apply(self, newknotvector: KnotVector, matrix: Tuple[Tuple[float]]): - """Applies the linear transformation for every control point - - new ctrlpoints = matrix @ old ctrlpoints - new weights = matrix @ old weights - - Example use - ----------- - - >>> from pynurbs import Curve - >>> curve = Curve([0, 0, 0.5, 1, 1]) - >>> curve.ctrlpoints = [2, 4, 2] - >>> matrix = [(0, 1, 0), (-1, 0, 1), (2, -1, 0)] - >>> curve.apply(matrix) - >>> print(curve) - Spline curve of degree 1 and 3 control points - KnotVector = (0, 0, 0.5, 1, 1) - ControlPoints = [4, 0, 0] - - """ - if not isinstance(newknotvector, KnotVector): - newknotvector = KnotVector(newknotvector) - oldctrlpoints = self.ctrlpoints - oldweights = self.weights - if oldctrlpoints is None and oldweights is None: - self.knotvector = newknotvector - return - self.ctrlpoints = None - self.weights = None - self.knotvector = newknotvector - if oldweights is None: - self.ctrlpoints = np.dot(matrix, oldctrlpoints) - return - newweights = np.dot(matrix, oldweights) - self.weights = newweights - - if oldctrlpoints is not None: - oldctrlpoints = list(oldctrlpoints) - for i, weight in enumerate(oldweights): - oldctrlpoints[i] *= weight - newctrlpoints = [] - for i, line in enumerate(matrix): - newctrlpoints.append(0 * oldctrlpoints[0]) - for j, point in enumerate(oldctrlpoints): - newpoint = line[j] * point - newpoint /= self.weights[i] - newctrlpoints[i] += newpoint - self.ctrlpoints = newctrlpoints +from .base import BaseCurve class Curve(BaseCurve): From 6d7701c47acd8d42b5e6a862247ef7d0c4986c34 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 4 Jul 2025 21:59:30 +0200 Subject: [PATCH 087/116] fix: problem due to call child class on parent class --- src/pynurbs/curves/base.py | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py index 22e1d3f..07829c8 100644 --- a/src/pynurbs/curves/base.py +++ b/src/pynurbs/curves/base.py @@ -1,7 +1,7 @@ from __future__ import annotations from copy import copy -from typing import Optional, Tuple, Union +from typing import Any, Iterable, Optional, Tuple, Union import numpy as np @@ -28,12 +28,17 @@ def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: class BaseCurve: - def __init__(self, knotvector: KnotVector): - self.__ctrlpoints = None - self.__weights = None + def __init__( + self, + knotvector: KnotVector, + ctrlpoints: Union[None, Iterable[Any]] = None, + weights: Union[None, Iterable[Any]] = None, + ): if not isinstance(knotvector, KnotVector): knotvector = KnotVector(knotvector) self.__knotvector = knotvector + self.__ctrlpoints = ctrlpoints + self.__weights = weights def __call__(self, nodes: np.ndarray) -> np.ndarray: return self.eval(nodes) @@ -80,7 +85,7 @@ def __add__(self, other: object): if self.weights is None and other.weights is None: vecta, vectb = tuple(self.knotvector), tuple(other.knotvector) matra, matrb = heavy.MathOperations.add_spline_curve(vecta, vectb) - curve = Curve(self.knotvector | other.knotvector) + curve = self.__class__(self.knotvector | other.knotvector) ctrlpoints = np.array(matra) @ self.ctrlpoints ctrlpoints += np.array(matrb) @ other.ctrlpoints curve.ctrlpoints = ctrlpoints @@ -115,7 +120,7 @@ def __mul__(self, other: object): np.moveaxis(self.ctrlpoints, 0, -1), matrix3d, axes=1 ) ctrlpoints = ctrlpoints @ other.ctrlpoints - curve = Curve(vectmul, ctrlpoints) + curve = self.__class__(vectmul, ctrlpoints) return curve numa, dena = self.fraction() numb, denb = other.fraction() @@ -152,7 +157,7 @@ def __matmul__(self, other: object): newctrlpt = np.tensordot(matrix3d[:, i, :], matrix2d, axes=2) newctrlpts[i] = newctrlpt ctrlpoints = newctrlpts - curve = Curve(vectmul, ctrlpoints) + curve = self.__class__(vectmul, ctrlpoints) return curve numa, dena = self.fraction() numb, denb = other.fraction() From 66d5e25b55479b139937902f0dc2c671a3ec858e Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Fri, 4 Jul 2025 22:31:01 +0200 Subject: [PATCH 088/116] refactor: set tolerance as attribute of Curve --- src/pynurbs/curves/base.py | 70 ++++++++++++------------------------ src/pynurbs/curves/curves.py | 16 ++++----- 2 files changed, 31 insertions(+), 55 deletions(-) diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py index 07829c8..8c39b20 100644 --- a/src/pynurbs/curves/base.py +++ b/src/pynurbs/curves/base.py @@ -39,6 +39,7 @@ def __init__( self.__knotvector = knotvector self.__ctrlpoints = ctrlpoints self.__weights = weights + self.tolerance = 1e-9 def __call__(self, nodes: np.ndarray) -> np.ndarray: return self.eval(nodes) @@ -238,6 +239,10 @@ def __or__(self, other: object): newcurve.knot_clean([umaxleft]) return newcurve + @property + def tolerance(self) -> Union[None, float]: + return self.__tolerance + @property def knotvector(self): """Knot Vector @@ -363,11 +368,28 @@ def ctrlpoints(self): return None return tuple(self.__ctrlpoints) + @tolerance.setter + def tolerance(self, value: Union[None, float]): + if value is not None and (not isnumber(value) or value <= 0): + raise ValueError + self.__tolerance = value + @knotvector.setter def knotvector(self, value: KnotVector): if not isinstance(value, KnotVector): value = KnotVector(value) - self.update(value) + if self.ctrlpoints is not None and self.knotvector != value: + if self.knotvector.limits != value.limits: + raise ValueError + temp_curve = self.__class__(value) + error = temp_curve.fit_curve(self) + if self.tolerance is not None and error > self.tolerance: + error_msg = "Cannot update knotvector cause error is " + error_msg += f" {float(error):.2e} > {self.tolerance}" + raise ValueError(error_msg) + self.__ctrlpoints = temp_curve.ctrlpoints + self.__weights = temp_curve.weights + self.__knotvector = value @degree.setter def degree(self, value: int): @@ -471,52 +493,6 @@ def fraction(self) -> Tuple[BaseCurve]: denominator.ctrlpoints = self.weights return numerator, denominator - def update( - self, - newknotvector: KnotVector, - tolerance: Optional[float] = 1e-9, - nodes: Optional[tuple[float]] = None, - ): - """Update the knotvector to newknotvector - - This function compute the new control points and - new weights such the new curve is near the old curve - - If the error is bigger than tolerance, then raises a ValueError - - If ``nodes`` are given, the new curve will fit on these ``nodes`` - - Example use - ----------- - - >>> from pynurbs import Curve - >>> curve = Curve([0, 0, 1, 1], [1, 2]) - >>> curve.update([0, 0, 0.5, 1, 1]) # Insert knot [0.5] - >>> curve = Curve([0, 0, 1, 1], [1, 2]) - >>> curve.update([0, 0, 0, 1, 1, 1]) # Degree elevate - >>> curve = Curve([0, 0, 0.5, 1, 1], [1, 2, 1]) - >>> curve.update([0, 0, 1, 1], nodes = (0, 1)) # Remove knot [0.5] - - """ - if not isinstance(newknotvector, KnotVector): - newknotvector = KnotVector(newknotvector) - if newknotvector == self.knotvector: - return - if self.ctrlpoints is None: - self.__knotvector = newknotvector - return - if self.knotvector.limits != newknotvector.limits: - raise ValueError - temp_curve = self.__class__(newknotvector) - error = temp_curve.fit_curve(self, nodes) - if tolerance and error > tolerance: - error_msg = "Cannot update knotvector cause error is " - error_msg += f" {float(error):.2e} > {tolerance}" - raise ValueError(error_msg) - self.__knotvector = newknotvector - self.ctrlpoints = temp_curve.ctrlpoints - self.weights = temp_curve.weights - def apply(self, newknotvector: KnotVector, matrix: Tuple[Tuple[float]]): """Applies the linear transformation for every control point diff --git a/src/pynurbs/curves/curves.py b/src/pynurbs/curves/curves.py index 59fdcd7..f742104 100644 --- a/src/pynurbs/curves/curves.py +++ b/src/pynurbs/curves/curves.py @@ -157,10 +157,10 @@ def knot_remove(self, nodes: Tuple[float], tolerance: float = 1e-9) -> None: ControlPoints = [1.0, 2.0, -3.0] """ - old_vector = self.knotvector.internal - new_vector = remove_knots(old_vector, nodes) - knots = new_vector.knots if new_vector.degree != 0 else None - self.update(new_vector, tolerance, knots) + old_tolerance = self.tolerance + self.tolerance = tolerance + self.knotvector = remove_knots(self.knotvector.internal, nodes) + self.tolerance = old_tolerance def knot_clean( self, nodes: Optional[Tuple[float]] = None, tolerance: Optional[float] = 1e-9 @@ -275,10 +275,10 @@ def degree_decrease( if tolerance is not None: float(tolerance) assert tolerance >= 0 - old_vector = self.knotvector.internal - new_vector = decrease_degree(old_vector, times) - knots = new_vector.knots if new_vector.degree != 0 else None - self.update(new_vector, tolerance, knots) + old_tolerance = self.tolerance + self.tolerance = tolerance + self.knotvector = decrease_degree(self.knotvector.internal, times) + self.tolerance = old_tolerance def degree_clean(self, tolerance: float = 1e-9): """Reduces au maximum the degree of the curve for given tolerance. From e369d2b19425c5f0d7fa9213a70ef8e80a0b7f2a Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 5 Jul 2025 11:40:15 +0200 Subject: [PATCH 089/116] fix: vectorize function to use isnumber --- src/pynurbs/operations/tools.py | 36 +++++++++++++++++---------------- 1 file changed, 19 insertions(+), 17 deletions(-) diff --git a/src/pynurbs/operations/tools.py b/src/pynurbs/operations/tools.py index 6f44138..974af22 100644 --- a/src/pynurbs/operations/tools.py +++ b/src/pynurbs/operations/tools.py @@ -7,6 +7,8 @@ import numpy as np +from ..core.custom_math import isnumber + # Creates a decorator to vectorize functions that receives floats # Or an array of floats depending on the dimension @@ -32,25 +34,25 @@ def decorator(func): def wrapper(*args, **kwargs): param = args[position] if dimension == 0: - try: + if isnumber(param): float(param) return func(*args, **kwargs) - except TypeError: - result = ( - func(*args[:position], p, *args[position + 1 :], **kwargs) - for p in param - ) - result = tuple(result) - for key, tipo in conversion.items(): - if isinstance(param, key): - if tipo is not None: - result = tipo(result) - return result - if isinstance(param, np.ndarray): - result = np.array(result, dtype=param.dtype) - else: - result = param.__class__(result) - return result + + result = ( + func(*args[:position], p, *args[position + 1 :], **kwargs) + for p in param + ) + result = tuple(result) + for key, tipo in conversion.items(): + if isinstance(param, key): + if tipo is not None: + result = tipo(result) + return result + if isinstance(param, np.ndarray): + result = np.array(result, dtype=param.dtype) + else: + result = param.__class__(result) + return result raise NotImplementedError return wrapper From be48b90808b10ea6149e2257503fbd0e0657d1d2 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 5 Jul 2025 11:44:25 +0200 Subject: [PATCH 090/116] refactor: rename isnumber to isscalar --- src/pynurbs/core/custom_math.py | 2 +- src/pynurbs/core/knotvector.py | 8 ++++---- src/pynurbs/core/polynomial.py | 8 ++++---- src/pynurbs/curves/base.py | 6 +++--- src/pynurbs/operations/heavy.py | 4 ++-- src/pynurbs/operations/tools.py | 4 ++-- 6 files changed, 16 insertions(+), 16 deletions(-) diff --git a/src/pynurbs/core/custom_math.py b/src/pynurbs/core/custom_math.py index 86a325c..5bf5941 100644 --- a/src/pynurbs/core/custom_math.py +++ b/src/pynurbs/core/custom_math.py @@ -108,7 +108,7 @@ def binom(n: int, i: int): return int(prod) -def isnumber(obj: Any) -> bool: +def isscalar(obj: Any) -> bool: """ Tells if an object is a number """ diff --git a/src/pynurbs/core/knotvector.py b/src/pynurbs/core/knotvector.py index 9b37a74..5e06286 100644 --- a/src/pynurbs/core/knotvector.py +++ b/src/pynurbs/core/knotvector.py @@ -4,7 +4,7 @@ from numbers import Real from typing import Iterable, Tuple, Union -from .custom_math import isnumber +from .custom_math import isscalar def find_degree(vector: Tuple[Real, ...]) -> int: @@ -22,7 +22,7 @@ def __init__(self, vector: Iterable[Real], degree: Union[None, int] = None): vector = tuple(vector) except Exception: raise ValueError(f"Wrong argument: '{vector}'") - if not all(map(isnumber, vector)): + if not all(map(isscalar, vector)): raise ValueError(f"Cannot create KnotVector with {vector}") if not is_sorted(vector): raise ValueError(f"Cannot create KnotVector with {vector}") @@ -82,7 +82,7 @@ def __eq__(self, other: object) -> bool: return NotImplemented def span(self, node: Real) -> int: - if not isnumber(node): + if not isscalar(node): raise ValueError(f"Node '{node}' must be Real instance") if node < self[self.degree] or self[self.npts] < node: raise ValueError(f"Node {node} outside [{self.knots[0], self.knots[-1]}]") @@ -100,7 +100,7 @@ def span(self, node: Real) -> int: return mid def mult(self, node: Real) -> int: - if not isnumber(node): + if not isscalar(node): raise ValueError(f"Node '{node}' must be Real instance") if node < self[self.degree] or self[self.npts] < node: raise ValueError(f"Node {node} outside [{self.knots[0], self.knots[-1]}]") diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 7908718..4a22d5d 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -9,7 +9,7 @@ from numbers import Real from typing import Iterable, List, Tuple, Union -from .custom_math import isnumber, supports_linear_operation +from .custom_math import isscalar, supports_linear_operation class Polynomial: @@ -34,10 +34,10 @@ class Polynomial: """ def __init__(self, coefs: Iterable[Real]): - coefs = tuple(coefs) if not isnumber(coefs) else (coefs,) + coefs = tuple(coefs) if not isscalar(coefs) else (coefs,) if len(coefs) == 0: raise ValueError("Cannot receive an empty tuple") - if isnumber(coefs[0]): + if isscalar(coefs[0]): degree = max((i for i, v in enumerate(coefs) if v), default=0) else: degree = len(coefs) - 1 @@ -139,7 +139,7 @@ def __str__(self): if self.degree == 0: return str(self[0]) msgs: List[str] = [] - if not isnumber(self[0]): + if not isscalar(self[0]): msgs.append(f"({self[0]})") if self.degree > 0: msgs.append(f"({self[1]}) * x") diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py index 8c39b20..8ecbb33 100644 --- a/src/pynurbs/curves/base.py +++ b/src/pynurbs/curves/base.py @@ -5,7 +5,7 @@ import numpy as np -from ..core.custom_math import isnumber, supports_linear_operation +from ..core.custom_math import isscalar, supports_linear_operation from ..knotspace import KnotVector from ..operations import heavy @@ -370,7 +370,7 @@ def ctrlpoints(self): @tolerance.setter def tolerance(self, value: Union[None, float]): - if value is not None and (not isnumber(value) or value <= 0): + if value is not None and (not isscalar(value) or value <= 0): raise ValueError self.__tolerance = value @@ -407,7 +407,7 @@ def weights(self, value: Tuple[float]): if value is None: self.__weights = None return - if not all(map(isnumber, value)): + if not all(map(isscalar, value)): raise ValueError if not all(number > 0 for number in value): raise ValueError diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index 286a5fd..8a64ddf 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -10,7 +10,7 @@ import numpy as np -from ..core.custom_math import Linalg, NodeSample, isnumber, totuple +from ..core.custom_math import Linalg, NodeSample, isscalar, totuple from ..core.knotvector import ImmutableKnotVector from ..operations import knotvector as opekv from .least_square import eval_spline_nodes, spline2spline @@ -28,7 +28,7 @@ def find_roots( We do it by sampling """ ctrlvalues = tuple(ctrlvalues) - if not all(map(isnumber, ctrlvalues)): + if not all(map(isscalar, ctrlvalues)): raise ValueError knotvector = ImmutableKnotVector(knotvector) assert isinstance(ctrlvalues, tuple) diff --git a/src/pynurbs/operations/tools.py b/src/pynurbs/operations/tools.py index 974af22..fb3015f 100644 --- a/src/pynurbs/operations/tools.py +++ b/src/pynurbs/operations/tools.py @@ -7,7 +7,7 @@ import numpy as np -from ..core.custom_math import isnumber +from ..core.custom_math import isscalar # Creates a decorator to vectorize functions that receives floats @@ -34,7 +34,7 @@ def decorator(func): def wrapper(*args, **kwargs): param = args[position] if dimension == 0: - if isnumber(param): + if isscalar(param): float(param) return func(*args, **kwargs) From 4ce8f4778242c41c8e59810a2222b64a6555abba Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 5 Jul 2025 11:48:16 +0200 Subject: [PATCH 091/116] refactor: move evaluation of nurbs curve into BaseCurve --- src/pynurbs/curves/base.py | 20 +++++++++++++++++--- src/pynurbs/curves/curves.py | 34 +++++----------------------------- 2 files changed, 22 insertions(+), 32 deletions(-) diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py index 8ecbb33..56c55b9 100644 --- a/src/pynurbs/curves/base.py +++ b/src/pynurbs/curves/base.py @@ -1,13 +1,16 @@ from __future__ import annotations from copy import copy -from typing import Any, Iterable, Optional, Tuple, Union +from numbers import Real +from typing import Any, Iterable, Tuple, Union import numpy as np from ..core.custom_math import isscalar, supports_linear_operation +from ..core.spline_basis import ImmutableSplineBasis from ..knotspace import KnotVector from ..operations import heavy +from ..operations.tools import vectorize def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: @@ -41,8 +44,19 @@ def __init__( self.__weights = weights self.tolerance = 1e-9 - def __call__(self, nodes: np.ndarray) -> np.ndarray: - return self.eval(nodes) + @vectorize(1, 0) + def __call__(self, node: Real) -> Any: + if self.ctrlpoints is None: + raise ValueError("Cannot evaluate") + vector = self.knotvector.internal + basis = ImmutableSplineBasis(vector) + result = basis(node) + zero = 0 * self.ctrlpoints[0] + if self.weights is None: + return sum((r * c for r, c in zip(result, self.ctrlpoints)), zero) + result = tuple(w * r for w, r in zip(self.weights, result)) + denom = 1 / sum(result) + return sum((r * c * denom for r, c in zip(result, self.ctrlpoints)), zero) def __eq__(self, other: object) -> bool: if type(self) is not type(other): diff --git a/src/pynurbs/curves/curves.py b/src/pynurbs/curves/curves.py index f742104..1ff9d5e 100644 --- a/src/pynurbs/curves/curves.py +++ b/src/pynurbs/curves/curves.py @@ -1,12 +1,12 @@ from __future__ import annotations from fractions import Fraction +from numbers import Real from typing import Any, Callable, Optional, Tuple, Union import numpy as np from ..core.custom_math import number_type -from ..core.spline_basis import ImmutableSplineBasis from ..knotspace import KnotVector from ..operations import heavy from ..operations.knotvector import ( @@ -16,6 +16,7 @@ remove_knots, ) from ..operations.least_square import fit_function, func2func, spline2spline +from ..operations.tools import vectorize from .base import BaseCurve @@ -47,21 +48,8 @@ def __str__(self) -> str: msg += "]\n" return msg - def __eval(self, nodes: Tuple[float]) -> Tuple[Any]: - """ - Private method to evaluate points in the curve - """ - vector = self.knotvector.internal - nodes = tuple(nodes) - basis = ImmutableSplineBasis(vector) - matrix = np.transpose(tuple(map(basis, nodes))) - if self.weights is not None: - denominators = 1 / np.dot(self.weights, matrix) - matrix = np.einsum("j,ij,i->ij", denominators, matrix, self.weights) - result = np.moveaxis(matrix, 0, -1) @ self.ctrlpoints - return tuple(result) - - def eval(self, nodes: Union[float, Tuple[float]]) -> Union[Any, Tuple[Any]]: + @vectorize(1, 0) + def eval(self, node: Real) -> Any: """Point evaluation function :param nodes: The nodes to evaluates @@ -87,19 +75,7 @@ def eval(self, nodes: Union[float, Tuple[float]]) -> Union[Any, Tuple[Any]]: (1.0, 2.0, -3.0) """ - if self.ctrlpoints is None: - error_msg = "Cannot evaluate: There are no control points" - raise ValueError(error_msg) - try: - nodes = tuple(nodes) - onevalue = False - except TypeError: - nodes = (nodes,) - onevalue = True - if not self.knotvector.valid(nodes): - raise ValueError(f"Received invalid nodes to eval: {nodes}") - result = self.__eval(nodes) - return result[0] if onevalue else result + return self(node) def knot_insert(self, nodes: Tuple[float]) -> None: """Insert given nodes inside knotvector From 6edb25cdfe8ad2bb7b265b72609d27f51d28a6d0 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sat, 5 Jul 2025 12:38:44 +0200 Subject: [PATCH 092/116] refactor: make degree increase and degree decrease call degree setter --- src/pynurbs/curves/base.py | 8 +++--- src/pynurbs/curves/curves.py | 50 +++++++++++++++++------------------- tests/curves/test_bezier.py | 9 +++---- 3 files changed, 31 insertions(+), 36 deletions(-) diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py index 56c55b9..ebe7f60 100644 --- a/src/pynurbs/curves/base.py +++ b/src/pynurbs/curves/base.py @@ -10,6 +10,7 @@ from ..core.spline_basis import ImmutableSplineBasis from ..knotspace import KnotVector from ..operations import heavy +from ..operations.knotvector import decrease_degree, increase_degree from ..operations.tools import vectorize @@ -410,11 +411,10 @@ def degree(self, value: int): if not isinstance(value, int) or value < 0: raise ValueError(f"Cannot set degree {value}") times = value - self.degree - if times == 0: - return if times > 0: - return self.degree_increase(times) - return self.degree_decrease(-times) + self.knotvector = increase_degree(self.knotvector.internal, times) + elif times < 0: + self.knotvector = decrease_degree(self.knotvector.internal, -times) @weights.setter def weights(self, value: Tuple[float]): diff --git a/src/pynurbs/curves/curves.py b/src/pynurbs/curves/curves.py index 1ff9d5e..bd4baf5 100644 --- a/src/pynurbs/curves/curves.py +++ b/src/pynurbs/curves/curves.py @@ -6,15 +6,10 @@ import numpy as np -from ..core.custom_math import number_type +from ..core.custom_math import isscalar, number_type from ..knotspace import KnotVector from ..operations import heavy -from ..operations.knotvector import ( - decrease_degree, - increase_degree, - insert_knots, - remove_knots, -) +from ..operations.knotvector import insert_knots, remove_knots from ..operations.least_square import fit_function, func2func, spline2spline from ..operations.tools import vectorize from .base import BaseCurve @@ -176,17 +171,20 @@ def knot_clean( ControlPoints = [1.0, 2.0, -3.0] """ - float(tolerance) - assert tolerance >= 0 + if not isscalar(tolerance) or tolerance <= 0: + raise ValueError("Tolerance must be positive") if nodes is None: nodes = self.knotvector.knots nodes = tuple(set(nodes) - set(self.knotvector.limits)) + oldtolerance = self.tolerance + self.tolerance = tolerance for knot in nodes: try: while True: - self.knot_remove((knot,), tolerance) + self.knot_remove([knot]) except ValueError: pass + self.tolerance = oldtolerance def degree_increase(self, times: Optional[int] = 1): """Increase the degree of the curve by an amount ``times`` @@ -211,12 +209,9 @@ def degree_increase(self, times: Optional[int] = 1): KnotVector = (0, 0, 0, 0, 0.5, 0.5, 1, 1, 1, 1) ControlPoints = [1.0, 1.33, 1.17, -0.17, -1.33, -3.0] """ - if not isinstance(times, int) or times <= 0: + if not isinstance(times, int) or times < 0: raise ValueError - old_vector = self.knotvector.internal - new_vector = increase_degree(old_vector, times) - matrix = heavy.Operations.degree_increase(old_vector, times) - self.apply(new_vector, matrix) + self.degree += int(times) def degree_decrease( self, times: Optional[int] = 1, tolerance: Optional[float] = 1e-9 @@ -246,15 +241,16 @@ def degree_decrease( KnotVector = (0, 0, 0, 0.5, 1, 1, 1) ControlPoints = [1, 1.5, -0.5, -3] """ - if not isinstance(times, int) or times <= 0: + if not isinstance(times, int) or times < 0: raise ValueError(f"times = {times}") if tolerance is not None: - float(tolerance) - assert tolerance >= 0 - old_tolerance = self.tolerance - self.tolerance = tolerance - self.knotvector = decrease_degree(self.knotvector.internal, times) - self.tolerance = old_tolerance + if not isscalar(tolerance) or tolerance <= 0: + raise ValueError("Tolerance must be None or positive value") + if times > 0: + old_tolerance = self.tolerance + self.tolerance = tolerance + self.degree -= int(times) + self.tolerance = old_tolerance def degree_clean(self, tolerance: float = 1e-9): """Reduces au maximum the degree of the curve for given tolerance. @@ -282,13 +278,15 @@ def degree_clean(self, tolerance: float = 1e-9): KnotVector = (0, 0, 0, 0.5, 1, 1, 1) ControlPoints = [1, 1.5, -0.5, -3] """ - float(tolerance) - assert tolerance >= 0 + if not isscalar(tolerance) or tolerance <= 0: + raise ValueError("Given tolerance must be positive") + oldtolerance = self.tolerance try: + self.tolerance = tolerance while True: - self.degree_decrease(1, tolerance) + self.degree -= 1 except ValueError: - pass + self.tolerance = oldtolerance def clean(self, tolerance: float = 1e-9): """Calls degree_clean and knot_clean diff --git a/tests/curves/test_bezier.py b/tests/curves/test_bezier.py index 4c5edc9..2e2bff3 100644 --- a/tests/curves/test_bezier.py +++ b/tests/curves/test_bezier.py @@ -437,8 +437,9 @@ def test_increase_once_degree3(self): [0, 0, 3 / 4, 1 / 4], [0, 0, 0, 1], ] - Pgood = matrix @ ctrlpoints - np.testing.assert_allclose(curve.ctrlpoints, Pgood) + Pgood = tuple(np.dot(matrix, ctrlpoints)) + Ptest = np.array(curve.ctrlpoints, dtype="float64") + np.testing.assert_allclose(Ptest, Pgood) @pytest.mark.order(33) @pytest.mark.timeout(15) @@ -578,10 +579,6 @@ def test_fails(self): curve.degree_decrease("asd") with pytest.raises(ValueError): curve.degree_increase("asd") - with pytest.raises(ValueError): - curve.degree_decrease(0) - with pytest.raises(ValueError): - curve.degree_increase(0) @pytest.mark.order(33) @pytest.mark.dependency( From f0ef4b12ad35298cb6d9affa7bce07bd4fc068ef Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 19:35:44 +0200 Subject: [PATCH 093/116] test: add test for factorial --- tests/core/test_custom_math.py | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) diff --git a/tests/core/test_custom_math.py b/tests/core/test_custom_math.py index 11f35bb..ca6f099 100644 --- a/tests/core/test_custom_math.py +++ b/tests/core/test_custom_math.py @@ -65,6 +65,22 @@ def test_comb(self): assert Math.comb(3, 2) == 3 assert Math.comb(3, 3) == 1 + @pytest.mark.order(11) + @pytest.mark.dependency(depends=["TestMath::test_begin"]) + def test_factorial(self): + assert Math.factorial(0) == 1 + assert Math.factorial(1) == 1 + assert Math.factorial(2) == 2 + assert Math.factorial(3) == 6 + assert Math.factorial(4) == 24 + assert Math.factorial(5) == 120 + assert Math.factorial(6) == 720 + + result = 1 + for n in range(1, 10): + result *= n + assert Math.factorial(n) == result + @pytest.mark.order(11) @pytest.mark.dependency( depends=[ @@ -72,6 +88,7 @@ def test_comb(self): "TestMath::test_gcd", "TestMath::test_lcm", "TestMath::test_comb", + "TestMath::test_factorial", ] ) def test_end(self): From 0a8330f7334ee07d41b37699fdf5beb483533b9a Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 19:51:18 +0200 Subject: [PATCH 094/116] refactor: use Math.binom instead of Math.comb --- src/pynurbs/core/custom_math.py | 33 ++++++++++-------------------- src/pynurbs/core/polynomial.py | 8 +++----- tests/core/test_custom_math.py | 36 +++++++++++++++++++-------------- tests/core/test_polynomial.py | 1 + tests/test_basis_functions.py | 6 +++--- 5 files changed, 39 insertions(+), 45 deletions(-) diff --git a/src/pynurbs/core/custom_math.py b/src/pynurbs/core/custom_math.py index 5bf5941..40b0c09 100644 --- a/src/pynurbs/core/custom_math.py +++ b/src/pynurbs/core/custom_math.py @@ -38,6 +38,16 @@ def lcm(*numbers: Tuple[int]) -> int: return y if x == 0 else y return x * y // Math.gcd(x, y) + @staticmethod + def binom(n: int, i: int) -> int: + """ + Returns binomial (n, i) + """ + numerator = Math.factorial(n) + denominator = Math.factorial(i) + denominator *= Math.factorial(n - i) + return numerator // denominator + @staticmethod def factorial(number: int) -> int: if number < 2: @@ -47,13 +57,6 @@ def factorial(number: int) -> int: prod *= i return prod - @staticmethod - def comb(upper: int, lower: int) -> int: - numerator = Math.factorial(upper) - denominator = Math.factorial(lower) - denominator *= Math.factorial(upper - lower) - return numerator // denominator - def number_type(number: Union[int, float, Fraction]): """ @@ -94,20 +97,6 @@ def totuple(array): return tuple(array) -def binom(n: int, i: int): - """ - Returns binomial (n, i) - """ - assert isinstance(n, int) - assert isinstance(i, int) - prod = 1 - if i <= 0 or i >= n: - return 1 - for j in range(i): - prod *= (n - j) / (i - j) - return int(prod) - - def isscalar(obj: Any) -> bool: """ Tells if an object is a number @@ -299,7 +288,7 @@ def interpolate_bezier(nodes: Tuple[float]) -> Tuple[Tuple[float]]: for k, uk in enumerate(nodes): for i in range(degree + 1): matrix_bezier[i, k] = ( - Math.comb(degree, i) * (1 - uk) ** (degree - i) * (uk**i) + Math.binom(degree, i) * (1 - uk) ** (degree - i) * (uk**i) ) matrix_bezier = totuple(matrix_bezier) inverse = Linalg.invert(matrix_bezier) diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 4a22d5d..65fdc64 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -5,11 +5,10 @@ from __future__ import annotations -import math from numbers import Real from typing import Iterable, List, Tuple, Union -from .custom_math import isscalar, supports_linear_operation +from .custom_math import Math, isscalar, supports_linear_operation class Polynomial: @@ -216,8 +215,7 @@ def shift(polynomial: Polynomial, amount: Real) -> Polynomial: newcoefs = list(polynomial) for i, coef in enumerate(polynomial): for j in range(i): - binom = math.comb(i, j) - value = binom * (amount ** (i - j)) + value = Math.binom(i, j) * (amount ** (i - j)) if (i + j) % 2: value *= -1 newcoefs[j] += coef * value @@ -240,7 +238,7 @@ def derivate(polynomial: Polynomial, times: int = 1) -> Polynomial: if polynomial.degree < times: return Polynomial([0 * polynomial[0]]) coefs = ( - math.factorial(n + times) // math.factorial(n) * coef + Math.factorial(n + times) // Math.factorial(n) * coef for n, coef in enumerate(polynomial[times:]) ) return Polynomial(coefs) diff --git a/tests/core/test_custom_math.py b/tests/core/test_custom_math.py index ca6f099..7810be9 100644 --- a/tests/core/test_custom_math.py +++ b/tests/core/test_custom_math.py @@ -54,16 +54,22 @@ def test_lcm(self): @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestMath::test_begin"]) - def test_comb(self): - assert Math.comb(1, 0) == 1 - assert Math.comb(1, 1) == 1 - assert Math.comb(2, 0) == 1 - assert Math.comb(2, 1) == 2 - assert Math.comb(2, 2) == 1 - assert Math.comb(3, 0) == 1 - assert Math.comb(3, 1) == 3 - assert Math.comb(3, 2) == 3 - assert Math.comb(3, 3) == 1 + def test_binom(self): + + assert Math.binom(0, 0) == 1 + assert Math.binom(1, 0) == 1 + assert Math.binom(1, 1) == 1 + assert Math.binom(2, 0) == 1 + assert Math.binom(2, 1) == 2 + assert Math.binom(2, 2) == 1 + assert Math.binom(3, 0) == 1 + assert Math.binom(3, 1) == 3 + assert Math.binom(3, 2) == 3 + assert Math.binom(3, 3) == 1 + + for n in range(1, 11): + for i in range(0, n + 1): + assert Math.binom(n, i) == math.comb(n, i) @pytest.mark.order(11) @pytest.mark.dependency(depends=["TestMath::test_begin"]) @@ -87,7 +93,7 @@ def test_factorial(self): "TestMath::test_begin", "TestMath::test_gcd", "TestMath::test_lcm", - "TestMath::test_comb", + "TestMath::test_binom", "TestMath::test_factorial", ] ) @@ -153,7 +159,7 @@ def test_invert_integer(self): for n in range(side, side + 10): for i in range(side): for j in range(side): - matrix[i, j] = Math.comb(n + j, i) + matrix[i, j] = Math.binom(n + j, i) inverse = Linalg.invert(matrix) inverse = np.array(inverse, dtype="int64") np.testing.assert_allclose(np.dot(inverse, matrix), np.eye(side)) @@ -185,7 +191,7 @@ def test_invert_fraction(self): for n in range(side, side + 10): for i in range(side): for j in range(side): - matrix[i, j] = Fraction(Math.comb(n + j, i)) + matrix[i, j] = Fraction(Math.binom(n + j, i)) inverse = Linalg.invert(matrix) inverse = np.array(inverse, dtype="int64") matrix = np.array(matrix, dtype="int64") @@ -283,7 +289,7 @@ def test_solve_integer(self): for n in range(side, side + 10): for i in range(side): for j in range(side): - matrix[i, j] = Math.comb(n + j, i) + matrix[i, j] = Math.binom(n + j, i) solution = Linalg.solve(matrix, force) mult = np.dot(matrix, solution) np.testing.assert_allclose(mult, force) @@ -314,7 +320,7 @@ def test_solve_fraction(self): for n in range(side, side + 10): for i in range(side): for j in range(side): - matrix[i, j] = Fraction(Math.comb(n + j, i)) + matrix[i, j] = Fraction(Math.binom(n + j, i)) inverse = Linalg.invert(matrix) inverse = np.array(inverse, dtype="int64") matrix = np.array(matrix, dtype="int64") diff --git a/tests/core/test_polynomial.py b/tests/core/test_polynomial.py index 50387cb..1aa257b 100644 --- a/tests/core/test_polynomial.py +++ b/tests/core/test_polynomial.py @@ -1,5 +1,6 @@ import pytest +from pynurbs.core.custom_math import Math from pynurbs.core.polynomial import Polynomial, derivate, integrate, scale, shift diff --git a/tests/test_basis_functions.py b/tests/test_basis_functions.py index 77fb61d..fb8b463 100644 --- a/tests/test_basis_functions.py +++ b/tests/test_basis_functions.py @@ -4,7 +4,7 @@ import pytest from pynurbs.basis_functions import BasisFunctions -from pynurbs.core.custom_math import binom +from pynurbs.core.custom_math import Math from pynurbs.knotspace import GeneratorKnotVector @@ -301,7 +301,7 @@ def test_tablevalues_random_degree(self): matrix_good = np.zeros((len(nodestest), degree + 1)) for i, node in enumerate(nodestest): for j in range(degree + 1): - value = binom(degree, j) * (1 - node) ** (degree - j) * node**j + value = Math.binom(degree, j) * (1 - node) ** (degree - j) * node**j matrix_good[i, j] = value np.testing.assert_allclose(matrix_test, matrix_good) @@ -325,7 +325,7 @@ def test_shifted_scaled_bezier(self): matrix_good = np.zeros((len(nodestest), degree + 1)) for i, node in enumerate(nodesgood): for j in range(degree + 1): - value = binom(degree, j) * (1 - node) ** (degree - j) * node**j + value = Math.binom(degree, j) * (1 - node) ** (degree - j) * node**j matrix_good[i, j] = value np.testing.assert_allclose(matrix_test, matrix_good) From a07d1a3ff9920573b9ab942476019c316a988751 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 20:50:15 +0200 Subject: [PATCH 095/116] dev: update dependencies to include rbool --- pyproject.toml | 11 ++++++++++- 1 file changed, 10 insertions(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 2a0e717..99736af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -7,10 +7,19 @@ authors = ["Carlos Adir "] packages = [{ include = "pynurbs", from = "src" }] [tool.poetry.dependencies] -python = "^3.7" +python = "^3.9" +numpy = "^1" +rbool = "^0" [tool.poetry.dev-dependencies] +python = "^3.9" +numpy = "^1" +rbool = "^0" pytest = "^5.2" +pytest-order = "^1.0" +pytest-timeout = "^1.0" +pytest-coverage = "^0" +pytest-dependency = "^0.6" pre-commit = "^2.19.0" scriv = {extras = ["toml"], version = "^0.15.2"} From 5bd7ab24d4144b63f8a510b8a6432919247d1e1b Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 20:52:22 +0200 Subject: [PATCH 096/116] feat: include rbool as package to make boolean operations on real line --- src/pynurbs/operations/roots.py | 44 +++++++++++++++++++++++++++------ tests/operations/test_roots.py | 21 +++++++++++----- 2 files changed, 52 insertions(+), 13 deletions(-) diff --git a/src/pynurbs/operations/roots.py b/src/pynurbs/operations/roots.py index 48a6ae4..604fb8f 100644 --- a/src/pynurbs/operations/roots.py +++ b/src/pynurbs/operations/roots.py @@ -3,11 +3,13 @@ """ from fractions import Fraction -from numbers import Real -from typing import Tuple +from typing import Tuple, Union import numpy as np +import rbool +from ..core.custom_math import isscalar +from ..core.piecepoly import PiecewisePolynomial from ..core.polynomial import Polynomial @@ -38,7 +40,7 @@ def division(poly: Polynomial, doly: Polynomial) -> Tuple[Polynomial, Polynomial return qoly, Polynomial(roly[: doly.degree]) -def roots(poly: Polynomial) -> Tuple[Real, ...]: +def roots_polynomial(poly: Polynomial) -> rbool.SubSetR1: """ Finds the real roots of the given polynomial @@ -50,8 +52,36 @@ def roots(poly: Polynomial) -> Tuple[Real, ...]: >>> roots(x**3 - 6*x**2 + 11*x - 6) (1, 2, 3) """ - values = sorted(np.roots(tuple(poly)[::-1])) - for i, value in enumerate(values): + if not isinstance(poly, Polynomial): + raise TypeError + if not all(map(isscalar, poly)): + raise ValueError + if poly.degree == 0: + return rbool.WholeR1() if poly[0] == 0 else rbool.EmptyR1() + result = rbool.EmptyR1() + for value in np.roots(tuple(poly)[::-1]): if abs(round(1440 * value, 0) - 1440 * value) < 1e-6: - values[i] = Fraction(round(1440 * value), 1440) - return tuple(values) + value = Fraction(round(1440 * value), 1440) + result |= value + return result + + +def roots_piecewise(piece: PiecewisePolynomial) -> rbool.SubSetR1: + """ + Finds the real roots of the piecewise polynomial function + """ + result = rbool.EmptyR1() + for i, poly in enumerate(piece.functions): + knota, knotb = piece.knots[i], piece.knots[i + 1] + closed_right = i + 1 == len(piece.functions) + interval = rbool.IntervalR1(knota, knotb, True, closed_right) + result |= interval & roots_polynomial(poly) + return result + + +def roots(function: Union[Polynomial, PiecewisePolynomial]) -> rbool.SubSetR1: + if isinstance(function, Polynomial): + return roots_polynomial(function) + elif isinstance(function, PiecewisePolynomial): + return roots_piecewise(function) + raise ValueError diff --git a/tests/operations/test_roots.py b/tests/operations/test_roots.py index c622fc9..5db5ef1 100644 --- a/tests/operations/test_roots.py +++ b/tests/operations/test_roots.py @@ -1,5 +1,6 @@ import pytest +from pynurbs.core.piecepoly import PiecewisePolynomial from pynurbs.core.polynomial import Polynomial from pynurbs.operations.roots import division, roots @@ -67,12 +68,20 @@ def test_division(): @pytest.mark.dependency(depends=["test_begin"]) def test_roots(): x = Polynomial([0, 1]) - values = roots(x**2 + 3 * x + 2) - print(values) - assert values == (-2, -1) - values = roots(x**3 - 6 * x**2 + 11 * x - 6) - print(values) - assert values == (1, 2, 3) + + assert roots(Polynomial([0])) == (float("-inf"), float("inf")) + assert roots(Polynomial([1])) == {} + + poly = x**2 + 3 * x + 2 + assert roots(poly) == {-2, -1} + poly = x**3 - 6 * x**2 + 11 * x - 6 + assert roots(poly) == {1, 2, 3} + + functions = [x**2 + 3 * x + 2, x**3 - 6 * x**2 + 11 * x - 6] + piecewise = PiecewisePolynomial(functions, [-10, 0, 10]) + assert roots(piecewise) == {-2, -1, 1, 2, 3} + piecewise = PiecewisePolynomial(functions, [-1.5, 0, 2.5]) + assert roots(piecewise) == {-1, 1, 2} @pytest.mark.order(21) From 6aa1cd3b7cfbe6157f50824d5f216e4f51a55a04 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 20:53:08 +0200 Subject: [PATCH 097/116] del: remove manifold - it will be added only in future version --- src/pynurbs/core/manifold.py | 111 ----------------------------------- 1 file changed, 111 deletions(-) delete mode 100644 src/pynurbs/core/manifold.py diff --git a/src/pynurbs/core/manifold.py b/src/pynurbs/core/manifold.py deleted file mode 100644 index 81592ca..0000000 --- a/src/pynurbs/core/manifold.py +++ /dev/null @@ -1,111 +0,0 @@ -from numbers import Real -from typing import Any, Generic, Iterable, Tuple, Union - -import numpy as np - -from .spline_basis import ImmutableSplineBasis - - -def permutations(numbers: Tuple[int, ...]) -> Iterable[Tuple[int, ...]]: - """ - Computes the permutations of the numbers - - Example - ------- - >>> permutations([2]) - [(0, ), (1, )] - >>> permutations([2, 3]) - [(0, 0), (0, 1), (0, 2), (1, 0), (1, 1), (1, 2)] - """ - if len(numbers) > 1: - for index in range(numbers[0]): - for permu in permutations(numbers[1:]): - yield (index,) + permu - else: - for index in range(numbers[0]): - yield (index,) - - -class Container(Generic[Any]): - - @staticmethod - def __find_ndim(ctrlpoints: Any) -> int: - ndim = 0 - try: - while True: - iter(ctrlpoints) - ndim += 1 - ctrlpoints = ctrlpoints[0] - except Exception: - return ndim - - @staticmethod - def __find_shape(ctrlpoints: Any, ndim: int) -> Tuple[int, ...]: - shape = [0] * ndim - for i in range(ndim): - shape[i] = len(ctrlpoints) - ctrlpoints = ctrlpoints[0] - return tuple(shape) - - def __init__(self, ctrlpoints: Any, ndim: Union[None, int] = None): - if ndim is None: - ndim = Container.__find_ndim(ctrlpoints) - self.__shape = Container.__find_shape(ctrlpoints, ndim) - self.__ctrlpoints = ctrlpoints - - @property - def ndim(self) -> int: - return len(self.shape) - - @property - def shape(self) -> Tuple[int, ...]: - return self.__shape - - def __getitem__(self, indexs: Tuple[int, ...]) -> Any: - ctrlpoint = self.__ctrlpoints - for index in indexs: - ctrlpoint = ctrlpoint[index] - return ctrlpoint - - -class ImmuntableManifold: - """ - f(x1, x2, ..., xn) - """ - - def __init__( - self, allbasis: Iterable[ImmutableSplineBasis], ctrlpoints: Container[Any] - ): - - allbasis = tuple(allbasis) - if not all(isinstance(fun, ImmutableSplineBasis) for fun in allbasis): - raise TypeError - if isinstance(ctrlpoints, Container): - if ctrlpoints.ndim != len(allbasis): - raise ValueError - else: - ctrlpoints = Container(ctrlpoints, len(allbasis)) - self.__allbasis = allbasis - self.__ctrlpoints = ctrlpoints - - @property - def ndim(self) -> int: - return len(self.__allbasis) - - @property - def shape(self) -> Tuple[int, ...]: - return tuple(basis.npts for basis in self.__allbasis) - - def __call__(self, node: Tuple[Real, ...]) -> Any: - node = tuple(node) - if len(node) != self.ndim: - raise ValueError - result = 0 * self.__ctrlpoints[*(0,) * self.ndim] - basivalues = [basis(nodei) for nodei, basis in zip(node, self.__allbasis)] - for indexs in permutations(self.shape): - scalar = 1 - for i, index in enumerate(indexs): - scalar *= basivalues[i][index] - if scalar: - result += scalar * self.__ctrlpoints[*indexs] - return result From db78b11326fb82dcf357de7a78ac714555777c32 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 21:06:59 +0200 Subject: [PATCH 098/116] fix: ignore complex roots of polynomial --- src/pynurbs/operations/roots.py | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/pynurbs/operations/roots.py b/src/pynurbs/operations/roots.py index 604fb8f..f097633 100644 --- a/src/pynurbs/operations/roots.py +++ b/src/pynurbs/operations/roots.py @@ -60,9 +60,10 @@ def roots_polynomial(poly: Polynomial) -> rbool.SubSetR1: return rbool.WholeR1() if poly[0] == 0 else rbool.EmptyR1() result = rbool.EmptyR1() for value in np.roots(tuple(poly)[::-1]): - if abs(round(1440 * value, 0) - 1440 * value) < 1e-6: - value = Fraction(round(1440 * value), 1440) - result |= value + if not isinstance(value, complex): + if abs(round(1440 * value, 0) - 1440 * value) < 1e-6: + value = Fraction(round(1440 * value), 1440) + result |= value return result From 61742b0a3365d507954622c4e7831677c4047736 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 21:08:23 +0200 Subject: [PATCH 099/116] refactor: check of roots of weight curve --- src/pynurbs/curves/base.py | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py index ebe7f60..9313d41 100644 --- a/src/pynurbs/curves/base.py +++ b/src/pynurbs/curves/base.py @@ -5,12 +5,14 @@ from typing import Any, Iterable, Tuple, Union import numpy as np +import rbool from ..core.custom_math import isscalar, supports_linear_operation from ..core.spline_basis import ImmutableSplineBasis from ..knotspace import KnotVector from ..operations import heavy from ..operations.knotvector import decrease_degree, increase_degree +from ..operations.roots import roots_piecewise from ..operations.tools import vectorize @@ -44,6 +46,7 @@ def __init__( self.__ctrlpoints = ctrlpoints self.__weights = weights self.tolerance = 1e-9 + self.__denominator = None @vectorize(1, 0) def __call__(self, node: Real) -> Any: @@ -421,17 +424,22 @@ def weights(self, value: Tuple[float]): if value is None: self.__weights = None return + value = tuple(value) if not all(map(isscalar, value)): raise ValueError if not all(number > 0 for number in value): raise ValueError + if len(value) != self.npts: + raise ValueError # Verify if there's roots - vector = tuple(self.knotvector) - roots = heavy.find_roots(vector, value) - if roots: - error_msg = f"Zero division at nodes {roots}" - raise ValueError(error_msg) + basis = ImmutableSplineBasis(self.knotvector.internal) + denominator = 0 + for i, weight in enumerate(value): + denominator += weight * basis[i] + roots_values = roots_piecewise(denominator) + if roots_values != rbool.EmptyR1(): + raise ValueError(f"Zero division at {roots_values}") self.__weights = tuple(value) @ctrlpoints.setter From 33546b04feb750a09ef11ab9557a8c7d645f2c19 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 21:10:27 +0200 Subject: [PATCH 100/116] del: remove unused code --- src/pynurbs/operations/heavy.py | 97 --------------------------------- 1 file changed, 97 deletions(-) diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index 8a64ddf..4a1a01e 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -16,92 +16,6 @@ from .least_square import eval_spline_nodes, spline2spline -def find_roots( - knotvector: ImmutableKnotVector, ctrlvalues: Tuple[float] -) -> Tuple[float]: - """ - Finds the roots of given a spline function - Each subinterval [u_{k}, u_{k+1}] can be interpoled - by a polynomial of degree p. - Taking out the case of constant equal - - We do it by sampling - """ - ctrlvalues = tuple(ctrlvalues) - if not all(map(isscalar, ctrlvalues)): - raise ValueError - knotvector = ImmutableKnotVector(knotvector) - assert isinstance(ctrlvalues, tuple) - tolerance = 1e-8 - for value in ctrlvalues: - float(value) - ctrlvalues = np.array(ctrlvalues, dtype="float64") - knots = knotvector.knots - degree = knotvector.degree - nsample = 100 - nodes0to1 = NodeSample.open_linspace(nsample) - manynodes = [] - for start, end in zip(knots[:-1], knots[1:]): - nodes = [start + (end - start) * node for node in nodes0to1] - manynodes += nodes - manynodes = tuple(sorted(manynodes + list(knots))) - matrixeval = eval_spline_nodes(knotvector, manynodes, degree) - manyvalues = np.dot(np.transpose(matrixeval), ctrlvalues) - manyvalues = tuple(manyvalues) - while 0 in manyvalues: - index = manyvalues.index(0) - manyvalues.pop(index) - manynodes.pop(index) - # return tuple(sorted(manynodes)) - - # Bissection algorithm - lefts = [] # a - righs = [] # b - fleft = [] # f(a) - frigh = [] # f(b) - maxdist = 0 - for i, (aval, bval) in enumerate(zip(manyvalues[:-1], manyvalues[1:])): - if aval * bval < 0: - maxdist = max(maxdist, manynodes[i + 1] - manynodes[i]) - lefts.append(manynodes[i]) - righs.append(manynodes[i + 1]) - fleft.append(aval) - frigh.append(bval) - nintervs = len(lefts) - if nintervs == 0: - return tuple() - lefts = np.array(lefts, dtype="float64") - righs = np.array(righs, dtype="float64") - fleft = np.array(fleft, dtype="float64") - frigh = np.array(frigh, dtype="float64") - niters = 1 + int(np.ceil(np.log2(maxdist / tolerance))) - for i in range(niters): - mednodes = (lefts + righs) / 2 - matrixeval = eval_spline_nodes(knotvector, tuple(mednodes), degree) - medvals = np.dot(np.transpose(matrixeval), ctrlvalues) - for i, medval in enumerate(medvals): - if medval == 0: - lefts[i] = mednodes[i] - righs[i] = mednodes[i] - fleft[i] = 0 - frigh[i] = 0 - elif fleft[i] * medval < 0: - righs[i] = mednodes[i] - frigh[i] = medval - else: - lefts[i] = mednodes[i] - fleft[i] = medval - roots = (lefts + righs) / 2 - filtered_roots = [] - for root in roots: - for filtroot in filtered_roots: - if abs(root - filtroot) < tolerance: - break - else: - filtered_roots.append(root) - return tuple(sorted(filtered_roots)) - - class Operations: """ Contains algorithms to @@ -258,17 +172,6 @@ def knot_insert(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix knotvector = opekv.insert_knots(knotvector, times * [node]) return totuple(matrix) - def knot_remove(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix2D": - """ """ - knotvector = ImmutableKnotVector(knotvector) - if not all( - knotvector.knots[0] <= node <= knotvector.knots[-1] for node in nodes - ): - raise ValueError(f"Invalid nodes {nodes} in knotvector {knotvector}") - newknotvector = opekv.remove_knots(knotvector, nodes) - matrix, _ = spline2spline(knotvector, newknotvector) - return totuple(matrix) - def degree_increase_bezier_once(knotvector: ImmutableKnotVector) -> "Matrix2D": knotvector = ImmutableKnotVector(knotvector) one = knotvector[-1] - knotvector[0] From b34be8408084d754a21274416e0bb1e40fc7ade2 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 21:31:59 +0200 Subject: [PATCH 101/116] dev: fix coverage requirement --- pyproject.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pyproject.toml b/pyproject.toml index 99736af..17d65aa 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -16,9 +16,9 @@ python = "^3.9" numpy = "^1" rbool = "^0" pytest = "^5.2" +coverage = "^7" pytest-order = "^1.0" pytest-timeout = "^1.0" -pytest-coverage = "^0" pytest-dependency = "^0.6" pre-commit = "^2.19.0" scriv = {extras = ["toml"], version = "^0.15.2"} From f9f9d431de367e48c9f42970acf59f4c0b9f5d8e Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 21:32:21 +0200 Subject: [PATCH 102/116] dev: fix poetry lock file --- README.md | 7 + poetry.lock | 994 +++++++++++++++++++++++++++++++--------------------- 2 files changed, 609 insertions(+), 392 deletions(-) diff --git a/README.md b/README.md index b4605c4..0b57914 100644 --- a/README.md +++ b/README.md @@ -47,6 +47,13 @@ The documentation can be found at [pynurbs.readthedocs.io][docs-url] Please use the [Issues][issues-url] or refer to the email ```compmecgit@gmail.com``` +For developers, please use `poetry` as virtual enviroment + +``` +poetry shell +pytest +``` + diff --git a/poetry.lock b/poetry.lock index 779305b..80cb26d 100644 --- a/poetry.lock +++ b/poetry.lock @@ -1,71 +1,181 @@ +# This file is automatically @generated by Poetry 1.8.2 and should not be changed by hand. + [[package]] name = "atomicwrites" version = "1.4.1" description = "Atomic file writes." -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" +files = [ + {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, +] [[package]] name = "attrs" -version = "22.1.0" +version = "25.3.0" description = "Classes Without Boilerplate" -category = "dev" optional = false -python-versions = ">=3.5" +python-versions = ">=3.8" +files = [ + {file = "attrs-25.3.0-py3-none-any.whl", hash = "sha256:427318ce031701fea540783410126f03899a97ffc6f61596ad581ac2e40e3bc3"}, + {file = "attrs-25.3.0.tar.gz", hash = "sha256:75d7cefc7fb576747b2c81b4442d4d4a1ce0900973527c011d1030fd3bf4af1b"}, +] [package.extras] -dev = ["cloudpickle", "coverage[toml] (>=5.0.2)", "furo", "hypothesis", "mypy (>=0.900,!=0.940)", "pre-commit", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "sphinx", "sphinx-notfound-page", "zope.interface"] -docs = ["furo", "sphinx", "sphinx-notfound-page", "zope.interface"] -tests = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "zope.interface"] -tests-no-zope = ["cloudpickle", "coverage[toml] (>=5.0.2)", "hypothesis", "mypy (>=0.900,!=0.940)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins"] +benchmark = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-codspeed", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +cov = ["cloudpickle", "coverage[toml] (>=5.3)", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +dev = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pre-commit-uv", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +docs = ["cogapp", "furo", "myst-parser", "sphinx", "sphinx-notfound-page", "sphinxcontrib-towncrier", "towncrier"] +tests = ["cloudpickle", "hypothesis", "mypy (>=1.11.1)", "pympler", "pytest (>=4.3.0)", "pytest-mypy-plugins", "pytest-xdist[psutil]"] +tests-mypy = ["mypy (>=1.11.1)", "pytest-mypy-plugins"] [[package]] name = "certifi" -version = "2022.9.24" +version = "2025.6.15" description = "Python package for providing Mozilla's CA Bundle." -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.7" +files = [ + {file = "certifi-2025.6.15-py3-none-any.whl", hash = "sha256:2e0c7ce7cb5d8f8634ca55d2ba7e6ec2689a2fd6537d8dec1296a477a4910057"}, + {file = "certifi-2025.6.15.tar.gz", hash = "sha256:d747aa5a8b9bbbb1bb8c22bb13e22bd1f18e9796defa16bab421f7f7a317323b"}, +] [[package]] name = "cfgv" -version = "3.3.1" +version = "3.4.0" description = "Validate configuration and produce human readable error messages." -category = "dev" optional = false -python-versions = ">=3.6.1" +python-versions = ">=3.8" +files = [ + {file = "cfgv-3.4.0-py2.py3-none-any.whl", hash = "sha256:b7265b1f29fd3316bfcd2b330d63d024f2bfd8bcb8b0272f8e19a504856c48f9"}, + {file = "cfgv-3.4.0.tar.gz", hash = "sha256:e52591d4c5f5dead8e0f673fb16db7949d2cfb3f7da4582893288f0ded8fe560"}, +] [[package]] name = "charset-normalizer" -version = "2.1.1" +version = "3.4.2" description = "The Real First Universal Charset Detector. Open, modern and actively maintained alternative to Chardet." -category = "dev" optional = false -python-versions = ">=3.6.0" - -[package.extras] -unicode-backport = ["unicodedata2"] +python-versions = ">=3.7" +files = [ + {file = "charset_normalizer-3.4.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7c48ed483eb946e6c04ccbe02c6b4d1d48e51944b6db70f697e089c193404941"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:b2d318c11350e10662026ad0eb71bb51c7812fc8590825304ae0bdd4ac283acd"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:9cbfacf36cb0ec2897ce0ebc5d08ca44213af24265bd56eca54bee7923c48fd6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:18dd2e350387c87dabe711b86f83c9c78af772c748904d372ade190b5c7c9d4d"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:8075c35cd58273fee266c58c0c9b670947c19df5fb98e7b66710e04ad4e9ff86"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:5bf4545e3b962767e5c06fe1738f951f77d27967cb2caa64c28be7c4563e162c"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:7a6ab32f7210554a96cd9e33abe3ddd86732beeafc7a28e9955cdf22ffadbab0"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:b33de11b92e9f75a2b545d6e9b6f37e398d86c3e9e9653c4864eb7e89c5773ef"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_ppc64le.whl", hash = "sha256:8755483f3c00d6c9a77f490c17e6ab0c8729e39e6390328e42521ef175380ae6"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_s390x.whl", hash = "sha256:68a328e5f55ec37c57f19ebb1fdc56a248db2e3e9ad769919a58672958e8f366"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:21b2899062867b0e1fde9b724f8aecb1af14f2778d69aacd1a5a1853a597a5db"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win32.whl", hash = "sha256:e8082b26888e2f8b36a042a58307d5b917ef2b1cacab921ad3323ef91901c71a"}, + {file = "charset_normalizer-3.4.2-cp310-cp310-win_amd64.whl", hash = "sha256:f69a27e45c43520f5487f27627059b64aaf160415589230992cec34c5e18a509"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:be1e352acbe3c78727a16a455126d9ff83ea2dfdcbc83148d2982305a04714c2"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:aa88ca0b1932e93f2d961bf3addbb2db902198dca337d88c89e1559e066e7645"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:d524ba3f1581b35c03cb42beebab4a13e6cdad7b36246bd22541fa585a56cccd"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:28a1005facc94196e1fb3e82a3d442a9d9110b8434fc1ded7a24a2983c9888d8"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fdb20a30fe1175ecabed17cbf7812f7b804b8a315a25f24678bcdf120a90077f"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:0f5d9ed7f254402c9e7d35d2f5972c9bbea9040e99cd2861bd77dc68263277c7"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:efd387a49825780ff861998cd959767800d54f8308936b21025326de4b5a42b9"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:f0aa37f3c979cf2546b73e8222bbfa3dc07a641585340179d768068e3455e544"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_ppc64le.whl", hash = "sha256:e70e990b2137b29dc5564715de1e12701815dacc1d056308e2b17e9095372a82"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_s390x.whl", hash = "sha256:0c8c57f84ccfc871a48a47321cfa49ae1df56cd1d965a09abe84066f6853b9c0"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:6b66f92b17849b85cad91259efc341dce9c1af48e2173bf38a85c6329f1033e5"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win32.whl", hash = "sha256:daac4765328a919a805fa5e2720f3e94767abd632ae410a9062dff5412bae65a"}, + {file = "charset_normalizer-3.4.2-cp311-cp311-win_amd64.whl", hash = "sha256:e53efc7c7cee4c1e70661e2e112ca46a575f90ed9ae3fef200f2a25e954f4b28"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:0c29de6a1a95f24b9a1aa7aefd27d2487263f00dfd55a77719b530788f75cff7"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:cddf7bd982eaa998934a91f69d182aec997c6c468898efe6679af88283b498d3"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:fcbe676a55d7445b22c10967bceaaf0ee69407fbe0ece4d032b6eb8d4565982a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d41c4d287cfc69060fa91cae9683eacffad989f1a10811995fa309df656ec214"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4e594135de17ab3866138f496755f302b72157d115086d100c3f19370839dd3a"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cf713fe9a71ef6fd5adf7a79670135081cd4431c2943864757f0fa3a65b1fafd"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:a370b3e078e418187da8c3674eddb9d983ec09445c99a3a263c2011993522981"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:a955b438e62efdf7e0b7b52a64dc5c3396e2634baa62471768a64bc2adb73d5c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_ppc64le.whl", hash = "sha256:7222ffd5e4de8e57e03ce2cef95a4c43c98fcb72ad86909abdfc2c17d227fc1b"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_s390x.whl", hash = "sha256:bee093bf902e1d8fc0ac143c88902c3dfc8941f7ea1d6a8dd2bcb786d33db03d"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:dedb8adb91d11846ee08bec4c8236c8549ac721c245678282dcb06b221aab59f"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win32.whl", hash = "sha256:db4c7bf0e07fc3b7d89ac2a5880a6a8062056801b83ff56d8464b70f65482b6c"}, + {file = "charset_normalizer-3.4.2-cp312-cp312-win_amd64.whl", hash = "sha256:5a9979887252a82fefd3d3ed2a8e3b937a7a809f65dcb1e068b090e165bbe99e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:926ca93accd5d36ccdabd803392ddc3e03e6d4cd1cf17deff3b989ab8e9dbcf0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:eba9904b0f38a143592d9fc0e19e2df0fa2e41c3c3745554761c5f6447eedabf"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:3fddb7e2c84ac87ac3a947cb4e66d143ca5863ef48e4a5ecb83bd48619e4634e"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:98f862da73774290f251b9df8d11161b6cf25b599a66baf087c1ffe340e9bfd1"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6c9379d65defcab82d07b2a9dfbfc2e95bc8fe0ebb1b176a3190230a3ef0e07c"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e635b87f01ebc977342e2697d05b56632f5f879a4f15955dfe8cef2448b51691"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:1c95a1e2902a8b722868587c0e1184ad5c55631de5afc0eb96bc4b0d738092c0"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:ef8de666d6179b009dce7bcb2ad4c4a779f113f12caf8dc77f0162c29d20490b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_ppc64le.whl", hash = "sha256:32fc0341d72e0f73f80acb0a2c94216bd704f4f0bce10aedea38f30502b271ff"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_s390x.whl", hash = "sha256:289200a18fa698949d2b39c671c2cc7a24d44096784e76614899a7ccf2574b7b"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:4a476b06fbcf359ad25d34a057b7219281286ae2477cc5ff5e3f70a246971148"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win32.whl", hash = "sha256:aaeeb6a479c7667fbe1099af9617c83aaca22182d6cf8c53966491a0f1b7ffb7"}, + {file = "charset_normalizer-3.4.2-cp313-cp313-win_amd64.whl", hash = "sha256:aa6af9e7d59f9c12b33ae4e9450619cf2488e2bbe9b44030905877f0b2324980"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1cad5f45b3146325bb38d6855642f6fd609c3f7cad4dbaf75549bf3b904d3184"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:b2680962a4848b3c4f155dc2ee64505a9c57186d0d56b43123b17ca3de18f0fa"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:36b31da18b8890a76ec181c3cf44326bf2c48e36d393ca1b72b3f484113ea344"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f4074c5a429281bf056ddd4c5d3b740ebca4d43ffffe2ef4bf4d2d05114299da"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c9e36a97bee9b86ef9a1cf7bb96747eb7a15c2f22bdb5b516434b00f2a599f02"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_aarch64.whl", hash = "sha256:1b1bde144d98e446b056ef98e59c256e9294f6b74d7af6846bf5ffdafd687a7d"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_i686.whl", hash = "sha256:915f3849a011c1f593ab99092f3cecfcb4d65d8feb4a64cf1bf2d22074dc0ec4"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_ppc64le.whl", hash = "sha256:fb707f3e15060adf5b7ada797624a6c6e0138e2a26baa089df64c68ee98e040f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_s390x.whl", hash = "sha256:25a23ea5c7edc53e0f29bae2c44fcb5a1aa10591aae107f2a2b2583a9c5cbc64"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-musllinux_1_2_x86_64.whl", hash = "sha256:770cab594ecf99ae64c236bc9ee3439c3f46be49796e265ce0cc8bc17b10294f"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win32.whl", hash = "sha256:6a0289e4589e8bdfef02a80478f1dfcb14f0ab696b5a00e1f4b8a14a307a3c58"}, + {file = "charset_normalizer-3.4.2-cp37-cp37m-win_amd64.whl", hash = "sha256:6fc1f5b51fa4cecaa18f2bd7a003f3dd039dd615cd69a2afd6d3b19aed6775f2"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:76af085e67e56c8816c3ccf256ebd136def2ed9654525348cfa744b6802b69eb"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e45ba65510e2647721e35323d6ef54c7974959f6081b58d4ef5d87c60c84919a"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:046595208aae0120559a67693ecc65dd75d46f7bf687f159127046628178dc45"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:75d10d37a47afee94919c4fab4c22b9bc2a8bf7d4f46f87363bcf0573f3ff4f5"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6333b3aa5a12c26b2a4d4e7335a28f1475e0e5e17d69d55141ee3cab736f66d1"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e8323a9b031aa0393768b87f04b4164a40037fb2a3c11ac06a03ffecd3618027"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_aarch64.whl", hash = "sha256:24498ba8ed6c2e0b56d4acbf83f2d989720a93b41d712ebd4f4979660db4417b"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_i686.whl", hash = "sha256:844da2b5728b5ce0e32d863af26f32b5ce61bc4273a9c720a9f3aa9df73b1455"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_ppc64le.whl", hash = "sha256:65c981bdbd3f57670af8b59777cbfae75364b483fa8a9f420f08094531d54a01"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_s390x.whl", hash = "sha256:3c21d4fca343c805a52c0c78edc01e3477f6dd1ad7c47653241cf2a206d4fc58"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-musllinux_1_2_x86_64.whl", hash = "sha256:dc7039885fa1baf9be153a0626e337aa7ec8bf96b0128605fb0d77788ddc1681"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win32.whl", hash = "sha256:8272b73e1c5603666618805fe821edba66892e2870058c94c53147602eab29c7"}, + {file = "charset_normalizer-3.4.2-cp38-cp38-win_amd64.whl", hash = "sha256:70f7172939fdf8790425ba31915bfbe8335030f05b9913d7ae00a87d4395620a"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:005fa3432484527f9732ebd315da8da8001593e2cf46a3d817669f062c3d9ed4"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:e92fca20c46e9f5e1bb485887d074918b13543b1c2a1185e69bb8d17ab6236a7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_ppc64le.manylinux2014_ppc64le.whl", hash = "sha256:50bf98d5e563b83cc29471fa114366e6806bc06bc7a25fd59641e41445327836"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:721c76e84fe669be19c5791da68232ca2e05ba5185575086e384352e2c309597"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:82d8fd25b7f4675d0c47cf95b594d4e7b158aca33b76aa63d07186e13c0e0ab7"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b3daeac64d5b371dea99714f08ffc2c208522ec6b06fbc7866a450dd446f5c0f"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:dccab8d5fa1ef9bfba0590ecf4d46df048d18ffe3eec01eeb73a42e0d9e7a8ba"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:aaf27faa992bfee0264dc1f03f4c75e9fcdda66a519db6b957a3f826e285cf12"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_ppc64le.whl", hash = "sha256:eb30abc20df9ab0814b5a2524f23d75dcf83cde762c161917a2b4b7b55b1e518"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_s390x.whl", hash = "sha256:c72fbbe68c6f32f251bdc08b8611c7b3060612236e960ef848e0a517ddbe76c5"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:982bb1e8b4ffda883b3d0a521e23abcd6fd17418f6d2c4118d257a10199c0ce3"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win32.whl", hash = "sha256:43e0933a0eff183ee85833f341ec567c0980dae57c464d8a508e1b2ceb336471"}, + {file = "charset_normalizer-3.4.2-cp39-cp39-win_amd64.whl", hash = "sha256:d11b54acf878eef558599658b0ffca78138c8c3655cf4f3a4a673c437e67732e"}, + {file = "charset_normalizer-3.4.2-py3-none-any.whl", hash = "sha256:7f56930ab0abd1c45cd15be65cc741c28b1c9a34876ce8c17a2fa107810c0af0"}, + {file = "charset_normalizer-3.4.2.tar.gz", hash = "sha256:5baececa9ecba31eff645232d59845c07aa030f0c81ee70184a90d35099a0e63"}, +] [[package]] name = "click" -version = "8.1.3" +version = "8.1.8" description = "Composable command line interface toolkit" -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "click-8.1.8-py3-none-any.whl", hash = "sha256:63c132bbbed01578a06712a2d1f497bb62d9c1c0d329b7903a866228027263b2"}, + {file = "click-8.1.8.tar.gz", hash = "sha256:ed53c9d8990d83c2a27deae68e4ee337473f6330c040a31d4225c9574d16096a"}, +] [package.dependencies] colorama = {version = "*", markers = "platform_system == \"Windows\""} -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} [[package]] name = "click-log" version = "0.4.0" description = "Logging integration for Click" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "click-log-0.4.0.tar.gz", hash = "sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975"}, + {file = "click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756"}, +] [package.dependencies] click = "*" @@ -74,73 +184,157 @@ click = "*" name = "colorama" version = "0.4.6" description = "Cross-platform colored terminal text." -category = "dev" optional = false python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, + {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, +] + +[[package]] +name = "coverage" +version = "7.9.2" +description = "Code coverage measurement for Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "coverage-7.9.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:66283a192a14a3854b2e7f3418d7db05cdf411012ab7ff5db98ff3b181e1f912"}, + {file = "coverage-7.9.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:4e01d138540ef34fcf35c1aa24d06c3de2a4cffa349e29a10056544f35cca15f"}, + {file = "coverage-7.9.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f22627c1fe2745ee98d3ab87679ca73a97e75ca75eb5faee48660d060875465f"}, + {file = "coverage-7.9.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:4b1c2d8363247b46bd51f393f86c94096e64a1cf6906803fa8d5a9d03784bdbf"}, + {file = "coverage-7.9.2-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c10c882b114faf82dbd33e876d0cbd5e1d1ebc0d2a74ceef642c6152f3f4d547"}, + {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:de3c0378bdf7066c3988d66cd5232d161e933b87103b014ab1b0b4676098fa45"}, + {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:1e2f097eae0e5991e7623958a24ced3282676c93c013dde41399ff63e230fcf2"}, + {file = "coverage-7.9.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:28dc1f67e83a14e7079b6cea4d314bc8b24d1aed42d3582ff89c0295f09b181e"}, + {file = "coverage-7.9.2-cp310-cp310-win32.whl", hash = "sha256:bf7d773da6af9e10dbddacbf4e5cab13d06d0ed93561d44dae0188a42c65be7e"}, + {file = "coverage-7.9.2-cp310-cp310-win_amd64.whl", hash = "sha256:0c0378ba787681ab1897f7c89b415bd56b0b2d9a47e5a3d8dc0ea55aac118d6c"}, + {file = "coverage-7.9.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:a7a56a2964a9687b6aba5b5ced6971af308ef6f79a91043c05dd4ee3ebc3e9ba"}, + {file = "coverage-7.9.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:123d589f32c11d9be7fe2e66d823a236fe759b0096f5db3fb1b75b2fa414a4fa"}, + {file = "coverage-7.9.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:333b2e0ca576a7dbd66e85ab402e35c03b0b22f525eed82681c4b866e2e2653a"}, + {file = "coverage-7.9.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:326802760da234baf9f2f85a39e4a4b5861b94f6c8d95251f699e4f73b1835dc"}, + {file = "coverage-7.9.2-cp311-cp311-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:19e7be4cfec248df38ce40968c95d3952fbffd57b400d4b9bb580f28179556d2"}, + {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:0b4a4cb73b9f2b891c1788711408ef9707666501ba23684387277ededab1097c"}, + {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:2c8937fa16c8c9fbbd9f118588756e7bcdc7e16a470766a9aef912dd3f117dbd"}, + {file = "coverage-7.9.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:42da2280c4d30c57a9b578bafd1d4494fa6c056d4c419d9689e66d775539be74"}, + {file = "coverage-7.9.2-cp311-cp311-win32.whl", hash = "sha256:14fa8d3da147f5fdf9d298cacc18791818f3f1a9f542c8958b80c228320e90c6"}, + {file = "coverage-7.9.2-cp311-cp311-win_amd64.whl", hash = "sha256:549cab4892fc82004f9739963163fd3aac7a7b0df430669b75b86d293d2df2a7"}, + {file = "coverage-7.9.2-cp311-cp311-win_arm64.whl", hash = "sha256:c2667a2b913e307f06aa4e5677f01a9746cd08e4b35e14ebcde6420a9ebb4c62"}, + {file = "coverage-7.9.2-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:ae9eb07f1cfacd9cfe8eaee6f4ff4b8a289a668c39c165cd0c8548484920ffc0"}, + {file = "coverage-7.9.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:9ce85551f9a1119f02adc46d3014b5ee3f765deac166acf20dbb851ceb79b6f3"}, + {file = "coverage-7.9.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:f8f6389ac977c5fb322e0e38885fbbf901743f79d47f50db706e7644dcdcb6e1"}, + {file = "coverage-7.9.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:ff0d9eae8cdfcd58fe7893b88993723583a6ce4dfbfd9f29e001922544f95615"}, + {file = "coverage-7.9.2-cp312-cp312-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:fae939811e14e53ed8a9818dad51d434a41ee09df9305663735f2e2d2d7d959b"}, + {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:31991156251ec202c798501e0a42bbdf2169dcb0f137b1f5c0f4267f3fc68ef9"}, + {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:d0d67963f9cbfc7c7f96d4ac74ed60ecbebd2ea6eeb51887af0f8dce205e545f"}, + {file = "coverage-7.9.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:49b752a2858b10580969ec6af6f090a9a440a64a301ac1528d7ca5f7ed497f4d"}, + {file = "coverage-7.9.2-cp312-cp312-win32.whl", hash = "sha256:88d7598b8ee130f32f8a43198ee02edd16d7f77692fa056cb779616bbea1b355"}, + {file = "coverage-7.9.2-cp312-cp312-win_amd64.whl", hash = "sha256:9dfb070f830739ee49d7c83e4941cc767e503e4394fdecb3b54bfdac1d7662c0"}, + {file = "coverage-7.9.2-cp312-cp312-win_arm64.whl", hash = "sha256:4e2c058aef613e79df00e86b6d42a641c877211384ce5bd07585ed7ba71ab31b"}, + {file = "coverage-7.9.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:985abe7f242e0d7bba228ab01070fde1d6c8fa12f142e43debe9ed1dde686038"}, + {file = "coverage-7.9.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:82c3939264a76d44fde7f213924021ed31f55ef28111a19649fec90c0f109e6d"}, + {file = "coverage-7.9.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ae5d563e970dbe04382f736ec214ef48103d1b875967c89d83c6e3f21706d5b3"}, + {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:bdd612e59baed2a93c8843c9a7cb902260f181370f1d772f4842987535071d14"}, + {file = "coverage-7.9.2-cp313-cp313-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:256ea87cb2a1ed992bcdfc349d8042dcea1b80436f4ddf6e246d6bee4b5d73b6"}, + {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:f44ae036b63c8ea432f610534a2668b0c3aee810e7037ab9d8ff6883de480f5b"}, + {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:82d76ad87c932935417a19b10cfe7abb15fd3f923cfe47dbdaa74ef4e503752d"}, + {file = "coverage-7.9.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:619317bb86de4193debc712b9e59d5cffd91dc1d178627ab2a77b9870deb2868"}, + {file = "coverage-7.9.2-cp313-cp313-win32.whl", hash = "sha256:0a07757de9feb1dfafd16ab651e0f628fd7ce551604d1bf23e47e1ddca93f08a"}, + {file = "coverage-7.9.2-cp313-cp313-win_amd64.whl", hash = "sha256:115db3d1f4d3f35f5bb021e270edd85011934ff97c8797216b62f461dd69374b"}, + {file = "coverage-7.9.2-cp313-cp313-win_arm64.whl", hash = "sha256:48f82f889c80af8b2a7bb6e158d95a3fbec6a3453a1004d04e4f3b5945a02694"}, + {file = "coverage-7.9.2-cp313-cp313t-macosx_10_13_x86_64.whl", hash = "sha256:55a28954545f9d2f96870b40f6c3386a59ba8ed50caf2d949676dac3ecab99f5"}, + {file = "coverage-7.9.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:cdef6504637731a63c133bb2e6f0f0214e2748495ec15fe42d1e219d1b133f0b"}, + {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:bcd5ebe66c7a97273d5d2ddd4ad0ed2e706b39630ed4b53e713d360626c3dbb3"}, + {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:9303aed20872d7a3c9cb39c5d2b9bdbe44e3a9a1aecb52920f7e7495410dfab8"}, + {file = "coverage-7.9.2-cp313-cp313t-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bc18ea9e417a04d1920a9a76fe9ebd2f43ca505b81994598482f938d5c315f46"}, + {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6406cff19880aaaadc932152242523e892faff224da29e241ce2fca329866584"}, + {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:2d0d4f6ecdf37fcc19c88fec3e2277d5dee740fb51ffdd69b9579b8c31e4232e"}, + {file = "coverage-7.9.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:c33624f50cf8de418ab2b4d6ca9eda96dc45b2c4231336bac91454520e8d1fac"}, + {file = "coverage-7.9.2-cp313-cp313t-win32.whl", hash = "sha256:1df6b76e737c6a92210eebcb2390af59a141f9e9430210595251fbaf02d46926"}, + {file = "coverage-7.9.2-cp313-cp313t-win_amd64.whl", hash = "sha256:f5fd54310b92741ebe00d9c0d1d7b2b27463952c022da6d47c175d246a98d1bd"}, + {file = "coverage-7.9.2-cp313-cp313t-win_arm64.whl", hash = "sha256:c48c2375287108c887ee87d13b4070a381c6537d30e8487b24ec721bf2a781cb"}, + {file = "coverage-7.9.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:ddc39510ac922a5c4c27849b739f875d3e1d9e590d1e7b64c98dadf037a16cce"}, + {file = "coverage-7.9.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a535c0c7364acd55229749c2b3e5eebf141865de3a8f697076a3291985f02d30"}, + {file = "coverage-7.9.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:df0f9ef28e0f20c767ccdccfc5ae5f83a6f4a2fbdfbcbcc8487a8a78771168c8"}, + {file = "coverage-7.9.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:2f3da12e0ccbcb348969221d29441ac714bbddc4d74e13923d3d5a7a0bebef7a"}, + {file = "coverage-7.9.2-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:0a17eaf46f56ae0f870f14a3cbc2e4632fe3771eab7f687eda1ee59b73d09fe4"}, + {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:669135a9d25df55d1ed56a11bf555f37c922cf08d80799d4f65d77d7d6123fcf"}, + {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:9d3a700304d01a627df9db4322dc082a0ce1e8fc74ac238e2af39ced4c083193"}, + {file = "coverage-7.9.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:71ae8b53855644a0b1579d4041304ddc9995c7b21c8a1f16753c4d8903b4dfed"}, + {file = "coverage-7.9.2-cp39-cp39-win32.whl", hash = "sha256:dd7a57b33b5cf27acb491e890720af45db05589a80c1ffc798462a765be6d4d7"}, + {file = "coverage-7.9.2-cp39-cp39-win_amd64.whl", hash = "sha256:f65bb452e579d5540c8b37ec105dd54d8b9307b07bcaa186818c104ffda22441"}, + {file = "coverage-7.9.2-pp39.pp310.pp311-none-any.whl", hash = "sha256:8a1166db2fb62473285bcb092f586e081e92656c7dfa8e9f62b4d39d7e6b5050"}, + {file = "coverage-7.9.2-py3-none-any.whl", hash = "sha256:e425cd5b00f6fc0ed7cdbd766c70be8baab4b7839e4d4fe5fac48581dd968ea4"}, + {file = "coverage-7.9.2.tar.gz", hash = "sha256:997024fa51e3290264ffd7492ec97d0690293ccd2b45a6cd7d82d945a4a80c8b"}, +] + +[package.extras] +toml = ["tomli"] [[package]] name = "distlib" -version = "0.3.6" +version = "0.3.9" description = "Distribution utilities" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "distlib-0.3.9-py2.py3-none-any.whl", hash = "sha256:47f8c22fd27c27e25a65601af709b38e4f0a45ea4fc2e710f65755fa8caaaf87"}, + {file = "distlib-0.3.9.tar.gz", hash = "sha256:a60f20dea646b8a33f3e7772f74dc0b2d0772d2837ee1342a00645c81edf9403"}, +] [[package]] name = "filelock" -version = "3.8.0" +version = "3.18.0" description = "A platform independent file lock." -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +files = [ + {file = "filelock-3.18.0-py3-none-any.whl", hash = "sha256:c401f4f8377c4464e6db25fff06205fd89bdd83b65eb0488ed1b160f780e21de"}, + {file = "filelock-3.18.0.tar.gz", hash = "sha256:adbc88eabb99d2fec8c9c1b229b171f18afa655400173ddc653d5d01501fb9f2"}, +] [package.extras] -docs = ["furo (>=2022.6.21)", "sphinx (>=5.1.1)", "sphinx-autodoc-typehints (>=1.19.1)"] -testing = ["covdefaults (>=2.2)", "coverage (>=6.4.2)", "pytest (>=7.1.2)", "pytest-cov (>=3)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2024.8.6)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +testing = ["covdefaults (>=2.3)", "coverage (>=7.6.10)", "diff-cover (>=9.2.1)", "pytest (>=8.3.4)", "pytest-asyncio (>=0.25.2)", "pytest-cov (>=6)", "pytest-mock (>=3.14)", "pytest-timeout (>=2.3.1)", "virtualenv (>=20.28.1)"] +typing = ["typing-extensions (>=4.12.2)"] [[package]] name = "identify" -version = "2.5.9" +version = "2.6.12" description = "File identification library for Python" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +files = [ + {file = "identify-2.6.12-py2.py3-none-any.whl", hash = "sha256:ad9672d5a72e0d2ff7c5c8809b62dfa60458626352fb0eb7b55e69bdc45334a2"}, + {file = "identify-2.6.12.tar.gz", hash = "sha256:d8de45749f1efb108badef65ee8386f0f7bb19a7f26185f74de6367bffbaf0e6"}, +] [package.extras] license = ["ukkonen"] [[package]] name = "idna" -version = "3.4" +version = "3.10" description = "Internationalized Domain Names in Applications (IDNA)" -category = "dev" optional = false -python-versions = ">=3.5" - -[[package]] -name = "importlib-metadata" -version = "5.1.0" -description = "Read metadata from Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.dependencies] -typing-extensions = {version = ">=3.6.4", markers = "python_version < \"3.8\""} -zipp = ">=0.5" +python-versions = ">=3.6" +files = [ + {file = "idna-3.10-py3-none-any.whl", hash = "sha256:946d195a0d259cbba61165e88e65941f16e9b36ea6ddb97f00452bae8b1287d3"}, + {file = "idna-3.10.tar.gz", hash = "sha256:12f65c9b470abda6dc35cf8e63cc574b1c52b11df2c86030af0ac09b01b13ea9"}, +] [package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -perf = ["ipython"] -testing = ["flake8 (<5)", "flufl.flake8", "importlib-resources (>=1.3)", "packaging", "pyfakefs", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf (>=0.9.2)"] +all = ["flake8 (>=7.1.1)", "mypy (>=1.11.2)", "pytest (>=8.3.2)", "ruff (>=0.6.2)"] [[package]] name = "jinja2" -version = "3.1.2" +version = "3.1.6" description = "A very fast and expressive template engine." -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "jinja2-3.1.6-py3-none-any.whl", hash = "sha256:85ece4451f492d0c13c5dd7c13a64681a86afae63a5f347908daf103ce6d2f67"}, + {file = "jinja2-3.1.6.tar.gz", hash = "sha256:0137fb05990d35f1275a587e9aee6d56da821fc83491a0fb838183be43f66d6d"}, +] [package.dependencies] MarkupSafe = ">=2.0" @@ -150,117 +344,226 @@ i18n = ["Babel (>=2.7)"] [[package]] name = "markupsafe" -version = "2.1.1" +version = "3.0.2" description = "Safely add untrusted strings to HTML/XML markup." -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +files = [ + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:7e94c425039cde14257288fd61dcfb01963e658efbc0ff54f5306b06054700f8"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9e2d922824181480953426608b81967de705c3cef4d1af983af849d7bd619158"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:38a9ef736c01fccdd6600705b09dc574584b89bea478200c5fbf112a6b0d5579"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:bbcb445fa71794da8f178f0f6d66789a28d7319071af7a496d4d507ed566270d"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:57cb5a3cf367aeb1d316576250f65edec5bb3be939e9247ae594b4bcbc317dfb"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_aarch64.whl", hash = "sha256:3809ede931876f5b2ec92eef964286840ed3540dadf803dd570c3b7e13141a3b"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_i686.whl", hash = "sha256:e07c3764494e3776c602c1e78e298937c3315ccc9043ead7e685b7f2b8d47b3c"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-musllinux_1_2_x86_64.whl", hash = "sha256:b424c77b206d63d500bcb69fa55ed8d0e6a3774056bdc4839fc9298a7edca171"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win32.whl", hash = "sha256:fcabf5ff6eea076f859677f5f0b6b5c1a51e70a376b0579e0eadef8db48c6b50"}, + {file = "MarkupSafe-3.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:6af100e168aa82a50e186c82875a5893c5597a0c1ccdb0d8b40240b1f28b969a"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_10_9_universal2.whl", hash = "sha256:9025b4018f3a1314059769c7bf15441064b2207cb3f065e6ea1e7359cb46db9d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:93335ca3812df2f366e80509ae119189886b0f3c2b81325d39efdb84a1e2ae93"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:2cb8438c3cbb25e220c2ab33bb226559e7afb3baec11c4f218ffa7308603c832"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:a123e330ef0853c6e822384873bef7507557d8e4a082961e1defa947aa59ba84"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:1e084f686b92e5b83186b07e8a17fc09e38fff551f3602b249881fec658d3eca"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:d8213e09c917a951de9d09ecee036d5c7d36cb6cb7dbaece4c71a60d79fb9798"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:5b02fb34468b6aaa40dfc198d813a641e3a63b98c2b05a16b9f80b7ec314185e"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:0bff5e0ae4ef2e1ae4fdf2dfd5b76c75e5c2fa4132d05fc1b0dabcd20c7e28c4"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win32.whl", hash = "sha256:6c89876f41da747c8d3677a2b540fb32ef5715f97b66eeb0c6b66f5e3ef6f59d"}, + {file = "MarkupSafe-3.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:70a87b411535ccad5ef2f1df5136506a10775d267e197e4cf531ced10537bd6b"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_10_13_universal2.whl", hash = "sha256:9778bd8ab0a994ebf6f84c2b949e65736d5575320a17ae8984a77fab08db94cf"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:846ade7b71e3536c4e56b386c2a47adf5741d2d8b94ec9dc3e92e5e1ee1e2225"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1c99d261bd2d5f6b59325c92c73df481e05e57f19837bdca8413b9eac4bd8028"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e17c96c14e19278594aa4841ec148115f9c7615a47382ecb6b82bd8fea3ab0c8"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:88416bd1e65dcea10bc7569faacb2c20ce071dd1f87539ca2ab364bf6231393c"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:2181e67807fc2fa785d0592dc2d6206c019b9502410671cc905d132a92866557"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:52305740fe773d09cffb16f8ed0427942901f00adedac82ec8b67752f58a1b22"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:ad10d3ded218f1039f11a75f8091880239651b52e9bb592ca27de44eed242a48"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win32.whl", hash = "sha256:0f4ca02bea9a23221c0182836703cbf8930c5e9454bacce27e767509fa286a30"}, + {file = "MarkupSafe-3.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:8e06879fc22a25ca47312fbe7c8264eb0b662f6db27cb2d3bbbc74b1df4b9b87"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_10_13_universal2.whl", hash = "sha256:ba9527cdd4c926ed0760bc301f6728ef34d841f405abf9d4f959c478421e4efd"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:f8b3d067f2e40fe93e1ccdd6b2e1d16c43140e76f02fb1319a05cf2b79d99430"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:569511d3b58c8791ab4c2e1285575265991e6d8f8700c7be0e88f86cb0672094"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:15ab75ef81add55874e7ab7055e9c397312385bd9ced94920f2802310c930396"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:f3818cb119498c0678015754eba762e0d61e5b52d34c8b13d770f0719f7b1d79"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:cdb82a876c47801bb54a690c5ae105a46b392ac6099881cdfb9f6e95e4014c6a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:cabc348d87e913db6ab4aa100f01b08f481097838bdddf7c7a84b7575b7309ca"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:444dcda765c8a838eaae23112db52f1efaf750daddb2d9ca300bcae1039adc5c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win32.whl", hash = "sha256:bcf3e58998965654fdaff38e58584d8937aa3096ab5354d493c77d1fdd66d7a1"}, + {file = "MarkupSafe-3.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:e6a2a455bd412959b57a172ce6328d2dd1f01cb2135efda2e4576e8a23fa3b0f"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_10_13_universal2.whl", hash = "sha256:b5a6b3ada725cea8a5e634536b1b01c30bcdcd7f9c6fff4151548d5bf6b3a36c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-macosx_11_0_arm64.whl", hash = "sha256:a904af0a6162c73e3edcb969eeeb53a63ceeb5d8cf642fade7d39e7963a22ddb"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4aa4e5faecf353ed117801a068ebab7b7e09ffb6e1d5e412dc852e0da018126c"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:c0ef13eaeee5b615fb07c9a7dadb38eac06a0608b41570d8ade51c56539e509d"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:d16a81a06776313e817c951135cf7340a3e91e8c1ff2fac444cfd75fffa04afe"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_aarch64.whl", hash = "sha256:6381026f158fdb7c72a168278597a5e3a5222e83ea18f543112b2662a9b699c5"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_i686.whl", hash = "sha256:3d79d162e7be8f996986c064d1c7c817f6df3a77fe3d6859f6f9e7be4b8c213a"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-musllinux_1_2_x86_64.whl", hash = "sha256:131a3c7689c85f5ad20f9f6fb1b866f402c445b220c19fe4308c0b147ccd2ad9"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win32.whl", hash = "sha256:ba8062ed2cf21c07a9e295d5b8a2a5ce678b913b45fdf68c32d95d6c1291e0b6"}, + {file = "MarkupSafe-3.0.2-cp313-cp313t-win_amd64.whl", hash = "sha256:e444a31f8db13eb18ada366ab3cf45fd4b31e4db1236a4448f68778c1d1a5a2f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:eaa0a10b7f72326f1372a713e73c3f739b524b3af41feb43e4921cb529f5929a"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:48032821bbdf20f5799ff537c7ac3d1fba0ba032cfc06194faffa8cda8b560ff"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1a9d3f5f0901fdec14d8d2f66ef7d035f2157240a433441719ac9a3fba440b13"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:88b49a3b9ff31e19998750c38e030fc7bb937398b1f78cfa599aaef92d693144"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:cfad01eed2c2e0c01fd0ecd2ef42c492f7f93902e39a42fc9ee1692961443a29"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_aarch64.whl", hash = "sha256:1225beacc926f536dc82e45f8a4d68502949dc67eea90eab715dea3a21c1b5f0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_i686.whl", hash = "sha256:3169b1eefae027567d1ce6ee7cae382c57fe26e82775f460f0b2778beaad66c0"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-musllinux_1_2_x86_64.whl", hash = "sha256:eb7972a85c54febfb25b5c4b4f3af4dcc731994c7da0d8a0b4a6eb0640e1d178"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win32.whl", hash = "sha256:8c4e8c3ce11e1f92f6536ff07154f9d49677ebaaafc32db9db4620bc11ed480f"}, + {file = "MarkupSafe-3.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:6e296a513ca3d94054c2c881cc913116e90fd030ad1c656b3869762b754f5f8a"}, + {file = "markupsafe-3.0.2.tar.gz", hash = "sha256:ee55d3edf80167e48ea11a923c7386f4669df67d7994554387f84e7d8b0a2bf0"}, +] [[package]] name = "more-itertools" -version = "9.0.0" +version = "10.7.0" description = "More routines for operating on iterables, beyond itertools" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +files = [ + {file = "more_itertools-10.7.0-py3-none-any.whl", hash = "sha256:d43980384673cb07d2f7d2d918c616b30c659c089ee23953f601d6609c67510e"}, + {file = "more_itertools-10.7.0.tar.gz", hash = "sha256:9fddd5403be01a94b204faadcff459ec3568cf110265d3c54323e1e866ad29d3"}, +] [[package]] name = "nodeenv" -version = "1.7.0" +version = "1.9.1" description = "Node.js virtual environment builder" -category = "dev" optional = false -python-versions = ">=2.7,!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*" - -[package.dependencies] -setuptools = "*" +python-versions = "!=3.0.*,!=3.1.*,!=3.2.*,!=3.3.*,!=3.4.*,!=3.5.*,!=3.6.*,>=2.7" +files = [ + {file = "nodeenv-1.9.1-py2.py3-none-any.whl", hash = "sha256:ba11c9782d29c27c70ffbdda2d7415098754709be8a7056d79a737cd901155c9"}, + {file = "nodeenv-1.9.1.tar.gz", hash = "sha256:6ec12890a2dab7946721edbfbcd91f3319c6ccc9aec47be7c7e6b7011ee6645f"}, +] + +[[package]] +name = "numpy" +version = "1.26.4" +description = "Fundamental package for array computing in Python" +optional = false +python-versions = ">=3.9" +files = [ + {file = "numpy-1.26.4-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:9ff0f4f29c51e2803569d7a51c2304de5554655a60c5d776e35b4a41413830d0"}, + {file = "numpy-1.26.4-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:2e4ee3380d6de9c9ec04745830fd9e2eccb3e6cf790d39d7b98ffd19b0dd754a"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d209d8969599b27ad20994c8e41936ee0964e6da07478d6c35016bc386b66ad4"}, + {file = "numpy-1.26.4-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ffa75af20b44f8dba823498024771d5ac50620e6915abac414251bd971b4529f"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:62b8e4b1e28009ef2846b4c7852046736bab361f7aeadeb6a5b89ebec3c7055a"}, + {file = "numpy-1.26.4-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:a4abb4f9001ad2858e7ac189089c42178fcce737e4169dc61321660f1a96c7d2"}, + {file = "numpy-1.26.4-cp310-cp310-win32.whl", hash = "sha256:bfe25acf8b437eb2a8b2d49d443800a5f18508cd811fea3181723922a8a82b07"}, + {file = "numpy-1.26.4-cp310-cp310-win_amd64.whl", hash = "sha256:b97fe8060236edf3662adfc2c633f56a08ae30560c56310562cb4f95500022d5"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:4c66707fabe114439db9068ee468c26bbdf909cac0fb58686a42a24de1760c71"}, + {file = "numpy-1.26.4-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:edd8b5fe47dab091176d21bb6de568acdd906d1887a4584a15a9a96a1dca06ef"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:7ab55401287bfec946ced39700c053796e7cc0e3acbef09993a9ad2adba6ca6e"}, + {file = "numpy-1.26.4-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:666dbfb6ec68962c033a450943ded891bed2d54e6755e35e5835d63f4f6931d5"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:96ff0b2ad353d8f990b63294c8986f1ec3cb19d749234014f4e7eb0112ceba5a"}, + {file = "numpy-1.26.4-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:60dedbb91afcbfdc9bc0b1f3f402804070deed7392c23eb7a7f07fa857868e8a"}, + {file = "numpy-1.26.4-cp311-cp311-win32.whl", hash = "sha256:1af303d6b2210eb850fcf03064d364652b7120803a0b872f5211f5234b399f20"}, + {file = "numpy-1.26.4-cp311-cp311-win_amd64.whl", hash = "sha256:cd25bcecc4974d09257ffcd1f098ee778f7834c3ad767fe5db785be9a4aa9cb2"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:b3ce300f3644fb06443ee2222c2201dd3a89ea6040541412b8fa189341847218"}, + {file = "numpy-1.26.4-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:03a8c78d01d9781b28a6989f6fa1bb2c4f2d51201cf99d3dd875df6fbd96b23b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:9fad7dcb1aac3c7f0584a5a8133e3a43eeb2fe127f47e3632d43d677c66c102b"}, + {file = "numpy-1.26.4-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:675d61ffbfa78604709862923189bad94014bef562cc35cf61d3a07bba02a7ed"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:ab47dbe5cc8210f55aa58e4805fe224dac469cde56b9f731a4c098b91917159a"}, + {file = "numpy-1.26.4-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:1dda2e7b4ec9dd512f84935c5f126c8bd8b9f2fc001e9f54af255e8c5f16b0e0"}, + {file = "numpy-1.26.4-cp312-cp312-win32.whl", hash = "sha256:50193e430acfc1346175fcbdaa28ffec49947a06918b7b92130744e81e640110"}, + {file = "numpy-1.26.4-cp312-cp312-win_amd64.whl", hash = "sha256:08beddf13648eb95f8d867350f6a018a4be2e5ad54c8d8caed89ebca558b2818"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:7349ab0fa0c429c82442a27a9673fc802ffdb7c7775fad780226cb234965e53c"}, + {file = "numpy-1.26.4-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:52b8b60467cd7dd1e9ed082188b4e6bb35aa5cdd01777621a1658910745b90be"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d5241e0a80d808d70546c697135da2c613f30e28251ff8307eb72ba696945764"}, + {file = "numpy-1.26.4-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:f870204a840a60da0b12273ef34f7051e98c3b5961b61b0c2c1be6dfd64fbcd3"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:679b0076f67ecc0138fd2ede3a8fd196dddc2ad3254069bcb9faf9a79b1cebcd"}, + {file = "numpy-1.26.4-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:47711010ad8555514b434df65f7d7b076bb8261df1ca9bb78f53d3b2db02e95c"}, + {file = "numpy-1.26.4-cp39-cp39-win32.whl", hash = "sha256:a354325ee03388678242a4d7ebcd08b5c727033fcff3b2f536aea978e15ee9e6"}, + {file = "numpy-1.26.4-cp39-cp39-win_amd64.whl", hash = "sha256:3373d5d70a5fe74a2c1bb6d2cfd9609ecf686d47a2d7b1d37a8f3b6bf6003aea"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-macosx_10_9_x86_64.whl", hash = "sha256:afedb719a9dcfc7eaf2287b839d8198e06dcd4cb5d276a3df279231138e83d30"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:95a7476c59002f2f6c590b9b7b998306fba6a5aa646b1e22ddfeaf8f78c3a29c"}, + {file = "numpy-1.26.4-pp39-pypy39_pp73-win_amd64.whl", hash = "sha256:7e50d0a0cc3189f9cb0aeb3a6a6af18c16f59f004b866cd2be1c14b36134a4a0"}, + {file = "numpy-1.26.4.tar.gz", hash = "sha256:2a02aba9ed12e4ac4eb3ea9421c420301a0c6460d9830d74a9df87efa4912010"}, +] [[package]] name = "packaging" -version = "21.3" +version = "25.0" description = "Core utilities for Python packages" -category = "dev" optional = false -python-versions = ">=3.6" - -[package.dependencies] -pyparsing = ">=2.0.2,<3.0.5 || >3.0.5" +python-versions = ">=3.8" +files = [ + {file = "packaging-25.0-py3-none-any.whl", hash = "sha256:29572ef2b1f17581046b3a2227d5c611fb25ec70ca1ba8554b24b0e69331a484"}, + {file = "packaging-25.0.tar.gz", hash = "sha256:d443872c98d677bf60f6a1f2f8c1cb748e8fe762d2bf9d3148b5599295b0fc4f"}, +] [[package]] name = "platformdirs" -version = "2.5.4" -description = "A small Python package for determining appropriate platform-specific dirs, e.g. a \"user data dir\"." -category = "dev" +version = "4.3.8" +description = "A small Python package for determining appropriate platform-specific dirs, e.g. a `user data dir`." optional = false -python-versions = ">=3.7" +python-versions = ">=3.9" +files = [ + {file = "platformdirs-4.3.8-py3-none-any.whl", hash = "sha256:ff7059bb7eb1179e2685604f4aaf157cfd9535242bd23742eadc3c13542139b4"}, + {file = "platformdirs-4.3.8.tar.gz", hash = "sha256:3d512d96e16bcb959a814c9f348431070822a6496326a4be0911c40b5a74c2bc"}, +] [package.extras] -docs = ["furo (>=2022.9.29)", "proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-autodoc-typehints (>=1.19.4)"] -test = ["appdirs (==1.4.4)", "pytest (>=7.2)", "pytest-cov (>=4)", "pytest-mock (>=3.10)"] +docs = ["furo (>=2024.8.6)", "proselint (>=0.14)", "sphinx (>=8.1.3)", "sphinx-autodoc-typehints (>=3)"] +test = ["appdirs (==1.4.4)", "covdefaults (>=2.3)", "pytest (>=8.3.4)", "pytest-cov (>=6)", "pytest-mock (>=3.14)"] +type = ["mypy (>=1.14.1)"] [[package]] name = "pluggy" version = "0.13.1" description = "plugin and hook calling mechanisms for python" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*" - -[package.dependencies] -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} +files = [ + {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, + {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, +] [package.extras] dev = ["pre-commit", "tox"] [[package]] name = "pre-commit" -version = "2.20.0" +version = "2.21.0" description = "A framework for managing and maintaining multi-language pre-commit hooks." -category = "dev" optional = false python-versions = ">=3.7" +files = [ + {file = "pre_commit-2.21.0-py2.py3-none-any.whl", hash = "sha256:e2f91727039fc39a92f58a588a25b87f936de6567eed4f0e673e0507edc75bad"}, + {file = "pre_commit-2.21.0.tar.gz", hash = "sha256:31ef31af7e474a8d8995027fefdfcf509b5c913ff31f2015b4ec4beb26a6f658"}, +] [package.dependencies] cfgv = ">=2.0.0" identify = ">=1.0.0" -importlib-metadata = {version = "*", markers = "python_version < \"3.8\""} nodeenv = ">=0.11.1" pyyaml = ">=5.1" -toml = "*" -virtualenv = ">=20.0.8" +virtualenv = ">=20.10.0" [[package]] name = "py" version = "1.11.0" description = "library with cross-python path, ini-parsing, io, code, log facilities" -category = "dev" optional = false python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*" - -[[package]] -name = "pyparsing" -version = "3.0.9" -description = "pyparsing module - Classes and methods to define and execute parsing grammars" -category = "dev" -optional = false -python-versions = ">=3.6.8" - -[package.extras] -diagrams = ["jinja2", "railroad-diagrams"] +files = [ + {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, + {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, +] [[package]] name = "pytest" version = "5.4.3" description = "pytest: simple powerful testing with Python" -category = "dev" optional = false python-versions = ">=3.5" +files = [ + {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, + {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, +] [package.dependencies] atomicwrites = {version = ">=1.0", markers = "sys_platform == \"win32\""} attrs = ">=17.4.0" colorama = {version = "*", markers = "sys_platform == \"win32\""} -importlib-metadata = {version = ">=0.12", markers = "python_version < \"3.8\""} more-itertools = ">=4.0.0" packaging = "*" pluggy = ">=0.12,<1.0" @@ -271,27 +574,147 @@ wcwidth = "*" checkqa-mypy = ["mypy (==v0.761)"] testing = ["argcomplete", "hypothesis (>=3.56)", "mock", "nose", "requests", "xmlschema"] +[[package]] +name = "pytest-dependency" +version = "0.6.0" +description = "Manage dependencies of tests" +optional = false +python-versions = ">=3.4" +files = [ + {file = "pytest-dependency-0.6.0.tar.gz", hash = "sha256:934b0e6a39d95995062c193f7eaeed8a8ffa06ff1bcef4b62b0dc74a708bacc1"}, +] + +[[package]] +name = "pytest-order" +version = "1.0.0" +description = "pytest plugin to run your tests in a specific order" +optional = false +python-versions = "*" +files = [ + {file = "pytest-order-1.0.0.tar.gz", hash = "sha256:5997a262b31234eebb461f9a9ef24687bf732029b499845a4398b69edb5ac321"}, + {file = "pytest_order-1.0.0-py3-none-any.whl", hash = "sha256:a4cdf12f4c83c76bdfd6e6088e2e157df7fdf91a9c3e3ca7d8809e8dabce0f4b"}, +] + +[package.dependencies] +pytest = ">=5.0" + +[[package]] +name = "pytest-order" +version = "1.3.0" +description = "pytest plugin to run your tests in a specific order" +optional = false +python-versions = ">=3.7" +files = [ + {file = "pytest_order-1.3.0-py3-none-any.whl", hash = "sha256:2cd562a21380345dd8d5774aa5fd38b7849b6ee7397ca5f6999bbe6e89f07f6e"}, + {file = "pytest_order-1.3.0.tar.gz", hash = "sha256:51608fec3d3ee9c0adaea94daa124a5c4c1d2bb99b00269f098f414307f23dde"}, +] + +[package.dependencies] +pytest = {version = ">=5.0", markers = "python_version < \"3.10\""} + +[[package]] +name = "pytest-timeout" +version = "1.4.2" +description = "py.test plugin to abort hanging tests" +optional = false +python-versions = "*" +files = [ + {file = "pytest-timeout-1.4.2.tar.gz", hash = "sha256:20b3113cf6e4e80ce2d403b6fb56e9e1b871b510259206d40ff8d609f48bda76"}, + {file = "pytest_timeout-1.4.2-py2.py3-none-any.whl", hash = "sha256:541d7aa19b9a6b4e475c759fd6073ef43d7cdc9a92d95644c260076eb257a063"}, +] + +[package.dependencies] +pytest = ">=3.6.0" + [[package]] name = "pyyaml" -version = "6.0" +version = "6.0.2" description = "YAML parser and emitter for Python" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "PyYAML-6.0.2-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:0a9a2848a5b7feac301353437eb7d5957887edbf81d56e903999a75a3d743086"}, + {file = "PyYAML-6.0.2-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:29717114e51c84ddfba879543fb232a6ed60086602313ca38cce623c1d62cfbf"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8824b5a04a04a047e72eea5cec3bc266db09e35de6bdfe34c9436ac5ee27d237"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:7c36280e6fb8385e520936c3cb3b8042851904eba0e58d277dca80a5cfed590b"}, + {file = "PyYAML-6.0.2-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:ec031d5d2feb36d1d1a24380e4db6d43695f3748343d99434e6f5f9156aaa2ed"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:936d68689298c36b53b29f23c6dbb74de12b4ac12ca6cfe0e047bedceea56180"}, + {file = "PyYAML-6.0.2-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:23502f431948090f597378482b4812b0caae32c22213aecf3b55325e049a6c68"}, + {file = "PyYAML-6.0.2-cp310-cp310-win32.whl", hash = "sha256:2e99c6826ffa974fe6e27cdb5ed0021786b03fc98e5ee3c5bfe1fd5015f42b99"}, + {file = "PyYAML-6.0.2-cp310-cp310-win_amd64.whl", hash = "sha256:a4d3091415f010369ae4ed1fc6b79def9416358877534caf6a0fdd2146c87a3e"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:cc1c1159b3d456576af7a3e4d1ba7e6924cb39de8f67111c735f6fc832082774"}, + {file = "PyYAML-6.0.2-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:1e2120ef853f59c7419231f3bf4e7021f1b936f6ebd222406c3b60212205d2ee"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:5d225db5a45f21e78dd9358e58a98702a0302f2659a3c6cd320564b75b86f47c"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:5ac9328ec4831237bec75defaf839f7d4564be1e6b25ac710bd1a96321cc8317"}, + {file = "PyYAML-6.0.2-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3ad2a3decf9aaba3d29c8f537ac4b243e36bef957511b4766cb0057d32b0be85"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_aarch64.whl", hash = "sha256:ff3824dc5261f50c9b0dfb3be22b4567a6f938ccce4587b38952d85fd9e9afe4"}, + {file = "PyYAML-6.0.2-cp311-cp311-musllinux_1_1_x86_64.whl", hash = "sha256:797b4f722ffa07cc8d62053e4cff1486fa6dc094105d13fea7b1de7d8bf71c9e"}, + {file = "PyYAML-6.0.2-cp311-cp311-win32.whl", hash = "sha256:11d8f3dd2b9c1207dcaf2ee0bbbfd5991f571186ec9cc78427ba5bd32afae4b5"}, + {file = "PyYAML-6.0.2-cp311-cp311-win_amd64.whl", hash = "sha256:e10ce637b18caea04431ce14fabcf5c64a1c61ec9c56b071a4b7ca131ca52d44"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_10_9_x86_64.whl", hash = "sha256:c70c95198c015b85feafc136515252a261a84561b7b1d51e3384e0655ddf25ab"}, + {file = "PyYAML-6.0.2-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:ce826d6ef20b1bc864f0a68340c8b3287705cae2f8b4b1d932177dcc76721725"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:1f71ea527786de97d1a0cc0eacd1defc0985dcf6b3f17bb77dcfc8c34bec4dc5"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9b22676e8097e9e22e36d6b7bda33190d0d400f345f23d4065d48f4ca7ae0425"}, + {file = "PyYAML-6.0.2-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:80bab7bfc629882493af4aa31a4cfa43a4c57c83813253626916b8c7ada83476"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_aarch64.whl", hash = "sha256:0833f8694549e586547b576dcfaba4a6b55b9e96098b36cdc7ebefe667dfed48"}, + {file = "PyYAML-6.0.2-cp312-cp312-musllinux_1_1_x86_64.whl", hash = "sha256:8b9c7197f7cb2738065c481a0461e50ad02f18c78cd75775628afb4d7137fb3b"}, + {file = "PyYAML-6.0.2-cp312-cp312-win32.whl", hash = "sha256:ef6107725bd54b262d6dedcc2af448a266975032bc85ef0172c5f059da6325b4"}, + {file = "PyYAML-6.0.2-cp312-cp312-win_amd64.whl", hash = "sha256:7e7401d0de89a9a855c839bc697c079a4af81cf878373abd7dc625847d25cbd8"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:efdca5630322a10774e8e98e1af481aad470dd62c3170801852d752aa7a783ba"}, + {file = "PyYAML-6.0.2-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:50187695423ffe49e2deacb8cd10510bc361faac997de9efef88badc3bb9e2d1"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:0ffe8360bab4910ef1b9e87fb812d8bc0a308b0d0eef8c8f44e0254ab3b07133"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:17e311b6c678207928d649faa7cb0d7b4c26a0ba73d41e99c4fff6b6c3276484"}, + {file = "PyYAML-6.0.2-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:70b189594dbe54f75ab3a1acec5f1e3faa7e8cf2f1e08d9b561cb41b845f69d5"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_aarch64.whl", hash = "sha256:41e4e3953a79407c794916fa277a82531dd93aad34e29c2a514c2c0c5fe971cc"}, + {file = "PyYAML-6.0.2-cp313-cp313-musllinux_1_1_x86_64.whl", hash = "sha256:68ccc6023a3400877818152ad9a1033e3db8625d899c72eacb5a668902e4d652"}, + {file = "PyYAML-6.0.2-cp313-cp313-win32.whl", hash = "sha256:bc2fa7c6b47d6bc618dd7fb02ef6fdedb1090ec036abab80d4681424b84c1183"}, + {file = "PyYAML-6.0.2-cp313-cp313-win_amd64.whl", hash = "sha256:8388ee1976c416731879ac16da0aff3f63b286ffdd57cdeb95f3f2e085687563"}, + {file = "PyYAML-6.0.2-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:24471b829b3bf607e04e88d79542a9d48bb037c2267d7927a874e6c205ca7e9a"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d7fded462629cfa4b685c5416b949ebad6cec74af5e2d42905d41e257e0869f5"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:d84a1718ee396f54f3a086ea0a66d8e552b2ab2017ef8b420e92edbc841c352d"}, + {file = "PyYAML-6.0.2-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9056c1ecd25795207ad294bcf39f2db3d845767be0ea6e6a34d856f006006083"}, + {file = "PyYAML-6.0.2-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:82d09873e40955485746739bcb8b4586983670466c23382c19cffecbf1fd8706"}, + {file = "PyYAML-6.0.2-cp38-cp38-win32.whl", hash = "sha256:43fa96a3ca0d6b1812e01ced1044a003533c47f6ee8aca31724f78e93ccc089a"}, + {file = "PyYAML-6.0.2-cp38-cp38-win_amd64.whl", hash = "sha256:01179a4a8559ab5de078078f37e5c1a30d76bb88519906844fd7bdea1b7729ff"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:688ba32a1cffef67fd2e9398a2efebaea461578b0923624778664cc1c914db5d"}, + {file = "PyYAML-6.0.2-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:a8786accb172bd8afb8be14490a16625cbc387036876ab6ba70912730faf8e1f"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d8e03406cac8513435335dbab54c0d385e4a49e4945d2909a581c83647ca0290"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:f753120cb8181e736c57ef7636e83f31b9c0d1722c516f7e86cf15b7aa57ff12"}, + {file = "PyYAML-6.0.2-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:3b1fdb9dc17f5a7677423d508ab4f243a726dea51fa5e70992e59a7411c89d19"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:0b69e4ce7a131fe56b7e4d770c67429700908fc0752af059838b1cfb41960e4e"}, + {file = "PyYAML-6.0.2-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:a9f8c2e67970f13b16084e04f134610fd1d374bf477b17ec1599185cf611d725"}, + {file = "PyYAML-6.0.2-cp39-cp39-win32.whl", hash = "sha256:6395c297d42274772abc367baaa79683958044e5d3835486c16da75d2a694631"}, + {file = "PyYAML-6.0.2-cp39-cp39-win_amd64.whl", hash = "sha256:39693e1f8320ae4f43943590b49779ffb98acb81f788220ea932a6b6c51004d8"}, + {file = "pyyaml-6.0.2.tar.gz", hash = "sha256:d584d9ec91ad65861cc08d42e834324ef890a082e591037abe114850ff7bbc3e"}, +] + +[[package]] +name = "rbool" +version = "0.0.1" +description = "1D boolean operations" +optional = false +python-versions = "<4.0,>=3.9" +files = [ + {file = "rbool-0.0.1-py3-none-any.whl", hash = "sha256:b4faf2871ac857e1dfbe948d1bdfac3593e8712942d094bb24a032038077f780"}, + {file = "rbool-0.0.1.tar.gz", hash = "sha256:03bc6d2913ee1813421b87be02ede8d886b7b3458504ef1b510b9124e03d1353"}, +] [[package]] name = "requests" -version = "2.28.1" +version = "2.32.4" description = "Python HTTP for Humans." -category = "dev" optional = false -python-versions = ">=3.7, <4" +python-versions = ">=3.8" +files = [ + {file = "requests-2.32.4-py3-none-any.whl", hash = "sha256:27babd3cda2a6d50b30443204ee89830707d396671944c998b5975b031ac2b2c"}, + {file = "requests-2.32.4.tar.gz", hash = "sha256:27d0316682c8a29834d3264820024b62a36942083d52caf2f14c0591336d3422"}, +] [package.dependencies] certifi = ">=2017.4.17" -charset-normalizer = ">=2,<3" +charset_normalizer = ">=2,<4" idna = ">=2.5,<4" -urllib3 = ">=1.21.1,<1.27" +urllib3 = ">=1.21.1,<3" [package.extras] socks = ["PySocks (>=1.5.6,!=1.5.7)"] @@ -301,9 +724,12 @@ use-chardet-on-py3 = ["chardet (>=3.0.2,<6)"] name = "scriv" version = "0.15.2" description = "Scriv changelog management tool" -category = "dev" optional = false python-versions = "*" +files = [ + {file = "scriv-0.15.2-py2.py3-none-any.whl", hash = "sha256:24ea4b8c40a5af26121545ea3048d75bb4a480a05dcc7e8d6b09f558d1efe39d"}, + {file = "scriv-0.15.2.tar.gz", hash = "sha256:3be76949b24dab7143a5158168ada6ff2dc7104953dd041cf005b001aadc2705"}, +] [package.dependencies] attrs = "*" @@ -316,312 +742,96 @@ tomli = {version = "*", optional = true, markers = "extra == \"toml\""} [package.extras] toml = ["tomli"] -[[package]] -name = "setuptools" -version = "65.6.3" -description = "Easily download, build, install, upgrade, and uninstall Python packages" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "pygments-github-lexers (==0.0.5)", "rst.linker (>=1.9)", "sphinx (>=3.5)", "sphinx-favicon", "sphinx-hoverxref (<2)", "sphinx-inline-tabs", "sphinx-notfound-page (==0.8.3)", "sphinx-reredirects", "sphinxcontrib-towncrier"] -testing = ["build[virtualenv]", "filelock (>=3.4.0)", "flake8 (<5)", "flake8-2020", "ini2toml[lite] (>=0.9)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pip (>=19.1)", "pip-run (>=8.8)", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)", "pytest-perf", "pytest-timeout", "pytest-xdist", "tomli-w (>=1.0.0)", "virtualenv (>=13.0.0)", "wheel"] -testing-integration = ["build[virtualenv]", "filelock (>=3.4.0)", "jaraco.envs (>=2.2)", "jaraco.path (>=3.2.0)", "pytest", "pytest-enabler", "pytest-xdist", "tomli", "virtualenv (>=13.0.0)", "wheel"] - -[[package]] -name = "toml" -version = "0.10.2" -description = "Python Library for Tom's Obvious, Minimal Language" -category = "dev" -optional = false -python-versions = ">=2.6, !=3.0.*, !=3.1.*, !=3.2.*" - [[package]] name = "tomli" -version = "2.0.1" +version = "2.2.1" description = "A lil' TOML parser" -category = "dev" -optional = false -python-versions = ">=3.7" - -[[package]] -name = "typing-extensions" -version = "4.4.0" -description = "Backported and Experimental Type Hints for Python 3.7+" -category = "dev" optional = false -python-versions = ">=3.7" +python-versions = ">=3.8" +files = [ + {file = "tomli-2.2.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:678e4fa69e4575eb77d103de3df8a895e1591b48e740211bd1067378c69e8249"}, + {file = "tomli-2.2.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:023aa114dd824ade0100497eb2318602af309e5a55595f76b626d6d9f3b7b0a6"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:ece47d672db52ac607a3d9599a9d48dcb2f2f735c6c2d1f34130085bb12b112a"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:6972ca9c9cc9f0acaa56a8ca1ff51e7af152a9f87fb64623e31d5c83700080ee"}, + {file = "tomli-2.2.1-cp311-cp311-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:c954d2250168d28797dd4e3ac5cf812a406cd5a92674ee4c8f123c889786aa8e"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:8dd28b3e155b80f4d54beb40a441d366adcfe740969820caf156c019fb5c7ec4"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_i686.whl", hash = "sha256:e59e304978767a54663af13c07b3d1af22ddee3bb2fb0618ca1593e4f593a106"}, + {file = "tomli-2.2.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:33580bccab0338d00994d7f16f4c4ec25b776af3ffaac1ed74e0b3fc95e885a8"}, + {file = "tomli-2.2.1-cp311-cp311-win32.whl", hash = "sha256:465af0e0875402f1d226519c9904f37254b3045fc5084697cefb9bdde1ff99ff"}, + {file = "tomli-2.2.1-cp311-cp311-win_amd64.whl", hash = "sha256:2d0f2fdd22b02c6d81637a3c95f8cd77f995846af7414c5c4b8d0545afa1bc4b"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:4a8f6e44de52d5e6c657c9fe83b562f5f4256d8ebbfe4ff922c495620a7f6cea"}, + {file = "tomli-2.2.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:8d57ca8095a641b8237d5b079147646153d22552f1c637fd3ba7f4b0b29167a8"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:4e340144ad7ae1533cb897d406382b4b6fede8890a03738ff1683af800d54192"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:db2b95f9de79181805df90bedc5a5ab4c165e6ec3fe99f970d0e302f384ad222"}, + {file = "tomli-2.2.1-cp312-cp312-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:40741994320b232529c802f8bc86da4e1aa9f413db394617b9a256ae0f9a7f77"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:400e720fe168c0f8521520190686ef8ef033fb19fc493da09779e592861b78c6"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_i686.whl", hash = "sha256:02abe224de6ae62c19f090f68da4e27b10af2b93213d36cf44e6e1c5abd19fdd"}, + {file = "tomli-2.2.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:b82ebccc8c8a36f2094e969560a1b836758481f3dc360ce9a3277c65f374285e"}, + {file = "tomli-2.2.1-cp312-cp312-win32.whl", hash = "sha256:889f80ef92701b9dbb224e49ec87c645ce5df3fa2cc548664eb8a25e03127a98"}, + {file = "tomli-2.2.1-cp312-cp312-win_amd64.whl", hash = "sha256:7fc04e92e1d624a4a63c76474610238576942d6b8950a2d7f908a340494e67e4"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:f4039b9cbc3048b2416cc57ab3bda989a6fcf9b36cf8937f01a6e731b64f80d7"}, + {file = "tomli-2.2.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:286f0ca2ffeeb5b9bd4fcc8d6c330534323ec51b2f52da063b11c502da16f30c"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a92ef1a44547e894e2a17d24e7557a5e85a9e1d0048b0b5e7541f76c5032cb13"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:9316dc65bed1684c9a98ee68759ceaed29d229e985297003e494aa825ebb0281"}, + {file = "tomli-2.2.1-cp313-cp313-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:e85e99945e688e32d5a35c1ff38ed0b3f41f43fad8df0bdf79f72b2ba7bc5272"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:ac065718db92ca818f8d6141b5f66369833d4a80a9d74435a268c52bdfa73140"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_i686.whl", hash = "sha256:d920f33822747519673ee656a4b6ac33e382eca9d331c87770faa3eef562aeb2"}, + {file = "tomli-2.2.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:a198f10c4d1b1375d7687bc25294306e551bf1abfa4eace6650070a5c1ae2744"}, + {file = "tomli-2.2.1-cp313-cp313-win32.whl", hash = "sha256:d3f5614314d758649ab2ab3a62d4f2004c825922f9e370b29416484086b264ec"}, + {file = "tomli-2.2.1-cp313-cp313-win_amd64.whl", hash = "sha256:a38aa0308e754b0e3c67e344754dff64999ff9b513e691d0e786265c93583c69"}, + {file = "tomli-2.2.1-py3-none-any.whl", hash = "sha256:cb55c73c5f4408779d0cf3eef9f762b9c9f147a77de7b258bef0a5628adc85cc"}, + {file = "tomli-2.2.1.tar.gz", hash = "sha256:cd45e1dc79c835ce60f7404ec8119f2eb06d38b1deba146f07ced3bbc44505ff"}, +] [[package]] name = "urllib3" -version = "1.26.13" +version = "2.5.0" description = "HTTP library with thread-safe connection pooling, file post, and more." -category = "dev" optional = false -python-versions = ">=2.7, !=3.0.*, !=3.1.*, !=3.2.*, !=3.3.*, !=3.4.*, !=3.5.*" +python-versions = ">=3.9" +files = [ + {file = "urllib3-2.5.0-py3-none-any.whl", hash = "sha256:e6b01673c0fa6a13e374b50871808eb3bf7046c4b125b216f6bf1cc604cff0dc"}, + {file = "urllib3-2.5.0.tar.gz", hash = "sha256:3fc47733c7e419d4bc3f6b3dc2b4f890bb743906a30d56ba4a5bfa4bbff92760"}, +] [package.extras] -brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)", "brotlipy (>=0.6.0)"] -secure = ["certifi", "cryptography (>=1.3.4)", "idna (>=2.0.0)", "ipaddress", "pyOpenSSL (>=0.14)", "urllib3-secure-extra"] -socks = ["PySocks (>=1.5.6,!=1.5.7,<2.0)"] +brotli = ["brotli (>=1.0.9)", "brotlicffi (>=0.8.0)"] +h2 = ["h2 (>=4,<5)"] +socks = ["pysocks (>=1.5.6,!=1.5.7,<2.0)"] +zstd = ["zstandard (>=0.18.0)"] [[package]] name = "virtualenv" -version = "20.17.0" +version = "20.31.2" description = "Virtual Python Environment builder" -category = "dev" optional = false -python-versions = ">=3.6" +python-versions = ">=3.8" +files = [ + {file = "virtualenv-20.31.2-py3-none-any.whl", hash = "sha256:36efd0d9650ee985f0cad72065001e66d49a6f24eb44d98980f630686243cf11"}, + {file = "virtualenv-20.31.2.tar.gz", hash = "sha256:e10c0a9d02835e592521be48b332b6caee6887f332c111aa79a09b9e79efc2af"}, +] [package.dependencies] -distlib = ">=0.3.6,<1" -filelock = ">=3.4.1,<4" -importlib-metadata = {version = ">=4.8.3", markers = "python_version < \"3.8\""} -platformdirs = ">=2.4,<3" +distlib = ">=0.3.7,<1" +filelock = ">=3.12.2,<4" +platformdirs = ">=3.9.1,<5" [package.extras] -docs = ["proselint (>=0.13)", "sphinx (>=5.3)", "sphinx-argparse (>=0.3.2)", "sphinx-rtd-theme (>=1)", "towncrier (>=22.8)"] -testing = ["coverage (>=6.2)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=21.3)", "pytest (>=7.0.1)", "pytest-env (>=0.6.2)", "pytest-freezegun (>=0.4.2)", "pytest-mock (>=3.6.1)", "pytest-randomly (>=3.10.3)", "pytest-timeout (>=2.1)"] +docs = ["furo (>=2023.7.26)", "proselint (>=0.13)", "sphinx (>=7.1.2,!=7.3)", "sphinx-argparse (>=0.4)", "sphinxcontrib-towncrier (>=0.2.1a0)", "towncrier (>=23.6)"] +test = ["covdefaults (>=2.3)", "coverage (>=7.2.7)", "coverage-enable-subprocess (>=1)", "flaky (>=3.7)", "packaging (>=23.1)", "pytest (>=7.4)", "pytest-env (>=0.8.2)", "pytest-freezer (>=0.4.8)", "pytest-mock (>=3.11.1)", "pytest-randomly (>=3.12)", "pytest-timeout (>=2.1)", "setuptools (>=68)", "time-machine (>=2.10)"] [[package]] name = "wcwidth" -version = "0.2.5" +version = "0.2.13" description = "Measures the displayed width of unicode strings in a terminal" -category = "dev" optional = false python-versions = "*" - -[[package]] -name = "zipp" -version = "3.11.0" -description = "Backport of pathlib-compatible object wrapper for zip files" -category = "dev" -optional = false -python-versions = ">=3.7" - -[package.extras] -docs = ["furo", "jaraco.packaging (>=9)", "jaraco.tidelift (>=1.4)", "rst.linker (>=1.9)", "sphinx (>=3.5)"] -testing = ["flake8 (<5)", "func-timeout", "jaraco.functools", "jaraco.itertools", "more-itertools", "pytest (>=6)", "pytest-black (>=0.3.7)", "pytest-checkdocs (>=2.4)", "pytest-cov", "pytest-enabler (>=1.3)", "pytest-flake8", "pytest-mypy (>=0.9.1)"] +files = [ + {file = "wcwidth-0.2.13-py2.py3-none-any.whl", hash = "sha256:3da69048e4540d84af32131829ff948f1e022c1c6bdb8d6102117aac784f6859"}, + {file = "wcwidth-0.2.13.tar.gz", hash = "sha256:72ea0c06399eb286d978fdedb6923a9eb47e1c486ce63e9b4e64fc18303972b5"}, +] [metadata] -lock-version = "1.1" -python-versions = "^3.7" -content-hash = "ee1c082ac21664ba5cf11e5836f5442993e9b59466d8f9fa68c41ec8e1a2c0c8" - -[metadata.files] -atomicwrites = [ - {file = "atomicwrites-1.4.1.tar.gz", hash = "sha256:81b2c9071a49367a7f770170e5eec8cb66567cfbbc8c73d20ce5ca4a8d71cf11"}, -] -attrs = [ - {file = "attrs-22.1.0-py2.py3-none-any.whl", hash = "sha256:86efa402f67bf2df34f51a335487cf46b1ec130d02b8d39fd248abfd30da551c"}, - {file = "attrs-22.1.0.tar.gz", hash = "sha256:29adc2665447e5191d0e7c568fde78b21f9672d344281d0c6e1ab085429b22b6"}, -] -certifi = [ - {file = "certifi-2022.9.24-py3-none-any.whl", hash = "sha256:90c1a32f1d68f940488354e36370f6cca89f0f106db09518524c88d6ed83f382"}, - {file = "certifi-2022.9.24.tar.gz", hash = "sha256:0d9c601124e5a6ba9712dbc60d9c53c21e34f5f641fe83002317394311bdce14"}, -] -cfgv = [ - {file = "cfgv-3.3.1-py2.py3-none-any.whl", hash = "sha256:c6a0883f3917a037485059700b9e75da2464e6c27051014ad85ba6aaa5884426"}, - {file = "cfgv-3.3.1.tar.gz", hash = "sha256:f5a830efb9ce7a445376bb66ec94c638a9787422f96264c98edc6bdeed8ab736"}, -] -charset-normalizer = [ - {file = "charset-normalizer-2.1.1.tar.gz", hash = "sha256:5a3d016c7c547f69d6f81fb0db9449ce888b418b5b9952cc5e6e66843e9dd845"}, - {file = "charset_normalizer-2.1.1-py3-none-any.whl", hash = "sha256:83e9a75d1911279afd89352c68b45348559d1fc0506b054b346651b5e7fee29f"}, -] -click = [ - {file = "click-8.1.3-py3-none-any.whl", hash = "sha256:bb4d8133cb15a609f44e8213d9b391b0809795062913b383c62be0ee95b1db48"}, - {file = "click-8.1.3.tar.gz", hash = "sha256:7682dc8afb30297001674575ea00d1814d808d6a36af415a82bd481d37ba7b8e"}, -] -click-log = [ - {file = "click-log-0.4.0.tar.gz", hash = "sha256:3970f8570ac54491237bcdb3d8ab5e3eef6c057df29f8c3d1151a51a9c23b975"}, - {file = "click_log-0.4.0-py2.py3-none-any.whl", hash = "sha256:a43e394b528d52112af599f2fc9e4b7cf3c15f94e53581f74fa6867e68c91756"}, -] -colorama = [ - {file = "colorama-0.4.6-py2.py3-none-any.whl", hash = "sha256:4f1d9991f5acc0ca119f9d443620b77f9d6b33703e51011c16baf57afb285fc6"}, - {file = "colorama-0.4.6.tar.gz", hash = "sha256:08695f5cb7ed6e0531a20572697297273c47b8cae5a63ffc6d6ed5c201be6e44"}, -] -distlib = [ - {file = "distlib-0.3.6-py2.py3-none-any.whl", hash = "sha256:f35c4b692542ca110de7ef0bea44d73981caeb34ca0b9b6b2e6d7790dda8f80e"}, - {file = "distlib-0.3.6.tar.gz", hash = "sha256:14bad2d9b04d3a36127ac97f30b12a19268f211063d8f8ee4f47108896e11b46"}, -] -filelock = [ - {file = "filelock-3.8.0-py3-none-any.whl", hash = "sha256:617eb4e5eedc82fc5f47b6d61e4d11cb837c56cb4544e39081099fa17ad109d4"}, - {file = "filelock-3.8.0.tar.gz", hash = "sha256:55447caa666f2198c5b6b13a26d2084d26fa5b115c00d065664b2124680c4edc"}, -] -identify = [ - {file = "identify-2.5.9-py2.py3-none-any.whl", hash = "sha256:a390fb696e164dbddb047a0db26e57972ae52fbd037ae68797e5ae2f4492485d"}, - {file = "identify-2.5.9.tar.gz", hash = "sha256:906036344ca769539610436e40a684e170c3648b552194980bb7b617a8daeb9f"}, -] -idna = [ - {file = "idna-3.4-py3-none-any.whl", hash = "sha256:90b77e79eaa3eba6de819a0c442c0b4ceefc341a7a2ab77d7562bf49f425c5c2"}, - {file = "idna-3.4.tar.gz", hash = "sha256:814f528e8dead7d329833b91c5faa87d60bf71824cd12a7530b5526063d02cb4"}, -] -importlib-metadata = [ - {file = "importlib_metadata-5.1.0-py3-none-any.whl", hash = "sha256:d84d17e21670ec07990e1044a99efe8d615d860fd176fc29ef5c306068fda313"}, - {file = "importlib_metadata-5.1.0.tar.gz", hash = "sha256:d5059f9f1e8e41f80e9c56c2ee58811450c31984dfa625329ffd7c0dad88a73b"}, -] -jinja2 = [ - {file = "Jinja2-3.1.2-py3-none-any.whl", hash = "sha256:6088930bfe239f0e6710546ab9c19c9ef35e29792895fed6e6e31a023a182a61"}, - {file = "Jinja2-3.1.2.tar.gz", hash = "sha256:31351a702a408a9e7595a8fc6150fc3f43bb6bf7e319770cbc0db9df9437e852"}, -] -markupsafe = [ - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_universal2.whl", hash = "sha256:86b1f75c4e7c2ac2ccdaec2b9022845dbb81880ca318bb7a0a01fbf7813e3812"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:f121a1420d4e173a5d96e47e9a0c0dcff965afdf1626d28de1460815f7c4ee7a"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:a49907dd8420c5685cfa064a1335b6754b74541bbb3706c259c02ed65b644b3e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:10c1bfff05d95783da83491be968e8fe789263689c02724e0c691933c52994f5"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:b7bd98b796e2b6553da7225aeb61f447f80a1ca64f41d83612e6139ca5213aa4"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_aarch64.whl", hash = "sha256:b09bf97215625a311f669476f44b8b318b075847b49316d3e28c08e41a7a573f"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_i686.whl", hash = "sha256:694deca8d702d5db21ec83983ce0bb4b26a578e71fbdbd4fdcd387daa90e4d5e"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-musllinux_1_1_x86_64.whl", hash = "sha256:efc1913fd2ca4f334418481c7e595c00aad186563bbc1ec76067848c7ca0a933"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win32.whl", hash = "sha256:4a33dea2b688b3190ee12bd7cfa29d39c9ed176bda40bfa11099a3ce5d3a7ac6"}, - {file = "MarkupSafe-2.1.1-cp310-cp310-win_amd64.whl", hash = "sha256:dda30ba7e87fbbb7eab1ec9f58678558fd9a6b8b853530e176eabd064da81417"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:671cd1187ed5e62818414afe79ed29da836dde67166a9fac6d435873c44fdd02"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:3799351e2336dc91ea70b034983ee71cf2f9533cdff7c14c90ea126bfd95d65a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:e72591e9ecd94d7feb70c1cbd7be7b3ebea3f548870aa91e2732960fa4d57a37"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:6fbf47b5d3728c6aea2abb0589b5d30459e369baa772e0f37a0320185e87c980"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_aarch64.whl", hash = "sha256:d5ee4f386140395a2c818d149221149c54849dfcfcb9f1debfe07a8b8bd63f9a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_i686.whl", hash = "sha256:bcb3ed405ed3222f9904899563d6fc492ff75cce56cba05e32eff40e6acbeaa3"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-musllinux_1_1_x86_64.whl", hash = "sha256:e1c0b87e09fa55a220f058d1d49d3fb8df88fbfab58558f1198e08c1e1de842a"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win32.whl", hash = "sha256:8dc1c72a69aa7e082593c4a203dcf94ddb74bb5c8a731e4e1eb68d031e8498ff"}, - {file = "MarkupSafe-2.1.1-cp37-cp37m-win_amd64.whl", hash = "sha256:97a68e6ada378df82bc9f16b800ab77cbf4b2fada0081794318520138c088e4a"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_universal2.whl", hash = "sha256:e8c843bbcda3a2f1e3c2ab25913c80a3c5376cd00c6e8c4a86a89a28c8dc5452"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0212a68688482dc52b2d45013df70d169f542b7394fc744c02a57374a4207003"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:8e576a51ad59e4bfaac456023a78f6b5e6e7651dcd383bcc3e18d06f9b55d6d1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:4b9fe39a2ccc108a4accc2676e77da025ce383c108593d65cc909add5c3bd601"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:96e37a3dc86e80bf81758c152fe66dbf60ed5eca3d26305edf01892257049925"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_aarch64.whl", hash = "sha256:6d0072fea50feec76a4c418096652f2c3238eaa014b2f94aeb1d56a66b41403f"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_i686.whl", hash = "sha256:089cf3dbf0cd6c100f02945abeb18484bd1ee57a079aefd52cffd17fba910b88"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-musllinux_1_1_x86_64.whl", hash = "sha256:6a074d34ee7a5ce3effbc526b7083ec9731bb3cbf921bbe1d3005d4d2bdb3a63"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win32.whl", hash = "sha256:421be9fbf0ffe9ffd7a378aafebbf6f4602d564d34be190fc19a193232fd12b1"}, - {file = "MarkupSafe-2.1.1-cp38-cp38-win_amd64.whl", hash = "sha256:fc7b548b17d238737688817ab67deebb30e8073c95749d55538ed473130ec0c7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_universal2.whl", hash = "sha256:e04e26803c9c3851c931eac40c695602c6295b8d432cbe78609649ad9bd2da8a"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:b87db4360013327109564f0e591bd2a3b318547bcef31b468a92ee504d07ae4f"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:99a2a507ed3ac881b975a2976d59f38c19386d128e7a9a18b7df6fff1fd4c1d6"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:56442863ed2b06d19c37f94d999035e15ee982988920e12a5b4ba29b62ad1f77"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-manylinux_2_5_i686.manylinux1_i686.manylinux_2_17_i686.manylinux2014_i686.whl", hash = "sha256:3ce11ee3f23f79dbd06fb3d63e2f6af7b12db1d46932fe7bd8afa259a5996603"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_aarch64.whl", hash = "sha256:33b74d289bd2f5e527beadcaa3f401e0df0a89927c1559c8566c066fa4248ab7"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_i686.whl", hash = "sha256:43093fb83d8343aac0b1baa75516da6092f58f41200907ef92448ecab8825135"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-musllinux_1_1_x86_64.whl", hash = "sha256:8e3dcf21f367459434c18e71b2a9532d96547aef8a871872a5bd69a715c15f96"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win32.whl", hash = "sha256:d4306c36ca495956b6d568d276ac11fdd9c30a36f1b6eb928070dc5360b22e1c"}, - {file = "MarkupSafe-2.1.1-cp39-cp39-win_amd64.whl", hash = "sha256:46d00d6cfecdde84d40e572d63735ef81423ad31184100411e6e3388d405e247"}, - {file = "MarkupSafe-2.1.1.tar.gz", hash = "sha256:7f91197cc9e48f989d12e4e6fbc46495c446636dfc81b9ccf50bb0ec74b91d4b"}, -] -more-itertools = [ - {file = "more-itertools-9.0.0.tar.gz", hash = "sha256:5a6257e40878ef0520b1803990e3e22303a41b5714006c32a3fd8304b26ea1ab"}, - {file = "more_itertools-9.0.0-py3-none-any.whl", hash = "sha256:250e83d7e81d0c87ca6bd942e6aeab8cc9daa6096d12c5308f3f92fa5e5c1f41"}, -] -nodeenv = [ - {file = "nodeenv-1.7.0-py2.py3-none-any.whl", hash = "sha256:27083a7b96a25f2f5e1d8cb4b6317ee8aeda3bdd121394e5ac54e498028a042e"}, - {file = "nodeenv-1.7.0.tar.gz", hash = "sha256:e0e7f7dfb85fc5394c6fe1e8fa98131a2473e04311a45afb6508f7cf1836fa2b"}, -] -packaging = [ - {file = "packaging-21.3-py3-none-any.whl", hash = "sha256:ef103e05f519cdc783ae24ea4e2e0f508a9c99b2d4969652eed6a2e1ea5bd522"}, - {file = "packaging-21.3.tar.gz", hash = "sha256:dd47c42927d89ab911e606518907cc2d3a1f38bbd026385970643f9c5b8ecfeb"}, -] -platformdirs = [ - {file = "platformdirs-2.5.4-py3-none-any.whl", hash = "sha256:af0276409f9a02373d540bf8480021a048711d572745aef4b7842dad245eba10"}, - {file = "platformdirs-2.5.4.tar.gz", hash = "sha256:1006647646d80f16130f052404c6b901e80ee4ed6bef6792e1f238a8969106f7"}, -] -pluggy = [ - {file = "pluggy-0.13.1-py2.py3-none-any.whl", hash = "sha256:966c145cd83c96502c3c3868f50408687b38434af77734af1e9ca461a4081d2d"}, - {file = "pluggy-0.13.1.tar.gz", hash = "sha256:15b2acde666561e1298d71b523007ed7364de07029219b604cf808bfa1c765b0"}, -] -pre-commit = [ - {file = "pre_commit-2.20.0-py2.py3-none-any.whl", hash = "sha256:51a5ba7c480ae8072ecdb6933df22d2f812dc897d5fe848778116129a681aac7"}, - {file = "pre_commit-2.20.0.tar.gz", hash = "sha256:a978dac7bc9ec0bcee55c18a277d553b0f419d259dadb4b9418ff2d00eb43959"}, -] -py = [ - {file = "py-1.11.0-py2.py3-none-any.whl", hash = "sha256:607c53218732647dff4acdfcd50cb62615cedf612e72d1724fb1a0cc6405b378"}, - {file = "py-1.11.0.tar.gz", hash = "sha256:51c75c4126074b472f746a24399ad32f6053d1b34b68d2fa41e558e6f4a98719"}, -] -pyparsing = [ - {file = "pyparsing-3.0.9-py3-none-any.whl", hash = "sha256:5026bae9a10eeaefb61dab2f09052b9f4307d44aee4eda64b309723d8d206bbc"}, - {file = "pyparsing-3.0.9.tar.gz", hash = "sha256:2b020ecf7d21b687f219b71ecad3631f644a47f01403fa1d1036b0c6416d70fb"}, -] -pytest = [ - {file = "pytest-5.4.3-py3-none-any.whl", hash = "sha256:5c0db86b698e8f170ba4582a492248919255fcd4c79b1ee64ace34301fb589a1"}, - {file = "pytest-5.4.3.tar.gz", hash = "sha256:7979331bfcba207414f5e1263b5a0f8f521d0f457318836a7355531ed1a4c7d8"}, -] -pyyaml = [ - {file = "PyYAML-6.0-cp310-cp310-macosx_10_9_x86_64.whl", hash = "sha256:d4db7c7aef085872ef65a8fd7d6d09a14ae91f691dec3e87ee5ee0539d516f53"}, - {file = "PyYAML-6.0-cp310-cp310-macosx_11_0_arm64.whl", hash = "sha256:9df7ed3b3d2e0ecfe09e14741b857df43adb5a3ddadc919a2d94fbdf78fea53c"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:77f396e6ef4c73fdc33a9157446466f1cff553d979bd00ecb64385760c6babdc"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:a80a78046a72361de73f8f395f1f1e49f956c6be882eed58505a15f3e430962b"}, - {file = "PyYAML-6.0-cp310-cp310-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:f84fbc98b019fef2ee9a1cb3ce93e3187a6df0b2538a651bfb890254ba9f90b5"}, - {file = "PyYAML-6.0-cp310-cp310-win32.whl", hash = "sha256:2cd5df3de48857ed0544b34e2d40e9fac445930039f3cfe4bcc592a1f836d513"}, - {file = "PyYAML-6.0-cp310-cp310-win_amd64.whl", hash = "sha256:daf496c58a8c52083df09b80c860005194014c3698698d1a57cbcfa182142a3a"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:d4b0ba9512519522b118090257be113b9468d804b19d63c71dbcf4a48fa32358"}, - {file = "PyYAML-6.0-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:81957921f441d50af23654aa6c5e5eaf9b06aba7f0a19c18a538dc7ef291c5a1"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:afa17f5bc4d1b10afd4466fd3a44dc0e245382deca5b3c353d8b757f9e3ecb8d"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:dbad0e9d368bb989f4515da330b88a057617d16b6a8245084f1b05400f24609f"}, - {file = "PyYAML-6.0-cp311-cp311-manylinux_2_17_x86_64.manylinux2014_x86_64.whl", hash = "sha256:432557aa2c09802be39460360ddffd48156e30721f5e8d917f01d31694216782"}, - {file = "PyYAML-6.0-cp311-cp311-win32.whl", hash = "sha256:bfaef573a63ba8923503d27530362590ff4f576c626d86a9fed95822a8255fd7"}, - {file = "PyYAML-6.0-cp311-cp311-win_amd64.whl", hash = "sha256:01b45c0191e6d66c470b6cf1b9531a771a83c1c4208272ead47a3ae4f2f603bf"}, - {file = "PyYAML-6.0-cp36-cp36m-macosx_10_9_x86_64.whl", hash = "sha256:897b80890765f037df3403d22bab41627ca8811ae55e9a722fd0392850ec4d86"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:50602afada6d6cbfad699b0c7bb50d5ccffa7e46a3d738092afddc1f9758427f"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:48c346915c114f5fdb3ead70312bd042a953a8ce5c7106d5bfb1a5254e47da92"}, - {file = "PyYAML-6.0-cp36-cp36m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:98c4d36e99714e55cfbaaee6dd5badbc9a1ec339ebfc3b1f52e293aee6bb71a4"}, - {file = "PyYAML-6.0-cp36-cp36m-win32.whl", hash = "sha256:0283c35a6a9fbf047493e3a0ce8d79ef5030852c51e9d911a27badfde0605293"}, - {file = "PyYAML-6.0-cp36-cp36m-win_amd64.whl", hash = "sha256:07751360502caac1c067a8132d150cf3d61339af5691fe9e87803040dbc5db57"}, - {file = "PyYAML-6.0-cp37-cp37m-macosx_10_9_x86_64.whl", hash = "sha256:819b3830a1543db06c4d4b865e70ded25be52a2e0631ccd2f6a47a2822f2fd7c"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:473f9edb243cb1935ab5a084eb238d842fb8f404ed2193a915d1784b5a6b5fc0"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:0ce82d761c532fe4ec3f87fc45688bdd3a4c1dc5e0b4a19814b9009a29baefd4"}, - {file = "PyYAML-6.0-cp37-cp37m-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:231710d57adfd809ef5d34183b8ed1eeae3f76459c18fb4a0b373ad56bedcdd9"}, - {file = "PyYAML-6.0-cp37-cp37m-win32.whl", hash = "sha256:c5687b8d43cf58545ade1fe3e055f70eac7a5a1a0bf42824308d868289a95737"}, - {file = "PyYAML-6.0-cp37-cp37m-win_amd64.whl", hash = "sha256:d15a181d1ecd0d4270dc32edb46f7cb7733c7c508857278d3d378d14d606db2d"}, - {file = "PyYAML-6.0-cp38-cp38-macosx_10_9_x86_64.whl", hash = "sha256:0b4624f379dab24d3725ffde76559cff63d9ec94e1736b556dacdfebe5ab6d4b"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:213c60cd50106436cc818accf5baa1aba61c0189ff610f64f4a3e8c6726218ba"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:9fa600030013c4de8165339db93d182b9431076eb98eb40ee068700c9c813e34"}, - {file = "PyYAML-6.0-cp38-cp38-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:277a0ef2981ca40581a47093e9e2d13b3f1fbbeffae064c1d21bfceba2030287"}, - {file = "PyYAML-6.0-cp38-cp38-win32.whl", hash = "sha256:d4eccecf9adf6fbcc6861a38015c2a64f38b9d94838ac1810a9023a0609e1b78"}, - {file = "PyYAML-6.0-cp38-cp38-win_amd64.whl", hash = "sha256:1e4747bc279b4f613a09eb64bba2ba602d8a6664c6ce6396a4d0cd413a50ce07"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_10_9_x86_64.whl", hash = "sha256:055d937d65826939cb044fc8c9b08889e8c743fdc6a32b33e2390f66013e449b"}, - {file = "PyYAML-6.0-cp39-cp39-macosx_11_0_arm64.whl", hash = "sha256:e61ceaab6f49fb8bdfaa0f92c4b57bcfbea54c09277b1b4f7ac376bfb7a7c174"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_aarch64.manylinux2014_aarch64.whl", hash = "sha256:d67d839ede4ed1b28a4e8909735fc992a923cdb84e618544973d7dfc71540803"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_17_s390x.manylinux2014_s390x.whl", hash = "sha256:cba8c411ef271aa037d7357a2bc8f9ee8b58b9965831d9e51baf703280dc73d3"}, - {file = "PyYAML-6.0-cp39-cp39-manylinux_2_5_x86_64.manylinux1_x86_64.manylinux_2_12_x86_64.manylinux2010_x86_64.whl", hash = "sha256:40527857252b61eacd1d9af500c3337ba8deb8fc298940291486c465c8b46ec0"}, - {file = "PyYAML-6.0-cp39-cp39-win32.whl", hash = "sha256:b5b9eccad747aabaaffbc6064800670f0c297e52c12754eb1d976c57e4f74dcb"}, - {file = "PyYAML-6.0-cp39-cp39-win_amd64.whl", hash = "sha256:b3d267842bf12586ba6c734f89d1f5b871df0273157918b0ccefa29deb05c21c"}, - {file = "PyYAML-6.0.tar.gz", hash = "sha256:68fb519c14306fec9720a2a5b45bc9f0c8d1b9c72adf45c37baedfcd949c35a2"}, -] -requests = [ - {file = "requests-2.28.1-py3-none-any.whl", hash = "sha256:8fefa2a1a1365bf5520aac41836fbee479da67864514bdb821f31ce07ce65349"}, - {file = "requests-2.28.1.tar.gz", hash = "sha256:7c5599b102feddaa661c826c56ab4fee28bfd17f5abca1ebbe3e7f19d7c97983"}, -] -scriv = [ - {file = "scriv-0.15.2-py2.py3-none-any.whl", hash = "sha256:24ea4b8c40a5af26121545ea3048d75bb4a480a05dcc7e8d6b09f558d1efe39d"}, - {file = "scriv-0.15.2.tar.gz", hash = "sha256:3be76949b24dab7143a5158168ada6ff2dc7104953dd041cf005b001aadc2705"}, -] -setuptools = [ - {file = "setuptools-65.6.3-py3-none-any.whl", hash = "sha256:57f6f22bde4e042978bcd50176fdb381d7c21a9efa4041202288d3737a0c6a54"}, - {file = "setuptools-65.6.3.tar.gz", hash = "sha256:a7620757bf984b58deaf32fc8a4577a9bbc0850cf92c20e1ce41c38c19e5fb75"}, -] -toml = [ - {file = "toml-0.10.2-py2.py3-none-any.whl", hash = "sha256:806143ae5bfb6a3c6e736a764057db0e6a0e05e338b5630894a5f779cabb4f9b"}, - {file = "toml-0.10.2.tar.gz", hash = "sha256:b3bda1d108d5dd99f4a20d24d9c348e91c4db7ab1b749200bded2f839ccbe68f"}, -] -tomli = [ - {file = "tomli-2.0.1-py3-none-any.whl", hash = "sha256:939de3e7a6161af0c887ef91b7d41a53e7c5a1ca976325f429cb46ea9bc30ecc"}, - {file = "tomli-2.0.1.tar.gz", hash = "sha256:de526c12914f0c550d15924c62d72abc48d6fe7364aa87328337a31007fe8a4f"}, -] -typing-extensions = [ - {file = "typing_extensions-4.4.0-py3-none-any.whl", hash = "sha256:16fa4864408f655d35ec496218b85f79b3437c829e93320c7c9215ccfd92489e"}, - {file = "typing_extensions-4.4.0.tar.gz", hash = "sha256:1511434bb92bf8dd198c12b1cc812e800d4181cfcb867674e0f8279cc93087aa"}, -] -urllib3 = [ - {file = "urllib3-1.26.13-py2.py3-none-any.whl", hash = "sha256:47cc05d99aaa09c9e72ed5809b60e7ba354e64b59c9c173ac3018642d8bb41fc"}, - {file = "urllib3-1.26.13.tar.gz", hash = "sha256:c083dd0dce68dbfbe1129d5271cb90f9447dea7d52097c6e0126120c521ddea8"}, -] -virtualenv = [ - {file = "virtualenv-20.17.0-py3-none-any.whl", hash = "sha256:40a7e06a98728fd5769e1af6fd1a706005b4bb7e16176a272ed4292473180389"}, - {file = "virtualenv-20.17.0.tar.gz", hash = "sha256:7d6a8d55b2f73b617f684ee40fd85740f062e1f2e379412cb1879c7136f05902"}, -] -wcwidth = [ - {file = "wcwidth-0.2.5-py2.py3-none-any.whl", hash = "sha256:beb4802a9cebb9144e99086eff703a642a13d6a0052920003a230f3294bbe784"}, - {file = "wcwidth-0.2.5.tar.gz", hash = "sha256:c4d647b99872929fdb7bdcaa4fbe7f01413ed3d98077df798530e5b04f116c83"}, -] -zipp = [ - {file = "zipp-3.11.0-py3-none-any.whl", hash = "sha256:83a28fcb75844b5c0cdaf5aa4003c2d728c77e05f5aeabe8e95e56727005fbaa"}, - {file = "zipp-3.11.0.tar.gz", hash = "sha256:a7a22e05929290a67401440b39690ae6563279bced5f314609d9d03798f56766"}, -] +lock-version = "2.0" +python-versions = "^3.9" +content-hash = "42e1a33d3e284c457cfc963cafe65e77c74af3ae8190d99338b00e8f0b593948" From c4c4adf638e854e7e04d766ba4cb8f39ef2b254d Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 21:33:38 +0200 Subject: [PATCH 103/116] refactor: rename piecepoly to piecewise --- src/pynurbs/core/{piecepoly.py => piecewise.py} | 0 src/pynurbs/core/spline_basis.py | 2 +- src/pynurbs/operations/roots.py | 2 +- tests/core/test_piecewise.py | 2 +- tests/core/test_spline_basis.py | 2 +- tests/operations/test_roots.py | 2 +- 6 files changed, 5 insertions(+), 5 deletions(-) rename src/pynurbs/core/{piecepoly.py => piecewise.py} (100%) diff --git a/src/pynurbs/core/piecepoly.py b/src/pynurbs/core/piecewise.py similarity index 100% rename from src/pynurbs/core/piecepoly.py rename to src/pynurbs/core/piecewise.py diff --git a/src/pynurbs/core/spline_basis.py b/src/pynurbs/core/spline_basis.py index 640836c..c00d886 100644 --- a/src/pynurbs/core/spline_basis.py +++ b/src/pynurbs/core/spline_basis.py @@ -5,7 +5,7 @@ from .custom_math import totuple from .knotvector import ImmutableKnotVector -from .piecepoly import PiecewisePolynomial +from .piecewise import PiecewisePolynomial from .polynomial import Polynomial, scale, shift diff --git a/src/pynurbs/operations/roots.py b/src/pynurbs/operations/roots.py index f097633..10468d8 100644 --- a/src/pynurbs/operations/roots.py +++ b/src/pynurbs/operations/roots.py @@ -9,7 +9,7 @@ import rbool from ..core.custom_math import isscalar -from ..core.piecepoly import PiecewisePolynomial +from ..core.piecewise import PiecewisePolynomial from ..core.polynomial import Polynomial diff --git a/tests/core/test_piecewise.py b/tests/core/test_piecewise.py index 898c645..89dd3a1 100644 --- a/tests/core/test_piecewise.py +++ b/tests/core/test_piecewise.py @@ -5,7 +5,7 @@ import numpy as np import pytest -from pynurbs.core.piecepoly import PiecewisePolynomial, Polynomial, find_span +from pynurbs.core.piecewise import PiecewisePolynomial, Polynomial, find_span def get_random_knots( diff --git a/tests/core/test_spline_basis.py b/tests/core/test_spline_basis.py index 6ff87a2..cf60090 100644 --- a/tests/core/test_spline_basis.py +++ b/tests/core/test_spline_basis.py @@ -4,7 +4,7 @@ import pytest from pynurbs.core.knotvector import ImmutableKnotVector -from pynurbs.core.piecepoly import PiecewisePolynomial +from pynurbs.core.piecewise import PiecewisePolynomial from pynurbs.core.polynomial import Polynomial from pynurbs.core.spline_basis import ImmutableSplineBasis diff --git a/tests/operations/test_roots.py b/tests/operations/test_roots.py index 5db5ef1..75ab128 100644 --- a/tests/operations/test_roots.py +++ b/tests/operations/test_roots.py @@ -1,6 +1,6 @@ import pytest -from pynurbs.core.piecepoly import PiecewisePolynomial +from pynurbs.core.piecewise import PiecewisePolynomial from pynurbs.core.polynomial import Polynomial from pynurbs.operations.roots import division, roots From edd2b8abd974d4a65e889fd1835501a0b2e9acfb Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 21:49:27 +0200 Subject: [PATCH 104/116] dev: flake8 ignore not used imports on __init__.py file --- tox.ini | 3 +++ 1 file changed, 3 insertions(+) diff --git a/tox.ini b/tox.ini index 0bf3e65..4f3613a 100644 --- a/tox.ini +++ b/tox.ini @@ -33,3 +33,6 @@ commands = pytest --cov={envsitepackagesdir}/pynurbs --cov-report=xml tests coverage report -m --fail-under 60 coverage xml + +[flake8] +per-file-ignores = __init__.py:F401 From 4be27363c1bcf80c7ea6bca720ab3617b236f9b4 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 21:52:45 +0200 Subject: [PATCH 105/116] style: set maximal line lenght to 79 chars --- pyproject.toml | 3 + src/pynurbs/basis_functions.py | 13 +++- src/pynurbs/core/custom_math.py | 8 ++- src/pynurbs/core/knotvector.py | 12 +++- src/pynurbs/core/piecewise.py | 19 ++++-- src/pynurbs/core/spline_basis.py | 4 +- src/pynurbs/curves/base.py | 35 +++++++--- src/pynurbs/curves/curves.py | 39 ++++++++--- src/pynurbs/knotspace.py | 16 +++-- src/pynurbs/operations/__init__.py | 7 +- src/pynurbs/operations/advanced.py | 21 ++++-- src/pynurbs/operations/calculus.py | 11 +++- src/pynurbs/operations/heavy.py | 35 +++++++--- src/pynurbs/operations/knotvector.py | 28 ++++++-- src/pynurbs/operations/least_square.py | 26 ++++++-- src/pynurbs/operations/roots.py | 4 +- tests/core/test_custom_math.py | 88 +++++++++++++++++++------ tests/core/test_knotvector.py | 12 +++- tests/core/test_piecewise.py | 4 +- tests/core/test_polynomial.py | 40 ++++++++++-- tests/core/test_spline_basis.py | 14 +++- tests/curves/test_bezier.py | 13 +++- tests/curves/test_rational.py | 90 ++++++++++++++++++++------ tests/curves/test_spline.py | 9 ++- tests/operations/test_advanced.py | 11 +++- tests/operations/test_calculus.py | 37 ++++++++--- tests/operations/test_customstruc.py | 15 ++++- tests/operations/test_fitting.py | 8 ++- tests/test_basis_functions.py | 32 +++++++-- tests/test_knotspace.py | 21 ++++-- 30 files changed, 525 insertions(+), 150 deletions(-) diff --git a/pyproject.toml b/pyproject.toml index 17d65aa..2ac89af 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -30,3 +30,6 @@ version = "literal: src/pynurbs/__init__.py: __version__" [build-system] requires = ["poetry-core>=1.0.0"] build-backend = "poetry.core.masonry.api" + +[tool.black] +line-length = 79 diff --git a/src/pynurbs/basis_functions.py b/src/pynurbs/basis_functions.py index 4400259..0ad84ab 100644 --- a/src/pynurbs/basis_functions.py +++ b/src/pynurbs/basis_functions.py @@ -12,7 +12,9 @@ class BaseFunction: def __init__( - self, knotvector: KnotVector, weights: Union[None, Iterable[Real]] = None + self, + knotvector: KnotVector, + weights: Union[None, Iterable[Real]] = None, ): self.knotvector = knotvector self.weights = weights @@ -20,7 +22,10 @@ def __init__( def __eq__(self, other: BaseFunction) -> bool: if not isinstance(other, BaseFunction): return NotImplemented - return self.knotvector == other.knotvector and self.weights == other.weights + return ( + self.knotvector == other.knotvector + and self.weights == other.weights + ) @property def knotvector(self) -> KnotVector: @@ -146,7 +151,9 @@ def weights(self, weights: Union[None, Tuple[Real, ...]]): return weights = tuple(weights) if len(weights) != self.npts: - raise ValueError(f"Weights must have len {self.npts} != {len(weights)}") + raise ValueError( + f"Weights must have len {self.npts} != {len(weights)}" + ) if not all(float(weight) > 0 for weight in weights): raise ValueError("All weights must be positive!") # Still needs to check if there are no roots diff --git a/src/pynurbs/core/custom_math.py b/src/pynurbs/core/custom_math.py index 40b0c09..f8d2769 100644 --- a/src/pynurbs/core/custom_math.py +++ b/src/pynurbs/core/custom_math.py @@ -136,7 +136,9 @@ class NodeSample: __gauss = {1: (Fraction(1, 2),)} @staticmethod - def closed_linspace(npts: int, cls: Optional[type] = Fraction) -> Tuple[float]: + def closed_linspace( + npts: int, cls: Optional[type] = Fraction + ) -> Tuple[float]: """Returns equally distributed nodes in [0, 1] Include the extremities @@ -160,7 +162,9 @@ def closed_linspace(npts: int, cls: Optional[type] = Fraction) -> Tuple[float]: return nums @staticmethod - def open_linspace(npts: int, cls: Optional[type] = Fraction) -> Tuple[float]: + def open_linspace( + npts: int, cls: Optional[type] = Fraction + ) -> Tuple[float]: """Returns equally distributed nodes in (0, 1) Exclude the extremities diff --git a/src/pynurbs/core/knotvector.py b/src/pynurbs/core/knotvector.py index 5e06286..b2e1df5 100644 --- a/src/pynurbs/core/knotvector.py +++ b/src/pynurbs/core/knotvector.py @@ -17,7 +17,9 @@ def is_sorted(vector: Tuple[Real, ...]) -> bool: class ImmutableKnotVector: - def __init__(self, vector: Iterable[Real], degree: Union[None, int] = None): + def __init__( + self, vector: Iterable[Real], degree: Union[None, int] = None + ): try: vector = tuple(vector) except Exception: @@ -85,7 +87,9 @@ def span(self, node: Real) -> int: if not isscalar(node): raise ValueError(f"Node '{node}' must be Real instance") if node < self[self.degree] or self[self.npts] < node: - raise ValueError(f"Node {node} outside [{self.knots[0], self.knots[-1]}]") + raise ValueError( + f"Node {node} outside [{self.knots[0], self.knots[-1]}]" + ) if node == self[self.npts]: # Special case return self.npts - 1 low, high = self.degree, self.npts + 1 # Do binary search @@ -103,5 +107,7 @@ def mult(self, node: Real) -> int: if not isscalar(node): raise ValueError(f"Node '{node}' must be Real instance") if node < self[self.degree] or self[self.npts] < node: - raise ValueError(f"Node {node} outside [{self.knots[0], self.knots[-1]}]") + raise ValueError( + f"Node {node} outside [{self.knots[0], self.knots[-1]}]" + ) return sum(abs(node - knot) < 1e-9 for knot in self) diff --git a/src/pynurbs/core/piecewise.py b/src/pynurbs/core/piecewise.py index ce844e8..7577e87 100644 --- a/src/pynurbs/core/piecewise.py +++ b/src/pynurbs/core/piecewise.py @@ -47,9 +47,12 @@ class PiecewisePolynomial: Defines a Polynomial piecewise function """ - def __init__(self, functions: Iterable[Polynomial], knots: Iterable[Real]) -> None: + def __init__( + self, functions: Iterable[Polynomial], knots: Iterable[Real] + ) -> None: functions = tuple( - f if isinstance(f, Polynomial) else Polynomial(f) for f in functions + f if isinstance(f, Polynomial) else Polynomial(f) + for f in functions ) knots = tuple(knots) if len(knots) != 1 + len(functions): @@ -97,7 +100,9 @@ def __add__( self, other: Union[Real, Polynomial, PiecewisePolynomial] ) -> PiecewisePolynomial: if not isinstance(other, PiecewisePolynomial): - return self.__class__((func + other for func in self.functions), self.knots) + return self.__class__( + (func + other for func in self.functions), self.knots + ) allknots = sorted(set(self.knots) | set(other.knots)) functions = [None] * (len(allknots) - 1) for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): @@ -111,7 +116,9 @@ def __mul__( self, other: Union[Real, Polynomial, PiecewisePolynomial] ) -> PiecewisePolynomial: if not isinstance(other, PiecewisePolynomial): - return self.__class__((func * other for func in self.functions), self.knots) + return self.__class__( + (func * other for func in self.functions), self.knots + ) allknots = sorted(set(self.knots) | set(other.knots)) functions = [None] * (len(allknots) - 1) for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): @@ -125,7 +132,9 @@ def __matmul__( self, other: Union[Real, Polynomial, PiecewisePolynomial] ) -> PiecewisePolynomial: if not isinstance(other, PiecewisePolynomial): - return self.__class__((func @ other for func in self.functions), self.knots) + return self.__class__( + (func @ other for func in self.functions), self.knots + ) allknots = sorted(set(self.knots) | set(other.knots)) functions = [None] * (len(allknots) - 1) for i, (knota, knotb) in enumerate(zip(allknots, allknots[1:])): diff --git a/src/pynurbs/core/spline_basis.py b/src/pynurbs/core/spline_basis.py index c00d886..bc79974 100644 --- a/src/pynurbs/core/spline_basis.py +++ b/src/pynurbs/core/spline_basis.py @@ -123,4 +123,6 @@ def __call__(self, node: Real) -> Tuple[Real, ...]: return tuple(result) def __str__(self): - return "{" + ", ".join(f"N{i}: {self[i]}" for i in range(self.npts)) + "}" + return ( + "{" + ", ".join(f"N{i}: {self[i]}" for i in range(self.npts)) + "}" + ) diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py index 9313d41..b6c78e9 100644 --- a/src/pynurbs/curves/base.py +++ b/src/pynurbs/curves/base.py @@ -60,7 +60,9 @@ def __call__(self, node: Real) -> Any: return sum((r * c for r, c in zip(result, self.ctrlpoints)), zero) result = tuple(w * r for w, r in zip(self.weights, result)) denom = 1 / sum(result) - return sum((r * c * denom for r, c in zip(result, self.ctrlpoints)), zero) + return sum( + (r * c * denom for r, c in zip(result, self.ctrlpoints)), zero + ) def __eq__(self, other: object) -> bool: if type(self) is not type(other): @@ -167,7 +169,8 @@ def __matmul__(self, other: object): vectmul = heavy.MathOperations.knotvector_mul(vecta, vectb) matrix3d = heavy.MathOperations.mul_spline_curve(vecta, vectb) matrix2d = [ - [pt0 @ pt1 for pt0 in self.ctrlpoints] for pt1 in other.ctrlpoints + [pt0 @ pt1 for pt0 in self.ctrlpoints] + for pt1 in other.ctrlpoints ] matrix3d = np.array(matrix3d) matrix2d = np.array(matrix2d) @@ -202,10 +205,16 @@ def __truediv__(self, other: object): if self.weights is None and other.weights is None: copyse = copy(self) copyot = copy(other) - vectora, vectorb = tuple(copyse.knotvector), tuple(copyot.knotvector) + vectora, vectorb = tuple(copyse.knotvector), tuple( + copyot.knotvector + ) vectorc = tuple(copyse.knotvector | copyot.knotvector) - transctrlpts = heavy.Operations.matrix_transformation(vectora, vectorc) - transweights = heavy.Operations.matrix_transformation(vectorb, vectorc) + transctrlpts = heavy.Operations.matrix_transformation( + vectora, vectorc + ) + transweights = heavy.Operations.matrix_transformation( + vectorb, vectorc + ) weights = np.dot(transweights, copyot.ctrlpoints) ctrlpts = np.dot(transctrlpts, copyse.ctrlpoints) ctrlpts = [pti / wi for pti, wi in zip(ctrlpts, weights)] @@ -453,11 +462,17 @@ def ctrlpoints(self, newpoints: np.ndarray): for knot in self.knotvector.knots: knot * point for otherpoint in newpoints: - point + otherpoint # Verify if we can sum every point, same type + ( + point + otherpoint + ) # Verify if we can sum every point, same type if len(newpoints) != self.npts: - error_msg = f"The number of control points ({len(newpoints)}) must be " - error_msg += f"the same as npts of KnotVector ({self.knotvector.npts})\n" + error_msg = ( + f"The number of control points ({len(newpoints)}) must be " + ) + error_msg += ( + f"the same as npts of KnotVector ({self.knotvector.npts})\n" + ) error_msg += f" knotvector.npts = {self.npts}" error_msg += f" len(ctrlpoints) = {len(newpoints)}" raise ValueError(error_msg) @@ -511,7 +526,9 @@ def fraction(self) -> Tuple[BaseCurve]: ctrlpoints = [copy(point) for point in self.ctrlpoints] numerator = self.__class__(copy(self.knotvector)) denominator = self.__class__(copy(self.knotvector)) - numerator.ctrlpoints = [wi * pt for wi, pt in zip(self.weights, ctrlpoints)] + numerator.ctrlpoints = [ + wi * pt for wi, pt in zip(self.weights, ctrlpoints) + ] denominator.ctrlpoints = self.weights return numerator, denominator diff --git a/src/pynurbs/curves/curves.py b/src/pynurbs/curves/curves.py index bd4baf5..6122572 100644 --- a/src/pynurbs/curves/curves.py +++ b/src/pynurbs/curves/curves.py @@ -99,7 +99,9 @@ def knot_insert(self, nodes: Tuple[float]) -> None: matrix = heavy.Operations.knot_insert(oldvector, nodes) self.apply(newvector, matrix) - def knot_remove(self, nodes: Tuple[float], tolerance: float = 1e-9) -> None: + def knot_remove( + self, nodes: Tuple[float], tolerance: float = 1e-9 + ) -> None: """Remove given nodes from knotvector :param nodes: The nodes to be removed @@ -134,7 +136,9 @@ def knot_remove(self, nodes: Tuple[float], tolerance: float = 1e-9) -> None: self.tolerance = old_tolerance def knot_clean( - self, nodes: Optional[Tuple[float]] = None, tolerance: Optional[float] = 1e-9 + self, + nodes: Optional[Tuple[float]] = None, + tolerance: Optional[float] = 1e-9, ) -> None: """Remove all unnecessary knots. @@ -323,8 +327,12 @@ def clean(self, tolerance: float = 1e-9): knotvector = tuple(self.knotvector) weights = tuple(self.weights) ctrlpoints = tuple(self.ctrlpoints) - mattrans, materror = func2func(knotvector, weights, knotvector, [1] * self.npts) - error = np.dot(np.moveaxis(ctrlpoints, 0, -1), np.dot(materror, ctrlpoints)) + mattrans, materror = func2func( + knotvector, weights, knotvector, [1] * self.npts + ) + error = np.dot( + np.moveaxis(ctrlpoints, 0, -1), np.dot(materror, ctrlpoints) + ) error = np.max(abs(error)) error = max(error, np.dot(weights, np.dot(materror, weights))) if error < tolerance: @@ -418,22 +426,29 @@ def fit_curve(self, other: Curve, nodes: Tuple[float] = None) -> float: weightsa = self.weights if self.weights else [1] * self.npts weightsb = other.weights if other.weights else [1] * other.npts lstsq = func2func - transmat, materror = lstsq(vectorb, weightsb, vectora, weightsa, nodes) + transmat, materror = lstsq( + vectorb, weightsb, vectora, weightsa, nodes + ) transmat = np.array(transmat) ctrlpoints = np.dot(transmat, other.ctrlpoints) error = np.dot( - np.moveaxis(other.ctrlpoints, 0, -1), np.dot(materror, other.ctrlpoints) + np.moveaxis(other.ctrlpoints, 0, -1), + np.dot(materror, other.ctrlpoints), ) error = np.max(np.abs(error)) if other.weights is not None: error += np.dot(other.weights, np.dot(materror, other.ctrlpoints)) weights = np.dot(transmat, weightsb) - ctrlpoints = [point / weig for point, weig in zip(ctrlpoints, weights)] + ctrlpoints = [ + point / weig for point, weig in zip(ctrlpoints, weights) + ] self.weights = weights self.ctrlpoints = ctrlpoints return error - def fit_function(self, function: Callable, nodes: Tuple[float] = None) -> None: + def fit_function( + self, function: Callable, nodes: Tuple[float] = None + ) -> None: """Finds the control points such this curve keeps as near as possible to ``function`` @@ -471,7 +486,9 @@ def fit_function(self, function: Callable, nodes: Tuple[float] = None) -> None: raise NotImplementedError assert not isinstance(function, self.__class__) knots = self.knotvector.knots - npts_each = 1 + int(np.ceil(self.degree * self.npts / (len(knots) - 1))) + npts_each = 1 + int( + np.ceil(self.degree * self.npts / (len(knots) - 1)) + ) nodes = [] numbtype = number_type(knots) if numbtype in (float, np.floating): @@ -485,7 +502,9 @@ def fit_function(self, function: Callable, nodes: Tuple[float] = None) -> None: funcvals = [function(node) for node in nodes] return self.fit_points(funcvals, nodes) - def fit_points(self, points: Tuple[Any], nodes: Tuple[float] = None) -> None: + def fit_points( + self, points: Tuple[Any], nodes: Tuple[float] = None + ) -> None: """Finds the control points such this curve keeps as near as possible to ``points`` diff --git a/src/pynurbs/knotspace.py b/src/pynurbs/knotspace.py index dcdbd3f..f011f55 100644 --- a/src/pynurbs/knotspace.py +++ b/src/pynurbs/knotspace.py @@ -315,7 +315,9 @@ def scale(self, value: Real) -> KnotVector: self.internal = ImmutableKnotVector(knoti * value for knoti in self) return self - def convert(self, cls: type, tolerance: Union[None, Real] = 1e-9) -> KnotVector: + def convert( + self, cls: type, tolerance: Union[None, Real] = 1e-9 + ) -> KnotVector: """Convert the knots from current type to given type. If ``tolerance`` is too small, it raises a ValueError cause cannot convert. @@ -584,7 +586,9 @@ def bezier(degree: int, cls: Optional[type] = int) -> KnotVector: return KnotVector(knotvector) @staticmethod - def integer(degree: int, npts: int, cls: Optional[type] = int) -> KnotVector: + def integer( + degree: int, npts: int, cls: Optional[type] = int + ) -> KnotVector: """Creates a KnotVector of equally integer spaced. :param degree: The degree of the curve, non-negative @@ -623,7 +627,9 @@ def integer(degree: int, npts: int, cls: Optional[type] = int) -> KnotVector: return knotvector @staticmethod - def uniform(degree: int, npts: int, cls: Optional[type] = int) -> KnotVector: + def uniform( + degree: int, npts: int, cls: Optional[type] = int + ) -> KnotVector: """Creates a equally distributed knotvector between [0, 1] :param degree: The degree of the curve, non-negative @@ -656,7 +662,9 @@ def uniform(degree: int, npts: int, cls: Optional[type] = int) -> KnotVector: return knotvector @staticmethod - def random(degree: int, npts: int, cls: Optional[type] = float) -> KnotVector: + def random( + degree: int, npts: int, cls: Optional[type] = float + ) -> KnotVector: """Creates a random distributed knotvector between [0, 1] :param degree: The degree of the curve, non-negative diff --git a/src/pynurbs/operations/__init__.py b/src/pynurbs/operations/__init__.py index 56c5b9d..7b89f40 100644 --- a/src/pynurbs/operations/__init__.py +++ b/src/pynurbs/operations/__init__.py @@ -1 +1,6 @@ -from .knotvector import decrease_degree, increase_degree, insert_knots, remove_knots +from .knotvector import ( + decrease_degree, + increase_degree, + insert_knots, + remove_knots, +) diff --git a/src/pynurbs/operations/advanced.py b/src/pynurbs/operations/advanced.py index 92d20c6..2b75bae 100644 --- a/src/pynurbs/operations/advanced.py +++ b/src/pynurbs/operations/advanced.py @@ -168,7 +168,10 @@ def filter_pairs(pairs: Tuple[Tuple[float]], tolerance: float = 1e-9): @staticmethod def pairs_min_distance( - pairs: Tuple[float], curvea: Curve, curveb: Curve, tolerance: float = 1e-9 + pairs: Tuple[float], + curvea: Curve, + curveb: Curve, + tolerance: float = 1e-9, ): """ Filter the pairs (t*, u*) such abs(curvea(t*) - curveb(u*)) > tolerance @@ -232,7 +235,9 @@ def __newton_bcurve_and_bcurve( return tuple(pair) @staticmethod - def bcurve_and_bcurve(beziera: Curve, bezierb: Curve) -> Tuple[float, float]: + def bcurve_and_bcurve( + beziera: Curve, bezierb: Curve + ) -> Tuple[float, float]: """Return the parameters t*, u* such beziera(t*) = bezierb(u*) Given two bezier curves, A(t) and B(u), this function returns the @@ -250,7 +255,9 @@ def bcurve_and_bcurve(beziera: Curve, bezierb: Curve) -> Tuple[float, float]: assert isinstance(bezierb, Curve) assert beziera.degree + 1 == beziera.npts assert beziera.degree + 1 == beziera.npts - if not Intersection._inse_retangle(beziera.ctrlpoints, bezierb.ctrlpoints): + if not Intersection._inse_retangle( + beziera.ctrlpoints, bezierb.ctrlpoints + ): return tuple() curvesa = [beziera] @@ -264,8 +271,12 @@ def bcurve_and_bcurve(beziera: Curve, bezierb: Curve) -> Tuple[float, float]: uamin, uamax = beziera.knotvector.limits ubmin, ubmax = bezierb.knotvector.limits limits = ((uamin, uamax), (ubmin, ubmax)) - nodes_a_sample = [0] + [(2 * i + 1) / (2 * nsma) for i in range(nsma)] + [1] - nodes_b_sample = [0] + [(2 * i + 1) / (2 * nsmb) for i in range(nsmb)] + [1] + nodes_a_sample = ( + [0] + [(2 * i + 1) / (2 * nsma) for i in range(nsma)] + [1] + ) + nodes_b_sample = ( + [0] + [(2 * i + 1) / (2 * nsmb) for i in range(nsmb)] + [1] + ) uasample = [uamin + (uamax - uamin) * node for node in nodes_a_sample] ubsample = [ubmin + (ubmax - ubmin) * node for node in nodes_b_sample] pairs = set() diff --git a/src/pynurbs/operations/calculus.py b/src/pynurbs/operations/calculus.py index ab98013..c3fb8e9 100644 --- a/src/pynurbs/operations/calculus.py +++ b/src/pynurbs/operations/calculus.py @@ -61,7 +61,9 @@ def rational_bezier(curve: Curve) -> Curve: assert np.all(np.array(curve.weights) != 0) knotvector = curve.knotvector.internal - matrixup, matrixdo = heavy.Calculus.derivate_rational_bezier(knotvector) + matrixup, matrixdo = heavy.Calculus.derivate_rational_bezier( + knotvector + ) num, den = curve.fraction() matrixup = np.dot(matrixup, den.ctrlpoints) matrixdo = np.dot(matrixdo, den.ctrlpoints) @@ -69,10 +71,13 @@ def rational_bezier(curve: Curve) -> Curve: dennumctrlpts = den.ctrlpoints @ matrixdo newnumctrlpts = np.dot(np.transpose(matrixup), num.ctrlpoints) newnumctrlpts = [ - point / weight for point, weight in zip(newnumctrlpts, dennumctrlpts) + point / weight + for point, weight in zip(newnumctrlpts, dennumctrlpts) ] number_bound = 1 + 2 * curve.degree - newknotvector = number_bound * [knotvector[0]] + number_bound * [knotvector[-1]] + newknotvector = number_bound * [knotvector[0]] + number_bound * [ + knotvector[-1] + ] finalcurve = curve.__class__(newknotvector) finalcurve.ctrlpoints = newnumctrlpts finalcurve.weights = dennumctrlpts diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index 4a1a01e..fc049c0 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -88,7 +88,9 @@ def one_knot_insert_once( """ knotvector = ImmutableKnotVector(knotvector) if not (knotvector.knots[0] <= node <= knotvector.knots[-1]): - raise ValueError(f"Invalid nodes {node} in knotvector {knotvector}") + raise ValueError( + f"Invalid nodes {node} in knotvector {knotvector}" + ) oldnpts = knotvector.npts degree = knotvector.degree @@ -137,7 +139,9 @@ def one_knot_insert( knotvector = opekv.insert_knots(knotvector, [node]) return totuple(matrix) - def knot_insert(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix2D": + def knot_insert( + knotvector: ImmutableKnotVector, nodes: Tuple[float] + ) -> "Matrix2D": """ Given the knotvector and a node to be inserted, this function returns a matrix of transformation T of control points @@ -156,11 +160,16 @@ def knot_insert(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix knotvector = ImmutableKnotVector(knotvector) if not all( - knotvector.knots[0] <= node <= knotvector.knots[-1] for node in nodes + knotvector.knots[0] <= node <= knotvector.knots[-1] + for node in nodes ): - raise ValueError(f"Invalid nodes {nodes} in knotvector {knotvector}") + raise ValueError( + f"Invalid nodes {nodes} in knotvector {knotvector}" + ) nodes = tuple(nodes) - setnodes = tuple(sorted(set(nodes) - set([knotvector[0], knotvector[-1]]))) + setnodes = tuple( + sorted(set(nodes) - set([knotvector[0], knotvector[-1]])) + ) oldnpts = knotvector.npts matrix = np.eye(oldnpts, dtype="object") if len(nodes) == 0: @@ -172,7 +181,9 @@ def knot_insert(knotvector: ImmutableKnotVector, nodes: Tuple[float]) -> "Matrix knotvector = opekv.insert_knots(knotvector, times * [node]) return totuple(matrix) - def degree_increase_bezier_once(knotvector: ImmutableKnotVector) -> "Matrix2D": + def degree_increase_bezier_once( + knotvector: ImmutableKnotVector, + ) -> "Matrix2D": knotvector = ImmutableKnotVector(knotvector) one = knotvector[-1] - knotvector[0] one /= one @@ -211,7 +222,9 @@ def degree_increase_bezier( knotvector = opekv.increase_degree(knotvector, 1) return totuple(matrix) - def degree_increase(knotvector: ImmutableKnotVector, times: int) -> "Matrix2D": + def degree_increase( + knotvector: ImmutableKnotVector, times: int + ) -> "Matrix2D": """ Given a curve A(u) associated with control points P, we want to do a degree elevation @@ -235,7 +248,9 @@ def degree_increase(knotvector: ImmutableKnotVector, times: int) -> "Matrix2D": bigmatrix = [] for splitedvector, splitedmatrix in zip(newvectors, matrices): splitedmatrix = np.array(splitedmatrix) - elevatedmatrix = Operations.degree_increase_bezier(splitedvector, times) + elevatedmatrix = Operations.degree_increase_bezier( + splitedvector, times + ) newmatrix = elevatedmatrix @ splitedmatrix for linemat in newmatrix: bigmatrix.append(linemat) @@ -275,7 +290,9 @@ def matrix_transformation( degreea = knotvectora.degree degreeb = knotvectorb.degree assert degreea <= degreeb - matrix_deginc = Operations.degree_increase(knotvectora, degreeb - degreea) + matrix_deginc = Operations.degree_increase( + knotvectora, degreeb - degreea + ) if degreea < degreeb: knotvectora = opekv.increase_degree(knotvectora, degreeb - degreea) diff --git a/src/pynurbs/operations/knotvector.py b/src/pynurbs/operations/knotvector.py index b191057..1b44821 100644 --- a/src/pynurbs/operations/knotvector.py +++ b/src/pynurbs/operations/knotvector.py @@ -48,7 +48,9 @@ def remove_knots( return ImmutableKnotVector(new_knots, knotvector.degree) -def increase_degree(knotvector: ImmutableKnotVector, times: int) -> ImmutableKnotVector: +def increase_degree( + knotvector: ImmutableKnotVector, times: int +) -> ImmutableKnotVector: """ Increases the degree of the given knotvector @@ -70,7 +72,9 @@ def increase_degree(knotvector: ImmutableKnotVector, times: int) -> ImmutableKno return ImmutableKnotVector(new_knots, knotvector.degree + times) -def decrease_degree(knotvector: ImmutableKnotVector, times: int) -> ImmutableKnotVector: +def decrease_degree( + knotvector: ImmutableKnotVector, times: int +) -> ImmutableKnotVector: """ Decreases the degree of the given knotvector @@ -134,13 +138,17 @@ def union_knotvectors( if not all(isinstance(vec, ImmutableKnotVector) for vec in knotvectors): raise TypeError left, right = knotvectors[0].knots[0], knotvectors[0].knots[-1] - if any(vec.knots[0] != left or vec.knots[-1] != right for vec in knotvectors): + if any( + vec.knots[0] != left or vec.knots[-1] != right for vec in knotvectors + ): raise ValueError maxdeg = max(vec.degree for vec in knotvectors) internals = {} for knotvector in knotvectors: if knotvector.degree < maxdeg: - knotvector = increase_degree(knotvector, maxdeg - knotvector.degree) + knotvector = increase_degree( + knotvector, maxdeg - knotvector.degree + ) for knot in knotvector.knots[1:-1]: if knot not in internals: internals[knot] = 0 @@ -162,19 +170,25 @@ def intersect_knotvectors( if not all(isinstance(vec, ImmutableKnotVector) for vec in knotvectors): raise TypeError left, right = knotvectors[0].knots[0], knotvectors[0].knots[-1] - if any(vec.knots[0] != left or vec.knots[-1] != right for vec in knotvectors): + if any( + vec.knots[0] != left or vec.knots[-1] != right for vec in knotvectors + ): raise ValueError mindeg = min(vec.degree for vec in knotvectors) internals = {} for knotvector in knotvectors: if knotvector.degree > mindeg: - knotvector = decrease_degree(knotvector, knotvector.degree - mindeg) + knotvector = decrease_degree( + knotvector, knotvector.degree - mindeg + ) for knot in knotvector.knots[1:-1]: if knot not in internals: internals[knot] = knotvector.mult(knot) for knotvector in knotvectors: if knotvector.degree > mindeg: - knotvector = decrease_degree(knotvector, knotvector.degree - mindeg) + knotvector = decrease_degree( + knotvector, knotvector.degree - mindeg + ) for knot in internals.keys(): internals[knot] = min(internals[knot], knotvector.mult(knot)) final = [left] * (mindeg + 1) diff --git a/src/pynurbs/operations/least_square.py b/src/pynurbs/operations/least_square.py index 872bc91..5ed3dc1 100644 --- a/src/pynurbs/operations/least_square.py +++ b/src/pynurbs/operations/least_square.py @@ -28,7 +28,13 @@ import numpy as np -from ..core.custom_math import IntegratorArray, Linalg, NodeSample, number_type, totuple +from ..core.custom_math import ( + IntegratorArray, + Linalg, + NodeSample, + number_type, + totuple, +) from ..core.knotvector import ImmutableKnotVector from ..core.spline_basis import ImmutableSplineBasis @@ -103,7 +109,9 @@ def spline2spline( newnpts = newknotvector.npts oldweights = [Fraction(1) for i in range(oldnpts)] newweights = [Fraction(1) for i in range(newnpts)] - result = func2func(oldknotvector, oldweights, newknotvector, newweights, fit_nodes) + result = func2func( + oldknotvector, oldweights, newknotvector, newweights, fit_nodes + ) return totuple(result) @@ -137,10 +145,12 @@ def func2func( newknots = newknotvector.knots oldknotvector = tuple( - Fraction(node) if isinstance(node, int) else node for node in oldknotvector + Fraction(node) if isinstance(node, int) else node + for node in oldknotvector ) newknotvector = tuple( - Fraction(node) if isinstance(node, int) else node for node in newknotvector + Fraction(node) if isinstance(node, int) else node + for node in newknotvector ) oldknotvector = ImmutableKnotVector(oldknotvector, olddegree) newknotvector = ImmutableKnotVector(newknotvector, newdegree) @@ -189,8 +199,12 @@ def func2func( fit_nodes = tuple( Fraction(node) if isinstance(node, int) else node for node in fit_nodes ) - F = eval_rational_nodes(oldknotvector, oldweights, tuple(fit_nodes), olddegree) - G = eval_rational_nodes(newknotvector, newweights, tuple(fit_nodes), newdegree) + F = eval_rational_nodes( + oldknotvector, oldweights, tuple(fit_nodes), olddegree + ) + G = eval_rational_nodes( + newknotvector, newweights, tuple(fit_nodes), newdegree + ) F = np.array(F, dtype="object").T GT = np.array(G, dtype="object") G = np.transpose(GT) diff --git a/src/pynurbs/operations/roots.py b/src/pynurbs/operations/roots.py index 10468d8..c72aa36 100644 --- a/src/pynurbs/operations/roots.py +++ b/src/pynurbs/operations/roots.py @@ -13,7 +13,9 @@ from ..core.polynomial import Polynomial -def division(poly: Polynomial, doly: Polynomial) -> Tuple[Polynomial, Polynomial]: +def division( + poly: Polynomial, doly: Polynomial +) -> Tuple[Polynomial, Polynomial]: """ Given the polynomials poly and doly, finds qoly and roly such: diff --git a/tests/core/test_custom_math.py b/tests/core/test_custom_math.py index 7810be9..31a5366 100644 --- a/tests/core/test_custom_math.py +++ b/tests/core/test_custom_math.py @@ -36,7 +36,9 @@ def test_gcd(self): assert Math.gcd(6, 9, 12) == 3 @pytest.mark.order(11) - @pytest.mark.dependency(depends=["TestMath::test_begin", "TestMath::test_gcd"]) + @pytest.mark.dependency( + depends=["TestMath::test_begin", "TestMath::test_gcd"] + ) def test_lcm(self): assert Math.lcm(0) == 0 assert Math.lcm(1) == 1 @@ -178,7 +180,8 @@ def test_invert_fraction(self): for side in range(1, 10): zero, one = Fraction(0), Fraction(1) matrix = [ - [one if i == j else zero for j in range(side)] for i in range(side) + [one if i == j else zero for j in range(side)] + for i in range(side) ] test = Linalg.invert(matrix) test = np.array(test, dtype="int64") @@ -225,7 +228,10 @@ def test_invert_fraction(self): for j, elem in enumerate(line): fracmatrix[i, j] = Fraction(elem).limit_denominator(10) fracmatrix += np.transpose(fracmatrix) - if abs(np.linalg.det(np.array(fracmatrix, dtype="float64"))) > 1e-6: + if ( + abs(np.linalg.det(np.array(fracmatrix, dtype="float64"))) + > 1e-6 + ): break invfracmatrix = Linalg.invert(fracmatrix) product = fracmatrix @ invfracmatrix @@ -249,28 +255,40 @@ def test_solve_float(self): ) def test_solve_integer(self): side, nsols = 2, 4 - force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] + force = [ + [np.random.randint(-5, 6) for j in range(nsols)] + for i in range(side) + ] matrix = ((1, 0), (0, 1)) solution = Linalg.solve(matrix, force) mult = np.dot(matrix, solution) np.testing.assert_allclose(mult, force) side, nsols = 2, 4 - force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] + force = [ + [np.random.randint(-5, 6) for j in range(nsols)] + for i in range(side) + ] matrix = ((1, 1), (2, 3)) solution = Linalg.solve(matrix, force) mult = np.dot(matrix, solution) np.testing.assert_allclose(mult, force) side, nsols = 2, 4 - force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] + force = [ + [np.random.randint(-5, 6) for j in range(nsols)] + for i in range(side) + ] matrix = ((1, 1), (11, 12)) solution = Linalg.solve(matrix, force) mult = np.dot(matrix, solution) np.testing.assert_allclose(mult, force) side, nsols = 4, 4 - force = [[np.random.randint(-5, 6) for j in range(nsols)] for i in range(side)] + force = [ + [np.random.randint(-5, 6) for j in range(nsols)] + for i in range(side) + ] matrix = ( (1, 1, 1, 1), (11, 12, 13, 14), @@ -307,7 +325,8 @@ def test_solve_fraction(self): for side in range(1, 10): zero, one = Fraction(0), Fraction(1) matrix = [ - [one if i == j else zero for j in range(side)] for i in range(side) + [one if i == j else zero for j in range(side)] + for i in range(side) ] test = Linalg.invert(matrix) test = np.array(test, dtype="int64") @@ -338,7 +357,15 @@ def test_solve_fraction(self): def test_specific_case(self): f = Fraction B = [ - [f(1, 9), f(1, 12), f(5, 84), f(5, 126), f(1, 42), f(1, 84), f(17, 4235)], + [ + f(1, 9), + f(1, 12), + f(5, 84), + f(5, 126), + f(1, 42), + f(1, 84), + f(17, 4235), + ], [ f(1, 36), f(1, 21), @@ -503,7 +530,10 @@ def test_chebyshev(self): np.testing.assert_allclose(nodes, good) nodes = NodeSample.chebyshev(5) - good = np.sin(np.pi * np.array([1 / 20, 3 / 20, 5 / 20, 7 / 20, 9 / 20])) ** 2 + good = ( + np.sin(np.pi * np.array([1 / 20, 3 / 20, 5 / 20, 7 / 20, 9 / 20])) + ** 2 + ) np.testing.assert_allclose(nodes, good) @pytest.mark.order(11) @@ -525,7 +555,12 @@ def test_gauss_legendre(self): nodes = NodeSample.gauss_legendre(4) minor = np.sqrt(3 / 7 + 2 * np.sqrt(6 / 5) / 7) middl = np.sqrt(3 / 7 - 2 * np.sqrt(6 / 5) / 7) - good = [(1 - minor) / 2, (1 - middl) / 2, (1 + middl) / 2, (1 + minor) / 2] + good = [ + (1 - minor) / 2, + (1 - middl) / 2, + (1 + middl) / 2, + (1 + minor) / 2, + ] np.testing.assert_allclose(nodes, good) nodes = NodeSample.gauss_legendre(5) @@ -575,7 +610,10 @@ def test_closed_newton_cotes(self): npts = max(2, degree + 1) # Number integration points numers = np.random.randint(-5, 5, degree + 1) denoms = np.random.randint(2, 8, degree + 1) - coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] + coefs = [ + Fraction(int(num), int(den)) + for num, den in zip(numers, denoms) + ] good = sum( ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) for i, ci in enumerate(coefs) @@ -599,7 +637,10 @@ def test_open_newton_cotes(self): npts = degree + 1 # Number integration points numers = np.random.randint(-5, 5, degree + 1) denoms = np.random.randint(2, 8, degree + 1) - coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] + coefs = [ + Fraction(int(num), int(den)) + for num, den in zip(numers, denoms) + ] good = sum( ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) for i, ci in enumerate(coefs) @@ -623,7 +664,10 @@ def test_chebyshev(self): npts = degree + 1 # Number integration points numers = np.random.randint(-5, 5, degree + 1) denoms = np.random.randint(2, 8, degree + 1) - coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] + coefs = [ + Fraction(int(num), int(den)) + for num, den in zip(numers, denoms) + ] good = sum( ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) for i, ci in enumerate(coefs) @@ -647,7 +691,10 @@ def test_gauss_legendre(self): npts = degree + 1 # Number integration points numers = np.random.randint(-5, 5, degree + 1) denoms = np.random.randint(2, 8, degree + 1) - coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] + coefs = [ + Fraction(int(num), int(den)) + for num, den in zip(numers, denoms) + ] good = sum( ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) for i, ci in enumerate(coefs) @@ -679,7 +726,10 @@ def test_exact_integral_fraction(self): npts = 1 + 2 * math.floor(degree / 2) # Number integration points numers = np.random.randint(-5, 5, degree + 1) denoms = np.random.randint(2, 8, degree + 1) - coefs = [Fraction(int(num), int(den)) for num, den in zip(numers, denoms)] + coefs = [ + Fraction(int(num), int(den)) + for num, den in zip(numers, denoms) + ] good = sum( ci * (b ** (i + 1) - a ** (i + 1)) / (i + 1) for i, ci in enumerate(coefs) @@ -690,7 +740,8 @@ def test_exact_integral_fraction(self): weights = IntegratorArray.open_newton_cotes(npts) nodes = tuple(a + (b - a) * node for node in nodes) funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + sum([cj * xi**j for j, cj in enumerate(coefs)]) + for xi in nodes ) test = (b - a) * np.inner(weights, funcvals) assert test == good @@ -748,7 +799,8 @@ def test_approx_integral(self): weights = IntegratorArray.open_newton_cotes(npts) nodes = tuple(a + (b - a) * node for node in nodes) funcvals = tuple( - sum([cj * xi**j for j, cj in enumerate(coefs)]) for xi in nodes + sum([cj * xi**j for j, cj in enumerate(coefs)]) + for xi in nodes ) test = (b - a) * np.inner(weights, funcvals) assert abs(test - good) < 1e-9 diff --git a/tests/core/test_knotvector.py b/tests/core/test_knotvector.py index 4d7fe1e..d2a08a8 100644 --- a/tests/core/test_knotvector.py +++ b/tests/core/test_knotvector.py @@ -128,7 +128,9 @@ def test_ValuesNumberPoints(): @pytest.mark.order(12) @pytest.mark.timeout(2) -@pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) +@pytest.mark.dependency( + depends=["test_ValuesDegree", "test_ValuesNumberPoints"] +) def test_findspans_single(): U = ImmutableKnotVector([0, 0, 0.2, 0.4, 0.5, 0.6, 0.8, 1, 1]) assert U.degree == 1 @@ -155,7 +157,9 @@ def test_findspans_single(): @pytest.mark.order(12) @pytest.mark.timeout(2) -@pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) +@pytest.mark.dependency( + depends=["test_ValuesDegree", "test_ValuesNumberPoints"] +) def test_findmult_single(): U = ImmutableKnotVector([0, 0, 0.2, 0.4, 0.5, 0.6, 0.8, 1, 1]) assert U.degree == 1 @@ -206,7 +210,9 @@ def test_findmult_array(): @pytest.mark.order(12) @pytest.mark.timeout(4) -@pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) +@pytest.mark.dependency( + depends=["test_ValuesDegree", "test_ValuesNumberPoints"] +) def test_CompareImmutableKnotVector(): U1 = ImmutableKnotVector([0, 0, 1, 1]) U2 = ImmutableKnotVector([0, 0, 1, 1]) diff --git a/tests/core/test_piecewise.py b/tests/core/test_piecewise.py index 89dd3a1..042d525 100644 --- a/tests/core/test_piecewise.py +++ b/tests/core/test_piecewise.py @@ -122,7 +122,9 @@ def test_neg(): @pytest.mark.order(13) -@pytest.mark.dependency(depends=["test_build", "test_evaluate", "test_add", "test_neg"]) +@pytest.mark.dependency( + depends=["test_build", "test_evaluate", "test_add", "test_neg"] +) def test_sub(): nsegs, degree = 6, 4 diff --git a/tests/core/test_polynomial.py b/tests/core/test_polynomial.py index 1aa257b..53a038b 100644 --- a/tests/core/test_polynomial.py +++ b/tests/core/test_polynomial.py @@ -1,7 +1,13 @@ import pytest from pynurbs.core.custom_math import Math -from pynurbs.core.polynomial import Polynomial, derivate, integrate, scale, shift +from pynurbs.core.polynomial import ( + Polynomial, + derivate, + integrate, + scale, + shift, +) @pytest.mark.order(12) @@ -207,7 +213,13 @@ def test_pow(): @pytest.mark.order(12) @pytest.mark.dependency( - depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] + depends=[ + "test_build", + "test_degree", + "test_evaluate", + "test_add", + "test_mul", + ] ) def test_derivate(): poly = Polynomial([0]) @@ -226,7 +238,13 @@ def test_derivate(): @pytest.mark.order(12) @pytest.mark.dependency( - depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] + depends=[ + "test_build", + "test_degree", + "test_evaluate", + "test_add", + "test_mul", + ] ) def test_integrate(): poly = Polynomial([1]) @@ -241,7 +259,13 @@ def test_integrate(): @pytest.mark.order(12) @pytest.mark.dependency( - depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] + depends=[ + "test_build", + "test_degree", + "test_evaluate", + "test_add", + "test_mul", + ] ) def test_shift(): """ @@ -266,7 +290,13 @@ def test_shift(): @pytest.mark.order(12) @pytest.mark.dependency( - depends=["test_build", "test_degree", "test_evaluate", "test_add", "test_mul"] + depends=[ + "test_build", + "test_degree", + "test_evaluate", + "test_add", + "test_mul", + ] ) def test_scale(): """ diff --git a/tests/core/test_spline_basis.py b/tests/core/test_spline_basis.py index cf60090..4d9c795 100644 --- a/tests/core/test_spline_basis.py +++ b/tests/core/test_spline_basis.py @@ -290,7 +290,9 @@ def test_tablevalues_degree1npts3(self): @pytest.mark.order(14) @pytest.mark.timeout(5) - @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree1npts3"]) + @pytest.mark.dependency( + depends=["TestSpline::test_tablevalues_degree1npts3"] + ) def test_tablevalues_degree2npts4(self): knotvector = [0, 0, 0, 0.5, 1, 1, 1] knotvector = ImmutableKnotVector(knotvector) @@ -318,7 +320,9 @@ def test_tablevalues_degree2npts4(self): @pytest.mark.order(14) @pytest.mark.timeout(5) - @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree2npts4"]) + @pytest.mark.dependency( + depends=["TestSpline::test_tablevalues_degree2npts4"] + ) def test_tablevalues_degree3npts5(self): knotvector = [0, 0, 0, 0, 0.5, 1, 1, 1, 1] knotvector = ImmutableKnotVector(knotvector) @@ -386,7 +390,11 @@ def test_piecewise_polynomial(self): (-92 + 62 * x - 8 * x * x) * one / 45, (49 - 14 * x + x * x) * one / 10, ], - [0, (4 - 4 * x + x * x) * one / 15, (-203 + 78 * x - 7 * x * x) * one / 20], + [ + 0, + (4 - 4 * x + x * x) * one / 15, + (-203 + 78 * x - 7 * x * x) * one / 20, + ], [0, 0, (25 - 10 * x + x * x) * one / 4], ] for i, functs in enumerate(functions): diff --git a/tests/curves/test_bezier.py b/tests/curves/test_bezier.py index 2e2bff3..999e27e 100644 --- a/tests/curves/test_bezier.py +++ b/tests/curves/test_bezier.py @@ -493,7 +493,9 @@ def test_increase_random(self): curve.degree += times assert curve.degree == (degree + times) np.testing.assert_allclose(curve.ctrlpoints[0], ctrlpoints[0]) - np.testing.assert_allclose(curve.ctrlpoints[-1], ctrlpoints[-1]) + np.testing.assert_allclose( + curve.ctrlpoints[-1], ctrlpoints[-1] + ) @pytest.mark.order(33) @pytest.mark.timeout(15) @@ -513,7 +515,9 @@ def test_decrease_random(self): curve.degree += times assert curve.degree == (degree + times) np.testing.assert_allclose(curve.ctrlpoints[0], ctrlpoints[0]) - np.testing.assert_allclose(curve.ctrlpoints[-1], ctrlpoints[-1]) + np.testing.assert_allclose( + curve.ctrlpoints[-1], ctrlpoints[-1] + ) @pytest.mark.order(33) @pytest.mark.timeout(10) @@ -559,7 +563,10 @@ def test_clean(self): @pytest.mark.order(33) @pytest.mark.timeout(10) @pytest.mark.dependency( - depends=["TestDegreeOperations::test_begin", "TestDegreeOperations::test_clean"] + depends=[ + "TestDegreeOperations::test_begin", + "TestDegreeOperations::test_clean", + ] ) def test_fails(self): U = KnotVector([0, 0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 1]) diff --git a/tests/curves/test_rational.py b/tests/curves/test_rational.py index 684795e..870cc22 100644 --- a/tests/curves/test_rational.py +++ b/tests/curves/test_rational.py @@ -94,7 +94,10 @@ def test_bezier_known(self): reason="Standard fraction fails due to lack of precision. sympy.Rational works" ) @pytest.mark.dependency( - depends=["TestAddSubMulDiv::test_begin", "TestAddSubMulDiv::test_bezier_known"] + depends=[ + "TestAddSubMulDiv::test_begin", + "TestAddSubMulDiv::test_bezier_known", + ] ) def test_random_bezier_fractions(self): maxdenom = 2 @@ -103,13 +106,25 @@ def test_random_bezier_fractions(self): npts = knotvector.npts curvea = Curve(knotvector) curveb = Curve(knotvector) - randnumbers = [np.random.randint(maxdenom + 1) for i in range(npts)] - curvea.ctrlpoints = [1 + frac(num, maxdenom) for num in randnumbers] - randnumbers = [np.random.randint(maxdenom + 1) for i in range(npts)] + randnumbers = [ + np.random.randint(maxdenom + 1) for i in range(npts) + ] + curvea.ctrlpoints = [ + 1 + frac(num, maxdenom) for num in randnumbers + ] + randnumbers = [ + np.random.randint(maxdenom + 1) for i in range(npts) + ] curvea.weights = [1 + frac(num, maxdenom) for num in randnumbers] - randnumbers = [np.random.randint(maxdenom + 1) for i in range(npts)] - curveb.ctrlpoints = [1 + frac(num, maxdenom) for num in randnumbers] - randnumbers = [np.random.randint(maxdenom + 1) for i in range(npts)] + randnumbers = [ + np.random.randint(maxdenom + 1) for i in range(npts) + ] + curveb.ctrlpoints = [ + 1 + frac(num, maxdenom) for num in randnumbers + ] + randnumbers = [ + np.random.randint(maxdenom + 1) for i in range(npts) + ] curveb.weights = [1 + frac(num, maxdenom) for num in randnumbers] aaddb = curvea + curveb @@ -121,7 +136,9 @@ def test_random_bezier_fractions(self): adivb = curvea / curveb bdiva = curveb / curvea - randnumbers = [np.random.randint(maxdenom + 1) for i in range(npts)] + randnumbers = [ + np.random.randint(maxdenom + 1) for i in range(npts) + ] usample = [frac(num, maxdenom) for num in range(maxdenom + 1)] avals = curvea(usample) bvals = curveb(usample) @@ -138,7 +155,10 @@ def test_random_bezier_fractions(self): @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.dependency( - depends=["TestAddSubMulDiv::test_begin", "TestAddSubMulDiv::test_bezier_known"] + depends=[ + "TestAddSubMulDiv::test_begin", + "TestAddSubMulDiv::test_bezier_known", + ] ) def test_random_bezier_float64(self): for degree in range(1, 4): @@ -176,7 +196,10 @@ def test_random_bezier_float64(self): @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.dependency( - depends=["TestAddSubMulDiv::test_begin", "TestAddSubMulDiv::test_bezier_known"] + depends=[ + "TestAddSubMulDiv::test_begin", + "TestAddSubMulDiv::test_bezier_known", + ] ) def test_others(self): knotvector = GeneratorKnotVector.bezier(3) @@ -203,7 +226,10 @@ def test_others(self): @pytest.mark.order(35) @pytest.mark.timeout(10) @pytest.mark.dependency( - depends=["TestAddSubMulDiv::test_begin", "TestAddSubMulDiv::test_bezier_known"] + depends=[ + "TestAddSubMulDiv::test_begin", + "TestAddSubMulDiv::test_bezier_known", + ] ) def test_zero_division(self): knotvector = GeneratorKnotVector.bezier(3) @@ -256,7 +282,9 @@ def test_quarter_circle_standard(self): @pytest.mark.order(35) @pytest.mark.timeout(1) - @pytest.mark.dependency(depends=["TestCircle::test_quarter_circle_standard"]) + @pytest.mark.dependency( + depends=["TestCircle::test_quarter_circle_standard"] + ) def test_quarter_circle_symmetric(self): knotvector = [0, 0, 0, 1, 1, 1] ctrlpoints = [(1, 0), (1, 1), (0, 1)] @@ -303,7 +331,15 @@ def test_half_circle(self): ) def test_full_circle(self): knotvector = [0, 0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 1] - ctrlpoints = [(1, 0), (1, 2), (-1, 2), (-1, 0), (-1, -2), (1, -2), (1, 0)] + ctrlpoints = [ + (1, 0), + (1, 2), + (-1, 2), + (-1, 0), + (-1, -2), + (1, -2), + (1, 0), + ] weights = [3, 1, 1, 3, 1, 1, 3] curve = Curve(knotvector) curve.ctrlpoints = np.array(ctrlpoints) @@ -412,7 +448,9 @@ def test_const_weights_fraction(self): knotvector = GeneratorKnotVector.uniform(degree, npts, frac) randnums = [np.random.randint(denmax + 1) for i in range(npts)] ctrlpoints = [frac(num, denmax) for num in randnums] - weights = [frac(np.random.randint(1, denmax + 1), denmax)] * npts + weights = [ + frac(np.random.randint(1, denmax + 1), denmax) + ] * npts oldcurve = Curve(knotvector) oldcurve.ctrlpoints = ctrlpoints oldcurve.weights = weights @@ -448,7 +486,9 @@ def test_random_weights_fraction(self): knotvector = GeneratorKnotVector.uniform(degree, npts, frac) randnums = [np.random.randint(denmax + 1) for i in range(npts)] ctrlpoints = [frac(num, denmax) for num in randnums] - randnums = [np.random.randint(1, denmax + 1) for i in range(npts)] + randnums = [ + np.random.randint(1, denmax + 1) for i in range(npts) + ] weights = [frac(num, denmax) for num in randnums] oldcurve = Curve(knotvector) oldcurve.ctrlpoints = ctrlpoints @@ -514,7 +554,9 @@ def test_quarter_circle_standard(self): @pytest.mark.order(35) @pytest.mark.timeout(1) - @pytest.mark.dependency(depends=["TestInsKnotCircle::test_quarter_circle_standard"]) + @pytest.mark.dependency( + depends=["TestInsKnotCircle::test_quarter_circle_standard"] + ) def test_quarter_circle_symmetric(self): knotvector = [0, 0, 0, 1, 1, 1] ctrlpoints = [(1, 0), (1, 1), (0, 1)] @@ -567,7 +609,15 @@ def test_half_circle(self): ) def test_full_circle(self): knotvector = [0, 0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 1] - ctrlpoints = [(1, 0), (1, 2), (-1, 2), (-1, 0), (-1, -2), (1, -2), (1, 0)] + ctrlpoints = [ + (1, 0), + (1, 2), + (-1, 2), + (-1, 0), + (-1, -2), + (1, -2), + (1, 0), + ] weights = [3, 1, 1, 3, 1, 1, 3] curve = Curve(knotvector) curve.ctrlpoints = np.array(ctrlpoints, dtype="float64") @@ -661,7 +711,11 @@ def test_end(self): @pytest.mark.order(35) @pytest.mark.dependency( - depends=["test_begin", "TestCircle::test_end", "TestInsKnotCircle::test_end"] + depends=[ + "test_begin", + "TestCircle::test_end", + "TestInsKnotCircle::test_end", + ] ) def test_end(): pass diff --git a/tests/curves/test_spline.py b/tests/curves/test_spline.py index 5b19721..e1e5bf4 100644 --- a/tests/curves/test_spline.py +++ b/tests/curves/test_spline.py @@ -335,7 +335,9 @@ def test_callvect_vectpts(self): lower = npts + degree + 2 upper = npts + degree + 9 nsample = np.random.randint(lower, upper) - tparam = np.linspace(knotvector[0], knotvector[-1], nsample) + tparam = np.linspace( + knotvector[0], knotvector[-1], nsample + ) curvevalues = curve(tparam) assert len(curvevalues) == nsample assert type(curvevalues[0]) == type(ctrlpoints[0]) @@ -884,7 +886,10 @@ def test_clean(self): @pytest.mark.order(34) @pytest.mark.timeout(10) @pytest.mark.dependency( - depends=["TestDegreeOperations::test_begin", "TestDegreeOperations::test_clean"] + depends=[ + "TestDegreeOperations::test_begin", + "TestDegreeOperations::test_clean", + ] ) def test_fails(self): U = KnotVector([0, 0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 1]) diff --git a/tests/operations/test_advanced.py b/tests/operations/test_advanced.py index 48ef1bd..a6d673e 100644 --- a/tests/operations/test_advanced.py +++ b/tests/operations/test_advanced.py @@ -71,7 +71,10 @@ def test_point_on_curve(self): @pytest.mark.order(42) @pytest.mark.dependency( - depends=["TestProjection::test_begin", "TestProjection::test_point_on_curve"] + depends=[ + "TestProjection::test_begin", + "TestProjection::test_point_on_curve", + ] ) def test_end(self): pass @@ -210,7 +213,11 @@ def test_end(self): @pytest.mark.order(42) @pytest.mark.dependency( - depends=["test_begin", "TestProjection::test_end", "TestIntersection::test_end"] + depends=[ + "test_begin", + "TestProjection::test_end", + "TestIntersection::test_end", + ] ) def test_end(): pass diff --git a/tests/operations/test_calculus.py b/tests/operations/test_calculus.py index 19f37e8..320ab44 100644 --- a/tests/operations/test_calculus.py +++ b/tests/operations/test_calculus.py @@ -131,12 +131,17 @@ def test_bezier(self): dcurve = Derivate(curve) for node in usample: - dnumer = (curve(node + deltau) - curve(node - deltau)) / (2 * deltau) + dnumer = (curve(node + deltau) - curve(node - deltau)) / ( + 2 * deltau + ) assert np.abs(dcurve(node) - dnumer) < 1e-6 @pytest.mark.order(41) @pytest.mark.dependency( - depends=["TestNumericalDeriv::test_begin", "TestNumericalDeriv::test_bezier"] + depends=[ + "TestNumericalDeriv::test_begin", + "TestNumericalDeriv::test_bezier", + ] ) def test_spline(self): deltau = 1e-6 @@ -151,11 +156,13 @@ def test_spline(self): dcurve = Derivate(curve) for start, end in zip(knots[:-1], knots[1:]): - usample = np.linspace(start + 2 * deltau, end - 2 * deltau, 5) + usample = np.linspace( + start + 2 * deltau, end - 2 * deltau, 5 + ) for node in usample: - dnumer = (curve(node + deltau) - curve(node - deltau)) / ( - 2 * deltau - ) + dnumer = ( + curve(node + deltau) - curve(node - deltau) + ) / (2 * deltau) assert np.abs(dcurve(node) - dnumer) < 1e-6 @pytest.mark.order(41) @@ -206,7 +213,9 @@ def test_rationalspline(self): dcurve = Derivate(curve) for start, end in zip(knots[:-1], knots[1:]): - usample = np.linspace(start + 2 * deltau, end - 2 * deltau, 5) + usample = np.linspace( + start + 2 * deltau, end - 2 * deltau, 5 + ) for node in usample: dnumer = curve(node + deltau) - curve(node - deltau) dnumer /= 2 * deltau @@ -311,7 +320,8 @@ def test_lenght_integral(self): test = Integrate.lenght(curve) good = sum( - np.linalg.norm(points[i + 1] - points[i]) for i in range(curve.npts - 1) + np.linalg.norm(points[i + 1] - points[i]) + for i in range(curve.npts - 1) ) assert abs(test - good) < 1e-9 @@ -345,7 +355,12 @@ def test_winding_number(self): knotvector = GeneratorKnotVector.uniform(1, 5, Fraction) curvex = Curve(knotvector, [1, 0, -1, 0, 1]) curvey = Curve(knotvector, [0, 1, 0, -1, 0]) - new_knots = [Fraction(1, 8), Fraction(3, 8), Fraction(5, 8), Fraction(7, 8)] + new_knots = [ + Fraction(1, 8), + Fraction(3, 8), + Fraction(5, 8), + Fraction(7, 8), + ] curvex.knot_insert(new_knots) curvey.knot_insert(new_knots) knotvector += new_knots @@ -356,7 +371,9 @@ def test_winding_number(self): denom = lambda u: curvex(u) ** 2 + curvey(u) ** 2 function = lambda u: numer(u) / denom(u) - test = Integrate.function(knotvector, function, "closed-newton-cotes", 6) + test = Integrate.function( + knotvector, function, "closed-newton-cotes", 6 + ) assert abs(test - 2 * np.pi) < 1e-3 test = Integrate.function(knotvector, function, "open-newton-cotes", 6) assert abs(test - 2 * np.pi) < 1e-3 diff --git a/tests/operations/test_customstruc.py b/tests/operations/test_customstruc.py index cfb5251..49f95f7 100644 --- a/tests/operations/test_customstruc.py +++ b/tests/operations/test_customstruc.py @@ -93,7 +93,9 @@ def __ge__(self, other): return self.__eq__(other) or self.__gt__(other) def __abs__(self): - return self.__class__(self.internal if self.internal > 0 else -self.internal) + return self.__class__( + self.internal if self.internal > 0 else -self.internal + ) class CustomPoint: @@ -197,7 +199,10 @@ def test_creation(self): @pytest.mark.order(41) @pytest.mark.dependency( - depends=["TestBasisFunctions::test_begin", "TestBasisFunctions::test_creation"] + depends=[ + "TestBasisFunctions::test_begin", + "TestBasisFunctions::test_creation", + ] ) def test_end(self): pass @@ -205,7 +210,11 @@ def test_end(self): @pytest.mark.order(41) @pytest.mark.dependency( - depends=["test_begin", "TestKnotVector::test_end", "TestBasisFunctions::test_end"] + depends=[ + "test_begin", + "TestKnotVector::test_end", + "TestBasisFunctions::test_end", + ] ) def test_end(): pass diff --git a/tests/operations/test_fitting.py b/tests/operations/test_fitting.py index 5100e0d..78aa141 100644 --- a/tests/operations/test_fitting.py +++ b/tests/operations/test_fitting.py @@ -115,7 +115,9 @@ def test_overdefined_spline(self): usample = np.linspace(0, 1, 33) for degree_base in range(0, 4): coefs = np.random.uniform(-1, 1, 1 + degree_base) - function = lambda u: sum([cof * u**i for i, cof in enumerate(coefs)]) + function = lambda u: sum( + [cof * u**i for i, cof in enumerate(coefs)] + ) for degree in range(degree_base, 7): vector = GeneratorKnotVector.bezier(degree) test_curve = Curve(vector) @@ -135,7 +137,9 @@ def test_overdefined_rational(self): usample = np.linspace(0, 1, 33) for degree_base in range(0, 4): coefs = np.random.uniform(-1, 1, 1 + degree_base) - function = lambda u: sum([cof * u**i for i, cof in enumerate(coefs)]) + function = lambda u: sum( + [cof * u**i for i, cof in enumerate(coefs)] + ) for degree in range(degree_base, 7): vector = GeneratorKnotVector.bezier(degree) test_curve = Curve(vector) diff --git a/tests/test_basis_functions.py b/tests/test_basis_functions.py index fb8b463..71a27f5 100644 --- a/tests/test_basis_functions.py +++ b/tests/test_basis_functions.py @@ -301,13 +301,19 @@ def test_tablevalues_random_degree(self): matrix_good = np.zeros((len(nodestest), degree + 1)) for i, node in enumerate(nodestest): for j in range(degree + 1): - value = Math.binom(degree, j) * (1 - node) ** (degree - j) * node**j + value = ( + Math.binom(degree, j) + * (1 - node) ** (degree - j) + * node**j + ) matrix_good[i, j] = value np.testing.assert_allclose(matrix_test, matrix_good) @pytest.mark.order(32) @pytest.mark.timeout(5) - @pytest.mark.dependency(depends=["TestBezier::test_tablevalues_random_degree"]) + @pytest.mark.dependency( + depends=["TestBezier::test_tablevalues_random_degree"] + ) def test_shifted_scaled_bezier(self): for degree in range(1, 6): knotvector = GeneratorKnotVector.bezier(degree) @@ -325,7 +331,11 @@ def test_shifted_scaled_bezier(self): matrix_good = np.zeros((len(nodestest), degree + 1)) for i, node in enumerate(nodesgood): for j in range(degree + 1): - value = Math.binom(degree, j) * (1 - node) ** (degree - j) * node**j + value = ( + Math.binom(degree, j) + * (1 - node) ** (degree - j) + * node**j + ) matrix_good[i, j] = value np.testing.assert_allclose(matrix_test, matrix_good) @@ -426,7 +436,9 @@ def test_evalfuncs_degree1npts3(self): @pytest.mark.order(32) @pytest.mark.timeout(5) - @pytest.mark.dependency(depends=["TestSpline::test_evalfuncs_degree1npts3"]) + @pytest.mark.dependency( + depends=["TestSpline::test_evalfuncs_degree1npts3"] + ) def test_tablevalues_degree1npts3(self): spline = BasisFunctions([0, 0, 0.5, 1, 1]) assert spline.degree == 1 @@ -467,7 +479,9 @@ def test_tablevalues_degree1npts3(self): @pytest.mark.order(32) @pytest.mark.timeout(5) - @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree1npts3"]) + @pytest.mark.dependency( + depends=["TestSpline::test_tablevalues_degree1npts3"] + ) def test_tablevalues_degree2npts4(self): spline = BasisFunctions([0, 0, 0, 0.5, 1, 1, 1]) assert spline.degree == 2 @@ -524,7 +538,9 @@ def test_tablevalues_degree2npts4(self): @pytest.mark.order(32) @pytest.mark.timeout(5) - @pytest.mark.dependency(depends=["TestSpline::test_tablevalues_degree2npts4"]) + @pytest.mark.dependency( + depends=["TestSpline::test_tablevalues_degree2npts4"] + ) def test_tablevalues_degree3npts5(self): knotvector = [0, 0, 0, 0, 0.5, 1, 1, 1, 1] spline = BasisFunctions(knotvector) @@ -739,7 +755,9 @@ def test_quarter_circle_standard(self): @pytest.mark.order(32) @pytest.mark.timeout(1) - @pytest.mark.dependency(depends=["TestRational::test_quarter_circle_standard"]) + @pytest.mark.dependency( + depends=["TestRational::test_quarter_circle_standard"] + ) def test_quarter_circle_symmetric(self): knotvector = [0, 0, 0, 1, 1, 1] rational = BasisFunctions(knotvector) diff --git a/tests/test_knotspace.py b/tests/test_knotspace.py index e50c0b3..f280802 100644 --- a/tests/test_knotspace.py +++ b/tests/test_knotspace.py @@ -133,7 +133,9 @@ def test_ValuesNumberPoints(): @pytest.mark.order(30) @pytest.mark.timeout(2) -@pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) +@pytest.mark.dependency( + depends=["test_ValuesDegree", "test_ValuesNumberPoints"] +) def test_findspans_single(): U = KnotVector([0, 0, 0.2, 0.4, 0.5, 0.6, 0.8, 1, 1]) assert U.degree == 1 @@ -160,7 +162,9 @@ def test_findspans_single(): @pytest.mark.order(30) @pytest.mark.timeout(2) -@pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) +@pytest.mark.dependency( + depends=["test_ValuesDegree", "test_ValuesNumberPoints"] +) def test_findmult_single(): U = KnotVector([0, 0, 0.2, 0.4, 0.5, 0.6, 0.8, 1, 1]) assert U.degree == 1 @@ -213,7 +217,9 @@ def test_findmult_array(): @pytest.mark.order(30) @pytest.mark.timeout(4) -@pytest.mark.dependency(depends=["test_ValuesDegree", "test_ValuesNumberPoints"]) +@pytest.mark.dependency( + depends=["test_ValuesDegree", "test_ValuesNumberPoints"] +) def test_CompareKnotvector(): U1 = KnotVector([0, 0, 1, 1]) U2 = KnotVector([0, 0, 1, 1]) @@ -895,7 +901,14 @@ def test_others(): def test_fractions(): from fractions import Fraction as frac - knotvect = [frac(0), frac(1, 5), frac(2, 5), frac(3, 5), frac(4, 5), frac(1)] + knotvect = [ + frac(0), + frac(1, 5), + frac(2, 5), + frac(3, 5), + frac(4, 5), + frac(1), + ] knotvect = KnotVector(knotvect) assert knotvect.degree == 0 assert knotvect.npts == 5 From f669b0557bca17c6c2b3bb0b9f38306d8207c44f Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 22:09:04 +0200 Subject: [PATCH 106/116] style: fix format with flake8 --- src/pynurbs/operations/calculus.py | 10 ++++++++-- src/pynurbs/operations/heavy.py | 4 ++-- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/src/pynurbs/operations/calculus.py b/src/pynurbs/operations/calculus.py index c3fb8e9..b4e592b 100644 --- a/src/pynurbs/operations/calculus.py +++ b/src/pynurbs/operations/calculus.py @@ -156,7 +156,10 @@ def scalar( } assert isinstance(curve, Curve) if function is None: - function = lambda u: 1 + + def function(u): + return 1 + if method is not None: pass elif isinstance(curve.knotvector[0], (int, Fraction)): @@ -260,7 +263,10 @@ def density( } assert isinstance(curve, Curve) if function is None: - function = lambda u: 1 + + def function(u): + return 1 + if method is not None: pass elif isinstance(curve.knotvector[0], (int, Fraction)): diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index fc049c0..a020672 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -10,10 +10,10 @@ import numpy as np -from ..core.custom_math import Linalg, NodeSample, isscalar, totuple +from ..core.custom_math import Linalg, NodeSample, totuple from ..core.knotvector import ImmutableKnotVector from ..operations import knotvector as opekv -from .least_square import eval_spline_nodes, spline2spline +from .least_square import eval_spline_nodes class Operations: From 78b1d871841141148eb60aec9907e9f6606bb653 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 22:12:51 +0200 Subject: [PATCH 107/116] style: flake8 improve format to remove 'Matrix2D' type --- src/pynurbs/operations/heavy.py | 16 ++++++++-------- src/pynurbs/operations/least_square.py | 2 +- 2 files changed, 9 insertions(+), 9 deletions(-) diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index a020672..0845896 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -74,7 +74,7 @@ def split_curve(knotvector: ImmutableKnotVector, nodes: Tuple[float]): def one_knot_insert_once( knotvector: ImmutableKnotVector, node: float - ) -> "Matrix2D": + ) -> Tuple[Tuple[float, ...], ...]: """ Given the knotvector and a node to be inserted, this function returns a matrix of transformation T of control points @@ -111,7 +111,7 @@ def one_knot_insert_once( def one_knot_insert( knotvector: ImmutableKnotVector, node: float, times: int - ) -> "Matrix2D": + ) -> Tuple[Tuple[float, ...], ...]: """ Given the knotvector and a node to be inserted, this function returns a matrix of transformation T of control points @@ -141,7 +141,7 @@ def one_knot_insert( def knot_insert( knotvector: ImmutableKnotVector, nodes: Tuple[float] - ) -> "Matrix2D": + ) -> Tuple[Tuple[float, ...], ...]: """ Given the knotvector and a node to be inserted, this function returns a matrix of transformation T of control points @@ -183,7 +183,7 @@ def knot_insert( def degree_increase_bezier_once( knotvector: ImmutableKnotVector, - ) -> "Matrix2D": + ) -> Tuple[Tuple[float, ...], ...]: knotvector = ImmutableKnotVector(knotvector) one = knotvector[-1] - knotvector[0] one /= one @@ -199,7 +199,7 @@ def degree_increase_bezier_once( def degree_increase_bezier( knotvector: ImmutableKnotVector, times: int - ) -> "Matrix2D": + ) -> Tuple[Tuple[float, ...], ...]: """ Given a bezier curve A(u) of degree p, we want a new bezier curve B(u) of degree (p+t) such B(u) = A(u) for every u @@ -224,7 +224,7 @@ def degree_increase_bezier( def degree_increase( knotvector: ImmutableKnotVector, times: int - ) -> "Matrix2D": + ) -> Tuple[Tuple[float, ...], ...]: """ Given a curve A(u) associated with control points P, we want to do a degree elevation @@ -350,7 +350,7 @@ def knotvector_mul( @staticmethod def add_spline_curve( knotvectora: Tuple[float], knotvectorb: Tuple[float] - ) -> Tuple["Matrix2D"]: + ) -> Tuple[Tuple[Tuple[float, ...], ...]]: """ Given two spline curves, A(u) and B(u), such A(u) = sum_{i=0}^{n} N_i(u) * P_i @@ -386,7 +386,7 @@ def add_spline_curve( @staticmethod def mul_spline_curve( knotvectora: Tuple[float], knotvectorb: Tuple[float] - ) -> Tuple["Matrix3D"]: + ) -> Tuple[Tuple[Tuple[float, ...], ...], ...]: """ Given two spline curves, called A(u) and B(u), it computes and returns a new curve C(u) such C(u) = A(u) * B(u) for every u diff --git a/src/pynurbs/operations/least_square.py b/src/pynurbs/operations/least_square.py index 5ed3dc1..703925c 100644 --- a/src/pynurbs/operations/least_square.py +++ b/src/pynurbs/operations/least_square.py @@ -95,7 +95,7 @@ def spline2spline( oldknotvector: ImmutableKnotVector, newknotvector: ImmutableKnotVector, fit_nodes: Tuple[float] = None, -) -> Tuple["Matrix2D"]: +) -> Tuple[Tuple[float, ...], ...]: """ Given two bspline curves A(u) and B(u), this function returns a matrix [M] such From b68f06fe36a01a5005165a89566c06c99ca44de4 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 22:15:21 +0200 Subject: [PATCH 108/116] style: improve format with flake8 --- src/pynurbs/curves/base.py | 4 ++-- src/pynurbs/curves/curves.py | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py index b6c78e9..7e49b7a 100644 --- a/src/pynurbs/curves/base.py +++ b/src/pynurbs/curves/base.py @@ -479,10 +479,10 @@ def ctrlpoints(self, newpoints: np.ndarray): self.__ctrlpoints = tuple(newpoints) - def __copy__(self) -> Curve: + def __copy__(self) -> BaseCurve: return self.__deepcopy__(None) - def __deepcopy__(self, memo) -> Curve: + def __deepcopy__(self, memo) -> BaseCurve: knotvector = copy(self.knotvector) curve = self.__class__(knotvector) if self.ctrlpoints is not None: diff --git a/src/pynurbs/curves/curves.py b/src/pynurbs/curves/curves.py index 6122572..b0cc25b 100644 --- a/src/pynurbs/curves/curves.py +++ b/src/pynurbs/curves/curves.py @@ -341,7 +341,7 @@ def clean(self, tolerance: float = 1e-9): assert NotImplementedError # Needs correction self.clean(tolerance) - def split(self, nodes: Optional[Tuple[float]] = None) -> Tuple[Curve]: + def split(self, nodes: Optional[Tuple[float]] = None) -> Tuple[Curve, ...]: """Separate the current curve at specified nodes If no arguments are given, it splits at every knot, returning a @@ -377,7 +377,7 @@ def split(self, nodes: Optional[Tuple[float]] = None) -> Tuple[Curve]: newcurves = [] for newvector, matrix in zip(newvectors, matrices): matrix = np.array(matrix) - newcurve = Curve(newvector) + newcurve = self.__class__(newvector) newcurve.ctrlpoints = np.dot(matrix, self.ctrlpoints) if self.weights is not None: newcurve.weights = np.dot(matrix, self.weights) From a0b0a83110dccdd9f246cdc8e2f672e61d9e60cc Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 22:18:42 +0200 Subject: [PATCH 109/116] style: flake8 and pylint ignore some errors to fix later --- .pylintrc | 11 +++++++++++ tox.ini | 1 + 2 files changed, 12 insertions(+) create mode 100644 .pylintrc diff --git a/.pylintrc b/.pylintrc new file mode 100644 index 0000000..9e51259 --- /dev/null +++ b/.pylintrc @@ -0,0 +1,11 @@ +[MAIN] + +disable= + too-many-lines, + duplicate-code, + missing-module-docstring, + missing-class-docstring, + missing-function-docstring, + invalid-name, + too-many-locals, + too-many-statements, diff --git a/tox.ini b/tox.ini index 4f3613a..e14e707 100644 --- a/tox.ini +++ b/tox.ini @@ -36,3 +36,4 @@ commands = [flake8] per-file-ignores = __init__.py:F401 +extend-ignore=E203,E501 From 24bf529934901d8feef1fbe54f4a73810aba09b7 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Sun, 6 Jul 2025 22:25:40 +0200 Subject: [PATCH 110/116] fix: add missing static method decorator --- src/pynurbs/core/custom_math.py | 1 + src/pynurbs/operations/calculus.py | 1 + src/pynurbs/operations/heavy.py | 8 ++++++++ 3 files changed, 10 insertions(+) diff --git a/src/pynurbs/core/custom_math.py b/src/pynurbs/core/custom_math.py index f8d2769..224415f 100644 --- a/src/pynurbs/core/custom_math.py +++ b/src/pynurbs/core/custom_math.py @@ -486,6 +486,7 @@ def lstsq(matrix: Tuple[Tuple[float]]): return Linalg.solve(matrix, ident) return Linalg.solve(matrix.T @ matrix, matrix.T) + @staticmethod def invert_integer_matrix( matrix: Tuple[Tuple[int]], ) -> Tuple[Tuple[int], Tuple[Tuple[int]]]: diff --git a/src/pynurbs/operations/calculus.py b/src/pynurbs/operations/calculus.py index b4e592b..8babdbc 100644 --- a/src/pynurbs/operations/calculus.py +++ b/src/pynurbs/operations/calculus.py @@ -83,6 +83,7 @@ def rational_bezier(curve: Curve) -> Curve: finalcurve.weights = dennumctrlpts return finalcurve + @staticmethod def nonrational_spline(curve: Curve) -> Curve: assert isinstance(curve, Curve) assert curve.weights is None diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index 0845896..fd490dc 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -25,6 +25,7 @@ class Operations: * degree decrease """ + @staticmethod def split_curve(knotvector: ImmutableKnotVector, nodes: Tuple[float]): """ Breaks curves in the nodes @@ -72,6 +73,7 @@ def split_curve(knotvector: ImmutableKnotVector, nodes: Tuple[float]): matrices.append(newmatrix) return matrices + @staticmethod def one_knot_insert_once( knotvector: ImmutableKnotVector, node: float ) -> Tuple[Tuple[float, ...], ...]: @@ -109,6 +111,7 @@ def one_knot_insert_once( matrix[i, i - 1] = 1 - alpha return totuple(matrix) + @staticmethod def one_knot_insert( knotvector: ImmutableKnotVector, node: float, times: int ) -> Tuple[Tuple[float, ...], ...]: @@ -139,6 +142,7 @@ def one_knot_insert( knotvector = opekv.insert_knots(knotvector, [node]) return totuple(matrix) + @staticmethod def knot_insert( knotvector: ImmutableKnotVector, nodes: Tuple[float] ) -> Tuple[Tuple[float, ...], ...]: @@ -181,6 +185,7 @@ def knot_insert( knotvector = opekv.insert_knots(knotvector, times * [node]) return totuple(matrix) + @staticmethod def degree_increase_bezier_once( knotvector: ImmutableKnotVector, ) -> Tuple[Tuple[float, ...], ...]: @@ -197,6 +202,7 @@ def degree_increase_bezier_once( matrix[degree + 1, degree] = one return totuple(matrix) + @staticmethod def degree_increase_bezier( knotvector: ImmutableKnotVector, times: int ) -> Tuple[Tuple[float, ...], ...]: @@ -222,6 +228,7 @@ def degree_increase_bezier( knotvector = opekv.increase_degree(knotvector, 1) return totuple(matrix) + @staticmethod def degree_increase( knotvector: ImmutableKnotVector, times: int ) -> Tuple[Tuple[float, ...], ...]: @@ -269,6 +276,7 @@ def degree_increase( finalmatrix = removematrix @ bigmatrix return totuple(finalmatrix) + @staticmethod def matrix_transformation( knotvectora: ImmutableKnotVector, knotvectorb: ImmutableKnotVector ): From b1001400f8a66e9f8bdc0b9608ae732c93e23c7d Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Mon, 7 Jul 2025 19:41:00 +0200 Subject: [PATCH 111/116] style: general improvements on code formatting --- src/pynurbs/basis_functions.py | 6 +++--- src/pynurbs/core/polynomial.py | 7 +------ src/pynurbs/curves/base.py | 7 +++---- src/pynurbs/operations/calculus.py | 5 ++--- src/pynurbs/operations/heavy.py | 10 +++++----- src/pynurbs/operations/knotvector.py | 4 ++-- src/pynurbs/operations/roots.py | 2 +- 7 files changed, 17 insertions(+), 24 deletions(-) diff --git a/src/pynurbs/basis_functions.py b/src/pynurbs/basis_functions.py index 0ad84ab..fa3aa2f 100644 --- a/src/pynurbs/basis_functions.py +++ b/src/pynurbs/basis_functions.py @@ -200,13 +200,13 @@ def __valid_first_index(self, index: Union[int, slice]): raise TypeError if isinstance(index, int): npts = self.npts - if not (-npts <= index < npts): + if not -npts <= index < npts: raise IndexError def __valid_second_index(self, index: int): if not isinstance(index, int): raise TypeError - if not (0 <= index <= self.degree): + if not 0 <= index <= self.degree: error_msg = f"Second index (={index}) " error_msg += f"must be in [0, {self.degree}]" raise IndexError(error_msg) @@ -252,7 +252,7 @@ def __repr__(self) -> str: """Official printing""" if self.npts == self.degree + 1: return f"Bezier function of degree {self.degree}" - elif self.weights is None: + if self.weights is None: msg = "Spline" else: msg = "Rational" diff --git a/src/pynurbs/core/polynomial.py b/src/pynurbs/core/polynomial.py index 65fdc64..b8aabd4 100644 --- a/src/pynurbs/core/polynomial.py +++ b/src/pynurbs/core/polynomial.py @@ -149,12 +149,7 @@ def __str__(self): for i, coef in enumerate(self): if coef == 0: continue - if coef < 0: - msg = "- " - elif flag: - msg = "+ " - else: - msg = "" + msg = "- " if coef < 0 else "+ " if flag else "" flag = True coef = abs(coef) if coef != 1 or i == 0: diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py index 7e49b7a..60c4c9c 100644 --- a/src/pynurbs/curves/base.py +++ b/src/pynurbs/curves/base.py @@ -16,7 +16,7 @@ from ..operations.tools import vectorize -def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: +def norm(obj: Union[float, Tuple[float]], L: int = 0) -> float: """ Computes recursively a norm of an object. If L = 0, it means infinity norm @@ -25,12 +25,12 @@ def norm(object: Union[float, Tuple[float]], L: int = 0) -> float: """ try: soma = 0 - for item in object: + for item in obj: norma = norm(item, L) soma = max(soma, norma) if L == 0 else soma + norma**L return soma if L == 0 else soma ** (1 / L) except TypeError: - return abs(object) + return abs(obj) class BaseCurve: @@ -46,7 +46,6 @@ def __init__( self.__ctrlpoints = ctrlpoints self.__weights = weights self.tolerance = 1e-9 - self.__denominator = None @vectorize(1, 0) def __call__(self, node: Real) -> Any: diff --git a/src/pynurbs/operations/calculus.py b/src/pynurbs/operations/calculus.py index 8babdbc..a1f014c 100644 --- a/src/pynurbs/operations/calculus.py +++ b/src/pynurbs/operations/calculus.py @@ -43,7 +43,6 @@ def spline(curve: Curve) -> Curve: @staticmethod def nonrational_bezier(curve: Curve) -> Curve: - """ """ assert curve.degree + 1 == curve.npts assert curve.weights is None vector = tuple(curve.knotvector) @@ -158,7 +157,7 @@ def scalar( assert isinstance(curve, Curve) if function is None: - def function(u): + def function(_): return 1 if method is not None: @@ -265,7 +264,7 @@ def density( assert isinstance(curve, Curve) if function is None: - def function(u): + def function(_): return 1 if method is not None: diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index fd490dc..4aad71a 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -89,7 +89,7 @@ def one_knot_insert_once( [Q] = [T] @ [P] """ knotvector = ImmutableKnotVector(knotvector) - if not (knotvector.knots[0] <= node <= knotvector.knots[-1]): + if not knotvector.knots[0] <= node <= knotvector.knots[-1]: raise ValueError( f"Invalid nodes {node} in knotvector {knotvector}" ) @@ -127,7 +127,7 @@ def one_knot_insert( [Q] = [T] @ [P] """ knotvector = ImmutableKnotVector(knotvector) - if not (knotvector.knots[0] <= node <= knotvector.knots[-1]): + if not knotvector.knots[0] <= node <= knotvector.knots[-1]: raise ValueError(f"Invalid node {node} in knotvector {knotvector}") if not isinstance(times, int): msg = f"Times must be an int, not {times}" @@ -222,7 +222,7 @@ def degree_increase_bezier( raise ValueError(f"Times must be positive! Received {times}") degree = knotvector.degree matrix = np.eye(degree + 1, dtype="object") - for i in range(times): + for _ in range(times): elevateonce = Operations.degree_increase_bezier_once(knotvector) matrix = elevateonce @ matrix knotvector = opekv.increase_degree(knotvector, 1) @@ -242,7 +242,7 @@ def degree_increase( raise TypeError(msg) if times == 0: return totuple(np.eye(knotvector.npts, dtype="object")) - elif times < 0: + if times < 0: raise ValueError(f"Times must be >= 0! Received {times}") degree = knotvector.degree npts = knotvector.npts @@ -491,7 +491,7 @@ def derivate_nonrational_bezier( assert degree > 0 matrix = np.zeros((degree, degree + 1), dtype="object") for i in range(degree): - matrix[i, i] = -degree + matrix[i, i] = -1 * degree matrix[i, i + 1] = degree matrix /= knotvector[-1] - knotvector[0] if reduce: diff --git a/src/pynurbs/operations/knotvector.py b/src/pynurbs/operations/knotvector.py index 1b44821..fb31f53 100644 --- a/src/pynurbs/operations/knotvector.py +++ b/src/pynurbs/operations/knotvector.py @@ -123,7 +123,7 @@ def split_knotvector( degree = knotvector.degree nodes = sorted(set(nodes) | {knotvector.knots[0], knotvector.knots[-1]}) for a, b in zip(nodes[:-1], nodes[1:]): - middle = list(knot for knot in knotvector if (a < knot < b)) + middle = list(knot for knot in knotvector if a < knot < b) newknotvect = (degree + 1) * [a] + middle + (degree + 1) * [b] yield ImmutableKnotVector(newknotvect) @@ -189,7 +189,7 @@ def intersect_knotvectors( knotvector = decrease_degree( knotvector, knotvector.degree - mindeg ) - for knot in internals.keys(): + for knot in internals: internals[knot] = min(internals[knot], knotvector.mult(knot)) final = [left] * (mindeg + 1) for knot in sorted(internals.keys()): diff --git a/src/pynurbs/operations/roots.py b/src/pynurbs/operations/roots.py index c72aa36..1e762a6 100644 --- a/src/pynurbs/operations/roots.py +++ b/src/pynurbs/operations/roots.py @@ -85,6 +85,6 @@ def roots_piecewise(piece: PiecewisePolynomial) -> rbool.SubSetR1: def roots(function: Union[Polynomial, PiecewisePolynomial]) -> rbool.SubSetR1: if isinstance(function, Polynomial): return roots_polynomial(function) - elif isinstance(function, PiecewisePolynomial): + if isinstance(function, PiecewisePolynomial): return roots_piecewise(function) raise ValueError From 3054e020a7851553a9d78420bf3bfa10c3d4d251 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Mon, 7 Jul 2025 20:00:03 +0200 Subject: [PATCH 112/116] feat: add function to set temporary tolerance --- src/pynurbs/curves/base.py | 14 ++++++++++++ src/pynurbs/curves/curves.py | 41 +++++++++++++++--------------------- 2 files changed, 31 insertions(+), 24 deletions(-) diff --git a/src/pynurbs/curves/base.py b/src/pynurbs/curves/base.py index 60c4c9c..fbc1a84 100644 --- a/src/pynurbs/curves/base.py +++ b/src/pynurbs/curves/base.py @@ -1,5 +1,6 @@ from __future__ import annotations +from contextlib import contextmanager from copy import copy from numbers import Real from typing import Any, Iterable, Tuple, Union @@ -265,6 +266,19 @@ def __or__(self, other: object): newcurve.knot_clean([umaxleft]) return newcurve + @contextmanager + def temporary(self, **kwargs): + oldvalues = {} + for key, value in kwargs.items(): + if hasattr(self, key): + oldvalues[key] = getattr(self, key) + setattr(self, key, value) + try: + yield + finally: + for key, value in oldvalues.items(): + setattr(self, key, value) + @property def tolerance(self) -> Union[None, float]: return self.__tolerance diff --git a/src/pynurbs/curves/curves.py b/src/pynurbs/curves/curves.py index b0cc25b..ac18ec4 100644 --- a/src/pynurbs/curves/curves.py +++ b/src/pynurbs/curves/curves.py @@ -130,10 +130,8 @@ def knot_remove( ControlPoints = [1.0, 2.0, -3.0] """ - old_tolerance = self.tolerance - self.tolerance = tolerance - self.knotvector = remove_knots(self.knotvector.internal, nodes) - self.tolerance = old_tolerance + with self.temporary(tolerance=tolerance): + self.knotvector = remove_knots(self.knotvector.internal, nodes) def knot_clean( self, @@ -180,15 +178,13 @@ def knot_clean( if nodes is None: nodes = self.knotvector.knots nodes = tuple(set(nodes) - set(self.knotvector.limits)) - oldtolerance = self.tolerance - self.tolerance = tolerance - for knot in nodes: - try: - while True: - self.knot_remove([knot]) - except ValueError: - pass - self.tolerance = oldtolerance + with self.temporary(tolerance=tolerance): + for knot in nodes: + try: + while True: + self.knot_remove([knot]) + except ValueError: + pass def degree_increase(self, times: Optional[int] = 1): """Increase the degree of the curve by an amount ``times`` @@ -251,10 +247,8 @@ def degree_decrease( if not isscalar(tolerance) or tolerance <= 0: raise ValueError("Tolerance must be None or positive value") if times > 0: - old_tolerance = self.tolerance - self.tolerance = tolerance - self.degree -= int(times) - self.tolerance = old_tolerance + with self.temporary(tolerance=tolerance): + self.degree -= int(times) def degree_clean(self, tolerance: float = 1e-9): """Reduces au maximum the degree of the curve for given tolerance. @@ -284,13 +278,12 @@ def degree_clean(self, tolerance: float = 1e-9): """ if not isscalar(tolerance) or tolerance <= 0: raise ValueError("Given tolerance must be positive") - oldtolerance = self.tolerance - try: - self.tolerance = tolerance - while True: - self.degree -= 1 - except ValueError: - self.tolerance = oldtolerance + with self.temporary(tolerance=tolerance): + try: + while True: + self.degree -= 1 + except ValueError: + pass def clean(self, tolerance: float = 1e-9): """Calls degree_clean and knot_clean From 03b2e8de8cb28ffed649e012c96bcd5779a19033 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 8 Jul 2025 20:33:47 +0200 Subject: [PATCH 113/116] refactor: move least_square to curves submodule --- src/pynurbs/curves/curves.py | 2 +- src/pynurbs/{operations => curves}/least_square.py | 0 src/pynurbs/operations/heavy.py | 2 +- tests/operations/test_least_square.py | 2 +- 4 files changed, 3 insertions(+), 3 deletions(-) rename src/pynurbs/{operations => curves}/least_square.py (100%) diff --git a/src/pynurbs/curves/curves.py b/src/pynurbs/curves/curves.py index ac18ec4..2dfbf46 100644 --- a/src/pynurbs/curves/curves.py +++ b/src/pynurbs/curves/curves.py @@ -10,9 +10,9 @@ from ..knotspace import KnotVector from ..operations import heavy from ..operations.knotvector import insert_knots, remove_knots -from ..operations.least_square import fit_function, func2func, spline2spline from ..operations.tools import vectorize from .base import BaseCurve +from .least_square import fit_function, func2func, spline2spline class Curve(BaseCurve): diff --git a/src/pynurbs/operations/least_square.py b/src/pynurbs/curves/least_square.py similarity index 100% rename from src/pynurbs/operations/least_square.py rename to src/pynurbs/curves/least_square.py diff --git a/src/pynurbs/operations/heavy.py b/src/pynurbs/operations/heavy.py index 4aad71a..66072fc 100644 --- a/src/pynurbs/operations/heavy.py +++ b/src/pynurbs/operations/heavy.py @@ -12,8 +12,8 @@ from ..core.custom_math import Linalg, NodeSample, totuple from ..core.knotvector import ImmutableKnotVector +from ..curves.least_square import eval_spline_nodes from ..operations import knotvector as opekv -from .least_square import eval_spline_nodes class Operations: diff --git a/tests/operations/test_least_square.py b/tests/operations/test_least_square.py index 5387738..2e8cd17 100644 --- a/tests/operations/test_least_square.py +++ b/tests/operations/test_least_square.py @@ -1,7 +1,7 @@ import numpy as np import pytest -from pynurbs.operations.least_square import spline2spline +from pynurbs.curves.least_square import spline2spline @pytest.mark.order(21) From 8350b1f6dff2c9ddbc9bdc95746f6efba4ccb492 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 8 Jul 2025 21:02:33 +0200 Subject: [PATCH 114/116] refactor: move projection and intersection to curves submodule --- src/pynurbs/__init__.py | 1 - src/pynurbs/curves/intersection.py | 223 ++++++++++++++++++++ src/pynurbs/curves/projection.py | 95 +++++++++ src/pynurbs/operations/advanced.py | 327 ----------------------------- tests/curves/test_intersection.py | 154 ++++++++++++++ tests/curves/test_projection.py | 75 +++++++ tests/operations/test_advanced.py | 223 -------------------- 7 files changed, 547 insertions(+), 551 deletions(-) create mode 100644 src/pynurbs/curves/intersection.py create mode 100644 src/pynurbs/curves/projection.py delete mode 100644 src/pynurbs/operations/advanced.py create mode 100644 tests/curves/test_intersection.py create mode 100644 tests/curves/test_projection.py delete mode 100644 tests/operations/test_advanced.py diff --git a/src/pynurbs/__init__.py b/src/pynurbs/__init__.py index 9387db0..80de5b6 100644 --- a/src/pynurbs/__init__.py +++ b/src/pynurbs/__init__.py @@ -1,6 +1,5 @@ from .basis_functions import BasisFunctions from .knotspace import GeneratorKnotVector, KnotVector -from .operations.advanced import Intersection, Projection from .operations.calculus import Derivate, Integrate __version__ = "1.1.0" diff --git a/src/pynurbs/curves/intersection.py b/src/pynurbs/curves/intersection.py new file mode 100644 index 0000000..4f0e014 --- /dev/null +++ b/src/pynurbs/curves/intersection.py @@ -0,0 +1,223 @@ +""" +File that contains the algorithms to compute the +intersection between two curves +""" + +from typing import Any, Tuple + +import numpy as np + +from ..operations import heavy +from ..operations.calculus import Derivate +from .curves import Curve + + +def _inse_retangle_float(avals: Tuple[float], bvals: Tuple[float]) -> bool: + """ + Given two array of floats, if verifies if the region + [min(avals), max(avals)] cap [min(bvals), max(bvals)] + is not empty + + """ + mina, maxa = min(avals), max(avals) + minb, maxb = min(bvals), max(bvals) + avals = (mina, (mina + maxa) / 2, maxa) + bvals = (minb, (minb + maxb) / 2, maxb) + for aval in avals: + if (aval - minb) * (aval - maxb) < 0: + return True + for bval in bvals: + if (bval - mina) * (bval - maxa) < 0: + return True + return False + + +def _inse_retangle(ctrlptsa: Tuple[Any], ctrlptsb: Tuple[Any]) -> bool: + """Given two curves A(u) and B(t), we test if the rectangular + region made by points A intersects the retangular region + made by points of B. + + - If A control points are scalars, it verifies if the region + + """ + try: + nsuba = len(ctrlptsa[0]) + assert nsuba == len(ctrlptsb[0]) + for i in range(nsuba): + valsa = [pt[i] for pt in ctrlptsa] + valsb = [pt[i] for pt in ctrlptsb] + inside = _inse_retangle(valsa, valsb) + if not inside: + return False + return True + except TypeError: + return _inse_retangle_float(ctrlptsa, ctrlptsb) + + +def filter_pairs(pairs: Tuple[Tuple[float]], tolerance: float = 1e-9): + """Filter the repeted knots within a given tolerance""" + pairs = np.array(pairs, dtype="float64") + filteredpairs = [] + for pair in pairs: + inside = False + for filtpair in filteredpairs: + if np.linalg.norm(pair - filtpair) < tolerance: + inside = True + if not inside: + filteredpairs.append(pair) + filteredpairs = tuple(map(tuple, filteredpairs)) + return filteredpairs + + +def pairs_min_distance( + pairs: Tuple[float], + curvea: Curve, + curveb: Curve, + tolerance: float = 1e-9, +): + """ + Filter the pairs (t*, u*) such abs(curvea(t*) - curveb(u*)) > tolerance + """ + pairs = heavy.totuple(pairs) + distances = np.empty(len(pairs), dtype="float64") + for k, (pti, puj) in enumerate(pairs): + pointati = curvea.eval(pti) + pointbuj = curveb.eval(puj) + distances[k] = np.linalg.norm(pointati - pointbuj) + distances = np.abs(distances) + matchs = np.abs(distances - np.min(distances)) < tolerance + pairs = np.array(pairs, dtype="float64")[matchs] + return heavy.totuple(pairs) + + +def __newton_bcurve_and_bcurve( + pair: Tuple[float], + curvesa: Tuple[Curve], + curvesb: Tuple[Curve], + limits: Tuple[float], +): + """ + Uses newton iterations to get the intersection + between two bezier curves. + + We supose pair is inside limits + """ + tmin, tmax = limits[0] + umin, umax = limits[1] + for _ in range(10): + diff = curvesa[0].eval(pair[0]) + dati = curvesa[1].eval(pair[0]) + ddati = curvesa[2].eval(pair[0]) + diff -= curvesb[0].eval(pair[1]) + dbuj = curvesb[1].eval(pair[1]) + ddbuj = curvesb[2].eval(pair[1]) + grad = np.array([np.inner(dati, diff), -np.inner(dbuj, diff)]) + ggrad = np.zeros((2, 2), dtype="float64") + ggrad[0, 0] = np.inner(ddati, diff) + ggrad[0, 0] += np.linalg.norm(dati) ** 2 + ggrad[1, 1] = -np.inner(ddbuj, diff) + ggrad[1, 1] += np.linalg.norm(dbuj) ** 2 + ggrad[0, 1] = -np.inner(dati, dbuj) + ggrad[1, 0] = ggrad[0, 1] + denom = np.linalg.det(ggrad) + if np.abs(denom) < 1e-9: + return tuple() # no convergence + deltapair = np.linalg.solve(ggrad, grad) + pair -= deltapair + if pair[0] < tmin: + pair[0] = tmin + elif tmax < pair[0]: + pair[0] = tmax + if pair[1] < umin: + pair[1] = umin + elif umax < pair[1]: + pair[1] = umax + if np.linalg.norm(deltapair) < 1e-9: + return tuple(pair) # convergence + return tuple(pair) + + +def bcurve_and_bcurve(beziera: Curve, bezierb: Curve) -> Tuple[float, float]: + """Return the parameters t*, u* such beziera(t*) = bezierb(u*) + + Given two bezier curves, A(t) and B(u), this function returns the + intersections between A and B. It can be: + + - If A(t) don't touch B(u), returns empty tuple + - If A(t) touches B(u) in a finite number of points, it returns + the pairs [(ta, ua), (tb, ub), ..., (tk, uk)] + - If A(t) overlaps B(u) in some interval, it returns + The interval [(ta, tb), (ua, ub)] + Still needs implementation + + """ + assert isinstance(beziera, Curve) + assert isinstance(bezierb, Curve) + assert beziera.degree + 1 == beziera.npts + assert beziera.degree + 1 == beziera.npts + if not _inse_retangle(beziera.ctrlpoints, bezierb.ctrlpoints): + return tuple() + + curvesa = [beziera] + curvesa.append(Derivate(curvesa[0])) + curvesa.append(Derivate(curvesa[1])) + curvesb = [bezierb] + curvesb.append(Derivate(curvesb[0])) + curvesb.append(Derivate(curvesb[1])) + dega, degb = beziera.degree, bezierb.degree + nsma, nsmb = dega + 1, degb + 1 # Number of samples + uamin, uamax = beziera.knotvector.limits + ubmin, ubmax = bezierb.knotvector.limits + limits = ((uamin, uamax), (ubmin, ubmax)) + nodes_a_sample = ( + [0] + [(2 * i + 1) / (2 * nsma) for i in range(nsma)] + [1] + ) + nodes_b_sample = ( + [0] + [(2 * i + 1) / (2 * nsmb) for i in range(nsmb)] + [1] + ) + uasample = [uamin + (uamax - uamin) * node for node in nodes_a_sample] + ubsample = [ubmin + (ubmax - ubmin) * node for node in nodes_b_sample] + pairs = set() + for nodea in uasample: + for nodeb in ubsample: + # Newton's iteration + pair = np.array((nodea, nodeb), dtype="float64") + pair = __newton_bcurve_and_bcurve(pair, curvesa, curvesb, limits) + if len(pair) != 0: + pairs |= set((pair,)) + if len(pairs) == 0: + return tuple() + pairs = tuple(pairs) + pairs = filter_pairs(pairs) + pairs = pairs_min_distance(pairs, curvesa[0], curvesb[0]) + return heavy.totuple(pairs) + + +def curve_and_curve(curvea: Curve, curveb: Curve) -> Tuple[Curve]: + """Return the parameters t*, u* such curvea(t*) = curveb(u*) + + Given two curves, A(t) and B(u), this function returns the + intersections between A and B. It can be: + + - If A(t) don't touch B(u), returns empty tuple + - If A(t) touches B(u) in a finite number of points, it returns + the pairs [(ta, ua), (tb, ub), ..., (tk, uk)] + - If A(t) overlaps B(u) in some interval, it returns + The interval [(ta, tb), (ua, ub)] + + """ + beziersa = curvea.split() + beziersb = curveb.split() + for bez in beziersa: + bez.clean() + for bez in beziersb: + bez.clean() + pairs = set() + for beziera in beziersa: + for bezierb in beziersb: + newpair = bcurve_and_bcurve(beziera, bezierb) + pairs |= set(newpair) + pairs = tuple(pairs) + pairs = filter_pairs(pairs) + pairs = pairs_min_distance(pairs, curvea, curveb) + return pairs diff --git a/src/pynurbs/curves/projection.py b/src/pynurbs/curves/projection.py new file mode 100644 index 0000000..e6446c7 --- /dev/null +++ b/src/pynurbs/curves/projection.py @@ -0,0 +1,95 @@ +""" +File that contains the algorithms to find the nearest point/curve with respect +to another point/curve +""" + +from typing import Tuple + +import numpy as np + +from ..operations.calculus import Derivate +from .curves import Curve + + +def __newton_point_on_curve( + point: Tuple[float], curves: Tuple[Curve], initparam: float +) -> float: + """ + Returns the parameter ui from newton's iteration + u_{i+1} = u_{i} - f(u_{i})/f'(u_{i}) + The point is + + """ + tolerance1 = 1e-6 + umin, umax = curves[0].knotvector.limits + niter = 0 + while True: + bezui = curves[0](initparam) - point + dbezui = curves[1](initparam) + ddbezui = curves[2](initparam) + upper = np.inner(dbezui, bezui) + lower = np.inner(ddbezui, bezui) + lower += np.inner(dbezui, dbezui) + diff = upper / lower + initparam -= diff + if initparam < umin: + return (umin,) + if initparam > umax: + return (umax,) + if np.abs(diff) < tolerance1: + return [initparam] + niter += 1 + + +def point_on_bezier(point: Tuple[float], bezier: Curve) -> Tuple[float]: + """Finds the parameters t* such + bezier(t*) is the near point + """ + umin, umax = bezier.knotvector.limits + curves = [bezier] + curves.append(Derivate(curves[0])) + curves.append(Derivate(curves[1])) + tparams = np.linspace(umin, umax, 5) + tvalues = set() + for tparam in tparams: + newt = __newton_point_on_curve(point, curves, tparam) + tvalues |= set(newt) + return tuple(tvalues) + + +def point_on_curve(point: Tuple[float], curve: Curve) -> Tuple[float]: + """Finds the parameters t* such curve(t*) is near point + + This function finds the parameter tstar in [tmin, tmax] such + minimizes the distance abs(curve(tstar) - point). + + Trully, it minimizes the distance square, related to the inner + product < C(u) - P, C(u) - P > = abs(C(u)-P)^2 + This function finds the solution of + f(u) = < C'(u), C(u) - P > = 0 + + Since it's possible to have more than one solution: + for example, the center of a circle is at equal distance always + then we return a list of parameters + + First, we decompose the curve in beziers, and try to find + the minimum distance of each bezier curve. + We use Newton's method + + """ + point = np.array(point) + beziers = curve.split() + for bez in beziers: + bez.clean() + tvalues = set() + for bezier in beziers: + newtvalues = point_on_bezier(point, bezier) + tvalues |= set(newtvalues) + tvalues = tuple(tvalues) + tvalues = np.array(tvalues) + distances = [np.linalg.norm(point - curve(t)) for t in tvalues] + minimaldistance = np.min(distances) + indexs = np.where(abs(distances - minimaldistance) < 1e-6)[0] + tvalues = tvalues[indexs] + tvalues.sort() + return tuple(tvalues) diff --git a/src/pynurbs/operations/advanced.py b/src/pynurbs/operations/advanced.py deleted file mode 100644 index 2b75bae..0000000 --- a/src/pynurbs/operations/advanced.py +++ /dev/null @@ -1,327 +0,0 @@ -""" -This file contains Advanced Geometric Algorithms -In Nurbs book, it correspond to chapter 6 -""" - -from typing import Any, Tuple - -import numpy as np - -from ..curves.curves import Curve -from . import heavy -from .calculus import Derivate - - -class Projection: - """ - Projection class to evaluate the nearest point/curve with respect - to another point/curve - """ - - @staticmethod - def __newton_point_on_curve( - point: Tuple[float], curves: Tuple[Curve], initparam: float - ) -> float: - """ - Returns the parameter ui from newton's iteration - u_{i+1} = u_{i} - f(u_{i})/f'(u_{i}) - The point is - - """ - tolerance1 = 1e-6 - umin, umax = curves[0].knotvector.limits - niter = 0 - while True: - bezui = curves[0](initparam) - point - dbezui = curves[1](initparam) - ddbezui = curves[2](initparam) - upper = np.inner(dbezui, bezui) - lower = np.inner(ddbezui, bezui) - lower += np.inner(dbezui, dbezui) - diff = upper / lower - initparam -= diff - if initparam < umin: - return (umin,) - if initparam > umax: - return (umax,) - if np.abs(diff) < tolerance1: - return [initparam] - niter += 1 - - @staticmethod - def point_on_bezier(point: Tuple[float], bezier: Curve) -> Tuple[float]: - """Finds the parameters t* such - bezier(t*) is the near point - """ - umin, umax = bezier.knotvector.limits - curves = [bezier] - curves.append(Derivate(curves[0])) - curves.append(Derivate(curves[1])) - tparams = np.linspace(umin, umax, 5) - tvalues = set() - for tparam in tparams: - newt = Projection.__newton_point_on_curve(point, curves, tparam) - tvalues |= set(newt) - return tuple(tvalues) - - @staticmethod - def point_on_curve(point: Tuple[float], curve: Curve) -> Tuple[float]: - """Finds the parameters t* such curve(t*) is near point - - This function finds the parameter tstar in [tmin, tmax] such - minimizes the distance abs(curve(tstar) - point). - - Trully, it minimizes the distance square, related to the inner - product < C(u) - P, C(u) - P > = abs(C(u)-P)^2 - This function finds the solution of - f(u) = < C'(u), C(u) - P > = 0 - - Since it's possible to have more than one solution: - for example, the center of a circle is at equal distance always - then we return a list of parameters - - First, we decompose the curve in beziers, and try to find - the minimum distance of each bezier curve. - We use Newton's method - - """ - point = np.array(point) - beziers = curve.split() - for bez in beziers: - bez.clean() - tvalues = set() - for bezier in beziers: - newtvalues = Projection.point_on_bezier(point, bezier) - tvalues |= set(newtvalues) - tvalues = tuple(tvalues) - tvalues = np.array(tvalues) - distances = [np.linalg.norm(point - curve(t)) for t in tvalues] - minimaldistance = np.min(distances) - indexs = np.where(abs(distances - minimaldistance) < 1e-6)[0] - tvalues = tvalues[indexs] - tvalues.sort() - return tuple(tvalues) - - -class Intersection: - """Intersection static class, responsible to compute the intersection - between two objects, like curve and curve, surface and curve, and so on - - """ - - @staticmethod - def _inse_retangle_float(avals: Tuple[float], bvals: Tuple[float]) -> bool: - """ - Given two array of floats, if verifies if the region - [min(avals), max(avals)] cap [min(bvals), max(bvals)] - is not empty - - """ - mina, maxa = min(avals), max(avals) - minb, maxb = min(bvals), max(bvals) - avals = (mina, (mina + maxa) / 2, maxa) - bvals = (minb, (minb + maxb) / 2, maxb) - for aval in avals: - if (aval - minb) * (aval - maxb) < 0: - return True - for bval in bvals: - if (bval - mina) * (bval - maxa) < 0: - return True - return False - - @staticmethod - def _inse_retangle(ctrlptsa: Tuple[Any], ctrlptsb: Tuple[Any]) -> bool: - """Given two curves A(u) and B(t), we test if the rectangular - region made by points A intersects the retangular region - made by points of B. - - - If A control points are scalars, it verifies if the region - - """ - try: - nsuba = len(ctrlptsa[0]) - assert nsuba == len(ctrlptsb[0]) - for i in range(nsuba): - valsa = [pt[i] for pt in ctrlptsa] - valsb = [pt[i] for pt in ctrlptsb] - inside = Intersection._inse_retangle(valsa, valsb) - if not inside: - return False - return True - except TypeError: - return Intersection._inse_retangle_float(ctrlptsa, ctrlptsb) - - @staticmethod - def filter_pairs(pairs: Tuple[Tuple[float]], tolerance: float = 1e-9): - """Filter the repeted knots within a given tolerance""" - pairs = np.array(pairs, dtype="float64") - filteredpairs = [] - for pair in pairs: - inside = False - for filtpair in filteredpairs: - if np.linalg.norm(pair - filtpair) < tolerance: - inside = True - if not inside: - filteredpairs.append(pair) - filteredpairs = tuple(map(tuple, filteredpairs)) - return filteredpairs - - @staticmethod - def pairs_min_distance( - pairs: Tuple[float], - curvea: Curve, - curveb: Curve, - tolerance: float = 1e-9, - ): - """ - Filter the pairs (t*, u*) such abs(curvea(t*) - curveb(u*)) > tolerance - """ - pairs = heavy.totuple(pairs) - distances = np.empty(len(pairs), dtype="float64") - for k, (pti, puj) in enumerate(pairs): - pointati = curvea.eval(pti) - pointbuj = curveb.eval(puj) - distances[k] = np.linalg.norm(pointati - pointbuj) - distances = np.abs(distances) - matchs = np.abs(distances - np.min(distances)) < tolerance - pairs = np.array(pairs, dtype="float64")[matchs] - return heavy.totuple(pairs) - - @staticmethod - def __newton_bcurve_and_bcurve( - pair: Tuple[float], - curvesa: Tuple[Curve], - curvesb: Tuple[Curve], - limits: Tuple[float], - ): - """ - Uses newton iterations to get the intersection - between two bezier curves. - - We supose pair is inside limits - """ - tmin, tmax = limits[0] - umin, umax = limits[1] - for _ in range(10): - diff = curvesa[0].eval(pair[0]) - dati = curvesa[1].eval(pair[0]) - ddati = curvesa[2].eval(pair[0]) - diff -= curvesb[0].eval(pair[1]) - dbuj = curvesb[1].eval(pair[1]) - ddbuj = curvesb[2].eval(pair[1]) - grad = np.array([np.inner(dati, diff), -np.inner(dbuj, diff)]) - ggrad = np.zeros((2, 2), dtype="float64") - ggrad[0, 0] = np.inner(ddati, diff) - ggrad[0, 0] += np.linalg.norm(dati) ** 2 - ggrad[1, 1] = -np.inner(ddbuj, diff) - ggrad[1, 1] += np.linalg.norm(dbuj) ** 2 - ggrad[0, 1] = -np.inner(dati, dbuj) - ggrad[1, 0] = ggrad[0, 1] - denom = np.linalg.det(ggrad) - if np.abs(denom) < 1e-9: - return tuple() # no convergence - deltapair = np.linalg.solve(ggrad, grad) - pair -= deltapair - if pair[0] < tmin: - pair[0] = tmin - elif tmax < pair[0]: - pair[0] = tmax - if pair[1] < umin: - pair[1] = umin - elif umax < pair[1]: - pair[1] = umax - if np.linalg.norm(deltapair) < 1e-9: - return tuple(pair) # convergence - return tuple(pair) - - @staticmethod - def bcurve_and_bcurve( - beziera: Curve, bezierb: Curve - ) -> Tuple[float, float]: - """Return the parameters t*, u* such beziera(t*) = bezierb(u*) - - Given two bezier curves, A(t) and B(u), this function returns the - intersections between A and B. It can be: - - - If A(t) don't touch B(u), returns empty tuple - - If A(t) touches B(u) in a finite number of points, it returns - the pairs [(ta, ua), (tb, ub), ..., (tk, uk)] - - If A(t) overlaps B(u) in some interval, it returns - The interval [(ta, tb), (ua, ub)] - Still needs implementation - - """ - assert isinstance(beziera, Curve) - assert isinstance(bezierb, Curve) - assert beziera.degree + 1 == beziera.npts - assert beziera.degree + 1 == beziera.npts - if not Intersection._inse_retangle( - beziera.ctrlpoints, bezierb.ctrlpoints - ): - return tuple() - - curvesa = [beziera] - curvesa.append(Derivate(curvesa[0])) - curvesa.append(Derivate(curvesa[1])) - curvesb = [bezierb] - curvesb.append(Derivate(curvesb[0])) - curvesb.append(Derivate(curvesb[1])) - dega, degb = beziera.degree, bezierb.degree - nsma, nsmb = dega + 1, degb + 1 # Number of samples - uamin, uamax = beziera.knotvector.limits - ubmin, ubmax = bezierb.knotvector.limits - limits = ((uamin, uamax), (ubmin, ubmax)) - nodes_a_sample = ( - [0] + [(2 * i + 1) / (2 * nsma) for i in range(nsma)] + [1] - ) - nodes_b_sample = ( - [0] + [(2 * i + 1) / (2 * nsmb) for i in range(nsmb)] + [1] - ) - uasample = [uamin + (uamax - uamin) * node for node in nodes_a_sample] - ubsample = [ubmin + (ubmax - ubmin) * node for node in nodes_b_sample] - pairs = set() - for nodea in uasample: - for nodeb in ubsample: - # Newton's iteration - pair = np.array((nodea, nodeb), dtype="float64") - pair = Intersection.__newton_bcurve_and_bcurve( - pair, curvesa, curvesb, limits - ) - if len(pair) != 0: - pairs |= set((pair,)) - if len(pairs) == 0: - return tuple() - pairs = tuple(pairs) - pairs = Intersection.filter_pairs(pairs) - pairs = Intersection.pairs_min_distance(pairs, curvesa[0], curvesb[0]) - return heavy.totuple(pairs) - - @staticmethod - def curve_and_curve(curvea: Curve, curveb: Curve) -> Tuple[Curve]: - """Return the parameters t*, u* such curvea(t*) = curveb(u*) - - Given two curves, A(t) and B(u), this function returns the - intersections between A and B. It can be: - - - If A(t) don't touch B(u), returns empty tuple - - If A(t) touches B(u) in a finite number of points, it returns - the pairs [(ta, ua), (tb, ub), ..., (tk, uk)] - - If A(t) overlaps B(u) in some interval, it returns - The interval [(ta, tb), (ua, ub)] - - """ - beziersa = curvea.split() - beziersb = curveb.split() - for bez in beziersa: - bez.clean() - for bez in beziersb: - bez.clean() - pairs = set() - for beziera in beziersa: - for bezierb in beziersb: - newpair = Intersection.bcurve_and_bcurve(beziera, bezierb) - pairs |= set(newpair) - pairs = tuple(pairs) - pairs = Intersection.filter_pairs(pairs) - pairs = Intersection.pairs_min_distance(pairs, curvea, curveb) - return pairs diff --git a/tests/curves/test_intersection.py b/tests/curves/test_intersection.py new file mode 100644 index 0000000..f979193 --- /dev/null +++ b/tests/curves/test_intersection.py @@ -0,0 +1,154 @@ +""" +This file is responsible to testing the code inside the file ```calculus.py``` +Its functions are getting derivatives, computing integrals along curves and so on +""" + +import numpy as np +import pytest + +from pynurbs.curves.curves import Curve +from pynurbs.curves.intersection import bcurve_and_bcurve, curve_and_curve + + +@pytest.mark.order(42) +@pytest.mark.dependency( + depends=[ + "tests/test_knotspace.py::test_end", + "tests/test_basis_functions.py::test_end", + "tests/curves/test_bezier.py::test_end", + "tests/curves/test_spline.py::test_end", + "tests/operations/test_calculus.py::test_end", + ], + scope="session", +) +def test_begin(): + pass + + +@pytest.mark.order(42) +@pytest.mark.timeout(4) +@pytest.mark.dependency(depends=["test_begin"]) +def test_bcurve_and_bcurve(): + beziera = Curve([0, 0, 1, 1]) + beziera.ctrlpoints = np.array([(0, 0), (1, 1)]) + bezierb = Curve([0, 0, 1, 1]) + bezierb.ctrlpoints = np.array([(0, 1), (1, 0)]) + inters = bcurve_and_bcurve(beziera, bezierb) + + assert len(inters) == 1 + np.testing.assert_allclose(inters[0], (0.5, 0.5)) + + beziera.knot_insert([0.2]) + bezierb.knot_insert([0.7]) + inters = curve_and_curve(beziera, bezierb) + + assert len(inters) == 1 + np.testing.assert_allclose(inters[0], (0.5, 0.5)) + + +@pytest.mark.order(42) +@pytest.mark.timeout(50) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_bcurve_and_bcurve", + ] +) +def test_quarter_circles(): + knotvector = [0, 0, 0, 1, 1, 1] + pointsa = [(1, 0), (1, 1), (0, 1)] + pointsb = [(0, 0), (0, 1), (1, 1)] + circlea = Curve(knotvector, np.array(pointsa)) + circleb = Curve(knotvector, np.array(pointsb)) + + inters = bcurve_and_bcurve(circlea, circleb) + assert len(inters) == 1 + root = 1 / np.sqrt(2) + np.testing.assert_allclose(inters[0], (root, root)) + + circlea.weights = (1, 1, 1) + circleb.weights = (1, 1, 1) + inters = bcurve_and_bcurve(circlea, circleb) + assert len(inters) == 1 + np.testing.assert_allclose(inters[0], (root, root)) + + circlea.weights = (1, 1, 2) + circleb.weights = (1, 1, 2) + inters = bcurve_and_bcurve(circlea, circleb) + assert len(inters) == 1 + root = 1 / np.sqrt(3) + np.testing.assert_allclose(inters[0], (root, root)) + + +@pytest.mark.order(42) +@pytest.mark.timeout(50) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_bcurve_and_bcurve", + "test_quarter_circles", + ] +) +def test_half_circles(): + knotvector = [0, 0, 0, 0, 1, 1, 1, 1] + weights = [3, 1, 1, 3] + pointsa = [(1, 0), (1, 2), (-1, 2), (-1, 0)] + pointsb = [(0, 0), (0, 2), (2, 2), (2, 0)] + circlea = Curve(knotvector, np.array(pointsa), weights) + circleb = Curve(knotvector, np.array(pointsb), weights) + + inters = bcurve_and_bcurve(circlea, circleb) + assert len(inters) == 1 + root = (np.sqrt(3) - 1) / 2 + np.testing.assert_allclose(inters[0], (root, root)) + + +@pytest.mark.order(42) +@pytest.mark.timeout(50) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_bcurve_and_bcurve", + "test_quarter_circles", + "test_half_circles", + ] +) +def test_circle_and_circle(): + knotvector = [0, 0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 1] + weights = [3, 1, 1, 3, 1, 1, 3] + ctrlpointsa = [ + (1, 0), + (1, 2), + (-1, 2), + (-1, 0), + (-1, -2), + (1, -2), + (1, 0), + ] + ctrlpointsa = np.array(ctrlpointsa, dtype="float64") + circlea = Curve(knotvector, ctrlpointsa, weights) + + ctrlpointsb = np.copy(ctrlpointsa) + ctrlpointsb[:, 0] += 1 + circleb = Curve(knotvector, ctrlpointsb, weights) + + inters = curve_and_curve(circlea, circleb) + for ua, ub in inters: + pointa = circlea(ua) + pointb = circleb(ub) + distance = np.abs(pointa - pointb) + assert np.all(distance < 1e-9) + + +@pytest.mark.order(42) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_bcurve_and_bcurve", + "test_quarter_circles", + "test_half_circles", + "test_circle_and_circle", + ] +) +def test_end(): + pass diff --git a/tests/curves/test_projection.py b/tests/curves/test_projection.py new file mode 100644 index 0000000..833fb5d --- /dev/null +++ b/tests/curves/test_projection.py @@ -0,0 +1,75 @@ +""" +This file is responsible to testing the code inside the file ```calculus.py``` +Its functions are getting derivatives, computing integrals along curves and so on +""" + +import numpy as np +import pytest + +from pynurbs.curves.curves import Curve +from pynurbs.curves.projection import point_on_curve + + +@pytest.mark.order(42) +@pytest.mark.dependency( + depends=[ + "tests/test_knotspace.py::test_end", + "tests/test_basis_functions.py::test_end", + "tests/curves/test_bezier.py::test_end", + "tests/curves/test_spline.py::test_end", + "tests/operations/test_calculus.py::test_end", + ], + scope="session", +) +def test_begin(): + pass + + +@pytest.mark.order(42) +@pytest.mark.timeout(4) +@pytest.mark.dependency(depends=["test_begin"]) +def test_point_on_curve(): + knotvector = [0, 0, 1, 1] + points = [(0, 0), (1, 0)] + curve = Curve(knotvector) + curve.ctrlpoints = np.array(points, dtype="float64") + + project = lambda point: point_on_curve(point, curve) + np.testing.assert_allclose(project((0, 0)), (0,)) + np.testing.assert_allclose(project((1, 0)), (1,)) + np.testing.assert_allclose(project((0.5, 0)), (0.5,)) + + knotvector = [0, 0, 1, 2, 2] + points = [(1, -1), (0, 0), (1, 1)] + curve = Curve(knotvector) + curve.ctrlpoints = np.array(points, dtype="float64") + + project = lambda point: point_on_curve(point, curve) + np.testing.assert_allclose(project((1, -1)), (0,)) + np.testing.assert_allclose(project((0, 0)), (1,)) + np.testing.assert_allclose(project((1, 1)), (2,)) + np.testing.assert_allclose(project((1, 0)), (0.5, 1.5)) + + knotvector = [0, 0, 1, 2, 3, 4, 4] + points = [(1, -2), (1, -1), (0, 0), (1, 1), (1, 2)] + curve = Curve(knotvector) + curve.ctrlpoints = np.array(points, dtype="float64") + + project = lambda point: point_on_curve(point, curve) + np.testing.assert_allclose(project((1, -2)), (0,)) + np.testing.assert_allclose(project((1, -1)), (1,)) + np.testing.assert_allclose(project((0, 0)), (2,)) + np.testing.assert_allclose(project((1, 1)), (3,)) + np.testing.assert_allclose(project((1, 2)), (4,)) + np.testing.assert_allclose(project((1, 0)), (1.5, 2.5)) + + +@pytest.mark.order(42) +@pytest.mark.dependency( + depends=[ + "test_begin", + "test_point_on_curve", + ] +) +def test_end(): + pass diff --git a/tests/operations/test_advanced.py b/tests/operations/test_advanced.py deleted file mode 100644 index a6d673e..0000000 --- a/tests/operations/test_advanced.py +++ /dev/null @@ -1,223 +0,0 @@ -""" -This file is responsible to testing the code inside the file ```calculus.py``` -Its functions are getting derivatives, computing integrals along curves and so on -""" - -import numpy as np -import pytest - -from pynurbs.curves.curves import Curve -from pynurbs.operations.advanced import Intersection, Projection - - -@pytest.mark.order(42) -@pytest.mark.dependency( - depends=[ - "tests/test_knotspace.py::test_end", - "tests/test_basis_functions.py::test_end", - "tests/curves/test_bezier.py::test_end", - "tests/curves/test_spline.py::test_end", - "tests/operations/test_calculus.py::test_end", - ], - scope="session", -) -def test_begin(): - pass - - -class TestProjection: - @pytest.mark.order(42) - @pytest.mark.dependency(depends=["test_begin"]) - def test_begin(self): - pass - - @pytest.mark.order(42) - @pytest.mark.timeout(4) - @pytest.mark.dependency(depends=["TestProjection::test_begin"]) - def test_point_on_curve(self): - knotvector = [0, 0, 1, 1] - points = [(0, 0), (1, 0)] - curve = Curve(knotvector) - curve.ctrlpoints = np.array(points, dtype="float64") - - project = lambda point: Projection.point_on_curve(point, curve) - np.testing.assert_allclose(project((0, 0)), (0,)) - np.testing.assert_allclose(project((1, 0)), (1,)) - np.testing.assert_allclose(project((0.5, 0)), (0.5,)) - - knotvector = [0, 0, 1, 2, 2] - points = [(1, -1), (0, 0), (1, 1)] - curve = Curve(knotvector) - curve.ctrlpoints = np.array(points, dtype="float64") - - project = lambda point: Projection.point_on_curve(point, curve) - np.testing.assert_allclose(project((1, -1)), (0,)) - np.testing.assert_allclose(project((0, 0)), (1,)) - np.testing.assert_allclose(project((1, 1)), (2,)) - np.testing.assert_allclose(project((1, 0)), (0.5, 1.5)) - - knotvector = [0, 0, 1, 2, 3, 4, 4] - points = [(1, -2), (1, -1), (0, 0), (1, 1), (1, 2)] - curve = Curve(knotvector) - curve.ctrlpoints = np.array(points, dtype="float64") - - project = lambda point: Projection.point_on_curve(point, curve) - np.testing.assert_allclose(project((1, -2)), (0,)) - np.testing.assert_allclose(project((1, -1)), (1,)) - np.testing.assert_allclose(project((0, 0)), (2,)) - np.testing.assert_allclose(project((1, 1)), (3,)) - np.testing.assert_allclose(project((1, 2)), (4,)) - np.testing.assert_allclose(project((1, 0)), (1.5, 2.5)) - - @pytest.mark.order(42) - @pytest.mark.dependency( - depends=[ - "TestProjection::test_begin", - "TestProjection::test_point_on_curve", - ] - ) - def test_end(self): - pass - - -class TestIntersection: - @pytest.mark.order(42) - @pytest.mark.dependency(depends=["test_begin"]) - def test_begin(self): - pass - - @pytest.mark.order(42) - @pytest.mark.timeout(4) - @pytest.mark.dependency(depends=["TestIntersection::test_begin"]) - def test_bcurve_and_bcurve(self): - beziera = Curve([0, 0, 1, 1]) - beziera.ctrlpoints = np.array([(0, 0), (1, 1)]) - bezierb = Curve([0, 0, 1, 1]) - bezierb.ctrlpoints = np.array([(0, 1), (1, 0)]) - inters = Intersection.bcurve_and_bcurve(beziera, bezierb) - - assert len(inters) == 1 - np.testing.assert_allclose(inters[0], (0.5, 0.5)) - - beziera.knot_insert([0.2]) - bezierb.knot_insert([0.7]) - inters = Intersection.curve_and_curve(beziera, bezierb) - - assert len(inters) == 1 - np.testing.assert_allclose(inters[0], (0.5, 0.5)) - - @pytest.mark.order(42) - @pytest.mark.timeout(50) - @pytest.mark.dependency( - depends=[ - "TestIntersection::test_begin", - "TestIntersection::test_bcurve_and_bcurve", - ] - ) - def test_quarter_circles(self): - knotvector = [0, 0, 0, 1, 1, 1] - pointsa = [(1, 0), (1, 1), (0, 1)] - pointsb = [(0, 0), (0, 1), (1, 1)] - circlea = Curve(knotvector, np.array(pointsa)) - circleb = Curve(knotvector, np.array(pointsb)) - - inters = Intersection.bcurve_and_bcurve(circlea, circleb) - assert len(inters) == 1 - root = 1 / np.sqrt(2) - np.testing.assert_allclose(inters[0], (root, root)) - - circlea.weights = (1, 1, 1) - circleb.weights = (1, 1, 1) - inters = Intersection.bcurve_and_bcurve(circlea, circleb) - assert len(inters) == 1 - np.testing.assert_allclose(inters[0], (root, root)) - - circlea.weights = (1, 1, 2) - circleb.weights = (1, 1, 2) - inters = Intersection.bcurve_and_bcurve(circlea, circleb) - assert len(inters) == 1 - root = 1 / np.sqrt(3) - np.testing.assert_allclose(inters[0], (root, root)) - - @pytest.mark.order(42) - @pytest.mark.timeout(50) - @pytest.mark.dependency( - depends=[ - "TestIntersection::test_begin", - "TestIntersection::test_bcurve_and_bcurve", - "TestIntersection::test_quarter_circles", - ] - ) - def test_half_circles(self): - knotvector = [0, 0, 0, 0, 1, 1, 1, 1] - weights = [3, 1, 1, 3] - pointsa = [(1, 0), (1, 2), (-1, 2), (-1, 0)] - pointsb = [(0, 0), (0, 2), (2, 2), (2, 0)] - circlea = Curve(knotvector, np.array(pointsa), weights) - circleb = Curve(knotvector, np.array(pointsb), weights) - - inters = Intersection.bcurve_and_bcurve(circlea, circleb) - assert len(inters) == 1 - root = (np.sqrt(3) - 1) / 2 - np.testing.assert_allclose(inters[0], (root, root)) - - @pytest.mark.order(42) - @pytest.mark.timeout(50) - @pytest.mark.dependency( - depends=[ - "TestIntersection::test_begin", - "TestIntersection::test_bcurve_and_bcurve", - "TestIntersection::test_quarter_circles", - "TestIntersection::test_half_circles", - ] - ) - def test_circle_and_circle(self): - knotvector = [0, 0, 0, 0, 0.5, 0.5, 0.5, 1, 1, 1, 1] - weights = [3, 1, 1, 3, 1, 1, 3] - ctrlpointsa = [ - (1, 0), - (1, 2), - (-1, 2), - (-1, 0), - (-1, -2), - (1, -2), - (1, 0), - ] - ctrlpointsa = np.array(ctrlpointsa, dtype="float64") - circlea = Curve(knotvector, ctrlpointsa, weights) - - ctrlpointsb = np.copy(ctrlpointsa) - ctrlpointsb[:, 0] += 1 - circleb = Curve(knotvector, ctrlpointsb, weights) - - inters = Intersection.curve_and_curve(circlea, circleb) - for ua, ub in inters: - pointa = circlea(ua) - pointb = circleb(ub) - distance = np.abs(pointa - pointb) - assert np.all(distance < 1e-9) - - @pytest.mark.order(42) - @pytest.mark.dependency( - depends=[ - "TestIntersection::test_begin", - "TestIntersection::test_bcurve_and_bcurve", - "TestIntersection::test_quarter_circles", - "TestIntersection::test_half_circles", - "TestIntersection::test_circle_and_circle", - ] - ) - def test_end(self): - pass - - -@pytest.mark.order(42) -@pytest.mark.dependency( - depends=[ - "test_begin", - "TestProjection::test_end", - "TestIntersection::test_end", - ] -) -def test_end(): - pass From 94990240e0d1beeaab744827c9945365450d13ef Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 8 Jul 2025 21:20:11 +0200 Subject: [PATCH 115/116] docs: add documentation for math functions --- src/pynurbs/core/custom_math.py | 66 +++++++++++++++++++++++++++++++-- 1 file changed, 63 insertions(+), 3 deletions(-) diff --git a/src/pynurbs/core/custom_math.py b/src/pynurbs/core/custom_math.py index 224415f..677bd9b 100644 --- a/src/pynurbs/core/custom_math.py +++ b/src/pynurbs/core/custom_math.py @@ -1,3 +1,11 @@ +""" +Module that contains mathematical functions used in the module. +Some functions are already defined in the standard library `math` but +* some are not available in lower versions (ex. math.comb was added + only in py3.8, math.lcm was added in py3.9), +* conversions to base types (int/float) are made and custom types are lost +""" + import math from copy import deepcopy from fractions import Fraction @@ -8,8 +16,23 @@ class Math: + """ + Defines some mathematical functions + + """ + @staticmethod - def gcd(*numbers: Tuple[int]) -> int: + def gcd(*numbers: int) -> int: + """ + Return the greatest common divisor of the specified integer arguments + + Example + ------- + >>> gcd(12, 9) + 3 + >>> gcd(120, 35) + 5 + """ lenght = len(numbers) if lenght == 1: return abs(numbers[0]) @@ -24,7 +47,17 @@ def gcd(*numbers: Tuple[int]) -> int: return abs(x) @staticmethod - def lcm(*numbers: Tuple[int]) -> int: + def lcm(*numbers: int) -> int: + """ + Return the least common multiple of the specified integer arguments + + Example + ------- + >>> lcm(12, 9) + 36 + >>> gcd(120, 35) + 840 + """ lenght = len(numbers) if lenght == 1: return numbers[0] @@ -41,7 +74,16 @@ def lcm(*numbers: Tuple[int]) -> int: @staticmethod def binom(n: int, i: int) -> int: """ - Returns binomial (n, i) + Return the binomial number (n, i) + + Evaluates to `n! / (i! * (n - i)!)` when `0 <= i <= n`. + + Example + ------- + >>> binom(2, 3) + 2 + >>> binom(5, 2) + 10 """ numerator = Math.factorial(n) denominator = Math.factorial(i) @@ -50,6 +92,24 @@ def binom(n: int, i: int) -> int: @staticmethod def factorial(number: int) -> int: + """ + Return factorial of the nonnegative integer n. + + Example + ------- + >>> factorial(0) + 1 + >>> factorial(1) + 1 + >>> factorial(2) + 2 + >>> factorial(3) + 6 + >>> factorial(4) + 24 + >>> factorial(5) + 120 + """ if number < 2: return 1 prod = 1 From 1ebe493acc4b240b60bd50e936d1a58843fbf221 Mon Sep 17 00:00:00 2001 From: Carlos Adir Date: Tue, 8 Jul 2025 23:03:47 +0200 Subject: [PATCH 116/116] dev: update python version --- .github/workflows/build.yaml | 2 +- pyproject.toml | 6 +++--- tox.ini | 7 ++++--- 3 files changed, 8 insertions(+), 7 deletions(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f33fda0..d1c7247 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -22,7 +22,7 @@ jobs: strategy: matrix: os: [ubuntu-latest, windows-latest] - python-version: ['3.7', '3.8', '3.9', '3.10'] + python-version: ['3.9', '3.10', '3.11', '3.12', '3.13'] steps: - name: Checkout sources uses: actions/checkout@v2 diff --git a/pyproject.toml b/pyproject.toml index 2ac89af..dc5557c 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,18 +1,18 @@ [tool.poetry] name = "pynurbs" -version = "1.1.0" +version = "1.2.0" description = "NURBS python object-oriented library" readme = "README.md" authors = ["Carlos Adir "] packages = [{ include = "pynurbs", from = "src" }] [tool.poetry.dependencies] -python = "^3.9" +python = "^3.5" numpy = "^1" rbool = "^0" [tool.poetry.dev-dependencies] -python = "^3.9" +python = "^3.5" numpy = "^1" rbool = "^0" pytest = "^5.2" diff --git a/tox.ini b/tox.ini index e14e707..c7051ed 100644 --- a/tox.ini +++ b/tox.ini @@ -1,6 +1,6 @@ [tox] isolated_build = True -envlist = python3.7, python3.8, python3.9, python3.10 +envlist = python3.5, python3.6, python3.7, python3.8, python3.9, python3.10, python3.11, python3.12, python3.13 [testenv] deps = @@ -14,10 +14,11 @@ commands = [gh-actions] python = - 3.7: py37 - 3.8: py38 3.9: py39 3.10: py310 + 3.11: py311 + 3.12: py312 + 3.13: py313 [testenv:coverage] deps =