2626import functools
2727from itertools import zip_longest as zipl
2828from .types import ArrayLike , MatrixLike , VectorLike , Array , Matrix , Vector , SupportsFloatOrInt
29- from typing import Optional , Callable , Sequence , List , Union , Iterator , Tuple , Any , Iterable , overload , cast
29+ from typing import Optional , Callable , Sequence , List , Union , Iterator , Tuple , Any , Iterable , overload , Dict , cast
3030
3131NaN = float ('nan' )
3232INF = float ('inf' )
@@ -41,7 +41,7 @@ def prod(values: Iterable[SupportsFloatOrInt]) -> SupportsFloatOrInt:
4141 if not values :
4242 return 1
4343
44- return functools .reduce (lambda x , y : x * y , values )
44+ return functools .reduce (operator . mul , values )
4545
4646# Shortcut for math operations
4747# Specify one of these in divide, multiply, dot, etc.
@@ -66,6 +66,9 @@ def prod(values: Iterable[SupportsFloatOrInt]) -> SupportsFloatOrInt:
6666D2_D1 = (2 , 1 )
6767DN_DM = (3 , 3 )
6868
69+ # Vector used to create a special matrix used in natural splines
70+ M141 = [1 , 4 , 1 ]
71+
6972
7073################################
7174# General math
@@ -155,10 +158,257 @@ def npow(base: float, exp: float) -> float:
155158 return math .copysign (abs (base ) ** exp , base )
156159
157160
158- def lerp (a : float , b : float , t : float ) -> float :
161+ ################################
162+ # Interpolation and splines
163+ ################################
164+ def lerp (p0 : float , p1 : float , t : float ) -> float :
159165 """Linear interpolation."""
160166
161- return a + (b - a ) * t
167+ return p0 + (p1 - p0 ) * t
168+
169+
170+ @functools .lru_cache (maxsize = 10 )
171+ def _matrix_141 (n : int ) -> Matrix :
172+ """Get matrix '1 4 1'."""
173+
174+ m = [[0 ] * n for _ in range (n )] # type: Matrix
175+ m [0 ][0 :2 ] = M141 [1 :]
176+ m [- 1 ][- 2 :] = M141 [:- 1 ]
177+ for x in range (n - 2 ):
178+ m [x + 1 ][x :x + 3 ] = M141
179+ return inv (m )
180+
181+
182+ def naturalize_bspline_controls (coordinates : List [Vector ]) -> None :
183+ """
184+ Given a set of B-spline control points in the Nth dimension, create new naturalized interpolation control points.
185+
186+ Using the color points as `S0...Sn`, calculate `B0...Bn`, such that interpolation will
187+ pass through `S0...Sn`.
188+
189+ When given 2 data points, the operation will be returned as linear, so there is nothing to do.
190+ """
191+
192+ n = len (coordinates ) - 2
193+
194+ # Special case 3 data points
195+ if n == 1 :
196+ coordinates [1 ] = [
197+ (a * 6 - (b + c )) / 4 for a , b , c in zip (coordinates [1 ], coordinates [0 ], coordinates [2 ])
198+ ]
199+
200+ # Handle all other cases where n does not result in linear interpolation
201+ elif n > 1 :
202+ # Create [1, 4, 1] matrix for size `n` set of control points
203+ m = _matrix_141 (n )
204+
205+ # Create C matrix from the data points
206+ c = []
207+ for r in range (1 , n + 1 ):
208+ if r == 1 :
209+ c .append ([a * 6 - b for a , b in zip (coordinates [r ], coordinates [r - 1 ])])
210+ elif r == n :
211+ c .append ([a * 6 - b for a , b in zip (coordinates [n ], coordinates [n + 1 ])])
212+ else :
213+ c .append ([a * 6 for a in coordinates [r ]])
214+
215+ # Dot M^-1 and C to get B (control points)
216+ v = dot (m , c , dims = D2 )
217+ for r in range (1 , n + 1 ):
218+ coordinates [r ] = v [r - 1 ]
219+
220+
221+ def bspline (p0 : float , p1 : float , p2 : float , p3 : float , t : float ) -> float :
222+ """Calculate the new point using the provided values."""
223+
224+ # Save some time calculating this once
225+ t2 = t ** 2
226+ t3 = t2 * t
227+
228+ # Insert control points to algorithm
229+ return (
230+ ((1 - t ) ** 3 ) * p0 + # B0
231+ (3 * t3 - 6 * t2 + 4 ) * p1 + # B1
232+ (- 3 * t3 + 3 * t2 + 3 * t + 1 ) * p2 + # B2
233+ t3 * p3 # B3
234+ ) / 6
235+
236+
237+ def catrom (p0 : float , p1 : float , p2 : float , p3 : float , t : float ) -> float :
238+ """Calculate the new point using the provided values."""
239+
240+ # Save some time calculating this once
241+ t2 = t ** 2
242+ t3 = t2 * t
243+
244+ # Insert control points to algorithm
245+ return (
246+ (- t3 + 2 * t2 - t ) * p0 + # B0
247+ (3 * t3 - 5 * t2 + 2 ) * p1 + # B1
248+ (- 3 * t3 + 4 * t2 + t ) * p2 + # B2
249+ (t3 - t2 ) * p3 # B3
250+ ) / 2
251+
252+
253+ def monotone (p0 : float , p1 : float , p2 : float , p3 : float , t : float ) -> float :
254+ """
255+ Monotone spline based on Hermite.
256+
257+ We calculate our secants for our four samples (the center pair being our interpolation target).
258+
259+ From those, we calculate an initial gradient, and test to see if it is needed. In the event
260+ that our there is no increase or decrease between the point, we can infer that the gradient
261+ should be horizontal. We also test if they have opposing signs, if so, we also consider the
262+ gradient to be zero.
263+
264+ Lastly, we ensure that the gradient is confined within a circle with radius 3 as it has been
265+ observed that such a circle encapsulates the entire monotonicity region.
266+
267+ Once gradients are calculated, we simply perform the Hermite spline calculation and clean up
268+ floating point math errors to ensure monotonicity.
269+
270+ We could build up secant and gradient info ahead of time, but currently we do it on the fly.
271+
272+ http://jbrd.github.io/2020/12/27/monotone-cubic-interpolation.html
273+ https://ui.adsabs.harvard.edu/abs/1990A%26A...239..443S/abstract
274+ https://citeseerx.ist.psu.edu/viewdoc/summary?doi=10.1.1.39.6720
275+ https://en.wikipedia.org/w/index.php?title=Monotone_cubic_interpolation&oldid=950478742
276+ """
277+
278+ # Save some time calculating this once
279+ t2 = t ** 2
280+ t3 = t2 * t
281+
282+ # Calculate the secants for the differing segments
283+ s0 = p1 - p0
284+ s1 = p2 - p1
285+ s2 = p3 - p2
286+
287+ # Calculate initial gradients
288+ m1 = (s0 + s1 ) * 0.5
289+ m2 = (s1 + s2 ) * 0.5
290+
291+ # Center segment should be horizontal as there is no increase/decrease between the two points
292+ if math .isclose (p1 , p2 ):
293+ m1 = m2 = 0.0
294+ else :
295+
296+ # Gradient is zero if segment is horizontal or if the left hand secant differs in sign from current.
297+ if math .isclose (p0 , p1 ) or (math .copysign (1.0 , s0 ) != math .copysign (1.0 , s1 )):
298+ m1 = 0.0
299+
300+ # Ensure gradient magnitude is either 3 times the left or current secant (smaller being preferred).
301+ else :
302+ m1 *= min (3.0 * s0 / m1 , min (3.0 * s1 / m1 , 1.0 ))
303+
304+ # Gradient is zero if segment is horizontal or if the right hand secant differs in sign from current.
305+ if math .isclose (p2 , p3 ) or (math .copysign (1.0 , s1 ) != math .copysign (1.0 , s2 )):
306+ m2 = 0.0
307+
308+ # Ensure gradient magnitude is either 3 times the current or right secant (smaller being preferred).
309+ else :
310+ m2 *= min (3.0 * s1 / m2 , min (3.0 * s2 / m2 , 1.0 ))
311+
312+ # Now we can evaluate the Hermite spline
313+ result = (
314+ (m1 + m2 - 2 * s1 ) * t3 +
315+ (3.0 * s1 - 2.0 * m1 - m2 ) * t2 +
316+ m1 * t +
317+ p1
318+ )
319+
320+ # As the spline is monotonic, all interpolated values should be confined between the endpoints.
321+ # Floating point arithmetic can cause this to be out of bounds on occasions.
322+ mn = min (p1 , p2 )
323+ mx = max (p1 , p2 )
324+ return min (max (result , mn ), mx )
325+
326+
327+ SPLINES = {
328+ 'natural' : bspline ,
329+ 'bspline' : bspline ,
330+ 'catrom' : catrom ,
331+ 'monotone' : monotone ,
332+ 'linear' : lerp
333+ } # type: Dict[str, Callable[..., float]]
334+
335+
336+ class Interpolate :
337+ """Interpolation object."""
338+
339+ def __init__ (
340+ self ,
341+ points : List [Vector ],
342+ callback : Callable [..., float ],
343+ length : int ,
344+ linear : bool = False
345+ ) -> None :
346+ """Initialize."""
347+
348+ self .length = length
349+ self .num_coords = len (points [0 ])
350+ self .points = list (zip (* points ))
351+ self .callback = callback
352+ self .linear = linear
353+
354+ def steps (self , count : int ) -> List [Vector ]:
355+ """Generate steps."""
356+
357+ divisor = count - 1
358+ return [self (r / divisor ) for r in range (0 , count )]
359+
360+ def __call__ (self , t : float ) -> Vector :
361+ """Interpolate."""
362+
363+ n = self .length - 1
364+ i = max (min (math .floor (t * n ), n - 1 ), 0 )
365+ t = (t - i / n ) * n if 0 <= t <= 1 else t
366+ if not self .linear :
367+ i += 1
368+
369+ # Iterate the coordinates and apply the spline to each component
370+ # returning the completed, interpolated coordinate set.
371+ coord = []
372+ for idx in range (self .num_coords ):
373+ c = self .points [idx ]
374+ if self .linear or t < 0 or t > 1 :
375+ coord .append (lerp (c [i ], c [i + 1 ], t ))
376+ else :
377+ coord .append (
378+ self .callback (
379+ c [i - 1 ],
380+ c [i ],
381+ c [i + 1 ],
382+ c [i + 2 ],
383+ t
384+ )
385+ )
386+
387+ return coord
388+
389+
390+ def interpolate (points : List [Vector ], method : str = 'linear' ) -> Interpolate :
391+ """Generic interpolation method."""
392+
393+ points = points [:]
394+ length = len (points )
395+
396+ # Natural requires some preprocessing of the B-spline points.
397+ if method == 'natural' :
398+ naturalize_bspline_controls (points )
399+
400+ # Get the spline method
401+ s = SPLINES [method ]
402+ linear = method == 'linear'
403+
404+ # Clamp end points
405+ if not linear :
406+ start = [2 * a - b for a , b in zip (points [0 ], points [1 ])]
407+ end = [2 * a - b for a , b in zip (points [- 1 ], points [- 2 ])]
408+ points .insert (0 , start )
409+ points .append (end )
410+
411+ return Interpolate (points , s , length , linear )
162412
163413
164414################################
@@ -167,7 +417,10 @@ def lerp(a: float, b: float, t: float) -> float:
167417def vdot (a : VectorLike , b : VectorLike ) -> float :
168418 """Dot two vectors."""
169419
170- return sum ([x * y for x , y in zipl (a , b )])
420+ s = 0.0
421+ for x , y in zipl (a , b ):
422+ s += x * y
423+ return s
171424
172425
173426def vcross (v1 : VectorLike , v2 : VectorLike ) -> Vector : # pragma: no cover
@@ -967,9 +1220,8 @@ class BroadcastTo:
9671220 By flattening the data, we are able to slice out the bits we need in the order we need
9681221 and duplicate them to expand the matrix to fit the provided shape.
9691222
970- We need 4 things to do this:
1223+ We need 3 things to do this:
9711224 - The original array.
972- - The original array shape.
9731225 - The stage 1 array shape (with prepended 1s). This helps us calculate our loop iterations.
9741226 - The new shape.
9751227 """
@@ -1059,8 +1311,8 @@ def __next__(self) -> float:
10591311 self ._loop2 = self .expand
10601312
10611313 if self ._chunk_index >= self ._chunk_max :
1062- # We are actually at then of all the data, let's see
1063- # if we need to process all the data again.
1314+ # We are actually at the end of all the data,
1315+ # let's see if we need to process all the data again.
10641316 self ._loop1 -= 1
10651317 if self ._loop1 :
10661318 # We need to keep going
0 commit comments