Skip to content

Commit ea1e728

Browse files
committed
Merge branch 'main' into ako/compression
2 parents 724fe15 + dd88dfd commit ea1e728

27 files changed

Lines changed: 7537 additions & 62 deletions

.github/codecov.yml

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
1+
codecov:
2+
require_ci_to_pass: false
3+
notify:
4+
wait_for_ci: true
15
coverage:
26
status:
37
project:
48
default:
5-
informational: true
9+
target: auto
10+
threshold: 0%
11+
informational: false
612
only_pulls: false
713
patch: off
814
comment:

.github/pull_request_template.md

Lines changed: 5 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,14 @@
1-
<!--
2-
Please use a PR title that conforms to *conventional commits*: "<commit_type>: Describe your change"; for example: "fix: prevent race condition". Some other commit types are: fix, feat, ci, doc, refactor...
3-
For a full list of commit types visit https://www.conventionalcommits.org/en/v1.0.0/
4-
-->
1+
> _Remember that PR titles become commit messages after merging, so keep them informative for other devs and users. As a rule of thumb, PR titles should be able to double as a changelog entry with no further clarification needed._
2+
> _Please use a PR title that conforms to *conventional commits*: `<commit_type>: <description>`, for example: "fix: prevent race condition when using data adapter". Some other commit types are `fix`, `feat`, `ci`, `doc`, `refactor`... For a full list of commit types visit https://www.conventionalcommits.org/en/v1.0.0/ **Then delete this text.**_
53
64
#### Relevant issue or PR
75

8-
<!-- If the changes resolve an issue or follow some other PR, link to them here. Only link something if it is directly relevant. -->
6+
> _If the changes resolve an issue or follow some other PR, link to them here. Only link something if it is directly relevant. **Then delete this text.**_
97
108
#### Description of changes
119

12-
<!-- Add a high-level description of changes, focusing on the *what* and *why*. -->
10+
> _Explain *why* this change exists and *what* it does at a high level — write for a reviewer seeing it for the first time, and for someone reading it a year from now to understand the motivation. Give the context the diff can't: the problem, the approach, and any trade-offs or alternatives considered. Don't restate what the code already shows or pad with detail. **Then delete this text.**_
1311
1412
#### Testing done
1513

16-
<!-- Describe how the changes were tested; e.g., "CI passes", "Tested manually in stagingrepo#123", screenshots of a terminal session that verify the changes, or any other evidence of testing the changes. -->
14+
> _Describe how you convinced yourself the change works, so a reviewer can judge whether the evidence is enough; e.g., "CI passes", "Tested manually in stagingrepo#123", screenshots of a terminal session, or any other evidence. Note anything you couldn't test and why. **Then delete this text.**_

.github/workflows/build_docs.yml

Lines changed: 0 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -34,13 +34,6 @@ jobs:
3434
run: |
3535
uv sync --extra docs --frozen
3636
37-
# TEMPORARY: sphinx-click cannot introspect typer's vendored click (typer
38-
# >= 0.26), which breaks the CLI reference pages. Pin to the last real-click
39-
# typer for the docs build only (does not affect the shipped package) until
40-
# we settle on a long-term docs fix. See the docs CLI reference pages.
41-
- name: Pin typer < 0.26 for docs build (sphinx-click compatibility)
42-
run: uv pip install 'typer<0.26'
43-
4437
- name: Build docs
4538
working-directory: docs
4639
run: |

.github/workflows/run_tests.yml

Lines changed: 0 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -86,31 +86,12 @@ jobs:
8686
- name: Set up Git repository
8787
uses: actions/checkout@v7
8888

