Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions .github/workflows/run_tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ jobs:
# test with oldest and latest supported Python versions
# NOTE: If bumping the minimum Python version here, also do it in
# ruff.toml, setup.py and other CI files as well.
python-version: ["3.10", "3.12"]
python-version: ["3.10", "3.13"]
runtime-deps: ["latest"]

include:
- os: ubuntu-latest
python-version: "3.12"
python-version: "3.10"
runtime-deps: "oldest"

fail-fast: false
Expand Down Expand Up @@ -199,7 +199,7 @@ jobs:
strategy:
matrix:
os: [ubuntu-24.04]
python-version: ["3.12"]
python-version: ["3.13"]

arch: ["x64"]
docker-engine: ["docker"]
Expand All @@ -214,18 +214,18 @@ jobs:
# Test on arm to ensure compatibility with Apple M1 chips
# (OSX runners don't have access to Docker so we use Linux ARM runners instead)
- os: "ubuntu-24.04"
python-version: "3.12"
python-version: "3.13"
arch: "arm"
docker-engine: "docker"
unit-tesseract: "base"
- os: "ubuntu-24.04"
python-version: "3.12"
python-version: "3.13"
arch: "arm"
docker-engine: "docker"
unit-tesseract: "pyvista-arm64"
# Run tests using Podman
- os: "ubuntu-24.04"
python-version: "3.12"
python-version: "3.13"
arch: "x64"
docker-engine: "podman"
unit-tesseract: "base"
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,10 @@ class OutputSchema(BaseModel):


def apply(inputs: InputSchema) -> OutputSchema:
# Ensure that the Python version is what we expect (3.9)
# Ensure that the Python version is what we expect (3.10)
import sys

assert sys.version_info[:2] == (3, 9)
assert sys.version_info[:2] == (3, 10)
return OutputSchema(bar=0)


Expand Down
7 changes: 7 additions & 0 deletions examples/py310/tesseract_config.yaml
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
name: "py310"
version: "0.1.0"
description: |
Empty Tesseract that requires Python 3.10 (set through a custom Docker image).

build_config:
base_image: "python:3.10-slim-bookworm"
7 changes: 0 additions & 7 deletions examples/py39/tesseract_config.yaml

This file was deleted.

15 changes: 7 additions & 8 deletions examples/qp_solve/tesseract_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,6 @@
# Tesseract API module for lp_solve
# Generated by tesseract 0.9.0 on 2025-06-04T13:11:17.208977
import functools
from typing import Optional

