A full-stack web app for solving and visualizing partial differential equations using finite difference methods.
This is a passion project that I have been building and researching since my Partial Differential Equations class at Augustana College, 2023. After helping many of my classmates with our Heat & Wave Equation Lab work in Python, I decided to break our computer lab questions programmatically and build a visualizer tool for different modelling and PDE equations for the Heat & Wave formulas.
Solves the Heat Equation (Forward Euler, thermal diffusion) and Wave Equation (central differences, wave propagation) with automatic CFL stability validation. Users configure spatial/temporal grids, boundary conditions, and initial conditions through an interactive UI, then watch the solution animate in real-time.
-
Backend: FastAPI + NumPy computes the complete solution in one request (
POST /api/simulations/solve). Includes 6 preset configurations (3 heat, 3 wave) and stability checking before simulation. -
Frontend: React + TypeScript + Plotly.js with client-side playback control. Supports 2D line plots, 3D surface plots, heatmaps, and a grid view.
Animation runs at 50 FPS with play/pause/reset, frame scrubbing, step controls, and 0.25x-4x speed adjustment. Axes are fixed from global min/max to prevent jumping.
Key design choice: The entire solution matrix (u_values[nt][nx]) is computed once and returned to the client, enabling instant replay without re-computation. This trades memory for responsiveness — typical simulations are 1-5 MB.
Dockerized with multi-stage builds and nginx reverse proxy for production deployment. make dev for local development with hot-reload.
- Demo
- The Mathematics: Finite Difference Method
- System Architecture
- Technology Stack
- Design Decisions & Trade-offs
- Getting Started
- API Reference
- Project Structure
- Development
- Roadmap
Thermal diffusion of a sine wave initial condition. The temperature profile smoothly decays toward equilibrium as heat dissipates through the material.
A plucked string vibrating over time. The wave reflects off fixed boundaries and produces standing wave patterns characteristic of string instruments.
The Finite Difference Method (FDM) approximates derivatives by replacing them with difference quotients on a discrete grid. Instead of solving PDEs analytically (often impossible), we compute approximate solutions at discrete points.
Continuous Domain Discrete Grid
u(x, t) u[i, n] = u(i*dx, n*dt)
t n
│ │
│ u(x,t) │ ●──●──●──●
│ ╱ │ │ │ │ │
│ ╱ │ ●──●──●──●
│ ╱ │ │ │ │ │
└────────── x └──●──●──●──● i
Physical Meaning: Describes how temperature distributes over time in a material.
Mathematical Form:
∂u/∂t = β · ∂²u/∂x²
where:
u(x,t) = temperature at position x and time t
β = thermal diffusivity (how fast heat spreads)
Finite Difference Approximation:
Forward difference in time: ∂u/∂t ≈ (u[i,n+1] - u[i,n]) / Δt
Central difference in space: ∂²u/∂x² ≈ (u[i-1,n] - 2·u[i,n] + u[i+1,n]) / Δx²
Update Formula (Forward Euler):
u[i, n+1] = u[i, n] + σ · (u[i-1, n] - 2·u[i, n] + u[i+1, n])
where σ = β·Δt/Δx² (stability parameter)
Stability Condition: The scheme is stable only if σ < 0.5
If σ ≥ 0.5: Solution explodes exponentially (numerical instability)
If σ < 0.5: Solution remains bounded and converges to true solution
Physical Meaning: Describes vibrations of a string, membrane, or acoustic waves.
Mathematical Form:
∂²u/∂t² = c² · ∂²u/∂x²
where:
u(x,t) = displacement at position x and time t
c = wave speed
Finite Difference Approximation (Central differences in both time and space):
∂²u/∂t² ≈ (u[i,n+1] - 2·u[i,n] + u[i,n-1]) / Δt²
∂²u/∂x² ≈ (u[i-1,n] - 2·u[i,n] + u[i+1,n]) / Δx²
Update Formula (Three-level scheme):
u[i, n+1] = 2·u[i, n] - u[i, n-1] + σ · (u[i-1, n] - 2·u[i, n] + u[i+1, n])
where σ = (c·Δt/Δx)² (CFL number)
Stability Condition (Courant-Friedrichs-Lewy): σ ≤ 1
σ = 1: Exact solution at grid points (optimal)
σ > 1: Unstable - solution grows without bound
σ < 1: Stable but introduces numerical dispersion
Heat Equation Stencil: Wave Equation Stencil:
n+1: [ ? ] n+1: [ ? ]
↑ ↑
n: [σ][1-2σ][σ] n: [σ][2-2σ][σ]
↓
n-1: [-1]
To compute u[i,n+1], we need:
- Heat: only values at time n
- Wave: values at times n and n-1 (requires 2 initial conditions)
┌─────────────────────────────────────────────────────────────────────────┐
│ CLIENT (Browser) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ React + TypeScript + Vite │ │
│ │ ┌─────────────┐ ┌──────────────┐ ┌─────────────────────┐ │ │
│ │ │ Parameter │ │ Visualization │ │ Simulation │ │ │
│ │ │ Panel │──│ Canvas │──│ Controls │ │ │
│ │ │ (Config) │ │ (Plotly.js) │ │ (Play/Pause/Speed) │ │ │
│ │ └─────────────┘ └──────────────┘ └─────────────────────┘ │ │
│ │ │ ▲ │ │
│ │ │ │ frameData = u_values[currentFrame] │ │
│ │ ▼ │ │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ App.tsx (State Management) │ │ │
│ │ │ • completeSolution: {x_values, t_values, u_values} │ │ │
│ │ │ • currentFrame: number (animated via RAF) │ │ │
│ │ │ • playbackSpeed: 0.25x - 4x │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ │ │ │ │
│ │ │ POST /api/simulations/solve │ │
│ │ ▼ │ │
│ │ ┌────────────────────────────────────────────────────────┐ │ │
│ │ │ API Service (Axios) │ │ │
│ │ └────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
│
│ HTTP / WebSocket
▼
┌─────────────────────────────────────────────────────────────────────────┐
│ SERVER (FastAPI) │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ API Routes │ │
│ │ POST /api/simulations/solve ─→ SimulationService.solve() │ │
│ │ POST /api/simulations/validate ─→ StabilityValidator │ │
│ │ GET /api/presets ─→ Preset configurations │ │
│ └─────────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────────┐ │
│ │ PDESimulator │ │
│ │ ┌───────────────────┐ ┌───────────────────┐ │ │
│ │ │ HeatEquationSolver │ │ WaveEquationSolver │ │ │
│ │ │ (Forward Euler) │ │ (Central Diff) │ │ │
│ │ └───────────────────┘ └───────────────────┘ │ │
│ │ │ │ │ │
│ │ └────────────┬───────────────┘ │ │
│ │ ▼ │ │
│ │ ┌─────────────────────────────────────────────────────────┐ │ │
│ │ │ NumPy Array: u_values[nt, nx] (Complete Solution) │ │ │
│ │ └─────────────────────────────────────────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────────┘
┌──────────┐ ┌──────────┐ ┌──────────┐ ┌──────────┐
│ User │ │ Frontend │ │ Backend │ │ Solver │
└────┬─────┘ └────┬─────┘ └────┬─────┘ └────┬─────┘
│ │ │ │
│ Configure │ │ │
│ Parameters │ │ │
│───────────────>│ │ │
│ │ │ │
│ Click "Solve" │ │ │
│───────────────>│ │ │
│ │ POST /solve │ │
│ │ {config} │ │
│ │───────────────>│ │
│ │ │ solve(config) │
│ │ │───────────────>│
│ │ │ │
│ │ │ ┌─────────────────┐
│ │ │ │ Time-stepping │
│ │ │ │ loop: │
│ │ │ │ for n in 0..nt │
│ │ │ │ u[n+1] = f(u) │
│ │ │ └─────────────────┘
│ │ │ │
│ │ │<───────────────│
│ │ │ u_values[nt,nx]│
│ │<───────────────│ │
│ │ CompleteSolution │
│ │ │ │
│ │ ┌─────────────────────────────┐ │
│ │ │ Animation Loop (50 FPS): │ │
│ │ │ frame++ │ │
│ │ │ render(u_values[frame]) │ │
│ │ └─────────────────────────────┘ │
│<───────────────│ │ │
│ Animated │ │ │
│ Visualization │ │ │
│ │ │ │
┌─────────────────────────────────────────────────────────────────────┐
│ FRONTEND │
│ │
│ ┌─────────────────┐ ┌─────────────────┐ ┌─────────────────┐ │
│ │ ParameterPanel │ │ PresetSelector │ │SimulationControls│ │
│ │ ───────────── │ │ ─────────────── │ │ ─────────────── │ │
│ │ • Equation │ │ • Heat Gaussian │ │ • Play/Pause │ │
│ │ • Spatial dx │ │ • Heat Sine │ │ • Reset │ │
│ │ • Temporal dt │ │ • Wave Standing │ │ • Step ±1 │ │
│ │ • Boundaries │ │ • Wave Plucked │ │ • Speed 0.25-4x │ │
│ │ • Initial Cond │ │ • Wave Gaussian │ │ • Time Scrubber │ │
│ │ • β or c │ │ • Heat Step │ │ │ │
│ └────────┬────────┘ └────────┬────────┘ └────────┬────────┘ │
│ │ │ │ │
│ └──────────────────────┴──────────────────────┘ │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────────────────┐ │
│ │ VisualizationCanvas │ │
│ │ ┌─────────────┐ ┌─────────────┐ ┌─────────────┐ ┌─────────┐ │ │
│ │ │ 2D Plot │ │ 3D Surface │ │ Heatmap │ │ Grid │ │ │
│ │ │ u(x) line │ │ u(x,t) │ │ x vs t │ │ 2x2 │ │ │
│ │ └─────────────┘ └─────────────┘ └─────────────┘ └─────────┘ │ │
│ └───────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
┌─────────────────────────────────────────────────────────────────────┐
│ BACKEND │
│ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ SimulationService │ │
│ │ • create_simulation() • solve_simulation() │ │
│ │ • validate_config() • stream_results() │ │
│ └─────────────────────────────────────────────────────────────┘ │
│ │ │
│ ▼ │
│ ┌─────────────────────────────────────────────────────────────┐ │
│ │ PDESimulator │ │
│ │ │ │ │
│ │ ┌──────────────────────┼──────────────────────┐ │ │
│ │ ▼ ▼ ▼ │ │
│ │ ┌──────────────┐ ┌───────────────┐ ┌────────────────┐ │ │
│ │ │ Initial │ │ Boundary │ │ Stability │ │ │
│ │ │ Condition │ │ Condition │ │ Validator │ │ │
│ │ │ Manager │ │ Manager │ │ (CFL Check) │ │ │
│ │ └──────────────┘ └───────────────┘ └────────────────┘ │ │
│ │ │ │ │
│ │ ┌──────────────────────┴──────────────────────┐ │ │
│ │ ▼ ▼ │ │
│ │ ┌──────────────────────┐ ┌──────────────────────┐ │ │
│ │ │ HeatEquationSolver │ │ WaveEquationSolver │ │ │
│ │ │ ────────────────── │ │ ────────────────── │ │ │
│ │ │ • Forward Euler │ │ • Central Diff │ │ │
│ │ │ • σ = β·dt/dx² │ │ • σ = (c·dt/dx)² │ │ │
│ │ │ • Stability: σ<0.5 │ │ • Stability: σ≤1 │ │ │
│ │ └──────────────────────┘ └──────────────────────┘ │ │
│ └─────────────────────────────────────────────────────────────┘ │
└─────────────────────────────────────────────────────────────────────┘
| Component | Technology | Purpose |
|---|---|---|
| Framework | FastAPI 0.109 | Async REST API with auto-docs |
| Server | Uvicorn | High-performance ASGI server |
| Validation | Pydantic | Data models & validation |
| Numerics | NumPy, SciPy | Matrix operations & solvers |
| Real-time | WebSockets | Optional streaming support |
| Runtime | Python 3.11 | Latest stable Python |
| Component | Technology | Purpose |
|---|---|---|
| Framework | React 18.2 | Component-based UI |
| Language | TypeScript | Type-safe development |
| Build | Vite 5.0 | Fast HMR & bundling |
| Visualization | Plotly.js 2.27 | Interactive 2D/3D plots |
| HTTP Client | Axios 1.6 | API requests |
| Styling | CSS Modules | Scoped styling |
| Component | Technology | Purpose |
|---|---|---|
| Containers | Docker | Consistent environments |
| Orchestration | Docker Compose | Multi-service deployment |
| Reverse Proxy | Nginx | Static files & API proxy |
| Build Automation | Make | Developer commands |
Decision: Compute the entire solution on the backend, return as one response, animate on the frontend.
| Approach | Pros | Cons |
|---|---|---|
| Complete Solution (chosen) | Simple, deterministic, no sync issues | Higher memory for large grids |
| WebSocket Streaming | Lower latency, progressive display | Complex state, potential desync |
Rationale: For typical grid sizes (< 10,000 points), memory is not a concern. The complete solution approach eliminates race conditions and simplifies the frontend animation logic.
Decision: Use explicit Forward Euler for the heat equation.
| Method | Pros | Cons |
|---|---|---|
| Forward Euler (chosen) | Simple, fast per step, vectorizable | Requires σ < 0.5 (small time steps) |
| Backward Euler | Unconditionally stable | Requires solving linear system each step |
| Crank-Nicolson | 2nd order accurate, stable | More complex implementation |
Rationale: For educational purposes and interactive demos, Forward Euler's simplicity and speed are valuable. The stability restriction is communicated clearly to users.
Decision: Use React's built-in useState and useEffect hooks.
| Approach | Pros | Cons |
|---|---|---|
| useState (chosen) | Simple, no dependencies, sufficient for scope | Manual prop drilling |
| Redux | Centralized state, time-travel debugging | Boilerplate, overkill for this app |
| Zustand | Minimal boilerplate, easy setup | Another dependency to manage |
Rationale: The application has a relatively simple state tree (config, solution, frame index, playback state). React's built-in state management is sufficient and keeps the bundle small.
Decision: Use Plotly.js for all visualizations.
| Library | Pros | Cons |
|---|---|---|
| Plotly.js (chosen) | Built-in 2D/3D, scientific defaults, interactive | Large bundle (~3MB) |
| D3.js | Maximum flexibility, small core | No built-in 3D, steep learning curve |
| Three.js | Best 3D performance | Overkill for surface plots |
Rationale: Plotly provides scientific visualization out-of-the-box with good interactivity. The bundle size trade-off is acceptable for a simulation platform.
Decision: Use REST endpoints.
| Approach | Pros | Cons |
|---|---|---|
| REST (chosen) | Simple, well-understood, good caching | Multiple requests for related data |
| GraphQL | Single request, typed schema | Complexity, learning curve |
Rationale: The API surface is simple (solve, validate, presets). REST's simplicity is preferred over GraphQL's flexibility for this use case.
Decision: Use multi-stage builds for production images.
# Builder stage: Node 18, npm install, build
# Production stage: Nginx Alpine, copy dist onlyBenefit: Final image is ~50MB instead of ~1GB (no node_modules).
Decision: Fix axis ranges based on global min/max of the complete solution.
Problem: If axes auto-scale per frame, the plot "jumps" during animation.
Solution: Compute global_min and global_max from metadata, set as fixed range.
- Docker & Docker Compose (recommended)
- OR Python 3.11+ and Node.js 18+ (for local development)
# 1. Clone the repository
git clone https://github.com/your-username/Math460.git
cd Math460
# 2. Build and start containers
make build
make up
# 3. Access the application
# Frontend: http://localhost
# Backend API: http://localhost:8000
# API Docs: http://localhost:8000/docs# 1. Clone and enter directory
git clone https://github.com/your-username/Math460.git
cd Math460
# 2. Install dependencies
make install
# 3. Start development servers
make dev
# Frontend: http://localhost:5173
# Backend: http://localhost:8000Backend:
cd backend
python -m venv venv
source venv/bin/activate # Windows: venv\Scripts\activate
pip install -r requirements.txt
uvicorn app.main:app --reload --port 8000Frontend:
cd frontend
npm install
npm run dev# Check backend health
curl http://localhost:8000/health
# Expected response:
# {"status": "healthy", "version": "1.0.0"}| Method | Endpoint | Description |
|---|---|---|
GET |
/health |
Health check |
POST |
/api/simulations/validate |
Validate configuration |
POST |
/api/simulations/solve |
Compute complete solution |
GET |
/api/presets |
List preset configurations |
curl -X POST http://localhost:8000/api/simulations/solve \
-H "Content-Type: application/json" \
-d '{
"equation_type": "heat",
"spatial_domain": {"x_min": 0, "x_max": 1, "dx": 0.01},
"temporal_domain": {"t_min": 0, "t_max": 0.5, "dt": 0.0001},
"physical_parameters": {"beta": 0.1},
"boundary_condition": {"type": "dirichlet", "left_value": 0, "right_value": 0},
"initial_condition": {"type": "gaussian", "parameters": {"amplitude": 1, "center": 0.5, "width": 0.1}}
}'{
"simulation_id": "abc123",
"config": { ... },
"x_values": [0, 0.01, 0.02, ...],
"t_values": [0, 0.0001, 0.0002, ...],
"u_values": [[...], [...], ...],
"metadata": {
"global_min": -0.05,
"global_max": 1.0,
"nx": 101,
"nt": 5001,
"computation_time_ms": 45,
"stability_parameter": 0.1
}
}const ws = new WebSocket('ws://localhost:8000/ws/simulation/abc123');
ws.onmessage = (event) => {
const msg = JSON.parse(event.data);
if (msg.type === 'data') {
// msg.data contains { time_index, time_value, x_values, u_values }
}
};
ws.send(JSON.stringify({ command: 'start' }));Math460/
├── backend/ # FastAPI Backend
│ ├── app/
│ │ ├── main.py # Application entry point
│ │ ├── api/
│ │ │ └── routes.py # REST endpoints
│ │ ├── core/
│ │ │ ├── pde_simulator.py # Solver orchestrator
│ │ │ ├── heat_equation_solver.py # Heat equation (Forward Euler)
│ │ │ ├── wave_equation_solver.py # Wave equation (Central Diff)
│ │ │ ├── stability_validator.py # CFL condition checker
│ │ │ ├── initial_condition_manager.py
│ │ │ └── boundary_condition_manager.py
│ │ ├── models/
│ │ │ └── schemas.py # Pydantic data models
│ │ ├── services/
│ │ │ └── simulation_service.py
│ │ ├── websockets/
│ │ │ └── handlers.py # WebSocket streaming
│ │ └── presets/
│ │ └── simulation_presets.py
│ ├── tests/
│ ├── requirements.txt
│ └── Dockerfile
│
├── frontend/ # React Frontend
│ ├── src/
│ │ ├── App.tsx # Main component & state
│ │ ├── main.tsx # Entry point
│ │ ├── components/
│ │ │ ├── ParameterPanel.tsx # Configuration UI
│ │ │ ├── VisualizationCanvas.tsx # Plotly renderer
│ │ │ ├── SimulationControls.tsx # Playback controls
│ │ │ ├── PresetSelector.tsx
│ │ │ └── ...
│ │ ├── services/
│ │ │ ├── api.ts # Axios client
│ │ │ └── websocket.ts # WebSocket client
│ │ ├── types/
│ │ │ └── simulation.ts # TypeScript interfaces
│ │ └── utils/
│ │ └── validation.ts
│ ├── package.json
│ ├── vite.config.ts
│ ├── nginx.conf
│ └── Dockerfile
│
├── Model/ # Original Python implementations
│ ├── wave_eq.py
│ └── wave_final.py
│
├── docker-compose.yml # Production deployment
├── docker-compose.dev.yml # Development with hot-reload
├── Makefile # Build commands
├── CLAUDE.md # Architecture documentation
└── README.md # This file
make help # Show all commands
make install # Install all dependencies
make dev # Run both servers locally
make build # Build Docker images
make up # Start production containers
make up-dev # Start dev containers with hot-reload
make down # Stop all containers
make logs # View all logs
make test # Run backend tests
make lint # Run linters
make format # Format code
make clean # Clean build artifacts# Backend tests
cd backend
pytest tests/ -v
# With coverage
pytest tests/ --cov=app --cov-report=htmlBackend (backend/.env):
DEBUG=true
ENVIRONMENT=development
HOST=0.0.0.0
PORT=8000
ALLOWED_ORIGINS=http://localhost:3000,http://localhost:5173Frontend (frontend/.env):
VITE_API_BASE_URL=http://localhost:8001
VITE_WS_BASE_URL=ws://localhost:8001- Heat equation solver with Forward Euler
- Wave equation solver with central differences
- CFL stability validation
- REST API with FastAPI
- React frontend with Plotly.js
- 2D, 3D, and heatmap visualizations
- Client-side animation playback
- Docker containerization
- 6 preset configurations
- WebSocket streaming refinement
- Enhanced 3D visualization controls
- Parameter sweep mode
- 2D spatial domain (heat in a plate)
- Neumann and periodic boundary conditions
- Custom expression parser for initial conditions
- Export to MP4/GIF
- GPU acceleration (WebGL shaders)
- Simulation comparison view
Educational and research use only. See institution guidelines.
- Numerical methods based on classical finite difference theory
- Original implementations in
heat_eq.pyandModel/wave_eq.py - Built with FastAPI, React, and Plotly.js communities
Version: 2.0.0 Last Updated: February 2026 Status: Active Development



