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.