import jax
import jax.numpy as jnp
Expand All @@ -23,29 +22,29 @@ class InputSchema(BaseModel):
Q: Differentiable[Array[(None, None), Float32]] = Field(
description="Quadratic cost matrix Q for the quadratic program."
)
q: Optional[Differentiable[Array[(None,), Float32]]] = Field(
q: Differentiable[Array[(None,), Float32]] | None = Field(
description="Linear cost vector q for the quadratic program.", default=None
)
A: Optional[Differentiable[Array[(None, None), Float32]]] = Field(
A: Differentiable[Array[(None, None), Float32]] | None = Field(
default=None,
description="Linear equality constraint matrix A for the quadratic program.",
)
b: Optional[Differentiable[Array[(None,), Float32]]] = Field(
b: Differentiable[Array[(None,), Float32]] | None = Field(
default=None,
description="Linear equality constraint rhs b for the quadratic program.",
)
G: Optional[Differentiable[Array[(None, None), Float32]]] = Field(
G: Differentiable[Array[(None, None), Float32]] | None = Field(
description="Linear inequality constraint matrix G for the quadratic program.",
default=None,
)
h: Optional[Differentiable[Array[(None,), Float32]]] = Field(
h: Differentiable[Array[(None,), Float32]] | None = Field(
description="Linear inequality constraint rhs h for the quadratic program.",
default=None,
)
solver_tol: Optional[Float32] = Field(
solver_tol: Float32 | None = Field(
description="Tolerance for the solver convergence.", default=1e-4
)
target_kappa: Optional[Float32] = Field(
target_kappa: Float32 | None = Field(
description="QPAX parameter for QP relaxation.", default=1e-3
)

Expand Down
2 changes: 1 addition & 1 deletion examples/univariate/optimize.py
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,6 @@ def rosenbrock_gradient(x: np.ndarray) -> np.ndarray:
callback=lambda xs: trajectory.append(xs.tolist()),
)

anim = make_animation(*list(zip(*trajectory)))
anim = make_animation(*list(zip(*trajectory, strict=True)))
# anim.save("rosenbrock_optimization.gif", writer="pillow", fps=2, dpi=150)
plt.show()
1 change: 1 addition & 0 deletions pyproject.toml
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ filterwarnings = [
"ignore:numpy.ufunc size changed",
# sometimes, dependencies leak resources
"ignore:.*socket\\.socket.*:pytest.PytestUnraisableExceptionWarning",
"ignore:.*sqlite3\\.Connection.*:pytest.PytestUnraisableExceptionWarning",
]

[tool.coverage.run]
Expand Down
3 changes: 1 addition & 2 deletions ruff.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,5 @@
# Set to the lowest supported Python version.
# !! tesseract_runtime still supports Python 3.9 !!
target-version = "py39"
target-version = "py310"

# Set the target line length for formatting.
line-length = 88
Expand Down
40 changes: 20 additions & 20 deletions tesseract_core/runtime/array_encoding.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@
import re
from collections.abc import Sequence
from pathlib import Path
from typing import Annotated, Any, Literal, Optional, Union, get_args
from typing import Annotated, Any, Literal, get_args
from uuid import uuid4

import numpy as np
Expand Down Expand Up @@ -45,8 +45,8 @@
"complex128",
]
EllipsisType = type(Ellipsis)
ArrayLike = Union[np.ndarray, np.number, np.bool_]
ShapeType = Union[tuple[Optional[int], ...], EllipsisType]
ArrayLike = np.ndarray | np.number | np.bool_
ShapeType = tuple[int | None, ...] | EllipsisType

MAX_BINREF_BUFFER_SIZE = 100 * 1024 * 1024 # 100 MB

Expand Down Expand Up @@ -94,12 +94,12 @@ class EncodedArrayModel(BaseModel):
object_type: Literal["array"]
shape: tuple[PositiveInt, ...]
dtype: AllowedDtypes
data: Union[BinrefArrayData, Base64ArrayData, JsonArrayData]
data: BinrefArrayData | Base64ArrayData | JsonArrayData
model_config = ConfigDict(extra="forbid")


def get_array_model(
expected_shape: ShapeType, expected_dtype: Optional[str], flags: Sequence[str]
expected_shape: ShapeType, expected_dtype: str | None, flags: Sequence[str]
) -> type[EncodedArrayModel]:
"""Create a Pydantic model for an encoded array that does validation on the given expected shape and dtype."""
if expected_dtype is None:
Expand Down Expand Up @@ -177,7 +177,7 @@ def get_array_model(
),
# Choose the appropriate data structure based on the encoding
"data": (
Union[BinrefArrayData, Base64ArrayData, JsonArrayData],
BinrefArrayData | Base64ArrayData | JsonArrayData,
Field(discriminator="encoding"),
),
"model_config": (ConfigDict, config),
Expand All @@ -203,12 +203,12 @@ def get_array_model(


def _dump_binref_arraydict(
arr: Union[np.ndarray, np.number, np.bool_],
base_dir: Union[Path, str],
subdir: Optional[Union[Path, str]],
arr: np.ndarray | np.number | np.bool_,
base_dir: Path | str,
subdir: Path | str | None,
current_binref_uuid: str,
max_file_size: int = MAX_BINREF_BUFFER_SIZE,
) -> tuple[dict[str, Union[str, dict[str, str]]], str]:
) -> tuple[dict[str, str | dict[str, str]], str]:
"""Dump array to json+binref encoded array dict.

Writes a .bin file and returns json encoded data.
Expand Down Expand Up @@ -243,8 +243,8 @@ def _dump_binref_arraydict(


def _dump_base64_arraydict(
arr: Union[np.ndarray, np.number, np.bool_],
) -> dict[str, Union[str, dict[str, str]]]:
arr: np.ndarray | np.number | np.bool_,
) -> dict[str, str | dict[str, str]]:
"""Dump array to json+base64 encoded array dict."""
data = {
"buffer": pybase64.b64encode(arr.tobytes()).decode(),
Expand All @@ -260,8 +260,8 @@ def _dump_base64_arraydict(


def _dump_json_arraydict(
arr: Union[np.ndarray, np.number, np.bool_],
) -> dict[str, Union[str, dict[str, str]]]:
arr: np.ndarray | np.number | np.bool_,
) -> dict[str, str | dict[str, str]]:
"""Dump array to json encoded array dict."""
data = {
"buffer": arr.tolist(),
Expand All @@ -282,7 +282,7 @@ def _load_base64_arraydict(val: dict) -> np.ndarray:
return np.frombuffer(buffer, dtype=val["dtype"]).reshape(val["shape"])


def _load_binref_arraydict(val: dict, base_dir: Union[str, Path, None]) -> np.ndarray:
def _load_binref_arraydict(val: dict, base_dir: str | Path | None) -> np.ndarray:
"""Load array from json+binref encoded array dict."""
path_match = re.match(r"^(?P<path>.+?)(\:(?P<offset>\d+))?$", val["data"]["buffer"])
if not path_match:
Expand Down Expand Up @@ -316,7 +316,7 @@ def _load_binref_arraydict(val: dict, base_dir: Union[str, Path, None]) -> np.nd


def _coerce_shape_dtype(
arr: ArrayLike, expected_shape: ShapeType, expected_dtype: Optional[str]
arr: ArrayLike, expected_shape: ShapeType, expected_dtype: str | None
) -> ArrayLike:
"""Coerce the shape and dtype of the passed array to the expected values."""
if expected_shape is Ellipsis:
Expand Down Expand Up @@ -363,7 +363,7 @@ def _coerce_shape_dtype(


def python_to_array(
val: Any, expected_shape: ShapeType, expected_dtype: Optional[str]
val: Any, expected_shape: ShapeType, expected_dtype: str | None
) -> ArrayLike:
"""Convert a Python object to a NumPy array."""
val = np.asarray(val, order="C")
Expand All @@ -380,7 +380,7 @@ def decode_array(
val: EncodedArrayModel,
info: ValidationInfo,
expected_shape: ShapeType,
expected_dtype: Optional[str],
expected_dtype: str | None,
) -> ArrayLike:
"""Decode an EncodedArrayModel to a NumPy array."""
from tesseract_core.runtime.config import get_config
Expand Down Expand Up @@ -422,8 +422,8 @@ def decode_array(


def encode_array(
arr: ArrayLike, info: Any, expected_shape: ShapeType, expected_dtype: Optional[str]
) -> Union[EncodedArrayModel, ArrayLike]:
arr: ArrayLike, info: Any, expected_shape: ShapeType, expected_dtype: str | None
) -> EncodedArrayModel | ArrayLike:
"""Encode a NumPy array as an EncodedArrayModel."""
from tesseract_core.runtime.config import get_config

Expand Down
15 changes: 6 additions & 9 deletions tesseract_core/runtime/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,6 @@
Annotated,
Any,
Literal,
Optional,
get_args,
get_origin,
)
Expand Down Expand Up @@ -219,23 +218,23 @@ def check_gradients(
),
],
input_paths: Annotated[
Optional[list[str]],
list[str] | None,
typer.Option(
"--input-paths",
help="Paths to differentiable inputs to check gradients for.",
show_default="check all",
),
] = None,
output_paths: Annotated[
Optional[list[str]],
list[str] | None,
typer.Option(
"--output-paths",
help="Paths to differentiable outputs to check gradients for.",
show_default="check all",
),
] = None,
endpoints: Annotated[
Optional[list[str]],
list[str] | None,
typer.Option(
"--endpoints",
help="Endpoints to check gradients for.",
Expand Down Expand Up @@ -275,7 +274,7 @@ def check_gradients(
),
] = 10,
seed: Annotated[
Optional[int],
int | None,
typer.Option(
"--seed",
help="Seed for random number generator. If not set, a random seed is used.",
Expand Down Expand Up @@ -367,7 +366,7 @@ def serve(


def _create_user_defined_cli_command(
app: typer.Typer, user_function: Callable, out_stream: Optional[io.TextIOBase]
app: typer.Typer, user_function: Callable, out_stream: io.TextIOBase | None
) -> None:
"""Creates a click command which sends requests to Tesseract endpoints.

Expand Down Expand Up @@ -459,9 +458,7 @@ def command_func():
decorator(command_func)


def _add_user_commands_to_cli(
app: typer.Typer, out_stream: Optional[io.IOBase]
) -> None:
def _add_user_commands_to_cli(app: typer.Typer, out_stream: io.IOBase | None) -> None:
tesseract_package = get_tesseract_api()
endpoints = create_endpoints(tesseract_package)

Expand Down
8 changes: 3 additions & 5 deletions tesseract_core/runtime/core.py
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@
from io import TextIOBase
from pathlib import Path
from types import ModuleType
from typing import Any, TextIO, Union
from typing import Any, TextIO

from pydantic import BaseModel

Expand All @@ -22,9 +22,7 @@


@contextmanager
def redirect_fd(
from_: TextIO, to_: Union[TextIO, int]
) -> Generator[TextIO, None, None]:
def redirect_fd(from_: TextIO, to_: TextIO | int) -> Generator[TextIO, None, None]:
"""Redirect a file descriptor at OS level.

Args:
Expand All @@ -49,7 +47,7 @@ def redirect_fd(
orig_fd_file.close()


def load_module_from_path(path: Union[Path, str]) -> ModuleType:
def load_module_from_path(path: Path | str) -> ModuleType:
"""Load a module from a file path.

Temporarily puts the module's parent folder on PYTHONPATH to ensure local imports work as expected.
Expand Down
6 changes: 2 additions & 4 deletions tesseract_core/runtime/experimental.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,13 +2,11 @@
# SPDX-License-Identifier: Apache-2.0

import json
from collections.abc import Iterator, Sequence
from collections.abc import Callable, Iterator, Sequence
from pathlib import Path
from typing import (
Annotated,
Any,
Callable,
Union,
get_args,
get_origin,
)
Expand Down Expand Up @@ -114,7 +112,7 @@ def __get_pydantic_core_schema__(
Does most of the heavy lifting for validation and serialization.
"""

def create_sequence(maybe_path: Union[str, Sequence[Any]]) -> LazySequence:
def create_sequence(maybe_path: str | Sequence[Any]) -> LazySequence:
"""Expand a glob pattern into a LazySequence if needed."""
validator = SchemaValidator(item_schema)

Expand Down
Loading
Loading