89-
- name: Get all changed files
90-
id: changed-files
91-
uses: tj-actions/changed-files@9426d40962ed5378910ee2e21d5f8c6fcbf2dd96 # v47
92-
with:
93-
files: |
94-
tesseract_core/**
95-
tests/endtoend_tests/**
96-
demo/**
97-
examples/**
98-
production.uv.lock
99-
.github/workflows/run_tests.yml
100-
10189
- name: Get available test matrix
10290
id: get-matrix
10391
env:
10492
EXAMPLES_DIR: examples
10593
DEMO_DIR: demo
10694
run: |
107-
# if no changed files in the current PR, skip E2E tests
108-
if [[ "${{ steps.changed-files.outputs.any_changed }}" != "true" && ${{ github.event_name == 'pull_request' }} ]]; then
109-
echo "No changed files detected, skipping E2E tests."
110-
echo "matrix=[]" >> $GITHUB_OUTPUT
111-
echo "demos=[]" >> $GITHUB_OUTPUT
112-
exit 0
113-
fi
11495
# get JSON array of directories in EXAMPLES_DIR, except ones that start with _
11596
subjobs=$(
11697
find "${{ env.EXAMPLES_DIR }}" -type d -maxdepth 1 -mindepth 1 -exec basename {} \; \

.gitignore

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -153,3 +153,6 @@ logs/
153153

154154
# Jupyter execution artifacts
155155
jupyter_execute/
156+
157+
# Ruff cache
158+
.ruff_cache/

.pre-commit-config.yaml

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ repos:
66
additional_dependencies: [--isolated]
77
args: ["--maxkb=2000"]
88
# Add exceptions here, as a regex
9-
exclude: ""
9+
# exclude: ""
1010

1111
- id: check-json
1212
additional_dependencies: [--isolated]
@@ -62,7 +62,7 @@ repos:
6262
mv uv.lock.bak uv.lock; \
6363
exit $rc;
6464
'
65-
additional_dependencies: [uv==0.6.11, --isolated]
65+
additional_dependencies: [uv==0.11.21, --isolated]
6666

6767
# Update requirements.txt after production.uv.lock changes
6868
- id: update-requirements
@@ -80,7 +80,7 @@ repos:
8080
mv uv.lock.bak uv.lock; \
8181
exit $rc;
8282
'
83-
additional_dependencies: [uv==0.6.11, --isolated]
83+
additional_dependencies: [uv==0.11.21, --isolated]
8484

8585
- repo: https://github.com/pasteurlabs/mirrors-prettier
8686
rev: v3.7.4 # Use the latest version

AGENTS.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -40,4 +40,5 @@ Each of these is a separate repository/Python package.
4040

4141
- **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.
4242
- **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.
43+
- **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.
4344
- **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`).

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -122,6 +122,7 @@ with Tesseract.from_image("my-tesseract") as t:
122122

123123
- **[Tesseract Core](https://github.com/pasteurlabs/tesseract-core)** — CLI, Python SDK, and runtime (this repo).
124124
- **[Tesseract-JAX](https://github.com/pasteurlabs/tesseract-jax)** — Embed Tesseracts as JAX primitives into end-to-end differentiable JAX programs.
125+
- **[Tesseract-Torch](https://github.com/pasteurlabs/tesseract-torch)** — Embed Tesseracts as PyTorch operators into end-to-end differentiable PyTorch programs.
125126
- **[Tesseract-Streamlit](https://github.com/pasteurlabs/tesseract-streamlit)** — Auto-generate interactive web apps from Tesseracts.
126127

127128
## Learn more
Lines changed: 161 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,161 @@
1+
# Copyright 2025 Pasteur Labs. All Rights Reserved.
2+
# SPDX-License-Identifier: Apache-2.0
3+
4+
"""Single-timestep Burgers' equation solver Tesseract (PyTorch).
5+
6+
Solves one explicit Euler step of the 1D viscous Burgers' equation:
7+
8+
u^{n+1} = u^n + dt * (-u * du/dx + nu * d²u/dx²)
9+
10+
The viscosity field nu is provided as an input — the solver does not compute it.
11+
This clean interface (state + material field → next state) is the same contract
12+
that a Fortran solver with an adjoint could implement. The outer time-stepping
13+
loop and closure evaluation live in the caller, enabling per-timestep closure
14+
calls and end-to-end gradient flow through both solver and closure.
15+
"""
16+
17+
from typing import Any
18+
19+
import numpy as np
20+
import torch
21+
from pydantic import BaseModel, Field
22+
from torch.utils._pytree import tree_map
23+
24+
from tesseract_core.runtime import Array, Differentiable, Float64
25+
from tesseract_core.runtime.tree_transforms import filter_func, flatten_with_paths
26+
27+
# Default grid size
28+
N = 128
29+
30+
# --- Grid setup (fixed for this Tesseract) ---
31+
DX = 1.0 / (N - 1)
32+
33+
to_tensor = lambda x: (
34+
torch.tensor(x, dtype=torch.float64)
35+
if isinstance(x, np.generic | np.ndarray)
36+
else x
37+
)
38+
39+
40+
class InputSchema(BaseModel):
41+
u: Differentiable[Array[(N,), Float64]] = Field(
42+
description="Current velocity field on the grid"
43+
)
44+
nu: Differentiable[Array[(N,), Float64]] = Field(
45+
description="Viscosity field at each grid point (must be positive)"
46+
)
47+
dt: float = Field(description="Time step size", default=1e-4)
48+
49+
50+
class OutputSchema(BaseModel):
51+
u_next: Differentiable[Array[(N,), Float64]] = Field(
52+
description="Velocity field after one time step"
53+
)
54+
55+
56+
def evaluate(inputs: dict) -> dict:
57+
"""Core differentiable computation — pure torch operations."""
58+
u = inputs["u"]
59+
nu = inputs["nu"]
60+
dt = inputs["dt"]
61+
62+
# Spatial derivatives via plain central differences. The flow stays smooth
63+
# and low-Reynolds here (gentle low-frequency ICs, viscosity bounded by the
64+
# closure's sigmoid so nu*dt/dx^2 stays within the explicit CFL limit), so no
65+
# shocks form and we don't need upwinding, a conservative/flux form, or a
66+
# stiff integrator (e.g. ETDRK). A real solver in a harder regime would.
67+
dudx = torch.zeros_like(u)
68+
dudx[1:-1] = (u[2:] - u[:-2]) / (2 * DX)
69+
70+
d2udx2 = torch.zeros_like(u)
71+
d2udx2[1:-1] = (u[2:] - 2 * u[1:-1] + u[:-2]) / (DX**2)
72+
73+
# Burgers' equation: du/dt = -u * du/dx + nu * d²u/dx²
74+
dudt = -u * dudx + nu * d2udx2
75+
76+
# Forward Euler step
77+
u_next = u + dt * dudt
78+
79+
# Enforce boundary conditions (Dirichlet: hold boundary values)
80+
u_next = torch.cat([u[:1], u_next[1:-1], u[-1:]])
81+
82+
return {"u_next": u_next}
83+
84+
85+
def apply(inputs: InputSchema) -> OutputSchema:
86+
tensor_inputs = tree_map(to_tensor, inputs.model_dump())
87+
return evaluate(tensor_inputs)
88+
89+
90+
def abstract_eval(abstract_inputs: Any) -> Any:
91+
return {"u_next": {"shape": [N], "dtype": "float64"}}
92+
93+
94+
def jacobian_vector_product(
95+
inputs: InputSchema,
96+
jvp_inputs: set[str],
97+
jvp_outputs: set[str],
98+
tangent_vector: dict[str, Any],
99+
):
100+
jvp_inputs = tuple(jvp_inputs)
101+
tangent_vector = {key: tangent_vector[key] for key in jvp_inputs}
102+
103+
tensor_inputs = tree_map(to_tensor, inputs.model_dump())
104+
pos_tangent = tree_map(to_tensor, tangent_vector).values()
105+
pos_inputs = flatten_with_paths(tensor_inputs, jvp_inputs).values()
106+
107+
filtered_pos_eval = filter_func(
108+
evaluate, tensor_inputs, jvp_outputs, input_paths=jvp_inputs
109+
)
110+
111+
return torch.func.jvp(filtered_pos_eval, tuple(pos_inputs), tuple(pos_tangent))[1]
112+
113+
114+
def vector_jacobian_product(
115+
inputs: InputSchema,
116+
vjp_inputs: set[str],
117+
vjp_outputs: set[str],
118+
cotangent_vector: dict[str, Any],
119+
):
120+
vjp_inputs = tuple(vjp_inputs)
121+
cotangent_vector = {key: cotangent_vector[key] for key in vjp_outputs}
122+
123+
tensor_inputs = tree_map(to_tensor, inputs.model_dump())
124+
tensor_cotangent = tree_map(to_tensor, cotangent_vector)
125+
pos_inputs = flatten_with_paths(tensor_inputs, vjp_inputs).values()
126+
127+
filtered_pos_func = filter_func(
128+
evaluate, tensor_inputs, vjp_outputs, input_paths=vjp_inputs
129+
)
130+
131+
_, vjp_func = torch.func.vjp(filtered_pos_func, *pos_inputs)
132+
vjp_vals = vjp_func(tensor_cotangent)
133+
return dict(zip(vjp_inputs, vjp_vals, strict=True))
134+
135+
136+
def jacobian(
137+
inputs: InputSchema,
138+
jac_inputs: set[str],
139+
jac_outputs: set[str],
140+
):
141+
jac_inputs = tuple(jac_inputs)
142+
tensor_inputs = tree_map(to_tensor, inputs.model_dump())
143+
pos_inputs = flatten_with_paths(tensor_inputs, jac_inputs).values()
144+
145+
filtered_pos_eval = filter_func(
146+
evaluate, tensor_inputs, jac_outputs, input_paths=jac_inputs
147+
)
148+
149+
def filtered_pos_eval_flat(*args):
150+
res = filtered_pos_eval(*args)
151+
return tuple(res[k] for k in jac_outputs)
152+
153+
jac = torch.autograd.functional.jacobian(filtered_pos_eval_flat, tuple(pos_inputs))
154+
155+
jac_dict = {}
156+
for dy, dys in zip(jac_outputs, jac, strict=True):
157+
jac_dict[dy] = {}
158+
for dx, dxs in zip(jac_inputs, dys, strict=True):
159+
jac_dict[dy][dx] = dxs
160+
161+
return jac_dict
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
name: "burgers-solver"
2+
version: "0.1.0"
3+
description: "1D Burgers equation solver with pluggable neural viscosity closure (PyTorch)"
4+
5+
build_config:
6+
target_platform: "native"

0 commit comments

Comments
 (0)