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.
- scicomp3-0.2.0/.github/workflows/ci.yml +26 -0
- scicomp3-0.2.0/.gitignore +11 -0
- scicomp3-0.2.0/.vscode/settings.json +7 -0
- scicomp3-0.2.0/PKG-INFO +17 -0
- scicomp3-0.2.0/README.md +316 -0
- scicomp3-0.2.0/assignment01.py +118 -0
- scicomp3-0.2.0/images/gifs/a1_6_insulators_sor.gif +0 -0
- scicomp3-0.2.0/images/gifs/a1_6_sinks_sor.gif +0 -0
- scicomp3-0.2.0/images/gifs/diffusion_dt=2.2e-05_Tsim=1.5_D=1.0_N=100.gif +0 -0
- scicomp3-0.2.0/images/gifs/diffusion_dt=9e-05_Tsim=1.5_D=1.0_N=50.gif +0 -0
- scicomp3-0.2.0/images/gifs/vibrating_string_case=1_dt=0.001_Tsim=5_c=1_L=1_N=90.gif +0 -0
- scicomp3-0.2.0/images/gifs/vibrating_string_case=2_dt=0.001_Tsim=5_c=1_L=1_N=90.gif +0 -0
- scicomp3-0.2.0/images/gifs/vibrating_string_case=3_dt=0.001_Tsim=5_c=1_L=1_N=90.gif +0 -0
- 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
- 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
- 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
- scicomp3-0.2.0/pyproject.toml +24 -0
- scicomp3-0.2.0/run_assignment01_01_smoke_plot.py +26 -0
- scicomp3-0.2.0/run_assignment01_02_cases_plot.py +34 -0
- scicomp3-0.2.0/run_assignment01_03_cases_animation.py +4 -0
- scicomp3-0.2.0/scripts/a1_1_cases_animation.py +130 -0
- scicomp3-0.2.0/scripts/a1_1_cases_compared_to_analytical.py +165 -0
- scicomp3-0.2.0/scripts/a1_1_cases_plot.py +123 -0
- scicomp3-0.2.0/scripts/a1_1_smoke_test.py +69 -0
- scicomp3-0.2.0/scripts/a1_2_diffusion.py +146 -0
- scicomp3-0.2.0/scripts/a1_2_diffusion_animation.py +100 -0
- scicomp3-0.2.0/scripts/a1_2_diffusion_verification.py +191 -0
- scicomp3-0.2.0/scripts/a1_6_insulators_gauss_seidel.py +88 -0
- scicomp3-0.2.0/scripts/a1_6_insulators_jacobi.py +88 -0
- scicomp3-0.2.0/scripts/a1_6_insulators_k_impact.py +128 -0
- scicomp3-0.2.0/scripts/a1_6_insulators_sor.py +161 -0
- scicomp3-0.2.0/scripts/a1_6_insulators_sor_animation.py +112 -0
- scicomp3-0.2.0/scripts/a1_6_iterative_convergence.py +74 -0
- scicomp3-0.2.0/scripts/a1_6_iterative_gauss_seidel.py +80 -0
- scicomp3-0.2.0/scripts/a1_6_iterative_jacobi.py +80 -0
- scicomp3-0.2.0/scripts/a1_6_iterative_methods.py +315 -0
- scicomp3-0.2.0/scripts/a1_6_iterative_sor.py +86 -0
- scicomp3-0.2.0/scripts/a1_6_objects_k_impact.py +139 -0
- scicomp3-0.2.0/scripts/a1_6_omega_for_various_N_plot.py +70 -0
- scicomp3-0.2.0/scripts/a1_6_omega_for_various_N_sim.py +53 -0
- scicomp3-0.2.0/scripts/a1_6_omega_values.py +63 -0
- scicomp3-0.2.0/scripts/a1_6_seeking_optimal_omega.py +124 -0
- scicomp3-0.2.0/scripts/a1_6_sinks_and_insulators_sor.py +98 -0
- scicomp3-0.2.0/scripts/a1_6_sinks_gauss_seidel.py +88 -0
- scicomp3-0.2.0/scripts/a1_6_sinks_jacobi.py +88 -0
- scicomp3-0.2.0/scripts/a1_6_sinks_k_impact.py +120 -0
- scicomp3-0.2.0/scripts/a1_6_sinks_sor.py +153 -0
- scicomp3-0.2.0/scripts/a1_6_sinks_sor_animation.py +112 -0
- scicomp3-0.2.0/src/scicomp3/__init__.py +27 -0
- scicomp3-0.2.0/src/scicomp3/bvp/__init__.py +6 -0
- scicomp3-0.2.0/src/scicomp3/bvp/methods.py +227 -0
- scicomp3-0.2.0/src/scicomp3/bvp/omega.py +102 -0
- scicomp3-0.2.0/src/scicomp3/bvp/solver.py +90 -0
- scicomp3-0.2.0/src/scicomp3/core/__init__.py +6 -0
- scicomp3-0.2.0/src/scicomp3/core/grid.py +66 -0
- scicomp3-0.2.0/src/scicomp3/core/result.py +47 -0
- scicomp3-0.2.0/src/scicomp3/objects/insulator.py +28 -0
- scicomp3-0.2.0/src/scicomp3/objects/shapes.py +34 -0
- scicomp3-0.2.0/src/scicomp3/objects/sink.py +31 -0
- scicomp3-0.2.0/src/scicomp3/ode/__init__.py +6 -0
- scicomp3-0.2.0/src/scicomp3/ode/methods.py +81 -0
- scicomp3-0.2.0/src/scicomp3/ode/solver.py +60 -0
- scicomp3-0.2.0/src/scicomp3/pde/__init__.py +17 -0
- scicomp3-0.2.0/src/scicomp3/pde/diffusion.py +118 -0
- scicomp3-0.2.0/src/scicomp3/pde/wave.py +91 -0
- scicomp3-0.2.0/src/scicomp3/validation/validation.py +27 -0
- scicomp3-0.2.0/tests/__init__.py +1 -0
- scicomp3-0.2.0/tests/test_boundary_conditions.py +116 -0
- scicomp3-0.2.0/tests/test_diffusion.py +135 -0
- scicomp3-0.2.0/tests/test_gauss_seidel.py +105 -0
- scicomp3-0.2.0/tests/test_jacobi.py +85 -0
- scicomp3-0.2.0/tests/test_scripts.py +94 -0
- scicomp3-0.2.0/tests/test_solver_comparison.py +335 -0
- 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
|
scicomp3-0.2.0/PKG-INFO
ADDED
|
@@ -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'
|
scicomp3-0.2.0/README.md
ADDED
|
@@ -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}'")
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
Binary file
|
|
@@ -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
|
+
|