├── .gitignore
├── README.md
├── __init__.py
├── examples
├── Problem Set 1
│ ├── main.py
│ └── solution.py
├── Problem Set 10
│ ├── main.py
│ └── solution.py
├── Problem Set 11
│ ├── main.py
│ └── solution.py
├── Problem Set 2
│ ├── main.py
│ ├── solution.py
│ └── warmup.py
├── Problem Set 3
│ ├── main.py
│ └── solution.py
├── Problem Set 4
│ ├── main.py
│ └── solution.py
├── Problem Set 5
│ ├── main.py
│ └── solution.py
├── Problem Set 6
│ ├── main.py
│ └── solution.py
├── Problem Set 7
│ ├── main.py
│ └── solution.py
├── Problem Set 8
│ ├── main.py
│ └── solution.py
└── Problem Set 9
│ ├── main.py
│ └── solution.py
├── function.py
└── util.py
/.gitignore:
--------------------------------------------------------------------------------
1 | **/__pycache__/
2 | **/try.py
3 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | # Numerical Methods and Functions Library
2 |
3 | **Created during UMC 202 at the Indian Institute of Science, Bengaluru**
4 |
5 |
6 | Functions
7 |
8 | - Support for polynomials
9 | - Support for function arithmetic, composition and other methods
10 | - Support for derivatives and differentiation
11 | - Forward difference
12 | - Backward difference
13 | - Central difference
14 | - n-th order derivative
15 | - Allows probing algorithms
16 | - Support for integration
17 | - Rectangular
18 | - Midpoint
19 | - Trapezoidal
20 | - Simpson's
21 | - Gaussian Quadrature (n = 1, 2)
22 | - Support for multivariable functions
23 | - Support for vectors and vector-valued functions
24 | - Support for matrices (vector of vectors)
25 |
26 | **Other Utilities**
27 | - Plotting (requires `matplotlib`)
28 |
29 |
30 |
31 | Methods
32 |
33 | - Root finding
34 | - Bisection
35 | - Newton-Raphson and Modified Newton
36 | - Fixed-point iteration
37 | - Secant
38 | - Regula-Falsi
39 | - Interpolating Polynomial
40 | - Lagrange
41 | - Newton's divided difference
42 | - Forward difference
43 | - Backward difference
44 | - Solving initial value problems on one-dimensional first order linear ODEs (and systems of such ODEs)
45 | - Euler's method
46 | - Taylor's method (for n = 1, 2)
47 | - Runga Kutta (for n = 1, 2, 3, 4)
48 | - Trapezoidal
49 | - Adam-Bashforth (for n = 2, 3, 4)
50 | - Adam-Moulton (for n = 2, 3, 4)
51 | - Predictor-Corrector (with initial approximation from Runga-Kutta order-4, predictor as Adam-Bashforth order-4 and corrector as Adam-Moulton order-3)
52 | - Solving boundary value problems on one-dimensional second order linear ODEs
53 | - Shooting method
54 | - Finite difference method
55 | - Solving initial value problems on one-dimensional second order ODEs
56 | - All methods from solving first order linear ODEs
57 | - Solving boundary value problems on one-dimensional second order ODEs
58 | - Shooting method
59 | - Newton method
60 | - Solving systems of linear equations
61 | - Gaussian elimination with backward substitution
62 | - Gauss-Jacobi method
63 | - Gauss-Seidel method
64 |
65 |
66 | ## Usage
67 |
68 | The file `function.py` contains all the important classes and methods, and is the main file to be imported. The file `util.py` has a few helper methods required by the main file. Some practice problem sets have been included in the `examples` directory to demonstrate the usage of the library.
69 |
70 | > [!NOTE]
71 | > Documentation below was generated automatically using [pdoc](https://pdoc3.github.io/).
72 |
73 | # Module `function`
74 |
75 |
76 |
77 |
78 |
79 |
80 |
81 |
82 | ## Classes
83 |
84 |
85 |
86 | ### Class `BivariateFunction`
87 |
88 |
89 |
90 |
91 | > class BivariateFunction(
92 | > function
93 | > )
94 |
95 |
96 |
97 |
98 |
99 |
100 | #### Ancestors (in MRO)
101 |
102 | * [function.MultiVariableFunction](#function.MultiVariableFunction)
103 |
104 |
105 |
106 |
107 |
108 |
109 |
110 | ### Class `Cos`
111 |
112 |
113 |
114 |
115 | > class Cos(
116 | > f: function.Function
117 | > )
118 |
119 |
120 |
121 |
122 |
123 |
124 | #### Ancestors (in MRO)
125 |
126 | * [function.Function](#function.Function)
127 |
128 |
129 |
130 |
131 |
132 |
133 |
134 | ### Class `Exponent`
135 |
136 |
137 |
138 |
139 | > class Exponent(
140 | > f: function.Function,
141 | > base: float = 2.718281828459045
142 | > )
143 |
144 |
145 |
146 |
147 |
148 |
149 | #### Ancestors (in MRO)
150 |
151 | * [function.Function](#function.Function)
152 |
153 |
154 |
155 |
156 |
157 |
158 |
159 | ### Class `FirstOrderLinearODE`
160 |
161 |
162 |
163 |
164 | > class FirstOrderLinearODE(
165 | > f: function.BivariateFunction,
166 | > a: float,
167 | > b: float,
168 | > y0: float
169 | > )
170 |
171 |
172 | y'(x) = f(x, y(x))
173 | These are initial value problems.
174 |
175 | f is a function of x and y(x)
176 |
177 |
178 |
179 | #### Ancestors (in MRO)
180 |
181 | * [function.LinearODE](#function.LinearODE)
182 | * [function.OrdinaryDifferentialEquation](#function.OrdinaryDifferentialEquation)
183 |
184 |
185 |
186 |
187 |
188 |
189 |
190 | #### Methods
191 |
192 |
193 |
194 | ##### Method `solve`
195 |
196 |
197 |
198 |
199 | > def solve(
200 | > self,
201 | > h: float = 0.1,
202 | > method: Literal['euler', 'runge-kutta', 'taylor', 'trapezoidal', 'adam-bashforth', 'adam-moulton', 'predictor-corrector'] = 'euler',
203 | > n: int = 1,
204 | > step: int = 2,
205 | > points: list[float] = []
206 | > )
207 |
208 |
209 |
210 |
211 |
212 | ##### Method `solve_adam_bashforth`
213 |
214 |
215 |
216 |
217 | > def solve_adam_bashforth(
218 | > self,
219 | > h: float,
220 | > step: int,
221 | > points: list[float]
222 | > ) ‑> function.Polynomial
223 |
224 |
225 |
226 |
227 |
228 | ##### Method `solve_adam_moulton`
229 |
230 |
231 |
232 |
233 | > def solve_adam_moulton(
234 | > self,
235 | > h: float,
236 | > step: int,
237 | > points: list[float]
238 | > ) ‑> function.Polynomial
239 |
240 |
241 |
242 |
243 |
244 | ##### Method `solve_predictor_corrector`
245 |
246 |
247 |
248 |
249 | > def solve_predictor_corrector(
250 | > self,
251 | > h: float
252 | > ) ‑> function.Polynomial
253 |
254 |
255 |
256 |
257 |
258 | ##### Method `solve_runge_kutta`
259 |
260 |
261 |
262 |
263 | > def solve_runge_kutta(
264 | > self,
265 | > h: float,
266 | > n: int
267 | > ) ‑> function.Polynomial
268 |
269 |
270 |
271 |
272 |
273 | ##### Method `solve_taylor`
274 |
275 |
276 |
277 |
278 | > def solve_taylor(
279 | > self,
280 | > h: float,
281 | > n: int
282 | > ) ‑> function.Polynomial
283 |
284 |
285 |
286 |
287 |
288 | ##### Method `solve_trapezoidal`
289 |
290 |
291 |
292 |
293 | > def solve_trapezoidal(
294 | > self,
295 | > h: float
296 | > ) ‑> function.Polynomial
297 |
298 |
299 |
300 |
301 |
302 | ### Class `Function`
303 |
304 |
305 |
306 |
307 | > class Function(
308 | > function
309 | > )
310 |
311 |
312 |
313 |
314 |
315 |
316 |
317 | #### Descendants
318 |
319 | * [function.Cos](#function.Cos)
320 | * [function.Exponent](#function.Exponent)
321 | * [function.Log](#function.Log)
322 | * [function.Polynomial](#function.Polynomial)
323 | * [function.Sin](#function.Sin)
324 | * [function.Tan](#function.Tan)
325 |
326 |
327 |
328 |
329 |
330 |
331 | #### Methods
332 |
333 |
334 |
335 | ##### Method `bisection`
336 |
337 |
338 |
339 |
340 | > def bisection(
341 | > self,
342 | > a: float,
343 | > b: float,
344 | > TOLERANCE=1e-10,
345 | > N=100,
346 | > early_stop: int = None
347 | > )
348 |
349 |
350 |
351 |
352 |
353 | ##### Method `differentiate`
354 |
355 |
356 |
357 |
358 | > def differentiate(
359 | > self,
360 | > func=None,
361 | > h=1e-05,
362 | > method: Literal['forward', 'backward', 'central'] = 'forward'
363 | > )
364 |
365 |
366 | Sets or returns the derivative of the function.
367 | If func is None, returns the derivative.
368 | If func is a Function, sets the derivative to func.
369 | If func is a lambda, sets the derivative to a Function with the lambda.
370 |
371 |
372 | ##### Method `differentiate_central`
373 |
374 |
375 |
376 |
377 | > def differentiate_central(
378 | > self,
379 | > h
380 | > )
381 |
382 |
383 |
384 |
385 |
386 | ##### Method `differentiate_forward`
387 |
388 |
389 |
390 |
391 | > def differentiate_forward(
392 | > self,
393 | > h
394 | > )
395 |
396 |
397 |
398 |
399 |
400 | ##### Method `fixed_point`
401 |
402 |
403 |
404 |
405 | > def fixed_point(
406 | > self,
407 | > p0: float,
408 | > TOLERANCE=1e-10,
409 | > N=100
410 | > )
411 |
412 |
413 |
414 |
415 |
416 | ##### Method `integral`
417 |
418 |
419 |
420 |
421 | > def integral(
422 | > self,
423 | > func=None,
424 | > h=1e-05
425 | > )
426 |
427 |
428 | Sets or returns the integral of the function.
429 | If func is None, returns the integral.
430 | If func is a Function, sets the integral to func.
431 | If func is a lambda, sets the integral to a Function with the lambda.
432 |
433 |
434 | ##### Method `integrate`
435 |
436 |
437 |
438 |
439 | > def integrate(
440 | > self,
441 | > a: float,
442 | > b: float,
443 | > method: Literal['rectangular', 'midpoint', 'trapezoidal', 'simpson', 'gauss'] = None,
444 | > n: int = None
445 | > )
446 |
447 |
448 | Definite integral of the function from a to b.
449 |
450 |
451 | ##### Method `integrate_gauss`
452 |
453 |
454 |
455 |
456 | > def integrate_gauss(
457 | > self,
458 | > a: float,
459 | > b: float,
460 | > n: int = None
461 | > )
462 |
463 |
464 |
465 |
466 |
467 | ##### Method `integrate_midpoint`
468 |
469 |
470 |
471 |
472 | > def integrate_midpoint(
473 | > self,
474 | > a: float,
475 | > b: float,
476 | > n: int = None
477 | > )
478 |
479 |
480 |
481 |
482 |
483 | ##### Method `integrate_rectangular`
484 |
485 |
486 |
487 |
488 | > def integrate_rectangular(
489 | > self,
490 | > a: float,
491 | > b: float,
492 | > n: int = None
493 | > )
494 |
495 |
496 |
497 |
498 |
499 | ##### Method `integrate_simpson`
500 |
501 |
502 |
503 |
504 | > def integrate_simpson(
505 | > self,
506 | > a: float,
507 | > b: float,
508 | > n: int = None
509 | > )
510 |
511 |
512 |
513 |
514 |
515 | ##### Method `integrate_trapezoidal`
516 |
517 |
518 |
519 |
520 | > def integrate_trapezoidal(
521 | > self,
522 | > a: float,
523 | > b: float,
524 | > n: int = None
525 | > )
526 |
527 |
528 |
529 |
530 |
531 | ##### Method `modified_newton`
532 |
533 |
534 |
535 |
536 | > def modified_newton(
537 | > self,
538 | > p0: float,
539 | > TOLERANCE=1e-10,
540 | > N=100,
541 | > early_stop: int = None
542 | > )
543 |
544 |
545 |
546 |
547 |
548 | ##### Method `multi_differentiate`
549 |
550 |
551 |
552 |
553 | > def multi_differentiate(
554 | > self,
555 | > n: int,
556 | > h=1e-05,
557 | > method: Literal['forward', 'backward', 'central'] = 'forward'
558 | > )
559 |
560 |
561 | Returns the nth derivative of the function.
562 |
563 |
564 | ##### Method `newton`
565 |
566 |
567 |
568 |
569 | > def newton(
570 | > self,
571 | > p0: float,
572 | > TOLERANCE=1e-10,
573 | > N=100,
574 | > early_stop: int = None
575 | > )
576 |
577 |
578 |
579 |
580 |
581 | ##### Method `plot`
582 |
583 |
584 |
585 |
586 | > def plot(
587 | > self,
588 | > min: float,
589 | > max: float,
590 | > N=1000,
591 | > file: str = '',
592 | > clear: bool = False
593 | > )
594 |
595 |
596 |
597 |
598 |
599 | ##### Method `regula_falsi`
600 |
601 |
602 |
603 |
604 | > def regula_falsi(
605 | > self,
606 | > p0: float,
607 | > p1: float,
608 | > TOLERANCE=1e-10,
609 | > N=100,
610 | > early_stop: int = None
611 | > )
612 |
613 |
614 |
615 |
616 |
617 | ##### Method `root`
618 |
619 |
620 |
621 |
622 | > def root(
623 | > self,
624 | > method: Literal['bisection', 'newton', 'secant', 'regula_falsi', 'modified_newton'],
625 | > a: float = None,
626 | > b: float = None,
627 | > p0: float = None,
628 | > p1: float = None,
629 | > TOLERANCE=1e-10,
630 | > N=100,
631 | > return_iterations=False,
632 | > early_stop: int = None
633 | > )
634 |
635 |
636 |
637 |
638 |
639 | ##### Method `secant`
640 |
641 |
642 |
643 |
644 | > def secant(
645 | > self,
646 | > p0: float,
647 | > p1: float,
648 | > TOLERANCE=1e-10,
649 | > N=100,
650 | > early_stop: int = None
651 | > )
652 |
653 |
654 |
655 |
656 |
657 | ### Class `LinearODE`
658 |
659 |
660 |
661 |
662 | > class LinearODE
663 |
664 |
665 |
666 |
667 |
668 |
669 | #### Ancestors (in MRO)
670 |
671 | * [function.OrdinaryDifferentialEquation](#function.OrdinaryDifferentialEquation)
672 |
673 |
674 |
675 | #### Descendants
676 |
677 | * [function.FirstOrderLinearODE](#function.FirstOrderLinearODE)
678 | * [function.SecondOrderLinearODE_BVP](#function.SecondOrderLinearODE_BVP)
679 |
680 |
681 |
682 |
683 |
684 |
685 | ### Class `LinearSystem`
686 |
687 |
688 |
689 |
690 | > class LinearSystem(
691 | > A: function.Matrix,
692 | > b: function.Vector
693 | > )
694 |
695 |
696 | A system of linear equations.
697 |
698 |
699 |
700 |
701 |
702 |
703 |
704 |
705 | #### Methods
706 |
707 |
708 |
709 | ##### Method `solve`
710 |
711 |
712 |
713 |
714 | > def solve(
715 | > self,
716 | > method: Literal['gauss_elimination', 'gauss_jacobi', 'gauss_seidel'] = 'gauss_elimination',
717 | > TOL: float = 1e-05,
718 | > initial_approximation: function.Vector = None,
719 | > MAX_ITERATIONS: int = 100
720 | > )
721 |
722 |
723 |
724 |
725 |
726 | ##### Method `solve_gauss_elimination`
727 |
728 |
729 |
730 |
731 | > def solve_gauss_elimination(
732 | > self
733 | > ) ‑> function.Vector
734 |
735 |
736 |
737 |
738 |
739 | ##### Method `solve_gauss_jacobi`
740 |
741 |
742 |
743 |
744 | > def solve_gauss_jacobi(
745 | > self,
746 | > TOL: float,
747 | > initial_approximation: function.Vector,
748 | > MAX_ITERATIONS
749 | > ) ‑> function.Vector
750 |
751 |
752 |
753 |
754 |
755 | ##### Method `solve_gauss_seidel`
756 |
757 |
758 |
759 |
760 | > def solve_gauss_seidel(
761 | > self,
762 | > TOL: float,
763 | > initial_approximation: function.Vector,
764 | > MAX_ITERATIONS
765 | > ) ‑> function.Vector
766 |
767 |
768 |
769 |
770 |
771 | ### Class `Log`
772 |
773 |
774 |
775 |
776 | > class Log(
777 | > f: function.Function,
778 | > base: float = 2.718281828459045
779 | > )
780 |
781 |
782 |
783 |
784 |
785 |
786 | #### Ancestors (in MRO)
787 |
788 | * [function.Function](#function.Function)
789 |
790 |
791 |
792 |
793 |
794 |
795 |
796 | ### Class `Matrix`
797 |
798 |
799 |
800 |
801 | > class Matrix(
802 | > *rows: list[function.Vector]
803 | > )
804 |
805 |
806 |
807 |
808 |
809 |
810 |
811 |
812 |
813 |
814 |
815 | ### Class `MultiVariableFunction`
816 |
817 |
818 |
819 |
820 | > class MultiVariableFunction(
821 | > function
822 | > )
823 |
824 |
825 |
826 |
827 |
828 |
829 |
830 | #### Descendants
831 |
832 | * [function.BivariateFunction](#function.BivariateFunction)
833 |
834 |
835 |
836 |
837 |
838 |
839 | ### Class `OrdinaryDifferentialEquation`
840 |
841 |
842 |
843 |
844 | > class OrdinaryDifferentialEquation
845 |
846 |
847 |
848 |
849 |
850 |
851 |
852 | #### Descendants
853 |
854 | * [function.LinearODE](#function.LinearODE)
855 | * [function.SecondOrderODE_BVP](#function.SecondOrderODE_BVP)
856 | * [function.SecondOrderODE_IVP](#function.SecondOrderODE_IVP)
857 |
858 |
859 |
860 |
861 |
862 |
863 | ### Class `Polynomial`
864 |
865 |
866 |
867 |
868 | > class Polynomial(
869 | > *coefficients
870 | > )
871 |
872 |
873 | coefficients are in the form a_0, a_1, ... a_n
874 |
875 |
876 |
877 | #### Ancestors (in MRO)
878 |
879 | * [function.Function](#function.Function)
880 |
881 |
882 |
883 |
884 |
885 |
886 | #### Static methods
887 |
888 |
889 |
890 | ##### `Method interpolate`
891 |
892 |
893 |
894 |
895 | > def interpolate(
896 | > data: tuple,
897 | > method: Literal['lagrange', 'newton'] = 'newton',
898 | > f: function.Function = None,
899 | > form: Literal['standard', 'backward_diff', 'forward_diff'] = 'standard'
900 | > )
901 |
902 |
903 | data is a list of (x, y) tuples.
904 | alternative: f is a Function that returns the y values and data is a list of x values.
905 |
906 |
907 | ##### `Method interpolate_lagrange`
908 |
909 |
910 |
911 |
912 | > def interpolate_lagrange(
913 | > data: tuple
914 | > )
915 |
916 |
917 | data is a tuple of (x, y) tuples
918 |
919 |
920 | ##### `Method interpolate_newton`
921 |
922 |
923 |
924 |
925 | > def interpolate_newton(
926 | > data: tuple
927 | > )
928 |
929 |
930 | data is a tuple of (x, y) tuples
931 |
932 |
933 | ##### `Method interpolate_newton_backward_diff`
934 |
935 |
936 |
937 |
938 | > def interpolate_newton_backward_diff(
939 | > data: tuple
940 | > )
941 |
942 |
943 | data is a tuple of (x, y) tuples
944 |
945 |
946 | ##### `Method interpolate_newton_forward_diff`
947 |
948 |
949 |
950 |
951 | > def interpolate_newton_forward_diff(
952 | > data: tuple
953 | > )
954 |
955 |
956 | data is a tuple of (x, y) tuples
957 |
958 |
959 |
960 | ### Class `SecondOrderLinearODE_BVP`
961 |
962 |
963 |
964 |
965 | > class SecondOrderLinearODE_BVP(
966 | > p: function.Function,
967 | > q: function.Function,
968 | > r: function.Function,
969 | > a: float,
970 | > b: float,
971 | > y0: float,
972 | > y1: float
973 | > )
974 |
975 |
976 | y''(x) = p(x)y'(x) + q(x)y(x) + r(x)
977 | These are boundary value problems.
978 |
979 |
980 |
981 | #### Ancestors (in MRO)
982 |
983 | * [function.LinearODE](#function.LinearODE)
984 | * [function.OrdinaryDifferentialEquation](#function.OrdinaryDifferentialEquation)
985 |
986 |
987 |
988 |
989 |
990 |
991 |
992 | #### Methods
993 |
994 |
995 |
996 | ##### Method `solve`
997 |
998 |
999 |
1000 |
1001 | > def solve(
1002 | > self,
1003 | > h: float = 0.1,
1004 | > method: Literal['shooting', 'finite_difference'] = 'shooting'
1005 | > )
1006 |
1007 |
1008 |
1009 |
1010 |
1011 | ##### Method `solve_finite_difference`
1012 |
1013 |
1014 |
1015 |
1016 | > def solve_finite_difference(
1017 | > self,
1018 | > h: float
1019 | > ) ‑> function.Polynomial
1020 |
1021 |
1022 |
1023 |
1024 |
1025 | ##### Method `solve_shooting`
1026 |
1027 |
1028 |
1029 |
1030 | > def solve_shooting(
1031 | > self,
1032 | > h: float
1033 | > ) ‑> function.Polynomial
1034 |
1035 |
1036 |
1037 |
1038 |
1039 | ### Class `SecondOrderODE_BVP`
1040 |
1041 |
1042 |
1043 |
1044 | > class SecondOrderODE_BVP(
1045 | > f: function.MultiVariableFunction,
1046 | > a: float,
1047 | > b: float,
1048 | > y0: float,
1049 | > y1: float
1050 | > )
1051 |
1052 |
1053 | y''(x) = f(x, y(x), y'(x))
1054 | These are boundary value problems.
1055 |
1056 | f is a function of x, y(x), and y'(x)
1057 |
1058 |
1059 |
1060 | #### Ancestors (in MRO)
1061 |
1062 | * [function.OrdinaryDifferentialEquation](#function.OrdinaryDifferentialEquation)
1063 |
1064 |
1065 |
1066 |
1067 |
1068 |
1069 |
1070 | #### Methods
1071 |
1072 |
1073 |
1074 | ##### Method `solve`
1075 |
1076 |
1077 |
1078 |
1079 | > def solve(
1080 | > self,
1081 | > h: float = 0.1,
1082 | > method: Literal['shooting_newton'] = 'shooting_newton',
1083 | > M: int = 100,
1084 | > TOL: float = 1e-05,
1085 | > initial_approximation=None
1086 | > )
1087 |
1088 |
1089 |
1090 |
1091 |
1092 | ##### Method `solve_shooting_newton`
1093 |
1094 |
1095 |
1096 |
1097 | > def solve_shooting_newton(
1098 | > self,
1099 | > h: float,
1100 | > M,
1101 | > TOL,
1102 | > initial_approximation
1103 | > ) ‑> function.Polynomial
1104 |
1105 |
1106 |
1107 |
1108 |
1109 | ### Class `SecondOrderODE_IVP`
1110 |
1111 |
1112 |
1113 |
1114 | > class SecondOrderODE_IVP(
1115 | > f: function.MultiVariableFunction,
1116 | > a: float,
1117 | > b: float,
1118 | > y0: float,
1119 | > y1: float
1120 | > )
1121 |
1122 |
1123 | y''(x) = f(x, y(x), y'(x))
1124 | These are initial value problems.
1125 |
1126 | f is a function of x, y(x), and y'(x)
1127 |
1128 |
1129 |
1130 | #### Ancestors (in MRO)
1131 |
1132 | * [function.OrdinaryDifferentialEquation](#function.OrdinaryDifferentialEquation)
1133 |
1134 |
1135 |
1136 |
1137 |
1138 |
1139 |
1140 | #### Methods
1141 |
1142 |
1143 |
1144 | ##### Method `solve`
1145 |
1146 |
1147 |
1148 |
1149 | > def solve(
1150 | > self,
1151 | > h: float = 0.1,
1152 | > method: Literal['euler', 'runge-kutta', 'taylor', 'trapezoidal', 'adam-bashforth', 'adam-moulton', 'predictor-corrector'] = 'euler',
1153 | > n: int = 1,
1154 | > step: int = 2,
1155 | > points: list[float] = []
1156 | > )
1157 |
1158 |
1159 |
1160 |
1161 |
1162 | ### Class `Sin`
1163 |
1164 |
1165 |
1166 |
1167 | > class Sin(
1168 | > f: function.Function
1169 | > )
1170 |
1171 |
1172 |
1173 |
1174 |
1175 |
1176 | #### Ancestors (in MRO)
1177 |
1178 | * [function.Function](#function.Function)
1179 |
1180 |
1181 |
1182 |
1183 |
1184 |
1185 |
1186 | ### Class `Tan`
1187 |
1188 |
1189 |
1190 |
1191 | > class Tan(
1192 | > f: function.Function
1193 | > )
1194 |
1195 |
1196 |
1197 |
1198 |
1199 |
1200 | #### Ancestors (in MRO)
1201 |
1202 | * [function.Function](#function.Function)
1203 |
1204 |
1205 |
1206 |
1207 |
1208 |
1209 |
1210 | ### Class `Vector`
1211 |
1212 |
1213 |
1214 |
1215 | > class Vector(
1216 | > *components
1217 | > )
1218 |
1219 |
1220 |
1221 |
1222 |
1223 |
1224 |
1225 |
1226 |
1227 |
1228 |
1229 |
--------------------------------------------------------------------------------
/__init__.py:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/mrigankpawagi/NumericalMethods/486821de6f1ff50117d17cb07893f4f1ae64cbca/__init__.py
--------------------------------------------------------------------------------
/examples/Problem Set 1/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(10)
4 | for i, ans in enumerate(answers):
5 | print(f'Problem {i + 1}\t {ans}')
--------------------------------------------------------------------------------
/examples/Problem Set 1/solution.py:
--------------------------------------------------------------------------------
1 | from function import Function, Polynomial, Exponent, Sin, Cos, Tan
2 |
3 | class Solution:
4 |
5 | @staticmethod
6 | def solve(n):
7 | return getattr(Solution, f'problem{n}')()
8 |
9 | @staticmethod
10 | def solve_all(n):
11 | return [Solution.solve(i) for i in range(1, n + 1)]
12 |
13 | @staticmethod
14 | def problem1():
15 | """
16 | Use the bisection method to find the solutions accurate to 1e-4 for
17 | x^3 - 7x^2 + 14x - 6 = 0 on [0,1].
18 | """
19 | f = Polynomial(-6, 14, -7, 1)
20 | return f.root('bisection', a=0, b=1, TOLERANCE=1e-4)
21 |
22 | @staticmethod
23 | def problem2():
24 | """
25 | Use the bisection method to find the root of x = exp(-x) with an
26 | accuracy of 1e-4. How many iterations are required?
27 | """
28 | f = Polynomial(0, 1) - Exponent(Polynomial(0, -1))
29 | return f.root('bisection', a=0, b=1, TOLERANCE=1e-4, return_iterations=True)
30 |
31 | @staticmethod
32 | def problem3():
33 | """
34 | Use fixed point iteration method to determine a solution accurate
35 | to within 1e-2 for x^3 - 3x^2 - 3 = 0 on [1,2].
36 | """
37 | f = Polynomial(-3, 0, -3, 1)
38 | g = Polynomial(0, 1) - f
39 | return g.fixed_point(p0=1.5, TOLERANCE=1e-2)
40 |
41 | @staticmethod
42 | def problem4():
43 | """
44 | Determine an interval [a,b] on which fixed point iteration will converge.
45 | Estimate the number of iterations required to obtain approximations
46 | accurate to within 1e-5 and perform the claculations.
47 | """
48 | # (a) x = (2 - exp(x) + x^2) / 3
49 | # a = 0, b = 1
50 | f = (Polynomial(2, 0, 1) - Exponent(Polynomial(0, 1))) / Polynomial(3)
51 | ans_a = (0, 1), f.fixed_point(p0=0.5, TOLERANCE=1e-5)
52 |
53 | # (b) x = (5/x^2) + 2
54 | # a = 2, b = 3
55 | f = (Polynomial(5) / Polynomial(0, 0, 1)) + Polynomial(2)
56 | ans_b = (2, 3), f.fixed_point(p0=2.5, TOLERANCE=1e-5)
57 |
58 | # (c) x = 5^(-x)
59 | # a = 0, b = 1
60 | f = Exponent(Polynomial(0, -1), base=5)
61 | ans_c = (0, 1), f.fixed_point(p0=0.5, TOLERANCE=1e-5)
62 |
63 | # (d) x = 0.5(sinx + cosx)
64 | # a = 0, b = 3
65 | f = Polynomial(0.5) * (Sin(Polynomial(0, 1)) + Cos(Polynomial(0, 1)))
66 | ans_d = (0, 3), f.fixed_point(p0=1.5, TOLERANCE=1e-5)
67 |
68 | return ans_a, ans_b, ans_c, ans_d
69 |
70 | @staticmethod
71 | def problem5():
72 | """
73 | Write down the code for computing a root of a given function f(x) = 0
74 | using Newton Raphson's method.
75 | """
76 |
77 | # Already implemented in Function.root with method='newton'
78 | pass
79 |
80 | @staticmethod
81 | def problem6():
82 | """
83 | Let f(x) = -x^3 - cosx and p0 = -1. Use Newton's method to find p2.
84 | Could p0 = 0 be used?
85 | """
86 | f = Polynomial(0, 0, 0, -1) - Cos(Polynomial(0, 1))
87 | f.differentiate(Polynomial(0, 0, -3) + Sin(Polynomial(0, 1)))
88 | ans = f.root('newton', p0=-1, early_stop=2)
89 |
90 | # No, p0 = 0 cannot be used because f'(0) = 0
91 | return ans, False
92 |
93 | @staticmethod
94 | def problem7():
95 | """
96 | Use Newton's method to approximate to within 1e-4, the value of x that
97 | produces the point on the graph y = x^2 that is closed to (1,0).
98 | """
99 | # Distance function (squared): (x-1)^2 + (x^2-0)^2
100 | # We need minima of this function, that is, root of its derivative
101 | # which is -2 + 2x + 4x^3
102 | f = Polynomial(-2, 2, 0, 4)
103 | return f.root('newton', p0=0.5, TOLERANCE=1e-4)
104 |
105 | @staticmethod
106 | def problem8():
107 | """
108 | Apply Newton's method to find the approximation of the root x = tanx,
109 | starting with the initial guess x0 = 4 and x0 = 4.6. Compare the results
110 | obtained from these two intitial guesses. Does the method converge?
111 | """
112 | f = Polynomial(0, 1) - Tan(Polynomial(0, 1))
113 | ans_1 = f.root('newton', p0=4)
114 | ans_2 = f.root('newton', p0=4.6)
115 |
116 | return ans_1, ans_2
117 |
118 | @staticmethod
119 | def problem9():
120 | """
121 | Obtain an estimation (accurate till 4 decimal points) of the point of
122 | intersection of the curves y = x^2 - 2 and y = cosx.
123 | """
124 | f = Polynomial(-2, 0, 1) - Cos(Polynomial(0, 1))
125 | return f.root('newton', p0=1.5, TOLERANCE=1e-4)
126 |
127 | @staticmethod
128 | def problem10():
129 | """
130 | Apply Newton's method to the function f(x) = x^(2/3) if x >= 0 and
131 | f(x) = -x^(2/3) if x < 0, with the root x* = 0. What is the behavior of the
132 | iterates? Do they converge? If yes, at what order?
133 | """
134 | f = Function(lambda x: (x**(2))**(1/3) * (-1 if x < 0 else 1))
135 | return f.root('newton', p0=0.1, TOLERANCE=1e-5)
136 | # Converged at order 1.
137 |
--------------------------------------------------------------------------------
/examples/Problem Set 10/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(3)
4 |
--------------------------------------------------------------------------------
/examples/Problem Set 10/solution.py:
--------------------------------------------------------------------------------
1 | from function import Function, Sin, Cos, Log, Polynomial, Exponent, SecondOrderLinearODE_BVP
2 | import math
3 |
4 | class Solution:
5 |
6 | @staticmethod
7 | def solve(n):
8 | return getattr(Solution, f'problem{n}')()
9 |
10 | @staticmethod
11 | def solve_all(n):
12 | ans = [Solution.solve(i) for i in range(1, n + 1)]
13 | for i in range(len(ans)):
14 | print(f"Problem {i+1}: {ans[i]}")
15 |
16 | @staticmethod
17 | def problem1():
18 | """
19 | Use the Linear Finite-Difference Algorithm with N = 9 to approximate
20 | the solution to the boundary value problem
21 | y'' = -(2/x)y' + (2/x^2)y + sin(lnx)/x^2, 1 <= x <= 2, y(1) = 1, y(2) = 2
22 | and compare the results to those obtained using the linear shooting method for
23 | the same problem.
24 | """
25 | a = 1
26 | b = 2
27 | N = 9
28 | h = (b - a)/(N + 1)
29 |
30 | BVP = SecondOrderLinearODE_BVP(
31 | Function(lambda x: -2/x),
32 | Function(lambda x: 2/x**2),
33 | Function(lambda x: (Sin(Log(Polynomial(0, 1)))/Polynomial(0, 0, 1))(x)),
34 | a, b, y0=1, y1=2
35 | )
36 | sol1 = BVP.solve(h, method='shooting')
37 | sol2 = BVP.solve(h, method='finite_difference')
38 |
39 | return {
40 | "finite_difference": [sol2(a + i * h) for i in range(N + 2)],
41 | "shooting": [sol1(a + i * h) for i in range(N + 2)],
42 | "difference": [abs(sol2(a + i * h) - sol1(a + i * h)) for i in range(N + 2)]
43 | }
44 |
45 |
46 | @staticmethod
47 | def problem2():
48 | """
49 | Consider the boundary value problem
50 | y'' = -(x + 1)y' + 2y + (1 - x^2)e^(-x), 0 <= x <= 1, y(0) = 1, y(1) = 0.
51 | Use N = 9 and N = 19 respectively and apply the Linear Finite-Difference Algorithm
52 | to approximate the solution to the above boundary value problem.
53 | """
54 | a = 0
55 | b = 1
56 | N1 = 9
57 | N2 = 19
58 | h1 = (b - a)/(N1 + 1)
59 | h2 = (b - a)/(N2 + 1)
60 |
61 | BVP = SecondOrderLinearODE_BVP(
62 | Polynomial(-1, -1),
63 | Polynomial(2),
64 | Polynomial(1, 0, -1) * Exponent(Polynomial(0, -1)),
65 | a, b, y0=1, y1=0
66 | )
67 | sol1 = BVP.solve(h1, method='finite_difference')
68 | sol2 = BVP.solve(h2, method='finite_difference')
69 |
70 | return {
71 | "N=9": [sol1(a + i * h2) for i in range(N2 + 2)],
72 | "N=19": [sol2(a + i * h2) for i in range(N2 + 2)],
73 | "difference": [abs(sol1(a + i * h2) - sol2(a + i * h2)) for i in range(N2 + 2)]
74 | }
75 |
76 | @staticmethod
77 | def problem3():
78 | """
79 | Use the Linear Finite-Difference Algorithm with N = 4 to approximate the solution of the
80 | boundary value problem,
81 | y'' + 4y = cos x, 0 <= x <= pi/4, y(0) = 0, y(pi/4) = 0,
82 | and compare the results to the actual solution where the actual solution is given by
83 | y(x) = -1/3 cos 2x - sqrt(2)/6 sin 2x + 1/3 cos x.
84 | """
85 | a = 0
86 | b = math.pi/4
87 | N = 4
88 | h = (b - a)/(N + 1)
89 |
90 | BVP = SecondOrderLinearODE_BVP(
91 | Polynomial(0),
92 | Polynomial(-4),
93 | Cos(Polynomial(0, 1)),
94 | a, b, y0=0, y1=0
95 | )
96 |
97 | sol = BVP.solve(h, method='finite_difference')
98 |
99 | GT = - (1/3) * Cos(Polynomial(0, 2)) - (math.sqrt(2)/6) * Sin(Polynomial(0, 2)) + (1/3) * Cos(Polynomial(0, 1))
100 |
101 | return {
102 | "approximation": [sol(a + i * h) for i in range(N + 2)],
103 | "actual": [GT(a + i * h) for i in range(N + 2)],
104 | "difference": [abs(sol(a + i * h) - GT(a + i * h)) for i in range(N + 2)]
105 | }
106 |
--------------------------------------------------------------------------------
/examples/Problem Set 11/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(5)
4 |
--------------------------------------------------------------------------------
/examples/Problem Set 11/solution.py:
--------------------------------------------------------------------------------
1 | from function import Matrix, Vector, LinearSystem
2 | import math
3 |
4 | class Solution:
5 |
6 | @staticmethod
7 | def solve(n):
8 | return getattr(Solution, f'problem{n}')()
9 |
10 | @staticmethod
11 | def solve_all(n):
12 | ans = [Solution.solve(i) for i in range(1, n + 1)]
13 | for i in range(len(ans)):
14 | print(f"Problem {i+1}: {ans[i]}")
15 |
16 | @staticmethod
17 | def problem1():
18 | """
19 | Use Gaussian elimination with backward substitution with tolerance 10^-2 to
20 | solve the following linear system
21 | 4x1 - x2 + x3 = 8
22 | 2x1 + 5x2 + 2x3 = 3
23 | x1 + 2x2 + 4x3 = 11
24 | The exact solution of the system is x1 = 1, x2 = -1, x3 = 3.
25 | """
26 | A = Matrix(
27 | Vector(4, -1, 1),
28 | Vector(2, 5, 2),
29 | Vector(1, 2, 4)
30 | )
31 | b = Vector(8, 3, 11)
32 | system = LinearSystem(A, b)
33 | return system.solve(method='gauss_elimination')
34 |
35 | @staticmethod
36 | def problem2():
37 | """
38 | The following linear system
39 | 10x1 - x2 + 2x3 = 6
40 | -x1 + 11x2 - x3 + 3x4 = 25
41 | 2x1 - x2 + 10x3 - x4 = -11
42 | 3x2 - x3 + 8x4 = 15
43 | has the unique solution x = (1, 2, -1, 1). Use the Gauss Jacobi's iterative technique
44 | to find the approximations x^(k) to x with x^(0) = (0, 0, 0, 0) until
45 | ||x^(k) - x^(k-1)||/||x^(k)|| < 10^-3
46 | where ||x|| = max{|x1|, |x2|, ..., |x4|}.
47 | """
48 | A = Matrix(
49 | Vector(10, -1, 2, 0),
50 | Vector(-1, 11, -1, 3),
51 | Vector(2, -1, 10, -1),
52 | Vector(0, 3, -1, 8)
53 | )
54 | b = Vector(6, 25, -11, 15)
55 | system = LinearSystem(A, b)
56 | return system.solve(method='gauss_jacobi', TOL=1e-3, initial_approximation=Vector(0, 0, 0, 0))
57 |
58 | @staticmethod
59 | def problem3():
60 | """
61 | Solve problem 2 by Gauss Seidel iterative technique.
62 | """
63 | A = Matrix(
64 | Vector(10, -1, 2, 0),
65 | Vector(-1, 11, -1, 3),
66 | Vector(2, -1, 10, -1),
67 | Vector(0, 3, -1, 8)
68 | )
69 | b = Vector(6, 25, -11, 15)
70 | system = LinearSystem(A, b)
71 | return system.solve(method='gauss_seidel', TOL=1e-3, initial_approximation=Vector(0, 0, 0, 0))
72 |
73 | @staticmethod
74 | def problem4():
75 | """
76 | Use Gauss-Jacobi Iterations to attempt solving the linear system
77 | x1 + 2x2 + 3x3 = 5
78 | 2x1 - x2 + 2x3 = 1
79 | 3x1 + x2 - 2x3 = -1
80 | """
81 | A = Matrix(
82 | Vector(1, 2, 3),
83 | Vector(2, -1, 2),
84 | Vector(3, 1, -2)
85 | )
86 | b = Vector(5, 1, -1)
87 | system = LinearSystem(A, b)
88 | sol = system.solve(method='gauss_jacobi', TOL=1e-3, initial_approximation=Vector(0, 0, 0))
89 | if sol is None:
90 | return "Could not converge."
91 | return sol
92 |
93 | @staticmethod
94 | def problem5():
95 | """
96 | Use Gauss-Seidel Iterations to attempt solving the linear system
97 | 2x1 + 8x2 + 3x3 + x4 = -2
98 | 2x2 - x3 + 4x4 = 4
99 | 7x1 - 2x2 + x3 + 2x4 = 3
100 | -x1 + 2x2 + 5x3 = 5
101 | """
102 | A = Matrix(
103 | Vector(7, -2, 1, 2),
104 | Vector(2, 8, 3, 1),
105 | Vector(-1, 2, 5, 0),
106 | Vector(0, 2, -1, 4)
107 | )
108 | b = Vector(3, -2, 5, 4)
109 | system = LinearSystem(A, b)
110 | sol = system.solve(method='gauss_seidel', TOL=1e-3, initial_approximation=Vector(0, 0, 0, 0))
111 | if sol is None:
112 | return "Could not converge."
113 | return sol
114 |
--------------------------------------------------------------------------------
/examples/Problem Set 2/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(6)
4 | for i, ans in enumerate(answers):
5 | print(f'Problem {i + 1}\t {ans}')
--------------------------------------------------------------------------------
/examples/Problem Set 2/solution.py:
--------------------------------------------------------------------------------
1 | from function import Function, Polynomial, Exponent, Sin, Cos, Log
2 | import math
3 |
4 | class Solution:
5 |
6 | @staticmethod
7 | def solve(n):
8 | return getattr(Solution, f'problem{n}')()
9 |
10 | @staticmethod
11 | def solve_all(n):
12 | return [Solution.solve(i) for i in range(1, n + 1)]
13 |
14 | @staticmethod
15 | def problem1():
16 | """
17 | f(x) = x - 2 + log(x) has a root near x = 1.5. Use the Newton-Raphson formula to obtain a better estimate.
18 | """
19 | f = Polynomial(-2, 1) + Log(Polynomial(0, 1), base=10)
20 | return f.root('newton', p0=1.5)
21 |
22 | @staticmethod
23 | def problem2():
24 | """
25 | Use Newton's method, secant method and Regula Falsi method for finding the approximations of the two zeroes,
26 | one in [-1, 0] and the other in [0, 1] to withing 10^-3 accuracy of f(x) = 230x^4 + 18x^3 + 9x^2 - 221x - 9.
27 | Use the end points of the intervals as initial guesses for the secant method and the midpoint for Newton's
28 | method.
29 | """
30 | f = Polynomial(-9, -221, 9, 18, 230)
31 | return ((f.root('newton', p0=-0.5, TOLERANCE=1e-3), f.root('secant', p0=-1, p1=0, TOLERANCE=1e-3), f.root('regula_falsi', p0=-1, p1=0, TOLERANCE=1e-3)),
32 | (f.root('newton', p0=0.5, TOLERANCE=1e-3), f.root('secant', p0=0, p1=1, TOLERANCE=1e-3), f.root('regula_falsi', p0=0, p1=1, TOLERANCE=1e-3)))
33 | # The positive root is found only by Regula Falsi!
34 |
35 | @staticmethod
36 | def problem3():
37 | """
38 | Use Newton's method to find solutions accurate to within 10^-5 to the following problems.
39 | """
40 | # (a) x^3 - 2x^2 - 5 = 0 on the interval [1, 4]
41 | f = Polynomial(-5, 0, -2, 1)
42 | res1 = f.root('newton', p0=3, TOLERANCE=1e-5, return_iterations=True)
43 |
44 | # (b) x^2 - 2x e^-x + e^-2x = 0 on the interval [0, 1]
45 | f = (Polynomial(0, 1) - Exponent(Polynomial(0, -1))) ** Polynomial(2)
46 | # We will find the root of the derivative instead since the root of f is also the minimum and Newton's method fails around extrema.
47 | g = 2 * (Polynomial(0, 1) - Exponent(Polynomial(0, -1))) * (1 + Exponent(Polynomial(0, -1)))
48 | res2 = g.root('newton', p0=0.6, TOLERANCE=1e-5, return_iterations=True)
49 |
50 | # (c) x^3 - 3x^2 (2^-x) + 3x(4^-x) - 8^-x = 0 on the interval [0, 1]
51 | f = (Polynomial(0, 1) - Exponent(Polynomial(0, -1), base=2)) ** Polynomial(3)
52 | res3 = f.root('newton', p0=0.5, TOLERANCE=1e-5, return_iterations=True)
53 |
54 | return res1, res2, res3
55 |
56 | @staticmethod
57 | def problem4():
58 | """
59 | Repeat the above problem using the modified Newton's method with
60 | g(x) = x - f(x) * f'(x) / (f'(x)^2 - f(x) * f''(x))
61 | """
62 | # (a) x^3 - 2x^2 - 5 = 0 on the interval [1, 4]
63 | f = Polynomial(-5, 0, -2, 1)
64 | res1 = f.root('modified_newton', p0=3, TOLERANCE=1e-5, return_iterations=True)
65 |
66 | # (b) x^2 - 2x e^-x + e^-2x = 0 on the interval [0, 1]
67 | f = (Polynomial(0, 1) - Exponent(Polynomial(0, -1))) ** Polynomial(2)
68 | res2 = f.root('modified_newton', p0=0.6, TOLERANCE=1e-5, return_iterations=True)
69 |
70 | # (c) x^3 - 3x^2 (2^-x) + 3x(4^-x) - 8^-x = 0 on the interval [0, 1]
71 | f = (Polynomial(0, 1) - Exponent(Polynomial(0, -1), base=2)) ** Polynomial(3)
72 | res3 = f.root('modified_newton', p0=0.5, TOLERANCE=1e-5, return_iterations=True)
73 |
74 | return res1, res2, res3
75 |
76 | # This works around minima too!
77 | # This takes lesser iterations on average
78 |
79 | @staticmethod
80 | def problem5():
81 | """
82 | Use appropriate Lagrange interpolating polynomials of degree one, two and three to approximate each of the following.
83 | """
84 | # (a) f(8.4) if f(8.1) = 16.94410, f(8.3) = 17.56492, f(8.6) = 18.50515, f(8.7) = 18.82091
85 | f = Polynomial.interpolate([(8.1, 16.94410), (8.3, 17.56492), (8.6, 18.50515), (8.7, 18.82091)], method='lagrange')
86 | res1 = f(8.4)
87 |
88 | # (b) f(0.25) if f(0.1) = 0.62049958, f(0.2) = -0.28398668, f(0.3) = 0.00660095, f(0.4) = 0.24842440
89 | f = Polynomial.interpolate([(0.1, 0.62049958), (0.2, -0.28398668), (0.3, 0.00660095), (0.4, 0.24842440)], method='lagrange')
90 | res2 = f(0.25)
91 |
92 | return res1, res2
93 | # We can drop some of the points to get lower degree polynomials (these are both cubic)
94 |
95 | @staticmethod
96 | def problem6():
97 | """
98 | Construct the Lagrange interpolating polynomials for the following functions and find a bound for the absolute error
99 | on the interval [x_0, x_n]
100 | """
101 | # (a) f(x) = e^(2x) cos(3x), x0 = 0, x1 = 0.3, x2 = 0.6, n = 2
102 | f = Exponent(Polynomial(0, 2)) * Cos(Polynomial(0, 3))
103 | g = Polynomial.interpolate([0, 0.3, 0.6], method='lagrange', f=f)
104 |
105 |
106 | # (b) f(x) = sin(ln x), x0 = 2.0, x1 = 2.4, x2 = 2.6, n = 2
107 | f = Sin(Log(Polynomial(0, 1)))
108 | g = Polynomial.interpolate([2.0, 2.4, 2.6], method='lagrange', f=f)
109 |
110 | # (c) f(x) = ln x, x0 = 1, x1 = 1.1, x2 = 1.3, x3 = 1.4, n = 3
111 | f = Log(Polynomial(0, 1))
112 | g = Polynomial.interpolate([1, 1.1, 1.3, 1.4], method='lagrange', f=f)
113 |
114 | return
115 |
--------------------------------------------------------------------------------
/examples/Problem Set 2/warmup.py:
--------------------------------------------------------------------------------
1 | from function import Exponent, Polynomial
2 |
3 | # Find global minimum of e^x - x
4 | f = Exponent(Polynomial(0, 1)) - Polynomial(0, 1)
5 | g = -1 + Exponent(Polynomial(0, 1)) # derivative of e^x - x
6 | print(g.root(method='newton', p0=0.5, return_iterations=True))
7 |
8 | g.differentiate(Exponent(Polynomial(0, 1)))
9 | print(g.root(method='newton', p0=0.5, return_iterations=True))
10 | # print(f(g.root(method='newton', p0=0.5)))
11 |
12 | # Find global minimum of e^x - x^3
13 | f = Exponent(Polynomial(0, 1)) - Polynomial(0, 0, 0, 1)
14 | g = Exponent(Polynomial(0, 1)) - Polynomial(0, 0, 3) # derivative of e^x - x^3
15 | print(g.root(method='newton', p0=4, return_iterations=True))
16 | print(f(g.root(method='newton', p0=4)))
17 |
18 | g.differentiate(Exponent(Polynomial(0, 1)) - Polynomial(0, 6))
19 | # print(g.root(method='newton', p0=4, return_iterations=True))
20 |
21 | # Find P_n to Interpolate f(x) = 1/(1+x^2) with x in [-1, 1] at equally spaced points for different values of n
22 | f = 1 / Polynomial(1, 0, 1)
23 | min = -10
24 | max = 10
25 |
26 | for n in range(2, 15):
27 | # get n equally spaced points in [min, max] including the end points
28 | x = [min + i * (max - min) / (n - 1) for i in range(n)]
29 | P = Polynomial.interpolate(x, f=f)
30 | f.plot(min=-10, max=10, clear=True)
31 | P.plot(min=-10, max=10, file=f"fig{n}.png")
32 |
--------------------------------------------------------------------------------
/examples/Problem Set 3/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(4)
4 |
--------------------------------------------------------------------------------
/examples/Problem Set 3/solution.py:
--------------------------------------------------------------------------------
1 | from function import Function, Polynomial
2 | from util import Util
3 |
4 | class Solution:
5 |
6 | @staticmethod
7 | def solve(n):
8 | return getattr(Solution, f'problem{n}')()
9 |
10 | @staticmethod
11 | def solve_all(n):
12 | for i in range(1, n + 1):
13 | print(f"Problem {i}:")
14 | print(getattr(Solution, f'problem{i}')())
15 |
16 | # [Solution.solve(i) for i in range(1, n + 1)]
17 |
18 | @staticmethod
19 | def problem1():
20 | """Use Newton's forward difference formula to construct interpolating polynomials of degree 1, 2, and 3 for
21 | the following data"""
22 | # (a) f(-1/3) if f(-0.75) = -0.07181250, f(-0.5) = -0.02475, f(-0.25) = 0.3349375, f(0) = 1.101000
23 | data = [(-0.75, -0.07181250), (-0.5, -0.02475), (-0.25, 0.3349375), (0, 1.101000)]
24 | print("Part (a)")
25 | for n in range(2, 5):
26 | p1 = Polynomial.interpolate(data[:n], form='forward_diff')
27 | print(f"n = {n-1}, f(-1/3) = {p1(-1/3)}")
28 |
29 | # (b) f(0.25) if f(0.1) = -0.62049958, f(0.2) = -0.28398668, f(0.3) = 0.00660095, f(0.4) = 0.24842440
30 | data = [(0.1, -0.62049958), (0.2, -0.28398668), (0.3, 0.00660095), (0.4, 0.24842440)]
31 | print("\nPart (b)")
32 | for n in range(2, 5):
33 | p2 = Polynomial.interpolate(data[:n], form='forward_diff')
34 | print(f"n = {n-1}, f(0.25) = {p2(0.25)}")
35 |
36 | return ""
37 |
38 | @staticmethod
39 | def problem2():
40 | """Redo problem 1 using Newton's backward difference formula"""
41 | # (a) f(-1/3) if f(-0.75) = -0.07181250, f(-0.5) = -0.02475, f(-0.25) = 0.3349375, f(0) = 1.101000
42 | data = [(-0.75, -0.07181250), (-0.5, -0.02475), (-0.25, 0.3349375), (0, 1.101000)]
43 | print("Part (a)")
44 | for n in range(2, 5):
45 | p1 = Polynomial.interpolate(data[:n], form='backward_diff')
46 | print(f"n = {n-1}, f(-1/3) = {p1(-1/3)}")
47 |
48 | # (b) f(0.25) if f(0.1) = -0.62049958, f(0.2) = -0.28398668, f(0.3) = 0.00660095, f(0.4) = 0.24842440
49 | data = [(0.1, -0.62049958), (0.2, -0.28398668), (0.3, 0.00660095), (0.4, 0.24842440)]
50 | print("Part (b)")
51 | for n in range(2, 5):
52 | p2 = Polynomial.interpolate(data[:n], form='backward_diff')
53 | print(f"n = {n-1}, f(0.25) = {p2(0.25)}")
54 |
55 | return ""
56 |
57 | @staticmethod
58 | def problem3():
59 | """Find the degree of the polynomial which interpolates the following data.
60 | f(-2) = 1, f(-1) = 4, f(0) = 11, f(1) = 16, f(2) = 13, f(3) = -4"""
61 | data = [(-2, 1), (-1, 4), (0, 11), (1, 16), (2, 13), (3, -4)]
62 | p = Polynomial.interpolate(data)
63 | print(f"Forward differences: {Util.delta(1, 0, data)}, {Util.delta(2, 0, data)}, {Util.delta(3, 0, data)}, {Util.delta(4, 0, data)}, {Util.delta(5, 0, data)}")
64 | return "The degree of the polynomial is 3\n"
65 |
66 | @staticmethod
67 | def problem4():
68 | """
69 | Use appropriate Lagrange interpolating polynomials of degree 1, 2, and 3 to
70 | approximate the data given in Problem 1."""
71 | # (a) f(-1/3) if f(-0.75) = -0.07181250, f(-0.5) = -0.02475, f(-0.25) = 0.3349375, f(0) = 1.101000
72 | data = [(-0.75, -0.07181250), (-0.5, -0.02475), (-0.25, 0.3349375), (0, 1.101000)]
73 | print("Part (a)")
74 | for n in range(2, 5):
75 | p1 = Polynomial.interpolate(data[:n], method='lagrange')
76 | print(f"n = {n-1}, f(-1/3) = {p1(-1/3)}")
77 |
78 | # (b) f(0.25) if f(0.1) = -0.62049958, f(0.2) = -0.28398668, f(0.3) = 0.00660095, f(0.4) = 0.24842440
79 | data = [(0.1, -0.62049958), (0.2, -0.28398668), (0.3, 0.00660095), (0.4, 0.24842440)]
80 | print("\nPart (b)")
81 | for n in range(2, 5):
82 | p2 = Polynomial.interpolate(data[:n], method='lagrange')
83 | print(f"n = {n-1}, f(0.25) = {p2(0.25)}")
84 |
85 | return ""
86 |
--------------------------------------------------------------------------------
/examples/Problem Set 4/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(8)
4 |
--------------------------------------------------------------------------------
/examples/Problem Set 4/solution.py:
--------------------------------------------------------------------------------
1 | from function import Function, Polynomial, Cos, Sin, Exponent
2 | from util import Util
3 | import math
4 |
5 | class Solution:
6 |
7 | @staticmethod
8 | def solve(n):
9 | return getattr(Solution, f'problem{n}')()
10 |
11 | @staticmethod
12 | def solve_all(n):
13 | ans = [Solution.solve(i) for i in range(1, n + 1)]
14 | for i in range(len(ans)):
15 | print(f"Problem {i+1}: {ans[i]}")
16 |
17 | @staticmethod
18 | def problem1():
19 | """
20 | Use rectangular rule and midpoint rule to evluate the integral
21 | int_1^5 sqrt(1+x^2) dx
22 | """
23 | f = Polynomial(1, 0, 1) ** Polynomial(0.5)
24 | ans1 = f.integrate(1, 5, method='rectangular')
25 | ans2 = f.integrate(1, 5, method='midpoint')
26 | return ans1, ans2
27 |
28 | @staticmethod
29 | def problem2():
30 | """Redo problem 1 by using the trapezoidal rule."""
31 | f = Polynomial(1, 0, 1) ** Polynomial(0.5)
32 | return f.integrate(1, 5, method='trapezoidal')
33 |
34 | @staticmethod
35 | def problem3():
36 | """
37 | Use simpson's rule to find an approximate value of the integral
38 | int_4^6 1 / (3 - sqrt(x)) dx
39 | """
40 | f = 1 / (3 - Polynomial(0, 1) ** Polynomial(0.5))
41 | return f.integrate(4, 6, method='simpson')
42 |
43 | @staticmethod
44 | def problem4():
45 | """
46 | Use simpson's rule to evaluate the following.
47 | """
48 | # (a) int_0^(pi/3) cos^2(x) dx
49 | f = Cos(Polynomial(0, 1)) ** Polynomial(2)
50 | ans1 = f.integrate(0, math.pi / 3, method='simpson')
51 |
52 | # (b) int_0^(pi/3) sin^2(x) dx. Use answer to (a) to deduce an approximate value of this integral.
53 | ans2 = (math.pi / 3) - ans1
54 |
55 | return ans1, ans2
56 |
57 | @staticmethod
58 | def problem5():
59 | """
60 | Evaluate the integral int_0^4 (x^2 + cos x) dx by using the midpoint formula.
61 | """
62 | f = Polynomial(0, 0, 1) + Cos(Polynomial(0, 1))
63 | return f.integrate(0, 4, method='midpoint')
64 |
65 | @staticmethod
66 | def problem6():
67 | """
68 | Approximate the integral of f(x) = x^3 on the interval [1, 2] by using the composite trapezoidal method
69 | """
70 | f = Polynomial(0, 0, 0, 1)
71 |
72 | # (a) with four subintervals
73 | ans1 = f.integrate(1, 2, method='trapezoidal', n=4)
74 |
75 | # (b) with eight subintervals
76 | ans2 = f.integrate(1, 2, method='trapezoidal', n=8)
77 |
78 | # (c) Compute the true error in both cases
79 | f.integral(Polynomial(0, 0, 0, 0, 0.25))
80 | true_val = f.integrate(1, 2)
81 |
82 | return ans1, ans2, abs(ans1 - true_val), abs(ans2 - true_val)
83 |
84 |
85 | @staticmethod
86 | def problem7():
87 | """
88 | Redo problem 6 by using composite Simpson's rule.
89 | """
90 | f = Polynomial(0, 0, 0, 1)
91 |
92 | # (a) with four subintervals
93 | ans1 = f.integrate(1, 2, method='simpson', n=4)
94 |
95 | # (b) with eight subintervals
96 | ans2 = f.integrate(1, 2, method='simpson', n=8)
97 |
98 | # (c) Compute the true error in both cases
99 | f.integral(Polynomial(0, 0, 0, 0, 0.25))
100 | true_val = f.integrate(1, 2)
101 |
102 | return ans1, ans2, abs(ans1 - true_val), abs(ans2 - true_val)
103 |
104 | @staticmethod
105 | def problem8():
106 | """
107 | Using trapezoidal rule and Simpson's rule with n = 4 to approximate the value
108 | of the following integral and compute the true errors and approximation errors.
109 |
110 | int_0^2 e^x^2 dx
111 | """
112 | f = Exponent(Polynomial(0, 0, 1))
113 | trapz = f.integrate(0, 2, method='trapezoidal', n=4)
114 | simps = f.integrate(0, 2, method='simpson', n=4)
115 |
116 | true_val = f.integrate(0, 2, 'rectangular', n=10000)
117 |
118 | return trapz, simps, abs(trapz - true_val), abs(simps - true_val)
119 |
--------------------------------------------------------------------------------
/examples/Problem Set 5/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(6)
4 |
--------------------------------------------------------------------------------
/examples/Problem Set 5/solution.py:
--------------------------------------------------------------------------------
1 | from function import Function, Polynomial, Cos, Sin, Exponent, Log
2 | from util import Util
3 | import math
4 |
5 | class Solution:
6 |
7 | @staticmethod
8 | def solve(n):
9 | return getattr(Solution, f'problem{n}')()
10 |
11 | @staticmethod
12 | def solve_all(n):
13 | ans = [Solution.solve(i) for i in range(1, n + 1)]
14 | for i in range(len(ans)):
15 | print(f"Problem {i+1}: {ans[i]}")
16 |
17 | @staticmethod
18 | def problem1():
19 | """
20 | Approximate the integral of f(x) = x^3 5x^2 + 1 on the interval [1, 5]
21 | by using composite rectangular method and composite midpoint method.
22 | """
23 | f = Polynomial(1, 0, 5, 1)
24 |
25 | # (a) with five subintervals
26 | res1 = f.integrate(1, 5, method='rectangular', n=5)
27 | res2 = f.integrate(1, 5, method='midpoint', n=5)
28 |
29 | # (b) with ten subintervals
30 | res3 = f.integrate(1, 5, method='rectangular', n=10)
31 | res4 = f.integrate(1, 5, method='midpoint', n=10)
32 |
33 | # (c) Compute the true error in both the cases
34 | f.integral(Polynomial(0, 1, 0, 5/3, 1/4))
35 | true = f.integrate(1, 5)
36 |
37 | return {
38 | 'rectangular': {
39 | 'n=5': res1,
40 | 'n=10': res3,
41 | 'true error with n=5': abs(true - res1),
42 | 'true error with n=10': abs(true - res3)
43 | },
44 | 'midpoint': {
45 | 'n=5': res2,
46 | 'n=10': res4,
47 | 'true error with n=5': abs(true - res2),
48 | 'true error with n=10': abs(true - res4)
49 | }
50 | }
51 |
52 | @staticmethod
53 | def problem2():
54 | """Redo Problem 1 by using composite trapezoidal method and composite Simpson method."""
55 | f = Polynomial(1, 0, 5, 1)
56 |
57 | # (a) with five subintervals
58 | res1 = f.integrate(1, 5, method='trapezoidal', n=5)
59 | res2 = f.integrate(1, 5, method='simpson', n=5)
60 |
61 | # (b) with ten subintervals
62 | res3 = f.integrate(1, 5, method='trapezoidal', n=10)
63 | res4 = f.integrate(1, 5, method='simpson', n=10)
64 |
65 | # (c) Compute the true error in both the cases
66 | f.integral(Polynomial(0, 1, 0, 5/3, 1/4))
67 | true = f.integrate(1, 5)
68 |
69 | return {
70 | 'trapezoidal': {
71 | 'n=5': res1,
72 | 'n=10': res3,
73 | 'true error with n=5': abs(true - res1),
74 | 'true error with n=10': abs(true - res3)
75 | },
76 | 'simpson': {
77 | 'n=5': res2,
78 | 'n=10': res4,
79 | 'true error with n=5': abs(true - res2),
80 | 'true error with n=10': abs(true - res4)
81 | }
82 | }
83 |
84 | @staticmethod
85 | def problem3():
86 | """
87 | Evaluate the following integral by using one point Gauss quadrature and
88 | compute the true error.
89 | int_0^{pi/2} x sin(x) dx
90 | """
91 | f = Polynomial(0, 1) * Sin(Polynomial(0, 1))
92 | f.integral(Sin(Polynomial(0, 1)) - Polynomial(0, 1) * Cos(Polynomial(0, 1)))
93 |
94 | res = f.integrate(0, math.pi/2, method='gauss', n=1)
95 | true = f.integrate(0, math.pi/2)
96 |
97 | return res, abs(true - res)
98 |
99 | @staticmethod
100 | def problem4():
101 | """Redo Problem 3 by using two point Gauss quadrature formula."""
102 | f = Polynomial(0, 1) * Sin(Polynomial(0, 1))
103 | f.integral(Sin(Polynomial(0, 1)) - Polynomial(0, 1) * Cos(Polynomial(0, 1)))
104 |
105 | res = f.integrate(0, math.pi/2, method='gauss', n=2)
106 | true = f.integrate(0, math.pi/2)
107 |
108 | return res, abs(true - res)
109 |
110 | @staticmethod
111 | def problem5():
112 | """
113 | Use composite simpson's rule with n = 4 and m = 2 to approximate
114 | int_1.4^2.0 int_1.0^1.5 ln(x + 2y) dy dx
115 | """
116 | m, n = 2, 4
117 | a, b, c, d = 1.4, 2, 1, 1.5
118 |
119 | h = (d-c) / m
120 | f = lambda r: Log(Polynomial(2 * r, 1))
121 |
122 | g = (h / 6) * (
123 | f(d) + f(c) +
124 | 2 * sum(f(c + i * h) for i in range(1, m)) +
125 | 4 * sum(f(c + i * h + h/2) for i in range(m))
126 | )
127 | res = g.integrate(a, b, method='simpson', n=n)
128 |
129 | return res
130 |
131 | @staticmethod
132 | def problem6():
133 | """
134 | Redo Problem 5 using the Gaussian quadrature formula with n = 1, n = 2 in both dimensions.
135 | """
136 | a, b = 1.0, 1.5
137 | c, d = 1.4, 2.0
138 |
139 | f = lambda r: ((b-a)/2) * Log(Polynomial(2 * (((a + b)/2) + ((b-a)/2) * r), 1))
140 |
141 | g = f(-1/math.sqrt(3)) + f(1/math.sqrt(3))
142 | res = g.integrate(c, d, method='gauss', n=1)
143 |
144 | return res
145 |
--------------------------------------------------------------------------------
/examples/Problem Set 6/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(5)
4 |
--------------------------------------------------------------------------------
/examples/Problem Set 6/solution.py:
--------------------------------------------------------------------------------
1 | from function import Polynomial, Log, BivariateFunction, FirstOrderLinearODE, Exponent
2 | from util import Util
3 | import math
4 |
5 | class Solution:
6 |
7 | @staticmethod
8 | def solve(n):
9 | return getattr(Solution, f'problem{n}')()
10 |
11 | @staticmethod
12 | def solve_all(n):
13 | ans = [Solution.solve(i) for i in range(1, n + 1)]
14 | for i in range(len(ans)):
15 | print(f"Problem {i+1}: {ans[i]}")
16 |
17 | @staticmethod
18 | def problem1():
19 | """
20 | Use the forward difference formula to approximate the derivative of f(x) = ln x
21 | at x_0 = 1.8 using h = 0.1, 0.05, 0.01. Determine the bounds for the approximation errors.
22 | """
23 | f = Log(Polynomial(0, 1))
24 | a = 1.8
25 | max_err_func = lambda h: h / (2 * (a**2))
26 |
27 | # h = 0.1
28 | res1 = f.differentiate(h=0.1, method='forward')(a)
29 | b1 = max_err_func(0.1) # error bound
30 |
31 | # h = 0.05
32 | res2 = f.differentiate(h=0.05, method='forward')(a)
33 | b2 = max_err_func(0.05) # error bound
34 |
35 | # h = 0.01
36 | res3 = f.differentiate(h=0.01, method='forward')(a)
37 | b3 = max_err_func(0.01) # error bound
38 |
39 | return {
40 | 'h=0.1': {
41 | 'result': res1,
42 | 'error_bound': b1
43 | },
44 | 'h=0.05': {
45 | 'result': res2,
46 | 'error_bound': b2
47 | },
48 | 'h=0.001': {
49 | 'result': res3,
50 | 'error_bound': b3
51 | }
52 | }
53 |
54 | @staticmethod
55 | def problem2():
56 | """
57 | Redo problem 1 with the backward difference formula and the central difference formula.
58 | """
59 | f = Log(Polynomial(0, 1))
60 | a = 1.8
61 |
62 | # backward difference
63 | max_err_func = lambda h: h / (2 * ((a - h) ** 2))
64 |
65 | # h = 0.1
66 | res1 = f.differentiate(h=0.1, method='backward')(a)
67 |
68 | # h = 0.05
69 | res2 = f.differentiate(h=0.05, method='backward')(a)
70 |
71 | # h = 0.01
72 | res3 = f.differentiate(h=0.01, method='backward')(a)
73 |
74 | backward = {
75 | 'h=0.1': {
76 | 'result': res1,
77 | 'error_bound': max_err_func(0.1)
78 | },
79 | 'h=0.05': {
80 | 'result': res2,
81 | 'error_bound': max_err_func(0.05)
82 | },
83 | 'h=0.001': {
84 | 'result': res3,
85 | 'error_bound': max_err_func(0.01)
86 | }
87 | }
88 |
89 | # central difference
90 | max_err_func = lambda h: (h**2) / (3 * ((a-h)**3))
91 |
92 | # h = 0.1
93 | res1 = f.differentiate(h=0.1, method='central')(a)
94 |
95 | # h = 0.05
96 | res2 = f.differentiate(h=0.05, method='central')(a)
97 |
98 | # h = 0.01
99 | res3 = f.differentiate(h=0.01, method='central')(a)
100 |
101 | central = {
102 | 'h=0.1': {
103 | 'result': res1,
104 | 'error_bound': max_err_func(0.1)
105 | },
106 | 'h=0.05': {
107 | 'result': res2,
108 | 'error_bound': max_err_func(0.05)
109 | },
110 | 'h=0.001': {
111 | 'result': res3,
112 | 'error_bound': max_err_func(0.01)
113 | }
114 | }
115 |
116 | return {
117 | 'backward': backward,
118 | 'central': central
119 | }
120 |
121 | @staticmethod
122 | def problem3():
123 | """
124 | Consider the IVP y' = y ln y / x, y(2) = e.
125 | Use Euler's method with h = 0.1 to obtain the approximation to y(3).
126 | """
127 | f = BivariateFunction(lambda x, y: Polynomial(0, 1)(y) * Log(Polynomial(0, 1))(y) / Polynomial(0, 1)(x))
128 | a = 2
129 | b = 3
130 | IVP = FirstOrderLinearODE(f, a, b, math.e)
131 | sol = IVP.solve(h=0.1, method='euler')
132 |
133 | return sol(b)
134 |
135 | @staticmethod
136 | def problem4():
137 | """
138 | Consider the IVP y' = y - x, y(0) = 1/2.
139 | Use Euler's method with h = 0.1 and h = 0.05 to obtain the approximation to y(1).
140 | Given that the exact solution to the IVP is y(x) = x + 1 - 1/2 e^x, compare the errors in the two approximations.
141 | """
142 | a = 0
143 | b = 1
144 | f = BivariateFunction(lambda x, y: y - x)
145 | gt = (Polynomial(1, 1) - (0.5 * Exponent(Polynomial(0, 1))))(b)
146 | IVP = FirstOrderLinearODE(f, a, b, 0.5)
147 |
148 | # h = 0.1
149 | sol1 = IVP.solve(h=0.1, method='euler')(b)
150 | err1 = abs(gt - sol1)
151 |
152 | h = 0.05
153 | sol2 = IVP.solve(h=0.05, method='euler')(b)
154 | err2 = abs(gt - sol2)
155 |
156 | return {
157 | 'h=0.1': {
158 | 'result': sol1,
159 | 'error': err1
160 | },
161 | 'h=0.05': {
162 | 'result': sol2,
163 | 'error': err2
164 | }
165 | }
166 |
167 | @staticmethod
168 | def problem5():
169 | """
170 | Consider the IVP y' = 2xy^2, y(0) = 0.5.
171 | Use Euler's method with h = 0.1 to obtain the approximation to y(1).
172 | """
173 | a = 0
174 | b = 1
175 | f = BivariateFunction(lambda x, y: 2 * x * (y**2))
176 | IVP = FirstOrderLinearODE(f, a, b, 0.5)
177 | sol = IVP.solve(h=0.03, method='euler')
178 |
179 | return sol(b)
180 |
--------------------------------------------------------------------------------
/examples/Problem Set 7/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(4)
4 |
--------------------------------------------------------------------------------
/examples/Problem Set 7/solution.py:
--------------------------------------------------------------------------------
1 | from function import Function, Polynomial, Cos, Sin, Exponent, Log, FirstOrderLinearODE, BivariateFunction
2 | from util import Util
3 | import math
4 |
5 | class Solution:
6 |
7 | @staticmethod
8 | def solve(n):
9 | return getattr(Solution, f'problem{n}')()
10 |
11 | @staticmethod
12 | def solve_all(n):
13 | ans = [Solution.solve(i) for i in range(1, n + 1)]
14 | for i in range(len(ans)):
15 | print(f"Problem {i+1}: {ans[i]}")
16 |
17 | @staticmethod
18 | def problem1():
19 | """Use Taylor's series method of order 2 to approximate the solution for each
20 | of the following initial value problems.
21 | """
22 | def answer(f, a, b, y0, h):
23 | return FirstOrderLinearODE(f, a, b, y0).solve(h, method='taylor', n=2)(b)
24 |
25 | # (a) y' = y/x - (y/x)^2, y(1) = 1, 1 <= x <= 2, h = 0.1
26 | ans1 = answer(BivariateFunction(lambda x, y: y / x - (y / x)**2), 1, 2, 1, 0.1)
27 |
28 | # (b) y' = sinx + e^-x, y(0) = 0, 0 <= x <= 1, h = 0.5
29 | ans2 = answer(BivariateFunction(lambda x, y: Sin(Polynomial(0,1))(x) + Exponent(Polynomial(0, -1))(x)), 0, 1, 0, 0.5)
30 |
31 | # (c) y' = (y^2 + y)/x, y(1) = -2, 1 <= x <= 3, h = 0.5
32 | ans3 = answer(BivariateFunction(lambda x, y: (y**2 + y) / x), 1, 3, -2, 0.5)
33 |
34 | # (d) y' = -xy + 4x/y, y(0) = 1, 0 <= x <= 1, h = 0.25
35 | ans4 = answer(BivariateFunction(lambda x, y: -x*y + 4*x/y), 0, 1, 1, 0.25)
36 |
37 | return ans1, ans2, ans3, ans4
38 |
39 | @staticmethod
40 | def problem2():
41 | """
42 | Redo Problem 1 using the Runga Kutta method of order 2.
43 | """
44 | def answer(f, a, b, y0, h):
45 | return FirstOrderLinearODE(f, a, b, y0).solve(h, method='runge-kutta', n=2)(b)
46 |
47 | # (a) y' = y/x - (y/x)^2, y(1) = 1, 1 <= x <= 2, h = 0.1
48 | ans1 = answer(BivariateFunction(lambda x, y: y / x - (y / x)**2), 1, 2, 1, 0.1)
49 |
50 | # (b) y' = sinx + e^-x, y(0) = 0, 0 <= x <= 1, h = 0.5
51 | ans2 = answer(BivariateFunction(lambda x, y: Sin(Polynomial(0,1))(x) + Exponent(Polynomial(0, -1))(x)), 0, 1, 0, 0.5)
52 |
53 | # (c) y' = (y^2 + y)/x, y(1) = -2, 1 <= x <= 3, h = 0.5
54 | ans3 = answer(BivariateFunction(lambda x, y: (y**2 + y) / x), 1, 3, -2, 0.5)
55 |
56 | # (d) y' = -xy + 4x/y, y(0) = 1, 0 <= x <= 1, h = 0.25
57 | ans4 = answer(BivariateFunction(lambda x, y: -x*y + 4*x/y), 0, 1, 1, 0.25)
58 |
59 | return ans1, ans2, ans3, ans4
60 |
61 | @staticmethod
62 | def problem3():
63 | """
64 | Redo Problem 1 using the Trapezoidal method.
65 | """
66 | def answer(f, a, b, y0, h):
67 | return FirstOrderLinearODE(f, a, b, y0).solve(h, method='trapezoidal')(b)
68 |
69 | # (a) y' = y/x - (y/x)^2, y(1) = 1, 1 <= x <= 2, h = 0.1
70 | ans1 = answer(BivariateFunction(lambda x, y: y / x - (y / x)**2), 1, 2, 1, 0.1)
71 |
72 | # (b) y' = sinx + e^-x, y(0) = 0, 0 <= x <= 1, h = 0.5
73 | ans2 = answer(BivariateFunction(lambda x, y: Sin(Polynomial(0,1))(x) + Exponent(Polynomial(0, -1))(x)), 0, 1, 0, 0.5)
74 |
75 | # (c) y' = (y^2 + y)/x, y(1) = -2, 1 <= x <= 3, h = 0.5
76 | ans3 = answer(BivariateFunction(lambda x, y: (y**2 + y) / x), 1, 3, -2, 0.5)
77 |
78 | # (d) y' = -xy + 4x/y, y(0) = 1, 0 <= x <= 1, h = 0.25
79 | ans4 = answer(BivariateFunction(lambda x, y: -x*y + 4*x/y), 0, 1, 1, 0.25)
80 |
81 | return ans1, ans2, ans3, ans4
82 |
83 |
84 | @staticmethod
85 | def problem4():
86 | """
87 | Using the Taylor's series method of order 2, Runga Kutta method of order 2, and Trapezoidal method to approximate the solution of the following initial value problems and compare the
88 | results.
89 | """
90 | def answer(f, a, b, y0, h, gt):
91 | sol1 = FirstOrderLinearODE(f, a, b, y0).solve(h, method='taylor', n=2)(b)
92 | err1 = abs(gt(b) - sol1)
93 |
94 | sol2 = FirstOrderLinearODE(f, a, b, y0).solve(h, method='runge-kutta', n=2)(b)
95 | err2 = abs(gt(b) - sol2)
96 |
97 | sol3 = FirstOrderLinearODE(f, a, b, y0).solve(h, method='trapezoidal')(b)
98 | err3 = abs(gt(b) - sol3)
99 |
100 | return {
101 | 'taylor': {
102 | 'result': sol1,
103 | 'error': err1
104 | },
105 | 'runge-kutta': {
106 | 'result': sol2,
107 | 'error': err2
108 | },
109 | 'trapezoidal': {
110 | 'result': sol3,
111 | 'error': err3
112 | }
113 | }
114 |
115 | # (a) y' = xe^(3x) - 2y, y(0) = 0, 0 <= x <= 1, h = 0.1
116 | # actual solution: y = (1/5)xe^(3x) - (1/25)e^(3x) + (1/25)e^(-2x)
117 | ans1 = answer(BivariateFunction(lambda x, y: x * Exponent(Polynomial(0, 3))(x) - 2 * y),
118 | 0, 1, 0, 0.1,
119 | Polynomial(0, 1/5) * Exponent(Polynomial(0, 3)) - (1/25) * Exponent(Polynomial(0, 3)) + (1/25) * Exponent(Polynomial(0, -2)))
120 |
121 | # (b) y' = 1 + (x-y)^2, y(2) = 1, 2 <= x <= 3, h = 0.5
122 | # actual solution: y = x + 1/(1-x)
123 | ans2 = answer(BivariateFunction(lambda x, y: 1 + (x - y)**2),
124 | 2, 3, 1, 0.5,
125 | Polynomial(0, 1) + (1 / (Polynomial(1, -1))))
126 |
127 | # (c) y' = 1 + y/x, y(1) = 1, 1 <= x <= 2, h = 0.25
128 | # actual solution: y = xlnx + 2x
129 | ans3 = answer(BivariateFunction(lambda x, y: 1 + y / x),
130 | 1, 2, 1, 0.25,
131 | Polynomial(0, 1) * Log(Polynomial(0, 1)) + Polynomial(0, 2))
132 |
133 | return {
134 | 'a': ans1,
135 | 'b': ans2,
136 | 'c': ans3
137 | }
138 |
--------------------------------------------------------------------------------
/examples/Problem Set 8/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(5)
4 |
--------------------------------------------------------------------------------
/examples/Problem Set 8/solution.py:
--------------------------------------------------------------------------------
1 | from function import Function, Polynomial, Cos, Sin, Exponent, Log, FirstOrderLinearODE, BivariateFunction, Vector, MultiVariableFunction
2 | from util import Util
3 | import math
4 |
5 | class Solution:
6 |
7 | @staticmethod
8 | def solve(n):
9 | return getattr(Solution, f'problem{n}')()
10 |
11 | @staticmethod
12 | def solve_all(n):
13 | ans = [Solution.solve(i) for i in range(1, n + 1)]
14 | for i in range(len(ans)):
15 | print(f"Problem {i+1}: {ans[i]}")
16 |
17 | @staticmethod
18 | def problem1():
19 | """
20 | Use two step Adam-Bashforth explicit method to approximate
21 | the solutions of the following initial value problems. Compute
22 | the value of the solution at the end point of the interval and
23 | find the error.
24 | """
25 | def answer(f, a, b, y0, h, gt):
26 | sol = FirstOrderLinearODE(f, a, b, y0).solve(h, method='adam-bashforth', step=2, points=[gt(a+h)])(b)
27 | return {
28 | 'solution': sol,
29 | 'error': abs(gt(b) - sol)
30 | }
31 |
32 | # (a) y' = t e^(3t) - 2y, y(0) = 0, 0 <= t <= 1, h = 0.2
33 | # actual solution: y = (1/5) t e^(3t) - (1/25) e^(3t) + (1/25) e^(-2t)
34 | ans1 = answer(BivariateFunction(lambda t, y: t * Exponent(Polynomial(0, 3))(t) - 2*y),
35 | 0, 1, 0, 0.2,
36 | Polynomial(0, 1/5) * Exponent(Polynomial(0, 3)) - (1/25) * Exponent(Polynomial(0, 3)) + (1/25) * Exponent(Polynomial(0, -2)))
37 |
38 | # (b) y' = 1 + (t - y)^2, y(2) = 1, 2 <= t <= 3, h = 0.2
39 | # actual solution: y = t + 1/(1-t)
40 | ans2 = answer(BivariateFunction(lambda t, y: 1 + (t - y)**2),
41 | 2, 3, 1, 0.2,
42 | Polynomial(0, 1) + 1 / (Polynomial(1, -1)))
43 |
44 | # (c) y' = 1 + y/t, y(1) = 2, 1 <= t <= 2, h = 0.2
45 | # actual solution: y = tlnt + 2t
46 | ans3 = answer(BivariateFunction(lambda t, y: 1 + y/t),
47 | 1, 2, 2, 0.2,
48 | Polynomial(0, 1) * Log(Polynomial(0, 1)) + Polynomial(0, 2))
49 |
50 | return {
51 | 'a': ans1,
52 | 'b': ans2,
53 | 'c': ans3
54 | }
55 |
56 | @staticmethod
57 | def problem2():
58 | """
59 | Redo problem 1 by the two step Adams Moulton implicit method.
60 | Compare the results with Adam-Bashforth explicit method
61 | """
62 | def answer(f, a, b, y0, h, gt):
63 | sol = FirstOrderLinearODE(f, a, b, y0).solve(h, method='adam-moulton', step=2, points=[gt(a+h)])(b)
64 | return {
65 | 'solution': sol,
66 | 'error': abs(gt(b) - sol)
67 | }
68 |
69 | # (a) y' = t e^(3t) - 2y, y(0) = 0, 0 <= t <= 1, h = 0.2
70 | # actual solution: y = (1/5) t e^(3t) - (1/25) e^(3t) + (1/25) e^(-2t)
71 | ans1 = answer(BivariateFunction(lambda t, y: t * Exponent(Polynomial(0, 3))(t) - 2*y),
72 | 0, 1, 0, 0.2,
73 | Polynomial(0, 1/5) * Exponent(Polynomial(0, 3)) - (1/25) * Exponent(Polynomial(0, 3)) + (1/25) * Exponent(Polynomial(0, -2)))
74 |
75 | # (b) y' = 1 + (t - y)^2, y(2) = 1, 2 <= t <= 3, h = 0.2
76 | # actual solution: y = t + 1/(1-t)
77 | ans2 = answer(BivariateFunction(lambda t, y: 1 + (t - y)**2),
78 | 2, 3, 1, 0.2,
79 | Polynomial(0, 1) + 1 / (Polynomial(1, -1)))
80 |
81 | # (c) y' = 1 + y/t, y(1) = 2, 1 <= t <= 2, h = 0.2
82 | # actual solution: y = tlnt + 2t
83 | ans3 = answer(BivariateFunction(lambda t, y: 1 + y/t),
84 | 1, 2, 2, 0.2,
85 | Polynomial(0, 1) * Log(Polynomial(0, 1)) + Polynomial(0, 2))
86 |
87 | return {
88 | 'a': ans1,
89 | 'b': ans2,
90 | 'c': ans3
91 | }
92 |
93 |
94 | @staticmethod
95 | def problem3():
96 | """
97 | Redo problem 1 by the three step Adams Bashforth explicit
98 | method and three step Adams Moulton implicit method. Compare the results.
99 | """
100 | def answer(f, a, b, y0, h, gt):
101 | sol1 = FirstOrderLinearODE(f, a, b, y0).solve(h, method='adam-bashforth', step=3, points=[gt(a+h), gt(a + 2 * h)])(b)
102 | sol2 = FirstOrderLinearODE(f, a, b, y0).solve(h, method='adam-moulton', step=3, points=[gt(a+h), gt(a + 2 * h)])(b)
103 | return {
104 | 'adam-bashforth': {
105 | 'solution': sol1,
106 | 'error': abs(gt(b) - sol1)
107 | },
108 | 'adam-moulton': {
109 | 'solution': sol2,
110 | 'error': abs(gt(b) - sol2)
111 | }
112 | }
113 |
114 | # (a) y' = t e^(3t) - 2y, y(0) = 0, 0 <= t <= 1, h = 0.2
115 | # actual solution: y = (1/5) t e^(3t) - (1/25) e^(3t) + (1/25) e^(-2t)
116 | ans1 = answer(BivariateFunction(lambda t, y: t * Exponent(Polynomial(0, 3))(t) - 2*y),
117 | 0, 1, 0, 0.2,
118 | Polynomial(0, 1/5) * Exponent(Polynomial(0, 3)) - (1/25) * Exponent(Polynomial(0, 3)) + (1/25) * Exponent(Polynomial(0, -2)))
119 |
120 | # (b) y' = 1 + (t - y)^2, y(2) = 1, 2 <= t <= 3, h = 0.2
121 | # actual solution: y = t + 1/(1-t)
122 | ans2 = answer(BivariateFunction(lambda t, y: 1 + (t - y)**2),
123 | 2, 3, 1, 0.2,
124 | Polynomial(0, 1) + 1 / (Polynomial(1, -1)))
125 |
126 | # (c) y' = 1 + y/t, y(1) = 2, 1 <= t <= 2, h = 0.2
127 | # actual solution: y = tlnt + 2t
128 | ans3 = answer(BivariateFunction(lambda t, y: 1 + y/t),
129 | 1, 2, 2, 0.2,
130 | Polynomial(0, 1) * Log(Polynomial(0, 1)) + Polynomial(0, 2))
131 |
132 | return {
133 | 'a': ans1,
134 | 'b': ans2,
135 | 'c': ans3
136 | }
137 |
138 | @staticmethod
139 | def problem4():
140 | """
141 | Apply the Adams fourth order predictor corrector method with
142 | h = 0.2 and starting values from the Runge Kutta fourth order
143 | method to the initial value problem
144 | y' = y - t^2 + 1, 0 <= t <= 2, y(0) = 0.5
145 | """
146 | f = BivariateFunction(lambda t, y: y - t**2 + 1)
147 | a = 0
148 | b = 2
149 | y0 = 0.5
150 | h = 0.2
151 | IVP = FirstOrderLinearODE(f, a, b, y0)
152 | sol = IVP.solve(h, method='predictor-corrector')
153 |
154 | return sol(b)
155 |
156 | @staticmethod
157 | def problem5():
158 | """
159 | Use the Runge Kutta method of order two to approximate the
160 | solution of the following problem and compare the result to the
161 | actual solution.
162 | u1' = 3u1 + 2u2 - (2t^2 + 1)e^(2t), u1(0) = 1,
163 | u2' = 4u1 + u2 + (t^2 + 2t - 4)e^(2t), u2(0) = 0,
164 | 0 <= t <= 1, h = 0.2
165 |
166 | actual solution: u1 = (1/3) e^(5t) - (1/3) e^(-t) + e^(2t),
167 | u2 = (1/3) e^(5t) + (2/3) e^(-t) + t^2 e^(2t)
168 | """
169 | a = 0
170 | b = 1
171 | h = 0.2
172 | U0 = Vector(1, 0)
173 | F = Vector(
174 | MultiVariableFunction(lambda t, u1, u2: 3*u1 + 2*u2 - (2*(t**2) + 1)*Exponent(Polynomial(0, 2))(t)),
175 | MultiVariableFunction(lambda t, u1, u2: 4*u1 + u2 + (t**2 + 2*t - 4)*Exponent(Polynomial(0, 2))(t))
176 | )
177 |
178 | IVP = FirstOrderLinearODE(F, a, b, U0)
179 | sol = IVP.solve(h, method='runge-kutta', n=2)
180 |
181 | GT = [
182 | (1/3) * Exponent(Polynomial(0, 5)) - (1/3) * Exponent(Polynomial(0, -1)) + Exponent(Polynomial(0, 2)),
183 | (1/3) * Exponent(Polynomial(0, 5)) + (2/3) * Exponent(Polynomial(0, -1)) + Polynomial(0, 0, 1) * Exponent(Polynomial(0, 2))
184 | ]
185 |
186 | return {
187 | 'u1': {
188 | 'result': sol[-1][0],
189 | 'error': abs(GT[0](b) - sol[-1][0])
190 | },
191 | 'u2': {
192 | 'result': sol[-1][1],
193 | 'error': abs(GT[1](b) - sol[-1][1])
194 | }
195 | }
196 |
--------------------------------------------------------------------------------
/examples/Problem Set 9/main.py:
--------------------------------------------------------------------------------
1 | from solution import Solution
2 |
3 | answers = Solution.solve_all(4)
4 |
--------------------------------------------------------------------------------
/examples/Problem Set 9/solution.py:
--------------------------------------------------------------------------------
1 | from function import Function, Polynomial, Cos, Sin, Exponent, Log, SecondOrderLinearODE_BVP, MultiVariableFunction, SecondOrderODE_BVP
2 | from util import Util
3 | import math
4 |
5 | class Solution:
6 |
7 | @staticmethod
8 | def solve(n):
9 | return getattr(Solution, f'problem{n}')()
10 |
11 | @staticmethod
12 | def solve_all(n):
13 | ans = [Solution.solve(i) for i in range(1, n + 1)]
14 | for i in range(len(ans)):
15 | print(f"Problem {i+1}: {ans[i]}")
16 |
17 | @staticmethod
18 | def problem1():
19 | """
20 | Apply the Linear Shooting technique with N = 10 to the boundary value problem
21 | y'' = -(2/x)y' + (2/x^2)y + sin(ln(x))/x^2, 1 <= x <= 2, y(1) = 1, y(2) = 2
22 | and compare the result to those of the exact solution
23 | y = c1x + c2/x^2 -(3/10)sin(ln(x)) - (1/10)cos(ln(x)) where c1 = 1.139 and c2 = -0.039
24 | """
25 | a = 1
26 | b = 2
27 | N = 10
28 | h = (b - a) / (N+1)
29 |
30 | BVP = SecondOrderLinearODE_BVP(
31 | Function(lambda x: -(2/x)),
32 | Function(lambda x: (2/x**2)),
33 | Function(lambda x: Sin(Log(Polynomial(0, 1)))(x) / (x**2)),
34 | a, b, y0=1, y1=2
35 | )
36 |
37 | sol = BVP.solve(h, method='shooting')
38 |
39 | GT = Polynomial(0, 1.139) - 0.039 / Polynomial(0, 0, 1) - (3/10) * Sin(Log(Polynomial(0, 1))) - (1/10) * Cos(Log(Polynomial(0, 1)))
40 |
41 | return {
42 | 'solution': [sol(a + i * h) for i in range(N + 1)],
43 | 'ground_truth': [GT(a + i * h) for i in range(N + 1)],
44 | 'errors': [abs(sol(a + i * h) - GT(a + i * h)) for i in range(N + 1)]
45 | }
46 |
47 |
48 |
49 | @staticmethod
50 | def problem2():
51 | """
52 | The boundary value problem
53 | y'' = 4(y-x), 0 <= x <= 1, y(0) = 0, y(1) = 2
54 | has the solution y(x) = (e^2)(e^4 - 1)^(-1)(e^(2x) - e^(-2x)) + x. Use the Linear
55 | Shooting method to approximate the solution and compare the result to the actual
56 | solution for h = 1/4.
57 | """
58 | a = 0
59 | b = 1
60 | h = 1/4
61 |
62 | BVP = SecondOrderLinearODE_BVP(
63 | Function(lambda x: 0),
64 | Function(lambda x: 4),
65 | Function(lambda x: -4 * x),
66 | a, b, y0=0, y1=2
67 | )
68 |
69 | sol = BVP.solve(h, method='shooting')
70 |
71 | GT = ((math.e ** 2) / (math.e ** 4 - 1)) * (Exponent(Polynomial(0, 2)) - Exponent(Polynomial(0, -2))) + Polynomial(0, 1)
72 |
73 | return {
74 | 'solution': [sol(a + i * h) for i in range(int((b - a) / h) + 1)],
75 | 'ground_truth': [GT(a + i * h) for i in range(int((b - a) / h) + 1)],
76 | 'errors': [abs(sol(a + i * h) - GT(a + i * h)) for i in range(int((b - a) / h) + 1)]
77 | }
78 |
79 | @staticmethod
80 | def problem3():
81 | """
82 | Apply the shooting method with Newton's method to the boundary value problem
83 | y'' = (1/8)(32 + 2x^3 - yy'), 1 <= x <= 3, y(1) = 17, y(3) = 43/3.
84 | Use N = 20, M = 10 and TOL = 10^(-5), and compare the results with the exact solution
85 | y(x) = x^2 + 16/x.
86 | """
87 | a = 1
88 | b = 3
89 | N = 20
90 | h = (b - a) / (N + 1)
91 |
92 | f = MultiVariableFunction(lambda x, y, z: (1/8) * (32 + 2 * x**3 - y * z))
93 | BVP = SecondOrderODE_BVP(f, a, b, y0=17, y1=43/3)
94 | sol = BVP.solve(h, method='shooting_newton', M=100, TOL=1e-5)
95 |
96 | GT = Polynomial(0, 0, 1) + 16 / Polynomial(0, 1)
97 |
98 | return {
99 | 'solution': [sol(a + i * h) for i in range(N + 1)],
100 | 'ground_truth': [GT(a + i * h) for i in range(N + 1)],
101 | 'errors': [abs(sol(a + i * h) - GT(a + i * h)) for i in range(N + 1)]
102 | }
103 |
104 | @staticmethod
105 | def problem4():
106 | """
107 | Use the Nonlinear Shooting method with h = 0.5 to approximate the solution to
108 | the boundary value problem
109 | y'' = -(y')**2 - y + lnx, 1 <= x <= 2, y(1) = 0, y(2) = ln2
110 |
111 | Compare the results to the actual solution y(x) = ln(x).
112 | """
113 | a = 1
114 | b = 2
115 | h = 0.5
116 |
117 | f = MultiVariableFunction(lambda x, y, z: -(z**2) - y + Log(Polynomial(0, 1))(x))
118 | BVP = SecondOrderODE_BVP(f, a, b, y0=0, y1=math.log(2))
119 | sol = BVP.solve(h, method='shooting_newton')
120 |
121 | GT = Log(Polynomial(0, 1))
122 |
123 | return {
124 | 'solution': [sol(a + i * h) for i in range(int((b - a) / h) + 1)],
125 | 'ground_truth': [GT(a + i * h) for i in range(int((b - a) / h) + 1)],
126 | 'errors': [abs(sol(a + i * h) - GT(a + i * h)) for i in range(int((b - a) / h) + 1)]
127 | }
128 |
--------------------------------------------------------------------------------
/function.py:
--------------------------------------------------------------------------------
1 | from typing import Literal, Callable
2 | import math
3 |
4 | class Function:
5 |
6 | def __init__(self, function):
7 | self.function = function
8 |
9 | def __call__(self, x):
10 | if callable(x):
11 | return Function(lambda p: self(x(p)))
12 | return self.function(x)
13 |
14 | def __truediv__(f, g):
15 | return Function(lambda x: f(x) / g(x))
16 |
17 | def __mul__(f, g):
18 | return Function(lambda x: f(x) * g(x))
19 |
20 | def __add__(f, g):
21 | return Function(lambda x: f(x) + g(x))
22 |
23 | def __sub__(f, g):
24 | return Function(lambda x: f(x) - g(x))
25 |
26 | def __pow__(f, g):
27 | return Function(lambda x: f(x) ** g(x))
28 |
29 | def __rtruediv__(f, g):
30 | return Function(lambda x: g / f(x))
31 |
32 | def __rmul__(f, g):
33 | return Function(lambda x: g * f(x))
34 |
35 | def __radd__(f, g):
36 | return Function(lambda x: g + f(x))
37 |
38 | def __rsub__(f, g):
39 | return Function(lambda x: g - f(x))
40 |
41 | def __neg__(f):
42 | return Function(lambda x: -f(x))
43 |
44 | def differentiate(self, func=None, h=1e-5, method:Literal['forward', 'backward', 'central']='forward'):
45 | """
46 | Sets or returns the derivative of the function.
47 | If func is None, returns the derivative.
48 | If func is a Function, sets the derivative to func.
49 | If func is a lambda, sets the derivative to a Function with the lambda.
50 | """
51 | if isinstance(func, Function):
52 | self.derivative = func
53 | elif callable(func):
54 | self.derivative = Function(func)
55 | else:
56 | if method == 'forward':
57 | return self.differentiate_forward(h)
58 | if method == 'backward':
59 | return self.differentiate_forward(-h)
60 | if method == 'central':
61 | return self.differentiate_central(h)
62 |
63 | raise ValueError("Invalid method.")
64 |
65 | def differentiate_forward(self, h):
66 | return Function(lambda x: (self(x + h) - self(x)) / h)
67 |
68 | def differentiate_central(self, h):
69 | return Function(lambda x: (self(x + h) - self(x - h)) / (2 * h))
70 |
71 | def multi_differentiate(self, n: int, h=1e-5, method:Literal['forward', 'backward', 'central']='forward'):
72 | """
73 | Returns the nth derivative of the function.
74 | """
75 | if n == 0:
76 | return self
77 | return self.differentiate(h=h, method=method).multi_differentiate(n - 1, h, method)
78 |
79 | def integral(self, func=None, h=1e-5):
80 | """
81 | Sets or returns the integral of the function.
82 | If func is None, returns the integral.
83 | If func is a Function, sets the integral to func.
84 | If func is a lambda, sets the integral to a Function with the lambda.
85 | """
86 | if isinstance(func, Function):
87 | self.integral = func
88 | elif callable(func):
89 | self.integral = Function(func)
90 | else:
91 | raise NotImplementedError("Not implemented yet.")
92 |
93 | def integrate(self, a: float, b: float, method: Literal['rectangular', 'midpoint', 'trapezoidal', 'simpson', 'gauss']=None, n: int=None):
94 | """
95 | Definite integral of the function from a to b.
96 | """
97 | if method == 'rectangular':
98 | return self.integrate_rectangular(a, b, n)
99 | if method == 'midpoint':
100 | return self.integrate_midpoint(a, b, n)
101 | if method == 'trapezoidal':
102 | return self.integrate_trapezoidal(a, b, n)
103 | if method == 'simpson':
104 | return self.integrate_simpson(a, b, n)
105 | if method == 'gauss':
106 | return self.integrate_gauss(a, b, n)
107 |
108 | if hasattr(self, 'integral'):
109 | return self.integral(b) - self.integral(a)
110 |
111 | raise ValueError("Invalid method.")
112 |
113 | def integrate_rectangular(self, a: float, b: float, n: int = None):
114 | if not n:
115 | return (b - a) * self(a)
116 |
117 | h = (b - a) / n
118 | return h * sum(self(a + i * h) for i in range(n))
119 |
120 | def integrate_midpoint(self, a: float, b: float, n: int = None):
121 | if not n:
122 | return (b - a) * self((a + b) / 2)
123 |
124 | h = (b - a) / n
125 | return h * sum(self(a + i * h + h / 2) for i in range(n))
126 |
127 | def integrate_trapezoidal(self, a: float, b: float, n: int = None):
128 | if not n:
129 | return (b - a) * (self(a) + self(b)) / 2
130 |
131 | h = (b - a) / n
132 | return h * (self(a) + 2 * sum(self(a + i * h) for i in range(1, n)) + self(b)) / 2
133 |
134 | def integrate_simpson(self, a: float, b: float, n: int = None):
135 | if not n:
136 | return (b - a) * (self(a) + 4 * self((a + b) / 2) + self(b)) / 6
137 |
138 | h = (b - a) / n
139 | return h * (self(a) + 4 * sum(self(a + i * h + h / 2) for i in range(n)) + 2 * sum(self(a + i * h) for i in range(1, n)) + self(b)) / 6
140 |
141 | def integrate_gauss(self, a: float, b: float, n: int = None):
142 | t = Polynomial((a+b)/2, (b-a)/2)
143 | g = ((b-a)/2) * self(t)
144 | if n == 1:
145 | return 2 * g(0)
146 | if n == 2:
147 | return g(-1/math.sqrt(3)) + g(1/math.sqrt(3))
148 |
149 | raise NotImplementedError("Not implemented except for n=1 and n=2.")
150 |
151 |
152 | def root(self, method: Literal["bisection", "newton", "secant", "regula_falsi", "modified_newton"],
153 | a: float = None, b: float = None,
154 | p0: float = None, p1: float = None,
155 | TOLERANCE=1e-10, N=100,
156 | return_iterations=False, early_stop: int=None):
157 |
158 | if method == "bisection":
159 | assert a is not None, "a must be defined"
160 | assert b is not None, "b must be defined"
161 | assert a < b, "a must be less than b"
162 | assert self(a) * self(b) < 0, "f(a) and f(b) must have opposite signs"
163 |
164 | sol, n = self.bisection(a, b, TOLERANCE, N, early_stop)
165 | if return_iterations:
166 | return sol, n
167 | return sol
168 |
169 | if method == "newton":
170 | assert p0 is not None, "p0 must be defined"
171 |
172 | sol, n = self.newton(p0, TOLERANCE, N, early_stop)
173 | if return_iterations:
174 | return sol, n
175 | return sol
176 |
177 | if method == "secant":
178 | assert p0 is not None, "p0 must be defined"
179 | assert p1 is not None, "p1 must be defined"
180 |
181 | sol, n = self.secant(p0, p1, TOLERANCE, N, early_stop)
182 | if return_iterations:
183 | return sol, n
184 | return sol
185 |
186 | if method == "regula_falsi":
187 | assert p0 is not None, "p0 must be defined"
188 | assert p1 is not None, "p1 must be defined"
189 | assert self(p0) * self(p1) < 0, "f(p0) and f(p1) must have opposite signs"
190 |
191 | sol, n = self.regula_falsi(p0, p1, TOLERANCE, N, early_stop)
192 | if return_iterations:
193 | return sol, n
194 | return sol
195 |
196 | if method == "modified_newton":
197 | assert p0 is not None, "p0 must be defined"
198 |
199 | sol, n = self.modified_newton(p0, TOLERANCE, N, early_stop)
200 | if return_iterations:
201 | return sol, n
202 | return sol
203 |
204 |
205 | raise ValueError("Invalid method.")
206 |
207 | def bisection(self, a: float, b: float, TOLERANCE=1e-10, N=100, early_stop: int=None):
208 | for i in range(N):
209 | p = (a + b) / 2
210 | if self(p) == 0 or abs(a - b) < TOLERANCE or (early_stop is not None and i >= early_stop):
211 | return p, i + 1
212 | if self(a) * self(p) > 0:
213 | a = p
214 | else:
215 | b = p
216 | return None, N
217 |
218 | def newton(self, p0: float, TOLERANCE=1e-10, N=100, early_stop: int=None):
219 | deriv = self.differentiate()
220 |
221 | try:
222 | for i in range(N):
223 | p = p0 - self(p0) / deriv(p0)
224 | if abs(p - p0) < TOLERANCE or (early_stop is not None and i >= early_stop):
225 | return p, i + 1
226 | p0 = p
227 | return None, N
228 | except ZeroDivisionError or OverflowError:
229 | return None, i
230 |
231 | def modified_newton(self, p0: float, TOLERANCE=1e-10, N=100, early_stop: int=None):
232 | deriv = self.differentiate()
233 | double_deriv = deriv.differentiate()
234 |
235 | try:
236 | for i in range(N):
237 | p = p0 - self(p0) * deriv(p0) / (deriv(p0) ** 2 - self(p0) * double_deriv(p0))
238 | if abs(p - p0) < TOLERANCE or (early_stop is not None and i >= early_stop):
239 | return p, i + 1
240 | p0 = p
241 | return None, N
242 | except ZeroDivisionError or OverflowError:
243 | return None, i
244 |
245 | def secant(self, p0: float, p1: float, TOLERANCE=1e-10, N=100, early_stop: int=None):
246 | for i in range(N):
247 | p = p1 - self(p1) * (p1 - p0) / (self(p1) - self(p0))
248 | if abs(p - p1) < TOLERANCE or (early_stop is not None and i >= early_stop):
249 | return p, i + 1
250 | p0 = p1
251 | p1 = p
252 | return None, N
253 |
254 | def regula_falsi(self, p0: float, p1: float, TOLERANCE=1e-10, N=100, early_stop: int=None):
255 | for i in range(N):
256 | p = p1 - self(p1) * (p1 - p0) / (self(p1) - self(p0))
257 | if abs(p - p1) < TOLERANCE or (early_stop is not None and i >= early_stop):
258 | return p, i + 1
259 | if self(p0) * self(p) > 0:
260 | p0 = p1
261 | p1 = p
262 | return None, N
263 |
264 | def fixed_point(self, p0: float, TOLERANCE=1e-10, N=100):
265 | assert p0 is not None, "p0 must be defined"
266 |
267 | try:
268 | for i in range(N):
269 | p = self(p0)
270 | if abs(p - p0) < TOLERANCE:
271 | return p
272 | p0 = p
273 | return None
274 | except OverflowError:
275 | return None
276 |
277 | def plot(self, min: float, max: float, N=1000, file: str="", clear: bool=False):
278 | import numpy as np
279 | import matplotlib.pyplot as plt
280 |
281 | # get N equally spaced points in [min, max]
282 | x = [min + (i/N) * + (max - min) for i in range(N)]
283 | y = [self(t) for t in x]
284 |
285 | if clear:
286 | plt.clf()
287 | plt.plot(x, y)
288 | plt.xlabel('x')
289 | plt.ylabel('y')
290 | if file:
291 | plt.savefig(file)
292 | else:
293 | plt.show()
294 |
295 |
296 | class Polynomial(Function):
297 |
298 | def __init__(self, *coefficients):
299 | """
300 | coefficients are in the form a_0, a_1, ... a_n
301 | """
302 | self.function = lambda x: sum(a * x ** i for i, a in enumerate(coefficients))
303 | self.coefficients = coefficients
304 |
305 | @staticmethod
306 | def interpolate(data: tuple, method: Literal["lagrange", "newton"]='newton', f: Function=None,
307 | form: Literal["standard", "backward_diff", "forward_diff"]='standard'):
308 | """
309 | data is a list of (x, y) tuples.
310 | alternative: f is a Function that returns the y values and data is a list of x values.
311 | """
312 | if f is not None:
313 | data = [(x, f(x)) for x in data]
314 |
315 | if method == "lagrange":
316 | return Polynomial.interpolate_lagrange(data)
317 | if method == "newton":
318 | if form == "standard":
319 | return Polynomial.interpolate_newton(data)
320 | if form == "backward_diff":
321 | return Polynomial.interpolate_newton_backward_diff(data)
322 | if form == "forward_diff":
323 | return Polynomial.interpolate_newton_forward_diff(data)
324 | raise ValueError("Invalid method.")
325 |
326 | @staticmethod
327 | def interpolate_lagrange(data: tuple):
328 | """
329 | data is a tuple of (x, y) tuples
330 | """
331 | n = len(data)
332 | x = [data[i][0] for i in range(n)]
333 | y = [data[i][1] for i in range(n)]
334 | p = Polynomial(0)
335 |
336 | for i in range(n):
337 | L = Polynomial(1)
338 | for j in range(n):
339 | if i != j:
340 | L *= Polynomial(-x[j], 1) / Polynomial(x[i] - x[j])
341 | p += y[i] * L
342 |
343 | return p
344 |
345 | @staticmethod
346 | def interpolate_newton(data: tuple):
347 | """
348 | data is a tuple of (x, y) tuples
349 | """
350 | n = len(data)
351 | x = [data[i][0] for i in range(n)]
352 | y = [data[i][1] for i in range(n)]
353 |
354 | def divided_difference(i, j):
355 | if i == j:
356 | return y[i]
357 | return (divided_difference(i + 1, j) - divided_difference(i, j - 1)) / (x[j] - x[i])
358 |
359 | def factor_product(roots: list):
360 | if not roots:
361 | return Polynomial(1)
362 | return Polynomial(-roots[0], 1) * factor_product(roots[1:])
363 |
364 | coefficients = [divided_difference(0, i) for i in range(n)]
365 |
366 | p = Polynomial(coefficients[0])
367 | for i in range(1, n):
368 | p = p + coefficients[i] * factor_product(x[:i])
369 |
370 | return p
371 |
372 | @staticmethod
373 | def interpolate_newton_backward_diff(data: tuple):
374 | """
375 | data is a tuple of (x, y) tuples
376 | """
377 | from util import Util
378 | data = sorted(data, key=lambda x: x[0])
379 | diffs = sorted([data[i+1][0] - data[i][0] for i in range(len(data) - 1)])
380 | for j in range(len(diffs) - 1):
381 | assert abs(diffs[j+1] - diffs[j]) < 1e-6, "x values must be equally spaced"
382 |
383 | h = abs(diffs[0])
384 | n = len(data) - 1
385 |
386 | p = Polynomial(data[n][1])
387 | for k in range(1, n+1):
388 | p += (((-1) ** k) * Util.downdelta(k, n, data)) * Util.choose(Polynomial(data[n][0] / h, - 1 / h), k)
389 |
390 | return p
391 |
392 |
393 | @staticmethod
394 | def interpolate_newton_forward_diff(data: tuple):
395 | """
396 | data is a tuple of (x, y) tuples
397 | """
398 | from util import Util
399 | data = sorted(data, key=lambda x: x[0])
400 | diffs = sorted([data[i+1][0] - data[i][0] for i in range(len(data) - 1)])
401 | for j in range(len(diffs) - 1):
402 | assert abs(diffs[j+1] - diffs[j]) < 1e-6, "x values must be equally spaced"
403 |
404 | h = abs(diffs[0])
405 | n = len(data) - 1
406 |
407 | p = Polynomial(data[0][1])
408 | for k in range(1, n+1):
409 | p += Util.delta(k, 0, data) * Util.choose(Polynomial(-data[0][0] / h, 1 / h), k)
410 |
411 | return p
412 |
413 |
414 | class Exponent(Function):
415 |
416 | def __init__(self, f: Function, base: float=math.e):
417 | self.function = lambda x: base ** f(x)
418 |
419 | class Sin(Function):
420 |
421 | def __init__(self, f: Function):
422 | self.function = lambda x: math.sin(f(x))
423 |
424 | class Cos(Function):
425 |
426 | def __init__(self, f: Function):
427 | self.function = lambda x: math.cos(f(x))
428 |
429 | class Tan(Function):
430 |
431 | def __init__(self, f: Function):
432 | self.function = lambda x: math.tan(f(x))
433 |
434 | class Log(Function):
435 |
436 | def __init__(self, f: Function, base: float=math.e):
437 | self.function = lambda x: math.log(f(x), base)
438 |
439 | class MultiVariableFunction:
440 |
441 | def __init__(self, function):
442 | self.function = function
443 |
444 | def __call__(self, *args):
445 | unwrapped_args = []
446 | for arg in args:
447 | if isinstance(arg, Vector):
448 | unwrapped_args += arg.components
449 | else:
450 | unwrapped_args.append(arg)
451 | args = tuple(unwrapped_args)
452 |
453 | if all(arg is not None for arg in args):
454 | return self.function(*args)
455 | if all(arg is None for arg in args):
456 | raise ValueError("At least one argument must be defined.")
457 |
458 | def f(*z):
459 | arguments = []
460 | original_args = list(args)
461 | passed_args = list(z)
462 | for arg in original_args:
463 | if arg is None:
464 | arguments.append(passed_args.pop(0))
465 | else:
466 | arguments.append(arg)
467 | return self(*arguments)
468 |
469 | num_none = sum(arg is None for arg in args)
470 | if num_none == 1:
471 | return Function(f)
472 | return MultiVariableFunction(f)
473 |
474 | class BivariateFunction(MultiVariableFunction):
475 | pass
476 |
477 | class Vector:
478 |
479 | def __init__(self, *components):
480 | self.components = list(components)
481 |
482 | def __add__(self, other):
483 | return Vector(*[self.components[i] + other.components[i] for i in range(len(self.components))])
484 |
485 | def __sub__(self, other):
486 | return Vector(*[self.components[i] - other.components[i] for i in range(len(self.components))])
487 |
488 | def __rmul__(self, other):
489 | return Vector(*[other * self.components[i] for i in range(len(self.components))])
490 |
491 | def __call__(self, *args):
492 | return Vector(*[self.components[i](*args) for i in range(len(self.components))])
493 |
494 | def __getitem__(self, i):
495 | return self.components[i]
496 |
497 | def __setitem__(self, i, value):
498 | self.components[i] = value
499 |
500 | def __len__(self) -> int:
501 | return len(self.components)
502 |
503 | def __iter__(self):
504 | return iter(self.components)
505 |
506 | def __str__(self):
507 | return "<" + ", ".join(str(component) for component in self.components) + ">"
508 |
509 | class Matrix:
510 |
511 | def __init__(self, *rows: list[Vector]):
512 | self.rows = list(rows)
513 |
514 | def __len__(self) -> int:
515 | return len(self.rows)
516 |
517 | def __getitem__(self, i):
518 | return self.rows[i]
519 |
520 | def __setitem__(self, i, value):
521 | self.rows[i] = value
522 |
523 | def __iter__(self):
524 | return iter(self.rows)
525 |
526 | def __add__(self, other):
527 | return Matrix(*[self.rows[i] + other.rows[i] for i in range(len(self))])
528 |
529 | def __sub__(self, other):
530 | return Matrix(*[self.rows[i] - other.rows[i] for i in range(len(self))])
531 |
532 | def __rmul__(self, other):
533 | return Matrix(*[other * self.rows[i] for i in range(len(self))])
534 |
535 | class OrdinaryDifferentialEquation:
536 |
537 | def __init__(self):
538 | pass
539 |
540 | class LinearODE(OrdinaryDifferentialEquation):
541 |
542 | def __init__(self):
543 | pass
544 |
545 | class FirstOrderLinearODE(LinearODE):
546 | """
547 | y'(x) = f(x, y(x))
548 | These are initial value problems.
549 | """
550 |
551 | def __init__(self, f: BivariateFunction, a: float, b: float, y0: float):
552 | """
553 | f is a function of x and y(x)
554 | """
555 | self.f = f
556 | self.a = a
557 | self.b = b
558 | self.y0 = y0
559 |
560 | def solve(self, h: float = 0.1, method: Literal["euler", "runge-kutta", "taylor", "trapezoidal", "adam-bashforth", "adam-moulton", "predictor-corrector"]='euler', n: int = 1, step: int = 2, points: list[float]=[]):
561 | if method == "euler":
562 | return self.solve_taylor(h, 1)
563 | if method == "runge-kutta":
564 | return self.solve_runge_kutta(h, n)
565 | if method == "taylor":
566 | return self.solve_taylor(h, n)
567 | if method == "trapezoidal":
568 | return self.solve_trapezoidal(h)
569 | if method == "adam-bashforth":
570 | return self.solve_adam_bashforth(h, step, points)
571 | if method == "adam-moulton":
572 | return self.solve_adam_moulton(h, step, points)
573 | if method == "predictor-corrector":
574 | return self.solve_predictor_corrector(h)
575 | raise ValueError("Invalid method.")
576 |
577 | def solve_runge_kutta(self, h: float, n: int) -> Polynomial:
578 | w = [self.y0]
579 | N = int((self.b - self.a) / h)
580 | if n == 1:
581 | return self.solve(h, method='euler')
582 | elif n == 2:
583 | for i in range(N):
584 | xi = self.a + i * h
585 | w.append(w[i] + (h/2) * self.f(xi, w[i]) + (h/2) * self.f(xi + h, w[i] + h * self.f(xi, w[i])))
586 | elif n == 3:
587 | for i in range(N):
588 | xi = self.a + i * h
589 | k1 = self.f(xi, w[i])
590 | k2 = self.f(xi + (h/3), w[i] + (h/3) * k1)
591 | k3 = self.f(xi + (2/3) * h, w[i] + (2/3) * h * k2)
592 | w.append(w[i] + (h/4) * (k1 + 3 * k3))
593 | elif n == 4:
594 | for i in range(N):
595 | xi = self.a + i * h
596 | k1 = h * self.f(xi, w[i])
597 | k2 = h * self.f(xi + h/2, w[i] + 0.5 * k1)
598 | k3 = h * self.f(xi + h/2, w[i] + 0.5 * k2)
599 | k4 = h * self.f(xi + h, w[i] + k3)
600 | w.append(w[i] + (1/6) * (k1 + 2 * k2 + 2 * k3 + k4))
601 | else:
602 | raise NotImplementedError("Not implemented for n > 4 yet.")
603 |
604 | try:
605 | return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
606 | except:
607 | return w # return list of values if interpolation fails (like when w is a list of Vectors)
608 |
609 | def solve_taylor(self, h: float, n: int) -> Polynomial:
610 | w = [self.y0]
611 | N = int((self.b - self.a) / h)
612 | if n == 1:
613 | for i in range(N):
614 | xi = self.a + i * h
615 | w.append(w[i] + h * self.f(xi, w[i]))
616 | elif n == 2:
617 | for i in range(N):
618 | xi = self.a + i * h
619 | # g = f'
620 | g = self.f(None, w[i]).differentiate()(xi) + self.f(xi, w[i]) * self.f(xi, None).differentiate()(w[i])
621 | w.append(w[i] + h * self.f(xi, w[i]) + (h ** 2) * g / 2)
622 | else:
623 | raise NotImplementedError("Not implemented for n > 2 yet.")
624 |
625 | try:
626 | return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
627 | except:
628 | return w # return list of values if interpolation fails (like when w is a list of Vectors)
629 |
630 | def solve_trapezoidal(self, h: float) -> Polynomial:
631 | w = [self.y0]
632 | N = int((self.b - self.a) / h)
633 | for i in range(N):
634 | xi = self.a + i * h
635 | g = Function(lambda x: w[i] + (h/2) * (self.f(xi, w[i]) + self.f(xi + h, x)))
636 | w.append(g.fixed_point(w[i]))
637 |
638 | try:
639 | return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
640 | except:
641 | return w # return list of values if interpolation fails (like when w is a list of Vectors)
642 |
643 | def solve_adam_bashforth(self, h: float, step: int, points: list[float]) -> Polynomial:
644 | w = [self.y0] + points
645 | N = int((self.b - self.a) / h)
646 | if step == 2:
647 | for i in range(1, N):
648 | xi = self.a + i * h
649 | w.append(w[i] + (h/2) * (3 * self.f(xi, w[i]) - self.f(xi - h, w[i-1])))
650 | elif step == 3:
651 | for i in range(2, N):
652 | xi = self.a + i * h
653 | w.append(w[i] + (h/12) * (23 * self.f(xi, w[i]) - 16 * self.f(xi - h, w[i-1]) + 5 * self.f(xi - 2 * h, w[i-2])))
654 | elif step == 4:
655 | for i in range(3, N):
656 | xi = self.a + i * h
657 | w.append(w[i] + (h/24) * (55 * self.f(xi, w[i]) - 59 * self.f(xi - h, w[i-1]) + 37 * self.f(xi - 2 * h, w[i-2]) - 9 * self.f(xi - 3 * h, w[i-3])))
658 | else:
659 | if step > 1:
660 | raise NotImplementedError("Not implemented for step > 4 yet.")
661 | else:
662 | raise ValueError("Step must be greater than 1.")
663 |
664 | try:
665 | return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
666 | except:
667 | return w # return list of values if interpolation fails (like when w is a list of Vectors)
668 |
669 | def solve_adam_moulton(self, h: float, step: int, points: list[float]) -> Polynomial:
670 | w = [self.y0] + points
671 | N = int((self.b - self.a) / h)
672 | if step == 2:
673 | for i in range(1, N):
674 | xi = self.a + i * h
675 | g = Function(lambda x: w[i] + (h/12) * (5 * self.f(xi + h, x) + 8 * self.f(xi, w[i]) - self.f(xi - h, w[i-1])))
676 | w.append(g.fixed_point(w[i]))
677 | elif step == 3:
678 | for i in range(2, N):
679 | xi = self.a + i * h
680 | g = Function(lambda x: w[i] + (h/24) * (9 * self.f(xi + h, x) + 19 * self.f(xi, w[i]) - 5 * self.f(xi - h, w[i-1]) + self.f(xi - 2 * h, w[i-2])))
681 | w.append(g.fixed_point(w[i]))
682 | elif step == 4:
683 | for i in range(3, N):
684 | xi = self.a + i * h
685 | g = Function(lambda x: w[i] + (h/720) * (251 * self.f(xi + h, x) + 646 * self.f(xi, w[i]) - 264 * self.f(xi - h, w[i-1]) + 106 * self.f(xi - 2 * h, w[i-2]) - 19 * self.f(xi - 3 * h, w[i-3])))
686 | w.append(g.fixed_point(w[i]))
687 | else:
688 | if step > 1:
689 | raise NotImplementedError("Not implemented for step > 4 yet.")
690 | else:
691 | raise ValueError("Step must be greater than 1.")
692 |
693 | try:
694 | return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
695 | except:
696 | return w # return list of values if interpolation fails (like when w is a list of Vectors)
697 |
698 | def solve_predictor_corrector(self, h: float) -> Polynomial:
699 | w = [self.y0]
700 | N = int((self.b - self.a) / h)
701 | alphas = [self.a + h * i for i in range(1, 4)]
702 |
703 | # determine starting values with runge-kutta order-4
704 | g = self.f
705 | ivp = FirstOrderLinearODE(g, self.a, self.a + 3 * h, self.y0)
706 | rk_sol = ivp.solve_runge_kutta(h, 4)
707 | for i in range(3):
708 | w.append(rk_sol(alphas[i]))
709 |
710 | for i in range(3, N):
711 | xi = self.a + i * h
712 |
713 | # Predictor: Adams-Bashforth order-4
714 | prediction = w[i] + (h/24) * (55 * self.f(xi, w[i]) - 59 * self.f(xi - h, w[i-1]) + 37 * self.f(xi - 2 * h, w[i-2]) - 9 * self.f(xi - 3 * h, w[i-3]))
715 |
716 | # Corrector: Adams-Moulton order-3
717 | correction = w[i] + (h/24) * (9 * self.f(xi + h, prediction) + 19 * self.f(xi, w[i]) - 5 * self.f(xi - h, w[i-1]) + self.f(xi - 2 * h, w[i-2]))
718 |
719 | w.append(correction)
720 |
721 | try:
722 | return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 1)])
723 | except:
724 | return w # return list of values if interpolation fails (like when w is a list of Vectors)
725 |
726 | class SecondOrderLinearODE_BVP(LinearODE):
727 | """
728 | y''(x) = p(x)y'(x) + q(x)y(x) + r(x)
729 | These are boundary value problems.
730 | """
731 |
732 | def __init__(self, p: Function, q: Function, r: Function, a: float, b: float, y0: float, y1: float):
733 | self.p = p
734 | self.q = q
735 | self.r = r
736 | self.a = a
737 | self.b = b
738 | self.y0 = y0 # y(a)
739 | self.y1 = y1 # y(b)
740 |
741 | def solve(self, h: float = 0.1, method: Literal["shooting", "finite_difference"]="shooting"):
742 | if method == "shooting":
743 | return self.solve_shooting(h)
744 | if method == "finite_difference":
745 | return self.solve_finite_difference(h)
746 | raise ValueError("Invalid method.")
747 |
748 | def solve_shooting(self, h: float) -> Polynomial:
749 | IVP1 = SecondOrderODE_IVP(
750 | MultiVariableFunction(lambda t, u1, u2: self.p(t) * u2 + self.q(t) * u1 + self.r(t)),
751 | self.a, self.b, self.y0, 0
752 | )
753 | IVP2 = SecondOrderODE_IVP(
754 | MultiVariableFunction(lambda t, u1, u2: self.p(t) * u2 + self.q(t) * u1),
755 | self.a, self.b, 0, 1
756 | )
757 |
758 | sol1 = IVP1.solve(h)
759 | sol2 = IVP2.solve(h)
760 |
761 | c = (self.y1 - sol1(self.b)) / sol2(self.b)
762 |
763 | return sol1 + c * sol2
764 |
765 | def solve_finite_difference(self, h: float) -> Polynomial:
766 | N = int((self.b - self.a) / h) - 1
767 | A = Matrix(*[Vector(*[0 for _ in range(N)]) for _ in range(N)])
768 | b = Vector(*[0 for _ in range(N)])
769 |
770 | # First row
771 | A[0][0] = -(2 + (h ** 2) * self.q(self.a + h))
772 | A[0][1] = 1 - (h / 2) * self.p(self.a + h)
773 | b[0] = (h ** 2) * self.r(self.a + h) - (1 + (h / 2) * self.p(self.a + h)) * self.y0
774 |
775 | # Middle rows
776 | for i in range(1, N - 1):
777 | xi = self.a + (i+1) * h
778 | A[i][i-1] = 1 + (h / 2) * self.p(xi)
779 | A[i][i] = -(2 + (h ** 2) * self.q(xi))
780 | A[i][i+1] = 1 - (h / 2) * self.p(xi)
781 | b[i] = (h ** 2) * self.r(xi)
782 |
783 | # Last row
784 | A[N-1][N-2] = 1 + (h / 2) * self.p(self.b - h)
785 | A[N-1][N-1] = -(2 + (h ** 2) * self.q(self.b - h))
786 | b[N-1] = (h ** 2) * self.r(self.b - h) - (1 - (h / 2) * self.p(self.b - h)) * self.y1
787 |
788 | # Solve system of equations
789 | sol = LinearSystem(A, b).solve()
790 |
791 | w = [self.y0] + sol.components + [self.y1]
792 |
793 | return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(N + 2)])
794 |
795 |
796 | class SecondOrderODE_IVP(OrdinaryDifferentialEquation):
797 | """
798 | y''(x) = f(x, y(x), y'(x))
799 | These are initial value problems.
800 | """
801 |
802 | def __init__(self, f: MultiVariableFunction, a: float, b: float, y0: float, y1: float):
803 | """
804 | f is a function of x, y(x), and y'(x)
805 | """
806 | self.f = f
807 | self.a = a
808 | self.b = b
809 | self.y0 = y0 # y(a)
810 | self.y1 = y1 # y'(a)
811 |
812 | def solve(self, h: float = 0.1, method: Literal["euler", "runge-kutta", "taylor", "trapezoidal", "adam-bashforth", "adam-moulton", "predictor-corrector"]='euler', n: int = 1, step: int = 2, points: list[float]=[]):
813 | U0 = Vector(self.y0, self.y1)
814 | F = Vector(
815 | MultiVariableFunction(lambda t, u1, u2: u2),
816 | MultiVariableFunction(lambda t, u1, u2: self.f(t, u1, u2))
817 | )
818 | IVP = FirstOrderLinearODE(F, self.a, self.b, U0)
819 | sol = IVP.solve(h, method, n, step, points)
820 |
821 | w = [x[0] for x in sol]
822 | return Polynomial.interpolate([(self.a + i * h, w[i]) for i in range(len(w))])
823 |
824 | class SecondOrderODE_BVP(OrdinaryDifferentialEquation):
825 | """
826 | y''(x) = f(x, y(x), y'(x))
827 | These are boundary value problems.
828 | """
829 |
830 | def __init__(self, f: MultiVariableFunction, a: float, b: float, y0: float, y1: float):
831 | """
832 | f is a function of x, y(x), and y'(x)
833 | """
834 | self.f = f
835 | self.a = a
836 | self.b = b
837 | self.y0 = y0 # y(a)
838 | self.y1 = y1 # y(b)
839 |
840 | def solve(self, h: float = 0.1, method: Literal["shooting_newton"]="shooting_newton", M: int = 100, TOL: float = 1e-5, initial_approximation=None):
841 | if method == "shooting_newton":
842 | return self.solve_shooting_newton(h, M, TOL, initial_approximation)
843 | raise ValueError("Invalid method.")
844 |
845 | def solve_shooting_newton(self, h: float, M, TOL, initial_approximation) -> Polynomial:
846 | t = 1 if initial_approximation is None else initial_approximation # initial guess for y'(a)
847 | i = 0
848 |
849 | while i < M:
850 | IVP1 = SecondOrderODE_IVP(
851 | MultiVariableFunction(lambda t, u1, u2: self.f(t, u1, u2)),
852 | self.a, self.b, self.y0, t
853 | )
854 | y = IVP1.solve(h)
855 |
856 | p = Function(lambda x: self.f(x, None, y.differentiate()(x)).differentiate()(y(x)))
857 | q = Function(lambda x: self.f(x, y(x), None).differentiate()(y.differentiate()(x)))
858 | r = Function(lambda x: 0)
859 | IVP2 = SecondOrderLinearODE_BVP(p, q, r, self.a, self.b, 0, 1)
860 | z = IVP2.solve(h)
861 |
862 | t0 = t - (y(self.b) - self.y1) / z(self.b)
863 | if abs(t0 - t) < TOL:
864 | return y
865 |
866 | t = t0
867 | i += 1
868 |
869 | return None
870 |
871 | class LinearSystem:
872 | """
873 | A system of linear equations.
874 | """
875 |
876 | def __init__(self, A: Matrix, b: Vector):
877 | self.A = A
878 | self.b = b
879 | self.N = len(A)
880 |
881 | assert len(b) == self.N, "A and b must have the same number of rows."
882 |
883 | def solve(self, method: Literal["gauss_elimination", "gauss_jacobi", "gauss_seidel"]='gauss_elimination', TOL: float = 1e-5, initial_approximation: Vector = None, MAX_ITERATIONS: int = 100):
884 | if method == "gauss_elimination":
885 | return self.solve_gauss_elimination()
886 | if method == "gauss_jacobi":
887 | return self.solve_gauss_jacobi(TOL, initial_approximation, MAX_ITERATIONS)
888 | if method == "gauss_seidel":
889 | return self.solve_gauss_seidel(TOL, initial_approximation, MAX_ITERATIONS)
890 | raise ValueError("Invalid method.")
891 |
892 | def solve_gauss_elimination(self) -> Vector:
893 | for i in range(self.N - 1):
894 | # find pivot row
895 | p = None
896 | for j in range(i, self.N):
897 | if abs(self.A[j][i]) != 0:
898 | p = j
899 | break
900 | if p is None:
901 | raise ValueError("No unique solution exists.")
902 |
903 | if p != i:
904 | # swap rows
905 | self.A[i], self.A[p] = self.A[p], self.A[i]
906 | self.b[i], self.b[p] = self.b[p], self.b[i]
907 |
908 | for j in range(i + 1, self.N):
909 | m = self.A[j][i] / self.A[i][i]
910 | self.A[j] = self.A[j] - m * self.A[i]
911 | self.b[j] = self.b[j] - m * self.b[i]
912 |
913 | if abs(self.A[self.N - 1][self.N - 1]) == 0:
914 | raise ValueError("No unique solution exists.")
915 |
916 | x = [0] * self.N
917 | x[self.N - 1] = self.b[self.N - 1] / self.A[self.N - 1][self.N - 1]
918 | for i in range(self.N - 2, -1, -1):
919 | x[i] = (self.b[i] - sum(self.A[i][j] * x[j] for j in range(i + 1, self.N))) / self.A[i][i]
920 |
921 | return Vector(*x)
922 |
923 | def solve_gauss_jacobi(self, TOL: float, initial_approximation: Vector, MAX_ITERATIONS) -> Vector:
924 | assert initial_approximation is not None, "Initial approximation must be defined."
925 |
926 | x0 = initial_approximation
927 | k = 0
928 |
929 | while k < MAX_ITERATIONS:
930 | x1 = [0] * self.N
931 | for i in range(self.N):
932 | x1[i] = (self.b[i] - sum(self.A[i][j] * x0[j] for j in range(self.N) if j != i)) / self.A[i][i]
933 |
934 | if max(abs(x1[i] - x0[i]) for i in range(self.N)) / max(abs(x1[i]) for i in range(self.N)) < TOL:
935 | return Vector(*x1)
936 |
937 | x0 = x1
938 | k += 1
939 |
940 | return None
941 |
942 | def solve_gauss_seidel(self, TOL: float, initial_approximation: Vector, MAX_ITERATIONS) -> Vector:
943 | assert initial_approximation is not None, "Initial approximation must be defined."
944 |
945 | x0 = initial_approximation
946 | k = 0
947 |
948 | while k < MAX_ITERATIONS:
949 | x1 = [0] * self.N
950 | for i in range(self.N):
951 | x1[i] = (self.b[i] - sum(self.A[i][j] * x1[j] for j in range(i)) - sum(self.A[i][j] * x0[j] for j in range(i+1, self.N))) / self.A[i][i]
952 |
953 | if max(abs(x1[i] - x0[i]) for i in range(self.N)) / max(abs(x1[i]) for i in range(self.N)) < TOL:
954 | return Vector(*x1)
955 |
956 | x0 = x1
957 | k += 1
958 |
959 | return None
960 |
--------------------------------------------------------------------------------
/util.py:
--------------------------------------------------------------------------------
1 | from function import Function, Polynomial
2 | import math
3 |
4 |
5 | class Util:
6 |
7 | @staticmethod
8 | def choose(s: Function, k: int):
9 | res = Polynomial(1)
10 | for i in range(k):
11 | res *= (-i + s)
12 | return (1 / math.factorial(k)) * res
13 |
14 | @staticmethod
15 | def delta(k: int, i: int, data: list[tuple[float]]):
16 | if k == 1:
17 | return data[i+1][1] - data[i][1]
18 | return Util.delta(k - 1, i + 1, data) - Util.delta(k - 1, i, data)
19 |
20 | @staticmethod
21 | def downdelta(k: int, i: int, data: list[tuple[float]]):
22 | if k == 1:
23 | return data[i][1] - data[i-1][1]
24 | return Util.downdelta(k - 1, i, data) - Util.downdelta(k - 1, i - 1, data)
25 |
--------------------------------------------------------------------------------