99import math
1010import warnings
1111
12+ import array_api_compat
1213import numba
1314import numpy as np
1415import 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