Wrapping MATLAB Code¶
Context¶
MATLAB is one of the most widely used tools in engineering for simulation, control system design, and signal processing. Many organizations have decades of validated MATLAB code that they want to make accessible through modern APIs without rewriting. Tesseract provides a way to wrap MATLAB code by using the official MathWorks MATLAB Docker image as the base image and calling MATLAB via matlab -batch.
This example demonstrates the pattern with a spring-mass-damper ODE solver. The approach works for any MATLAB script or function that can run in batch mode.
Example Tesseract (examples/_matlab_springmass)¶
The MATLAB solver¶
The solver simulates a damped harmonic oscillator under a step force input:
using MATLAB’s ode45 (Dormand-Prince Runge-Kutta). It reads input parameters from a JSON file and writes results to a JSON file:
function spring_mass_damper(input_file, output_file)
%SPRING_MASS_DAMPER Solve a spring-mass-damper ODE system.
%
% Solves the damped harmonic oscillator equation:
% m * x'' + c * x' + k * x = F0 (step force input)
%
% using MATLAB's ode45 (Dormand-Prince Runge-Kutta).
%
% Arguments:
% input_file - Path to JSON file with input parameters
% output_file - Path to write JSON results
% Read input parameters
raw = fileread(input_file);
params = jsondecode(raw);
mass = params.mass;
damping = params.damping;
stiffness = params.stiffness;
force_amplitude = params.force_amplitude;
t_end = params.t_end;
n_output_points = params.n_output_points;
% Define the ODE system as first-order:
% y(1) = x (displacement)
% y(2) = x' (velocity)
%
% y(1)' = y(2)
% y(2)' = (F0 - c*y(2) - k*y(1)) / m
odefun = @(t, y) [
y(2);
(force_amplitude - damping * y(2) - stiffness * y(1)) / mass
];
% Initial conditions: at rest
y0 = [0; 0];
% Output time points
t_eval = linspace(0, t_end, n_output_points);
% Solve with ode45
opts = odeset('RelTol', 1e-8, 'AbsTol', 1e-10);
[t_sol, y_sol] = ode45(odefun, t_eval, y0, opts);
% Build output structure
result = struct();
result.time = t_sol(:)';
result.displacement = y_sol(:, 1)';
result.velocity = y_sol(:, 2)';
% Write output
json_str = jsonencode(result);
fid = fopen(output_file, 'w');
fprintf(fid, '%s', json_str);
fclose(fid);
fprintf('Solver completed: %d time steps, t_end = %.3f s\n', ...
n_output_points, t_end);
end
Input and output schemas¶
The InputSchema defines the physical parameters of the spring-mass-damper system:
class InputSchema(BaseModel):
"""Input parameters for the spring-mass-damper ODE solver.
Solves: m * x'' + c * x' + k * x = F0 (step force)
with initial conditions x(0) = 0, x'(0) = 0.
"""
mass: float = Field(
default=1.0,
gt=0.0,
description="Mass [kg].",
)
damping: float = Field(
default=0.5,
ge=0.0,
description="Damping coefficient [N*s/m]. Set to 0 for undamped oscillation.",
)
stiffness: float = Field(
default=10.0,
gt=0.0,
description="Spring stiffness [N/m].",
)
force_amplitude: float = Field(
default=1.0,
description="Step force amplitude [N]. Applied as a constant force for t >= 0.",
)
t_end: float = Field(
default=10.0,
gt=0.0,
description="Simulation end time [s].",
)
n_output_points: int = Field(
default=200,
ge=10,
le=10000,
description="Number of output time points.",
)
The OutputSchema returns time histories along with analytical system characteristics:
class OutputSchema(BaseModel):
"""Output from the spring-mass-damper solver."""
time: Array[(None,), Float64] = Field(
description="Time values [s]. Shape: (n_output_points,)",
)
displacement: Array[(None,), Float64] = Field(
description="Displacement x(t) [m]. Shape: (n_output_points,)",
)
velocity: Array[(None,), Float64] = Field(
description="Velocity x'(t) [m/s]. Shape: (n_output_points,)",
)
steady_state: float = Field(
description="Analytical steady-state displacement F0/k [m].",
)
damping_ratio: float = Field(
description="Damping ratio zeta = c / (2 * sqrt(k * m)) [-]. "
"< 1: underdamped, = 1: critically damped, > 1: overdamped.",
)
natural_frequency: float = Field(
description="Natural frequency omega_n = sqrt(k / m) [rad/s].",
)
Subprocess integration¶
The apply function writes input parameters as JSON, invokes MATLAB via matlab -batch, and reads back the JSON results:
def apply(inputs: InputSchema) -> OutputSchema:
"""Run the MATLAB spring-mass-damper solver.
This function:
1. Writes input parameters to a temporary JSON file
2. Calls MATLAB via `matlab -batch` to run the solver
3. Reads the JSON output file and returns results
"""
with TemporaryDirectory() as tmpdir:
tmpdir_path = Path(tmpdir)
input_file = tmpdir_path / "input.json"
output_file = tmpdir_path / "output.json"
# Write input parameters as JSON
input_data = {
"mass": inputs.mass,
"damping": inputs.damping,
"stiffness": inputs.stiffness,
"force_amplitude": inputs.force_amplitude,
"t_end": inputs.t_end,
"n_output_points": inputs.n_output_points,
}
input_file.write_text(json.dumps(input_data))
# Build the MATLAB command.
# addpath so MATLAB can find spring_mass_damper.m,
# then call it with input/output file paths.
matlab_cmd = (
f"addpath('{SOLVER_DIR}'); "
f"spring_mass_damper('{input_file}', '{output_file}')"
)
process = subprocess.Popen(
[MATLAB_BIN, "-batch", matlab_cmd],
stdout=None, # Inherit stdout — streams to parent process
stderr=None, # Inherit stderr — streams to parent process
)
returncode = process.wait()
if returncode != 0:
raise RuntimeError(f"MATLAB solver failed with return code {returncode}.")
# Read output JSON
output_data = json.loads(output_file.read_text())
time = np.array(output_data["time"], dtype=np.float64)
displacement = np.array(output_data["displacement"], dtype=np.float64)
velocity = np.array(output_data["velocity"], dtype=np.float64)
# Compute analytical quantities
steady_state = inputs.force_amplitude / inputs.stiffness
natural_frequency = math.sqrt(inputs.stiffness / inputs.mass)
damping_ratio = inputs.damping / (2.0 * math.sqrt(inputs.stiffness * inputs.mass))
return OutputSchema(
time=time,
displacement=displacement,
velocity=velocity,
steady_state=steady_state,
damping_ratio=damping_ratio,
natural_frequency=natural_frequency,
)
Build configuration¶
The tesseract_config.yaml uses the official MathWorks MATLAB Docker image as the base, so MATLAB is pre-installed — no compilation or additional toolboxes are needed:
name: "matlab-springmass"
version: "1.0.0"
description: |
Spring-mass-damper ODE solver demonstrating MATLAB integration.
This example shows how to wrap MATLAB code as a Tesseract using the
official MathWorks MATLAB Docker image. The solver uses ode45 to solve
the damped harmonic oscillator equation:
m * x'' + c * x' + k * x = F(t)
Industry relevance: vibration analysis, mechanical system design,
control system prototyping, and structural dynamics.
Prerequisites:
- A MATLAB network license server reachable from the container
- Set MLM_LICENSE_FILE at runtime (e.g. 27000@your-license-server)
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"
# Copy the MATLAB .m source file into the container
package_data:
- ["matlab/spring_mass_damper.m", "matlab/spring_mass_damper.m"]
Key points:
base_image: Usesmathworks/matlab:r2025b, which has a full MATLAB installationpackage_data: Copies the.msource file into the containerNo compilation step: MATLAB runs the
.mfile directly viamatlab -batch
Runtime requirements¶
Because the container includes MATLAB but not a license, you must provide a network license server at runtime:
tesseract run matlab-springmass apply '{}' \
--runtime-args "\
-e MLM_LICENSE_FILE=27000@your-license-server \
--add-host=your-license-server:YOUR_SERVER_IP \
--shm-size=512M"
The --shm-size=512M flag is required because MATLAB’s JVM uses POSIX shared memory, and Docker’s default of 64MB is insufficient.
Adapting this pattern¶
To wrap your own MATLAB code:
Choose a base image: Use
mathworks/matlab:<release>matching your license. Tags are available for R2024a and later.Structure your MATLAB code: Write a function that takes file paths as arguments for I/O. Use
jsondecode/jsonencode(available since R2016b) for data exchange.Create schemas: Map your MATLAB function’s parameters to Pydantic models.
Implement
apply(): Write the subprocess glue that callsmatlab -batch.Configure licensing: Ensure
MLM_LICENSE_FILEis passed at runtime.
For distributing Tesseracts to users without a MATLAB license, consider the MATLAB Compiler SDK: compile .m files into standalone executables and bundle the free MATLAB Runtime instead.