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 Debugging and Development 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 │
└─────────────────────┴───────────────────────┴────────────┴─────────┴───────────────────────────────────────────┘
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.