scicomp3 0.2.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. scicomp3-0.2.0/.github/workflows/ci.yml +26 -0
  2. scicomp3-0.2.0/.gitignore +11 -0
  3. scicomp3-0.2.0/.vscode/settings.json +7 -0
  4. scicomp3-0.2.0/PKG-INFO +17 -0
  5. scicomp3-0.2.0/README.md +316 -0
  6. scicomp3-0.2.0/assignment01.py +118 -0
  7. scicomp3-0.2.0/images/gifs/a1_6_insulators_sor.gif +0 -0
  8. scicomp3-0.2.0/images/gifs/a1_6_sinks_sor.gif +0 -0
  9. scicomp3-0.2.0/images/gifs/diffusion_dt=2.2e-05_Tsim=1.5_D=1.0_N=100.gif +0 -0
  10. scicomp3-0.2.0/images/gifs/diffusion_dt=9e-05_Tsim=1.5_D=1.0_N=50.gif +0 -0
  11. scicomp3-0.2.0/images/gifs/vibrating_string_case=1_dt=0.001_Tsim=5_c=1_L=1_N=90.gif +0 -0
  12. scicomp3-0.2.0/images/gifs/vibrating_string_case=2_dt=0.001_Tsim=5_c=1_L=1_N=90.gif +0 -0
  13. scicomp3-0.2.0/images/gifs/vibrating_string_case=3_dt=0.001_Tsim=5_c=1_L=1_N=90.gif +0 -0
  14. scicomp3-0.2.0/images/gifs/vibrating_string_fEuler_case=1_dt=0.001_Tsim=2.25_c=1_L=1_N=90.gif +0 -0
  15. scicomp3-0.2.0/images/gifs/vibrating_string_fEuler_case=2_dt=0.001_Tsim=2.25_c=1_L=1_N=90.gif +0 -0
  16. scicomp3-0.2.0/images/gifs/vibrating_string_fEuler_case=3_dt=0.001_Tsim=0.35_c=1_L=1_N=90.gif +0 -0
  17. scicomp3-0.2.0/pyproject.toml +24 -0
  18. scicomp3-0.2.0/run_assignment01_01_smoke_plot.py +26 -0
  19. scicomp3-0.2.0/run_assignment01_02_cases_plot.py +34 -0
  20. scicomp3-0.2.0/run_assignment01_03_cases_animation.py +4 -0
  21. scicomp3-0.2.0/scripts/a1_1_cases_animation.py +130 -0
  22. scicomp3-0.2.0/scripts/a1_1_cases_compared_to_analytical.py +165 -0
  23. scicomp3-0.2.0/scripts/a1_1_cases_plot.py +123 -0
  24. scicomp3-0.2.0/scripts/a1_1_smoke_test.py +69 -0
  25. scicomp3-0.2.0/scripts/a1_2_diffusion.py +146 -0
  26. scicomp3-0.2.0/scripts/a1_2_diffusion_animation.py +100 -0
  27. scicomp3-0.2.0/scripts/a1_2_diffusion_verification.py +191 -0
  28. scicomp3-0.2.0/scripts/a1_6_insulators_gauss_seidel.py +88 -0
  29. scicomp3-0.2.0/scripts/a1_6_insulators_jacobi.py +88 -0
  30. scicomp3-0.2.0/scripts/a1_6_insulators_k_impact.py +128 -0
  31. scicomp3-0.2.0/scripts/a1_6_insulators_sor.py +161 -0
  32. scicomp3-0.2.0/scripts/a1_6_insulators_sor_animation.py +112 -0
  33. scicomp3-0.2.0/scripts/a1_6_iterative_convergence.py +74 -0
  34. scicomp3-0.2.0/scripts/a1_6_iterative_gauss_seidel.py +80 -0
  35. scicomp3-0.2.0/scripts/a1_6_iterative_jacobi.py +80 -0
  36. scicomp3-0.2.0/scripts/a1_6_iterative_methods.py +315 -0
  37. scicomp3-0.2.0/scripts/a1_6_iterative_sor.py +86 -0
  38. scicomp3-0.2.0/scripts/a1_6_objects_k_impact.py +139 -0
  39. scicomp3-0.2.0/scripts/a1_6_omega_for_various_N_plot.py +70 -0
  40. scicomp3-0.2.0/scripts/a1_6_omega_for_various_N_sim.py +53 -0
  41. scicomp3-0.2.0/scripts/a1_6_omega_values.py +63 -0
  42. scicomp3-0.2.0/scripts/a1_6_seeking_optimal_omega.py +124 -0
  43. scicomp3-0.2.0/scripts/a1_6_sinks_and_insulators_sor.py +98 -0
  44. scicomp3-0.2.0/scripts/a1_6_sinks_gauss_seidel.py +88 -0
  45. scicomp3-0.2.0/scripts/a1_6_sinks_jacobi.py +88 -0
  46. scicomp3-0.2.0/scripts/a1_6_sinks_k_impact.py +120 -0
  47. scicomp3-0.2.0/scripts/a1_6_sinks_sor.py +153 -0
  48. scicomp3-0.2.0/scripts/a1_6_sinks_sor_animation.py +112 -0
  49. scicomp3-0.2.0/src/scicomp3/__init__.py +27 -0
  50. scicomp3-0.2.0/src/scicomp3/bvp/__init__.py +6 -0
  51. scicomp3-0.2.0/src/scicomp3/bvp/methods.py +227 -0
  52. scicomp3-0.2.0/src/scicomp3/bvp/omega.py +102 -0
  53. scicomp3-0.2.0/src/scicomp3/bvp/solver.py +90 -0
  54. scicomp3-0.2.0/src/scicomp3/core/__init__.py +6 -0
  55. scicomp3-0.2.0/src/scicomp3/core/grid.py +66 -0
  56. scicomp3-0.2.0/src/scicomp3/core/result.py +47 -0
  57. scicomp3-0.2.0/src/scicomp3/objects/insulator.py +28 -0
  58. scicomp3-0.2.0/src/scicomp3/objects/shapes.py +34 -0
  59. scicomp3-0.2.0/src/scicomp3/objects/sink.py +31 -0
  60. scicomp3-0.2.0/src/scicomp3/ode/__init__.py +6 -0
  61. scicomp3-0.2.0/src/scicomp3/ode/methods.py +81 -0
  62. scicomp3-0.2.0/src/scicomp3/ode/solver.py +60 -0
  63. scicomp3-0.2.0/src/scicomp3/pde/__init__.py +17 -0
  64. scicomp3-0.2.0/src/scicomp3/pde/diffusion.py +118 -0
  65. scicomp3-0.2.0/src/scicomp3/pde/wave.py +91 -0
  66. scicomp3-0.2.0/src/scicomp3/validation/validation.py +27 -0
  67. scicomp3-0.2.0/tests/__init__.py +1 -0
  68. scicomp3-0.2.0/tests/test_boundary_conditions.py +116 -0
  69. scicomp3-0.2.0/tests/test_diffusion.py +135 -0
  70. scicomp3-0.2.0/tests/test_gauss_seidel.py +105 -0
  71. scicomp3-0.2.0/tests/test_jacobi.py +85 -0
  72. scicomp3-0.2.0/tests/test_scripts.py +94 -0
  73. scicomp3-0.2.0/tests/test_solver_comparison.py +335 -0
  74. scicomp3-0.2.0/tests/test_sor.py +80 -0
