├── .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 | --------------------------------------------------------------------------------