Skip to content
Closed
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
126 changes: 76 additions & 50 deletions examples/_matlab_springmass/README.md
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
# MATLAB Spring-Mass-Damper Tesseract

This example demonstrates how to wrap MATLAB code as a Tesseract using
the official [MathWorks MATLAB Docker image](https://hub.docker.com/r/mathworks/matlab).
MATLAB is pre-installed in the base image — no compilation or additional
toolboxes required.
the official [MathWorks MATLAB Docker image](https://hub.docker.com/r/mathworks/matlab)
directly as the base image. MATLAB is pre-installed in the base image —
no compilation or additional toolboxes required.

The solver uses MATLAB's `ode45` to simulate a spring-mass-damper system:

Expand All @@ -13,65 +13,60 @@ m * x'' + c * x' + k * x = F₀ (step force input)

## Prerequisites

- **A MATLAB network license server** reachable from the container
- **Docker**
- **A valid MATLAB license**, either:
- A **network license server** reachable from the container (e.g. `27000@your-license-server`), or
- A **MATLAB Individual license file** (`.lic`) along with the activation MAC address

No local MATLAB installation or additional toolboxes are needed.
No local MATLAB installation is needed.

## Docker Runtime Requirements

MATLAB's JVM requires more shared memory than Docker's default 64MB.
Pass `--shm-size=512M` via `--runtime-args` in all `tesseract run` /
`tesseract serve` commands below. You also need to pass your license
server and its DNS mapping:
## Build the Tesseract

```bash
MATLAB_RUNTIME_ARGS="\
-e MLM_LICENSE_FILE=27000@your-license-server \
--add-host=your-license-server:YOUR_SERVER_IP \
--shm-size=512M"
tesseract build .
```

Set this once and reuse it in the commands below.

## Quick Start
The Tesseract image is built directly on top of `mathworks/matlab:r2026a`
(see [tesseract_config.yaml](tesseract_config.yaml)). To use a different
MATLAB release, edit the `base_image` tag — no other change is necessary,
since `tesseract_api.py` auto-discovers the MATLAB binary in `/opt/matlab/`.

### 1. Build the Tesseract

```bash
tesseract build .
```
## Run (network license)

### 2. Run
MATLAB's JVM requires more shared memory than Docker's 64 MB default.
Pass the license server via `--env`, plus shared memory and DNS mapping
via `--docker-args`:

```bash
tesseract run matlab-springmass apply '{}' \
--runtime-args "$MATLAB_RUNTIME_ARGS"
tesseract run matlab-springmass \
--env MLM_LICENSE_FILE=27000@your-license-server \
--docker-args "--shm-size=512M --add-host=your-license-server:YOUR_SERVER_IP" \
apply '{"inputs": {}}'
```

With custom parameters:

```bash
tesseract run matlab-springmass apply '{
"mass": 2.0,
"damping": 1.0,
"stiffness": 20.0,
"force_amplitude": 5.0
}' --runtime-args "$MATLAB_RUNTIME_ARGS"
tesseract run matlab-springmass \
--env MLM_LICENSE_FILE=27000@your-license-server \
--docker-args "--shm-size=512M --add-host=your-license-server:YOUR_SERVER_IP" \
apply '{"inputs": {"mass": 2.0, "damping": 1.0, "stiffness": 20.0, "force_amplitude": 5.0}}'
```

### 3. Or serve as a REST API
## Or serve as a REST API

```bash
tesseract serve matlab-springmass --runtime-args "$MATLAB_RUNTIME_ARGS"
tesseract serve matlab-springmass \
--env MLM_LICENSE_FILE=27000@your-license-server \
--docker-args "--shm-size=512M --add-host=your-license-server:YOUR_SERVER_IP"

# In another terminal:
curl -X POST http://localhost:8000/apply \
-H 'Content-Type: application/json' \
-d '{"inputs": {"mass": 1.0, "stiffness": 10.0}}'
```

### 4. Use from Python
## Use from Python

```python
from tesseract_core import Tesseract
Expand All @@ -86,41 +81,72 @@ with Tesseract.from_image("matlab-springmass") as t:
print(f"Final displacement: {result['displacement'][-1]:.6f} m")
```

## Visualization
## Using an Individual License

If you don't have access to a network license server, you can run the
Tesseract using your MATLAB Individual license file (`.lic`).

### 1. Get your authorized username and activation MAC address

An Individual license is bound to a specific OS username and the MAC
address of the machine it was activated on. You can find both in your
MathWorks Account license details.

### 2. Run with the license file mounted

```bash
python visualize.py
# Or with custom parameters:
python visualize.py --mass 2.0 --damping 3.0 --stiffness 50.0
tesseract run matlab-springmass \
--user 0:0 \
--env MATLAB_USERNAME=<your-username> \
--env MLM_LICENSE_FILE=/licenses/license.lic \
--volume /path/to/your/license.lic:/licenses/license.lic \
--docker-args "--shm-size=512M --mac-address=XX:XX:XX:XX:XX:XX" \
apply '{"inputs": {}}'
```

What the extra flags do:

- `--user 0:0` runs the container as root. This is needed for the next
step. The MATLAB process itself does **not** run as root — it gets
dropped to UID 1000 just before launch.
- `--env MATLAB_USERNAME=<your-username>` makes `tesseract_api.py`
rename the `ubuntu` user in `/etc/passwd` (and the libnss_wrapper
files in `/tmp/`) to `<your-username>` at startup, then spawn MATLAB
as that user. This is required because MATLAB's file-based license
check picks the `ubuntu` entry out of `/etc/passwd` regardless of the
actual running UID — renaming it lets the check see the correct user.
Comment on lines +115 to +117

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I don't understand this sentence. Why would it pick out ubuntu? Doesn't it look for <your-username>?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For some reason, if we don't rename the ubuntu entry, I got an error along the lines of "User ubuntu is not authorized to use this license", regardless of which user name/id I pass to the container.

- `--mac-address=XX:XX:XX:XX:XX:XX` must match the MAC your license
was activated against.

## How It Works

The `tesseract_config.yaml` uses `mathworks/matlab:r2025b` as the base
image, which has MATLAB pre-installed. The `.m` source file is copied in
at build time, and `tesseract_api.py` calls it via `matlab -batch`:
The `tesseract_config.yaml` uses `mathworks/matlab:r2026a` directly as
the base image. The `.m` source file is copied in at build time, and
`tesseract_api.py` invokes it via `matlab -batch`:

```
Python (tesseract_api.py)
├── Write input JSON → /tmp/input.json
├── subprocess: matlab -batch "addpath(...); spring_mass_damper('input.json', 'output.json')"
├── (if MATLAB_USERNAME set) Rename 'ubuntu' in /etc/passwd to <user>
├── Write input JSON → /tmp/<rand>/input.json
├── subprocess(user=<user>): matlab -batch "addpath(...); spring_mass_damper(...)"
│ └── MATLAB reads input JSON (jsondecode)
│ └── ode45 solves the ODE system
│ └── MATLAB writes output JSON (jsonencode)
└── Read output JSON → OutputSchema
```

## Licensing
## Visualization

The container includes MATLAB but **not** a license. You must provide
access to a network license server at runtime via the `MLM_LICENSE_FILE`
environment variable. See [MathWorks: Licensing for Containers](https://www.mathworks.com/help/cloudcenter/ug/matlab-container-on-docker-hub.html)
for details.
```bash
python visualize.py
# Or with custom parameters:
python visualize.py --mass 2.0 --damping 3.0 --stiffness 50.0
```

## File Structure

```
matlab_springmass/
_matlab_springmass/
├── matlab/
│ └── spring_mass_damper.m # MATLAB solver source
├── tesseract_api.py # Python wrapper (InputSchema, OutputSchema, apply)
Expand Down
67 changes: 63 additions & 4 deletions examples/_matlab_springmass/tesseract_api.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,9 +13,12 @@
with a unit step force input F(t) = F0 for t >= 0.
"""

import glob
import json
import math
import os
import pwd
import re
import subprocess
from pathlib import Path
from tempfile import TemporaryDirectory
Expand All @@ -25,10 +28,39 @@

from tesseract_core.runtime import Array, Float64

# Path to the MATLAB binary inside the container.
# Override with the MATLAB_BIN environment variable if your MATLAB
# is mounted at a non-standard path.
MATLAB_BIN = os.environ.get("MATLAB_BIN", "/opt/matlab/R2025b/bin/matlab")
# Path to the MATLAB binary. Auto-discovers /opt/matlab/<release>/bin/matlab
# (uses the highest version if multiple are installed). Override with the
# MATLAB_BIN env var for a non-standard path.
_matlab_candidates = sorted(glob.glob("/opt/matlab/*/bin/matlab"))
MATLAB_BIN = os.environ.get(
"MATLAB_BIN",
_matlab_candidates[-1] if _matlab_candidates else "/opt/matlab/R2026a/bin/matlab",
)

# If MATLAB_USERNAME is set, rename the 'ubuntu' user in /etc/passwd (and
# /etc/group) to that username at startup. MATLAB's file-based license check
# picks the 'ubuntu' entry out of /etc/passwd regardless of the running UID,
# so renaming it lets the check see the correct user without baking the
# username into the image. Requires the container to run as root, e.g.
# `tesseract run --user 0:0 --env MATLAB_USERNAME=<your-license-user> ...`.
_MATLAB_USERNAME = os.environ.get("MATLAB_USERNAME")
if _MATLAB_USERNAME:
# /etc/passwd is what MATLAB's license check reads directly. /tmp/passwd
# is what the tesseract template's libnss_wrapper redirects NSS lookups
# to (used by pwd.getpwnam below). Both must be updated.
for _path in ("/etc/passwd", "/etc/group", "/tmp/passwd", "/tmp/group"):
try:
with open(_path) as _f:
_content = _f.read()
_new = re.sub(
r"^ubuntu:", f"{_MATLAB_USERNAME}:", _content, flags=re.MULTILINE
)
if _new != _content:
with open(_path, "w") as _f:
_f.write(_new)
except (PermissionError, OSError, FileNotFoundError):
# Not running as root, file doesn't exist, or rename already done.
pass

# Path to the .m source file copied into the container at build time.
SOLVER_DIR = Path("/tesseract/matlab")
Expand Down Expand Up @@ -110,6 +142,14 @@ def apply(inputs: InputSchema) -> OutputSchema:
input_file = tmpdir_path / "input.json"
output_file = tmpdir_path / "output.json"

# If we'll spawn MATLAB as a different user, hand them the tmpdir.
if _MATLAB_USERNAME:
try:
_pw = pwd.getpwnam(_MATLAB_USERNAME)
os.chown(tmpdir, _pw.pw_uid, _pw.pw_gid)
except (KeyError, PermissionError):
pass

# Write input parameters as JSON
input_data = {
"mass": inputs.mass,
Expand All @@ -129,10 +169,29 @@ def apply(inputs: InputSchema) -> OutputSchema:
f"spring_mass_damper('{input_file}', '{output_file}')"
)

# If MATLAB_USERNAME is set, run MATLAB as that user (UID/GID resolved
# via getpwnam after the /etc/passwd rename above). Otherwise inherit.
popen_kwargs = {}
if _MATLAB_USERNAME:
try:
pw = pwd.getpwnam(_MATLAB_USERNAME)
env = os.environ.copy()
env.update(
{
"HOME": pw.pw_dir,
"USER": _MATLAB_USERNAME,
"LOGNAME": _MATLAB_USERNAME,
}
)
popen_kwargs.update(user=pw.pw_uid, group=pw.pw_gid, env=env)
except KeyError:
pass

process = subprocess.Popen(
[MATLAB_BIN, "-batch", matlab_cmd],
stdout=None, # Inherit stdout — streams to parent process
stderr=None, # Inherit stderr — streams to parent process
**popen_kwargs,
)
returncode = process.wait()

Expand Down
2 changes: 1 addition & 1 deletion examples/_matlab_springmass/tesseract_config.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@ description: |
build_config:
# Official MathWorks MATLAB image — includes a full MATLAB installation.
# Change the tag to match your license version if needed.
base_image: "mathworks/matlab:r2025b"
base_image: "mathworks/matlab:r2026a"

# Copy the MATLAB .m source file into the container
package_data:
Expand Down
Loading