@@ -0,0 +1,26 @@
1
+ name: Tests
2
+
3
+ on:
4
+ push:
5
+ branches: [main]
6
+ pull_request:
7
+ branches: [main]
8
+
9
+ jobs:
10
+ smoke-a1_1p1:
11
+ runs-on: ubuntu-22.04
12
+ steps:
13
+ - uses: actions/checkout@v4
14
+
15
+ - uses: actions/setup-python@v5
16
+ with:
17
+ python-version: "3.13"
18
+
19
+ - name: Install package with test dependencies
20
+ run: pip install -e ".[dev]"
21
+
22
+ - name: Smoke test (quick import check)
23
+ run: python -c "from scicomp3 import solve_ivp, wave1d_rhs; print('Import OK')"
24
+
25
+ - name: Run pytest
26
+ run: pytest tests/ -v
@@ -0,0 +1,11 @@
1
+ __pycache__
2
+ *.png
3
+ *.gif
4
+ !images/**/*.gif
5
+ *.pyc
6
+ dist/
7
+ build/
8
+ *.egg-info/
9
+ uv.lock
10
+ data/
11
+ .pkl
@@ -0,0 +1,7 @@
1
+ {
2
+ "files.trimTrailingWhitespace": true,
3
+ "editor.formatOnSave": true,
4
+ "[python]": {
5
+ "editor.defaultFormatter": "ms-python.black-formatter"
6
+ }
7
+ }
@@ -0,0 +1,17 @@
1
+ Metadata-Version: 2.4
2
+ Name: scicomp3
3
+ Version: 0.2.0
4
+ Requires-Python: >=3.10
5
+ Requires-Dist: joblib
6
+ Requires-Dist: numpy
7
+ Requires-Dist: scipy
8
+ Provides-Extra: dev
9
+ Requires-Dist: joblib; extra == 'dev'
10
+ Requires-Dist: matplotlib; extra == 'dev'
11
+ Requires-Dist: pytest; extra == 'dev'
12
+ Requires-Dist: scienceplots; extra == 'dev'
13
+ Provides-Extra: plot
14
+ Requires-Dist: matplotlib; extra == 'plot'
15
+ Requires-Dist: scienceplots; extra == 'plot'
16
+ Provides-Extra: test
17
+ Requires-Dist: pytest; extra == 'test'
@@ -0,0 +1,316 @@
1
+ # scicomp3 — Scientific Computing Assignment Package
2
+
3
+ Numerical solvers for the 1D wave equation, 2D diffusion equation, and steady-state Laplace equation, built for the Scientific Computing course (Assignment Set 1).
4
+
5
+ ## Quick Start
6
+
7
+ ```bash
8
+ # Clone and enter the project
9
+ cd assigment-submission
10
+
11
+ # Create/activate venv and install (editable mode with dev deps)
12
+ uv pip install -e ".[dev]"
13
+
14
+ # Run tests
15
+ pytest tests/ -v
16
+
17
+ # Run a script
18
+ python scripts/a1_1_smoke_test.py
19
+ ```
20
+
21
+ ## Project Structure
22
+
23
+ ```
24
+ .
25
+ ├── src/scicomp3/ # Main package
26
+ │ ├── core/
27
+ │ │ ├── grid.py # Grid1D, Grid2D — spatial discretization
28
+ │ │ └── result.py # ODEResult, BVPResult — solver output containers
29
+ │ ├── ode/
30
+ │ │ ├── methods.py # Time-stepping: Euler, symplectic Euler
31
+ │ │ └── solver.py # solve_ivp() — IVP solver entry point
32
+ │ ├── pde/
33
+ │ │ ├── wave.py # wave1d_rhs, initial conditions (cases i–iii), analytical solution
34
+ │ │ └── diffusion.py # diffusion2d_rhs, BCs, stable dt, analytical solution
35
+ │ ├── bvp/
36
+ │ │ ├── methods.py # Iterative methods: Jacobi, Gauss-Seidel, SOR
37
+ │ │ ├── solver.py # solve_bvp() — BVP solver entry point
38
+ │ │ └── omega.py # Optimal omega computation and search for SOR
39
+ │ ├── objects/
40
+ │ │ ├── shapes.py # Geometric coordinate generation (rectangles)
41
+ │ │ ├── sink.py # Sink region utilities
42
+ │ │ └── insulator.py # Insulator region utilities
43
+ │ └── validation/
44
+ │ └── validation.py # Boundary condition validation utilities
45
+
46
+ ├── tests/ # Pytest test suite
47
+ │ ├── test_boundary_conditions.py # Wave BC enforcement (3 cases)
48
+ │ ├── test_diffusion.py # Diffusion solver + analytical comparison
49
+ │ ├── test_jacobi.py # Jacobi iteration convergence + steady state
50
+ │ ├── test_gauss_seidel.py # Gauss-Seidel iteration convergence
51
+ │ ├── test_sor.py # SOR iteration with omega = 1.9
52
+ │ ├── test_scripts.py # Smoke tests for all scripts
53
+ │ └── test_solver_comparison.py # Legacy vs scicomp3 solver parity
54
+
55
+ ├── scripts/ # Runnable plotting/animation scripts
56
+ │ ├── a1_1_smoke_test.py # Quick wave equation smoke test
57
+ │ ├── a1_1_cases_plot.py # Wave plots for all 3 initial conditions
58
+ │ ├── a1_1_cases_animation.py # Wave animated GIFs
59
+ │ ├── a1_1_cases_compared_to_analytical.py # Numerical vs analytical error
60
+ │ ├── a1_2_diffusion.py # 2D diffusion snapshots
61
+ │ ├── a1_2_diffusion_animation.py # Diffusion animated GIF
62
+ │ ├── a1_2_diffusion_verification.py # Diffusion vs analytical verification
63
+ │ ├── a1_6_iterative_methods.py # Compare Jacobi/GS/SOR profiles + deviations
64
+ │ ├── a1_6_iterative_convergence.py # Convergence rate comparison
65
+ │ ├── a1_6_iterative_jacobi.py # Jacobi standalone
66
+ │ ├── a1_6_iterative_gauss_seidel.py # Gauss-Seidel standalone
67
+ │ ├── a1_6_iterative_sor.py # SOR standalone
68
+ │ ├── a1_6_objects_k_impact.py # Object impact on iteration count
69
+ │ ├── a1_6_seeking_optimal_omega.py # Optimal omega search with objects
70
+ │ ├── a1_6_omega_values.py # Omega parameter exploration
71
+ │ ├── a1_6_omega_for_various_N_sim.py # Omega vs grid size simulation
72
+ │ ├── a1_6_omega_for_various_N_plot.py # Omega vs grid size plotting
73
+ │ ├── a1_6_sinks_jacobi.py # Sink object with Jacobi
74
+ │ ├── a1_6_sinks_gauss_seidel.py # Sink object with Gauss-Seidel
75
+ │ ├── a1_6_sinks_sor.py # Sink object with SOR
76
+ │ ├── a1_6_sinks_sor_animation.py # Sink SOR animation
77
+ │ ├── a1_6_sinks_k_impact.py # Sink impact on convergence
78
+ │ ├── a1_6_sinks_and_insulators_sor.py # Combined sink + insulator
79
+ │ ├── a1_6_insulators_jacobi.py # Insulator with Jacobi
80
+ │ ├── a1_6_insulators_gauss_seidel.py # Insulator with Gauss-Seidel
81
+ │ ├── a1_6_insulators_sor.py # Insulator with SOR
82
+ │ ├── a1_6_insulators_sor_animation.py # Insulator SOR animation
83
+ │ └── a1_6_insulators_k_impact.py # Insulator impact on convergence
84
+
85
+ ├── assignment01.py # Legacy wave solver (kept for comparison tests)
86
+ ├── run_assignment01_*.py # Legacy plotting scripts using assignment01.py
87
+ ├── data/ # Cached simulation data (e.g. n_vs_omega.pkl)
88
+ ├── pyproject.toml # Build config (hatchling), deps, pytest settings
89
+ └── images/ # Generated figures and GIFs
90
+ ```
91
+
92
+ ## How It Works
93
+
94
+ ### Solving a PDE (Initial Value Problem)
95
+
96
+ The package separates concerns into three layers:
97
+
98
+ 1. **PDE right-hand side** (`pde/`) — defines the physics (spatial derivatives)
99
+ 2. **ODE solver** (`ode/`) — advances the solution in time
100
+ 3. **Post-step callback** — enforces boundary conditions after each step
101
+
102
+ Example: solving the wave equation end-to-end:
103
+
104
+ ```python
105
+ import numpy as np
106
+ from scicomp3 import Grid1D, solve_ivp, wave1d_rhs
107
+ from scicomp3.pde.wave import initial_condition_case_ii
108
+
109
+ # 1. Grid and parameters
110
+ grid = Grid1D(N=90, L=1.0)
111
+ c, dt, T_sim = 1.0, 1e-3, 2.0
112
+
113
+ # 2. Initial condition: Ψ(x,0) = sin(5πx), Ψ_t(x,0) = 0
114
+ psi0 = initial_condition_case_ii(grid.x)
115
+ psi0[0] = psi0[-1] = 0 # enforce BCs on IC
116
+ v0 = np.zeros(grid.N)
117
+ y0 = np.column_stack([psi0, v0]) # state = [Ψ, v], shape (N, 2)
118
+
119
+ # 3. Boundary condition callback
120
+ def fixed_ends(t, y):
121
+ y[0, 0] = y[-1, 0] = 0
122
+ return y
123
+
124
+ # 4. Solve
125
+ result = solve_ivp(
126
+ wave1d_rhs,
127
+ t_span=(0, T_sim),
128
+ y0=y0,
129
+ method="symplectic_euler", # energy-preserving for wave eq
130
+ dt=dt,
131
+ args=(c, grid.L, grid.N),
132
+ post_step=fixed_ends,
133
+ )
134
+
135
+ # 5. Extract results
136
+ amplitudes = result.y[:, :, 0] # Ψ at each saved time step
137
+ times = result.t
138
+ ```
139
+
140
+ Example: 2D diffusion equation:
141
+
142
+ ```python
143
+ import numpy as np
144
+ from scicomp3.core.grid import Grid2D
145
+ from scicomp3.ode.solver import solve_ivp
146
+ from scicomp3.pde.diffusion import (
147
+ diffusion2d_rhs, apply_diffusion_bc, diffusion_stable_dt, analytical_solution,
148
+ )
149
+
150
+ grid = Grid2D(N=50, L=1.0)
151
+ D = 1.0
152
+ dt = diffusion_stable_dt(D, grid.dx) # auto-compute safe dt
153
+
154
+ c0 = np.zeros((grid.N + 1, grid.N + 1))
155
+ apply_diffusion_bc(c0) # set top=1, bottom=0
156
+
157
+ result = solve_ivp(
158
+ diffusion2d_rhs,
159
+ t_span=(0, 1.0),
160
+ y0=c0,
161
+ method="forward_euler",
162
+ dt=dt,
163
+ args=(D, grid.dx),
164
+ post_step=lambda t, y: (apply_diffusion_bc(y), y)[1],
165
+ save_interval=100,
166
+ )
167
+ ```
168
+
169
+ ### Solving a BVP (Steady-State)
170
+
171
+ For steady-state problems (Laplace equation), iterative solvers are available:
172
+
173
+ ```python
174
+ from scicomp3.bvp.solver import solve_bvp
175
+ from scicomp3.bvp.omega import get_optimal_omega
176
+
177
+ result = solve_bvp(c0, method="sor", post_step=fixed_bc, tol=1e-5,
178
+ omega=get_optimal_omega(N))
179
+ # result.y — converged solution
180
+ # result.n_iter — iterations to convergence
181
+ # result.delta_history — convergence measure per iteration
182
+ ```
183
+
184
+ ### Available Time-Stepping Methods
185
+
186
+ | Name | Aliases | Order | Properties |
187
+ |------|---------|-------|------------|
188
+ | `"symplectic_euler"` | `"euler_cromer"`, `"semi_implicit_euler"` | 1st | Symplectic (energy-preserving). Default. Use for wave equation. |
189
+ | `"forward_euler"` | `"euler"` | 1st | Explicit. Use for diffusion equation. |
190
+
191
+ ### Available Iterative Methods (BVP)
192
+
193
+ | Name | Description |
194
+ |------|-------------|
195
+ | `"jacobi"` | Jacobi iteration — vectorized with `np.roll` |
196
+ | `"gauss_seidel"` | Gauss-Seidel — sequential updates using latest values |
197
+ | `"sor"` | Successive Over-Relaxation — accelerated Gauss-Seidel with relaxation parameter omega |
198
+
199
+ Methods are registered in `scicomp3.ode.methods.METHODS` (IVP) and `scicomp3.bvp.methods` (BVP), looked up by name in `solve_ivp()` and `solve_bvp()`.
200
+
201
+ ### Key Design Decisions
202
+
203
+ **Post-step callback for boundary conditions.** The PDE RHS functions use `np.roll` for the spatial stencil, which wraps boundary values incorrectly. The `post_step` callback in `solve_ivp` corrects this after every time step. This separates the physics from the constraints cleanly.
204
+
205
+ **Symplectic Euler for wave equation.** Forward Euler is dissipative — it loses energy over time. The symplectic Euler method preserves the phase-space structure of Hamiltonian systems, keeping energy bounded over long simulations. This is the default method.
206
+
207
+ **Explicit scheme for diffusion.** The FTCS (Forward Time, Centered Space) scheme is simple but conditionally stable. Use `diffusion_stable_dt()` to compute a safe time step (90% of the theoretical maximum δx² / 4D).
208
+
209
+ **Sink and insulator objects.** The BVP solvers support sink (Dirichlet, c=0) and insulator (Neumann, zero-flux) objects via coordinate arrays passed to `solve_bvp()`.
210
+
211
+ ## Running Tests
212
+
213
+ ```bash
214
+ # All tests
215
+ pytest tests/ -v
216
+
217
+ # Specific test file
218
+ pytest tests/test_diffusion.py -v
219
+
220
+ # With print output
221
+ pytest tests/ -v -s
222
+ ```
223
+
224
+ The test suite covers:
225
+ - **test_boundary_conditions.py** — Wave equation BCs hold for all 3 initial conditions; documents np.roll boundary pollution without post_step.
226
+ - **test_diffusion.py** — Diffusion BCs (top=1, bottom=0), convergence to steady state c=y, match against analytical erfc series solution, stability check.
227
+ - **test_jacobi.py** — Jacobi iteration convergence, steady-state profile, boundary checks, monotonicity.
228
+ - **test_gauss_seidel.py** — Gauss-Seidel convergence, fewer iterations than Jacobi.
229
+ - **test_sor.py** — SOR iteration with omega=1.9.
230
+ - **test_scripts.py** — Smoke tests that every script under `scripts/` runs without error.
231
+ - **test_solver_comparison.py** — Legacy `assignment01.py` and `scicomp3` solvers produce identical interior-point results.
232
+
233
+ ## Running Scripts
234
+
235
+ Scripts live in `scripts/` and produce plots or animations. They require `matplotlib` and `scienceplots`:
236
+
237
+ ```bash
238
+ # Quick smoke test (interactive plot)
239
+ python scripts/a1_1_smoke_test.py
240
+
241
+ # Generate static plots for all wave cases → images/figures/
242
+ python scripts/a1_1_cases_plot.py
243
+
244
+ # Generate animated GIFs for all wave cases → images/gifs/
245
+ python scripts/a1_1_cases_animation.py
246
+
247
+ # Numerical vs analytical comparison → images/figures/
248
+ python scripts/a1_1_cases_compared_to_analytical.py
249
+
250
+ # 2D diffusion: concentration fields → images/figures/
251
+ python scripts/a1_2_diffusion.py
252
+
253
+ # 2D diffusion vs analytical verification → images/figures/
254
+ python scripts/a1_2_diffusion_verification.py
255
+
256
+ # Compare iterative methods (Jacobi, GS, SOR) → images/figures/
257
+ python scripts/a1_6_iterative_methods.py
258
+
259
+ # Convergence rate comparison → images/figures/
260
+ python scripts/a1_6_iterative_convergence.py
261
+
262
+ # Object impact on iteration count → images/figures/
263
+ python scripts/a1_6_objects_k_impact.py
264
+
265
+ # Optimal omega search with objects → images/figures/
266
+ python scripts/a1_6_seeking_optimal_omega.py
267
+ ```
268
+
269
+ ## Adding a New Time-Stepping Method
270
+
271
+ 1. Define a step function in `src/scicomp3/ode/methods.py`:
272
+
273
+ ```python
274
+ def my_step(fun, t, y, dt, args=()):
275
+ """One step of my method. Returns (y_new, n_function_evals)."""
276
+ dydt = fun(t, y, *args)
277
+ y_new = ... # your update rule
278
+ return y_new, 1
279
+ ```
280
+
281
+ 2. Register it in `METHODS`:
282
+
283
+ ```python
284
+ METHODS["my_method"] = my_step
285
+ ```
286
+
287
+ 3. Use it:
288
+
289
+ ```python
290
+ result = solve_ivp(..., method="my_method")
291
+ ```
292
+
293
+ ## Adding a New PDE
294
+
295
+ 1. Create a RHS function in `src/scicomp3/pde/`:
296
+
297
+ ```python
298
+ def my_pde_rhs(t, y, *params):
299
+ """Compute dy/dt for my PDE. Returns array same shape as y."""
300
+ ...
301
+ ```
302
+
303
+ 2. Write a post_step if boundary conditions need enforcement.
304
+ 3. Call `solve_ivp(my_pde_rhs, ...)` with appropriate method and parameters.
305
+
306
+ ## Legacy Code
307
+
308
+ `assignment01.py` at the project root is the original wave equation implementation. It uses a different API (`integrate_euler` with `**kwargs`) and has known boundary pollution from `np.roll` without post-step correction. It is kept for backward compatibility and tested against `scicomp3` in `test_solver_comparison.py`.
309
+
310
+ ## Dependencies
311
+
312
+ - **Required**: `numpy`, `scipy`, `joblib`
313
+ - **Plotting**: `matplotlib`, `scienceplots` (optional, needed for scripts)
314
+ - **Testing**: `pytest` (optional)
315
+
316
+ Python >= 3.10.
@@ -0,0 +1,118 @@
1
+ import numpy as np
2
+ import matplotlib
3
+ import matplotlib.pyplot as plt
4
+ from matplotlib.animation import FuncAnimation, PillowWriter
5
+
6
+
7
+ def wave_eq_deriv(state, t, dt=1e-3, c=1, L=1, N=100):
8
+ """
9
+ Compute the derivatives for the 1D wave equation, to use for a forward Euler integration scheme.
10
+ Takes as arguments:
11
+ - current state vector [Ψ, Ψ_t];
12
+ - current time t;
13
+ - wave speed c;
14
+ - string length L;
15
+ - number of spatial points N.
16
+ Returns the derivatives [dΨ/dt, d^2 Ψ/dt^2] as a numpy array.
17
+ """
18
+ # psi, psi_t = state # unpack the state vector [Ψ, Ψ_t]
19
+ psi = state[:, 0]
20
+ psi_t = state[:, 1]
21
+ psi[0] = 0 # boundary condition at x=0
22
+ psi[-1] = 0 # boundary condition at x=L
23
+
24
+ dx = L / N # spatial step size
25
+ d2psi_dx2 = (
26
+ np.roll(psi, -1) - 2 * psi + np.roll(psi, 1)
27
+ ) / dx**2 # second deriv in space
28
+ dpsi_t_dt = c**2 * d2psi_dx2 # second deriv in time to update first deriv
29
+
30
+ dpsi_dt = psi_t + dt * dpsi_t_dt # first deriv in time to update Ψ
31
+
32
+ # Check that the boundary conditions remain satisfied
33
+ assert psi[0] == 0, f"Expected 0 at the boundary, got {d2psi_dx2[0]}"
34
+ assert psi[-1] == 0, f"Expected 0 at the boundary, got {d2psi_dx2[-1]}"
35
+
36
+ return np.array([dpsi_dt, dpsi_t_dt])
37
+
38
+
39
+ def integrate_euler(deriv_func, state0, dt=1e-3, T_sim=10, **kwargs):
40
+ """
41
+ Integrate a system of ODEs using the forward Euler method.
42
+ Takes as arguments:
43
+ - deriv_func: function that computes the derivatives in time;
44
+ - state0: initial state vector at time t=0;
45
+ - dt: time step size;
46
+ - T_sim: total simulation time;
47
+ - kwargs: additional arguments to pass to deriv_func.
48
+ Returns an array of states at each time step.
49
+ """
50
+ # Create the time array based on simulation time and time step size
51
+ time = np.arange(0, T_sim, dt)
52
+ # Create an array to hold the states at each time step
53
+ states = np.zeros((len(time),) + state0.shape)
54
+ # Fill it with the initial state
55
+ states[0] = state0
56
+ print(np.shape(states), np.shape(state0))
57
+
58
+ # Perform the forward Euler integration
59
+ for i in range(1, len(time)):
60
+ derivs = deriv_func(states[i - 1], time[i - 1], dt, **kwargs)
61
+ # Update according to: new_state = old_state + dt * derivs
62
+ state = states[i - 1] + dt * np.transpose(derivs)
63
+ # Save the updated state
64
+ states[i] = state
65
+
66
+ return time, states
67
+
68
+
69
+ def animate_wave(amplitudes: list, dpi=100, case="i", dt=1e-3, T_sim=10, **kwargs):
70
+ """Create and save an animated GIF showing the temporal evolution of CA grids."""
71
+
72
+ c, L, N = kwargs["c"], kwargs["L"], kwargs["N"]
73
+ dx = L / N
74
+
75
+ # Set up the figure
76
+ fig, ax = plt.subplots(figsize=(6, 4))
77
+ ax.plot(np.arange(N) * dx, amplitudes[0], color="ForestGreen")
78
+
79
+ ax.set_xlabel("Position along string (x)")
80
+ ax.set_ylabel("String amplitude (Ψ)")
81
+ ax.set_title("Vibrating string over at time 0", fontsize=14)
82
+ ylim = np.max(np.abs(amplitudes)) * 1.5
83
+ ax.set_ylim(-ylim, ylim)
84
+
85
+ # Define animation function
86
+ def animate(frame):
87
+ # i, grid = frame
88
+ i, plot = frame
89
+ ax.clear()
90
+ ax.plot(np.arange(N) * dx, plot, color="ForestGreen")
91
+ ax.set_ylim(-ylim, ylim)
92
+ ax.set_xlabel("Position along string (x)")
93
+ ax.set_ylabel("String amplitude (Ψ)")
94
+ ax.set_title(f"Vibrating string at time {i*10*dt:.2f}", fontsize=14)
95
+ return []
96
+
97
+ # Set up helper function to display progress
98
+ def progress_callback(current_frame, total_frames):
99
+ """Shows saving progress"""
100
+ if current_frame % 10 == 0:
101
+ print(f"Saving frame {current_frame}/{total_frames} ...")
102
+
103
+ amplitudes_to_animate = list(
104
+ enumerate(amplitudes[::10])
105
+ ) # Sample every 10th frame for animation
106
+
107
+ # Create animation
108
+ anim = FuncAnimation(fig, animate, amplitudes_to_animate, interval=10)
109
+ print("Saving animation ... (This can take a while depending on the dpi.)")
110
+
111
+ filename = f"../images/gifs/vibrating_string_case={case}_dt={dt}_Tsim={T_sim}_c={c}_L={L}_N={N}.gif"
112
+ anim.save(
113
+ filename,
114
+ writer=PillowWriter(fps=20),
115
+ dpi=dpi,
116
+ progress_callback=progress_callback,
117
+ )
118
+ print(f"Saved successfully as '{filename}'")
@@ -0,0 +1,24 @@
1
+ [build-system]
2
+ requires = ["hatchling"]
3
+ build-backend = "hatchling.build"
4
+
5
+ [project]
6
+ name = "scicomp3"
7
+ version = "0.2.0"
8
+ requires-python = ">=3.10"
9
+ dependencies = ["numpy", "scipy", "joblib"]
10
+
11
+ [project.optional-dependencies]
12
+ plot = ["matplotlib", "scienceplots"]
13
+ test = ["pytest"]
14
+ dev = ["matplotlib", "scienceplots", "pytest", "joblib"]
15
+
16
+ [tool.hatch.build.targets.wheel]
17
+ packages = ["src/scicomp3"]
18
+
19
+ [tool.pytest.ini_options]
20
+ addopts = ["--import-mode=importlib"]
21
+ testpaths = ["tests"]
22
+ markers = [
23
+ "slow: marks tests as slow (deselect with '-m \"not slow\"')",
24
+ ]
@@ -0,0 +1,26 @@
1
+ from assignment01 import *
2
+
3
+ c=1; L=1; N=90
4
+ dx = L/N
5
+ dt = 1e-3
6
+ T_sim = 10
7
+
8
+ # Use N+1 points to include both boundaries at x=0 and x=L
9
+ x = np.linspace(0, L, N+1) # [0, dx, 2dx, ..., L]
10
+ psi0 = np.sin(5*np.pi*x) # initial amplitudes Ψ
11
+ psi0[0] = 0 # enforce boundary condition at x=0
12
+ psi0[-1] = 0 # enforce boundary condition at x=L
13
+ psi_t0 = np.zeros(N+1) # initial velocities Ψ_t are set to 0
14
+ state0 = np.transpose([psi0, psi_t0]) # initial state vector [Ψ, Ψ_t]
15
+
16
+ time, states = integrate_euler(wave_eq_deriv, state0=state0, dt=dt, T_sim=T_sim, c=c, L=L, N=N+1)
17
+ amplitudes = states[:, :, 0] # Extract the amplitudes Ψ over time
18
+ fig, ax = plt.subplots(figsize=(6, 4))
19
+ for i in range(0, len(time), len(time)//11):
20
+ ax.plot(x, amplitudes[i], color=plt.cm.cividis(i/len(time)))
21
+ ax.set_xlabel('Position along string (x)')
22
+ ax.set_ylabel('String amplitude (Ψ)')
23
+ ax.set_title('Vibrating string over time')
24
+ cbar = fig.colorbar(plt.cm.ScalarMappable(cmap='cividis'), ax=ax, label='Time', ticks=np.linspace(0, 1, 3))
25
+ cbar.ax.set_yticklabels([f"{t:.1f}" for t in np.linspace(0, T_sim, 3)]) # Set colorbar ticks to actual time values
26
+ plt.show()
@@ -0,0 +1,34 @@
1
+ from assignment01 import *
2
+
3
+ c=1; L=1; N=90
4
+ dx = L/N
5
+ dt = 1e-3
6
+ T_sim = 10
7
+
8
+ psi0_i = np.sin(2*np.pi*np.arange(N)*dx) # initial amplitudes of case i
9
+ psi0_ii = np.sin(5*np.pi*np.arange(N)*dx) # initial amplitudes of case ii
10
+ psi0_iii = np.sin(5*np.pi*np.arange(N)*dx)
11
+ psi0_iii = np.where((np.arange(N)*dx < 1/5) | (np.arange(N)*dx > 2/5), 0, psi0_iii) # initial amplitudes of case iii
12
+ initial_conditions = [psi0_i, psi0_ii, psi0_iii]
13
+
14
+ psi_t0 = np.zeros(N) # initial velocities Ψ_t are set to 0
15
+
16
+ amplitude_list = []
17
+
18
+ for i, psi0 in enumerate(initial_conditions):
19
+ state0 = np.transpose([psi0, psi_t0]) # initial state vector [Ψ, Ψ_t]
20
+ time, states = integrate_euler(wave_eq_deriv, state0=state0, dt=dt, T_sim=T_sim, c=c, L=L, N=N)
21
+ amplitude_list.append(states[:, :, 0]) # Extract the amplitudes Ψ over time for each case
22
+
23
+ for i, amplitudes in enumerate(amplitude_list):
24
+ fig, ax = plt.subplots(figsize=(6, 4))
25
+ for j in range(0, len(time), len(time)//51):
26
+ ax.plot(np.arange(N)*dx, amplitudes[j], color=plt.cm.cividis(j/len(time)))
27
+ ax.set_xlabel('Position along string (x)')
28
+ ax.set_ylabel('String amplitude (Ψ)')
29
+ ax.set_title(f'Vibrating string over time - Case {i+1}')
30
+ cbar = fig.colorbar(plt.cm.ScalarMappable(cmap='cividis'), ax=ax, label='Time', ticks=np.linspace(0, 1, 3))
31
+ cbar.ax.set_yticklabels([f"{t:.1f}" for t in np.linspace(0, T_sim, 3)]) # Set colorbar ticks to actual time values
32
+ plt.savefig(f"../images/figures/vibrating_string_over_time_case={i}_dt={dt}_Tsim={T_sim}_c={c}_L={L}_N={N}.png", dpi=300)
33
+ plt.show()
34
+
@@ -0,0 +1,4 @@
1
+ from run_assignment01_02_cases_plot import *
2
+
3
+ for i, amplitudes in enumerate(amplitude_list):
4
+ animate_wave(amplitudes, case=i+1, dt=dt, T_sim=T_sim, c=c, L=L, N=N)