Skip to content

Commit 818ba5e

Browse files
committed
spline __call__ working with pytorch
1 parent c6f4186 commit 818ba5e

1 file changed

Lines changed: 81 additions & 56 deletions

File tree

‎src/splinebox/spline_curves.py‎

Lines changed: 81 additions & 56 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
import math
1010
import warnings
1111

12+
import array_api_compat
1213
import numba
1314
import numpy as np
1415
import scipy.integrate
@@ -154,7 +155,6 @@ class Spline:
154155
_wrong_array_size_msg = (
155156
"It looks like control_points is neither a 1 nor a 2D array. I don't know how to handle this yet."
156157
)
157-
_no_control_points_msg = "This spline object doesn't have any control points yet."
158158
_unimplemented_msg = "This function is not implemented."
159159

160160
def __init__(
@@ -188,14 +188,16 @@ def __init__(
188188
self._cached_segments = None
189189
self._cached_segment_lengths = None
190190

191-
def _check_control_points(self):
191+
def _get_control_points_xp(self):
192192
"""
193193
Most methods require control points to be set before they
194194
can be used. This helper function checks if the control points have been
195-
set.
195+
set and return the their array namespace.
196196
"""
197197
if self.control_points is None:
198-
raise RuntimeError(self._no_control_points_msg)
198+
raise RuntimeError("This spline object doesn't have any control points yet.")
199+
else:
200+
return array_api_compat.array_namespace(self.control_points)
199201

200202
def __str__(self):
201203
closed_str = "closed" if self.closed else "open"
@@ -231,7 +233,8 @@ def control_points(self):
231233
@control_points.setter
232234
def control_points(self, values):
233235
if values is not None:
234-
values = np.array(values)
236+
if not array_api_compat.is_array_api_obj(values):
237+
values = np.array(values)
235238
if values.ndim != 2:
236239
raise ValueError(
237240
"The control point array has to be 2D. The first dimension encodes the control points and the second corresponds to the dimensionality of the codomain of the spline."
@@ -796,7 +799,7 @@ def draw(self, x, y):
796799
>>> plt.plot(vals[:, 0], vals[:, 1]) # doctest: +SKIP
797800
>>> plt.show() # doctest: +SKIP
798801
"""
799-
self._check_control_points()
802+
self._get_control_points_xp()
800803

801804
if self.ndim != 2:
802805
raise RuntimeError("draw() can only be used with 2D curves")
@@ -822,7 +825,7 @@ def dtheta(self, t):
822825
.. math::
823826
\frac{d \theta}{dt} = \frac{1}{r^2} \left( x\frac{dy}{dt} - y\frac{dx}{dt} \right) \text{, where } r^2 = x^2 + y^2
824827
"""
825-
self._check_control_points()
828+
self._get_control_points_xp()
826829
if self.ndim != 2:
827830
raise RuntimeError("dtheta() is only defined for 2D curves.")
828831
t, single_value = self._convert_to_array(t)
@@ -891,7 +894,7 @@ def is_inside(self, x, y):
891894
"""
892895
if not self.closed:
893896
raise RuntimeError("isInside() can only be used with closed curves.")
894-
self._check_control_points()
897+
self._get_control_points_xp()
895898
if self.ndim != 2:
896899
raise RuntimeError("isInside() can only be used with 2D curves.")
897900

@@ -1090,7 +1093,7 @@ def arc_length(self, stop=None, start=0):
10901093
>>> spline.arc_length(np.arange(M - 1), np.arange(1, M))
10911094
array([4.982, 5.288, 3.09 , 3.675])
10921095
"""
1093-
self._check_control_points()
1096+
self._get_control_points_xp()
10941097

10951098
if stop is None:
10961099
stop = self.M if self.closed else self.M - 1
@@ -1209,7 +1212,7 @@ def arc_length_to_parameter(self, s, atol=1e-4):
12091212
>>> spline.arc_length(0, 2.12) # doctest: +NUMBER
12101213
2.2
12111214
"""
1212-
self._check_control_points()
1215+
self._get_control_points_xp()
12131216
s, single_value = self._convert_to_array(s)
12141217
sort_indices = np.argsort(s)
12151218
results = np.zeros_like(s, dtype=float)
@@ -1341,7 +1344,7 @@ def curvature(self, t):
13411344
>>> spline.curvature([1, 3])
13421345
array([ 2.035, -3.616])
13431346
"""
1344-
self._check_control_points()
1347+
self._get_control_points_xp()
13451348
t, single_value = self._convert_to_array(t)
13461349
first_deriv = self(t, derivative=1)
13471350
second_deriv = self(t, derivative=2)
@@ -1414,7 +1417,7 @@ def normal(self, t, frame="bishop", initial_vector=None, shift=None):
14141417
>>> plt.arrow(point[0], point[1], normal[0], normal[1]) # doctest: +SKIP
14151418
>>> plt.show() # doctest: +SKIP
14161419
"""
1417-
self._check_control_points()
1420+
self._get_control_points_xp()
14181421
t, single_value = self._convert_to_array(t)
14191422
if self.ndim not in (2, 3):
14201423
raise NotImplementedError(
@@ -1537,7 +1540,7 @@ def moving_frame(self, t, method="frenet", initial_vector=None, shift=None):
15371540
[-1., 0., 0.],
15381541
[ 0., -0., 1.]]])
15391542
"""
1540-
self._check_control_points()
1543+
self._get_control_points_xp()
15411544

15421545
if self.ndim != 3:
15431546
raise RuntimeError("A frame can only be computed for splines in 3D.")
@@ -1656,7 +1659,7 @@ def moving_frame(self, t, method="frenet", initial_vector=None, shift=None):
16561659
nopython=True,
16571660
cache=True,
16581661
)
1659-
def _compute_tval_and_indices(t, shift, closed, M, pad, tval, indices):
1662+
def _compute_tval_and_indices_numba(t, shift, closed, M, pad, tval, indices):
16601663
"""
16611664
Helper function for creating a csr sparse matrix.
16621665
@@ -1686,6 +1689,14 @@ def _compute_tval_and_indices(t, shift, closed, M, pad, tval, indices):
16861689
else:
16871690
indices[:] = (t - t_mod_1)[:, np.newaxis] + shift[np.newaxis, :] + pad
16881691

1692+
@staticmethod
1693+
def _compute_tval_and_indices_xp(t, shift, closed, M, pad, xp):
1694+
t_mod_1 = t % 1
1695+
tval = t_mod_1[:, xp.newaxis] - shift[xp.newaxis, :]
1696+
indices = xp.asarray(t - t_mod_1, dtype=int)[:, xp.newaxis] + shift[xp.newaxis, :]
1697+
indices = indices % M if closed else indices + pad
1698+
return tval, indices
1699+
16891700
def basis_matrix(self, t, derivative=0):
16901701
r"""
16911702
Computes the basis matrix :math:`\mathbf{\Phi}` as defined in :ref:`theory/data_approximation:Data approximation`.
@@ -1703,49 +1714,57 @@ def basis_matrix(self, t, derivative=0):
17031714
bm : scipy.sparse.csr_array
17041715
The basis matrix.
17051716
"""
1706-
t, single_value = self._convert_to_array(t)
1717+
xp = self._get_control_points_xp()
1718+
t, single_value = self._convert_to_array(t, xp)
17071719

1708-
minimum = np.min(t)
1709-
maximum = np.max(t)
1720+
minimum = xp.min(t)
1721+
maximum = xp.max(t)
17101722
if np.isnan(maximum):
17111723
raise ValueError("t should not contain any NaN values.")
17121724

17131725
bound = math.ceil(self.half_support)
17141726
shift = np.arange(-bound + 1, bound + 1)
17151727
n_control_points = self.M if self.closed else self.M + 2 * self.pad
17161728

1717-
tval = np.empty((len(t), len(shift)), dtype=float)
1718-
indices = np.empty((len(t), len(shift)), dtype=int)
1719-
self._compute_tval_and_indices(t, shift, self.closed, self.M, self.pad, tval, indices)
1720-
data = self.basis_function(tval, derivative=derivative)
1729+
if array_api_compat.is_numpy_namespace(xp):
1730+
tval = xp.empty((len(t), len(shift)), dtype=float)
1731+
indices = xp.empty((len(t), len(shift)), dtype=int)
1732+
self._compute_tval_and_indices_numba(t, shift, self.closed, self.M, self.pad, tval, indices)
17211733

1722-
if not self.closed and (minimum < 0 or maximum > self.M - 1):
1723-
mask = (indices >= 0) & (indices <= (self.M - 1 + 2 * self.pad))
1734+
data = self.basis_function(tval, derivative=derivative)
17241735

1725-
# Mask rows that are completely empty
1726-
row_mask = np.any(mask, axis=1)
1727-
data = data[row_mask]
1728-
indices = indices[row_mask]
1736+
if not self.closed and (minimum < 0 or maximum > self.M - 1):
1737+
mask = (indices >= 0) & (indices <= (self.M - 1 + 2 * self.pad))
17291738

1730-
mask = mask[row_mask]
1731-
data[~mask] = 0
1739+
# Mask rows that are completely empty
1740+
row_mask = np.any(mask, axis=1)
1741+
data = data[row_mask]
1742+
indices = indices[row_mask]
17321743

1733-
data = data.flatten()
1734-
indices = indices.flatten()
1744+
mask = mask[row_mask]
1745+
data[~mask] = 0
17351746

1736-
indptr = np.full(len(t) + 1, len(indices))
1737-
indptr[: len(indices) // len(shift) + 1] = np.arange(0, len(indices) + 1, len(shift))
1747+
data = data.flatten()
1748+
indices = indices.flatten()
1749+
1750+
indptr = np.full(len(t) + 1, len(indices))
1751+
indptr[: len(indices) // len(shift) + 1] = np.arange(0, len(indices) + 1, len(shift))
17381752

1753+
else:
1754+
data = data.flatten()
1755+
indices = indices.flatten()
1756+
indptr = np.arange(0, len(indices) + 1, len(shift))
1757+
1758+
# This is necessary because there is not enough padding of the control points.
1759+
# When t=M-1 shift reaches past the last padded control point.
1760+
# The seems to be the limiting factor. An alternative solution should be found.
1761+
bm = scipy.sparse.csr_array((data, indices, indptr), shape=(len(t), n_control_points + 1), copy=False)
1762+
bm = bm[:, :n_control_points]
17391763
else:
1740-
data = data.flatten()
1741-
indices = indices.flatten()
1742-
indptr = np.arange(0, len(indices) + 1, len(shift))
1764+
tval, indices = self._compute_tval_and_indices_xp(t, shift, self.closed, self.M, self.pad, xp)
1765+
1766+
basis_function_values = self.basis_function(tval, derivative=derivative)
17431767

1744-
# This is necessary because there is not enough padding of the control points.
1745-
# When t=M-1 shift reaches past the last padded control point.
1746-
# The seems to be the limiting factor. An alternative solution should be found.
1747-
bm = scipy.sparse.csr_array((data, indices, indptr), shape=(len(t), n_control_points + 1), copy=False)
1748-
bm = bm[:, :n_control_points]
17491768
return bm
17501769

17511770
def __call__(self, t, derivative=0):
@@ -1780,8 +1799,8 @@ def __call__(self, t, derivative=0):
17801799
[ 1.5, 0.5],
17811800
[ 3. , 1. ]])
17821801
"""
1783-
self._check_control_points()
1784-
t, single_value = self._convert_to_array(t)
1802+
xp = self._get_control_points_xp()
1803+
t, single_value = self._convert_to_array(t, xp)
17851804

17861805
bm = self.basis_matrix(t, derivative=derivative)
17871806

@@ -1891,7 +1910,7 @@ def derivative_of_curvature_energy_wrt_control_points(self):
18911910
"""
18921911
return 2 * np.einsum("mx,lm->lx", self.control_points, self._h3)
18931912

1894-
def _convert_to_array(self, t):
1913+
def _convert_to_array(self, t, xp):
18951914
"""
18961915
Helper function that converts a function input
18971916
to an array. This allows functions to accept int and float
@@ -1902,13 +1921,19 @@ def _convert_to_array(self, t):
19021921
"""
19031922
single_value = False
19041923
if not isinstance(t, collections.abc.Iterable):
1905-
t = np.array([t])
1924+
t = xp.asarray([t])
19061925
single_value = True
1907-
elif isinstance(t, np.ndarray) and t.shape == ():
1926+
elif not array_api_compat.is_array_api_obj(t):
1927+
t = xp.asarray(t)
1928+
elif array_api_compat.array_namespace(t) is not xp:
1929+
raise ValueError(
1930+
f"The control points are a {xp.__name__.split('.')[-1]} array but "
1931+
"the parameter values you provided are a "
1932+
f"{array_api_compat.array_namespace(t).__name__.split('.')[-1]} array."
1933+
)
1934+
elif t.shape == ():
19081935
# Array with only one element, e.g. np.array(0.)
1909-
t = np.array([t.item()])
1910-
elif not isinstance(t, np.ndarray):
1911-
t = np.array(t)
1936+
t = xp.asarray([t.item()])
19121937
if t.ndim > 1:
19131938
raise ValueError("The parameter array has to be 1D.")
19141939
return t, single_value
@@ -1919,7 +1944,7 @@ def _control_points_centroid(self):
19191944
and :meth:`splinebox.spline_curves.Spline.rotate`.
19201945
Computes the centroid of the coefficients.
19211946
"""
1922-
self._check_control_points()
1947+
self._get_control_points_xp()
19231948
return np.mean(self.control_points, axis=0)
19241949

19251950
def translate(self, vector):
@@ -1957,7 +1982,7 @@ def translate(self, vector):
19571982
>>> plt.legend() # doctest: +SKIP
19581983
>>> plt.show() # doctest: +SKIP
19591984
"""
1960-
self._check_control_points()
1985+
self._get_control_points_xp()
19611986
self.control_points = self.control_points + vector
19621987

19631988
def scale(self, scaling_factor):
@@ -1995,7 +2020,7 @@ def scale(self, scaling_factor):
19952020
>>> plt.legend() # doctest: +SKIP
19962021
>>> plt.show() # doctest: +SKIP
19972022
"""
1998-
self._check_control_points()
2023+
self._get_control_points_xp()
19992024
centroid = self._control_points_centroid()
20002025
self.translate(-centroid)
20012026
self.control_points *= scaling_factor
@@ -2038,7 +2063,7 @@ def rotate(self, rotation_matrix, centred=True):
20382063
>>> plt.legend() # doctest: +SKIP
20392064
>>> plt.show() # doctest: +SKIP
20402065
"""
2041-
self._check_control_points()
2066+
self._get_control_points_xp()
20422067
if self.ndim == 1:
20432068
raise RuntimeError("1D splines can not be rotated.")
20442069

@@ -2104,7 +2129,7 @@ def distance(self, points, return_t=False):
21042129
>>> plt.gca().set_aspect("equal") # doctest: +SKIP
21052130
>>> plt.show() # doctest: +SKIP
21062131
"""
2107-
self._check_control_points()
2132+
self._get_control_points_xp()
21082133
if self.ndim == 1:
21092134
raise RuntimeError("Cannot compute distance for 1D splines.")
21102135

@@ -2302,7 +2327,7 @@ def mesh(
23022327
>>> connectivity.shape
23032328
(6480, 4)
23042329
"""
2305-
self._check_control_points()
2330+
self._get_control_points_xp()
23062331
if self.ndim != 3:
23072332
raise NotImplementedError("Meshes are only implemented for splines in 3D.")
23082333
cap_ends = self._normalize_cap_ends(cap_ends) if mesh_type == "surface" and not self.closed else None
@@ -2575,7 +2600,7 @@ def _check_control_points_and_tangents(self):
25752600
can be used. This helper function checks if control points and tangents have been
25762601
set.
25772602
"""
2578-
self._check_control_points()
2603+
self._get_control_points_xp()
25792604
if self.tangents is None:
25802605
raise RuntimeError(self._no_tangents_msg)
25812606

0 commit comments

Comments
 (0)