From 102197e028709b7f83ff95bf8f7ac14640c78c5b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dion=20H=C3=A4fner?= Date: Thu, 2 Apr 2026 09:59:28 +0200 Subject: [PATCH 1/4] add learned closure demo --- .../burgers_solver/tesseract_api.py | 158 +++++ .../burgers_solver/tesseract_config.yaml | 6 + .../burgers_solver/tesseract_requirements.txt | 3 + demo/learned-closure/demo.ipynb | 588 ++++++++++++++++++ .../neural_viscosity/tesseract_api.py | 192 ++++++ .../neural_viscosity/tesseract_config.yaml | 6 + .../tesseract_requirements.txt | 2 + demo/learned-closure/requirements.txt | 6 + demo/learned-closure/test_solvers.py | 188 ++++++ pyproject.toml | 1 + 10 files changed, 1150 insertions(+) create mode 100644 demo/learned-closure/burgers_solver/tesseract_api.py create mode 100644 demo/learned-closure/burgers_solver/tesseract_config.yaml create mode 100644 demo/learned-closure/burgers_solver/tesseract_requirements.txt create mode 100644 demo/learned-closure/demo.ipynb create mode 100644 demo/learned-closure/neural_viscosity/tesseract_api.py create mode 100644 demo/learned-closure/neural_viscosity/tesseract_config.yaml create mode 100644 demo/learned-closure/neural_viscosity/tesseract_requirements.txt create mode 100644 demo/learned-closure/requirements.txt create mode 100644 demo/learned-closure/test_solvers.py diff --git a/demo/learned-closure/burgers_solver/tesseract_api.py b/demo/learned-closure/burgers_solver/tesseract_api.py new file mode 100644 index 000000000..fff57e4d0 --- /dev/null +++ b/demo/learned-closure/burgers_solver/tesseract_api.py @@ -0,0 +1,158 @@ +# Copyright 2025 Pasteur Labs. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Single-timestep Burgers' equation solver Tesseract. + +Solves one explicit Euler step of the 1D viscous Burgers' equation: + + u^{n+1} = u^n + dt * (-u * du/dx + nu * d²u/dx²) + +The viscosity field nu is provided as an input — the solver does not compute it. +This clean interface (state + material field → next state) is the same contract +that a Fortran solver with an adjoint could implement. The outer time-stepping +loop and closure evaluation live in the caller, enabling per-timestep closure +calls and end-to-end gradient flow through both solver and closure. +""" + +from typing import Any + +import equinox as eqx +import jax +import jax.numpy as jnp +from pydantic import BaseModel, Field + +from tesseract_core.runtime import Array, Differentiable, Float64 +from tesseract_core.runtime.tree_transforms import filter_func, flatten_with_paths + +# Default grid size +N = 128 + +# --- Grid setup (fixed for this Tesseract) --- +DX = 1.0 / (N - 1) + + +class InputSchema(BaseModel): + u: Differentiable[Array[(N,), Float64]] = Field( + description="Current velocity field on the grid" + ) + nu: Differentiable[Array[(N,), Float64]] = Field( + description="Viscosity field at each grid point (must be positive)" + ) + dt: float = Field(description="Time step size", default=1e-4) + + +class OutputSchema(BaseModel): + u_next: Differentiable[Array[(N,), Float64]] = Field( + description="Velocity field after one time step" + ) + + +@eqx.filter_jit +def apply_jit(inputs: dict) -> dict: + u = inputs["u"] + nu = inputs["nu"] + dt = inputs["dt"] + + # Spatial derivatives via central differences + dudx = jnp.zeros_like(u) + dudx = dudx.at[1:-1].set((u[2:] - u[:-2]) / (2 * DX)) + + d2udx2 = jnp.zeros_like(u) + d2udx2 = d2udx2.at[1:-1].set((u[2:] - 2 * u[1:-1] + u[:-2]) / (DX**2)) + + # Burgers' equation: du/dt = -u * du/dx + nu * d²u/dx² + dudt = -u * dudx + nu * d2udx2 + + # Forward Euler step + u_next = u + dt * dudt + + # Enforce boundary conditions (Dirichlet: hold boundary values) + u_next = u_next.at[0].set(u[0]) + u_next = u_next.at[-1].set(u[-1]) + + return {"u_next": u_next} + + +def apply(inputs: InputSchema) -> OutputSchema: + return apply_jit(inputs.model_dump()) + + +def abstract_eval(abstract_inputs: Any) -> Any: + return {"u_next": {"shape": [N], "dtype": "float64"}} + + +@eqx.filter_jit +def jvp_jit( + inputs: dict, + jvp_inputs: tuple[str], + jvp_outputs: tuple[str], + tangent_vector: dict, +) -> Any: + filtered_apply = filter_func(apply_jit, inputs, jvp_outputs) + return jax.jvp( + filtered_apply, + [flatten_with_paths(inputs, include_paths=jvp_inputs)], + [tangent_vector], + )[1] + + +def jacobian_vector_product( + inputs: InputSchema, + jvp_inputs: set[str], + jvp_outputs: set[str], + tangent_vector: dict[str, Any], +) -> Any: + return jvp_jit( + inputs.model_dump(), + tuple(jvp_inputs), + tuple(jvp_outputs), + tangent_vector, + ) + + +@eqx.filter_jit +def vjp_jit( + inputs: dict, + vjp_inputs: tuple[str], + vjp_outputs: tuple[str], + cotangent_vector: dict, +) -> Any: + filtered_apply = filter_func(apply_jit, inputs, vjp_outputs) + _, vjp_func = jax.vjp( + filtered_apply, flatten_with_paths(inputs, include_paths=vjp_inputs) + ) + return vjp_func(cotangent_vector)[0] + + +def vector_jacobian_product( + inputs: InputSchema, + vjp_inputs: set[str], + vjp_outputs: set[str], + cotangent_vector: dict[str, Any], +) -> Any: + return vjp_jit( + inputs.model_dump(), + tuple(vjp_inputs), + tuple(vjp_outputs), + cotangent_vector, + ) + + +@eqx.filter_jit +def jac_jit( + inputs: dict, + jac_inputs: tuple[str], + jac_outputs: tuple[str], +) -> Any: + filtered_apply = filter_func(apply_jit, inputs, jac_outputs) + return jax.jacrev(filtered_apply)( + flatten_with_paths(inputs, include_paths=jac_inputs) + ) + + +def jacobian( + inputs: InputSchema, + jac_inputs: set[str], + jac_outputs: set[str], +) -> Any: + return jac_jit(inputs.model_dump(), tuple(jac_inputs), tuple(jac_outputs)) diff --git a/demo/learned-closure/burgers_solver/tesseract_config.yaml b/demo/learned-closure/burgers_solver/tesseract_config.yaml new file mode 100644 index 000000000..d4cad068f --- /dev/null +++ b/demo/learned-closure/burgers_solver/tesseract_config.yaml @@ -0,0 +1,6 @@ +name: "burgers-solver" +version: "0.1.0" +description: "1D Burgers equation solver with pluggable neural viscosity closure" + +build_config: + target_platform: "native" diff --git a/demo/learned-closure/burgers_solver/tesseract_requirements.txt b/demo/learned-closure/burgers_solver/tesseract_requirements.txt new file mode 100644 index 000000000..bfb32e0f7 --- /dev/null +++ b/demo/learned-closure/burgers_solver/tesseract_requirements.txt @@ -0,0 +1,3 @@ +jax[cpu]==0.5.2 +equinox +tesseract-core diff --git a/demo/learned-closure/demo.ipynb b/demo/learned-closure/demo.ipynb new file mode 100644 index 000000000..7166967f8 --- /dev/null +++ b/demo/learned-closure/demo.ipynb @@ -0,0 +1,588 @@ +{ + "cells": [ + { + "cell_type": "markdown", + "id": "9w0p8edklyb", + "metadata": {}, + "source": "# Learned Closure: Training a Neural Viscosity Model Through a PDE Solver\n\nThis demo trains a neural network closure **end-to-end through a PDE solver**, with gradients flowing through both the solver and the network during training.\n\n**The setup**: a 1D Burgers' equation solver where the viscosity model — normally a hand-tuned constant — is replaced by a small neural network. The closure is called at every timestep to predict the current viscosity field from the current flow state, and we train the network by differentiating through the entire time-stepping loop.\n\n**The key result**: the learned closure recovers the true (unknown) viscosity profile from solution data alone, and produces better predictions than either the pure physics model (wrong constant viscosity) or a pure ML model (no physics structure).\n\n## Architecture: two Tesseracts, composed in an outer loop\n\nThe demo uses two independent Tesseracts:\n\n- **`burgers_solver`**: a single-timestep Burgers' equation solver. Takes the current velocity field `u` and a viscosity field `nu`, returns the velocity field after one explicit Euler step. This is a pure physics component with a clean interface: `(u, nu, dt) → u_next`.\n- **`neural_viscosity`**: a small MLP that maps local flow features $(u, \\partial u/\\partial x, x)$ to a viscosity field $\\nu$. Exposes standard gradient endpoints (VJP, JVP, Jacobian).\n\nThe outer time-stepping loop lives in the training script and calls both Tesseracts via `apply_tesseract` at every timestep:\n\n```\nfor each timestep:\n nu_field = apply_tesseract(closure, {u, dudx, x, weights})\n u_next = apply_tesseract(solver, {u, nu_field, dt})\n u = u_next\n```\n\nBecause `apply_tesseract` registers each call as a JAX custom primitive, `jax.grad` through the loop automatically dispatches VJP calls back through both Tesseracts. Gradients flow end-to-end with no manual plumbing.\n\n### Why this architecture matters\n\nThe solver's interface — `(u, nu_field, dt) → u_next` — is **not specific to JAX**. A Fortran solver with a hand-written adjoint, or one differentiated by [Enzyme](https://enzyme.mit.edu/) at the LLVM IR level (see the `enzyme_thermal_2d` demo), could implement the same contract. The outer loop and training code would be identical. This is the core Tesseract value proposition: **the closure researcher doesn't need to know how the solver computes its gradients**." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "p0o9kf3si4", + "metadata": {}, + "outputs": [], + "source": [ + "import sys\n", + "\n", + "sys.path.insert(0, \"neural_viscosity\")\n", + "sys.path.insert(0, \"burgers_solver\")\n", + "\n", + "import burgers_solver.tesseract_api as solver_api\n", + "import jax\n", + "import jax.numpy as jnp\n", + "import matplotlib.pyplot as plt\n", + "import neural_viscosity.tesseract_api as closure_api\n", + "import optax\n", + "from tesseract_jax import apply_tesseract\n", + "\n", + "from tesseract_core import Tesseract\n", + "\n", + "jax.config.update(\"jax_enable_x64\", True)\n", + "\n", + "# Grid setup (must match the solver)\n", + "N = 128\n", + "DX = 1.0 / (N - 1)\n", + "X = jnp.linspace(0.0, 1.0, N)\n", + "\n", + "# Load both Tesseracts (local dev mode — no Docker needed)\n", + "closure_tess = Tesseract.from_tesseract_api(\"neural_viscosity/tesseract_api.py\")\n", + "solver_tess = Tesseract.from_tesseract_api(\"burgers_solver/tesseract_api.py\")\n", + "\n", + "print(f\"Closure endpoints: {closure_tess.available_endpoints}\")\n", + "print(f\"Solver endpoints: {solver_tess.available_endpoints}\")" + ] + }, + { + "cell_type": "markdown", + "id": "t2spi8jeftd", + "metadata": {}, + "source": "## 1. The ground truth: Burgers' equation with spatially-varying viscosity\n\nWe generate training data from a Burgers' equation with a **known but non-trivial** viscosity profile:\n\n$$\\nu_{\\text{true}}(x) = \\nu_0 \\left(1 + A \\sin(\\pi x)\\right)$$\n\nThis represents a spatially-varying material property — analogous to a turbulence closure, constitutive law, or sub-grid model that varies across the domain. The neural closure's job is to recover this profile from solution data alone." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "kyq2477ckq", + "metadata": {}, + "outputs": [], + "source": [ + "# True viscosity profile\n", + "NU_0 = 0.02\n", + "A = 0.8\n", + "nu_true = NU_0 * (1.0 + A * jnp.sin(jnp.pi * X))\n", + "\n", + "\n", + "# Reference solver: Burgers' equation with known viscosity (no neural network).\n", + "# Uses the same single-step stencil as the solver Tesseract.\n", + "def burgers_reference(u0, nu_field, dt, n_steps):\n", + " \"\"\"Solve Burgers' equation with a prescribed viscosity field.\"\"\"\n", + "\n", + " def step(u, _):\n", + " out = solver_api.apply_jit({\"u\": u, \"nu\": nu_field, \"dt\": dt})\n", + " return out[\"u_next\"], u\n", + "\n", + " u_final, u_history = jax.lax.scan(step, u0, None, length=n_steps)\n", + " return u_final, u_history\n", + "\n", + "\n", + "# Generate training data: multiple initial conditions\n", + "DT = 5e-5\n", + "N_STEPS = 200\n", + "key = jax.random.PRNGKey(0)\n", + "\n", + "\n", + "def make_ic(key):\n", + " \"\"\"Random smooth initial condition: sum of low-frequency sinusoids.\"\"\"\n", + " k1, k2, k3 = jax.random.split(key, 3)\n", + " a1 = 0.5 + 0.5 * jax.random.uniform(k1)\n", + " a2 = 0.3 * jax.random.uniform(k2)\n", + " phase = jax.random.uniform(k3) * jnp.pi\n", + " u0 = a1 * jnp.sin(2 * jnp.pi * X + phase) + a2 * jnp.sin(4 * jnp.pi * X)\n", + " # Zero boundary conditions\n", + " u0 = u0.at[0].set(0.0).at[-1].set(0.0)\n", + " return u0\n", + "\n", + "\n", + "N_TRAIN = 8\n", + "N_TEST = 4\n", + "keys = jax.random.split(key, N_TRAIN + N_TEST)\n", + "\n", + "train_ics = jnp.stack([make_ic(keys[i]) for i in range(N_TRAIN)])\n", + "test_ics = jnp.stack([make_ic(keys[N_TRAIN + i]) for i in range(N_TEST)])\n", + "\n", + "# Generate ground-truth solutions\n", + "train_targets = jnp.stack(\n", + " [burgers_reference(ic, nu_true, DT, N_STEPS)[0] for ic in train_ics]\n", + ")\n", + "test_targets = jnp.stack(\n", + " [burgers_reference(ic, nu_true, DT, N_STEPS)[0] for ic in test_ics]\n", + ")\n", + "\n", + "print(f\"Training set: {N_TRAIN} initial conditions -> solutions\")\n", + "print(f\"Test set: {N_TEST} initial conditions -> solutions\")\n", + "print(f\"Grid: {N} points, dt={DT}, {N_STEPS} steps\")\n", + "\n", + "# Visualize one example\n", + "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", + "axes[0].plot(X, train_ics[0], label=\"Initial condition\")\n", + "axes[0].plot(X, train_targets[0], label=f\"Solution at t={DT * N_STEPS:.3f}\")\n", + "axes[0].set_xlabel(\"x\")\n", + "axes[0].set_ylabel(\"u\")\n", + "axes[0].legend()\n", + "axes[0].set_title(\"Example: Burgers' equation with true viscosity\")\n", + "\n", + "axes[1].plot(X, nu_true, \"k-\", linewidth=2)\n", + "axes[1].set_xlabel(\"x\")\n", + "axes[1].set_ylabel(r\"$\\nu(x)$\")\n", + "axes[1].set_title(\"True viscosity profile (to be learned)\")\n", + "axes[1].set_ylim(bottom=0)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "a6moibwyik9", + "metadata": {}, + "source": "## 2. Initialize the neural closure\n\nThe closure is a small MLP with 2 hidden layers of 32 units each. Its weights are passed as explicit inputs to the solver so that `jax.grad` can differentiate through the entire pipeline." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "rvuazdiorw", + "metadata": {}, + "outputs": [], + "source": [ + "def init_params(key):\n", + " \"\"\"Initialize neural closure weights with Xavier initialization.\"\"\"\n", + " keys = jax.random.split(key, 3)\n", + " return {\n", + " \"w1\": jax.random.normal(keys[0], (3, 32)) * jnp.sqrt(2.0 / 3),\n", + " \"b1\": jnp.zeros(32),\n", + " \"w2\": jax.random.normal(keys[1], (32, 32)) * jnp.sqrt(2.0 / 32),\n", + " \"b2\": jnp.zeros(32),\n", + " \"w3\": jax.random.normal(keys[2], (32, 1)) * jnp.sqrt(2.0 / 32),\n", + " \"b3\": jnp.zeros(1),\n", + " }\n", + "\n", + "\n", + "params = init_params(jax.random.PRNGKey(1))\n", + "print(f\"Total parameters: {sum(p.size for p in jax.tree.leaves(params))}\")\n", + "\n", + "# Verify the closure produces sensible output\n", + "test_nu = closure_api.apply_jit(\n", + " {\"u\": train_ics[0], \"dudx\": jnp.gradient(train_ics[0], DX), \"x\": X, **params}\n", + ")[\"nu\"]\n", + "print(\n", + " f\"Initial viscosity range: [{float(test_nu.min()):.4f}, {float(test_nu.max()):.4f}]\"\n", + ")" + ] + }, + { + "cell_type": "markdown", + "id": "81k5zamfwmo", + "metadata": {}, + "source": "## 3. End-to-end training: differentiating through the solver\n\nThe training loss is:\n\n$$\\mathcal{L}(\\theta) = \\frac{1}{M} \\sum_{i=1}^{M} \\left\\| u_{\\text{solver}}(u_0^{(i)}; \\nu_\\theta) - u_{\\text{data}}^{(i)} \\right\\|^2$$\n\nwhere $\\nu_\\theta$ is the neural viscosity closure with parameters $\\theta$, and $u_{\\text{solver}}$ runs the full Burgers' equation with $\\nu_\\theta$ called at every timestep.\n\nThe outer loop calls both Tesseracts via `apply_tesseract`:\n\n```python\nfor each timestep:\n nu = apply_tesseract(closure_tess, {u, dudx, x, weights})[\"nu\"]\n u = apply_tesseract(solver_tess, {u, nu, dt})[\"u_next\"]\n```\n\n`jax.grad` differentiates through the entire loop — through every timestep, through both `apply_tesseract` calls — automatically. Each `apply_tesseract` dispatches VJP calls back to the respective Tesseract during the backward pass." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "hcpebqrhika", + "metadata": {}, + "outputs": [], + "source": [ + "def solve_with_closure(u0, params, dt, n_steps):\n", + " \"\"\"Run the full time-stepping loop, calling both Tesseracts at each step.\n", + "\n", + " This is the composition pattern:\n", + " closure: (u, dudx, x, weights) -> nu_field\n", + " solver: (u, nu_field, dt) -> u_next\n", + "\n", + " The solver Tesseract is a pure physics component. In production, it could be\n", + " a Fortran solver with an adjoint — the interface is the same.\n", + " \"\"\"\n", + " u = u0\n", + " for _step in range(n_steps):\n", + " dudx = jnp.zeros_like(u)\n", + " dudx = dudx.at[1:-1].set((u[2:] - u[:-2]) / (2 * DX))\n", + "\n", + " # Closure: predict viscosity from current flow state\n", + " closure_out = apply_tesseract(\n", + " closure_tess, {\"u\": u, \"dudx\": dudx, \"x\": X, **params}\n", + " )\n", + " nu = closure_out[\"nu\"]\n", + "\n", + " # Solver: one explicit Euler step\n", + " solver_out = apply_tesseract(solver_tess, {\"u\": u, \"nu\": nu, \"dt\": dt})\n", + " u = solver_out[\"u_next\"]\n", + " return u\n", + "\n", + "\n", + "def loss_single(params, u0, target):\n", + " \"\"\"Loss for a single initial condition: MSE between solver output and data.\"\"\"\n", + " u_final = solve_with_closure(u0, params, DT, N_STEPS)\n", + " return jnp.mean((u_final - target) ** 2)\n", + "\n", + "\n", + "def loss_batch(params, ics, targets):\n", + " \"\"\"Mean loss over a batch of initial conditions.\"\"\"\n", + " losses = jax.vmap(lambda u0, tgt: loss_single(params, u0, tgt))(ics, targets)\n", + " return jnp.mean(losses)\n", + "\n", + "\n", + "grad_fn = jax.grad(loss_batch)\n", + "\n", + "# Verify gradients work\n", + "print(\"Testing forward pass + gradient...\")\n", + "l0 = loss_batch(params, train_ics, train_targets)\n", + "g0 = grad_fn(params, train_ics, train_targets)\n", + "print(f\" Initial loss: {float(l0):.6e}\")\n", + "print(\n", + " f\" Gradient norm: {float(jnp.sqrt(sum(jnp.sum(g**2) for g in jax.tree.leaves(g0)))):.6e}\"\n", + ")\n", + "print(\" Gradients flow: loss -> solver VJP -> closure VJP -> network weights.\")" + ] + }, + { + "cell_type": "markdown", + "id": "n9twlbyke1c", + "metadata": {}, + "source": "### Gradient validation against finite differences\n\nCorrectness proof: the AD gradients match finite differences to high precision." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "x9an2u42wj", + "metadata": {}, + "outputs": [], + "source": [ + "# Finite difference check on a few weight elements\n", + "eps = 1e-5\n", + "print(f\"{'Parameter':>10s} {'Index':>8s} {'AD':>14s} {'FD':>14s} {'Rel. Error':>12s}\")\n", + "for pname in [\"w1\", \"w2\", \"w3\"]:\n", + " idx = (0, 0)\n", + "\n", + " def fd_loss(val, _pname=pname, _idx=idx):\n", + " p = {**params, _pname: params[_pname].at[_idx].set(val)}\n", + " return loss_batch(p, train_ics[:2], train_targets[:2])\n", + "\n", + " v0 = params[pname][idx]\n", + " fd = (fd_loss(v0 + eps) - fd_loss(v0 - eps)) / (2 * eps)\n", + " ad = jax.grad(loss_batch)({**params}, train_ics[:2], train_targets[:2])[pname][idx]\n", + " rel_err = abs(float(ad) - float(fd)) / (abs(float(fd)) + 1e-30)\n", + " print(\n", + " f\"{pname:>10s} {idx!s:>8s} {float(ad):14.6e} {float(fd):14.6e} {rel_err:12.2e}\"\n", + " )" + ] + }, + { + "cell_type": "markdown", + "id": "k28hfvj2mk", + "metadata": {}, + "source": "### Training loop" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "dc1zxbtpmla", + "metadata": {}, + "outputs": [], + "source": [ + "N_EPOCHS = 500\n", + "LR = 3e-3\n", + "\n", + "# Re-initialize for a clean training run\n", + "params = init_params(jax.random.PRNGKey(1))\n", + "optimizer = optax.adam(LR)\n", + "opt_state = optimizer.init(params)\n", + "\n", + "train_losses = []\n", + "test_losses = []\n", + "\n", + "\n", + "@jax.jit\n", + "def train_step(params, opt_state, ics, targets):\n", + " loss, grads = jax.value_and_grad(loss_batch)(params, ics, targets)\n", + " updates, opt_state_new = optimizer.update(grads, opt_state, params)\n", + " params_new = optax.apply_updates(params, updates)\n", + " return params_new, opt_state_new, loss\n", + "\n", + "\n", + "print(\"Training neural closure through the solver...\")\n", + "for epoch in range(N_EPOCHS):\n", + " params, opt_state, train_loss = train_step(\n", + " params, opt_state, train_ics, train_targets\n", + " )\n", + " train_losses.append(float(train_loss))\n", + "\n", + " if epoch % 50 == 0 or epoch == N_EPOCHS - 1:\n", + " test_loss = float(loss_batch(params, test_ics, test_targets))\n", + " test_losses.append((epoch, test_loss))\n", + " print(\n", + " f\" Epoch {epoch:4d}: train loss = {train_losses[-1]:.4e}, test loss = {test_loss:.4e}\"\n", + " )\n", + "\n", + "print(f\"\\nFinal train loss: {train_losses[-1]:.4e}\")\n", + "print(f\"Final test loss: {test_losses[-1][1]:.4e}\")" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "g7fwvt08nj9", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", + "\n", + "axes[0].semilogy(train_losses, label=\"Train\")\n", + "test_epochs, test_vals = zip(*test_losses, strict=False)\n", + "axes[0].semilogy(test_epochs, test_vals, \"o-\", label=\"Test\")\n", + "axes[0].set_xlabel(\"Epoch\")\n", + "axes[0].set_ylabel(\"MSE Loss\")\n", + "axes[0].set_title(\"Training loss\")\n", + "axes[0].legend()\n", + "axes[0].grid(True, alpha=0.3)\n", + "\n", + "# Compare learned viscosity to true viscosity.\n", + "# Evaluate the closure Tesseract at several representative flow states.\n", + "nu_samples = []\n", + "for ic in train_ics:\n", + " dudx = jnp.zeros_like(ic)\n", + " dudx = dudx.at[1:-1].set((ic[2:] - ic[:-2]) / (2 * DX))\n", + " nu_i = apply_tesseract(closure_tess, {\"u\": ic, \"dudx\": dudx, \"x\": X, **params})[\n", + " \"nu\"\n", + " ]\n", + " nu_samples.append(nu_i)\n", + "nu_samples = jnp.stack(nu_samples)\n", + "nu_mean = jnp.mean(nu_samples, axis=0)\n", + "nu_std = jnp.std(nu_samples, axis=0)\n", + "\n", + "axes[1].plot(X, nu_true, \"k-\", linewidth=2, label=\"True viscosity\")\n", + "axes[1].plot(X, nu_mean, \"r--\", linewidth=2, label=\"Learned (mean over ICs)\")\n", + "axes[1].fill_between(\n", + " X, nu_mean - nu_std, nu_mean + nu_std, color=\"r\", alpha=0.15, label=\"Learned (std)\"\n", + ")\n", + "axes[1].set_xlabel(\"x\")\n", + "axes[1].set_ylabel(r\"$\\nu$\")\n", + "axes[1].set_title(\"Recovered viscosity profile\")\n", + "axes[1].legend(fontsize=9)\n", + "axes[1].set_ylim(bottom=0)\n", + "axes[1].grid(True, alpha=0.3)\n", + "\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "4vl39ef67ps", + "metadata": {}, + "source": "## 4. Baselines: why the hybrid model wins\n\nWe compare three approaches:\n\n| Model | Description |\n|---|---|\n| **Constant viscosity** | Burgers' solver with $\\nu = \\nu_0$ (the standard \"wrong\" closure) |\n| **Direct ML** | MLP trained to map $u_0 \\to u_\\text{final}$ directly, no physics |\n| **Learned closure** | Neural $\\nu(u, \\partial u/\\partial x, x)$ trained through the solver (this demo) |" + }, + { + "cell_type": "code", + "execution_count": null, + "id": "r6nx7wzmax", + "metadata": {}, + "outputs": [], + "source": [ + "# --- Baseline 1: Constant viscosity ---\n", + "nu_const = NU_0 * jnp.ones(N) # Wrong: uniform instead of spatially varying\n", + "const_preds_test = jnp.stack(\n", + " [burgers_reference(ic, nu_const, DT, N_STEPS)[0] for ic in test_ics]\n", + ")\n", + "const_mse = float(jnp.mean((const_preds_test - test_targets) ** 2))\n", + "print(f\"Constant viscosity test MSE: {const_mse:.4e}\")\n", + "\n", + "\n", + "# --- Baseline 2: Direct ML (MLP mapping u0 -> u_final, no physics) ---\n", + "def init_direct_params(key):\n", + " \"\"\"Larger MLP for direct prediction (more capacity since no physics inductive bias).\"\"\"\n", + " keys = jax.random.split(key, 4)\n", + " return {\n", + " \"w1\": jax.random.normal(keys[0], (N, 128)) * jnp.sqrt(2.0 / N),\n", + " \"b1\": jnp.zeros(128),\n", + " \"w2\": jax.random.normal(keys[1], (128, 128)) * jnp.sqrt(2.0 / 128),\n", + " \"b2\": jnp.zeros(128),\n", + " \"w3\": jax.random.normal(keys[2], (128, 64)) * jnp.sqrt(2.0 / 128),\n", + " \"b3\": jnp.zeros(64),\n", + " \"w4\": jax.random.normal(keys[3], (64, N)) * jnp.sqrt(2.0 / 64),\n", + " \"b4\": jnp.zeros(N),\n", + " }\n", + "\n", + "\n", + "def direct_predict(params, u0):\n", + " \"\"\"Pure ML: MLP maps u0 directly to u_final.\"\"\"\n", + " h = jnp.tanh(u0 @ params[\"w1\"] + params[\"b1\"])\n", + " h = jnp.tanh(h @ params[\"w2\"] + params[\"b2\"])\n", + " h = jnp.tanh(h @ params[\"w3\"] + params[\"b3\"])\n", + " return h @ params[\"w4\"] + params[\"b4\"]\n", + "\n", + "\n", + "def direct_loss(params, ics, targets):\n", + " preds = jax.vmap(lambda u0: direct_predict(params, u0))(ics)\n", + " return jnp.mean((preds - targets) ** 2)\n", + "\n", + "\n", + "# Train the direct ML baseline\n", + "direct_params = init_direct_params(jax.random.PRNGKey(2))\n", + "direct_opt = optax.adam(1e-3)\n", + "direct_opt_state = direct_opt.init(direct_params)\n", + "\n", + "\n", + "@jax.jit\n", + "def direct_train_step(params, opt_state, ics, targets):\n", + " loss, grads = jax.value_and_grad(direct_loss)(params, ics, targets)\n", + " updates, opt_state_new = direct_opt.update(grads, opt_state, params)\n", + " params_new = optax.apply_updates(params, updates)\n", + " return params_new, opt_state_new, loss\n", + "\n", + "\n", + "print(\"\\nTraining direct ML baseline...\")\n", + "direct_losses = []\n", + "for epoch in range(500):\n", + " direct_params, direct_opt_state, dl = direct_train_step(\n", + " direct_params, direct_opt_state, train_ics, train_targets\n", + " )\n", + " direct_losses.append(float(dl))\n", + " if epoch % 100 == 0:\n", + " test_dl = float(direct_loss(direct_params, test_ics, test_targets))\n", + " print(\n", + " f\" Epoch {epoch:4d}: train = {direct_losses[-1]:.4e}, test = {test_dl:.4e}\"\n", + " )\n", + "\n", + "direct_test_mse = float(direct_loss(direct_params, test_ics, test_targets))\n", + "print(f\"\\nDirect ML test MSE: {direct_test_mse:.4e}\")\n", + "\n", + "# --- Learned closure (already trained above) ---\n", + "learned_test_mse = float(loss_batch(params, test_ics, test_targets))\n", + "print(f\"Learned closure test MSE: {learned_test_mse:.4e}\")\n", + "\n", + "print(f\"\\n{'Model':<25s} {'Test MSE':>12s}\")\n", + "print(\"-\" * 40)\n", + "print(f\"{'Constant viscosity':<25s} {const_mse:12.4e}\")\n", + "print(f\"{'Direct ML':<25s} {direct_test_mse:12.4e}\")\n", + "print(f\"{'Learned closure':<25s} {learned_test_mse:12.4e}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ziblnuj20e", + "metadata": {}, + "source": "## 5. Solution comparison on test data\n\nThe key visual: for an unseen initial condition, compare the three models' predictions against the ground truth." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "trgeddr2cs", + "metadata": {}, + "outputs": [], + "source": [ + "fig, axes = plt.subplots(2, 2, figsize=(12, 9))\n", + "\n", + "for idx in range(4):\n", + " ax = axes[idx // 2, idx % 2]\n", + " ic = test_ics[idx]\n", + " target = test_targets[idx]\n", + "\n", + " # Constant viscosity prediction\n", + " const_pred = burgers_reference(ic, nu_const, DT, N_STEPS)[0]\n", + "\n", + " # Direct ML prediction\n", + " direct_pred = direct_predict(direct_params, ic)\n", + "\n", + " # Learned closure prediction (outer loop calling both Tesseracts)\n", + " learned_pred = solve_with_closure(ic, params, DT, N_STEPS)\n", + "\n", + " ax.plot(X, target, \"k-\", linewidth=2, label=\"Ground truth\")\n", + " ax.plot(X, const_pred, \"b--\", linewidth=1.5, alpha=0.7, label=\"Constant viscosity\")\n", + " ax.plot(X, direct_pred, \"g:\", linewidth=1.5, alpha=0.7, label=\"Direct ML\")\n", + " ax.plot(X, learned_pred, \"r-\", linewidth=1.5, label=\"Learned closure\")\n", + " ax.set_xlabel(\"x\")\n", + " ax.set_ylabel(\"u\")\n", + " ax.set_title(f\"Test case {idx + 1}\")\n", + " ax.grid(True, alpha=0.3)\n", + " if idx == 0:\n", + " ax.legend(fontsize=9)\n", + "\n", + "plt.suptitle(\n", + " \"Solution predictions on unseen initial conditions\", fontsize=13, fontweight=\"bold\"\n", + ")\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "73cvnq9gla", + "metadata": {}, + "source": "## 6. Modularity: swap the closure *or the solver*\n\nThe solver and closure are independent Tesseracts with a clean contract: the solver takes `(u, nu_field, dt)` and returns `u_next`. The closure takes `(u, dudx, x, weights)` and returns `nu`.\n\nThis means you can:\n- **Swap the closure**: replace the neural network architecture without touching the solver\n- **Swap the solver**: replace the JAX solver with a Fortran solver (differentiated by Enzyme or a hand-written adjoint) without touching the closure or training loop\n\nHere we demonstrate closure swapping — training a different closure initialization against the same solver." + }, + { + "cell_type": "code", + "execution_count": null, + "id": "jd62sfsyzn", + "metadata": {}, + "outputs": [], + "source": [ + "# Swap: re-initialize with a different random seed (different starting point).\n", + "# In production with Tesseract containers, this would mean pointing the solver\n", + "# at a completely different closure Tesseract URL. The solver code is untouched.\n", + "\n", + "swapped_params = init_params(jax.random.PRNGKey(99)) # different seed\n", + "swapped_opt = optax.adam(3e-3)\n", + "swapped_opt_state = swapped_opt.init(swapped_params)\n", + "\n", + "print(\"Training a different closure (same solver, different initialization)...\")\n", + "for _epoch in range(500):\n", + " swapped_params, swapped_opt_state, sl = train_step(\n", + " swapped_params, swapped_opt_state, train_ics, train_targets\n", + " )\n", + "\n", + "swapped_test_mse = float(loss_batch(swapped_params, test_ics, test_targets))\n", + "print(f\" Swapped closure test MSE: {swapped_test_mse:.4e}\")\n", + "print(f\" Original closure test MSE: {learned_test_mse:.4e}\")\n", + "print(\"\\nSolver code: unchanged. Only the closure was swapped.\")\n", + "\n", + "# Compare the two learned viscosity profiles\n", + "nu_orig = apply_tesseract(\n", + " closure_tess,\n", + " {\"u\": train_ics[0], \"dudx\": jnp.gradient(train_ics[0], DX), \"x\": X, **params},\n", + ")[\"nu\"]\n", + "nu_swap = apply_tesseract(\n", + " closure_tess,\n", + " {\n", + " \"u\": train_ics[0],\n", + " \"dudx\": jnp.gradient(train_ics[0], DX),\n", + " \"x\": X,\n", + " **swapped_params,\n", + " },\n", + ")[\"nu\"]\n", + "\n", + "fig, ax = plt.subplots(figsize=(8, 4))\n", + "ax.plot(X, nu_true, \"k-\", linewidth=2, label=\"True\")\n", + "ax.plot(X, nu_orig, \"r--\", linewidth=1.5, label=\"Closure A\")\n", + "ax.plot(X, nu_swap, \"b:\", linewidth=1.5, label=\"Closure B (swapped)\")\n", + "ax.set_xlabel(\"x\")\n", + "ax.set_ylabel(r\"$\\nu$\")\n", + "ax.set_title(\"Two different closures, same solver\")\n", + "ax.legend()\n", + "ax.set_ylim(bottom=0)\n", + "ax.grid(True, alpha=0.3)\n", + "plt.tight_layout()\n", + "plt.show()" + ] + }, + { + "cell_type": "markdown", + "id": "9ydczif0r8r", + "metadata": {}, + "source": "## Summary\n\n| What | How |\n|---|---|\n| Two independent Tesseracts | Solver (single-timestep PDE) + closure (neural viscosity) |\n| Composed in an outer loop | `apply_tesseract(closure, ...)` then `apply_tesseract(solver, ...)` at each timestep |\n| End-to-end gradients | `jax.grad` dispatches VJP through both Tesseracts automatically |\n| Gradient correctness | Validated against finite differences |\n| Learned closure beats baselines | Lower test MSE than constant viscosity or direct ML |\n| Modular swapping | Change either Tesseract without touching the other |\n\n### Line of sight to production\n\nThe solver Tesseract's interface — `(u, nu_field, dt) → u_next` with VJP — is not JAX-specific. A Fortran solver differentiated by [Enzyme](https://enzyme.mit.edu/) (see the `enzyme_thermal_2d` demo) or with a hand-written discrete adjoint could implement the same contract. The training loop and closure Tesseract would be **identical**. This is the core value proposition: closure researchers get access to a library of differentiable solvers without learning each solver's internals.\n\n### What's next\n\n- **Containerized deployment**: Build each Tesseract as a Docker image. The outer loop calls both over HTTP via `apply_tesseract` — same code, real container isolation.\n- **Legacy solver integration**: Wrap a Fortran/C++ solver with adjoint as a Tesseract. The closure training loop above works unchanged.\n- **Scale up**: Larger grids, 2D/3D problems, more complex closures (e.g., convolutional, attention-based).\n- **Real applications**: Replace the Burgers' equation with a turbulence model, climate sub-grid scheme, or materials constitutive law." + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + }, + "language_info": { + "name": "python", + "version": "3.12.0" + } + }, + "nbformat": 4, + "nbformat_minor": 5 +} diff --git a/demo/learned-closure/neural_viscosity/tesseract_api.py b/demo/learned-closure/neural_viscosity/tesseract_api.py new file mode 100644 index 000000000..a06850072 --- /dev/null +++ b/demo/learned-closure/neural_viscosity/tesseract_api.py @@ -0,0 +1,192 @@ +# Copyright 2025 Pasteur Labs. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 + +"""Neural viscosity closure Tesseract. + +A small MLP that predicts spatially-varying viscosity from local flow features. +Used as a learned closure inside a PDE solver — the solver calls this Tesseract +at every timestep to get the viscosity field, and gradients flow back through +both during training. + +The network weights are passed as explicit inputs (not internal state) so that +an external optimizer can differentiate through the full solver-closure pipeline. +""" + +from typing import Any + +import equinox as eqx +import jax +import jax.numpy as jnp +from pydantic import BaseModel, Field + +from tesseract_core.runtime import Array, Differentiable, Float64 +from tesseract_core.runtime.tree_transforms import filter_func, flatten_with_paths + +# Network architecture constants +HIDDEN_DIM = 32 +N_HIDDEN_LAYERS = 2 + + +class InputSchema(BaseModel): + u: Differentiable[Array[(None,), Float64]] = Field( + description="Velocity field at grid points" + ) + dudx: Differentiable[Array[(None,), Float64]] = Field( + description="Velocity gradient du/dx at grid points" + ) + x: Array[(None,), Float64] = Field(description="Spatial coordinates of grid points") + # Network weights as flat arrays for easy composition + w1: Differentiable[Array[(3, HIDDEN_DIM), Float64]] = Field( + description="First layer weights (3 input features -> hidden)" + ) + b1: Differentiable[Array[(HIDDEN_DIM,), Float64]] = Field( + description="First layer bias" + ) + w2: Differentiable[Array[(HIDDEN_DIM, HIDDEN_DIM), Float64]] = Field( + description="Second layer weights" + ) + b2: Differentiable[Array[(HIDDEN_DIM,), Float64]] = Field( + description="Second layer bias" + ) + w3: Differentiable[Array[(HIDDEN_DIM, 1), Float64]] = Field( + description="Output layer weights (hidden -> 1)" + ) + b3: Differentiable[Array[(1,), Float64]] = Field(description="Output layer bias") + + +class OutputSchema(BaseModel): + nu: Differentiable[Array[(None,), Float64]] = Field( + description="Predicted viscosity at each grid point (always positive)" + ) + + +@eqx.filter_jit +def apply_jit(inputs: dict) -> dict: + u = inputs["u"] + dudx = inputs["dudx"] + x = inputs["x"] + + # Stack features: [u, dudx, x] at each grid point -> (N, 3) + features = jnp.stack([u, dudx, x], axis=-1) + + # Forward pass through MLP + h = features @ inputs["w1"] + inputs["b1"] + h = jnp.tanh(h) + h = h @ inputs["w2"] + inputs["b2"] + h = jnp.tanh(h) + out = h @ inputs["w3"] + inputs["b3"] + + # Sigmoid * scale to keep viscosity in a physically reasonable range. + # Range [0, nu_max] prevents CFL violations in the explicit solver. + nu_max = 0.05 + nu = nu_max * jax.nn.sigmoid(out[:, 0]) + + return {"nu": nu} + + +def apply(inputs: InputSchema) -> OutputSchema: + return apply_jit(inputs.model_dump()) + + +def abstract_eval(abstract_inputs: Any) -> Any: + is_shapedtype_dict = lambda x: type(x) is dict and (x.keys() == {"shape", "dtype"}) + is_shapedtype_struct = lambda x: isinstance(x, jax.ShapeDtypeStruct) + + jaxified_inputs = jax.tree.map( + lambda x: jax.ShapeDtypeStruct(**x) if is_shapedtype_dict(x) else x, + abstract_inputs.model_dump(), + is_leaf=is_shapedtype_dict, + ) + dynamic_inputs, static_inputs = eqx.partition( + jaxified_inputs, filter_spec=is_shapedtype_struct + ) + + def wrapped_apply(dynamic_inputs: Any) -> Any: + inputs = eqx.combine(static_inputs, dynamic_inputs) + return apply_jit(inputs) + + jax_shapes = jax.eval_shape(wrapped_apply, dynamic_inputs) + return jax.tree.map( + lambda x: ( + {"shape": x.shape, "dtype": str(x.dtype)} if is_shapedtype_struct(x) else x + ), + jax_shapes, + is_leaf=is_shapedtype_struct, + ) + + +@eqx.filter_jit +def jvp_jit( + inputs: dict, + jvp_inputs: tuple[str], + jvp_outputs: tuple[str], + tangent_vector: dict, +) -> Any: + filtered_apply = filter_func(apply_jit, inputs, jvp_outputs) + return jax.jvp( + filtered_apply, + [flatten_with_paths(inputs, include_paths=jvp_inputs)], + [tangent_vector], + )[1] + + +def jacobian_vector_product( + inputs: InputSchema, + jvp_inputs: set[str], + jvp_outputs: set[str], + tangent_vector: dict[str, Any], +) -> Any: + return jvp_jit( + inputs.model_dump(), + tuple(jvp_inputs), + tuple(jvp_outputs), + tangent_vector, + ) + + +@eqx.filter_jit +def vjp_jit( + inputs: dict, + vjp_inputs: tuple[str], + vjp_outputs: tuple[str], + cotangent_vector: dict, +) -> Any: + filtered_apply = filter_func(apply_jit, inputs, vjp_outputs) + _, vjp_func = jax.vjp( + filtered_apply, flatten_with_paths(inputs, include_paths=vjp_inputs) + ) + return vjp_func(cotangent_vector)[0] + + +def vector_jacobian_product( + inputs: InputSchema, + vjp_inputs: set[str], + vjp_outputs: set[str], + cotangent_vector: dict[str, Any], +) -> Any: + return vjp_jit( + inputs.model_dump(), + tuple(vjp_inputs), + tuple(vjp_outputs), + cotangent_vector, + ) + + +@eqx.filter_jit +def jac_jit( + inputs: dict, + jac_inputs: tuple[str], + jac_outputs: tuple[str], +) -> Any: + filtered_apply = filter_func(apply_jit, inputs, jac_outputs) + return jax.jacrev(filtered_apply)( + flatten_with_paths(inputs, include_paths=jac_inputs) + ) + + +def jacobian( + inputs: InputSchema, + jac_inputs: set[str], + jac_outputs: set[str], +) -> Any: + return jac_jit(inputs.model_dump(), tuple(jac_inputs), tuple(jac_outputs)) diff --git a/demo/learned-closure/neural_viscosity/tesseract_config.yaml b/demo/learned-closure/neural_viscosity/tesseract_config.yaml new file mode 100644 index 000000000..04f5e5738 --- /dev/null +++ b/demo/learned-closure/neural_viscosity/tesseract_config.yaml @@ -0,0 +1,6 @@ +name: "neural-viscosity" +version: "0.1.0" +description: "Neural network closure that predicts spatially-varying viscosity from local flow features" + +build_config: + target_platform: "native" diff --git a/demo/learned-closure/neural_viscosity/tesseract_requirements.txt b/demo/learned-closure/neural_viscosity/tesseract_requirements.txt new file mode 100644 index 000000000..8f33da031 --- /dev/null +++ b/demo/learned-closure/neural_viscosity/tesseract_requirements.txt @@ -0,0 +1,2 @@ +jax[cpu]==0.5.2 +equinox diff --git a/demo/learned-closure/requirements.txt b/demo/learned-closure/requirements.txt new file mode 100644 index 000000000..6e65604eb --- /dev/null +++ b/demo/learned-closure/requirements.txt @@ -0,0 +1,6 @@ +jax[cpu]==0.5.2 +equinox +matplotlib +optax +tesseract-core +tesseract-jax diff --git a/demo/learned-closure/test_solvers.py b/demo/learned-closure/test_solvers.py new file mode 100644 index 000000000..650394eac --- /dev/null +++ b/demo/learned-closure/test_solvers.py @@ -0,0 +1,188 @@ +"""Smoke tests for the learned closure demo. + +Tests the composition pattern: an outer loop calls the closure Tesseract to get +a viscosity field, then calls the solver Tesseract to step forward. Gradients +flow end-to-end through both Tesseracts via apply_tesseract / jax.grad. + +This is the same pattern that would work with a Fortran solver Tesseract backed +by Enzyme or a hand-written adjoint — the solver just needs apply + VJP with +the interface (u, nu_field, dt) -> u_next. +""" + +import sys + +sys.path.insert(0, "neural_viscosity") +sys.path.insert(0, "burgers_solver") + +import jax +import jax.numpy as jnp +from tesseract_jax import apply_tesseract + +from tesseract_core import Tesseract + +jax.config.update("jax_enable_x64", True) + +import burgers_solver.tesseract_api as solver_api # noqa: E402 +import neural_viscosity.tesseract_api as closure_api # noqa: E402 + +CLOSURE_API_PATH = "neural_viscosity/tesseract_api.py" +SOLVER_API_PATH = "burgers_solver/tesseract_api.py" + +N = 128 +DX = 1.0 / (N - 1) +X_GRID = jnp.linspace(0.0, 1.0, N) + + +def _make_closure_params(key): + """Initialize random closure network weights.""" + keys = jax.random.split(key, 6) + w1 = jax.random.normal(keys[0], (3, 32)) * jnp.sqrt(2.0 / 3) + b1 = jnp.zeros(32) + w2 = jax.random.normal(keys[1], (32, 32)) * jnp.sqrt(2.0 / 32) + b2 = jnp.zeros(32) + w3 = jax.random.normal(keys[2], (32, 1)) * jnp.sqrt(2.0 / 32) + b3 = jnp.zeros(1) + return {"w1": w1, "b1": b1, "w2": w2, "b2": b2, "w3": w3, "b3": b3} + + +def _make_initial_condition(): + """Smooth initial condition: a sine wave.""" + u0 = jnp.sin(2 * jnp.pi * X_GRID) + return u0 + + +def test_closure_forward(): + print("=== Neural viscosity closure forward pass ===") + key = jax.random.PRNGKey(0) + params = _make_closure_params(key) + u0 = _make_initial_condition() + dudx = jnp.gradient(u0, DX) + + inputs = closure_api.InputSchema(u=u0, dudx=dudx, x=X_GRID, **params) + out = closure_api.apply(inputs) + nu = out["nu"] + + print(f" Shape: {nu.shape}, range: [{float(nu.min()):.4f}, {float(nu.max()):.4f}]") + assert nu.shape == (N,) + assert jnp.all(nu > 0), "Viscosity must be positive" + print(" PASSED") + + +def test_solver_single_step(): + print("\n=== Solver single timestep ===") + u0 = _make_initial_condition() + nu = jnp.full(N, 0.01) # constant viscosity + dt = 1e-4 + + inputs = solver_api.InputSchema(u=u0, nu=nu, dt=dt) + out = solver_api.apply(inputs) + u_next = out["u_next"] + + print(f" Shape: {u_next.shape}") + print(f" Max change: {float(jnp.max(jnp.abs(u_next - u0))):.6e}") + assert u_next.shape == (N,) + assert jnp.all(jnp.isfinite(u_next)), "Solution contains NaN or Inf" + # Boundary values should be preserved + assert float(u_next[0]) == float(u0[0]), "Left BC violated" + assert float(u_next[-1]) == float(u0[-1]), "Right BC violated" + print(" PASSED") + + +def test_solver_gradient(): + print("\n=== Solver gradient (VJP w.r.t. nu field) ===") + u0 = _make_initial_condition() + nu = jnp.full(N, 0.01) + dt = 1e-4 + + def loss_fn(nu_field): + out = solver_api.apply_jit({"u": u0, "nu": nu_field, "dt": dt}) + return jnp.mean(out["u_next"] ** 2) + + grad_nu = jax.grad(loss_fn)(nu) + print( + f" Gradient shape: {grad_nu.shape}, norm: {float(jnp.linalg.norm(grad_nu)):.6e}" + ) + assert grad_nu.shape == (N,) + assert jnp.all(jnp.isfinite(grad_nu)) + print(" PASSED") + + +def test_composition_forward(): + """Outer loop calling closure + solver via apply_tesseract.""" + print("\n=== Composed forward pass (closure + solver via apply_tesseract) ===") + closure_tess = Tesseract.from_tesseract_api(CLOSURE_API_PATH) + solver_tess = Tesseract.from_tesseract_api(SOLVER_API_PATH) + + key = jax.random.PRNGKey(42) + params = _make_closure_params(key) + u = _make_initial_condition() + dt = 1e-4 + n_steps = 50 + + for _step in range(n_steps): + dudx = jnp.gradient(u, DX) + closure_out = apply_tesseract( + closure_tess, {"u": u, "dudx": dudx, "x": X_GRID, **params} + ) + nu = closure_out["nu"] + solver_out = apply_tesseract(solver_tess, {"u": u, "nu": nu, "dt": dt}) + u = solver_out["u_next"] + + print(f" Shape: {u.shape}") + print(f" Range: [{float(u.min()):.4f}, {float(u.max()):.4f}]") + assert u.shape == (N,) + assert jnp.all(jnp.isfinite(u)), "Solution contains NaN or Inf" + print(" PASSED") + + +def test_composition_gradient(): + """End-to-end gradient through solver + closure via apply_tesseract.""" + print("\n=== End-to-end gradient (closure + solver via apply_tesseract) ===") + closure_tess = Tesseract.from_tesseract_api(CLOSURE_API_PATH) + solver_tess = Tesseract.from_tesseract_api(SOLVER_API_PATH) + + key = jax.random.PRNGKey(42) + params = _make_closure_params(key) + u0 = _make_initial_condition() + target = 0.9 * u0 + dt = 1e-4 + n_steps = 20 + + def loss_fn(w1): + u = u0 + p = {**params, "w1": w1} + for _step in range(n_steps): + dudx = jnp.gradient(u, DX) + closure_out = apply_tesseract( + closure_tess, {"u": u, "dudx": dudx, "x": X_GRID, **p} + ) + nu = closure_out["nu"] + solver_out = apply_tesseract(solver_tess, {"u": u, "nu": nu, "dt": dt}) + u = solver_out["u_next"] + return jnp.mean((u - target) ** 2) + + # AD gradient + grad_ad = jax.grad(loss_fn)(params["w1"]) + + # Finite difference check on one element + eps = 1e-5 + idx = (0, 0) + w1_plus = params["w1"].at[idx].add(eps) + w1_minus = params["w1"].at[idx].add(-eps) + fd = (loss_fn(w1_plus) - loss_fn(w1_minus)) / (2 * eps) + + rel_err = abs(float(grad_ad[idx]) - float(fd)) / (abs(float(fd)) + 1e-30) + print( + f" AD: {float(grad_ad[idx]):.6e}, FD: {float(fd):.6e}, Rel error: {rel_err:.2e}" + ) + assert rel_err < 1e-2, f"Gradient error too large: {rel_err}" + print(" PASSED") + + +if __name__ == "__main__": + test_closure_forward() + test_solver_single_step() + test_solver_gradient() + test_composition_forward() + test_composition_gradient() + print("\nAll smoke tests passed.") diff --git a/pyproject.toml b/pyproject.toml index 9742f2269..3f3e956d4 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -196,6 +196,7 @@ ignore = [ "tests/*" = ["D101", "D102", "D103", "D106", "ANN"] "benchmarks/*" = ["D101", "D102", "D103", "D106", "ANN"] "examples/**/*" = ["D101", "D102", "D103", "D106", "ANN"] +"demo/**/*" = ["D101", "D102", "D103", "D106", "ANN"] "tesseract_core/sdk/templates/*" = ["D101", "D102", "D103", "D106", "ANN"] [tool.ruff.lint.pydocstyle] From fe991f07dd3daf4234ef0eefff62247c71a039a1 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dion=20H=C3=A4fner?= Date: Fri, 8 May 2026 11:28:03 +0200 Subject: [PATCH 2/4] rewrite demo to use tesseract-torch --- .../burgers_solver/tesseract_api.py | 127 ++-- .../burgers_solver/tesseract_config.yaml | 2 +- .../burgers_solver/tesseract_requirements.txt | 3 +- demo/learned-closure/demo.ipynb | 582 +++++++++++------- .../neural_viscosity/tesseract_api.py | 151 ++--- .../neural_viscosity/tesseract_config.yaml | 2 +- .../tesseract_requirements.txt | 4 +- demo/learned-closure/requirements.txt | 6 +- demo/learned-closure/test_solvers.py | 111 ++-- 9 files changed, 557 insertions(+), 431 deletions(-) diff --git a/demo/learned-closure/burgers_solver/tesseract_api.py b/demo/learned-closure/burgers_solver/tesseract_api.py index fff57e4d0..c2d0ca08a 100644 --- a/demo/learned-closure/burgers_solver/tesseract_api.py +++ b/demo/learned-closure/burgers_solver/tesseract_api.py @@ -1,7 +1,7 @@ # Copyright 2025 Pasteur Labs. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -"""Single-timestep Burgers' equation solver Tesseract. +"""Single-timestep Burgers' equation solver Tesseract (PyTorch). Solves one explicit Euler step of the 1D viscous Burgers' equation: @@ -16,10 +16,10 @@ from typing import Any -import equinox as eqx -import jax -import jax.numpy as jnp +import numpy as np +import torch from pydantic import BaseModel, Field +from torch.utils._pytree import tree_map from tesseract_core.runtime import Array, Differentiable, Float64 from tesseract_core.runtime.tree_transforms import filter_func, flatten_with_paths @@ -30,6 +30,12 @@ # --- Grid setup (fixed for this Tesseract) --- DX = 1.0 / (N - 1) +to_tensor = lambda x: ( + torch.tensor(x, dtype=torch.float64) + if isinstance(x, np.generic | np.ndarray) + else x +) + class InputSchema(BaseModel): u: Differentiable[Array[(N,), Float64]] = Field( @@ -47,18 +53,18 @@ class OutputSchema(BaseModel): ) -@eqx.filter_jit -def apply_jit(inputs: dict) -> dict: +def evaluate(inputs: dict) -> dict: + """Core differentiable computation — pure torch operations.""" u = inputs["u"] nu = inputs["nu"] dt = inputs["dt"] # Spatial derivatives via central differences - dudx = jnp.zeros_like(u) - dudx = dudx.at[1:-1].set((u[2:] - u[:-2]) / (2 * DX)) + dudx = torch.zeros_like(u) + dudx[1:-1] = (u[2:] - u[:-2]) / (2 * DX) - d2udx2 = jnp.zeros_like(u) - d2udx2 = d2udx2.at[1:-1].set((u[2:] - 2 * u[1:-1] + u[:-2]) / (DX**2)) + d2udx2 = torch.zeros_like(u) + d2udx2[1:-1] = (u[2:] - 2 * u[1:-1] + u[:-2]) / (DX**2) # Burgers' equation: du/dt = -u * du/dx + nu * d²u/dx² dudt = -u * dudx + nu * d2udx2 @@ -67,61 +73,38 @@ def apply_jit(inputs: dict) -> dict: u_next = u + dt * dudt # Enforce boundary conditions (Dirichlet: hold boundary values) - u_next = u_next.at[0].set(u[0]) - u_next = u_next.at[-1].set(u[-1]) + u_next = torch.cat([u[:1], u_next[1:-1], u[-1:]]) return {"u_next": u_next} def apply(inputs: InputSchema) -> OutputSchema: - return apply_jit(inputs.model_dump()) + tensor_inputs = tree_map(to_tensor, inputs.model_dump()) + return evaluate(tensor_inputs) def abstract_eval(abstract_inputs: Any) -> Any: return {"u_next": {"shape": [N], "dtype": "float64"}} -@eqx.filter_jit -def jvp_jit( - inputs: dict, - jvp_inputs: tuple[str], - jvp_outputs: tuple[str], - tangent_vector: dict, -) -> Any: - filtered_apply = filter_func(apply_jit, inputs, jvp_outputs) - return jax.jvp( - filtered_apply, - [flatten_with_paths(inputs, include_paths=jvp_inputs)], - [tangent_vector], - )[1] - - def jacobian_vector_product( inputs: InputSchema, jvp_inputs: set[str], jvp_outputs: set[str], tangent_vector: dict[str, Any], -) -> Any: - return jvp_jit( - inputs.model_dump(), - tuple(jvp_inputs), - tuple(jvp_outputs), - tangent_vector, - ) +): + jvp_inputs = tuple(jvp_inputs) + tangent_vector = {key: tangent_vector[key] for key in jvp_inputs} + tensor_inputs = tree_map(to_tensor, inputs.model_dump()) + pos_tangent = tree_map(to_tensor, tangent_vector).values() + pos_inputs = flatten_with_paths(tensor_inputs, jvp_inputs).values() -@eqx.filter_jit -def vjp_jit( - inputs: dict, - vjp_inputs: tuple[str], - vjp_outputs: tuple[str], - cotangent_vector: dict, -) -> Any: - filtered_apply = filter_func(apply_jit, inputs, vjp_outputs) - _, vjp_func = jax.vjp( - filtered_apply, flatten_with_paths(inputs, include_paths=vjp_inputs) + filtered_pos_eval = filter_func( + evaluate, tensor_inputs, jvp_outputs, input_paths=jvp_inputs ) - return vjp_func(cotangent_vector)[0] + + return torch.func.jvp(filtered_pos_eval, tuple(pos_inputs), tuple(pos_tangent))[1] def vector_jacobian_product( @@ -129,30 +112,46 @@ def vector_jacobian_product( vjp_inputs: set[str], vjp_outputs: set[str], cotangent_vector: dict[str, Any], -) -> Any: - return vjp_jit( - inputs.model_dump(), - tuple(vjp_inputs), - tuple(vjp_outputs), - cotangent_vector, - ) +): + vjp_inputs = tuple(vjp_inputs) + cotangent_vector = {key: cotangent_vector[key] for key in vjp_outputs} + tensor_inputs = tree_map(to_tensor, inputs.model_dump()) + tensor_cotangent = tree_map(to_tensor, cotangent_vector) + pos_inputs = flatten_with_paths(tensor_inputs, vjp_inputs).values() -@eqx.filter_jit -def jac_jit( - inputs: dict, - jac_inputs: tuple[str], - jac_outputs: tuple[str], -) -> Any: - filtered_apply = filter_func(apply_jit, inputs, jac_outputs) - return jax.jacrev(filtered_apply)( - flatten_with_paths(inputs, include_paths=jac_inputs) + filtered_pos_func = filter_func( + evaluate, tensor_inputs, vjp_outputs, input_paths=vjp_inputs ) + _, vjp_func = torch.func.vjp(filtered_pos_func, *pos_inputs) + vjp_vals = vjp_func(tensor_cotangent) + return dict(zip(vjp_inputs, vjp_vals, strict=True)) + def jacobian( inputs: InputSchema, jac_inputs: set[str], jac_outputs: set[str], -) -> Any: - return jac_jit(inputs.model_dump(), tuple(jac_inputs), tuple(jac_outputs)) +): + jac_inputs = tuple(jac_inputs) + tensor_inputs = tree_map(to_tensor, inputs.model_dump()) + pos_inputs = flatten_with_paths(tensor_inputs, jac_inputs).values() + + filtered_pos_eval = filter_func( + evaluate, tensor_inputs, jac_outputs, input_paths=jac_inputs + ) + + def filtered_pos_eval_flat(*args): + res = filtered_pos_eval(*args) + return tuple(res[k] for k in jac_outputs) + + jac = torch.autograd.functional.jacobian(filtered_pos_eval_flat, tuple(pos_inputs)) + + jac_dict = {} + for dy, dys in zip(jac_outputs, jac, strict=True): + jac_dict[dy] = {} + for dx, dxs in zip(jac_inputs, dys, strict=True): + jac_dict[dy][dx] = dxs + + return jac_dict diff --git a/demo/learned-closure/burgers_solver/tesseract_config.yaml b/demo/learned-closure/burgers_solver/tesseract_config.yaml index d4cad068f..e7167d1c4 100644 --- a/demo/learned-closure/burgers_solver/tesseract_config.yaml +++ b/demo/learned-closure/burgers_solver/tesseract_config.yaml @@ -1,6 +1,6 @@ name: "burgers-solver" version: "0.1.0" -description: "1D Burgers equation solver with pluggable neural viscosity closure" +description: "1D Burgers equation solver with pluggable neural viscosity closure (PyTorch)" build_config: target_platform: "native" diff --git a/demo/learned-closure/burgers_solver/tesseract_requirements.txt b/demo/learned-closure/burgers_solver/tesseract_requirements.txt index bfb32e0f7..f6d69b182 100644 --- a/demo/learned-closure/burgers_solver/tesseract_requirements.txt +++ b/demo/learned-closure/burgers_solver/tesseract_requirements.txt @@ -1,3 +1,2 @@ -jax[cpu]==0.5.2 -equinox +torch tesseract-core diff --git a/demo/learned-closure/demo.ipynb b/demo/learned-closure/demo.ipynb index 7166967f8..74789e1f9 100644 --- a/demo/learned-closure/demo.ipynb +++ b/demo/learned-closure/demo.ipynb @@ -2,14 +2,44 @@ "cells": [ { "cell_type": "markdown", - "id": "9w0p8edklyb", "metadata": {}, - "source": "# Learned Closure: Training a Neural Viscosity Model Through a PDE Solver\n\nThis demo trains a neural network closure **end-to-end through a PDE solver**, with gradients flowing through both the solver and the network during training.\n\n**The setup**: a 1D Burgers' equation solver where the viscosity model — normally a hand-tuned constant — is replaced by a small neural network. The closure is called at every timestep to predict the current viscosity field from the current flow state, and we train the network by differentiating through the entire time-stepping loop.\n\n**The key result**: the learned closure recovers the true (unknown) viscosity profile from solution data alone, and produces better predictions than either the pure physics model (wrong constant viscosity) or a pure ML model (no physics structure).\n\n## Architecture: two Tesseracts, composed in an outer loop\n\nThe demo uses two independent Tesseracts:\n\n- **`burgers_solver`**: a single-timestep Burgers' equation solver. Takes the current velocity field `u` and a viscosity field `nu`, returns the velocity field after one explicit Euler step. This is a pure physics component with a clean interface: `(u, nu, dt) → u_next`.\n- **`neural_viscosity`**: a small MLP that maps local flow features $(u, \\partial u/\\partial x, x)$ to a viscosity field $\\nu$. Exposes standard gradient endpoints (VJP, JVP, Jacobian).\n\nThe outer time-stepping loop lives in the training script and calls both Tesseracts via `apply_tesseract` at every timestep:\n\n```\nfor each timestep:\n nu_field = apply_tesseract(closure, {u, dudx, x, weights})\n u_next = apply_tesseract(solver, {u, nu_field, dt})\n u = u_next\n```\n\nBecause `apply_tesseract` registers each call as a JAX custom primitive, `jax.grad` through the loop automatically dispatches VJP calls back through both Tesseracts. Gradients flow end-to-end with no manual plumbing.\n\n### Why this architecture matters\n\nThe solver's interface — `(u, nu_field, dt) → u_next` — is **not specific to JAX**. A Fortran solver with a hand-written adjoint, or one differentiated by [Enzyme](https://enzyme.mit.edu/) at the LLVM IR level (see the `enzyme_thermal_2d` demo), could implement the same contract. The outer loop and training code would be identical. This is the core Tesseract value proposition: **the closure researcher doesn't need to know how the solver computes its gradients**." + "source": [ + "# Learned Closure: Training a Neural Viscosity Model Through a PDE Solver (PyTorch)\n", + "\n", + "This demo trains a neural network closure **end-to-end through a PDE solver**, with gradients flowing through both the solver and the network during training.\n", + "\n", + "**The setup**: a 1D Burgers' equation solver where the viscosity model — normally a hand-tuned constant — is replaced by a small neural network. The closure is called at every timestep to predict the current viscosity field from the current flow state, and we train the network by differentiating through the entire time-stepping loop.\n", + "\n", + "**The key result**: the learned closure recovers the true (unknown) viscosity profile from solution data alone, and produces better predictions than either the pure physics model (wrong constant viscosity) or a pure ML model (no physics structure).\n", + "\n", + "**This version** uses **PyTorch** and **tesseract-torch** instead of JAX and tesseract-jax. The Tesseract API files use `torch.func` for autodiff, and the outer training loop uses `torch.autograd` for end-to-end gradient computation.\n", + "\n", + "## Architecture: two Tesseracts, composed in an outer loop\n", + "\n", + "The demo uses two independent Tesseracts:\n", + "\n", + "- **`burgers_solver`**: a single-timestep Burgers' equation solver. Takes the current velocity field `u` and a viscosity field `nu`, returns the velocity field after one explicit Euler step. This is a pure physics component with a clean interface: `(u, nu, dt) → u_next`.\n", + "- **`neural_viscosity`**: a small MLP that maps local flow features $(u, \\partial u/\\partial x, x)$ to a viscosity field $\\nu$. Exposes standard gradient endpoints (VJP, JVP, Jacobian).\n", + "\n", + "The outer time-stepping loop lives in the training script and calls both Tesseracts via `apply_tesseract` at every timestep:\n", + "\n", + "```\n", + "for each timestep:\n", + " nu_field = apply_tesseract(closure, {u, dudx, x, weights})\n", + " u_next = apply_tesseract(solver, {u, nu_field, dt})\n", + " u = u_next\n", + "```\n", + "\n", + "Because `apply_tesseract` registers each call as a PyTorch autograd custom function, `torch.autograd.grad` through the loop automatically dispatches VJP calls back through both Tesseracts. Gradients flow end-to-end with no manual plumbing.\n", + "\n", + "### Why this architecture matters\n", + "\n", + "The solver's interface — `(u, nu_field, dt) → u_next` — is **not specific to PyTorch**. A Fortran solver with a hand-written adjoint, or one differentiated by [Enzyme](https://enzyme.mit.edu/) at the LLVM IR level (see the `enzyme_thermal_2d` demo), could implement the same contract. The outer loop and training code would be identical. This is the core Tesseract value proposition: **the closure researcher doesn't need to know how the solver computes its gradients**." + ] }, { "cell_type": "code", "execution_count": null, - "id": "p0o9kf3si4", "metadata": {}, "outputs": [], "source": [ @@ -19,21 +49,18 @@ "sys.path.insert(0, \"burgers_solver\")\n", "\n", "import burgers_solver.tesseract_api as solver_api\n", - "import jax\n", - "import jax.numpy as jnp\n", "import matplotlib.pyplot as plt\n", "import neural_viscosity.tesseract_api as closure_api\n", - "import optax\n", - "from tesseract_jax import apply_tesseract\n", + "import numpy as np\n", + "import torch\n", + "from tesseract_torch import apply_tesseract\n", "\n", "from tesseract_core import Tesseract\n", "\n", - "jax.config.update(\"jax_enable_x64\", True)\n", - "\n", "# Grid setup (must match the solver)\n", "N = 128\n", "DX = 1.0 / (N - 1)\n", - "X = jnp.linspace(0.0, 1.0, N)\n", + "X = torch.linspace(0.0, 1.0, N, dtype=torch.float64)\n", "\n", "# Load both Tesseracts (local dev mode — no Docker needed)\n", "closure_tess = Tesseract.from_tesseract_api(\"neural_viscosity/tesseract_api.py\")\n", @@ -45,68 +72,76 @@ }, { "cell_type": "markdown", - "id": "t2spi8jeftd", "metadata": {}, - "source": "## 1. The ground truth: Burgers' equation with spatially-varying viscosity\n\nWe generate training data from a Burgers' equation with a **known but non-trivial** viscosity profile:\n\n$$\\nu_{\\text{true}}(x) = \\nu_0 \\left(1 + A \\sin(\\pi x)\\right)$$\n\nThis represents a spatially-varying material property — analogous to a turbulence closure, constitutive law, or sub-grid model that varies across the domain. The neural closure's job is to recover this profile from solution data alone." + "source": [ + "## 1. The ground truth: Burgers' equation with spatially-varying viscosity\n", + "\n", + "We generate training data from a Burgers' equation with a **known but non-trivial** viscosity profile:\n", + "\n", + "$$\\nu_{\\text{true}}(x) = \\nu_0 \\left(1 + A \\sin(\\pi x)\\right)$$\n", + "\n", + "This represents a spatially-varying material property — analogous to a turbulence closure, constitutive law, or sub-grid model that varies across the domain. The neural closure's job is to recover this profile from solution data alone." + ] }, { "cell_type": "code", "execution_count": null, - "id": "kyq2477ckq", "metadata": {}, "outputs": [], "source": [ "# True viscosity profile\n", "NU_0 = 0.02\n", "A = 0.8\n", - "nu_true = NU_0 * (1.0 + A * jnp.sin(jnp.pi * X))\n", + "nu_true = NU_0 * (1.0 + A * torch.sin(np.pi * X))\n", "\n", "\n", "# Reference solver: Burgers' equation with known viscosity (no neural network).\n", "# Uses the same single-step stencil as the solver Tesseract.\n", "def burgers_reference(u0, nu_field, dt, n_steps):\n", " \"\"\"Solve Burgers' equation with a prescribed viscosity field.\"\"\"\n", - "\n", - " def step(u, _):\n", - " out = solver_api.apply_jit({\"u\": u, \"nu\": nu_field, \"dt\": dt})\n", - " return out[\"u_next\"], u\n", - "\n", - " u_final, u_history = jax.lax.scan(step, u0, None, length=n_steps)\n", - " return u_final, u_history\n", + " u = u0.clone()\n", + " history = []\n", + " for _ in range(n_steps):\n", + " out = solver_api.evaluate(\n", + " {\"u\": u, \"nu\": nu_field, \"dt\": torch.tensor(dt, dtype=torch.float64)}\n", + " )\n", + " history.append(u)\n", + " u = out[\"u_next\"]\n", + " return u, history\n", "\n", "\n", "# Generate training data: multiple initial conditions\n", "DT = 5e-5\n", "N_STEPS = 200\n", - "key = jax.random.PRNGKey(0)\n", "\n", "\n", - "def make_ic(key):\n", + "def make_ic(seed):\n", " \"\"\"Random smooth initial condition: sum of low-frequency sinusoids.\"\"\"\n", - " k1, k2, k3 = jax.random.split(key, 3)\n", - " a1 = 0.5 + 0.5 * jax.random.uniform(k1)\n", - " a2 = 0.3 * jax.random.uniform(k2)\n", - " phase = jax.random.uniform(k3) * jnp.pi\n", - " u0 = a1 * jnp.sin(2 * jnp.pi * X + phase) + a2 * jnp.sin(4 * jnp.pi * X)\n", + " rng = torch.Generator().manual_seed(seed)\n", + " a1 = 0.5 + 0.5 * torch.rand(1, generator=rng).item()\n", + " a2 = 0.3 * torch.rand(1, generator=rng).item()\n", + " phase = torch.rand(1, generator=rng).item() * np.pi\n", + " u0 = a1 * torch.sin(2 * np.pi * X + phase) + a2 * torch.sin(4 * np.pi * X)\n", " # Zero boundary conditions\n", - " u0 = u0.at[0].set(0.0).at[-1].set(0.0)\n", + " u0[0] = 0.0\n", + " u0[-1] = 0.0\n", " return u0\n", "\n", "\n", "N_TRAIN = 8\n", "N_TEST = 4\n", - "keys = jax.random.split(key, N_TRAIN + N_TEST)\n", "\n", - "train_ics = jnp.stack([make_ic(keys[i]) for i in range(N_TRAIN)])\n", - "test_ics = jnp.stack([make_ic(keys[N_TRAIN + i]) for i in range(N_TEST)])\n", + "train_ics = torch.stack([make_ic(i) for i in range(N_TRAIN)])\n", + "test_ics = torch.stack([make_ic(N_TRAIN + i) for i in range(N_TEST)])\n", "\n", "# Generate ground-truth solutions\n", - "train_targets = jnp.stack(\n", - " [burgers_reference(ic, nu_true, DT, N_STEPS)[0] for ic in train_ics]\n", - ")\n", - "test_targets = jnp.stack(\n", - " [burgers_reference(ic, nu_true, DT, N_STEPS)[0] for ic in test_ics]\n", - ")\n", + "with torch.no_grad():\n", + " train_targets = torch.stack(\n", + " [burgers_reference(ic, nu_true, DT, N_STEPS)[0] for ic in train_ics]\n", + " )\n", + " test_targets = torch.stack(\n", + " [burgers_reference(ic, nu_true, DT, N_STEPS)[0] for ic in test_ics]\n", + " )\n", "\n", "print(f\"Training set: {N_TRAIN} initial conditions -> solutions\")\n", "print(f\"Test set: {N_TEST} initial conditions -> solutions\")\n", @@ -114,14 +149,16 @@ "\n", "# Visualize one example\n", "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", - "axes[0].plot(X, train_ics[0], label=\"Initial condition\")\n", - "axes[0].plot(X, train_targets[0], label=f\"Solution at t={DT * N_STEPS:.3f}\")\n", + "axes[0].plot(X.numpy(), train_ics[0].numpy(), label=\"Initial condition\")\n", + "axes[0].plot(\n", + " X.numpy(), train_targets[0].numpy(), label=f\"Solution at t={DT * N_STEPS:.3f}\"\n", + ")\n", "axes[0].set_xlabel(\"x\")\n", "axes[0].set_ylabel(\"u\")\n", "axes[0].legend()\n", "axes[0].set_title(\"Example: Burgers' equation with true viscosity\")\n", "\n", - "axes[1].plot(X, nu_true, \"k-\", linewidth=2)\n", + "axes[1].plot(X.numpy(), nu_true.numpy(), \"k-\", linewidth=2)\n", "axes[1].set_xlabel(\"x\")\n", "axes[1].set_ylabel(r\"$\\nu(x)$\")\n", "axes[1].set_title(\"True viscosity profile (to be learned)\")\n", @@ -133,37 +170,48 @@ }, { "cell_type": "markdown", - "id": "a6moibwyik9", "metadata": {}, - "source": "## 2. Initialize the neural closure\n\nThe closure is a small MLP with 2 hidden layers of 32 units each. Its weights are passed as explicit inputs to the solver so that `jax.grad` can differentiate through the entire pipeline." + "source": [ + "## 2. Initialize the neural closure\n", + "\n", + "The closure is a small MLP with 2 hidden layers of 32 units each. Its weights are passed as explicit inputs to the solver so that `torch.autograd` can differentiate through the entire pipeline." + ] }, { "cell_type": "code", "execution_count": null, - "id": "rvuazdiorw", "metadata": {}, "outputs": [], "source": [ - "def init_params(key):\n", + "def init_params(seed):\n", " \"\"\"Initialize neural closure weights with Xavier initialization.\"\"\"\n", - " keys = jax.random.split(key, 3)\n", + " rng = torch.Generator().manual_seed(seed)\n", " return {\n", - " \"w1\": jax.random.normal(keys[0], (3, 32)) * jnp.sqrt(2.0 / 3),\n", - " \"b1\": jnp.zeros(32),\n", - " \"w2\": jax.random.normal(keys[1], (32, 32)) * jnp.sqrt(2.0 / 32),\n", - " \"b2\": jnp.zeros(32),\n", - " \"w3\": jax.random.normal(keys[2], (32, 1)) * jnp.sqrt(2.0 / 32),\n", - " \"b3\": jnp.zeros(1),\n", + " \"w1\": torch.randn(3, 32, dtype=torch.float64, generator=rng) * np.sqrt(2.0 / 3),\n", + " \"b1\": torch.zeros(32, dtype=torch.float64),\n", + " \"w2\": torch.randn(32, 32, dtype=torch.float64, generator=rng)\n", + " * np.sqrt(2.0 / 32),\n", + " \"b2\": torch.zeros(32, dtype=torch.float64),\n", + " \"w3\": torch.randn(32, 1, dtype=torch.float64, generator=rng)\n", + " * np.sqrt(2.0 / 32),\n", + " \"b3\": torch.zeros(1, dtype=torch.float64),\n", " }\n", "\n", "\n", - "params = init_params(jax.random.PRNGKey(1))\n", - "print(f\"Total parameters: {sum(p.size for p in jax.tree.leaves(params))}\")\n", + "params = init_params(seed=1)\n", + "n_params = sum(p.numel() for p in params.values())\n", + "print(f\"Total parameters: {n_params}\")\n", "\n", "# Verify the closure produces sensible output\n", - "test_nu = closure_api.apply_jit(\n", - " {\"u\": train_ics[0], \"dudx\": jnp.gradient(train_ics[0], DX), \"x\": X, **params}\n", - ")[\"nu\"]\n", + "with torch.no_grad():\n", + " test_nu = closure_api.apply(\n", + " closure_api.InputSchema(\n", + " u=train_ics[0],\n", + " dudx=torch.gradient(train_ics[0], spacing=(DX,))[0],\n", + " x=X,\n", + " **params,\n", + " )\n", + " )[\"nu\"]\n", "print(\n", " f\"Initial viscosity range: [{float(test_nu.min()):.4f}, {float(test_nu.max()):.4f}]\"\n", ")" @@ -171,14 +219,30 @@ }, { "cell_type": "markdown", - "id": "81k5zamfwmo", "metadata": {}, - "source": "## 3. End-to-end training: differentiating through the solver\n\nThe training loss is:\n\n$$\\mathcal{L}(\\theta) = \\frac{1}{M} \\sum_{i=1}^{M} \\left\\| u_{\\text{solver}}(u_0^{(i)}; \\nu_\\theta) - u_{\\text{data}}^{(i)} \\right\\|^2$$\n\nwhere $\\nu_\\theta$ is the neural viscosity closure with parameters $\\theta$, and $u_{\\text{solver}}$ runs the full Burgers' equation with $\\nu_\\theta$ called at every timestep.\n\nThe outer loop calls both Tesseracts via `apply_tesseract`:\n\n```python\nfor each timestep:\n nu = apply_tesseract(closure_tess, {u, dudx, x, weights})[\"nu\"]\n u = apply_tesseract(solver_tess, {u, nu, dt})[\"u_next\"]\n```\n\n`jax.grad` differentiates through the entire loop — through every timestep, through both `apply_tesseract` calls — automatically. Each `apply_tesseract` dispatches VJP calls back to the respective Tesseract during the backward pass." + "source": [ + "## 3. End-to-end training: differentiating through the solver\n", + "\n", + "The training loss is:\n", + "\n", + "$$\\mathcal{L}(\\theta) = \\frac{1}{M} \\sum_{i=1}^{M} \\left\\| u_{\\text{solver}}(u_0^{(i)}; \\nu_\\theta) - u_{\\text{data}}^{(i)} \\right\\|^2$$\n", + "\n", + "where $\\nu_\\theta$ is the neural viscosity closure with parameters $\\theta$, and $u_{\\text{solver}}$ runs the full Burgers' equation with $\\nu_\\theta$ called at every timestep.\n", + "\n", + "The outer loop calls both Tesseracts via `apply_tesseract`:\n", + "\n", + "```python\n", + "for each timestep:\n", + " nu = apply_tesseract(closure_tess, {u, dudx, x, weights})[\"nu\"]\n", + " u = apply_tesseract(solver_tess, {u, nu, dt})[\"u_next\"]\n", + "```\n", + "\n", + "`torch.autograd.grad` differentiates through the entire loop — through every timestep, through both `apply_tesseract` calls — automatically. Each `apply_tesseract` dispatches VJP calls back to the respective Tesseract during the backward pass." + ] }, { "cell_type": "code", "execution_count": null, - "id": "hcpebqrhika", "metadata": {}, "outputs": [], "source": [ @@ -194,8 +258,8 @@ " \"\"\"\n", " u = u0\n", " for _step in range(n_steps):\n", - " dudx = jnp.zeros_like(u)\n", - " dudx = dudx.at[1:-1].set((u[2:] - u[:-2]) / (2 * DX))\n", + " dudx = torch.zeros_like(u)\n", + " dudx[1:-1] = (u[2:] - u[:-2]) / (2 * DX)\n", "\n", " # Closure: predict viscosity from current flow state\n", " closure_out = apply_tesseract(\n", @@ -212,70 +276,91 @@ "def loss_single(params, u0, target):\n", " \"\"\"Loss for a single initial condition: MSE between solver output and data.\"\"\"\n", " u_final = solve_with_closure(u0, params, DT, N_STEPS)\n", - " return jnp.mean((u_final - target) ** 2)\n", + " return torch.mean((u_final - target) ** 2)\n", "\n", "\n", "def loss_batch(params, ics, targets):\n", " \"\"\"Mean loss over a batch of initial conditions.\"\"\"\n", - " losses = jax.vmap(lambda u0, tgt: loss_single(params, u0, tgt))(ics, targets)\n", - " return jnp.mean(losses)\n", - "\n", + " losses = torch.stack(\n", + " [loss_single(params, ics[i], targets[i]) for i in range(ics.shape[0])]\n", + " )\n", + " return torch.mean(losses)\n", "\n", - "grad_fn = jax.grad(loss_batch)\n", "\n", "# Verify gradients work\n", "print(\"Testing forward pass + gradient...\")\n", - "l0 = loss_batch(params, train_ics, train_targets)\n", - "g0 = grad_fn(params, train_ics, train_targets)\n", - "print(f\" Initial loss: {float(l0):.6e}\")\n", - "print(\n", - " f\" Gradient norm: {float(jnp.sqrt(sum(jnp.sum(g**2) for g in jax.tree.leaves(g0)))):.6e}\"\n", + "\n", + "# Enable gradients on parameters\n", + "grad_params = {k: v.clone().requires_grad_(True) for k, v in params.items()}\n", + "\n", + "l0 = loss_batch(grad_params, train_ics, train_targets)\n", + "l0.backward()\n", + "\n", + "grad_norm = torch.sqrt(\n", + " sum(p.grad.pow(2).sum() for p in grad_params.values() if p.grad is not None)\n", ")\n", + "print(f\" Initial loss: {float(l0):.6e}\")\n", + "print(f\" Gradient norm: {float(grad_norm):.6e}\")\n", "print(\" Gradients flow: loss -> solver VJP -> closure VJP -> network weights.\")" ] }, { "cell_type": "markdown", - "id": "n9twlbyke1c", "metadata": {}, - "source": "### Gradient validation against finite differences\n\nCorrectness proof: the AD gradients match finite differences to high precision." + "source": [ + "### Gradient validation against finite differences\n", + "\n", + "Correctness proof: the AD gradients match finite differences to high precision." + ] }, { "cell_type": "code", "execution_count": null, - "id": "x9an2u42wj", "metadata": {}, "outputs": [], "source": [ "# Finite difference check on a few weight elements\n", "eps = 1e-5\n", "print(f\"{'Parameter':>10s} {'Index':>8s} {'AD':>14s} {'FD':>14s} {'Rel. Error':>12s}\")\n", + "\n", + "# Use a subset for speed\n", + "ics_sub = train_ics[:2]\n", + "tgt_sub = train_targets[:2]\n", + "\n", "for pname in [\"w1\", \"w2\", \"w3\"]:\n", " idx = (0, 0)\n", "\n", - " def fd_loss(val, _pname=pname, _idx=idx):\n", - " p = {**params, _pname: params[_pname].at[_idx].set(val)}\n", - " return loss_batch(p, train_ics[:2], train_targets[:2])\n", - "\n", - " v0 = params[pname][idx]\n", - " fd = (fd_loss(v0 + eps) - fd_loss(v0 - eps)) / (2 * eps)\n", - " ad = jax.grad(loss_batch)({**params}, train_ics[:2], train_targets[:2])[pname][idx]\n", - " rel_err = abs(float(ad) - float(fd)) / (abs(float(fd)) + 1e-30)\n", - " print(\n", - " f\"{pname:>10s} {idx!s:>8s} {float(ad):14.6e} {float(fd):14.6e} {rel_err:12.2e}\"\n", - " )" + " # AD gradient\n", + " gp = {k: v.clone().requires_grad_(True) for k, v in params.items()}\n", + " loss_val = loss_batch(gp, ics_sub, tgt_sub)\n", + " loss_val.backward()\n", + " ad = float(gp[pname].grad[idx])\n", + "\n", + " # Finite difference\n", + " with torch.no_grad():\n", + " p_plus = {k: v.clone() for k, v in params.items()}\n", + " p_plus[pname][idx] += eps\n", + " l_plus = float(loss_batch(p_plus, ics_sub, tgt_sub))\n", + "\n", + " p_minus = {k: v.clone() for k, v in params.items()}\n", + " p_minus[pname][idx] -= eps\n", + " l_minus = float(loss_batch(p_minus, ics_sub, tgt_sub))\n", + "\n", + " fd = (l_plus - l_minus) / (2 * eps)\n", + " rel_err = abs(ad - fd) / (abs(fd) + 1e-30)\n", + " print(f\"{pname:>10s} {idx!s:>8s} {ad:14.6e} {fd:14.6e} {rel_err:12.2e}\")" ] }, { "cell_type": "markdown", - "id": "k28hfvj2mk", "metadata": {}, - "source": "### Training loop" + "source": [ + "### Training loop" + ] }, { "cell_type": "code", "execution_count": null, - "id": "dc1zxbtpmla", "metadata": {}, "outputs": [], "source": [ @@ -283,36 +368,35 @@ "LR = 3e-3\n", "\n", "# Re-initialize for a clean training run\n", - "params = init_params(jax.random.PRNGKey(1))\n", - "optimizer = optax.adam(LR)\n", - "opt_state = optimizer.init(params)\n", + "params = init_params(seed=1)\n", + "# Wrap as nn.ParameterDict-style: plain tensors with requires_grad\n", + "train_params = {k: v.clone().requires_grad_(True) for k, v in params.items()}\n", + "\n", + "optimizer = torch.optim.Adam(train_params.values(), lr=LR)\n", "\n", "train_losses = []\n", "test_losses = []\n", "\n", - "\n", - "@jax.jit\n", - "def train_step(params, opt_state, ics, targets):\n", - " loss, grads = jax.value_and_grad(loss_batch)(params, ics, targets)\n", - " updates, opt_state_new = optimizer.update(grads, opt_state, params)\n", - " params_new = optax.apply_updates(params, updates)\n", - " return params_new, opt_state_new, loss\n", - "\n", - "\n", "print(\"Training neural closure through the solver...\")\n", "for epoch in range(N_EPOCHS):\n", - " params, opt_state, train_loss = train_step(\n", - " params, opt_state, train_ics, train_targets\n", - " )\n", + " optimizer.zero_grad()\n", + " train_loss = loss_batch(train_params, train_ics, train_targets)\n", + " train_loss.backward()\n", + " optimizer.step()\n", + "\n", " train_losses.append(float(train_loss))\n", "\n", " if epoch % 50 == 0 or epoch == N_EPOCHS - 1:\n", - " test_loss = float(loss_batch(params, test_ics, test_targets))\n", + " with torch.no_grad():\n", + " test_loss = float(loss_batch(train_params, test_ics, test_targets))\n", " test_losses.append((epoch, test_loss))\n", " print(\n", " f\" Epoch {epoch:4d}: train loss = {train_losses[-1]:.4e}, test loss = {test_loss:.4e}\"\n", " )\n", "\n", + "# Copy trained params back (detached)\n", + "params = {k: v.detach().clone() for k, v in train_params.items()}\n", + "\n", "print(f\"\\nFinal train loss: {train_losses[-1]:.4e}\")\n", "print(f\"Final test loss: {test_losses[-1][1]:.4e}\")" ] @@ -320,7 +404,6 @@ { "cell_type": "code", "execution_count": null, - "id": "g7fwvt08nj9", "metadata": {}, "outputs": [], "source": [ @@ -338,21 +421,28 @@ "# Compare learned viscosity to true viscosity.\n", "# Evaluate the closure Tesseract at several representative flow states.\n", "nu_samples = []\n", - "for ic in train_ics:\n", - " dudx = jnp.zeros_like(ic)\n", - " dudx = dudx.at[1:-1].set((ic[2:] - ic[:-2]) / (2 * DX))\n", - " nu_i = apply_tesseract(closure_tess, {\"u\": ic, \"dudx\": dudx, \"x\": X, **params})[\n", - " \"nu\"\n", - " ]\n", - " nu_samples.append(nu_i)\n", - "nu_samples = jnp.stack(nu_samples)\n", - "nu_mean = jnp.mean(nu_samples, axis=0)\n", - "nu_std = jnp.std(nu_samples, axis=0)\n", - "\n", - "axes[1].plot(X, nu_true, \"k-\", linewidth=2, label=\"True viscosity\")\n", - "axes[1].plot(X, nu_mean, \"r--\", linewidth=2, label=\"Learned (mean over ICs)\")\n", + "with torch.no_grad():\n", + " for ic in train_ics:\n", + " dudx = torch.zeros_like(ic)\n", + " dudx[1:-1] = (ic[2:] - ic[:-2]) / (2 * DX)\n", + " nu_i = apply_tesseract(closure_tess, {\"u\": ic, \"dudx\": dudx, \"x\": X, **params})[\n", + " \"nu\"\n", + " ]\n", + " nu_samples.append(nu_i)\n", + "nu_samples = torch.stack(nu_samples)\n", + "nu_mean = nu_samples.mean(dim=0)\n", + "nu_std = nu_samples.std(dim=0)\n", + "\n", + "x_np = X.numpy()\n", + "axes[1].plot(x_np, nu_true.numpy(), \"k-\", linewidth=2, label=\"True viscosity\")\n", + "axes[1].plot(x_np, nu_mean.numpy(), \"r--\", linewidth=2, label=\"Learned (mean over ICs)\")\n", "axes[1].fill_between(\n", - " X, nu_mean - nu_std, nu_mean + nu_std, color=\"r\", alpha=0.15, label=\"Learned (std)\"\n", + " x_np,\n", + " (nu_mean - nu_std).numpy(),\n", + " (nu_mean + nu_std).numpy(),\n", + " color=\"r\",\n", + " alpha=0.15,\n", + " label=\"Learned (std)\",\n", ")\n", "axes[1].set_xlabel(\"x\")\n", "axes[1].set_ylabel(r\"$\\nu$\")\n", @@ -367,87 +457,100 @@ }, { "cell_type": "markdown", - "id": "4vl39ef67ps", "metadata": {}, - "source": "## 4. Baselines: why the hybrid model wins\n\nWe compare three approaches:\n\n| Model | Description |\n|---|---|\n| **Constant viscosity** | Burgers' solver with $\\nu = \\nu_0$ (the standard \"wrong\" closure) |\n| **Direct ML** | MLP trained to map $u_0 \\to u_\\text{final}$ directly, no physics |\n| **Learned closure** | Neural $\\nu(u, \\partial u/\\partial x, x)$ trained through the solver (this demo) |" + "source": [ + "## 4. Baselines: why the hybrid model wins\n", + "\n", + "We compare three approaches:\n", + "\n", + "| Model | Description |\n", + "|---|---|\n", + "| **Constant viscosity** | Burgers' solver with $\\nu = \\nu_0$ (the standard \"wrong\" closure) |\n", + "| **Direct ML** | MLP trained to map $u_0 \\to u_\\text{final}$ directly, no physics |\n", + "| **Learned closure** | Neural $\\nu(u, \\partial u/\\partial x, x)$ trained through the solver (this demo) |" + ] }, { "cell_type": "code", "execution_count": null, - "id": "r6nx7wzmax", "metadata": {}, "outputs": [], "source": [ "# --- Baseline 1: Constant viscosity ---\n", - "nu_const = NU_0 * jnp.ones(N) # Wrong: uniform instead of spatially varying\n", - "const_preds_test = jnp.stack(\n", - " [burgers_reference(ic, nu_const, DT, N_STEPS)[0] for ic in test_ics]\n", - ")\n", - "const_mse = float(jnp.mean((const_preds_test - test_targets) ** 2))\n", + "nu_const = NU_0 * torch.ones(\n", + " N, dtype=torch.float64\n", + ") # Wrong: uniform instead of spatially varying\n", + "with torch.no_grad():\n", + " const_preds_test = torch.stack(\n", + " [burgers_reference(ic, nu_const, DT, N_STEPS)[0] for ic in test_ics]\n", + " )\n", + "const_mse = float(torch.mean((const_preds_test - test_targets) ** 2))\n", "print(f\"Constant viscosity test MSE: {const_mse:.4e}\")\n", "\n", "\n", "# --- Baseline 2: Direct ML (MLP mapping u0 -> u_final, no physics) ---\n", - "def init_direct_params(key):\n", + "def init_direct_params(seed):\n", " \"\"\"Larger MLP for direct prediction (more capacity since no physics inductive bias).\"\"\"\n", - " keys = jax.random.split(key, 4)\n", + " rng = torch.Generator().manual_seed(seed)\n", " return {\n", - " \"w1\": jax.random.normal(keys[0], (N, 128)) * jnp.sqrt(2.0 / N),\n", - " \"b1\": jnp.zeros(128),\n", - " \"w2\": jax.random.normal(keys[1], (128, 128)) * jnp.sqrt(2.0 / 128),\n", - " \"b2\": jnp.zeros(128),\n", - " \"w3\": jax.random.normal(keys[2], (128, 64)) * jnp.sqrt(2.0 / 128),\n", - " \"b3\": jnp.zeros(64),\n", - " \"w4\": jax.random.normal(keys[3], (64, N)) * jnp.sqrt(2.0 / 64),\n", - " \"b4\": jnp.zeros(N),\n", + " \"w1\": torch.randn(N, 128, dtype=torch.float64, generator=rng)\n", + " * np.sqrt(2.0 / N),\n", + " \"b1\": torch.zeros(128, dtype=torch.float64),\n", + " \"w2\": torch.randn(128, 128, dtype=torch.float64, generator=rng)\n", + " * np.sqrt(2.0 / 128),\n", + " \"b2\": torch.zeros(128, dtype=torch.float64),\n", + " \"w3\": torch.randn(128, 64, dtype=torch.float64, generator=rng)\n", + " * np.sqrt(2.0 / 128),\n", + " \"b3\": torch.zeros(64, dtype=torch.float64),\n", + " \"w4\": torch.randn(64, N, dtype=torch.float64, generator=rng)\n", + " * np.sqrt(2.0 / 64),\n", + " \"b4\": torch.zeros(N, dtype=torch.float64),\n", " }\n", "\n", "\n", "def direct_predict(params, u0):\n", " \"\"\"Pure ML: MLP maps u0 directly to u_final.\"\"\"\n", - " h = jnp.tanh(u0 @ params[\"w1\"] + params[\"b1\"])\n", - " h = jnp.tanh(h @ params[\"w2\"] + params[\"b2\"])\n", - " h = jnp.tanh(h @ params[\"w3\"] + params[\"b3\"])\n", + " h = torch.tanh(u0 @ params[\"w1\"] + params[\"b1\"])\n", + " h = torch.tanh(h @ params[\"w2\"] + params[\"b2\"])\n", + " h = torch.tanh(h @ params[\"w3\"] + params[\"b3\"])\n", " return h @ params[\"w4\"] + params[\"b4\"]\n", "\n", "\n", "def direct_loss(params, ics, targets):\n", - " preds = jax.vmap(lambda u0: direct_predict(params, u0))(ics)\n", - " return jnp.mean((preds - targets) ** 2)\n", + " preds = torch.stack([direct_predict(params, ics[i]) for i in range(ics.shape[0])])\n", + " return torch.mean((preds - targets) ** 2)\n", "\n", "\n", "# Train the direct ML baseline\n", - "direct_params = init_direct_params(jax.random.PRNGKey(2))\n", - "direct_opt = optax.adam(1e-3)\n", - "direct_opt_state = direct_opt.init(direct_params)\n", - "\n", - "\n", - "@jax.jit\n", - "def direct_train_step(params, opt_state, ics, targets):\n", - " loss, grads = jax.value_and_grad(direct_loss)(params, ics, targets)\n", - " updates, opt_state_new = direct_opt.update(grads, opt_state, params)\n", - " params_new = optax.apply_updates(params, updates)\n", - " return params_new, opt_state_new, loss\n", - "\n", + "direct_params = init_direct_params(seed=2)\n", + "direct_train_params = {\n", + " k: v.clone().requires_grad_(True) for k, v in direct_params.items()\n", + "}\n", + "direct_opt = torch.optim.Adam(direct_train_params.values(), lr=1e-3)\n", "\n", "print(\"\\nTraining direct ML baseline...\")\n", "direct_losses = []\n", "for epoch in range(500):\n", - " direct_params, direct_opt_state, dl = direct_train_step(\n", - " direct_params, direct_opt_state, train_ics, train_targets\n", - " )\n", + " direct_opt.zero_grad()\n", + " dl = direct_loss(direct_train_params, train_ics, train_targets)\n", + " dl.backward()\n", + " direct_opt.step()\n", " direct_losses.append(float(dl))\n", " if epoch % 100 == 0:\n", - " test_dl = float(direct_loss(direct_params, test_ics, test_targets))\n", + " with torch.no_grad():\n", + " test_dl = float(direct_loss(direct_train_params, test_ics, test_targets))\n", " print(\n", " f\" Epoch {epoch:4d}: train = {direct_losses[-1]:.4e}, test = {test_dl:.4e}\"\n", " )\n", "\n", - "direct_test_mse = float(direct_loss(direct_params, test_ics, test_targets))\n", + "direct_params = {k: v.detach().clone() for k, v in direct_train_params.items()}\n", + "with torch.no_grad():\n", + " direct_test_mse = float(direct_loss(direct_params, test_ics, test_targets))\n", "print(f\"\\nDirect ML test MSE: {direct_test_mse:.4e}\")\n", "\n", "# --- Learned closure (already trained above) ---\n", - "learned_test_mse = float(loss_batch(params, test_ics, test_targets))\n", + "with torch.no_grad():\n", + " learned_test_mse = float(loss_batch(params, test_ics, test_targets))\n", "print(f\"Learned closure test MSE: {learned_test_mse:.4e}\")\n", "\n", "print(f\"\\n{'Model':<25s} {'Test MSE':>12s}\")\n", @@ -459,43 +562,57 @@ }, { "cell_type": "markdown", - "id": "ziblnuj20e", "metadata": {}, - "source": "## 5. Solution comparison on test data\n\nThe key visual: for an unseen initial condition, compare the three models' predictions against the ground truth." + "source": [ + "## 5. Solution comparison on test data\n", + "\n", + "The key visual: for an unseen initial condition, compare the three models' predictions against the ground truth." + ] }, { "cell_type": "code", "execution_count": null, - "id": "trgeddr2cs", "metadata": {}, "outputs": [], "source": [ "fig, axes = plt.subplots(2, 2, figsize=(12, 9))\n", "\n", - "for idx in range(4):\n", - " ax = axes[idx // 2, idx % 2]\n", - " ic = test_ics[idx]\n", - " target = test_targets[idx]\n", - "\n", - " # Constant viscosity prediction\n", - " const_pred = burgers_reference(ic, nu_const, DT, N_STEPS)[0]\n", - "\n", - " # Direct ML prediction\n", - " direct_pred = direct_predict(direct_params, ic)\n", - "\n", - " # Learned closure prediction (outer loop calling both Tesseracts)\n", - " learned_pred = solve_with_closure(ic, params, DT, N_STEPS)\n", - "\n", - " ax.plot(X, target, \"k-\", linewidth=2, label=\"Ground truth\")\n", - " ax.plot(X, const_pred, \"b--\", linewidth=1.5, alpha=0.7, label=\"Constant viscosity\")\n", - " ax.plot(X, direct_pred, \"g:\", linewidth=1.5, alpha=0.7, label=\"Direct ML\")\n", - " ax.plot(X, learned_pred, \"r-\", linewidth=1.5, label=\"Learned closure\")\n", - " ax.set_xlabel(\"x\")\n", - " ax.set_ylabel(\"u\")\n", - " ax.set_title(f\"Test case {idx + 1}\")\n", - " ax.grid(True, alpha=0.3)\n", - " if idx == 0:\n", - " ax.legend(fontsize=9)\n", + "with torch.no_grad():\n", + " for idx in range(4):\n", + " ax = axes[idx // 2, idx % 2]\n", + " ic = test_ics[idx]\n", + " target = test_targets[idx]\n", + "\n", + " # Constant viscosity prediction\n", + " const_pred = burgers_reference(ic, nu_const, DT, N_STEPS)[0]\n", + "\n", + " # Direct ML prediction\n", + " direct_pred = direct_predict(direct_params, ic)\n", + "\n", + " # Learned closure prediction (outer loop calling both Tesseracts)\n", + " learned_pred = solve_with_closure(ic, params, DT, N_STEPS)\n", + "\n", + " ax.plot(x_np, target.numpy(), \"k-\", linewidth=2, label=\"Ground truth\")\n", + " ax.plot(\n", + " x_np,\n", + " const_pred.numpy(),\n", + " \"b--\",\n", + " linewidth=1.5,\n", + " alpha=0.7,\n", + " label=\"Constant viscosity\",\n", + " )\n", + " ax.plot(\n", + " x_np, direct_pred.numpy(), \"g:\", linewidth=1.5, alpha=0.7, label=\"Direct ML\"\n", + " )\n", + " ax.plot(\n", + " x_np, learned_pred.numpy(), \"r-\", linewidth=1.5, label=\"Learned closure\"\n", + " )\n", + " ax.set_xlabel(\"x\")\n", + " ax.set_ylabel(\"u\")\n", + " ax.set_title(f\"Test case {idx + 1}\")\n", + " ax.grid(True, alpha=0.3)\n", + " if idx == 0:\n", + " ax.legend(fontsize=9)\n", "\n", "plt.suptitle(\n", " \"Solution predictions on unseen initial conditions\", fontsize=13, fontweight=\"bold\"\n", @@ -506,14 +623,22 @@ }, { "cell_type": "markdown", - "id": "73cvnq9gla", "metadata": {}, - "source": "## 6. Modularity: swap the closure *or the solver*\n\nThe solver and closure are independent Tesseracts with a clean contract: the solver takes `(u, nu_field, dt)` and returns `u_next`. The closure takes `(u, dudx, x, weights)` and returns `nu`.\n\nThis means you can:\n- **Swap the closure**: replace the neural network architecture without touching the solver\n- **Swap the solver**: replace the JAX solver with a Fortran solver (differentiated by Enzyme or a hand-written adjoint) without touching the closure or training loop\n\nHere we demonstrate closure swapping — training a different closure initialization against the same solver." + "source": [ + "## 6. Modularity: swap the closure *or the solver*\n", + "\n", + "The solver and closure are independent Tesseracts with a clean contract: the solver takes `(u, nu_field, dt)` and returns `u_next`. The closure takes `(u, dudx, x, weights)` and returns `nu`.\n", + "\n", + "This means you can:\n", + "- **Swap the closure**: replace the neural network architecture without touching the solver\n", + "- **Swap the solver**: replace the PyTorch solver with a Fortran solver (differentiated by Enzyme or a hand-written adjoint) without touching the closure or training loop\n", + "\n", + "Here we demonstrate closure swapping — training a different closure initialization against the same solver." + ] }, { "cell_type": "code", "execution_count": null, - "id": "jd62sfsyzn", "metadata": {}, "outputs": [], "source": [ @@ -521,40 +646,40 @@ "# In production with Tesseract containers, this would mean pointing the solver\n", "# at a completely different closure Tesseract URL. The solver code is untouched.\n", "\n", - "swapped_params = init_params(jax.random.PRNGKey(99)) # different seed\n", - "swapped_opt = optax.adam(3e-3)\n", - "swapped_opt_state = swapped_opt.init(swapped_params)\n", + "swapped_params = init_params(seed=99)\n", + "swapped_train = {k: v.clone().requires_grad_(True) for k, v in swapped_params.items()}\n", + "swapped_opt = torch.optim.Adam(swapped_train.values(), lr=3e-3)\n", "\n", "print(\"Training a different closure (same solver, different initialization)...\")\n", "for _epoch in range(500):\n", - " swapped_params, swapped_opt_state, sl = train_step(\n", - " swapped_params, swapped_opt_state, train_ics, train_targets\n", - " )\n", - "\n", - "swapped_test_mse = float(loss_batch(swapped_params, test_ics, test_targets))\n", + " swapped_opt.zero_grad()\n", + " sl = loss_batch(swapped_train, train_ics, train_targets)\n", + " sl.backward()\n", + " swapped_opt.step()\n", + "\n", + "swapped_params = {k: v.detach().clone() for k, v in swapped_train.items()}\n", + "with torch.no_grad():\n", + " swapped_test_mse = float(loss_batch(swapped_params, test_ics, test_targets))\n", "print(f\" Swapped closure test MSE: {swapped_test_mse:.4e}\")\n", "print(f\" Original closure test MSE: {learned_test_mse:.4e}\")\n", "print(\"\\nSolver code: unchanged. Only the closure was swapped.\")\n", "\n", "# Compare the two learned viscosity profiles\n", - "nu_orig = apply_tesseract(\n", - " closure_tess,\n", - " {\"u\": train_ics[0], \"dudx\": jnp.gradient(train_ics[0], DX), \"x\": X, **params},\n", - ")[\"nu\"]\n", - "nu_swap = apply_tesseract(\n", - " closure_tess,\n", - " {\n", - " \"u\": train_ics[0],\n", - " \"dudx\": jnp.gradient(train_ics[0], DX),\n", - " \"x\": X,\n", - " **swapped_params,\n", - " },\n", - ")[\"nu\"]\n", + "with torch.no_grad():\n", + " dudx0 = torch.gradient(train_ics[0], spacing=(DX,))[0]\n", + " nu_orig = apply_tesseract(\n", + " closure_tess,\n", + " {\"u\": train_ics[0], \"dudx\": dudx0, \"x\": X, **params},\n", + " )[\"nu\"]\n", + " nu_swap = apply_tesseract(\n", + " closure_tess,\n", + " {\"u\": train_ics[0], \"dudx\": dudx0, \"x\": X, **swapped_params},\n", + " )[\"nu\"]\n", "\n", "fig, ax = plt.subplots(figsize=(8, 4))\n", - "ax.plot(X, nu_true, \"k-\", linewidth=2, label=\"True\")\n", - "ax.plot(X, nu_orig, \"r--\", linewidth=1.5, label=\"Closure A\")\n", - "ax.plot(X, nu_swap, \"b:\", linewidth=1.5, label=\"Closure B (swapped)\")\n", + "ax.plot(x_np, nu_true.numpy(), \"k-\", linewidth=2, label=\"True\")\n", + "ax.plot(x_np, nu_orig.numpy(), \"r--\", linewidth=1.5, label=\"Closure A\")\n", + "ax.plot(x_np, nu_swap.numpy(), \"b:\", linewidth=1.5, label=\"Closure B (swapped)\")\n", "ax.set_xlabel(\"x\")\n", "ax.set_ylabel(r\"$\\nu$\")\n", "ax.set_title(\"Two different closures, same solver\")\n", @@ -567,9 +692,30 @@ }, { "cell_type": "markdown", - "id": "9ydczif0r8r", "metadata": {}, - "source": "## Summary\n\n| What | How |\n|---|---|\n| Two independent Tesseracts | Solver (single-timestep PDE) + closure (neural viscosity) |\n| Composed in an outer loop | `apply_tesseract(closure, ...)` then `apply_tesseract(solver, ...)` at each timestep |\n| End-to-end gradients | `jax.grad` dispatches VJP through both Tesseracts automatically |\n| Gradient correctness | Validated against finite differences |\n| Learned closure beats baselines | Lower test MSE than constant viscosity or direct ML |\n| Modular swapping | Change either Tesseract without touching the other |\n\n### Line of sight to production\n\nThe solver Tesseract's interface — `(u, nu_field, dt) → u_next` with VJP — is not JAX-specific. A Fortran solver differentiated by [Enzyme](https://enzyme.mit.edu/) (see the `enzyme_thermal_2d` demo) or with a hand-written discrete adjoint could implement the same contract. The training loop and closure Tesseract would be **identical**. This is the core value proposition: closure researchers get access to a library of differentiable solvers without learning each solver's internals.\n\n### What's next\n\n- **Containerized deployment**: Build each Tesseract as a Docker image. The outer loop calls both over HTTP via `apply_tesseract` — same code, real container isolation.\n- **Legacy solver integration**: Wrap a Fortran/C++ solver with adjoint as a Tesseract. The closure training loop above works unchanged.\n- **Scale up**: Larger grids, 2D/3D problems, more complex closures (e.g., convolutional, attention-based).\n- **Real applications**: Replace the Burgers' equation with a turbulence model, climate sub-grid scheme, or materials constitutive law." + "source": [ + "## Summary\n", + "\n", + "| What | How |\n", + "|---|---|\n", + "| Two independent Tesseracts | Solver (single-timestep PDE) + closure (neural viscosity) |\n", + "| Composed in an outer loop | `apply_tesseract(closure, ...)` then `apply_tesseract(solver, ...)` at each timestep |\n", + "| End-to-end gradients | `torch.autograd` dispatches VJP through both Tesseracts automatically |\n", + "| Gradient correctness | Validated against finite differences |\n", + "| Learned closure beats baselines | Lower test MSE than constant viscosity or direct ML |\n", + "| Modular swapping | Change either Tesseract without touching the other |\n", + "\n", + "### Line of sight to production\n", + "\n", + "The solver Tesseract's interface — `(u, nu_field, dt) → u_next` with VJP — is not PyTorch-specific. A Fortran solver differentiated by [Enzyme](https://enzyme.mit.edu/) (see the `enzyme_thermal_2d` demo) or with a hand-written discrete adjoint could implement the same contract. The training loop and closure Tesseract would be **identical**. This is the core value proposition: closure researchers get access to a library of differentiable solvers without learning each solver's internals.\n", + "\n", + "### What's next\n", + "\n", + "- **Containerized deployment**: Build each Tesseract as a Docker image. The outer loop calls both over HTTP via `apply_tesseract` — same code, real container isolation.\n", + "- **Legacy solver integration**: Wrap a Fortran/C++ solver with adjoint as a Tesseract. The closure training loop above works unchanged.\n", + "- **Scale up**: Larger grids, 2D/3D problems, more complex closures (e.g., convolutional, attention-based).\n", + "- **Real applications**: Replace the Burgers' equation with a turbulence model, climate sub-grid scheme, or materials constitutive law." + ] } ], "metadata": { @@ -584,5 +730,5 @@ } }, "nbformat": 4, - "nbformat_minor": 5 + "nbformat_minor": 4 } diff --git a/demo/learned-closure/neural_viscosity/tesseract_api.py b/demo/learned-closure/neural_viscosity/tesseract_api.py index a06850072..6765cdb99 100644 --- a/demo/learned-closure/neural_viscosity/tesseract_api.py +++ b/demo/learned-closure/neural_viscosity/tesseract_api.py @@ -1,7 +1,7 @@ # Copyright 2025 Pasteur Labs. All Rights Reserved. # SPDX-License-Identifier: Apache-2.0 -"""Neural viscosity closure Tesseract. +"""Neural viscosity closure Tesseract (PyTorch). A small MLP that predicts spatially-varying viscosity from local flow features. Used as a learned closure inside a PDE solver — the solver calls this Tesseract @@ -14,10 +14,10 @@ from typing import Any -import equinox as eqx -import jax -import jax.numpy as jnp +import numpy as np +import torch from pydantic import BaseModel, Field +from torch.utils._pytree import tree_map from tesseract_core.runtime import Array, Differentiable, Float64 from tesseract_core.runtime.tree_transforms import filter_func, flatten_with_paths @@ -26,6 +26,12 @@ HIDDEN_DIM = 32 N_HIDDEN_LAYERS = 2 +to_tensor = lambda x: ( + torch.tensor(x, dtype=torch.float64) + if isinstance(x, np.generic | np.ndarray) + else x +) + class InputSchema(BaseModel): u: Differentiable[Array[(None,), Float64]] = Field( @@ -60,74 +66,39 @@ class OutputSchema(BaseModel): ) -@eqx.filter_jit -def apply_jit(inputs: dict) -> dict: +def evaluate(inputs: dict) -> dict: + """Core differentiable computation — pure torch operations.""" u = inputs["u"] dudx = inputs["dudx"] x = inputs["x"] # Stack features: [u, dudx, x] at each grid point -> (N, 3) - features = jnp.stack([u, dudx, x], axis=-1) + features = torch.stack([u, dudx, x], dim=-1) # Forward pass through MLP h = features @ inputs["w1"] + inputs["b1"] - h = jnp.tanh(h) + h = torch.tanh(h) h = h @ inputs["w2"] + inputs["b2"] - h = jnp.tanh(h) + h = torch.tanh(h) out = h @ inputs["w3"] + inputs["b3"] # Sigmoid * scale to keep viscosity in a physically reasonable range. # Range [0, nu_max] prevents CFL violations in the explicit solver. nu_max = 0.05 - nu = nu_max * jax.nn.sigmoid(out[:, 0]) + nu = nu_max * torch.sigmoid(out[:, 0]) return {"nu": nu} def apply(inputs: InputSchema) -> OutputSchema: - return apply_jit(inputs.model_dump()) + tensor_inputs = tree_map(to_tensor, inputs.model_dump()) + return evaluate(tensor_inputs) def abstract_eval(abstract_inputs: Any) -> Any: - is_shapedtype_dict = lambda x: type(x) is dict and (x.keys() == {"shape", "dtype"}) - is_shapedtype_struct = lambda x: isinstance(x, jax.ShapeDtypeStruct) - - jaxified_inputs = jax.tree.map( - lambda x: jax.ShapeDtypeStruct(**x) if is_shapedtype_dict(x) else x, - abstract_inputs.model_dump(), - is_leaf=is_shapedtype_dict, - ) - dynamic_inputs, static_inputs = eqx.partition( - jaxified_inputs, filter_spec=is_shapedtype_struct - ) - - def wrapped_apply(dynamic_inputs: Any) -> Any: - inputs = eqx.combine(static_inputs, dynamic_inputs) - return apply_jit(inputs) - - jax_shapes = jax.eval_shape(wrapped_apply, dynamic_inputs) - return jax.tree.map( - lambda x: ( - {"shape": x.shape, "dtype": str(x.dtype)} if is_shapedtype_struct(x) else x - ), - jax_shapes, - is_leaf=is_shapedtype_struct, - ) - - -@eqx.filter_jit -def jvp_jit( - inputs: dict, - jvp_inputs: tuple[str], - jvp_outputs: tuple[str], - tangent_vector: dict, -) -> Any: - filtered_apply = filter_func(apply_jit, inputs, jvp_outputs) - return jax.jvp( - filtered_apply, - [flatten_with_paths(inputs, include_paths=jvp_inputs)], - [tangent_vector], - )[1] + inputs_dict = abstract_inputs.model_dump() + n = inputs_dict["u"]["shape"][0] + return {"nu": {"shape": [n], "dtype": "float64"}} def jacobian_vector_product( @@ -135,27 +106,19 @@ def jacobian_vector_product( jvp_inputs: set[str], jvp_outputs: set[str], tangent_vector: dict[str, Any], -) -> Any: - return jvp_jit( - inputs.model_dump(), - tuple(jvp_inputs), - tuple(jvp_outputs), - tangent_vector, - ) +): + jvp_inputs = tuple(jvp_inputs) + tangent_vector = {key: tangent_vector[key] for key in jvp_inputs} + tensor_inputs = tree_map(to_tensor, inputs.model_dump()) + pos_tangent = tree_map(to_tensor, tangent_vector).values() + pos_inputs = flatten_with_paths(tensor_inputs, jvp_inputs).values() -@eqx.filter_jit -def vjp_jit( - inputs: dict, - vjp_inputs: tuple[str], - vjp_outputs: tuple[str], - cotangent_vector: dict, -) -> Any: - filtered_apply = filter_func(apply_jit, inputs, vjp_outputs) - _, vjp_func = jax.vjp( - filtered_apply, flatten_with_paths(inputs, include_paths=vjp_inputs) + filtered_pos_eval = filter_func( + evaluate, tensor_inputs, jvp_outputs, input_paths=jvp_inputs ) - return vjp_func(cotangent_vector)[0] + + return torch.func.jvp(filtered_pos_eval, tuple(pos_inputs), tuple(pos_tangent))[1] def vector_jacobian_product( @@ -163,30 +126,46 @@ def vector_jacobian_product( vjp_inputs: set[str], vjp_outputs: set[str], cotangent_vector: dict[str, Any], -) -> Any: - return vjp_jit( - inputs.model_dump(), - tuple(vjp_inputs), - tuple(vjp_outputs), - cotangent_vector, - ) +): + vjp_inputs = tuple(vjp_inputs) + cotangent_vector = {key: cotangent_vector[key] for key in vjp_outputs} + tensor_inputs = tree_map(to_tensor, inputs.model_dump()) + tensor_cotangent = tree_map(to_tensor, cotangent_vector) + pos_inputs = flatten_with_paths(tensor_inputs, vjp_inputs).values() -@eqx.filter_jit -def jac_jit( - inputs: dict, - jac_inputs: tuple[str], - jac_outputs: tuple[str], -) -> Any: - filtered_apply = filter_func(apply_jit, inputs, jac_outputs) - return jax.jacrev(filtered_apply)( - flatten_with_paths(inputs, include_paths=jac_inputs) + filtered_pos_func = filter_func( + evaluate, tensor_inputs, vjp_outputs, input_paths=vjp_inputs ) + _, vjp_func = torch.func.vjp(filtered_pos_func, *pos_inputs) + vjp_vals = vjp_func(tensor_cotangent) + return dict(zip(vjp_inputs, vjp_vals, strict=True)) + def jacobian( inputs: InputSchema, jac_inputs: set[str], jac_outputs: set[str], -) -> Any: - return jac_jit(inputs.model_dump(), tuple(jac_inputs), tuple(jac_outputs)) +): + jac_inputs = tuple(jac_inputs) + tensor_inputs = tree_map(to_tensor, inputs.model_dump()) + pos_inputs = flatten_with_paths(tensor_inputs, jac_inputs).values() + + filtered_pos_eval = filter_func( + evaluate, tensor_inputs, jac_outputs, input_paths=jac_inputs + ) + + def filtered_pos_eval_flat(*args): + res = filtered_pos_eval(*args) + return tuple(res[k] for k in jac_outputs) + + jac = torch.autograd.functional.jacobian(filtered_pos_eval_flat, tuple(pos_inputs)) + + jac_dict = {} + for dy, dys in zip(jac_outputs, jac, strict=True): + jac_dict[dy] = {} + for dx, dxs in zip(jac_inputs, dys, strict=True): + jac_dict[dy][dx] = dxs + + return jac_dict diff --git a/demo/learned-closure/neural_viscosity/tesseract_config.yaml b/demo/learned-closure/neural_viscosity/tesseract_config.yaml index 04f5e5738..904e3a449 100644 --- a/demo/learned-closure/neural_viscosity/tesseract_config.yaml +++ b/demo/learned-closure/neural_viscosity/tesseract_config.yaml @@ -1,6 +1,6 @@ name: "neural-viscosity" version: "0.1.0" -description: "Neural network closure that predicts spatially-varying viscosity from local flow features" +description: "Neural network closure that predicts spatially-varying viscosity from local flow features (PyTorch)" build_config: target_platform: "native" diff --git a/demo/learned-closure/neural_viscosity/tesseract_requirements.txt b/demo/learned-closure/neural_viscosity/tesseract_requirements.txt index 8f33da031..f6d69b182 100644 --- a/demo/learned-closure/neural_viscosity/tesseract_requirements.txt +++ b/demo/learned-closure/neural_viscosity/tesseract_requirements.txt @@ -1,2 +1,2 @@ -jax[cpu]==0.5.2 -equinox +torch +tesseract-core diff --git a/demo/learned-closure/requirements.txt b/demo/learned-closure/requirements.txt index 6e65604eb..0bb567a5d 100644 --- a/demo/learned-closure/requirements.txt +++ b/demo/learned-closure/requirements.txt @@ -1,6 +1,4 @@ -jax[cpu]==0.5.2 -equinox +torch matplotlib -optax tesseract-core -tesseract-jax +tesseract-torch diff --git a/demo/learned-closure/test_solvers.py b/demo/learned-closure/test_solvers.py index 650394eac..4ddacb574 100644 --- a/demo/learned-closure/test_solvers.py +++ b/demo/learned-closure/test_solvers.py @@ -1,8 +1,8 @@ -"""Smoke tests for the learned closure demo. +"""Smoke tests for the learned closure demo (PyTorch version). Tests the composition pattern: an outer loop calls the closure Tesseract to get a viscosity field, then calls the solver Tesseract to step forward. Gradients -flow end-to-end through both Tesseracts via apply_tesseract / jax.grad. +flow end-to-end through both Tesseracts via apply_tesseract / torch.autograd. This is the same pattern that would work with a Fortran solver Tesseract backed by Enzyme or a hand-written adjoint — the solver just needs apply + VJP with @@ -14,49 +14,45 @@ sys.path.insert(0, "neural_viscosity") sys.path.insert(0, "burgers_solver") -import jax -import jax.numpy as jnp -from tesseract_jax import apply_tesseract +import burgers_solver.tesseract_api as solver_api +import neural_viscosity.tesseract_api as closure_api +import numpy as np +import torch +from tesseract_torch import apply_tesseract from tesseract_core import Tesseract -jax.config.update("jax_enable_x64", True) - -import burgers_solver.tesseract_api as solver_api # noqa: E402 -import neural_viscosity.tesseract_api as closure_api # noqa: E402 - CLOSURE_API_PATH = "neural_viscosity/tesseract_api.py" SOLVER_API_PATH = "burgers_solver/tesseract_api.py" N = 128 DX = 1.0 / (N - 1) -X_GRID = jnp.linspace(0.0, 1.0, N) +X_GRID = torch.linspace(0.0, 1.0, N, dtype=torch.float64) -def _make_closure_params(key): +def _make_closure_params(seed=0): """Initialize random closure network weights.""" - keys = jax.random.split(key, 6) - w1 = jax.random.normal(keys[0], (3, 32)) * jnp.sqrt(2.0 / 3) - b1 = jnp.zeros(32) - w2 = jax.random.normal(keys[1], (32, 32)) * jnp.sqrt(2.0 / 32) - b2 = jnp.zeros(32) - w3 = jax.random.normal(keys[2], (32, 1)) * jnp.sqrt(2.0 / 32) - b3 = jnp.zeros(1) + rng = torch.Generator().manual_seed(seed) + w1 = torch.randn(3, 32, dtype=torch.float64, generator=rng) * np.sqrt(2.0 / 3) + b1 = torch.zeros(32, dtype=torch.float64) + w2 = torch.randn(32, 32, dtype=torch.float64, generator=rng) * np.sqrt(2.0 / 32) + b2 = torch.zeros(32, dtype=torch.float64) + w3 = torch.randn(32, 1, dtype=torch.float64, generator=rng) * np.sqrt(2.0 / 32) + b3 = torch.zeros(1, dtype=torch.float64) return {"w1": w1, "b1": b1, "w2": w2, "b2": b2, "w3": w3, "b3": b3} def _make_initial_condition(): """Smooth initial condition: a sine wave.""" - u0 = jnp.sin(2 * jnp.pi * X_GRID) + u0 = torch.sin(2 * np.pi * X_GRID) return u0 def test_closure_forward(): print("=== Neural viscosity closure forward pass ===") - key = jax.random.PRNGKey(0) - params = _make_closure_params(key) + params = _make_closure_params(seed=0) u0 = _make_initial_condition() - dudx = jnp.gradient(u0, DX) + dudx = torch.gradient(u0, spacing=(DX,))[0] inputs = closure_api.InputSchema(u=u0, dudx=dudx, x=X_GRID, **params) out = closure_api.apply(inputs) @@ -64,14 +60,14 @@ def test_closure_forward(): print(f" Shape: {nu.shape}, range: [{float(nu.min()):.4f}, {float(nu.max()):.4f}]") assert nu.shape == (N,) - assert jnp.all(nu > 0), "Viscosity must be positive" + assert torch.all(nu > 0), "Viscosity must be positive" print(" PASSED") def test_solver_single_step(): print("\n=== Solver single timestep ===") u0 = _make_initial_condition() - nu = jnp.full(N, 0.01) # constant viscosity + nu = torch.full((N,), 0.01, dtype=torch.float64) dt = 1e-4 inputs = solver_api.InputSchema(u=u0, nu=nu, dt=dt) @@ -79,9 +75,9 @@ def test_solver_single_step(): u_next = out["u_next"] print(f" Shape: {u_next.shape}") - print(f" Max change: {float(jnp.max(jnp.abs(u_next - u0))):.6e}") + print(f" Max change: {float(torch.max(torch.abs(u_next - u0))):.6e}") assert u_next.shape == (N,) - assert jnp.all(jnp.isfinite(u_next)), "Solution contains NaN or Inf" + assert torch.all(torch.isfinite(u_next)), "Solution contains NaN or Inf" # Boundary values should be preserved assert float(u_next[0]) == float(u0[0]), "Left BC violated" assert float(u_next[-1]) == float(u0[-1]), "Right BC violated" @@ -91,19 +87,24 @@ def test_solver_single_step(): def test_solver_gradient(): print("\n=== Solver gradient (VJP w.r.t. nu field) ===") u0 = _make_initial_condition() - nu = jnp.full(N, 0.01) + nu = torch.full((N,), 0.01, dtype=torch.float64, requires_grad=True) dt = 1e-4 - def loss_fn(nu_field): - out = solver_api.apply_jit({"u": u0, "nu": nu_field, "dt": dt}) - return jnp.mean(out["u_next"] ** 2) + tensor_inputs = { + "u": u0.clone(), + "nu": nu, + "dt": torch.tensor(dt, dtype=torch.float64), + } + out = solver_api.evaluate(tensor_inputs) + loss = torch.mean(out["u_next"] ** 2) + loss.backward() - grad_nu = jax.grad(loss_fn)(nu) + grad_nu = nu.grad print( - f" Gradient shape: {grad_nu.shape}, norm: {float(jnp.linalg.norm(grad_nu)):.6e}" + f" Gradient shape: {grad_nu.shape}, norm: {float(torch.linalg.norm(grad_nu)):.6e}" ) assert grad_nu.shape == (N,) - assert jnp.all(jnp.isfinite(grad_nu)) + assert torch.all(torch.isfinite(grad_nu)) print(" PASSED") @@ -113,14 +114,13 @@ def test_composition_forward(): closure_tess = Tesseract.from_tesseract_api(CLOSURE_API_PATH) solver_tess = Tesseract.from_tesseract_api(SOLVER_API_PATH) - key = jax.random.PRNGKey(42) - params = _make_closure_params(key) + params = _make_closure_params(seed=42) u = _make_initial_condition() dt = 1e-4 n_steps = 50 for _step in range(n_steps): - dudx = jnp.gradient(u, DX) + dudx = torch.gradient(u, spacing=(DX,))[0] closure_out = apply_tesseract( closure_tess, {"u": u, "dudx": dudx, "x": X_GRID, **params} ) @@ -131,7 +131,7 @@ def test_composition_forward(): print(f" Shape: {u.shape}") print(f" Range: [{float(u.min()):.4f}, {float(u.max()):.4f}]") assert u.shape == (N,) - assert jnp.all(jnp.isfinite(u)), "Solution contains NaN or Inf" + assert torch.all(torch.isfinite(u)), "Solution contains NaN or Inf" print(" PASSED") @@ -141,40 +141,45 @@ def test_composition_gradient(): closure_tess = Tesseract.from_tesseract_api(CLOSURE_API_PATH) solver_tess = Tesseract.from_tesseract_api(SOLVER_API_PATH) - key = jax.random.PRNGKey(42) - params = _make_closure_params(key) + params = _make_closure_params(seed=42) u0 = _make_initial_condition() target = 0.9 * u0 dt = 1e-4 n_steps = 20 - def loss_fn(w1): - u = u0 - p = {**params, "w1": w1} + # Make w1 require grad for end-to-end differentiation + w1 = params["w1"].clone().requires_grad_(True) + + def run_forward(w1_val): + u = u0.clone() + p = {**params, "w1": w1_val} for _step in range(n_steps): - dudx = jnp.gradient(u, DX) + dudx = torch.gradient(u, spacing=(DX,))[0] closure_out = apply_tesseract( closure_tess, {"u": u, "dudx": dudx, "x": X_GRID, **p} ) nu = closure_out["nu"] solver_out = apply_tesseract(solver_tess, {"u": u, "nu": nu, "dt": dt}) u = solver_out["u_next"] - return jnp.mean((u - target) ** 2) + return torch.mean((u - target) ** 2) # AD gradient - grad_ad = jax.grad(loss_fn)(params["w1"]) + loss = run_forward(w1) + (grad_ad,) = torch.autograd.grad(loss, w1) # Finite difference check on one element eps = 1e-5 idx = (0, 0) - w1_plus = params["w1"].at[idx].add(eps) - w1_minus = params["w1"].at[idx].add(-eps) - fd = (loss_fn(w1_plus) - loss_fn(w1_minus)) / (2 * eps) + ad_val = float(grad_ad[idx]) - rel_err = abs(float(grad_ad[idx]) - float(fd)) / (abs(float(fd)) + 1e-30) - print( - f" AD: {float(grad_ad[idx]):.6e}, FD: {float(fd):.6e}, Rel error: {rel_err:.2e}" - ) + w1_plus = w1.detach().clone() + w1_plus[idx] += eps + w1_minus = w1.detach().clone() + w1_minus[idx] -= eps + fd = (float(run_forward(w1_plus)) - float(run_forward(w1_minus))) / (2 * eps) + + rel_err = abs(ad_val - fd) / (abs(fd) + 1e-30) + print(f" AD: {ad_val:.6e}, FD: {fd:.6e}, Rel error: {rel_err:.2e}") assert rel_err < 1e-2, f"Gradient error too large: {rel_err}" print(" PASSED") From a6cb90b831bc50e876315d5c4d40b3c9f48943ce Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dion=20H=C3=A4fner?= Date: Wed, 24 Jun 2026 16:36:08 +0200 Subject: [PATCH 3/4] add missing t-torch mentions; overhaul demo --- AGENTS.md | 1 + README.md | 1 + demo/learned-closure/demo.ipynb | 764 ++++++++++-------- .../neural_viscosity/tesseract_api.py | 171 ---- .../neural_viscosity/tesseract_config.yaml | 6 - .../tesseract_requirements.txt | 2 - demo/learned-closure/test_solvers.py | 160 ++-- docs/_templates/page.html | 2 + docs/content/demo/demo.md | 8 +- docs/index.md | 49 +- 10 files changed, 576 insertions(+), 588 deletions(-) delete mode 100644 demo/learned-closure/neural_viscosity/tesseract_api.py delete mode 100644 demo/learned-closure/neural_viscosity/tesseract_config.yaml delete mode 100644 demo/learned-closure/neural_viscosity/tesseract_requirements.txt diff --git a/AGENTS.md b/AGENTS.md index 7f9c31657..1a6bc8abe 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -40,4 +40,5 @@ Each of these is a separate repository/Python package. - **Tesseract Core** is the main codebase that defines the Tesseract specification, the Python SDK for defining and building Tesseracts, and the runtime for executing Tesseracts in containers. - **Tesseract-JAX** is a mature package that supports full integration of Tesseract calls into JAX programs, including JIT compilation and automatic differentiation of code that mixes Tesseract calls and JAX operations. +- **Tesseract-Torch** is the PyTorch counterpart to Tesseract-JAX: it embeds Tesseract calls as PyTorch operators so that `torch.autograd` flows through code that mixes Tesseract calls and PyTorch operations. - **Tesseract-Streamlit** provides tools to auto-generate Streamlit apps from (externally running / locally built) Tesseracts. It can be used to quickly create interactive demos for Tesseracts and custom visualization without writing any Streamlit code, but is limited to forward application (`apply`). diff --git a/README.md b/README.md index 1d007575c..affe37c14 100644 --- a/README.md +++ b/README.md @@ -122,6 +122,7 @@ with Tesseract.from_image("my-tesseract") as t: - **[Tesseract Core](https://github.com/pasteurlabs/tesseract-core)** — CLI, Python SDK, and runtime (this repo). - **[Tesseract-JAX](https://github.com/pasteurlabs/tesseract-jax)** — Embed Tesseracts as JAX primitives into end-to-end differentiable JAX programs. +- **[Tesseract-Torch](https://github.com/pasteurlabs/tesseract-torch)** — Embed Tesseracts as PyTorch operators into end-to-end differentiable PyTorch programs. - **[Tesseract-Streamlit](https://github.com/pasteurlabs/tesseract-streamlit)** — Auto-generate interactive web apps from Tesseracts. ## Learn more diff --git a/demo/learned-closure/demo.ipynb b/demo/learned-closure/demo.ipynb index 74789e1f9..57a682327 100644 --- a/demo/learned-closure/demo.ipynb +++ b/demo/learned-closure/demo.ipynb @@ -6,88 +6,208 @@ "source": [ "# Learned Closure: Training a Neural Viscosity Model Through a PDE Solver (PyTorch)\n", "\n", - "This demo trains a neural network closure **end-to-end through a PDE solver**, with gradients flowing through both the solver and the network during training.\n", + "In this tutorial, you will learn how to:\n", "\n", - "**The setup**: a 1D Burgers' equation solver where the viscosity model — normally a hand-tuned constant — is replaced by a small neural network. The closure is called at every timestep to predict the current viscosity field from the current flow state, and we train the network by differentiating through the entire time-stepping loop.\n", + "1. **Build a Tesseract** that wraps a single-timestep differentiable PDE solver (a 1D Burgers' equation solver) and exposes a vector-Jacobian product.\n", + "2. **Use [Tesseract-Torch](https://github.com/pasteurlabs/tesseract-torch)** to call the served solver as a native PyTorch autograd layer via `apply_tesseract`, so gradients flow through the container automatically.\n", + "3. **Train a neural network closure end-to-end** through the containerized solver, differentiating through the entire time-stepping loop.\n", + "4. **Compare the learned closure against baselines** (a pure physics model and a pure ML model).\n", "\n", - "**The key result**: the learned closure recovers the true (unknown) viscosity profile from solution data alone, and produces better predictions than either the pure physics model (wrong constant viscosity) or a pure ML model (no physics structure).\n", + "We will replace the viscosity model of a Burgers' equation solver -- normally a hand-tuned constant -- with a small neural network, and train it so that it recovers the true (unknown) viscosity profile from solution data alone.\n", "\n", - "**This version** uses **PyTorch** and **tesseract-torch** instead of JAX and tesseract-jax. The Tesseract API files use `torch.func` for autodiff, and the outer training loop uses `torch.autograd` for end-to-end gradient computation.\n", + "## Context\n", "\n", - "## Architecture: two Tesseracts, composed in an outer loop\n", + "Closure modeling is a recurring problem across computational science: a simulation resolves the large scales but needs a model for the unresolved physics -- a turbulence closure, a sub-grid scheme, a constitutive law. These closures are often hand-tuned or empirical, and a natural idea is to *learn* them from data instead. The catch is that a closure only makes sense *inside* the solver: to train it well, gradients have to flow from the simulation output, through the solver, and back into the network.\n", "\n", - "The demo uses two independent Tesseracts:\n", + "This is hard in practice because the solver is usually not a 30-line function you can `import` into your training script. It is a heavyweight simulator with its own runtime, dependencies, and adjoint -- a Fortran/C++ CFD code, a legacy in-house solver, or something differentiated by [Enzyme](https://enzyme.mit.edu/) at the LLVM IR level.\n", "\n", - "- **`burgers_solver`**: a single-timestep Burgers' equation solver. Takes the current velocity field `u` and a viscosity field `nu`, returns the velocity field after one explicit Euler step. This is a pure physics component with a clean interface: `(u, nu, dt) → u_next`.\n", - "- **`neural_viscosity`**: a small MLP that maps local flow features $(u, \\partial u/\\partial x, x)$ to a viscosity field $\\nu$. Exposes standard gradient endpoints (VJP, JVP, Jacobian).\n", + "With Tesseracts, we wrap the solver in a container that exposes a clean differentiable interface, and call it over HTTP as a single layer inside an otherwise ordinary PyTorch training loop. The neural network lives in native PyTorch; the solver lives in a Docker image. [Tesseract-Torch](https://github.com/pasteurlabs/tesseract-torch) registers the served solver as a PyTorch autograd custom function, so `loss.backward()` dispatches a vector-Jacobian product (VJP) call back through the solver automatically -- the analogue of what [Tesseract-JAX](https://github.com/pasteurlabs/tesseract-jax) does for JAX in the other demos in this series.\n", "\n", - "The outer time-stepping loop lives in the training script and calls both Tesseracts via `apply_tesseract` at every timestep:\n", + "### The components\n", "\n", - "```\n", + "- **`burgers_solver`** (containerized Tesseract): a single-timestep Burgers' equation solver. Takes the current velocity field `u` and a viscosity field `nu`, returns the velocity field after one explicit Euler step -- a pure physics component with the interface $(u, \\nu, dt) \\to u_\\text{next}$. It exposes a VJP, so it slots into PyTorch autograd like any other layer, even though it runs in a separate container.\n", + "- **`ViscosityNet`** (`torch.nn.Module`): a small MLP that maps local flow features $(u, \\partial u/\\partial x, x)$ to a viscosity field $\\nu$. An ordinary network, trained with a standard optimizer, in this process.\n", + "\n", + "The outer time-stepping loop calls the network directly and the solver via `apply_tesseract`:\n", + "\n", + "```python\n", "for each timestep:\n", - " nu_field = apply_tesseract(closure, {u, dudx, x, weights})\n", - " u_next = apply_tesseract(solver, {u, nu_field, dt})\n", + " nu = viscosity_net(u, dudx, x) # plain torch, in-process\n", + " u_next = apply_tesseract(solver, {u, nu, dt})[\"u_next\"] # HTTP call to container\n", " u = u_next\n", "```\n", "\n", - "Because `apply_tesseract` registers each call as a PyTorch autograd custom function, `torch.autograd.grad` through the loop automatically dispatches VJP calls back through both Tesseracts. Gradients flow end-to-end with no manual plumbing.\n", - "\n", - "### Why this architecture matters\n", - "\n", - "The solver's interface — `(u, nu_field, dt) → u_next` — is **not specific to PyTorch**. A Fortran solver with a hand-written adjoint, or one differentiated by [Enzyme](https://enzyme.mit.edu/) at the LLVM IR level (see the `enzyme_thermal_2d` demo), could implement the same contract. The outer loop and training code would be identical. This is the core Tesseract value proposition: **the closure researcher doesn't need to know how the solver computes its gradients**." + "To learn more about building and running Tesseracts, please refer to the [Tesseract documentation](https://docs.pasteurlabs.ai/projects/tesseract-core/latest/)." ] }, { "cell_type": "code", "execution_count": null, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "FULL_RUN=False: 3 train / 2 test ICs, 40 steps, 100 epochs, dt=0.0003\n" + ] + } + ], "source": [ - "import sys\n", + "import os\n", "\n", - "sys.path.insert(0, \"neural_viscosity\")\n", - "sys.path.insert(0, \"burgers_solver\")\n", - "\n", - "import burgers_solver.tesseract_api as solver_api\n", "import matplotlib.pyplot as plt\n", - "import neural_viscosity.tesseract_api as closure_api\n", "import numpy as np\n", "import torch\n", + "import torch.nn as nn\n", "from tesseract_torch import apply_tesseract\n", "\n", "from tesseract_core import Tesseract\n", "\n", + "torch.set_default_dtype(torch.float64)\n", + "\n", + "# Workload size. Every solver call is an HTTP round-trip to the container, so\n", + "# the defaults are modest to keep the demo (and CI) to a few minutes.\n", + "# Set FULL_RUN=1 for the publication-quality run used to produce the figures.\n", + "FULL_RUN = os.environ.get(\"FULL_RUN\", \"0\") == \"1\"\n", + "\n", + "# Time step. The spatially-varying viscosity only becomes identifiable once\n", + "# diffusion has acted appreciably over the rollout, so we use a dt large enough\n", + "# for the closure to actually \"feel\" the viscosity profile (well within the\n", + "# explicit-diffusion CFL limit nu*dt/dx^2 < 0.5). Too small a dt and every\n", + "# viscosity profile fits the data equally well — the closure can't recover the\n", + "# shape no matter how long it trains.\n", + "DT = 3e-4\n", + "LR = 5e-3\n", + "\n", + "if FULL_RUN:\n", + " N_TRAIN, N_TEST = 8, 4\n", + " N_STEPS = 80\n", + " N_EPOCHS = 300\n", + " DIRECT_EPOCHS = 300\n", + "else:\n", + " # Small but enough to recover the viscosity hump and beat the constant\n", + " # baseline.\n", + " N_TRAIN, N_TEST = 3, 2\n", + " N_STEPS = 40\n", + " N_EPOCHS = 100\n", + " DIRECT_EPOCHS = 100\n", + "\n", + "print(\n", + " f\"FULL_RUN={FULL_RUN}: {N_TRAIN} train / {N_TEST} test ICs, \"\n", + " f\"{N_STEPS} steps, {N_EPOCHS} epochs, dt={DT}\"\n", + ")\n", + "\n", "# Grid setup (must match the solver)\n", "N = 128\n", "DX = 1.0 / (N - 1)\n", - "X = torch.linspace(0.0, 1.0, N, dtype=torch.float64)\n", + "X = torch.linspace(0.0, 1.0, N)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Build and serve the solver Tesseract\n", "\n", - "# Load both Tesseracts (local dev mode — no Docker needed)\n", - "closure_tess = Tesseract.from_tesseract_api(\"neural_viscosity/tesseract_api.py\")\n", - "solver_tess = Tesseract.from_tesseract_api(\"burgers_solver/tesseract_api.py\")\n", + "We build the solver into a Docker image with `tesseract build`, then serve it in a container and call it over HTTP -- the same way you would deploy a real simulator. The training loop below never imports the solver's code; it only ever talks to the running container.\n", "\n", - "print(f\"Closure endpoints: {closure_tess.available_endpoints}\")\n", - "print(f\"Solver endpoints: {solver_tess.available_endpoints}\")" + "First, build the image (this can take a few minutes the first time):" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [ + { + "name": "stderr", + "output_type": "stream", + "text": [ + "\u001b[2K \u001b[1;2m[\u001b[0m\u001b[34mi\u001b[0m\u001b[1;2m]\u001b[0m Building image \u001b[33m...\u001b[0m\n", + "\u001b[2K\u001b[37m⠇\u001b[0m \u001b[37mProcessing\u001b[0m\n", + "\u001b[1A\u001b[2K \u001b[1;2m[\u001b[0m\u001b[34mi\u001b[0m\u001b[1;2m]\u001b[0m Built image sh\u001b[1;92ma256:1506\u001b[0mefb7dade, \u001b[1m[\u001b[0m\u001b[32m'burgers-solver:0.1.0'\u001b[0m, \u001b[32m'burgers-solver:latest'\u001b[0m\u001b[1m]\u001b[0m\n" + ] + }, + { + "name": "stdout", + "output_type": "stream", + "text": [ + "[\"burgers-solver:0.1.0\", \"burgers-solver:latest\"]\n" + ] + } + ], + "source": [ + "%%bash\n", + "tesseract build burgers_solver/" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "Now load the built image and start a server container. `serve()` launches the container; we tear it down at the end of the notebook to free resources." + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Solver endpoints: ['apply', 'jacobian', 'jacobian_vector_product', 'vector_jacobian_product', 'health', 'abstract_eval', 'test']\n" + ] + } + ], + "source": [ + "solver_tess = Tesseract.from_image(\"burgers-solver\")\n", + "solver_tess.serve()\n", + "print(f\"Solver endpoints: {solver_tess.available_endpoints}\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 1. The ground truth: Burgers' equation with spatially-varying viscosity\n", + "## Step 2: The ground truth -- Burgers' equation with spatially-varying viscosity\n", "\n", "We generate training data from a Burgers' equation with a **known but non-trivial** viscosity profile:\n", "\n", "$$\\nu_{\\text{true}}(x) = \\nu_0 \\left(1 + A \\sin(\\pi x)\\right)$$\n", "\n", - "This represents a spatially-varying material property — analogous to a turbulence closure, constitutive law, or sub-grid model that varies across the domain. The neural closure's job is to recover this profile from solution data alone." + "This represents a spatially-varying material property -- analogous to a turbulence closure, constitutive law, or sub-grid model that varies across the domain. The neural closure's job is to recover this profile from solution data alone.\n", + "\n", + "The reference solutions come from the **same served solver**, called forward-only (no gradients) -- there is no second copy of the physics anywhere in this notebook." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 4, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training set: 3 initial conditions -> solutions\n", + "Test set: 2 initial conditions -> solutions\n", + "Grid: 128 points, dt=0.0003, 40 steps\n" + ] + }, + { + "data": { + "image/png": "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8jv4zJkzapOhypIlS9T/r1+/rq6XeEvcUy9hPHHiRPX+uWLFCossYUyWwfGU5XE8lfdjnhrHU+aPOcdT2ccxlWVF5ZPxFJNSZrZ8+XKtfPnyKvEhS2AeO3ZMf13btm3VG4ihzZs3a9WrV1fHy3KMu3btMvcpWnXMK1SooH5AU2/yRyWZJ+apcRBlmZgfOXJEa9GihRp8Va5cWZs7d65aSppyPt4JCQnazJkzVSLK2dlZ8/Ly0kaPHq09fPiQ4TbRwYMHjf5u1sVZvkrcU9+mYcOG6nskr3EfHx/GuwDheCpvx5zjKcvHPDWOpywTc46nso9jKss5mE/GUzbyj3lrsYiIiIiIiIiIiFJiTykiIiIiIiIiIrI4JqWIiIiIiIiIiMjimJQiIiIiIiIiIiKLY1KKiIiIiIiIiIgsjkkpIiIiIiIiIiKyOCaliIiIiIiIiIjI4piUIiIiIiIiIiIii2NSioiIiIiIiIiILI5JKSIiIiIiIiIisjgmpYiIiIiIiIiIyOKYlCIiIiIiIiIiIotjUoqIyEBoaCg8PDwwb948/b4jR47A0dER+/fvZ6yIiIiIMsDxFBGZykbTNM3ko4mIrMDu3bvRvXt3lYyqUaMGGjZsiG7dumHJkiW5fWpERERE+QLHU0RkCialiIiMGDNmDH799Vc0bdoUgYGBOHnyJJycnBgrIiIiIhNxPEVEGWFSiojIiJiYGNStWxc3b95EQEAA6tWrxzgRERERZQLHU0SUEfaUIiIy4sqVK7hz5w6SkpJw7do1xoiIiIgokzieIqKMsFKKiCiV+Ph4NG/eXPWSkp5SS5cuVVP43N3dGSsiIiIiE3A8RUSmYFKKiCiViRMnYuvWrfjjjz9QtGhRtG3bFm5ubti5cydjRURERGQCjqeIyBScvkdEZMDPz09VRn399ddwdXWFra2t+v/vv/+OVatWMVZEREREGeB4iohMxUopIiIiIiIiIiKyOFZKERERERERERGRxTEpRUREREREREREFsekFBERERERERERWRyTUkREREREREREZHFMShERERERERERkcUxKUVERERERERERBbHpBQREREREREREVkck1JERERERERERGRxTEoREREREREREZHFMSlFREREREREREQWx6QUERERERERERFZHJNSREREREREREQES/t/SnYUnn/JFBAAAAAASUVORK5CYII=", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "# True viscosity profile\n", "NU_0 = 0.02\n", @@ -95,24 +215,20 @@ "nu_true = NU_0 * (1.0 + A * torch.sin(np.pi * X))\n", "\n", "\n", - "# Reference solver: Burgers' equation with known viscosity (no neural network).\n", - "# Uses the same single-step stencil as the solver Tesseract.\n", "def burgers_reference(u0, nu_field, dt, n_steps):\n", - " \"\"\"Solve Burgers' equation with a prescribed viscosity field.\"\"\"\n", + " \"\"\"Reference solution with a prescribed viscosity field (no neural network).\n", + "\n", + " Calls the served solver Tesseract forward-only via `.apply()`. This is just\n", + " data generation, so it stays outside autograd. `.apply()` returns decoded\n", + " NumPy arrays, which we wrap back into tensors.\n", + " \"\"\"\n", " u = u0.clone()\n", - " history = []\n", " for _ in range(n_steps):\n", - " out = solver_api.evaluate(\n", - " {\"u\": u, \"nu\": nu_field, \"dt\": torch.tensor(dt, dtype=torch.float64)}\n", - " )\n", - " history.append(u)\n", - " u = out[\"u_next\"]\n", - " return u, history\n", - "\n", - "\n", - "# Generate training data: multiple initial conditions\n", - "DT = 5e-5\n", - "N_STEPS = 200\n", + " out = solver_tess.apply({\"u\": u, \"nu\": nu_field, \"dt\": dt})\n", + " # .apply() returns a (possibly read-only) NumPy array decoded from JSON;\n", + " # copy into a fresh tensor so PyTorch is happy.\n", + " u = torch.tensor(np.asarray(out[\"u_next\"]))\n", + " return u\n", "\n", "\n", "def make_ic(seed):\n", @@ -122,25 +238,20 @@ " a2 = 0.3 * torch.rand(1, generator=rng).item()\n", " phase = torch.rand(1, generator=rng).item() * np.pi\n", " u0 = a1 * torch.sin(2 * np.pi * X + phase) + a2 * torch.sin(4 * np.pi * X)\n", - " # Zero boundary conditions\n", " u0[0] = 0.0\n", " u0[-1] = 0.0\n", " return u0\n", "\n", "\n", - "N_TRAIN = 8\n", - "N_TEST = 4\n", - "\n", "train_ics = torch.stack([make_ic(i) for i in range(N_TRAIN)])\n", "test_ics = torch.stack([make_ic(N_TRAIN + i) for i in range(N_TEST)])\n", "\n", - "# Generate ground-truth solutions\n", "with torch.no_grad():\n", " train_targets = torch.stack(\n", - " [burgers_reference(ic, nu_true, DT, N_STEPS)[0] for ic in train_ics]\n", + " [burgers_reference(ic, nu_true, DT, N_STEPS) for ic in train_ics]\n", " )\n", " test_targets = torch.stack(\n", - " [burgers_reference(ic, nu_true, DT, N_STEPS)[0] for ic in test_ics]\n", + " [burgers_reference(ic, nu_true, DT, N_STEPS) for ic in test_ics]\n", " )\n", "\n", "print(f\"Training set: {N_TRAIN} initial conditions -> solutions\")\n", @@ -172,56 +283,65 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 2. Initialize the neural closure\n", + "## Step 3: The neural closure -- a plain PyTorch module\n", + "\n", + "The closure is an ordinary `torch.nn.Module`: a small MLP with 2 hidden layers of 32 units. It maps the local flow features $(u, \\partial u/\\partial x, x)$ at each grid point to a viscosity value. There is nothing Tesseract-specific about it -- it is the network the closure researcher brings, trained with a standard optimizer, entirely in this process.\n", "\n", - "The closure is a small MLP with 2 hidden layers of 32 units each. Its weights are passed as explicit inputs to the solver so that `torch.autograd` can differentiate through the entire pipeline." + "A sigmoid keeps the predicted viscosity in a physically reasonable range $[0, \\nu_{\\max}]$, which also prevents CFL violations in the explicit solver." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 5, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Closure parameters: 1217\n", + "Initial viscosity range: [0.0204, 0.0310]\n" + ] + } + ], "source": [ - "def init_params(seed):\n", - " \"\"\"Initialize neural closure weights with Xavier initialization.\"\"\"\n", - " rng = torch.Generator().manual_seed(seed)\n", - " return {\n", - " \"w1\": torch.randn(3, 32, dtype=torch.float64, generator=rng) * np.sqrt(2.0 / 3),\n", - " \"b1\": torch.zeros(32, dtype=torch.float64),\n", - " \"w2\": torch.randn(32, 32, dtype=torch.float64, generator=rng)\n", - " * np.sqrt(2.0 / 32),\n", - " \"b2\": torch.zeros(32, dtype=torch.float64),\n", - " \"w3\": torch.randn(32, 1, dtype=torch.float64, generator=rng)\n", - " * np.sqrt(2.0 / 32),\n", - " \"b3\": torch.zeros(1, dtype=torch.float64),\n", - " }\n", - "\n", - "\n", - "params = init_params(seed=1)\n", - "n_params = sum(p.numel() for p in params.values())\n", - "print(f\"Total parameters: {n_params}\")\n", - "\n", - "# Verify the closure produces sensible output\n", - "with torch.no_grad():\n", - " test_nu = closure_api.apply(\n", - " closure_api.InputSchema(\n", - " u=train_ics[0],\n", - " dudx=torch.gradient(train_ics[0], spacing=(DX,))[0],\n", - " x=X,\n", - " **params,\n", + "class ViscosityNet(nn.Module):\n", + " \"\"\"MLP closure: local flow features (u, du/dx, x) -> viscosity nu.\"\"\"\n", + "\n", + " def __init__(self, hidden_dim=32, nu_max=0.05):\n", + " super().__init__()\n", + " self.nu_max = nu_max\n", + " self.net = nn.Sequential(\n", + " nn.Linear(3, hidden_dim),\n", + " nn.Tanh(),\n", + " nn.Linear(hidden_dim, hidden_dim),\n", + " nn.Tanh(),\n", + " nn.Linear(hidden_dim, 1),\n", " )\n", - " )[\"nu\"]\n", - "print(\n", - " f\"Initial viscosity range: [{float(test_nu.min()):.4f}, {float(test_nu.max()):.4f}]\"\n", - ")" + "\n", + " def forward(self, u, dudx, x):\n", + " features = torch.stack([u, dudx, x], dim=-1) # (N, 3)\n", + " out = self.net(features)[:, 0] # (N,)\n", + " return self.nu_max * torch.sigmoid(out)\n", + "\n", + "\n", + "torch.manual_seed(1)\n", + "closure = ViscosityNet()\n", + "n_params = sum(p.numel() for p in closure.parameters())\n", + "print(f\"Closure parameters: {n_params}\")\n", + "\n", + "# Sanity check: the untrained closure produces sensible viscosities\n", + "with torch.no_grad():\n", + " dudx0 = torch.gradient(train_ics[0], spacing=(DX,))[0]\n", + " nu0 = closure(train_ics[0], dudx0, X)\n", + "print(f\"Initial viscosity range: [{float(nu0.min()):.4f}, {float(nu0.max()):.4f}]\")" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## 3. End-to-end training: differentiating through the solver\n", + "## Step 4: End-to-end training through the containerized solver\n", "\n", "The training loss is:\n", "\n", @@ -229,79 +349,73 @@ "\n", "where $\\nu_\\theta$ is the neural viscosity closure with parameters $\\theta$, and $u_{\\text{solver}}$ runs the full Burgers' equation with $\\nu_\\theta$ called at every timestep.\n", "\n", - "The outer loop calls both Tesseracts via `apply_tesseract`:\n", + "The time-stepping loop calls the network directly and the solver via `apply_tesseract`:\n", "\n", "```python\n", "for each timestep:\n", - " nu = apply_tesseract(closure_tess, {u, dudx, x, weights})[\"nu\"]\n", - " u = apply_tesseract(solver_tess, {u, nu, dt})[\"u_next\"]\n", + " nu = closure(u, dudx, x) # plain torch\n", + " u = apply_tesseract(solver, {u, nu, dt})[\"u_next\"] # HTTP call to container\n", "```\n", "\n", - "`torch.autograd.grad` differentiates through the entire loop — through every timestep, through both `apply_tesseract` calls — automatically. Each `apply_tesseract` dispatches VJP calls back to the respective Tesseract during the backward pass." + "`loss.backward()` differentiates through the entire loop -- through every timestep, through a VJP HTTP request to the container at each step, and into the network weights -- automatically." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 6, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Testing forward pass + gradient...\n", + " Initial loss: 1.356501e-04\n", + " Gradient norm: 1.082265e-03\n", + " Gradients flow: loss -> solver VJP (HTTP) -> network weights.\n" + ] + } + ], "source": [ - "def solve_with_closure(u0, params, dt, n_steps):\n", - " \"\"\"Run the full time-stepping loop, calling both Tesseracts at each step.\n", - "\n", - " This is the composition pattern:\n", - " closure: (u, dudx, x, weights) -> nu_field\n", - " solver: (u, nu_field, dt) -> u_next\n", + "def solve_with_closure(u0, closure, dt, n_steps):\n", + " \"\"\"Run the full time-stepping loop, calling the served solver each step.\n", "\n", - " The solver Tesseract is a pure physics component. In production, it could be\n", - " a Fortran solver with an adjoint — the interface is the same.\n", + " The closure is plain in-process PyTorch; the solver is the containerized\n", + " differentiable layer, reached over HTTP. In production this same loop would\n", + " drive a Fortran solver with an adjoint — only the served image changes.\n", " \"\"\"\n", " u = u0\n", " for _step in range(n_steps):\n", " dudx = torch.zeros_like(u)\n", " dudx[1:-1] = (u[2:] - u[:-2]) / (2 * DX)\n", "\n", - " # Closure: predict viscosity from current flow state\n", - " closure_out = apply_tesseract(\n", - " closure_tess, {\"u\": u, \"dudx\": dudx, \"x\": X, **params}\n", - " )\n", - " nu = closure_out[\"nu\"]\n", + " nu = closure(u, dudx, X) # closure: predict viscosity (native torch)\n", "\n", - " # Solver: one explicit Euler step\n", + " # Solver: one explicit Euler step, executed in the container\n", " solver_out = apply_tesseract(solver_tess, {\"u\": u, \"nu\": nu, \"dt\": dt})\n", " u = solver_out[\"u_next\"]\n", " return u\n", "\n", "\n", - "def loss_single(params, u0, target):\n", - " \"\"\"Loss for a single initial condition: MSE between solver output and data.\"\"\"\n", - " u_final = solve_with_closure(u0, params, DT, N_STEPS)\n", - " return torch.mean((u_final - target) ** 2)\n", - "\n", - "\n", - "def loss_batch(params, ics, targets):\n", - " \"\"\"Mean loss over a batch of initial conditions.\"\"\"\n", - " losses = torch.stack(\n", - " [loss_single(params, ics[i], targets[i]) for i in range(ics.shape[0])]\n", + "def loss_batch(closure, ics, targets):\n", + " \"\"\"Mean MSE over a batch of initial conditions.\"\"\"\n", + " preds = torch.stack(\n", + " [solve_with_closure(ics[i], closure, DT, N_STEPS) for i in range(ics.shape[0])]\n", " )\n", - " return torch.mean(losses)\n", + " return torch.mean((preds - targets) ** 2)\n", "\n", "\n", - "# Verify gradients work\n", + "# Verify the forward pass + gradient flow\n", "print(\"Testing forward pass + gradient...\")\n", - "\n", - "# Enable gradients on parameters\n", - "grad_params = {k: v.clone().requires_grad_(True) for k, v in params.items()}\n", - "\n", - "l0 = loss_batch(grad_params, train_ics, train_targets)\n", + "l0 = loss_batch(closure, train_ics, train_targets)\n", "l0.backward()\n", - "\n", "grad_norm = torch.sqrt(\n", - " sum(p.grad.pow(2).sum() for p in grad_params.values() if p.grad is not None)\n", + " sum(p.grad.pow(2).sum() for p in closure.parameters() if p.grad is not None)\n", ")\n", - "print(f\" Initial loss: {float(l0):.6e}\")\n", + "print(f\" Initial loss: {float(l0.detach()):.6e}\")\n", "print(f\" Gradient norm: {float(grad_norm):.6e}\")\n", - "print(\" Gradients flow: loss -> solver VJP -> closure VJP -> network weights.\")" + "print(\" Gradients flow: loss -> solver VJP (HTTP) -> network weights.\")\n", + "closure.zero_grad()" ] }, { @@ -310,45 +424,52 @@ "source": [ "### Gradient validation against finite differences\n", "\n", - "Correctness proof: the AD gradients match finite differences to high precision." + "Correctness check: the AD gradient (loss → solver VJP over HTTP → network) matches finite differences to high precision." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 7, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + " AD FD Rel. Error\n", + " -8.149260e-07 -8.149260e-07 1.34e-08\n" + ] + } + ], "source": [ - "# Finite difference check on a few weight elements\n", - "eps = 1e-5\n", - "print(f\"{'Parameter':>10s} {'Index':>8s} {'AD':>14s} {'FD':>14s} {'Rel. Error':>12s}\")\n", - "\n", - "# Use a subset for speed\n", + "# Finite difference check on a single weight element of the first layer.\n", "ics_sub = train_ics[:2]\n", "tgt_sub = train_targets[:2]\n", "\n", - "for pname in [\"w1\", \"w2\", \"w3\"]:\n", - " idx = (0, 0)\n", + "# AD gradient\n", + "closure.zero_grad()\n", + "loss_val = loss_batch(closure, ics_sub, tgt_sub)\n", + "loss_val.backward()\n", "\n", - " # AD gradient\n", - " gp = {k: v.clone().requires_grad_(True) for k, v in params.items()}\n", - " loss_val = loss_batch(gp, ics_sub, tgt_sub)\n", - " loss_val.backward()\n", - " ad = float(gp[pname].grad[idx])\n", + "w = closure.net[0].weight.data # first Linear layer (raw tensor, no autograd)\n", + "idx = (0, 0)\n", + "ad = float(closure.net[0].weight.grad[idx])\n", "\n", - " # Finite difference\n", - " with torch.no_grad():\n", - " p_plus = {k: v.clone() for k, v in params.items()}\n", - " p_plus[pname][idx] += eps\n", - " l_plus = float(loss_batch(p_plus, ics_sub, tgt_sub))\n", - "\n", - " p_minus = {k: v.clone() for k, v in params.items()}\n", - " p_minus[pname][idx] -= eps\n", - " l_minus = float(loss_batch(p_minus, ics_sub, tgt_sub))\n", - "\n", - " fd = (l_plus - l_minus) / (2 * eps)\n", - " rel_err = abs(ad - fd) / (abs(fd) + 1e-30)\n", - " print(f\"{pname:>10s} {idx!s:>8s} {ad:14.6e} {fd:14.6e} {rel_err:12.2e}\")" + "# Finite difference\n", + "eps = 1e-5\n", + "orig = w[idx].item()\n", + "with torch.no_grad():\n", + " w[idx] = orig + eps\n", + " l_plus = float(loss_batch(closure, ics_sub, tgt_sub))\n", + " w[idx] = orig - eps\n", + " l_minus = float(loss_batch(closure, ics_sub, tgt_sub))\n", + " w[idx] = orig # restore\n", + "\n", + "fd = (l_plus - l_minus) / (2 * eps)\n", + "rel_err = abs(ad - fd) / (abs(fd) + 1e-30)\n", + "print(f\"{'AD':>14s} {'FD':>14s} {'Rel. Error':>12s}\")\n", + "print(f\"{ad:14.6e} {fd:14.6e} {rel_err:12.2e}\")\n", + "closure.zero_grad()" ] }, { @@ -360,19 +481,36 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 8, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Training neural closure through the solver...\n", + " Epoch 0: train loss = 1.3565e-04, test loss = 9.9687e-06\n", + " Epoch 10: train loss = 1.4967e-05, test loss = 8.3721e-06\n", + " Epoch 20: train loss = 1.3913e-05, test loss = 7.6863e-06\n", + " Epoch 30: train loss = 1.1806e-05, test loss = 5.5764e-06\n", + " Epoch 40: train loss = 8.5923e-06, test loss = 5.1974e-06\n", + " Epoch 50: train loss = 6.8999e-06, test loss = 4.4420e-06\n", + " Epoch 60: train loss = 5.7348e-06, test loss = 3.9761e-06\n", + " Epoch 70: train loss = 4.9379e-06, test loss = 3.6837e-06\n", + " Epoch 80: train loss = 4.5155e-06, test loss = 3.8148e-06\n", + " Epoch 90: train loss = 4.2492e-06, test loss = 3.8258e-06\n", + " Epoch 99: train loss = 4.0613e-06, test loss = 3.7934e-06\n", + "\n", + "Final train loss: 4.0613e-06\n", + "Final test loss: 3.7934e-06\n" + ] + } + ], "source": [ - "N_EPOCHS = 500\n", - "LR = 3e-3\n", - "\n", "# Re-initialize for a clean training run\n", - "params = init_params(seed=1)\n", - "# Wrap as nn.ParameterDict-style: plain tensors with requires_grad\n", - "train_params = {k: v.clone().requires_grad_(True) for k, v in params.items()}\n", - "\n", - "optimizer = torch.optim.Adam(train_params.values(), lr=LR)\n", + "torch.manual_seed(1)\n", + "closure = ViscosityNet()\n", + "optimizer = torch.optim.Adam(closure.parameters(), lr=LR)\n", "\n", "train_losses = []\n", "test_losses = []\n", @@ -380,32 +518,40 @@ "print(\"Training neural closure through the solver...\")\n", "for epoch in range(N_EPOCHS):\n", " optimizer.zero_grad()\n", - " train_loss = loss_batch(train_params, train_ics, train_targets)\n", + " train_loss = loss_batch(closure, train_ics, train_targets)\n", " train_loss.backward()\n", " optimizer.step()\n", + " train_losses.append(float(train_loss.detach()))\n", "\n", - " train_losses.append(float(train_loss))\n", - "\n", - " if epoch % 50 == 0 or epoch == N_EPOCHS - 1:\n", + " if epoch % 10 == 0 or epoch == N_EPOCHS - 1:\n", " with torch.no_grad():\n", - " test_loss = float(loss_batch(train_params, test_ics, test_targets))\n", + " test_loss = float(loss_batch(closure, test_ics, test_targets))\n", " test_losses.append((epoch, test_loss))\n", " print(\n", - " f\" Epoch {epoch:4d}: train loss = {train_losses[-1]:.4e}, test loss = {test_loss:.4e}\"\n", + " f\" Epoch {epoch:4d}: train loss = {train_losses[-1]:.4e}, \"\n", + " f\"test loss = {test_loss:.4e}\"\n", " )\n", "\n", - "# Copy trained params back (detached)\n", - "params = {k: v.detach().clone() for k, v in train_params.items()}\n", - "\n", "print(f\"\\nFinal train loss: {train_losses[-1]:.4e}\")\n", "print(f\"Final test loss: {test_losses[-1][1]:.4e}\")" ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 9, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "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Q0NRokQJdOnSBXv27Em3Du+P/5c4LhMmTHDK3yciIiLFiyqlRAoyHmUvNyo0ApJigahjQMoFIOaEfcG6zNf38LE3SmdllbUEVkr/u29g3vwNO340Tdrdo48j7ViCatKeL3y9PNC9cahZjkSex6x1h/G/dUcQHpOI9//YaxZfL3e0qFIarauXMT9rlCuJikEl4K4+VCL57n//+x+GDRuGadOmmUDqnXfeQdeuXbF7926UL1/+svVXrlyJe++9F6+//jpuu+02zJw5E3fccQc2btyIRo0amXUmTZqE9957D1988QWqV6+OMWPGmNvcsWOHCbIsr7zyCh599NG00wEBAS76q0VERKQoUyglUpBVbW8PcNjU3PSQysjNfvljf9mn/KWkAHERQNRRIPqoPaRK+z31dOwp4GIicPaAfcmKTxAQVMkhsOISdim84v16+uQcSLEZe8ZtV5P2fBdWxg/Du9bD053rYOH2k/hl6wmsPRCJM3FJWLnvjFksPp7uqBZc0gRUDSsG4rpqZdA0rJQJuUTEdd5++20TDD344IPmNMOpn3/+GZ9++imef/75y9Z/99130a1bNwwfPtycHjduHBYtWoT333/fXJdVUgy2Ro8ejdtvv92s8+WXX6JChQqYN28e+vbtmy6ECgkJcdnfKnnnmWeewblz5/D5559nuc7gwYPRpEkTPPHEExp6F+H0+mbNmmH27NmoX7++xl1Eii2FUiIFGYOmbhNTgx23DOFO6hHUuk241OTc3R0IqGBf0DLz20xOAmKOZx5YsT9V1BEgIQpIjALCuezIvpIrs8CKi3+IqZDKPExTk/aCwtvTHT2bVjQLP6DuDY/F6gORWLP/DHaeiMbhyPNITE7B7lMxZvl120n79Tzc0bhykAmobqxTDq2qlVZfKhEnSkpKwoYNG/DCCy+knefu7m6m261atSrT6/B8VlY5YhUUAyc6cOAATp48aW7DEhQUZKqweF3HUIrT9RhqValSBf369cOzzz4LT8/MdyMTExPNYomOth9BNiUlxSyOeJr/91jL1bKuey23kZ1OnTqZ4I4BT2GT09js3bvXhJsMMa9k/Jw95gVVxucCwyWGuwz9+JoKDAxE06ZN8Z///AedO3fO8nb4+v33v/+NkSNH4vvvv7/scus1Yb1urNdKxteQOI/G3PU05kVrzHN7mwqlRAq6Br2Ae760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5qi6Y8eONUfb3bp1qx4sJ447X4c8zPu3336bbr+Lr19xzphnpKPvOX/MDx8+bI4qOXToUNvu3bvN+3/58uVtr776qp7mThpz/h/OMecRdffv32/77bffbDVr1jRHWpVr34fiWHPMLRxjfoYYPny4eQ+dMmWKzcPDw7ZgwQKbMymUykeTJ082oQjfxHl4zNWrV+fn5hQpfNFltnz22Wdp6/ADyxNPPGErXbq0efHdeeedZgdKnBtKadyd46effrI1atTIvNHXq1fP9tFHH6W7PCUlxTZmzBhbhQoVzDqdO3c2O1Ry9aKjo81zm/+P+/r6msNEjxo1ypaYmKhxz0N//PFHpv+f8wNYbp/bZ86cMSGUv7+/LTAw0Pbggw/qw3IR2V/ie4z1XLDMnj3bVqdOHbM+D239888/58NWF69xr1q1aqavU36gFOeMeUYKpVwz5itXrrS1adPGvN/wfZ9fCCYnJ1/lvRdPVzLmFy5csL300ksmiOK+VlhYmPn8dvbs2Xza+qK1DzVgwAAz5hmv06xZM/P48Dnu+PnZWdz4j3NrsURERERERERERNJTTykREREREREREXE5hVIiIiIiIiIiIuJyCqVERERERERERMTlFEqJiIiIiIiIiIjLKZQSERERERERERGXUyglIiIiIiIiIiIup1BKRERERERERERcTqGUiIiIiIiIiIi4nEIpEZE85ubmhnnz5mlcRUREREREsqFQSkSKlIEDB5pQKOPSrVu3/N40ERERERERceDpeEJEpChgAPXZZ5+lO8/HxyfftkdEREREREQup0opESlyGECFhISkW0qXLm0uY9XU1KlTceutt6JEiRKoUaMGvv3223TX37p1K26++WZzeXBwMAYNGoTY2Nh063z66ado2LChua/Q0FAMHTo03eWnT5/GnXfeCT8/P9SuXRs//vijC/5yERERkfwVERFh9r1ee+21tPNWrlwJb29vLF68OF+3TUQKHoVSIlLsjBkzBr1798bmzZvRv39/9O3bFzt37jSXxcXFoWvXribEWrduHebMmYPff/89XejEUGvIkCEmrGKAxcCpVq1a6e7j5Zdfxj333IMtW7age/fu5n4iIyNd/reKiIiIuFK5cuXMl3cvvfQS1q9fj5iYGNx///1mX6pz5856MEQkHTebzWZLf5aISOHuKfX111/D19c33fkjR440CyulBg8ebIIlS9u2bdGiRQt88MEHmD59Op577jkcOXIEJUuWNJf/8ssv6NmzJ44fP44KFSqgUqVKePDBB/Hqq69mug28j9GjR2PcuHFpQZe/vz9+/fVX9bYSERGRYoFf4PGLvVatWpkv8fhln9opiEhG6iklIkVOp06d0oVOVKZMmbTf27Vrl+4ynv7777/N76yYatq0aVogRR06dEBKSgp2795tAieGUzl909ekSZO033lbgYGBCA8Pv+a/TURERKQwePPNN9GoUSNTdb5hwwYFUiKSKYVSIlLkMATKOJ0ur7DPVG54eXmlO80wi8GWiIiISHGwb98+80Ue938OHjyIxo0b5/cmiUgBpJ5SIlLsrF69+rLT9evXN7/zJ3tNccqdZcWKFXB3d0fdunUREBCAatWqqVGniIiISBaSkpJw3333oU+fPqadwSOPPKKKcRHJlCqlRKTISUxMxMmTJ9Od5+npibJly5rfWUbO/gYdO3bEjBkzsHbtWnzyySfmMjYkHzt2LAYMGGAadPIIMk8++aRp0Ml+UsTz2ZeqfPny5ih+bODJ4IrriYiIiBR3o0aNQlRUFN577z3TV5P9OR966CHMnz8/vzdNRAoYVUqJSJGzYMEChIaGplsYQDkeGW/WrFmm79OXX36Jb775Bg0aNDCX+fn5YeHCheZIeddddx3uuusu0z/q/fffT7s+A6t33nnHNEZv2LAhbrvtNuzZsydf/lYRERGRgmTp0qVmP+mrr74yPTVZbc7fly1bdlnPTxERHX1PRIoV9naaO3cu7rjjjvzeFBERERERkWJNlVIiIiIiIiIiIuJyCqVERERERERERMTl1OhcRIoVm82W35sgIiIiIiIiqpQSEREREREREZH8oOl7IiIiIiIiIiLicgqlRERERERERETE5RRKiYiIiIiIiIiIyymUEhERERERERERl1MoJSIiIiIiIiIiLqdQSkREREREREREXE6hlIiIiIiIiIiIuJxCKRERERERERERgav9P6FP0zjNvKv/AAAAAElFTkSuQmCC", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ "fig, axes = plt.subplots(1, 2, figsize=(12, 4))\n", "\n", @@ -418,17 +564,13 @@ "axes[0].legend()\n", "axes[0].grid(True, alpha=0.3)\n", "\n", - "# Compare learned viscosity to true viscosity.\n", - "# Evaluate the closure Tesseract at several representative flow states.\n", + "# Compare learned viscosity to true viscosity, averaged over training ICs.\n", "nu_samples = []\n", "with torch.no_grad():\n", " for ic in train_ics:\n", " dudx = torch.zeros_like(ic)\n", " dudx[1:-1] = (ic[2:] - ic[:-2]) / (2 * DX)\n", - " nu_i = apply_tesseract(closure_tess, {\"u\": ic, \"dudx\": dudx, \"x\": X, **params})[\n", - " \"nu\"\n", - " ]\n", - " nu_samples.append(nu_i)\n", + " nu_samples.append(closure(ic, dudx, X))\n", "nu_samples = torch.stack(nu_samples)\n", "nu_mean = nu_samples.mean(dim=0)\n", "nu_std = nu_samples.std(dim=0)\n", @@ -459,7 +601,7 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 4. Baselines: why the hybrid model wins\n", + "## Step 5: Baselines\n", "\n", "We compare three approaches:\n", "\n", @@ -472,85 +614,93 @@ }, { "cell_type": "code", - "execution_count": null, + "execution_count": 10, "metadata": {}, - "outputs": [], + "outputs": [ + { + "name": "stdout", + "output_type": "stream", + "text": [ + "Constant viscosity test MSE: 8.7019e-06\n", + "\n", + "Training direct ML baseline...\n", + " Epoch 0: train = 3.3253e-01, test = 2.0101e-01\n", + " Epoch 20: train = 3.6253e-02, test = 1.0957e-01\n", + " Epoch 40: train = 4.0090e-03, test = 7.0837e-02\n", + " Epoch 60: train = 1.2066e-03, test = 7.3164e-02\n", + " Epoch 80: train = 6.3926e-04, test = 7.5776e-02\n", + "\n", + "Direct ML test MSE: 7.6441e-02\n", + "Learned closure test MSE: 3.7934e-06\n", + "\n", + "Model Test MSE\n", + "----------------------------------------\n", + "Constant viscosity 8.7019e-06\n", + "Direct ML 7.6441e-02\n", + "Learned closure 3.7934e-06\n" + ] + } + ], "source": [ - "# --- Baseline 1: Constant viscosity ---\n", - "nu_const = NU_0 * torch.ones(\n", - " N, dtype=torch.float64\n", - ") # Wrong: uniform instead of spatially varying\n", + "# --- Baseline 1: Constant (wrong) viscosity ---\n", + "nu_const = NU_0 * torch.ones(N)\n", "with torch.no_grad():\n", " const_preds_test = torch.stack(\n", - " [burgers_reference(ic, nu_const, DT, N_STEPS)[0] for ic in test_ics]\n", + " [burgers_reference(ic, nu_const, DT, N_STEPS) for ic in test_ics]\n", " )\n", "const_mse = float(torch.mean((const_preds_test - test_targets) ** 2))\n", "print(f\"Constant viscosity test MSE: {const_mse:.4e}\")\n", "\n", "\n", "# --- Baseline 2: Direct ML (MLP mapping u0 -> u_final, no physics) ---\n", - "def init_direct_params(seed):\n", - " \"\"\"Larger MLP for direct prediction (more capacity since no physics inductive bias).\"\"\"\n", - " rng = torch.Generator().manual_seed(seed)\n", - " return {\n", - " \"w1\": torch.randn(N, 128, dtype=torch.float64, generator=rng)\n", - " * np.sqrt(2.0 / N),\n", - " \"b1\": torch.zeros(128, dtype=torch.float64),\n", - " \"w2\": torch.randn(128, 128, dtype=torch.float64, generator=rng)\n", - " * np.sqrt(2.0 / 128),\n", - " \"b2\": torch.zeros(128, dtype=torch.float64),\n", - " \"w3\": torch.randn(128, 64, dtype=torch.float64, generator=rng)\n", - " * np.sqrt(2.0 / 128),\n", - " \"b3\": torch.zeros(64, dtype=torch.float64),\n", - " \"w4\": torch.randn(64, N, dtype=torch.float64, generator=rng)\n", - " * np.sqrt(2.0 / 64),\n", - " \"b4\": torch.zeros(N, dtype=torch.float64),\n", - " }\n", - "\n", - "\n", - "def direct_predict(params, u0):\n", - " \"\"\"Pure ML: MLP maps u0 directly to u_final.\"\"\"\n", - " h = torch.tanh(u0 @ params[\"w1\"] + params[\"b1\"])\n", - " h = torch.tanh(h @ params[\"w2\"] + params[\"b2\"])\n", - " h = torch.tanh(h @ params[\"w3\"] + params[\"b3\"])\n", - " return h @ params[\"w4\"] + params[\"b4\"]\n", - "\n", - "\n", - "def direct_loss(params, ics, targets):\n", - " preds = torch.stack([direct_predict(params, ics[i]) for i in range(ics.shape[0])])\n", + "class DirectNet(nn.Module):\n", + " \"\"\"Pure ML: maps the whole field u0 directly to u_final.\"\"\"\n", + "\n", + " def __init__(self, n=N, hidden=128):\n", + " super().__init__()\n", + " self.net = nn.Sequential(\n", + " nn.Linear(n, hidden),\n", + " nn.Tanh(),\n", + " nn.Linear(hidden, hidden),\n", + " nn.Tanh(),\n", + " nn.Linear(hidden, 64),\n", + " nn.Tanh(),\n", + " nn.Linear(64, n),\n", + " )\n", + "\n", + " def forward(self, u0):\n", + " return self.net(u0)\n", + "\n", + "\n", + "def direct_loss(model, ics, targets):\n", + " preds = model(ics)\n", " return torch.mean((preds - targets) ** 2)\n", "\n", "\n", - "# Train the direct ML baseline\n", - "direct_params = init_direct_params(seed=2)\n", - "direct_train_params = {\n", - " k: v.clone().requires_grad_(True) for k, v in direct_params.items()\n", - "}\n", - "direct_opt = torch.optim.Adam(direct_train_params.values(), lr=1e-3)\n", + "torch.manual_seed(2)\n", + "direct = DirectNet()\n", + "direct_opt = torch.optim.Adam(direct.parameters(), lr=1e-3)\n", "\n", "print(\"\\nTraining direct ML baseline...\")\n", - "direct_losses = []\n", - "for epoch in range(500):\n", + "for epoch in range(DIRECT_EPOCHS):\n", " direct_opt.zero_grad()\n", - " dl = direct_loss(direct_train_params, train_ics, train_targets)\n", + " dl = direct_loss(direct, train_ics, train_targets)\n", " dl.backward()\n", " direct_opt.step()\n", - " direct_losses.append(float(dl))\n", - " if epoch % 100 == 0:\n", + " if epoch % 20 == 0:\n", " with torch.no_grad():\n", - " test_dl = float(direct_loss(direct_train_params, test_ics, test_targets))\n", + " test_dl = float(direct_loss(direct, test_ics, test_targets))\n", " print(\n", - " f\" Epoch {epoch:4d}: train = {direct_losses[-1]:.4e}, test = {test_dl:.4e}\"\n", + " f\" Epoch {epoch:4d}: train = {float(dl.detach()):.4e}, test = {test_dl:.4e}\"\n", " )\n", "\n", - "direct_params = {k: v.detach().clone() for k, v in direct_train_params.items()}\n", "with torch.no_grad():\n", - " direct_test_mse = float(direct_loss(direct_params, test_ics, test_targets))\n", + " direct_test_mse = float(direct_loss(direct, test_ics, test_targets))\n", "print(f\"\\nDirect ML test MSE: {direct_test_mse:.4e}\")\n", "\n", "# --- Learned closure (already trained above) ---\n", "with torch.no_grad():\n", - " learned_test_mse = float(loss_batch(params, test_ics, test_targets))\n", + " learned_test_mse = float(loss_batch(closure, test_ics, test_targets))\n", "print(f\"Learned closure test MSE: {learned_test_mse:.4e}\")\n", "\n", "print(f\"\\n{'Model':<25s} {'Test MSE':>12s}\")\n", @@ -564,33 +714,43 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 5. Solution comparison on test data\n", + "## Step 6: Solution comparison on test data\n", "\n", - "The key visual: for an unseen initial condition, compare the three models' predictions against the ground truth." + "For unseen initial conditions, we compare the three models' predictions against the ground truth." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 11, "metadata": {}, - "outputs": [], + "outputs": [ + { + "data": { + "image/png": "iVBORw0KGgoAAAANSUhEUgAABKUAAAG+CAYAAABClLe0AAAAOnRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjEwLjksIGh0dHBzOi8vbWF0cGxvdGxpYi5vcmcvJkbTWQAAAAlwSFlzAAAPYQAAD2EBqD+naQABAABJREFUeJzs3QdYU1cbB/A/ICAgIA4QBUVx771n3Xu17r2qVq217ll3nXVr3bturbbuvffeCzcuRED2+p738IWCDBEhJPD/+eQxJDc3Jyc3Nzfvfc97DEJDQ0NBRERERERERESkRYbafDIiIiIiIiIiIiLBoBQREREREREREWkdg1JERERERERERKR1DEoREREREREREZHWMShFRERERERERERax6AUERERERERERFpHYNSRERERERERESkdQxKERERERERERGR1jEoRUREREREREREWsegFBFRMnX06FEYGBioi5OTU6I/X9WqVcOfb+XKlYn+fMlFTP3WqVOn8Nt/++23ZLGNEGnItq7Z9uQz8C3k86FZl3xukmK/pY3Pa1LRvC65PHnyJPx22Wdobpd9SVzJOiKuk4iIUrZUSd0AIqKU6uPHj5g6dSp27dqFx48fIygoCDY2NsiUKRMKFSqEWrVqoX379tAVEX9o9e/fH2nTpk3S9tCXyQ9FzY/FokWLokmTJuw2SlG439K+q1evYseOHeGBq68NFBIRUcrCoBQRURJwd3dH6dKl8fDhw0i3v3nzRl2uXbsGFxcXnQpKjR07Nvy6/Mj4PCg1d+5ceHh4qOu5c+fWevuSmxEjRqBbt27qetasWeO1DglIad63jh07RglKFStWDCdOnFDXU6dO/c1tJoqrevXqhW971tbW39RxXbp0QY0aNdR1Ozu7SPdxv5V4tmzZAj8/P3VdTqREDEpp+r1KlSpRglL29vbh7z0RERGDUkRESWD27NnhASkJOIwaNQo5cuSAr68vbt26hZ07d8LQUL9GWEf8UZJcffr0CWnSpNHKc+XKlUtdEpMEAypWrJioz0EUHVtbW3VJCLIPjW/gNiXstxJLyZIl4/U4U1NT7neIiCicfv3iISJKJs6fPx9+/ddff1UZMd999x3q16+PwYMH4+TJk/j333+jPM7Lywvjx49H8eLFYWlpqQ7uJZjVvXt3PHjw4JvrCEVXm0VTKyWi7NmzR6nDEltt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ui9Llizq8U+ePFHrknbJY7/77jtVU+rp06eYO3euOvi5dOmSClLJdMZS6Pz69etqSKA8nxRHv3HjhsrIknXFNi2xrHPNmjXquhxQSXBME8zLli0b2rdvzy2TiIgomeIxVPyOoeSYUY4PX758qQqcyzFcRHJcWa5cOS29i0QpVCLP7kdEFO/pjDUWL14cWqJEiVAzM7NQS0vL0EKFCoUOHjw49NWrV+r+y5cvh7Zu3To0a9asoaampqG2trahDRo0CL148WKk9Zw+fVqtx8TERD3PmDFjYm2Pm5tbaJ8+fUKzZMmiHuPg4BDasWPH0Pfv36v7Q0JCQidNmhSaLVs29bzFihUL/eeff9QycpvGli1bQmvVqqXaJeuRdv7444+hrq6ukZ7Py8srdNiwYaE5c+ZUy2XIkCG0fPnyodOnTw8NCAiIta1HjhxRrym6S5UqVeLQ+0RERKSveAwVv2MoFxeXGI+f5CLHdESUuJgpRUREREREREREWseaUkREREREREREpHUMShERERERERERkdYxKEVERERERERERFrHoBQREREREREREWkdg1JERERERERERKR1DEoREREREREREZHWpdL+UyY/ISEhePXqFSwtLWFgYJDUzSEiIqIEFBoaCi8vL2TOnBmGhjyfl1B4/ERERJR8xfX4iUGpBCABKUdHx4RYFREREemo58+fw8HBIambkWzw+ImIiCj5+9LxE4NSCUAypDSdbWVlhYQ+i/ju3TtkzJiRZ2e1gP2tXexv9ndyxu07+fS3p6enOvmk+b6nhMHjp+SD+zv2d3LG7Zv9nZyF6MDxE4NSCUAzZE8CUokRlPLz81Pr5ZCBxMf+1i72N/s7OeP2nfz6m0P0E6c/efyk/7i/Y38nZ9y+2d/JWYgOHD+xMAIREREREREREWkdg1JERERERERERKR1DEoREREREREREZHWMShFRERERERERERax6AUERERERERERFpHYNSRERERERERESkdQxKERERERERERGR1jEoRUREREREREREWsegFBERERERERERaR2DUkREREREREREpHUMShERERERUbQCAgAPDyAkhB1EREQJL1UirJOIiIj0WGhoKG68vYFCtoVgYGCQ1M0hIi3w9QXuXPXH651n4XPgIEzevYSp9wdY+n+AWfAnWDoZIbVTRpg4OMDHsTQeONRB7u8c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", + "text/plain": [ + "
" + ] + }, + "metadata": {}, + "output_type": "display_data" + } + ], "source": [ - "fig, axes = plt.subplots(2, 2, figsize=(12, 9))\n", + "n_show = min(4, N_TEST)\n", + "ncols = 2 if n_show > 1 else 1\n", + "nrows = (n_show + ncols - 1) // ncols\n", + "fig, axes = plt.subplots(nrows, ncols, figsize=(6 * ncols, 4.5 * nrows), squeeze=False)\n", "\n", "with torch.no_grad():\n", - " for idx in range(4):\n", - " ax = axes[idx // 2, idx % 2]\n", + " direct_preds = direct(test_ics)\n", + " for idx in range(n_show):\n", + " ax = axes[idx // ncols][idx % ncols]\n", " ic = test_ics[idx]\n", " target = test_targets[idx]\n", "\n", - " # Constant viscosity prediction\n", - " const_pred = burgers_reference(ic, nu_const, DT, N_STEPS)[0]\n", - "\n", - " # Direct ML prediction\n", - " direct_pred = direct_predict(direct_params, ic)\n", - "\n", - " # Learned closure prediction (outer loop calling both Tesseracts)\n", - " learned_pred = solve_with_closure(ic, params, DT, N_STEPS)\n", + " const_pred = burgers_reference(ic, nu_const, DT, N_STEPS)\n", + " direct_pred = direct_preds[idx]\n", + " learned_pred = solve_with_closure(ic, closure, DT, N_STEPS)\n", "\n", " ax.plot(x_np, target.numpy(), \"k-\", linewidth=2, label=\"Ground truth\")\n", " ax.plot(\n", @@ -625,96 +785,45 @@ "cell_type": "markdown", "metadata": {}, "source": [ - "## 6. Modularity: swap the closure *or the solver*\n", + "## Tear down the solver\n", "\n", - "The solver and closure are independent Tesseracts with a clean contract: the solver takes `(u, nu_field, dt)` and returns `u_next`. The closure takes `(u, dudx, x, weights)` and returns `nu`.\n", - "\n", - "This means you can:\n", - "- **Swap the closure**: replace the neural network architecture without touching the solver\n", - "- **Swap the solver**: replace the PyTorch solver with a Fortran solver (differentiated by Enzyme or a hand-written adjoint) without touching the closure or training loop\n", - "\n", - "Here we demonstrate closure swapping — training a different closure initialization against the same solver." + "Stop the solver container to free resources." ] }, { "cell_type": "code", - "execution_count": null, + "execution_count": 12, "metadata": {}, "outputs": [], "source": [ - "# Swap: re-initialize with a different random seed (different starting point).\n", - "# In production with Tesseract containers, this would mean pointing the solver\n", - "# at a completely different closure Tesseract URL. The solver code is untouched.\n", - "\n", - "swapped_params = init_params(seed=99)\n", - "swapped_train = {k: v.clone().requires_grad_(True) for k, v in swapped_params.items()}\n", - "swapped_opt = torch.optim.Adam(swapped_train.values(), lr=3e-3)\n", - "\n", - "print(\"Training a different closure (same solver, different initialization)...\")\n", - "for _epoch in range(500):\n", - " swapped_opt.zero_grad()\n", - " sl = loss_batch(swapped_train, train_ics, train_targets)\n", - " sl.backward()\n", - " swapped_opt.step()\n", - "\n", - "swapped_params = {k: v.detach().clone() for k, v in swapped_train.items()}\n", - "with torch.no_grad():\n", - " swapped_test_mse = float(loss_batch(swapped_params, test_ics, test_targets))\n", - "print(f\" Swapped closure test MSE: {swapped_test_mse:.4e}\")\n", - "print(f\" Original closure test MSE: {learned_test_mse:.4e}\")\n", - "print(\"\\nSolver code: unchanged. Only the closure was swapped.\")\n", - "\n", - "# Compare the two learned viscosity profiles\n", - "with torch.no_grad():\n", - " dudx0 = torch.gradient(train_ics[0], spacing=(DX,))[0]\n", - " nu_orig = apply_tesseract(\n", - " closure_tess,\n", - " {\"u\": train_ics[0], \"dudx\": dudx0, \"x\": X, **params},\n", - " )[\"nu\"]\n", - " nu_swap = apply_tesseract(\n", - " closure_tess,\n", - " {\"u\": train_ics[0], \"dudx\": dudx0, \"x\": X, **swapped_params},\n", - " )[\"nu\"]\n", - "\n", - "fig, ax = plt.subplots(figsize=(8, 4))\n", - "ax.plot(x_np, nu_true.numpy(), \"k-\", linewidth=2, label=\"True\")\n", - "ax.plot(x_np, nu_orig.numpy(), \"r--\", linewidth=1.5, label=\"Closure A\")\n", - "ax.plot(x_np, nu_swap.numpy(), \"b:\", linewidth=1.5, label=\"Closure B (swapped)\")\n", - "ax.set_xlabel(\"x\")\n", - "ax.set_ylabel(r\"$\\nu$\")\n", - "ax.set_title(\"Two different closures, same solver\")\n", - "ax.legend()\n", - "ax.set_ylim(bottom=0)\n", - "ax.grid(True, alpha=0.3)\n", - "plt.tight_layout()\n", - "plt.show()" + "# Tear down Tesseract after use to prevent resource leaks\n", + "solver_tess.teardown()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ - "## Summary\n", + "## Takeaways\n", "\n", - "| What | How |\n", - "|---|---|\n", - "| Two independent Tesseracts | Solver (single-timestep PDE) + closure (neural viscosity) |\n", - "| Composed in an outer loop | `apply_tesseract(closure, ...)` then `apply_tesseract(solver, ...)` at each timestep |\n", - "| End-to-end gradients | `torch.autograd` dispatches VJP through both Tesseracts automatically |\n", - "| Gradient correctness | Validated against finite differences |\n", - "| Learned closure beats baselines | Lower test MSE than constant viscosity or direct ML |\n", - "| Modular swapping | Change either Tesseract without touching the other |\n", + "In this tutorial, we trained a neural viscosity closure end-to-end through a containerized Burgers' equation solver. Here are the key points:\n", "\n", - "### Line of sight to production\n", + "1. **Containerized solver, native ML.** The Burgers' solver runs in a Docker container served as a Tesseract, while the neural viscosity closure is a plain in-process `torch.nn.Module`. The two are composed in an outer time-stepping loop: `closure(...)` then `apply_tesseract(solver, ...)` over HTTP at each step.\n", "\n", - "The solver Tesseract's interface — `(u, nu_field, dt) → u_next` with VJP — is not PyTorch-specific. A Fortran solver differentiated by [Enzyme](https://enzyme.mit.edu/) (see the `enzyme_thermal_2d` demo) or with a hand-written discrete adjoint could implement the same contract. The training loop and closure Tesseract would be **identical**. This is the core value proposition: closure researchers get access to a library of differentiable solvers without learning each solver's internals.\n", + "2. **End-to-end gradients through the container.** Because Tesseract-Torch registers the served solver as a PyTorch autograd function, `loss.backward()` dispatches the solver's VJP over HTTP and flows the gradient into the network -- no hand-coded plumbing. We validated these gradients against finite differences.\n", + "\n", + "3. **Physics structure beats both extremes.** The learned closure achieves lower test MSE than either a constant-viscosity physics model or a pure ML model, recovering the true viscosity profile from solution data alone.\n", + "\n", + "4. **Composability across a container boundary.** The closure (plain torch) and the solver (a different image exposing the same $(u, \\nu, dt) \\to u_\\text{next}$ contract) can be swapped independently. The training loop is identical regardless of what language the solver is written in or how its adjoint is produced.\n", "\n", "### What's next\n", "\n", - "- **Containerized deployment**: Build each Tesseract as a Docker image. The outer loop calls both over HTTP via `apply_tesseract` — same code, real container isolation.\n", - "- **Legacy solver integration**: Wrap a Fortran/C++ solver with adjoint as a Tesseract. The closure training loop above works unchanged.\n", - "- **Scale up**: Larger grids, 2D/3D problems, more complex closures (e.g., convolutional, attention-based).\n", - "- **Real applications**: Replace the Burgers' equation with a turbulence model, climate sub-grid scheme, or materials constitutive law." + "- **Wrap a legacy solver.** Replace the Burgers' Tesseract with a Fortran/C++ solver that exposes an adjoint (for example via [Enzyme](https://enzyme.mit.edu/)). The closure training loop above works unchanged -- only the image name changes.\n", + "- **Scale up.** Move to larger grids, 2D/3D problems, or richer closures (convolutional, attention-based). Batch multiple initial conditions per request to amortize HTTP overhead.\n", + "- **Tackle a real closure.** Swap the Burgers' equation for a turbulence model, a climate sub-grid scheme, or a materials constitutive law.\n", + "- **Explore other demos.** See the [CFD optimization](cfd-optimization.ipynb), [FEM shape optimization](fem-shape-optimization.ipynb), and [data assimilation](data-assimilation.ipynb) demos for the JAX side of the same composition pattern.\n", + "\n", + "Questions? Feedback? Please reach out through the [Tesseract Community Forum](https://si-tesseract.discourse.group/)." ] } ], @@ -725,8 +834,7 @@ "name": "python3" }, "language_info": { - "name": "python", - "version": "3.12.0" + "name": "python" } }, "nbformat": 4, diff --git a/demo/learned-closure/neural_viscosity/tesseract_api.py b/demo/learned-closure/neural_viscosity/tesseract_api.py deleted file mode 100644 index 6765cdb99..000000000 --- a/demo/learned-closure/neural_viscosity/tesseract_api.py +++ /dev/null @@ -1,171 +0,0 @@ -# Copyright 2025 Pasteur Labs. All Rights Reserved. -# SPDX-License-Identifier: Apache-2.0 - -"""Neural viscosity closure Tesseract (PyTorch). - -A small MLP that predicts spatially-varying viscosity from local flow features. -Used as a learned closure inside a PDE solver — the solver calls this Tesseract -at every timestep to get the viscosity field, and gradients flow back through -both during training. - -The network weights are passed as explicit inputs (not internal state) so that -an external optimizer can differentiate through the full solver-closure pipeline. -""" - -from typing import Any - -import numpy as np -import torch -from pydantic import BaseModel, Field -from torch.utils._pytree import tree_map - -from tesseract_core.runtime import Array, Differentiable, Float64 -from tesseract_core.runtime.tree_transforms import filter_func, flatten_with_paths - -# Network architecture constants -HIDDEN_DIM = 32 -N_HIDDEN_LAYERS = 2 - -to_tensor = lambda x: ( - torch.tensor(x, dtype=torch.float64) - if isinstance(x, np.generic | np.ndarray) - else x -) - - -class InputSchema(BaseModel): - u: Differentiable[Array[(None,), Float64]] = Field( - description="Velocity field at grid points" - ) - dudx: Differentiable[Array[(None,), Float64]] = Field( - description="Velocity gradient du/dx at grid points" - ) - x: Array[(None,), Float64] = Field(description="Spatial coordinates of grid points") - # Network weights as flat arrays for easy composition - w1: Differentiable[Array[(3, HIDDEN_DIM), Float64]] = Field( - description="First layer weights (3 input features -> hidden)" - ) - b1: Differentiable[Array[(HIDDEN_DIM,), Float64]] = Field( - description="First layer bias" - ) - w2: Differentiable[Array[(HIDDEN_DIM, HIDDEN_DIM), Float64]] = Field( - description="Second layer weights" - ) - b2: Differentiable[Array[(HIDDEN_DIM,), Float64]] = Field( - description="Second layer bias" - ) - w3: Differentiable[Array[(HIDDEN_DIM, 1), Float64]] = Field( - description="Output layer weights (hidden -> 1)" - ) - b3: Differentiable[Array[(1,), Float64]] = Field(description="Output layer bias") - - -class OutputSchema(BaseModel): - nu: Differentiable[Array[(None,), Float64]] = Field( - description="Predicted viscosity at each grid point (always positive)" - ) - - -def evaluate(inputs: dict) -> dict: - """Core differentiable computation — pure torch operations.""" - u = inputs["u"] - dudx = inputs["dudx"] - x = inputs["x"] - - # Stack features: [u, dudx, x] at each grid point -> (N, 3) - features = torch.stack([u, dudx, x], dim=-1) - - # Forward pass through MLP - h = features @ inputs["w1"] + inputs["b1"] - h = torch.tanh(h) - h = h @ inputs["w2"] + inputs["b2"] - h = torch.tanh(h) - out = h @ inputs["w3"] + inputs["b3"] - - # Sigmoid * scale to keep viscosity in a physically reasonable range. - # Range [0, nu_max] prevents CFL violations in the explicit solver. - nu_max = 0.05 - nu = nu_max * torch.sigmoid(out[:, 0]) - - return {"nu": nu} - - -def apply(inputs: InputSchema) -> OutputSchema: - tensor_inputs = tree_map(to_tensor, inputs.model_dump()) - return evaluate(tensor_inputs) - - -def abstract_eval(abstract_inputs: Any) -> Any: - inputs_dict = abstract_inputs.model_dump() - n = inputs_dict["u"]["shape"][0] - return {"nu": {"shape": [n], "dtype": "float64"}} - - -def jacobian_vector_product( - inputs: InputSchema, - jvp_inputs: set[str], - jvp_outputs: set[str], - tangent_vector: dict[str, Any], -): - jvp_inputs = tuple(jvp_inputs) - tangent_vector = {key: tangent_vector[key] for key in jvp_inputs} - - tensor_inputs = tree_map(to_tensor, inputs.model_dump()) - pos_tangent = tree_map(to_tensor, tangent_vector).values() - pos_inputs = flatten_with_paths(tensor_inputs, jvp_inputs).values() - - filtered_pos_eval = filter_func( - evaluate, tensor_inputs, jvp_outputs, input_paths=jvp_inputs - ) - - return torch.func.jvp(filtered_pos_eval, tuple(pos_inputs), tuple(pos_tangent))[1] - - -def vector_jacobian_product( - inputs: InputSchema, - vjp_inputs: set[str], - vjp_outputs: set[str], - cotangent_vector: dict[str, Any], -): - vjp_inputs = tuple(vjp_inputs) - cotangent_vector = {key: cotangent_vector[key] for key in vjp_outputs} - - tensor_inputs = tree_map(to_tensor, inputs.model_dump()) - tensor_cotangent = tree_map(to_tensor, cotangent_vector) - pos_inputs = flatten_with_paths(tensor_inputs, vjp_inputs).values() - - filtered_pos_func = filter_func( - evaluate, tensor_inputs, vjp_outputs, input_paths=vjp_inputs - ) - - _, vjp_func = torch.func.vjp(filtered_pos_func, *pos_inputs) - vjp_vals = vjp_func(tensor_cotangent) - return dict(zip(vjp_inputs, vjp_vals, strict=True)) - - -def jacobian( - inputs: InputSchema, - jac_inputs: set[str], - jac_outputs: set[str], -): - jac_inputs = tuple(jac_inputs) - tensor_inputs = tree_map(to_tensor, inputs.model_dump()) - pos_inputs = flatten_with_paths(tensor_inputs, jac_inputs).values() - - filtered_pos_eval = filter_func( - evaluate, tensor_inputs, jac_outputs, input_paths=jac_inputs - ) - - def filtered_pos_eval_flat(*args): - res = filtered_pos_eval(*args) - return tuple(res[k] for k in jac_outputs) - - jac = torch.autograd.functional.jacobian(filtered_pos_eval_flat, tuple(pos_inputs)) - - jac_dict = {} - for dy, dys in zip(jac_outputs, jac, strict=True): - jac_dict[dy] = {} - for dx, dxs in zip(jac_inputs, dys, strict=True): - jac_dict[dy][dx] = dxs - - return jac_dict diff --git a/demo/learned-closure/neural_viscosity/tesseract_config.yaml b/demo/learned-closure/neural_viscosity/tesseract_config.yaml deleted file mode 100644 index 904e3a449..000000000 --- a/demo/learned-closure/neural_viscosity/tesseract_config.yaml +++ /dev/null @@ -1,6 +0,0 @@ -name: "neural-viscosity" -version: "0.1.0" -description: "Neural network closure that predicts spatially-varying viscosity from local flow features (PyTorch)" - -build_config: - target_platform: "native" diff --git a/demo/learned-closure/neural_viscosity/tesseract_requirements.txt b/demo/learned-closure/neural_viscosity/tesseract_requirements.txt deleted file mode 100644 index f6d69b182..000000000 --- a/demo/learned-closure/neural_viscosity/tesseract_requirements.txt +++ /dev/null @@ -1,2 +0,0 @@ -torch -tesseract-core diff --git a/demo/learned-closure/test_solvers.py b/demo/learned-closure/test_solvers.py index 4ddacb574..859818049 100644 --- a/demo/learned-closure/test_solvers.py +++ b/demo/learned-closure/test_solvers.py @@ -1,62 +1,74 @@ """Smoke tests for the learned closure demo (PyTorch version). -Tests the composition pattern: an outer loop calls the closure Tesseract to get -a viscosity field, then calls the solver Tesseract to step forward. Gradients -flow end-to-end through both Tesseracts via apply_tesseract / torch.autograd. - -This is the same pattern that would work with a Fortran solver Tesseract backed -by Enzyme or a hand-written adjoint — the solver just needs apply + VJP with -the interface (u, nu_field, dt) -> u_next. +Tests the composition pattern: a plain torch.nn closure predicts a viscosity +field, then the solver Tesseract steps the Burgers' equation forward as a +differentiable layer. Gradients flow end-to-end from the loss, through the +solver's VJP (via apply_tesseract / torch.autograd), into the network weights. + +These tests load the solver via ``Tesseract.from_tesseract_api`` (in-process, no +Docker) so they run fast as a local smoke check. The demo notebook itself uses +``Tesseract.from_image`` to serve the solver in a container over HTTP — the same +``apply_tesseract`` call path works either way. This is also the same pattern +that would work with a Fortran solver Tesseract backed by Enzyme or a +hand-written adjoint: the solver just needs apply + VJP with the interface +(u, nu_field, dt) -> u_next. The closure stays ordinary PyTorch. """ import sys -sys.path.insert(0, "neural_viscosity") sys.path.insert(0, "burgers_solver") import burgers_solver.tesseract_api as solver_api -import neural_viscosity.tesseract_api as closure_api import numpy as np import torch +import torch.nn as nn from tesseract_torch import apply_tesseract from tesseract_core import Tesseract -CLOSURE_API_PATH = "neural_viscosity/tesseract_api.py" +torch.set_default_dtype(torch.float64) + SOLVER_API_PATH = "burgers_solver/tesseract_api.py" N = 128 DX = 1.0 / (N - 1) -X_GRID = torch.linspace(0.0, 1.0, N, dtype=torch.float64) +X_GRID = torch.linspace(0.0, 1.0, N) + +class ViscosityNet(nn.Module): + """MLP closure: local flow features (u, du/dx, x) -> viscosity nu.""" -def _make_closure_params(seed=0): - """Initialize random closure network weights.""" - rng = torch.Generator().manual_seed(seed) - w1 = torch.randn(3, 32, dtype=torch.float64, generator=rng) * np.sqrt(2.0 / 3) - b1 = torch.zeros(32, dtype=torch.float64) - w2 = torch.randn(32, 32, dtype=torch.float64, generator=rng) * np.sqrt(2.0 / 32) - b2 = torch.zeros(32, dtype=torch.float64) - w3 = torch.randn(32, 1, dtype=torch.float64, generator=rng) * np.sqrt(2.0 / 32) - b3 = torch.zeros(1, dtype=torch.float64) - return {"w1": w1, "b1": b1, "w2": w2, "b2": b2, "w3": w3, "b3": b3} + def __init__(self, hidden_dim=32, nu_max=0.05): + super().__init__() + self.nu_max = nu_max + self.net = nn.Sequential( + nn.Linear(3, hidden_dim), + nn.Tanh(), + nn.Linear(hidden_dim, hidden_dim), + nn.Tanh(), + nn.Linear(hidden_dim, 1), + ) + + def forward(self, u, dudx, x): + features = torch.stack([u, dudx, x], dim=-1) + out = self.net(features)[:, 0] + return self.nu_max * torch.sigmoid(out) def _make_initial_condition(): """Smooth initial condition: a sine wave.""" - u0 = torch.sin(2 * np.pi * X_GRID) - return u0 + return torch.sin(2 * np.pi * X_GRID) def test_closure_forward(): print("=== Neural viscosity closure forward pass ===") - params = _make_closure_params(seed=0) + torch.manual_seed(0) + closure = ViscosityNet() u0 = _make_initial_condition() dudx = torch.gradient(u0, spacing=(DX,))[0] - inputs = closure_api.InputSchema(u=u0, dudx=dudx, x=X_GRID, **params) - out = closure_api.apply(inputs) - nu = out["nu"] + with torch.no_grad(): + nu = closure(u0, dudx, X_GRID) print(f" Shape: {nu.shape}, range: [{float(nu.min()):.4f}, {float(nu.max()):.4f}]") assert nu.shape == (N,) @@ -67,7 +79,7 @@ def test_closure_forward(): def test_solver_single_step(): print("\n=== Solver single timestep ===") u0 = _make_initial_condition() - nu = torch.full((N,), 0.01, dtype=torch.float64) + nu = torch.full((N,), 0.01) dt = 1e-4 inputs = solver_api.InputSchema(u=u0, nu=nu, dt=dt) @@ -87,13 +99,13 @@ def test_solver_single_step(): def test_solver_gradient(): print("\n=== Solver gradient (VJP w.r.t. nu field) ===") u0 = _make_initial_condition() - nu = torch.full((N,), 0.01, dtype=torch.float64, requires_grad=True) + nu = torch.full((N,), 0.01, requires_grad=True) dt = 1e-4 tensor_inputs = { "u": u0.clone(), "nu": nu, - "dt": torch.tensor(dt, dtype=torch.float64), + "dt": torch.tensor(dt), } out = solver_api.evaluate(tensor_inputs) loss = torch.mean(out["u_next"] ** 2) @@ -108,25 +120,28 @@ def test_solver_gradient(): print(" PASSED") +def _solve_with_closure(u0, closure, solver_tess, dt, n_steps): + u = u0 + for _step in range(n_steps): + dudx = torch.zeros_like(u) + dudx[1:-1] = (u[2:] - u[:-2]) / (2 * DX) + nu = closure(u, dudx, X_GRID) + solver_out = apply_tesseract(solver_tess, {"u": u, "nu": nu, "dt": dt}) + u = solver_out["u_next"] + return u + + def test_composition_forward(): - """Outer loop calling closure + solver via apply_tesseract.""" - print("\n=== Composed forward pass (closure + solver via apply_tesseract) ===") - closure_tess = Tesseract.from_tesseract_api(CLOSURE_API_PATH) + """Outer loop: plain torch closure + solver Tesseract via apply_tesseract.""" + print("\n=== Composed forward pass (closure + solver Tesseract) ===") solver_tess = Tesseract.from_tesseract_api(SOLVER_API_PATH) - params = _make_closure_params(seed=42) - u = _make_initial_condition() - dt = 1e-4 - n_steps = 50 + torch.manual_seed(42) + closure = ViscosityNet() + u0 = _make_initial_condition() - for _step in range(n_steps): - dudx = torch.gradient(u, spacing=(DX,))[0] - closure_out = apply_tesseract( - closure_tess, {"u": u, "dudx": dudx, "x": X_GRID, **params} - ) - nu = closure_out["nu"] - solver_out = apply_tesseract(solver_tess, {"u": u, "nu": nu, "dt": dt}) - u = solver_out["u_next"] + with torch.no_grad(): + u = _solve_with_closure(u0, closure, solver_tess, dt=1e-4, n_steps=50) print(f" Shape: {u.shape}") print(f" Range: [{float(u.min()):.4f}, {float(u.max()):.4f}]") @@ -136,47 +151,40 @@ def test_composition_forward(): def test_composition_gradient(): - """End-to-end gradient through solver + closure via apply_tesseract.""" - print("\n=== End-to-end gradient (closure + solver via apply_tesseract) ===") - closure_tess = Tesseract.from_tesseract_api(CLOSURE_API_PATH) + """End-to-end gradient: loss -> solver VJP -> network weights.""" + print("\n=== End-to-end gradient (closure + solver Tesseract) ===") solver_tess = Tesseract.from_tesseract_api(SOLVER_API_PATH) - params = _make_closure_params(seed=42) + torch.manual_seed(42) + closure = ViscosityNet() u0 = _make_initial_condition() target = 0.9 * u0 - dt = 1e-4 n_steps = 20 - # Make w1 require grad for end-to-end differentiation - w1 = params["w1"].clone().requires_grad_(True) - - def run_forward(w1_val): - u = u0.clone() - p = {**params, "w1": w1_val} - for _step in range(n_steps): - dudx = torch.gradient(u, spacing=(DX,))[0] - closure_out = apply_tesseract( - closure_tess, {"u": u, "dudx": dudx, "x": X_GRID, **p} - ) - nu = closure_out["nu"] - solver_out = apply_tesseract(solver_tess, {"u": u, "nu": nu, "dt": dt}) - u = solver_out["u_next"] + def run_forward(): + u = _solve_with_closure( + u0.clone(), closure, solver_tess, dt=1e-4, n_steps=n_steps + ) return torch.mean((u - target) ** 2) - # AD gradient - loss = run_forward(w1) - (grad_ad,) = torch.autograd.grad(loss, w1) - - # Finite difference check on one element - eps = 1e-5 + # AD gradient on one weight element of the first layer + closure.zero_grad() + loss = run_forward() + loss.backward() + w = closure.net[0].weight idx = (0, 0) - ad_val = float(grad_ad[idx]) + ad_val = float(w.grad[idx]) - w1_plus = w1.detach().clone() - w1_plus[idx] += eps - w1_minus = w1.detach().clone() - w1_minus[idx] -= eps - fd = (float(run_forward(w1_plus)) - float(run_forward(w1_minus))) / (2 * eps) + # Finite difference check on the same element + eps = 1e-5 + with torch.no_grad(): + orig = w[idx].item() + w[idx] = orig + eps + l_plus = float(run_forward()) + w[idx] = orig - eps + l_minus = float(run_forward()) + w[idx] = orig + fd = (l_plus - l_minus) / (2 * eps) rel_err = abs(ad_val - fd) / (abs(fd) + 1e-30) print(f" AD: {ad_val:.6e}, FD: {fd:.6e}, Rel error: {rel_err:.2e}") diff --git a/docs/_templates/page.html b/docs/_templates/page.html index 2c884bb0b..8f7b0852a 100644 --- a/docs/_templates/page.html +++ b/docs/_templates/page.html @@ -34,6 +34,7 @@ @@ -45,6 +46,7 @@ diff --git a/docs/content/demo/demo.md b/docs/content/demo/demo.md index 8b05b53c6..d264a909e 100644 --- a/docs/content/demo/demo.md +++ b/docs/content/demo/demo.md @@ -10,6 +10,7 @@ data-assimilation.ipynb lorenz_tesseract.md cfd-optimization.ipynb fem-shape-optimization.ipynb +learned-closure.ipynb JAX Rosenbrock Minimization PyTorch Rosenbrock Minimization JAX RBF Fitting @@ -43,7 +44,7 @@ Detailed implementation of the JAX-based Lorenz-96 solver Tesseract used in the ## Simulation & design optimization demos -End-to-end differentiable optimization through physics simulators, using Tesseract-JAX to compose Tesseracts with JAX code. +End-to-end differentiable optimization through physics simulators, composing Tesseracts with JAX or PyTorch code via Tesseract-JAX and Tesseract-Torch. ::::{grid} 2 :gutter: 2 @@ -58,6 +59,11 @@ Optimize the initial velocity field of a 2D Navier-Stokes simulation so its vort Compose a geometry Tesseract (PyVista, finite-difference gradients) with a FEM Tesseract (jax-fem) to optimize structural bar configurations for minimum compliance. ::: +:::{grid-item-card} Learned Closure (PyTorch) +:link: learned-closure.html + +Train a native PyTorch neural viscosity closure end-to-end through a containerized Burgers' equation solver Tesseract used as a differentiable layer — gradients flow from the loss through the solver's VJP, over HTTP, into the network using Tesseract-Torch. +::: :::: diff --git a/docs/index.md b/docs/index.md index c5dc17053..9e42741c5 100644 --- a/docs/index.md +++ b/docs/index.md @@ -75,11 +75,11 @@ Fortran, C++, Julia, JAX, PyTorch, or shell scripts. Your code stays in its native language, Python is the glue. ::: -:::{grid-item-card} JAX native +:::{grid-item-card} JAX & PyTorch native :class-card: feature-card -Every Tesseract becomes a JAX primitive, -with full support for `grad`, `jit`, and `vmap`. +Every Tesseract becomes a JAX primitive or PyTorch operator, +with gradients flowing through `jax.grad` and `torch.autograd` alike. ::: :::{grid-item-card} Run anywhere @@ -210,6 +210,28 @@ jac_fn = jax.jacobian( t.teardown() ``` +::: +:::{tab-item} PyTorch + +```python +import torch +from tesseract_core import Tesseract +from tesseract_torch import apply_tesseract + +t = Tesseract.from_image("my-tesseract") +t.serve() + +x = torch.tensor([3.0], requires_grad=True) + +out = apply_tesseract(t, {"x": x}) +# out["y"] => tensor([9.0]) + +out["y"].sum().backward() +# x.grad => tensor([6.0]) (via the Tesseract's VJP endpoint) + +t.teardown() +``` + ::: :::: @@ -263,6 +285,17 @@ Compose a geometry Tesseract with a FEM solver Tesseract for end-to-end parametric structural optimization. ::: +:::{grid-item-card} Learned Closure (PyTorch) +:link: content/demo/learned-closure +:link-type: doc +:class-card: demo-card +:img-top: static/demo-learned-closure.svg +:class-img-top: demo-schematic invert-on-dark + +Train a neural viscosity closure end-to-end through a Burgers' equation +solver, with PyTorch gradients flowing through both via Tesseract-Torch. +::: + :::: :::{div} landing-cta @@ -278,7 +311,7 @@ parametric structural optimization. Tesseract Core is the foundation. Additional packages extend its capabilities. ::: -::::{grid} 1 1 3 3 +::::{grid} 1 1 2 2 :gutter: 3 :::{grid-item-card} Tesseract Core @@ -298,6 +331,14 @@ Embed Tesseracts as JAX primitives. Fully compatible with `jit`, `vmap`, and `grad`. ::: +:::{grid-item-card} Tesseract-Torch +:link: https://github.com/pasteurlabs/tesseract-torch +:class-card: ecosystem-card + +Embed Tesseracts as PyTorch operators. Gradients flow through with +`torch.autograd`. +::: + :::{grid-item-card} Tesseract-Streamlit :link: https://github.com/pasteurlabs/tesseract-streamlit :class-card: ecosystem-card From a2aea5f8b2dbccd5a13fbdc998e2eda09c01fa20 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Dion=20H=C3=A4fner?= Date: Wed, 24 Jun 2026 16:48:39 +0200 Subject: [PATCH 4/4] add missing svg --- docs/static/demo-learned-closure.svg | 56 ++++++++++++++++++++++++++++ 1 file changed, 56 insertions(+) create mode 100644 docs/static/demo-learned-closure.svg diff --git a/docs/static/demo-learned-closure.svg b/docs/static/demo-learned-closure.svg new file mode 100644 index 000000000..2a869bc6c --- /dev/null +++ b/docs/static/demo-learned-closure.svg @@ -0,0 +1,56 @@ + + + + + + + + flow u + + + + + + + + + + + + + + + + + + + + + neural ν + + + + + ν + + + + + + + Burgers + + + + + + + + loss + vs data + + + + + ∇ through solver → network +