Creating Tesseracts¶
This page walks through creating your own Tesseracts, starting from a basic example and building up to advanced patterns. We recommend reading the Get Started page first.
Initialize a new Tesseract¶
Run the following to initialize everything needed to define a new Tesseract in the current directory:
$ tesseract init --name my_tesseract
This creates three files:
tesseract_api.py— Python module defining the Tesseract’s computations.tesseract_config.yaml— metadata (name, version), build options (base image, custom build steps, external data), and more.tesseract_requirements.txt— Python dependencies in pip requirements file format.
Use --target-dir [DIRECTORY] to create these files elsewhere. Other useful options:
--recipe— use a ready-made template for common scenarios (e.g., generating gradient endpoints from JAX functions).--help— list all available options and recipes.
Define a simple Tesseract¶
The generated tesseract_api.py contains boilerplate code to guide you. Let’s walk through it section by section, implementing a simple helloworld Tesseract that accepts a name and returns "Hello {name}!".
The first section defines the input and output schemas:
class InputSchema(BaseModel):
pass
class OutputSchema(BaseModel):
pass
Input and output schemas are defined as Pydantic models[1]. For helloworld, we need a string in and a string out:
class InputSchema(BaseModel):
name: str = Field(
description="Name of the person you want to greet."
)
class OutputSchema(BaseModel):
greeting: str = Field(description="A greeting!")
Field descriptions are optional but recommended — they appear in the auto-generated docs and schemas. You can also use default values, validators, and other Pydantic features (see Pydantic docs).
Below the schemas, you’ll find the required endpoints:
def apply(inputs: InputSchema) -> OutputSchema:
...
Currently, only apply is required. This is where you define the Tesseract’s core computation. For helloworld:
def apply(inputs: InputSchema) -> OutputSchema:
"""Greet a person whose name is given as input."""
return OutputSchema(greeting=f"Hello {inputs.name}")
Note
Docstrings on apply (and other endpoints) are included in the auto-generated docs.
The last section contains templates for optional endpoints:
# def jacobian(inputs: InputSchema, jac_inputs: set[str], jac_outputs: set[str]):
# return {}
...
Leave these untouched for now — helloworld is not differentiable.
Tip
For a Tesseract with all optional endpoints implemented, see the Univariate example.
Finally, set the name and version in tesseract_config.yaml:
name: "helloworld"
version: "1.0.0"
description: "A sample Python app"
You’re now ready to build your first Tesseract.
Tip
Before building, you can test locally without containers using tesseract-runtime. See the Debugging Guide for details.
Build a Tesseract¶
To build, run tesseract build from the directory containing tesseract_api.py:
$ tesseract build .
The Tesseract name comes from tesseract_config.yaml. By default, the image is tagged with both the specified version (1.0.0) and latest, so you can reference it as helloworld:1.0.0 or helloworld:latest.
View built Tesseracts¶
To list all locally available Tesseracts:
$ tesseract list
The output is a table of Tesseract images with their ID, name, version, and description:
┏━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━┳━━━━━━━━━━━━┳━━━━━━━━━┳━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┓
┃ ID ┃ Tags ┃ Name ┃ Version ┃ Description ┃
┡━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━╇━━━━━━━━━━━━╇━━━━━━━━━╇━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━┩
│ sha256:d4bdc2c29eb1 │ ['helloworld:latest'] │ helloworld │ 1.0.0 │ A sample Python app │
└─────────────────────┴───────────────────────┴────────────┴─────────┴───────────────────────────────────────────┘
Test your Tesseract¶
Every Tesseract ships with a built-in test endpoint for regression testing. It calls one of your endpoints with a given input and checks the result against a known-good output (or against an expected exception), reporting a simple pass/fail.
A test case is a small JSON file. For the helloworld example, test_cases/test_apply.json looks like this:
{
"endpoint": "apply",
"payload": { "inputs": { "name": "Ozzy" } },
"expected_outputs": { "greeting": "Hello Ozzy!" }
}
Run it against a built image with tesseract run ... test, passing the spec as a file with the @ prefix:
$ tesseract run helloworld:latest test @test_cases/test_apply.json
The command prints a JSON result with a status of "passed", "failed", or "error". It always exits with code 0 and reports the outcome in the payload, so a test runner must read status rather than the exit code.
A spec has a few more knobs:
atol/rtol— absolute and relative tolerances for numeric comparisons (defaults:1e-8and1e-5). Loosen these for endpoints whose outputs aren’t bit-reproducible.expected_exception(with optionalexpected_exception_regex) — assert that the endpoint raises instead of returning. Give the exception’s name (e.g."pydantic.ValidationError") and, optionally, a regex the message must match. Provide exactly one ofexpected_outputsorexpected_exception.
To test the Tesseract API directly from Python without building an image, which is convenient while iterating, use the SDK. It imports your tesseract_api.py in debug mode so the test endpoint is available:
from tesseract_core import Tesseract
tess = Tesseract.from_tesseract_api("path/to/tesseract_api.py")
tess.test({
"endpoint": "apply",
"payload": {"inputs": {"name": "Ozzy"}},
"expected_outputs": {"greeting": "Hello Ozzy!"},
}) # raises AssertionError if the test fails
Note
Over HTTP, the test endpoint is only exposed when a Tesseract is served in debug mode (tesseract serve --debug). tesseract run ... test and Tesseract.from_tesseract_api(...) enable it for you, a plain tesseract serve does not.
Keeping a test_cases/ directory next to each Tesseract and running every spec in CI is the pattern the cookiecutter-tesseract template sets up for you.
Arrays in the schema¶
N-dimensional arrays are central to scientific computing. Use the tesseract_core.runtime.Array type annotation to define them:
from tesseract_core.runtime import Array, Float32
class InputSchema(BaseModel):
x: Array[(3,), Float32] = Field(
description="A 3D vector",
)
r: Array[(3, 3), Float32] = Field(
description="A 3x3 matrix",
)
s: Float64 = Field(description="A scalar")
v: Array[(None,), Float32] = Field(
description="A vector of unknown length",
)
p: Array[..., Float32] = Field(
description="An array of any shape",
)
The first parameter is the shape, the second is the dtype — both follow NumPy conventions.
Inside a Tesseract, Array fields are cast to numpy.ndarray objects with the given dtype and shape, so standard NumPy operations work directly. For example, r @ x + s would multiply the matrix r by the vector x and add the broadcasted scalar s.
For scalars, use tesseract_core.runtime.Float32, Float64, Int32, etc. (see the runtime API reference for the full list). Plain float works but does not support the differentiability features described below.
Nested schemas¶
Since InputSchema and OutputSchema are Pydantic BaseModels, they support nesting other models within them:
class Mesh(BaseModel):
"""A simple mesh schema."""
points: Array[(None, 3), Float32]
num_points_per_cell: Array[(None,), Float32]
cell_connectivity: Array[(None,), Int32]
class InputSchema(BaseModel):
wing_shape: Mesh
propeller_shape: Mesh
Dicts and lists in schemas¶
Schemas support dict and list containers for variable-length collections or dynamically keyed data.
Dicts¶
Use dict[str, ...] to define a dictionary with string keys:
class InputSchema(BaseModel):
params: dict[str, Differentiable[Array[(None,), Float32]]] = Field(
description="A dictionary of parameter arrays, e.g. {'x': array, 'y': array}.",
)
Inside apply, dict fields are accessed by key:
def apply(inputs: InputSchema) -> OutputSchema:
x = inputs.params["x"]
y = inputs.params["y"]
...
Lists¶
Use list[...] to define a list of values:
class InputSchema(BaseModel):
coefficients: list[Differentiable[Array[(None,), Float32]]] = Field(
description="A list of coefficient arrays.",
)
Inside apply, list fields are accessed by index:
def apply(inputs: InputSchema) -> OutputSchema:
c0 = inputs.coefficients[0]
c1 = inputs.coefficients[1]
...
Combining dicts, lists, and nested models¶
These containers can be freely nested and combined with BaseModel subclasses:
class NestedParams(BaseModel):
z: Differentiable[Array[(5,), Float32]]
gamma: dict[str, Differentiable[Array[(None,), Float32]]]
class InputSchema(BaseModel):
alpha: dict[str, Differentiable[Array[(None,), Float32]]]
beta: NestedParams
delta: list[Differentiable[Array[(None,), Float32]]]
class OutputSchema(BaseModel):
result: Differentiable[Array[(3,), Float32]]
result_dict: dict[str, Differentiable[Array[(None,), Float32]]]
result_list: list[Differentiable[Array[(None,), Float32]]]
Tip
Dicts and lists also work with non-differentiable types (e.g. dict[str, Array[(None,), Float32]]), and with non-array types (e.g. dict[str, str], list[int]).
Differentiability¶
Tesseracts can expose endpoints for computing derivatives, making it possible to compose multiple Tesseracts into automatically differentiable workflows for shape optimization, model calibration, and more.
The tesseract_core.runtime.Differentiable type annotation marks which inputs can be differentiated with respect to, and which outputs can be differentiated. All Differentiable outputs are considered differentiable with respect to all Differentiable inputs. Passing a non-differentiable field (e.g., jac_inputs=["non_differentiable_arg"]) raises a validation error before the endpoint runs.
For example:
from tesseract_core.runtime import Differentiable, Float64
class InputSchema(BaseModel):
x: Differentiable[Float64]
r: Differentiable[Array[(3, 3), Float32]]
s: float
class OutputSchema(BaseModel):
a: Differentiable[Float64]
b: int
Here, output a can be differentiated with respect to the scalar x and the matrix r, but not s.
Warning
Differentiable can only wrap tesseract_core.runtime.Array types (including rank-0 aliases like Float64). Using it on Python base types (e.g., Differentiable[float]) will raise an error.
Beyond marking fields, you also need to implement the derivative logic. With autodiff frameworks like JAX or PyTorch, these are usually one-liners. See Differentiable Programming for details on implementing jacobian, jacobian_vector_product, and vector_jacobian_product.