using DelimitedFiles, CSV, DataFrames
H_grid = readdlm("units/unit_01/data/bay_bathymetry.csv", ',', Float64)
mask = readdlm("units/unit_01/data/bay_mask.csv", ',', Int)
gauges = CSV.read("units/unit_01/data/bay_gauges.csv", DataFrame)
river = CSV.read("units/unit_01/data/river_source.csv", DataFrame)Unit 1: Introduction
This unit is motivation, not curriculum. We solve a real-looking coastal-oceanography problem end-to-end on day one so that everything that follows has a destination: the PDE theory in Unit 6, the deep-learning toolkit in Unit 2, the SciML stack in Units 3–4, the PINN machinery in Units 5 and 7, and the AIMS thermistor-chain capstone in Units 8–10. You are not expected to write or understand this code yet — the goal is to watch the workflow and build a mental picture of where the course lands.
This unit opens the course with a concrete, hands-on problem — a Brisbane River surge propagating into Moreton Bay — that threads together every theme you will revisit over the next nine units: PDEs, Julia, computation, real measurements, the inverse problem, ocean / water modelling, and PINNs. Then it previews the larger AIMS capstone in §1.3.
1.1 An example scientific question
Before we touch any code, let’s set up a concrete problem and the gap between what we can measure in it and what we actually want to know. The next two subsections sketch (a) the physical setting — Brisbane’s outflow into Moreton Bay — and (b) the asymmetry between cheap downstream observations and the hard-to-measure upstream forcing that drives them. That asymmetry is what the rest of the unit will try to close.
Where the Brisbane River meets Moreton Bay
Most of Brisbane’s storm runoff and a meaningful slice of South-East Queensland’s wastewater eventually drains down the Brisbane River and exits at Pile Light into Moreton Bay — a shallow, semi-enclosed body of water bounded by the mainland to the west, Moreton Island to the east, and North Stradbroke Island to the south-east. The bay is a Ramsar-listed estuarine nursery, a major shipping channel for the Port of Brisbane, and the recreational heart of South-East Queensland; what comes down the river matters to ecology, public health, fisheries, and shipping.
After heavy rain in the upper catchment, the river runs hard for a day or two and pushes a freshwater surge — a low-density plume carrying sediment, nutrients, and (occasionally) sewage overflow — out into the bay. The bay responds: tide gauges across the bay record a small, transient surface-elevation anomaly riding on top of the usual tide, even though the wind is calm. The Healthy Land & Water Moreton Bay catchment report cards have been tracking the downstream impact for years; the bay’s seagrass beds in the western flats still bear scars from the 2011 and 2022 floods.
What we actually measure, and what we don’t
Here is the operational catch: the bay has plenty of tide gauges (Mud Island, Wellington Point, Tangalooma anchorage, Russell / Macleay channel — Brisbane Bar / Pile Light too, but it sits so close to the river mouth that it doesn’t constrain the source much more than a direct measurement would), but the river outflow itself is hard to instrument in flood conditions — current meters get washed out, banks become inaccessible, suspended-sediment loads kill optical probes within hours. We rarely have a reliable time-history of what came out of the river during an event, only of how the bay responded over the days that followed.
A real tide gauge in Moreton Bay records the total water level
\eta_{\text{gauge}}(t) \;=\; \underbrace{\eta_{\text{tide}}(t)}_{\sim\, 1.5\,\text{m}} \;+\; \underbrace{\eta_{\text{surge}}(t)}_{\sim\, 0.45\,\text{m}} \;+\; \text{noise.}
The tidal part is huge compared to the surge we care about — roughly 1.5 m of astronomical tide vs. the ~45 cm freshwater surge — but it’s also predictable. From a long calibration record at each gauge, oceanographers fit a harmonic tidal model (a sum of \sim 30 sinusoids at known astronomical frequencies: M_2 at 12.42 h, S_2 at 12.00 h, K_1 at 23.93 h, O_1 at 25.82 h, and so on); \eta_{\text{tide}}(t) can then be reconstructed and subtracted from the live record. What remains is the de-tided residual, \eta_{\text{gauge}}(t) - \eta_{\text{tide}}(t) \approx \eta_{\text{surge}}(t) + \text{noise} — which is what we treat as the synthetic mooring data through this unit (with \sigma = 1.5\,\text{cm} Gaussian noise to imitate the harmonic-fit residual after de-tiding).
Two practical points worth carrying forward:
- De-tiding is itself an inverse problem of sorts. Errors in the harmonic constants (especially the shallow-water tidal components M_4, M_6 near the river mouth) leak directly into the surge residual. Any source-recovery inversion built on tide-gauge data inherits that calibration uncertainty.
- Surge timescale ≪ tidal timescale, but not by much. A 3-hour freshwater surge has substantial energy at periods near the diurnal and semi-diurnal tidal bands. Aggressive de-tiding (notch filters around the tidal lines) risks smearing surge content; sloppy de-tiding leaves tide leakage in the residual. The Brisbane River Hydraulic Model the Department of Environment uses for operational forecasts hits this trade-off head-on.
That sets up the scientific question we will solve in this unit:
Given sparse, noisy tide-gauge timeseries scattered across Moreton Bay, can we reconstruct the time-history of the surge at the river mouth that produced them?
If we can, the same machinery — a forward physical model plus a learned source — lets us infer contaminant load, freshwater discharge, and bay-flushing timescales from measurements that are already routinely collected. The river that you cannot instrument in a flood becomes legible through the gauges that survived in the bay.
The same shape of question recurs throughout coastal oceanography: given downstream observations, infer the upstream forcing. It is the classical inverse problem of geophysics — and it is notoriously ill-posed, because many different upstream scenarios can produce essentially the same downstream signal once the physics has smoothed, delayed, and noisified it. We will solve it twice in this unit so the ill-posedness is visible before we spend Units 5–9 learning the modern (PINN-based) tools that disambiguate it.
1.2 The worked example
This section walks the full pipeline end-to-end: we set up a tractable model of the bay, write down the physics, run a forward simulation to generate synthetic measurements, then try to invert those measurements back to the river-mouth source two different ways. Each subsubsection is one stage of that pipeline. All code lives under units/unit_01/scripts/ — the runtime + role table at the end of §1.2 gives the full inventory.
The geography, made tractable
We choose a domain that is small enough to evaluate in seconds on a laptop CPU but big enough to capture the geography that matters:
- A 50 \times 95\,\text{km} rectangle at 500\,\text{m} resolution — so the SWE mesh is 100 \times 190 = 19\,000 cells, of which about half are water.
- A hand-built bathymetry that echoes the real bay: a \sim 6\,\text{m} inner-bay flat, an eastern deep channel (the NE channel) rising to \sim 25\,\text{m} between Moreton Island and the mainland, the dredged \sim 14\,\text{m} Brisbane River shipping channel, and a 30–50\,\text{m} shelf east of Moreton Island.
- A land mask for the mainland coast, Moreton Island, North Stradbroke Island, the South Stradbroke spit, the Russell / Macleay / Karragarra island cluster in the southern bay, and a Peel / Mud cluster mid-bay. The southern bay genuinely is half-blocked off by islands and mangroves — modelling it as open water (as we did in an earlier draft of this unit) made the surge propagation visibly wrong.
This is not a bathymetric chart. For real work, students should use AusBathyTopo (Geoscience Australia) or the GEBCO global grids — both are open and downloadable. The hand-built model here is the smallest one that gets the shape of the answer right; it takes the place a real bathymetry file would in production.
The bay model is produced by scripts/build_bay.jl and stored as plain CSVs in units/unit_01/data/:
The CSVs are committed in the repo, so you have three options:
- Clone the whole course (recommended for hands-on work):
git clone https://github.com/open-AIMS/Julia_PINN_training_2026 - Download individual files straight from GitHub — click any of the linked filenames above and use Raw → Save as.
- Regenerate them with
julia --project=. units/unit_01/scripts/build_bay.jlfrom the repo root. The script is fast (a few seconds) and the only inputs are the constants at the top of the file, so you can change the grid, coastline, or gauge layout and rebuild.
Throughout the course, paths shown in backticks (e.g. scripts/build_bay.jl) are repo-relative; the same path also works as a GitHub link when rendered.
The physics in one paragraph
The water motion is described by the linearised shallow-water equations (de Wolff et al. 2021, eq. 6–7; reproduced in detail in §7.2). With H(x,y) the local depth, g gravity, and b a small linear drag, the surface elevation \eta(x, y, t) and depth-averaged horizontal velocity \mathbf{u}(x, y, t) = (u, v) satisfy
\underbrace{\frac{\partial \eta}{\partial t} + \nabla\!\cdot\!\bigl(H\,\mathbf{u}\bigr) = 0}_{\textbf{continuity (mass)}} \tag{1}
\underbrace{\frac{\partial \mathbf{u}}{\partial t} + g\,\nabla \eta = -b\,\mathbf{u}}_{\textbf{momentum}} \tag{2}
To see what kind of physics this encodes, take \partial/\partial t of the continuity equation and substitute \partial \mathbf{u}/\partial t from the momentum equation:
\frac{\partial^2 \eta}{\partial t^2} \;=\; -\nabla\!\cdot\!\Bigl(H\,\tfrac{\partial \mathbf{u}}{\partial t}\Bigr) \;=\; \nabla\!\cdot\!\bigl(g\,H\,\nabla \eta\bigr) \;+\; \nabla\!\cdot\!\bigl(H\,b\,\mathbf{u}\bigr).
The drag term is small over the few-hour surge window (we revisit it in the callout below); dropping it leaves a single 2-D wave equation for \eta,
\frac{\partial^2 \eta}{\partial t^2} \;=\; \nabla\!\cdot\!\bigl(g\,H\,\nabla \eta\bigr), \tag{3}
i.e. pressure waves on a free surface, propagating at a depth- dependent speed c(x,y) = \sqrt{g\,H(x,y)} — roughly 8\,\text{m/s} in the inner bay and 15\,\text{m/s} along the deeper eastern trough. This is the cleanest form to think about and is what the inverse solver below uses as its physics constraint.
We have dropped the Coriolis, advective (\mathbf{u}\!\cdot\!\nabla\mathbf{u}), and Reynolds-stress terms from the full SWE. For surge events of a few hours over a region \lesssim 100\,\text{km}, with no strong wind, the dominant balance is between pressure gradient and free-surface acceleration — exactly the same balance that gives a tsunami or tidal wave its speed. We will revisit when each dropped term matters in §7.2.
We close the system with a Dirichlet boundary condition at the river-mouth cell — a “Dirichlet” BC just means we pin the value of \eta at that point, as opposed to a “Neumann” BC which would pin its derivative:
\eta(x_r, y_r, t) \;=\; \psi(t),
where \psi(t) is the surface-elevation anomaly imposed by the river inflow. In the forward problem we prescribe \psi(t); in the inverse problem we will recover it from gauge data. Solid coasts get the no-flux condition \mathbf{u}\cdot\hat{\mathbf{n}}=0 (water doesn’t cross the coast), and a thin Rayleigh-damped sponge — a strip of cells where we add an artificial -\sigma\eta relaxation term — along the eastern edge of the domain lets surge waves leave through the open ocean without bouncing back into the solution.
A synthetic surge, propagated by finite differences
To generate the data we will work with, we run the linearised SWE forward on a staggered Arakawa-C grid — a layout where \eta lives at cell centres and the velocity components u, v live at the corresponding cell faces, which avoids the spurious checkerboard modes a co-located grid would suffer from — with explicit time stepping at \Delta t = 12\,\text{s} for 6 hours. The full driver is scripts/generate_surge_data.jl; Unit 6 covers staggered-grid FD schemes properly. The “ground-truth” surge is a two-pulse profile
\psi_{\text{truth}}(t) \;=\; 0.45 \exp\!\left[-\left(\tfrac{t - 2.0\,\text{h}}{0.55\,\text{h}}\right)^2\right] \;+\; 0.18 \exp\!\left[-\left(\tfrac{t - 4.3\,\text{h}}{0.55\,\text{h}}\right)^2\right]
— a \sim 45\,\text{cm} leading freshwater pulse from heavy catchment rain, followed by a \sim 18\,\text{cm} follow-on bulge about two hours later. These amplitudes are realistic for the de-tided residual of a moderate Brisbane River flood.
The inner FD step is just a discretisation of Equation 1 and Equation 2 — the listing below is what the code actually executes:
# Inner FD step (linearised SWE on an Arakawa-C grid).
# η lives at cell centres; u, v on the vertical/horizontal faces.
for step in 1:NT
t = step * DT
# 1. Mass continuity: ∂η/∂t = -∂(Hu·u)/∂x - ∂(Hv·v)/∂y
@inbounds for j in 2:NY-1, i in 2:NX-1
mask[j, i] == 1 || continue
flux_x_e = Hu[j, i ] * u[j, i ]
flux_x_w = Hu[j, i-1] * u[j, i-1]
flux_y_n = Hv[j, i] * v[j, i]
flux_y_s = Hv[j-1, i] * v[j-1, i]
η[j, i] -= DT * ((flux_x_e - flux_x_w) / DX +
(flux_y_n - flux_y_s) / DY)
end
# 2. Sponge layer near the east boundary absorbs outgoing waves
@inbounds for j in 1:NY, i in 1:NX
sponge[j, i] > 0 && (η[j, i] -= DT * sponge[j, i] * η[j, i])
end
# 3. River-mouth source (Dirichlet on η at the source cell)
η[JR, IR] = ψ_truth(t)
# 4. Momentum (using the new η): ∂u/∂t = -g ∂η/∂x - b·u
@inbounds for j in 1:NY, i in 1:NX-1
if u_open[j, i]
dηdx = (η[j, i+1] - η[j, i]) / DX
u[j, i] += DT * (-G_GRAV * dηdx - B_DRAG * u[j, i])
else
u[j, i] = 0.0
end
end
# ... v update mirrors u, omitted for brevity ...
endThe full forward solve takes \sim 12\,\text{s} on a laptop at 500\,\text{m} resolution.
Watching the surge cross the bay
The slider below scrubs through the FD solution: 73 snapshots, 5 minutes of real time per step, total 6 hours. Drag the handle to follow the surge from rest, through the two peaks, to the trailing ring-down. The red triangle is the Brisbane River source, the gold discs are the four tide gauges, and grey is land.
A few things to notice as you scrub:
- Wave speed depends on depth. c = \sqrt{g\,H}. In the inner bay (H \approx 6\,\text{m}) that is about 8\,\text{m/s}; along the eastern deep trough you can see the wavefront noticeably faster.
- Geometry matters. The surge wraps around the Peel / Mud cluster and reflects off Moreton Island; the southern bay is slow to fill because of the Russell / Macleay / Karragarra constriction.
- Different gauges see different signals. G1 (Mud Island, ~18 km NNE of the mouth) responds first and strongest. G2 (Wellington Point, ~26 km ESE) sees a clean propagated wavefront across the central bay. G3 (Tangalooma, ~35 km NE) and G4 (Russell Channel, ~50 km south) see attenuated, delayed, reflection-laden signals. It’s the contrast between gauges, not any one of them, that constrains ψ(t).
The measurements we get
The four gauges produce timeseries \eta_g(t_k) at one-minute sampling. We add \sigma = 1.5\,\text{cm} Gaussian observation noise to imitate real de-tided tide-gauge residuals, then thin to the kind of cadence (\sim 3 min) that operational systems actually use.
| Gauge | Location | Lat, Lon | Cell | Depth |
|---|---|---|---|---|
| G1 | Mud Island | -27.210°S, 153.215°E | (39, 143) | 6.0 m |
| G2 | Wellington Point | -27.480°S, 153.236°E | (43, 83) | 6.0 m |
| G3 | Tangalooma Roads | -27.205°S, 153.320°E | (60, 144) | 18.2 m |
| G4 | Russell Channel | -27.660°S, 153.290°E | (54, 43) | 6.0 m |
These four timeseries, the bathymetry, and the physics are all the information the inverse solver gets. The river-mouth source \psi(t) is hidden.
The obvious choice for the closest gauge would be the Brisbane Bar / Pile Light tide gauge — it’s a real BoM station, right at the river entrance. We deliberately don’t use it: at ~1.5 km from the model’s river-mouth cell, Brisbane Bar essentially measures ψ(t) directly (travel time ~3 min, no smoothing), so the inversion from that gauge alone reduces to a one-to-one read-off and the broader four-gauge machinery looks like overkill.
Mud Island (~18 km NNE of the mouth, real mid-bay tide-gauge site) is the closest gauge where the Green’s-function smoothing, geometric spreading, and reflection paths actually do something to the signal. With G1 at Mud Island, all four gauges are meaningful observers and the inverse-problem story is honest.
If you want to try the Brisbane-Bar version anyway, change the G1 row in scripts/build_bay.jl back to (-27.367, 153.166) and ./build.sh execute 1 to regenerate.
The inverse problem, two ways
We try two complementary approaches that bracket the spectrum covered by the rest of the course. You are not expected to read either listing in detail yet — they are sketches of what the inverse problem looks like once you have Units 2–9 in your toolbox.
Method A — Naive PINN. Train a smooth MLP (multi-layer perceptron, the basic feed-forward neural network — covered in Unit 2) \eta_\theta(x, y, t) on inputs scaled to [-1, 1]^3 to match the gauge observations, satisfy the wave equation (Equation 3) at random water collocation points (the random space-time points where we evaluate the PDE residual as a soft constraint on the network — covered in Unit 5), and respect the zero initial condition. Treat the recovered source as \psi_\theta(t) := \eta_\theta(x_r, y_r, t). This is what de Wolff et al. (2021) classified as the “vanilla PINN” baseline and what §7.2 of this course re-derives in detail. The full training driver is scripts/train_inverse_pinn.jl.
# Sketch of the naive PINN loss — see scripts/train_inverse_pinn.jl.
function total_loss(θ, Xphys, Kx, Ky, Xz, Xt, Xsm)
# Data: gauge readings ↔ η_θ at the gauge cells
L_data = mean((η_batch(GAUGE_INPUT, θ) .- GAUGE_TARGET).^2)
# Physics: ∂_ττ η = K_x ∂_ξξ η + K_y ∂_ζζ η in normalised coords,
# sampled at random water-cell collocation points (excluding a small
# radius around the Dirichlet source so we don't fight the BC).
η_p = reshape(η_batch(Xphys, θ), 7, :)
η_ξξ = (η_p[2, :] .- 2 .* η_p[1, :] .+ η_p[3, :]) ./ H_NX^2
η_ζζ = (η_p[4, :] .- 2 .* η_p[1, :] .+ η_p[5, :]) ./ H_NY^2
η_ττ = (η_p[6, :] .- 2 .* η_p[1, :] .+ η_p[7, :]) ./ H_NT^2
L_phys = mean((η_ττ .- Kx .* η_ξξ .- Ky .* η_ζζ) .^ 2)
# IC + smoothness terms — see script
return LAM_DATA*L_data + LAM_PHYS*L_phys + LAM_IC*L_ic + ...
endMethod B — Adjoint inverse using the simulator as a forward map. Because the linearised SWE is linear in \psi, the forward map G: \psi(\cdot) \mapsto \eta_g(\cdot) is a linear operator. Its columns are the Green’s function of the bay — the response at every gauge to a unit-impulse source at the river mouth. Compute it once (a single FD pass with a narrow Gaussian pulse), assemble G as a Toeplitz convolution matrix (the standard “discrete convolution as a matrix” trick: each row of G is the impulse response shifted in time by one sample), and solve the Tikhonov-regularised linear least-squares problem
\hat\psi \;=\; \arg\min_\psi \; \tfrac{1}{2}\,\|G\psi - g_{\text{obs}}\|_2^2 \;+\; \tfrac{\lambda^2}{2}\,\|\mathrm{L}^{(2)} \psi\|_2^2,
where \mathrm{L}^{(2)} is the discrete second-difference operator. The first term says “the predicted gauges should match observations”; the second term — Tikhonov regularisation, with strength \lambda — penalises non-smooth \psi, which makes the otherwise ill-posed inversion stable. No neural network — but it uses the same physics that the PINN tries to learn. The full driver is scripts/train_inverse_adjoint.jl.
# Sketch of the adjoint inverse — see scripts/train_inverse_adjoint.jl.
h_g = fd_forward(narrow_gaussian_pulse) # impulse response per gauge
G = vcat([toeplitz_from_impulse(h_g[:, k]) for k in 1:NG]...)
A = vcat(G, λ_smooth * L2, λ_anchor * e1') # stacked LHS
b = vcat(g_obs, zeros(size(L2, 1)), 0.0)
ψ̂ = A \ b # closed-form TikhonovWhat we recover
The plot is the centrepiece of Unit 1. Two observations matter pedagogically:
- Both methods fit the gauge observations. Look at any of the four gauge panels: the adjoint prediction (blue) and the naive-PINN prediction (dashed orange) both lie inside the noisy cloud of observations at G1–G4. By the metric the algorithms are optimising, both succeed.
- They disagree wildly on what produced those gauges. The adjoint inverse recovers both surge peaks at roughly correct amplitude (0.49\,\text{m} and 0.20\,\text{m}, vs truth 0.45 / 0.18). The naive PINN gets the timing of the main pulse but flattens its amplitude to \sim 0.12\,\text{m} — a factor of nearly four under.
This is the classic signature of an ill-posed inverse problem: many qualitatively different sources can produce the same downstream signal once it has been spread by the physics, smoothed by the gauges, and contaminated by noise. The remedy is to add a prior — the adjoint inverse adds it explicitly through Tikhonov smoothness; the naive PINN implicitly priors-in the smooth-MLP ansatz, which turns out to bias the answer in the wrong direction here.
de Wolff et al. (2021) documented exactly this failure mode on the same linearised SWE problem. §7.2–§7.3 walk through the modern fixes — Fourier feature embeddings, causal training, hard boundary-condition enforcement, adaptive loss weighting — that close the gap, so that by Unit 9 the PINN can do what the adjoint does here without the luxury of precomputing G.
What you have just seen
In one worked example we have touched, at an introductory depth, every theme of the course:
| Theme | Where it appeared |
|---|---|
| PDEs | Linearised SWE, derived as a 2-D wave equation |
| Julia | The whole forward + inverse stack, ~\!400 LOC |
| Computation | Staggered-grid FD solve in 12\,\text{s} |
| Measurements | Four noisy tide-gauge timeseries |
| Inverse problem | Recover \psi(t) from gauge data |
| Ocean / water modelling | Real geography (Moreton Bay), realistic surge amplitudes |
| PINNs | Naive PINN baseline, its failure mode, the path to fix it |
The scripts for the actual SWE solver, animation, and both inversions all live under units/unit_01/scripts/. Click any filename to jump to the source on GitHub:
| Script | Role | Runtime |
|---|---|---|
build_bay.jl |
bathymetry + land mask + gauge layout + bathymetry figure | ~\!1 s |
generate_site_map.py |
OSM/CartoDB site-map figure | ~\!10 s |
generate_surge_data.jl |
linearised-SWE forward solve, gauge timeseries, snapshots | ~\!12 s |
generate_surge_frames.jl |
per-snapshot PNG frames for the slider | ~\!90 s |
generate_surge_animation.jl |
optional GIF assembled from the surge frames | ~\!30 s |
train_inverse_pinn.jl |
naive PINN inverse (Lux.jl for the network, Zygote.jl for autodiff — both covered in Unit 2 / Unit 5) |
~\!3 min |
train_inverse_adjoint.jl |
Green’s-function / Tikhonov inverse | ~\!15 s |
render_recovery_plot.jl |
the ψ + 2×2 gauge comparison plot | ~\!2 s |
The whole pipeline is offline-reproducible on a single laptop CPU. Everything is written so it ports cleanly to a GPU once the rest of the course has motivated why we’d want one.
1.3 The capstone preview: an AIMS thermistor column
The Moreton Bay surge is a 2-D shallow-water problem. The capstone across Units 8–10 keeps the inverse-problem flavour but moves the physics: a 1-D vertical column of ocean at three AIMS-monitored reef sites along the central Great Barrier Reef, with a thermistor chain sampling temperature at five depths every hour.
After a storm passes through, the deeper sensors record an unexpected cooling pattern. The competing hypotheses:
- The storm intensified upwelling — cold water pumped up from depth.
- The storm enhanced vertical mixing — heat drawn down from the surface faster than usual.
- Surface heating reduced — cloud cover and evaporation cut the net flux into the ocean.
Each leaves a distinct fingerprint in the depth–time record. The full model specification — Task A (single-site, CPU) and Task B (three-site joint, GPU) — lives in Unit 9; the worked solution that recovers which mechanism dominated is in Unit 10.
Same intellectual shape as today’s worked example — physics + sparse data → unknown driver — but with a different geometry, different physics, and a richer inverse-problem story.
1.4 Course roadmap
Each remaining unit picks up one ingredient that today’s worked example assumed without explanation. In order:
- Unit 2 — Machine-learning + deep-learning basics. The function-approximation toolkit (linear models, MLPs, autodiff, optimisers) you’ll need to even write a PINN.
- Unit 3 — Scientific-ML landscape. A survey of physics-informed ML approaches, how PINNs fit in, and what other tools exist (Neural ODEs, operator learning, SINDy, …).
- Unit 4 — ODEs via universal approximation. Neural ODEs and Universal Differential Equations (UDEs) — the gateway between classical dynamical systems and learned components.
- Unit 5 — Your first PINN. Basic ODE and PDE models, the loss-as-physics-constraint pattern, the collocation-points machinery used above.
- Unit 6 — Formal PDE theory + classical numerics. What a wave equation is, where finite differences come from, why the staggered Arakawa-C grid we used isn’t arbitrary.
- Unit 7 — Modern PINN techniques for harder PDEs. Fourier features, causal training, hard BC enforcement, adaptive loss weighting — the fixes for the exact failure mode the naive PINN exhibited above.
- Unit 8 — PDE modelling in key AIMS domains. The reef-scale ocean physics the capstone column rests on: the column idealisation, advection vs diffusion in fluids, the surface energy budget, stratification and mixing, dimensionless regimes.
- Unit 9 — Capstone specification. Both versions of the project: Task A (single-site, CPU-friendly) and Task B (three-site joint inverse, GPU-class). Every equation, parameter, deliverable, and success criterion.
- Unit 10 — Capstone solution. Worked solutions for both tasks, behind a triple-click reveal so you can attempt the project first. Closes the loop by recovering the unknown driver from sparse mooring data — exactly the workflow you saw in §1.2.
75 % Julia, 25 % Python
Julia is the primary language: Lux.jl, NeuralPDE.jl, MethodOfLines.jl, the broader SciML stack. Python parallels appear in Unit 2 (scikit-learn, PyTorch) and Unit 10 (DeepXDE) so participants leave with a sense of the cross-ecosystem landscape.