-
Notifications
You must be signed in to change notification settings - Fork 19
Expand file tree
/
Copy pathChebyshevSubdivisionSolver.py
More file actions
1464 lines (1328 loc) · 63.1 KB
/
Copy pathChebyshevSubdivisionSolver.py
File metadata and controls
1464 lines (1328 loc) · 63.1 KB
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
730
731
732
733
734
735
736
737
738
739
740
741
742
743
744
745
746
747
748
749
750
751
752
753
754
755
756
757
758
759
760
761
762
763
764
765
766
767
768
769
770
771
772
773
774
775
776
777
778
779
780
781
782
783
784
785
786
787
788
789
790
791
792
793
794
795
796
797
798
799
800
801
802
803
804
805
806
807
808
809
810
811
812
813
814
815
816
817
818
819
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
836
837
838
839
840
841
842
843
844
845
846
847
848
849
850
851
852
853
854
855
856
857
858
859
860
861
862
863
864
865
866
867
868
869
870
871
872
873
874
875
876
877
878
879
880
881
882
883
884
885
886
887
888
889
890
891
892
893
894
895
896
897
898
899
900
901
902
903
904
905
906
907
908
909
910
911
912
913
914
915
916
917
918
919
920
921
922
923
924
925
926
927
928
929
930
931
932
933
934
935
936
937
938
939
940
941
942
943
944
945
946
947
948
949
950
951
952
953
954
955
956
957
958
959
960
961
962
963
964
965
966
967
968
969
970
971
972
973
974
975
976
977
978
979
980
981
982
983
984
985
986
987
988
989
990
991
992
993
994
995
996
997
998
999
1000
import numpy as np
from numba import njit, float64
from numba.types import UniTuple
from itertools import product
from scipy.spatial import HalfspaceIntersection, QhullError
from scipy.optimize import linprog
from yroots.QuadraticCheck import quadratic_check
from time import time
import copy
import warnings
class SolverOptions():
"""Settings for running interval checks, transformations, and subdivision in solvePolyRecursive.
Parameters
----------
verbose : bool
Defaults to False. Whether or not to output progress of solving to the terminal.
exact : bool
Defaults to False. Whether the transformation in TransformChebInPlaceND should minimize error.
constant_check : bool
Defaults to True. Whether or not to run constant term check after each subdivision.
low_dim_quadratic_check : bool
Defaults to True. Whether or not to run quadratic check in dim 2, 3.
all_dim_quadratic_check : bool
Defaults to False. Whether or not to run quadratic check in dim >= 4.
maxZoomCount : int
Maximum number of zooms allowed before subdividing (prevents infinite infintesimal shrinking)
level : int
Depth of subdivision for the given interval.
"""
def __init__(self):
#Init all the Options to default value
self.verbose = False
self.exact = False
self.constant_check = True
self.low_dim_quadratic_check = True
self.all_dim_quadratic_check = False
self.maxZoomCount = 25
self.level = 0
def copy(self):
return copy.copy(self) #Return shallow copy, everything should be a basic type
@njit
def TransformChebInPlace1D(coeffs, alpha, beta):
"""Applies the transformation alpha*x + beta to one dimension of a Chebyshev approximation.
Recursively finds each column of the transformation matrix C from the previous two columns
and then performs entrywise matrix multiplication for each entry of the column, thus enabling
the transformation to occur while only retaining three columns of C in memory at a time.
Parameters
----------
coeffs : numpy array
The coefficient array
alpha : double
The scaler of the transformation
beta : double
The shifting of the transformation
Returns
-------
transformedCoeffs : numpy array
The new coefficient array following the transformation
"""
transformedCoeffs = np.zeros_like(coeffs)
#Initialize three arrays to represent subsequent columns of the transformation matrix.
arr1 = np.zeros(len(coeffs))
arr2 = np.zeros(len(coeffs))
arr3 = np.zeros(len(coeffs))
#The first column of the transformation matrix C. Since T_0(alpha*x + beta) = T_0(x) = 1 has 1 in the top entry and 0's elsewhere.
arr1[0] = 1.
transformedCoeffs[0] = coeffs[0] # arr1[0] * coeffs[0] (matrix multiplication step)
#The second column of C. Note that T_1(alpha*x + beta) = alpha*T_1(x) + beta*T_0(x).
arr2[0] = beta
arr2[1] = alpha
transformedCoeffs[0] += beta * coeffs[1] # arr2[0] * coeffs[1] (matrix muliplication)
transformedCoeffs[1] += alpha * coeffs[1] # arr2[1] * coeffs[1] (matrix multiplication)
maxRow = 2
for col in range(2, len(coeffs)): # For each column, calculate each entry and do matrix mult
thisCoeff = coeffs[col] # the row of coeffs corresponding to the column col of C (for matrix mult)
# The first entry
arr3[0] = -arr1[0] + alpha*arr2[1] + 2*beta*arr2[0]
transformedCoeffs[0] += thisCoeff * arr3[0]
# The second entry
if maxRow > 2:
arr3[1] = -arr1[1] + alpha*(2*arr2[0] + arr2[2]) + 2*beta*arr2[1]
transformedCoeffs[1] += thisCoeff * arr3[1]
# All middle entries
for i in range(2, maxRow - 1):
arr3[i] = -arr1[i] + alpha*(arr2[i-1] + arr2[i+1]) + 2*beta*arr2[i]
transformedCoeffs[i] += thisCoeff * arr3[i]
# The second to last entry
i = maxRow - 1
arr3[i] = -arr1[i] + (2 if i == 1 else 1)*alpha*(arr2[i-1]) + 2*beta*arr2[i]
transformedCoeffs[i] += thisCoeff * arr3[i]
#The last entry
finalVal = alpha*arr2[i]
# This final entry is typically very small. If it is essentially machine epsilon,
# zero it out to save calculations.
if abs(finalVal) > 1e-16: #TODO: Justify this val!
arr3[maxRow] = finalVal
transformedCoeffs[maxRow] += thisCoeff * finalVal
maxRow += 1 # Next column will have one more entry than the current column.
# Save the values of arr2 and arr3 to arr1 and arr2 to get ready for calculating the next column.
arr = arr1
arr1 = arr2
arr2 = arr3
arr3 = arr
#
return transformedCoeffs[:maxRow]
@njit
def TransformChebInPlace1DErrorFree(coeffs, alpha, beta):
"""Applies the transformation alpha*x + beta to the Chebyshev polynomial coeffs with minimal error.
This function is identical to TransformChebInPlace1D except that this function is more careful to
minimize error by calling on functions to more precisely perform the multiplication and addition.
Parameters
----------
coeffs : numpy array
The coefficient array
alpha : double
The scaler of the transformation
beta : double
The shifting of the transformation
Returns
-------
coeffs : numpy array
The new coefficient array following the transformation
"""
if alpha == 0.5 and abs(beta) == 0.5:
return TransformChebInPlace1DErrorFreeSplit(coeffs, np.sign(beta))
transformedCoeffs = np.zeros_like(coeffs)
arr1 = np.zeros(len(coeffs))
arr2 = np.zeros(len(coeffs))
arr3 = np.zeros(len(coeffs))
arr1E = np.zeros(len(coeffs))
arr2E = np.zeros(len(coeffs))
arr3E = np.zeros(len(coeffs))
alpha1,alpha2 = Split(alpha)
beta1,beta2 = Split(beta)
#The first array
arr1[0] = 1.
transformedCoeffs[0] = coeffs[0]
#The second array
arr2[0] = beta
arr2[1] = alpha
transformedCoeffs[0] += beta * coeffs[1]
transformedCoeffs[1] += alpha * coeffs[1]
#Loop
maxRow = 2
for col in range(2, len(coeffs)):
thisCoeff = coeffs[col]
#Get the next arr from arr1 and arr2
#The 0 spot
# Calculate and store arr3[0] = -arr1[0] + alpha*arr2[1] + 2*beta*arr2[0]
V1, E1 = TwoProdWithSplit(beta, 2*arr2[0], beta1, beta2)
V2, E2 = TwoProdWithSplit(alpha, arr2[1], alpha1, alpha2)
V3, E3 = TwoSum(V1, V2)
V4, E4 = TwoSum(V3, -arr1[0])
arr3[0] = V4
# Now sum the error associated with this calculation and add it to the calculated value,
# then perform the matrix multiplication associated with this entry.
arr3E[0] = -arr1E[0] + alpha*arr2E[1] + 2*beta*arr2E[0] + E1 + E2 + E3 + E4
transformedCoeffs[0] += thisCoeff * (arr3[0] + arr3E[0])
# The procedure associated with minimizing error is the same for subsequent spots.
#The 1 spot
if maxRow > 2:
#arr3[1] = -arr1[1] + alpha*(2*arr2[0] + arr2[2]) + 2*beta*arr2[1]
V1, E1 = TwoSum(2*arr2[0], arr2[2])
V2, E2 = TwoProdWithSplit(beta, 2*arr2[1], beta1, beta2)
V3, E3 = TwoProdWithSplit(alpha, V1, alpha1, alpha2)
V4, E4 = TwoSum(V2, V3)
V5, E5 = TwoSum(V4, -arr1[1])
arr3[1] = V5
arr3E[1] = -arr1E[1] + alpha*(2*arr2E[0] + arr2E[2] + E1) + 2*beta*arr2E[1] + E2 + E3 + E4 + E5
transformedCoeffs[1] += thisCoeff * (arr3[1] + arr3E[1])
#The middle spots
for i in range(2, maxRow - 1):
#arr3[i] = -arr1[i] + alpha*(arr2[i-1] + arr2[i+1]) + 2*beta*arr2[i]
V1, E1 = TwoSum(arr2[i-1], arr2[i+1])
V2, E2 = TwoProdWithSplit(beta, 2*arr2[i], beta1, beta2)
V3, E3 = TwoProdWithSplit(alpha, V1, alpha1, alpha2)
V4, E4 = TwoSum(V2, V3)
V5, E5 = TwoSum(V4, -arr1[i])
arr3[i] = V5
arr3E[i] = -arr1E[i] + alpha*(arr2E[i-1] + arr2E[i+1] + E1) + 2*beta*arr2E[i] + E2 + E3 + E4 + E5
transformedCoeffs[i] += thisCoeff * (arr3[i] + arr3E[i])
#The second to last spot
i = maxRow - 1
C1 = (2 if i == 1 else 1)
#arr3[i] = -arr1[i] + C1*alpha*(arr2[i-1]) + 2*beta*arr2[i]
V1, E1 = TwoProdWithSplit(beta, 2*arr2[i], beta1, beta2)
V2, E2 = TwoProdWithSplit(alpha, C1*arr2[i-1], alpha1, alpha2)
V3, E3 = TwoSum(V1, V2)
V4, E4 = TwoSum(V3, -arr1[i])
arr3[i] = V4
arr3E[i] = -arr1E[i] + C1*alpha*arr2E[i-1] + 2*beta*arr2E[i] + E1 + E2 + E3 + E4
transformedCoeffs[i] += thisCoeff * (arr3[i] + arr3E[i])
#The last spot
#finalVal = alpha*arr2[i]
finalVal, finalValE = TwoProdWithSplit(alpha, arr2[i], alpha1, alpha2)
arr3E[maxRow] = finalValE + alpha * arr2E[i]
arr3[maxRow] = finalVal
transformedCoeffs[maxRow] += thisCoeff * (arr3[maxRow] + arr3E[maxRow])
if abs(arr3[maxRow] + arr3E[maxRow]) > 1e-32: #TODO: Justify this val!
maxRow += 1
#Rotate the vectors
arr = arr1
arr1 = arr2
arr2 = arr3
arr3 = arr
arr = arr1E
arr1E = arr2E
arr2E = arr3E
arr3E = arr
return transformedCoeffs[:maxRow]
@njit
def TransformChebInPlace1DErrorFreeSplit(coeffs, betaSign):
"""Applies the transformation 0.5*x +- 0.5 to the Chebyshev polynomial coeffs with minimal error.
This function is a special case of TransformChebInPlace1DErrorFree used to minimize computation
when alpha = 0.5 and beta = +- 0.5
Parameters
----------
coeffs : numpy array
The coefficient array
betaSign : int
1 if beta = 0.5; -1 if beta is -0.5
Returns
-------
coeffs : numpy array
The new coefficient array following the transformation
"""
transformedCoeffs = np.zeros_like(coeffs)
arr1 = np.zeros(len(coeffs))
arr2 = np.zeros(len(coeffs))
arr3 = np.zeros(len(coeffs))
arr1E = np.zeros(len(coeffs))
arr2E = np.zeros(len(coeffs))
arr3E = np.zeros(len(coeffs))
#The first array
arr1[0] = 1.
transformedCoeffs[0] = coeffs[0]
#The second array
arr2[0] = betaSign*0.5
arr2[1] = 0.5
transformedCoeffs[0] += betaSign*coeffs[1]/2
transformedCoeffs[1] += coeffs[1]/2
#Loop
maxRow = 2
for col in range(2, len(coeffs)):
thisCoeff = coeffs[col]
#Get the next arr from arr1 and arr2
#The 0 spot
#arr3[0] = -arr1[0] + alpha*arr2[1] + 2*beta*arr2[0]
V1, E1 = TwoSum(arr2[1]/2, betaSign*arr2[0])
V2, E2 = TwoSum(V1, -arr1[0])
arr3[0] = V2
arr3E[0] = -arr1E[0] + arr2E[1]/2 + betaSign*arr2E[0] + E1 + E2
transformedCoeffs[0] += thisCoeff * (arr3[0] + arr3E[0])
#The 1 spot
if maxRow > 2:
#arr3[1] = -arr1[1] + alpha*(2*arr2[0] + arr2[2]) + 2*beta*arr2[1]
V1, E1 = TwoSum(arr2[0], arr2[2]/2)
V2, E2 = TwoSum(V1, betaSign*arr2[1])
V3, E3 = TwoSum(V2, -arr1[1])
arr3[1] = V3
arr3E[1] = -arr1E[1] + arr2E[0] + arr2E[2]/2 + betaSign*arr2E[1] + E1 + E2 + E3
transformedCoeffs[1] += thisCoeff * (arr3[1] + arr3E[1])
#The middle spots
for i in range(2, maxRow - 1):
#arr3[i] = -arr1[i] + alpha*(arr2[i-1] + arr2[i+1]) + 2*beta*arr2[i]
V1, E1 = TwoSum(arr2[i-1], arr2[i+1])
V2, E2 = TwoSum(V1/2, betaSign*arr2[i])
V3, E3 = TwoSum(V2, -arr1[i])
arr3[i] = V3
arr3E[i] = -arr1E[i] + (arr2E[i-1] + arr2E[i+1] + E1)/2 + betaSign*arr2E[i] + E2 + E3
transformedCoeffs[i] += thisCoeff * (arr3[i] + arr3E[i])
#The second to last spot
i = maxRow - 1
C1 = (1 if i == 1 else 0.5)
#arr3[i] = -arr1[i] + C1*alpha*(arr2[i-1]) + 2*beta*arr2[i]
V1, E1 = TwoSum(C1*arr2[i-1], betaSign*arr2[i])
V2, E2 = TwoSum(V1, -arr1[i])
arr3[i] = V2
arr3E[i] = -arr1E[i] + C1*arr2E[i-1] + betaSign*arr2E[i] + E1 + E2
transformedCoeffs[i] += thisCoeff * (arr3[i] + arr3E[i])
#The last spot
#finalVal = alpha*arr2[i]
arr3[maxRow] = arr2[i]/2
arr3E[maxRow] = arr2E[i] / 2
transformedCoeffs[maxRow] += thisCoeff * (arr3[maxRow] + arr3E[maxRow])
if abs(arr3[maxRow] + arr3E[maxRow]) > 1e-32: #TODO: Justify this val!
maxRow += 1
#Rotate the vectors
arr = arr1
arr1 = arr2
arr2 = arr3
arr3 = arr
arr = arr1E
arr1E = arr2E
arr2E = arr3E
arr3E = arr
return transformedCoeffs[:maxRow]
def TransformChebInPlaceND(coeffs, dim, alpha, beta, exact):
"""Transforms a single dimension of a Chebyshev approximation for a polynomial.
Parameters
----------
coeffs : numpy array
The coefficient tensor to transform
dim : int
The index of the dimension to transform
alpha: double
The scaler of the transformation
beta: double
The shifting of the transformation
exact: bool
Whether to perform the transformation with higher precision to minimize error
Returns
-------
transformedCoeffs : numpy array
The new coefficient array following the transformation
"""
#TODO: Could we calculate the allowed error beforehand and pass it in here?
#TODO: Make this work for the power basis polynomials
if (alpha == 1.0 and beta == 0.0) or coeffs.shape[dim] == 1:
return coeffs # No need to transform if the degree of dim is 0 or transformation is the identity.
TransformFunc = TransformChebInPlace1DErrorFree if exact else TransformChebInPlace1D
if dim == 0:
return TransformFunc(coeffs, alpha, beta)
else: # Need to transpose the matrix to line up the multiplication for the current dim
# Move the current dimension to the dim 0 spot in the np array.
order = np.array([dim] + [i for i in range(dim)] + [i for i in range(dim+1, coeffs.ndim)])
# Then transpose with the inverted order after the transformation occurs.
backOrder = np.zeros(coeffs.ndim, dtype = int)
backOrder[order] = np.arange(coeffs.ndim)
return TransformFunc(coeffs.transpose(order), alpha, beta).transpose(backOrder)
class TrackedInterval:
"""Tracks the properties of and changes to each interval as it passes through the solver.
Parameters
----------
topInterval: numpy array
The original interval before any changes
interval: numpy array
The current interval (lower bound and upper bound for each dimension in order)
transforms: list
List of the alpha and beta values for all the transformations the interval has undergone
ndim: int
The number of dimensions of which the interval consists
empty: bool
Whether the interval is known to contain no roots
finalStep: bool
Whether the interval is in the final step (zooming in on the bounding box to a point at the end)
canThrowOutFinalStep: bool
Defaults to False. Whether or not the interval should be thrown out if empty in the final step
of solving. Changed to True if subdivision occurs in the final step.
possibleDuplicateRoots: list
Any multiple roots found through subdivision in the final step that would have been
returned as just one root before the final step
possibleExtraRoot: bool
Defaults to False. Whether or not the interval would have been thrown out during the final step.
nextTransformPoints: numpy array
Where the midpoint of the next subdivision should be for each dimension
"""
def __init__(self, interval):
self.topInterval = interval
self.interval = interval
self.transforms = []
self.ndim = len(self.interval)
self.empty = False
self.finalStep = False
self.canThrowOutFinalStep = False
self.possibleDuplicateRoots = []
self.possibleExtraRoot = False
self.nextTransformPoints = np.array([0.0394555475981047]*self.ndim) #Random Point near 0
def canThrowOut(self):
"""Ensures that an interval that has not subdivided cannot be thrown out on the final step."""
return not self.finalStep or self.canThrowOutFinalStep
def addTransform(self, subInterval):
"""Adds the next alpha and beta values to the list transforms and updates the current interval.
Parameters:
-----------
subInterval : numpy array
The subinterval to which the current interval is being reduced
"""
#Ensure the interval has non zero size; mark it empty if it doesn't
if np.any(subInterval[:,0] > subInterval[:,1]) and self.canThrowOut():
self.empty = True
return
elif np.any(subInterval[:,0] > subInterval[:,1]):
#If we can't throw the interval out, it should be bounded by [-1,1].
subInterval[:,0] = np.minimum(subInterval[:,0], np.ones_like(subInterval[:,0]))
subInterval[:,0] = np.maximum(subInterval[:,0], -np.ones_like(subInterval[:,0]))
subInterval[:,1] = np.minimum(subInterval[:,1], np.ones_like(subInterval[:,0]))
subInterval[:,1] = np.maximum(subInterval[:,1], subInterval[:,0])
# Get the alpha and beta associated with the transformation in each dimension
a1,b1 = subInterval.T # all the lower bounds and upper bounds of the new interval, respectively
a2,b2 = self.interval.T # all the lower bounds and upper bounds of the original interval
alpha1, beta1 = (b1-a1)/2, (b1+a1)/2
alpha2, beta2 = (b2-a2)/2, (b2+a2)/2
self.transforms.append(np.array([alpha1, beta1]))
#Update the lower and upper bounds of the current interval
for dim in range(self.ndim):
for i in range(2):
x = subInterval[dim][i]
#Be exact if x = +-1
if x == -1.0:
self.interval[dim][i] = self.interval[dim][0]
elif x == 1.0:
self.interval[dim][i] = self.interval[dim][1]
else:
self.interval[dim][i] = alpha2[dim]*x+beta2[dim]
def getLastTransform(self):
"""Gets the alpha and beta values of the last transformation the interval underwent."""
return self.transforms[-1]
def getFinalInterval(self):
"""Finds the interval that should be reported as containing a root.
The final interval is calculated by applying all of the recorded transformations that
occurred before the final step to topInterval, the original interval.
Returns
-------
finalInterval: numpy array
The final interval to be reported as containing a root
"""
# TODO: Make this a seperate function so it can use njit.
# Make these _NoNumba calls use floats so they call call the numba functions without a seperate compile
finalInterval = self.topInterval.T
finalIntervalError = np.zeros_like(finalInterval)
transformsToUse = self.transforms if not self.finalStep else self.preFinalTransforms
for alpha,beta in transformsToUse[::-1]: # Iteratively apply each saved transform
finalInterval, temp = TwoProd_NoNumba(finalInterval, alpha)
finalIntervalError = alpha * finalIntervalError + temp
finalInterval, temp = TwoSum_NoNumba(finalInterval,beta)
finalIntervalError += temp
finalInterval = finalInterval.T
finalIntervalError = finalIntervalError.T
self.finalInterval = finalInterval + finalIntervalError # Add the error and save the result.
self.finalAlpha, alphaError = TwoSum_NoNumba(-finalInterval[:,0]/2,finalInterval[:,1]/2)
self.finalAlpha += alphaError + (finalIntervalError[:,1] - finalIntervalError[:,0])/2
self.finalBeta, betaError = TwoSum_NoNumba(finalInterval[:,0]/2,finalInterval[:,1]/2)
self.finalBeta += betaError + (finalIntervalError[:,1] + finalIntervalError[:,0])/2
return self.finalInterval
def getFinalPoint(self):
"""Finds the point that should be reported as the root (midpoint of the final step interval).
Returns
-------
root: numpy array
The final point to be reported as the root of the interval
"""
#TODO: Make this a seperate function so it can use njit.
#Make these _NoNumba calls use floats so they call call the numba functions without a seperate compile
if not self.finalStep: #If no final step, use the midpoint of the calculated final interval.
self.root = (self.finalInterval[:,0] + self.finalInterval[:,1]) / 2
else: #If using the final step, recalculate the final interval using post-final transforms.
finalInterval = self.topInterval.T
finalIntervalError = np.zeros_like(finalInterval)
transformsToUse = self.transforms
for alpha,beta in transformsToUse[::-1]:
finalInterval, temp = TwoProd_NoNumba(finalInterval, alpha)
finalIntervalError = alpha * finalIntervalError + temp
finalInterval, temp = TwoSum_NoNumba(finalInterval,beta)
finalIntervalError += temp
finalInterval = finalInterval.T + finalIntervalError.T
self.root = (finalInterval[:,0] + finalInterval[:,1]) / 2 # Return the midpoint
return self.root
def size(self):
"""Gets the volume of the current interval."""
return np.product(self.interval[:,1] - self.interval[:,0])
def dimSize(self):
"""Gets the lengths along each dimension of the current interval."""
return self.interval[:,1] - self.interval[:,0]
def finalDimSize(self):
"""Gets the lengths along each dimension of the final interval."""
return self.finalInterval[:,1] - self.finalInterval[:,0]
def copy(self):
"""Returns a deep copy of the current interval with all changes and properties preserved."""
newone = TrackedInterval(self.topInterval)
newone.interval = self.interval.copy()
newone.transforms = self.transforms.copy()
newone.empty = self.empty
newone.nextTransformPoints = self.nextTransformPoints.copy()
if self.finalStep:
newone.finalStep = True
newone.canThrowOutFinalStep = self.canThrowOutFinalStep
newone.possibleDuplicateRoots = self.possibleDuplicateRoots.copy()
newone.possibleExtraRoot = self.possibleExtraRoot
newone.preFinalInterval = self.preFinalInterval.copy()
newone.preFinalTransforms = self.preFinalTransforms.copy()
return newone
def __contains__(self, point):
"""Determines if point is contained in the current interval."""
return np.all(point >= self.interval[:,0]) and np.all(point <= self.interval[:,1])
def overlapsWith(self, otherInterval):
"""Determines if the otherInterval overlaps with the current interval.
Returns True if the lower bound of one interval is less than the upper bound of the other
in EVERY dimension; returns False otherwise."""
for (a1,b1),(a2,b2) in zip(self.getIntervalForCombining(), otherInterval.getIntervalForCombining()):
if a1 > b2 or a2 > b1:
return False
return True
def isPoint(self):
"""Determines if the current interval has essentially length 0 in each dimension."""
return np.all(np.abs(self.interval[:,0] - self.interval[:,1]) < 1e-32)
def startFinalStep(self):
"""Prepares for the final step by saving the current interval and its transform list."""
self.finalStep = True
self.preFinalInterval = self.interval.copy()
self.preFinalTransforms = self.transforms.copy()
def getIntervalForCombining(self):
"""Returns the interval to be used in combining intervals to report at the end."""
return self.preFinalInterval if self.finalStep else self.interval
def __repr__(self):
return str(self)
def __str__(self):
return str(self.interval)
def getLinearTerms(M):
"""Gets the linear terms of the Chebyshev coefficient tensor M.
Uses the fact that the linear terms are located at
M[(0,0, ... ,0,1)]
M[(0,0, ... ,1,0)]
...
M[(0,1, ... ,0,0)]
M[(1,0, ... ,0,0)]
which are indexes
1, M.shape[-1], M.shape[-1]*M.shape[-2], ... when looking at M.ravel().
Parameters
----------
M : numpy array
The coefficient array to get the linear terms from
Returns
-------
A: numpy array
An array with the linear terms of M
"""
A = []
spot = 1
for i in M.shape[::-1]:
A.append(0 if i == 1 else M.ravel()[spot])
spot *= i
return A[::-1] # Return linear terms in dimension order.
@njit
def linearCheck1(totalErrs, A, consts):
"""Takes A, the linear terms of each function approximation, and makes any possible reduction
in the interval based on the totalErrs."""
dim = len(A)
a = -np.ones(dim) * np.inf
b = np.ones(dim) * np.inf
for row in range(dim):
for col in range(dim):
if A[row,col] != 0: #Don't bother running the check if the linear term is too small.
v1 = totalErrs[row] / abs(A[row,col]) - 1
v2 = 2 * consts[row] / A[row,col]
if v2 >= 0:
a_, b_ = -v1, v1-v2
else:
a_, b_ = -v2-v1, v1
a[col] = max(a[col], a_)
b[col] = min(b[col], b_)
return a, b
def BoundingIntervalLinearSystem(Ms, errors, finalStep, macheps = 2**-52):
"""Finds a smaller region in which any root must be.
Parameters
----------
Ms : list of numpy arrays
Each numpy array is the coefficient tensor of a chebyshev polynomials
errors : iterable of floats
The maximum error of chebyshev approximations
finalStep : bool
Whether we are in the final step of the algorithm
Returns
-------
newInterval : numpy array
The smaller interval where any root must be
changed : bool
Whether the interval has shrunk at all
should_stop : bool
Whether we should stop subdividing
throwout :
Whether we should throw out the interval entirely
"""
if finalStep:
errors = np.zeros_like(errors)
dim = Ms[0].ndim
#Some constants we use here
minZoomForChange = 0.99 #If the volume doesn't shrink by this amount say that it hasn't changed
minZoomForBaseCaseEnd = 0.4**dim #If the volume doesn't change by at least this amount when running with no error, stop
#Get the matrix of the linear terms
A = np.array([getLinearTerms(M) for M in Ms])
#Get the Vector of the constant terms
consts = np.array([M.ravel()[0] for M in Ms])
#Get the Error of everything else combined.
totalErrs = np.array([np.sum(np.abs(M)) + e for M,e in zip(Ms, errors)])
linear_sums = np.sum(np.abs(A),axis=1)
err = np.array([tE-abs(c)-l for tE,c,l in zip(totalErrs,consts,linear_sums)])
#Scale all the polynomials relative to one another
errors = errors.copy()
for i in range(dim):
scaleVal = np.max(np.abs(A[i]))
if scaleVal > 0:
s = 2.**int(np.floor(np.log2(abs(scaleVal))))
A[i] /= s
consts[i] /= s
totalErrs[i] /= s
linear_sums[i] /= s
err[i] /= s
errors[i] /= s
#Precondition the columns. (AP)X = B -> A(PX) = B. So scale columns, solve, then scale the solution.
colScaler = np.ones(dim)
for i in range(dim):
scaleVal = np.max(np.abs(A[:,i]))
if scaleVal > 0:
s = 2**(-np.floor(np.log2(abs(scaleVal))))
colScaler[i] = s
totalErrs += np.abs(A[:,i]) * (s - 1)
A[:,i] *= s
#Run linear algorithm for shrinking or deciding whether to subdivide.
#This loop will only execute the second time if the interval was not changed on the first iteration and it needs to run again with tighter errors
#Calculate the SVD outside of the for loop because it doesn't change
U, S, Vh = np.linalg.svd(A)
condNum = S[-1]/S[0]
wellConditioned = S[0] > 0 and condNum > 1e-10
#Add this width to the new intervals we find to avoid rounding error throwing out roots
widthToAdd = max(condNum,2)*macheps
Ainv = (1/S * Vh.T) @ U.T
center = -Ainv@consts
#Use the first interval shrinking method
a_init, b_init = linearCheck1(totalErrs, A, consts)
for i in range(2):
a = a_init
b = b_init
#We use the matrix inverse to find the width, so might as well use it both spots. Should be fine as dim is small.
if wellConditioned: #Make sure conditioning is ok.
#Ainv transforms the hyperrectangle of side lengths err into a parallelogram with these as the principal direction
#So summing over them gets the farthest the parallelogram can reach in each dimension.
width = np.sum(np.abs(Ainv*err),axis=1)
#Bound with previous result
a = np.maximum(center - width, a)
b = np.minimum(center + width, b)
#Undo the column preconditioning
a *= colScaler
b *= colScaler
#Add error and bound
a -= widthToAdd
b += widthToAdd
if np.any(a > b):
with open("num_of_times","a") as file:
file.write("1\n")
throwOut = np.any(a > b) or np.any(a > 1) or np.any(b < -1)
a[a < -1] = -1
b[b < -1] = -1
a[a > 1] = 1
b[b > 1] = 1
forceShouldStop = finalStep and not wellConditioned
# Calculate the "changed" variable
newRatio = np.product(b - a) / 2**dim
if throwOut:
changed = True
elif i == 0:
changed = newRatio < minZoomForChange
else:
changed = newRatio < minZoomForBaseCaseEnd
if i == 0 and changed:
#If it is the first time through the loop and there was a change, return the interval it shrunk down to and set "is_done" to false
return np.vstack([a,b]).T, changed, forceShouldStop, throwOut
elif i == 0 and not changed:
#If it is the first time through the loop and there was not a change, save the a and b as the original values to return,
#and then try running through the loop again with a tighter error to see if we shrink then
a_orig = a
b_orig = b
err = errors
elif changed:
#If it is the second time through the loop and it did change, it means we didn't change on the first time,
#but that the interval did shrink with tighter errors. So return the original interval with changed = False and is_done = False
return np.vstack([a_orig, b_orig]).T, False, forceShouldStop, False
else:
#If it is the second time through the loop and it did NOT change, it means we will not shrink the interval even if we subdivide,
#so return the original interval with changed = False and is_done = wellConditioned
return np.vstack([a_orig,b_orig]).T, False, wellConditioned or forceShouldStop, False
@njit(UniTuple(float64,2)(float64, float64))
def TwoSum(a,b):
"""Returns x,y such that a+b=x+y exactly, and a+b=x in floating point using numba."""
x = a+b
z = x-a
y = (a-(x-z)) + (b-z)
return x,y
def TwoSum_NoNumba(a,b):
"""Returns x,y such that a+b=x+y exactly, and a+b=x in floating point without using numba."""
x = a+b
z = x-a
y = (a-(x-z)) + (b-z)
return x,y
@njit(UniTuple(float64,2)(float64))
def Split(a):
"""Returns x,y such that a = x+y exactly and a = x in floating point using numba."""
c = (2**27 + 1) * a
x = c-(c-a)
y = a-x
return x,y
def Split_NoNumba(a):
"""Returns x,y such that a = x+y exactly and a = x in floating point without using numba."""
c = (2**27 + 1) * a
x = c-(c-a)
y = a-x
return x,y
@njit(UniTuple(float64,2)(float64, float64))
def TwoProd(a,b):
"""Returns x,y such that a*b=x+y exactly and a*b=x in floating point using numba."""
x = a*b
a1,a2 = Split(a)
b1,b2 = Split(b)
y=a2*b2-(((x-a1*b1)-a2*b1)-a1*b2)
return x,y
def TwoProd_NoNumba(a,b):
"""Returns x,y such that a*b=x+y exactly and a*b=x in floating point without usin numba."""
x = a*b
a1,a2 = Split_NoNumba(a)
b1,b2 = Split_NoNumba(b)
y=a2*b2-(((x-a1*b1)-a2*b1)-a1*b2)
return x,y
@njit(UniTuple(float64,2)(float64, float64, float64, float64))
def TwoProdWithSplit(a,b,a1,a2):
"""Returns x,y such that a*b = x+y exactly and a*b = x in floating point but with a already split."""
x = a*b
b1,b2 = Split(b)
y=a2*b2-(((x-a1*b1)-a2*b1)-a1*b2)
return x,y
def getTransformPoints(newInterval):
"""Gets the alpha and beta points needed to transform the current interval to newInterval."""
a,b = newInterval
return (b-a)/2, (b+a)/2
def getTransformationError(M, dim):
"""Returns an upper bound on the error of transforming the Chebyshev approximation M
In the transformation of dimension dim in M, the matrix multiplication of M by the transformation
matrix C has each element of M involved in n element multiplications, where n is the number of rows
in C, which is equal to the degree of approximation of M in dimension dim, or M.shape[dim].
Parameters
----------
M : numpy array
The Chebyshev approximation coefficient tensor being transformed
dim : int
The dimension of M being transformed
Returns
-------
error : float
The upper bound for the error associated with the transformation of dimension dim in M
"""
machEps = 2**-52
error = M.shape[dim] * machEps * np.sum(np.abs(M))
return error #TODO: Figure out a more rigurous bound!
def transformCheb(M, alphas, betas, error, exact):
"""Transforms an entire Chebyshev coefficient matrix using the transformation xHat = alpha*x + beta.
Parameters
----------
M : numpy array
The chebyshev coefficient matrix
alphas : iterable
The scalers in each dimension of the transformation.
betas : iterable
The offset in each dimension of the transformation.
error : float
A bound on the error of the chebyshev approximation
exact : bool
Whether to perform the transformation with higher precision to minimize error
Returns
-------
M : numpy array
The coefficient matrix transformed to the new interval
error : float
An upper bound on the error of the transformation
"""
#This just does the matrix multiplication on each dimension. Except it's by a tensor.
for dim,n,alpha,beta in zip(range(M.ndim),M.shape,alphas,betas):
error += getTransformationError(M, dim)
M = TransformChebInPlaceND(M,dim,alpha,beta,exact)
return M, error
def transformChebToInterval(Ms, alphas, betas, errors, exact):
"""Transforms an entire list of Chebyshev approximations to a new interval xHat = alpha*x + beta.
Parameters
----------
Ms : list of numpy arrays
The chebyshev coefficient matrices
alphas : iterable
The scalers of the transformation we are doing.
betas : iterable
The offsets of the transformation we are doing.
errors : numpy array
A bound on the error of each Chebyshev approximation
exact : bool
Whether to perform the transformation with higher precision to minimize error
Returns
-------
newMs : list of numpy arrays
The coefficient matrices transformed to the new interval
newErrors : list of numpy arrays
The new errors associated with the transformed coefficient matrices
"""
#Transform the chebyshev polynomials
newMs = []
newErrors = []
for M,e in zip(Ms, errors):
newM, newE = transformCheb(M, alphas, betas, e, exact)
newMs.append(newM)
newErrors.append(newE)
return newMs, np.array(newErrors)
def zoomInOnIntervalIter(Ms, errors, trackedInterval, exact):
"""One iteration of shrinking an interval that may contain roots.
Calls BoundingIntervaLinearSystem which determines a smaller interval in which any roots are
bound to lie. Then calls transformChebToInterval to transform the current coefficient
approximations to the new interval.
Parameters
----------
Ms : list of numpy arrays
The Chebyshev coefficient tensors of each approximation
errors : numpy array
An upper bound on the error of each Chebyshev approximation
trackedInterval : TrackedInterval
The current interval for which the Chebyshev approximations are valid
exact : bool
Whether the transformation should be done with higher precision to minimize error
Returns
-------
Ms : list of numpy arrays
The chebyshev coefficient matrices transformed to the new interval
errors : numpy array
The new errors associated with the transformed coefficient matrices
trackedInterval : TrackedInterval
The new interval that the transformed coefficient matrices are valid for
changed : bool
Whether or not the interval shrunk significantly during the iteration
should_stop : bool
Whether or not to continue subdiviing after the iteration of shrinking is completed
"""
dim = len(Ms)
#Zoom in on the current interval
interval, changed, should_stop, throwOut = BoundingIntervalLinearSystem(Ms, errors, trackedInterval.finalStep)
#Don't zoom in if we're already at a point
for dim in range(len(Ms)):
if trackedInterval.interval[dim,0] == trackedInterval.interval[dim,1]:
interval[dim, 0] = -1.
interval[dim, 1] = 1.
#We can't throw out on the final step
if throwOut and not trackedInterval.canThrowOut():
throwOut = False
should_stop = True
changed = True
#Check if we can throw out the whole thing
if throwOut:
trackedInterval.empty = True
return Ms, errors, trackedInterval, True, True
#Check if we are done iterating
if not changed:
return Ms, errors, trackedInterval, changed, should_stop
#Transform the chebyshev polynomials
trackedInterval.addTransform(interval)
Ms, errors = transformChebToInterval(Ms, *trackedInterval.getLastTransform(), errors, exact)
#We should stop in the final step once the interval has become a point
if trackedInterval.finalStep and trackedInterval.isPoint():
should_stop = True
changed = False
return Ms, errors, trackedInterval, changed, should_stop
def chebTransform1D(M, alpha, beta, transformDim, exact):
"""Transforms a single dimension of a Chebyshev coefficient matrix.
Parameters
----------
M : numpy array
The Chebyshev coefficient matrix
alpha:
The scaler of the transformation
beta:
The shifting of the transformation
transformDim:
The particular dimension of the approximation to be transformed
exact:
Whether the transformation should be performed with higher precision to minimize error
Returns
-------
transformed_M : numpy array
The Chebyshev coefficient matrix transformed to the new interval in dimension transformDim
"""
return TransformChebInPlaceND(M, transformDim, alpha, beta, exact)
def getInverseOrder(order):
"""Gets a particular order of matrices needed in getSubdivisionIntervals (helper function).
Takes the order of dimensions in which a Chebyshev coefficient tensor M was subdivided and gets
the order of the indexes that will arrange the list of resulting transformed matrices as if the
dimensions had bee subdivided in standard index order. For example, if dimensions 0, 3, 1 were
subdivided in that order, this function returns the order [0,2,1,3,4,6,5,7] corresponding to the
indices of currMs such that when arranged in this order, it appears as if the dimensions were
subdivided in order 0, 1, 3.
Parameters
----------
order : numpy array
The order of dimensions along which a coefficient tensor was subdivided
Returns
-------
invOrder : numpy array
The order of indices of currMs (in the function getSubdivisionIntervals) that arranges the
matrices resulting from the subdivision as if the original matrix had been subdivided in