Interoperability
LINE has four implementations, in MATLAB, Java, Python and C++, and they interoperate on three levels: a solver in one language can hand the solve to another through the lang option; all four read and write the same model.json interchange document and the same external formats (JMT, LQN XML, PNML); and each ships a command line that speaks those documents, beside the REST API and the MCP server.
Cross-Language Execution
Every solver takes a lang option naming the implementation that will produce the numbers. MATLAB reaches all four implementations; native Python reaches the JAR and the C++ binary. The option is an assertion about which engine ran, not a hint: apart from an absent line-cli binary, a construct the chosen backend refuses is reported as an error rather than served quietly by the local solver.
| Host | lang values | Bridge | Transport |
|---|---|---|---|
| MATLAB | matlab (default), java, python, cpp | JLINE.m, PYLINE.m, CPPLINE.m | JVM objects in process; MATLAB py.* in process; line-cli subprocess over model.json |
| Python | python (default), java, cpp | jar_dispatch.py, cpp_dispatch.py | Subprocess over model.json in both cases, so an install that never sets lang needs no JVM |
| Java, C++ | not applicable | not applicable | Each is itself a backend the other two hosts delegate to |
Coverage differs by backend. lang='python' is wired for MVA, NC, CTMC, MAM, FLD, SSA, AUTO, LN, ENV and JMT, the native Python SolverJMT writing the same JSIM document and running the same JMT.jar as the MATLAB wrapper does. lang='cpp' serves AG, AUTO, BA, CTMC, FLD, JMT, MAM, MVA, NC and SSA; LDES is a JSON subprocess client in every codebase and keeps its own path, while LQNS wraps external binaries (lqns, lqsim and qnsolver) the C++ port does not carry. An individual result a bridge cannot serve is refused with its reason, never answered from a different engine.
% MATLAB: name the engine that produces the numbers
model = Network('MyModel');
% ... define model ...
solver = MVA(model, 'lang','matlab'); % native MATLAB (default)
solver = MVA(model, 'lang','java'); % the canonical JAR, in process
solver = MVA(model, 'lang','python'); % native Python, through py.*
solver = MVA(model, 'lang','cpp'); % the C++ line-cli binary
AvgTable = solver.getAvgTable();
# Python: name the engine that produces the numbers
from line_solver import Network, MVA
model = Network('MyModel')
# ... define model ...
solver = MVA(model, lang='python') # native Python (default), no JVM
solver = MVA(model, lang='java') # the canonical JAR, as a subprocess
solver = MVA(model, lang='cpp') # the C++ line-cli binary
table = solver.getAvgTable()
The model.json Interchange
The canonical way a model crosses codebases is the LINE JSON document: a {format, version, model} envelope in the line-model format, carrying a Network, a LayeredNetwork, a Workflow or an Environment. All four implementations write it and read it back, and the same document is what the lang='cpp' bridges, the LDES subprocess clients and the cross-codebase parity harness put on the wire. A model saved in one language is the same model in the other three.
% MATLAB: write and read the interchange document
linemodel_save(model, 'model.json');
model = linemodel_load('model.json');
// Java: write and read the interchange document
import jline.io.LineModelIO;
import jline.lang.Network;
LineModelIO.save(model, "model.json");
Network reloaded = (Network) LineModelIO.load("model.json");
# Python: write and read the interchange document
from line_solver import save_model, load_model
save_model(model, 'model.json')
model = load_model('model.json')
// C++: write and read the interchange document
#include "line/io/network_reader.h"
#include "line/io/network_writer.h"
using namespace line;
qn::Network<double> model = io::read_network_json<double>("model.json");
io::write_network_json(model.get_struct(), "roundtrip.json");
Model Transformations and Formats
Beside its own interchange document, LINE imports and exports the formats of the tools it works with, converts between queueing formalisms, and writes a model back out as source code. Every row is ported to all four implementations. MATLAB is the reference, and each port carries its name: C++ spells it in lower case under line::io, and Python adds the snake_case form beside it (load_pnml, qn2lqn, parse_xml, write_xml).
| Document or transform | MATLAB | Java | Python | C++ |
|---|---|---|---|---|
| LINE JSON, write | linemodel_save | LineModelIO.save | save_model | io::linemodel_save |
| LINE JSON, read | linemodel_load | LineModelIO.load | load_model | io::linemodel_load |
JMT .jsimg, import | JSIM2LINE | new M2M().JSIM2LINE | JSIM2LINE | io::jsim2line |
JMT .jsimg, export | QN2JSIMG | QN2JSIMG.writeJSIM | QN2JSIMG | io::qn2jsimg |
JMT .jmva, import | JMVA2LINE | new M2M().JMVA2LINE | JMVA2LINE | io::jmva2line |
JMT .jmva, export | SolverJMT.writeJMVA | SolverJMT.writeJMVA | SolverJMT.writeJMVA | io::write_jmva |
| JMT, any format by extension | JMT2LINE | new M2M().JMT2LINE | JMT2LINE | io::jmt2line |
| LQN XML, import | LayeredNetwork.parseXML | LayeredNetwork.parseXML | LayeredNetwork.parseXML | io::read_lqnx_model |
| LQN XML, export | writeXML | writeXML | writeXML | io::write_lqnx |
| PNML (ISO/IEC 15909-2), import | pnml_load | PnmlIO.load | pnml_load | io::pnml_load |
| PNML, export | pnml_save | PnmlIO.save | pnml_save | io::pnml_save |
| Network → layered network | QN2LQN | QN2LQN.convert | QN2LQN | io::qn2lqn |
| Layered network → Network | LQN2QN | LQN2QN.convert | LQN2QN | io::lqn2qn |
| MAP network → random environment | map2renv | MAPQN2RENV.map2renv | map2renv | io::map2renv |
| WfCommons workflow, import | WfCommonsLoader.load | WfCommonsLoader.load | WfCommonsLoader.load | io::wfcommons_load |
| Network → MATLAB script | QN2MATLAB | QN2MATLAB.generate | qn2matlab | io::qn2matlab |
| Network → Java program | QN2JAVA | QN2JAVA.generate | qn2java | io::qn2java |
| Layered network → MATLAB script | LQN2MATLAB | LQN2MATLAB.generate | lqn2matlab | io::lqn2matlab |
| Layered network → Java program | LQN2JAVA | LQN2JAVA.generate | lqn2java | io::lqn2java |
| Any model → MATLAB or Java, by type | LINE2MATLAB, LINE2JAVA | LINE2MATLAB.write, LINE2JAVA.write | line2matlab, line2java | io::line2matlab, io::line2java |
| Network → TikZ diagram | toTikZ, exportTikZ | toTikZ, exportTikZ | toTikZ, exportTikZ | io::to_tikz, io::export_tikz |
| MATLAB ↔ JAR objects | JLINE.line_to_jline, JLINE.jline_to_line | not applicable | not applicable | not applicable |
Four entry points exist in one implementation only, because they serve that environment: M2M().MAT2LINE (Python, reads a MATLAB .mat model), qn2python and lqn2python (Python, write a native Python script), and LQN2UML.export (Java, draws a layered network as a UML diagram). The Java JMT importers are instance methods of M2M, so they are called on new M2M(). The C++ io::linemodel_load returns a std::variant over Network, layered network, Workflow and Environment; io::read_network_json is still there when the document is known to hold a Network.
% MATLAB: model transformations
model = Network('Example');
% ... build model ...
% Import a JMT model
model = JSIM2LINE('mymodel.jsimg');
AvgTable = MVA(model).getAvgTable();
% Import a layered model
lqn = LayeredNetwork.parseXML('system.lqnx');
solver = LN(lqn);
% Export to JMT and to LQN XML
QN2JSIMG(model, 'exported_model.jsimg');
lqn.writeXML('webservice.lqnx');
% Convert between formalisms, and generate scripts that rebuild each model
lqn = QN2LQN(model);
qn = LQN2QN(lqn);
LINE2MATLAB(model, 'rebuild_model.m');
LINE2MATLAB(lqn, 'rebuild_layered.m');
% Draw the network as a TikZ diagram
model.exportTikZ('network.pdf');
% Hand the model to the JAR and back
java_model = JLINE.line_to_jline(model);
recovered = JLINE.jline_to_line(java_model);
// Java: model transformations
import jline.io.LineModelIO;
import jline.io.M2M;
import jline.io.PnmlIO;
import jline.io.QN2JAVA;
import jline.io.QN2JSIMG;
import jline.io.QN2LQN;
import jline.lang.Network;
import jline.lang.layered.LayeredNetwork;
// Import a JMT model
Network model = new M2M().JSIM2LINE("mymodel.jsimg");
// Import a layered model
LayeredNetwork lqn = LayeredNetwork.parseXML("system.lqnx");
// Export to JMT, to LQN XML and to PNML
QN2JSIMG.writeJSIM(model, "exported_model.jsimg");
lqn.writeXML("webservice.lqnx");
PnmlIO.save(model, "model.pnml");
// Convert between formalisms, generate source code, and save the interchange document
LayeredNetwork asLayered = QN2LQN.convert(model);
String javaSource = QN2JAVA.generate(model, "rebuildModel");
LineModelIO.save(model, "model.json");
# Python: model transformations
from line_solver import LayeredNetwork, MVA, save_model
from line_solver.io import JSIM2LINE, QN2JSIMG, LQN2QN, pnml_save
from line_solver.api.io.code_gen import qn2matlab, qn2python
# Import a JMT model
model = JSIM2LINE('mymodel.jsimg')
table = MVA(model).getAvgTable()
# Import a layered model, and flatten it to a queueing network
lqn = LayeredNetwork.parse_xml('system.lqnx')
qn = LQN2QN(lqn)
# Export to JMT, to LQN XML and to PNML
QN2JSIMG(model, 'exported_model.jsimg')
lqn.write_xml('webservice.lqnx')
pnml_save(model, 'model.pnml')
# Generate scripts, draw the diagram, and save the interchange document
print(qn2python(model, 'my_model'))
print(qn2matlab(model, 'my_model'))
model.exportTikZ('network.pdf')
save_model(model, 'model.json')
// C++: model transformations
#include "line/io/code_gen.h"
#include "line/io/jmt_import.h"
#include "line/io/linemodel.h"
#include "line/io/lqn2qn.h"
#include "line/io/lqnx_io.h"
#include "line/io/pnml.h"
#include "line/io/qn2jsimg.h"
#include "line/io/qn2lqn.h"
#include "line/io/tikz.h"
#include <fstream>
using namespace line;
// Import a JMT model, a layered model, or any interchange document the other three write
qn::Network<double> model = io::jsim2line<double>("mymodel.jsimg");
lqn::LqnModel<double> lqn = io::read_lqnx_model<double>("system.lqnx");
io::LineModel<double> any = io::linemodel_load<double>("model.json");
// Export to JMT and PNML, and convert between formalisms
io::qn2jsimg(model, "exported_model.jsimg");
io::pnml_save(model.get_struct(), "model.pnml");
lqn::LqnModel<double> layered = io::qn2lqn(model);
qn::Network<double> flat = io::lqn2qn(lqn);
// Generate a MATLAB script, draw the diagram, write the interchange back out
std::ofstream script("rebuild_model.m");
io::qn2matlab(model, "my_model", script);
io::export_tikz(model, "network.pdf");
io::linemodel_save(model, "roundtrip.json");
Command-Line Interfaces
Each implementation ships a command line, and they take the same flags and read the same documents, so a model file can be handed to whichever engine is installed and the answers compared directly.
| Front end | Command | Reads | Writes |
|---|---|---|---|
| C++ | common/line-cli | json, jsim, jsimg, jsimw, lqnx, xml, pnml | readable, json |
| Java | java -jar common/jline.jar (jline.cli.LineCLI) | json, jsim, jsimg, jsimw, lqnx, xml, pnml | readable, json |
| Python | python3 -m line_solver.cli | json, jsim, jsimg, jsimw, lqnx, xml, pnml, mat, pkl | readable, json, csv, pickle, mat |
| MATLAB | matlab/cli/linemcr.m (MATLAB Compiler / Docker) | json, jsim, jsimg, jsimw, lqnx, xml, pnml | json, obj |
# One document, three engines, the same flags
common/line-cli -f model.json -i json -s mva -a avg
java -jar common/jline.jar -f model.json -i json -s mva -a avg
python3 -m line_solver.cli -f model.json -i json -s mva -a avg
# -o json everywhere, for a machine-readable answer
common/line-cli -f model.json -i json -s mva -a avg -o json
Two differences are deliberate. The native-Python front end additionally reads .mat and .pkl and writes CSV, pickle and .mat, because it is the one that runs inside a Python session. And the node and class indices of -n and -c are 1-based in line-cli, as every station index that CLI takes is, and 0-based in the JAR and Python front ends, which index as their language does; the lang bridges convert for the CLI they drive, so this only bites when a command line is moved by hand from one to the other.
REST API Interface
LINE ships a RESTful API for solving queueing models programmatically over HTTP. This enables integration with web applications, microservices, and any language that can make HTTP requests. The server lives in the io/rest-api/ Maven module, which is built separately from the root reactor and requires common/jline.jar on its classpath.
Starting the REST API Server
# Build the module (jline.jar must be in io/rest-api/common/)
cd io/rest-api
mvn clean package
# Start on the default port (8080)
java -cp target/line-rest.jar:common/jline.jar jline.rest.LineRestServer
# Custom port, API keys and rate limiting
java -cp target/line-rest.jar:common/jline.jar jline.rest.LineRestServer \
--port 9000 --api-keys key1,key2 --rate-limit 100
# Build and run the bundled image
cd io/rest-api
docker build -t line-rest .
docker run -p 8080:8080 line-rest
# Same, with authentication enabled
docker run -p 8080:8080 -e LINE_API_KEYS="key1,key2" line-rest
Note that python line-cli.py rest does not start this server: it is an alias of line-cli.py server and launches the JAR WebSocket server instead.
API Endpoints
All routes are served under the base path /api/v1.
| Endpoint | Method | Description |
|---|---|---|
/api/v1/models/solve | POST | Solve a model synchronously. Model, solver and options travel in the JSON body. |
/api/v1/models/solve/async | POST | Submit a solve as a background job, returning a job identifier. |
/api/v1/models/validate | POST | Check model syntax and structure without solving. |
/api/v1/models/convert | POST | Convert a model between supported formats. |
/api/v1/models/describe | POST | Return the model as the LINE JSON interchange document. |
/api/v1/jobs, /api/v1/jobs/{id} | GET, DELETE | List, inspect and cancel asynchronous jobs. |
/api/v1/jobs/{id}/stream | GET | Stream job progress as server-sent events. |
/api/v1/analysis/whatif | POST | Sweep one parameter and return the metric trajectory. |
/api/v1/analysis/sensitivity | POST | Rank parameters by the sensitivity of a chosen metric. |
/api/v1/analysis/bottleneck | POST | Identify saturated stations and report per-station metrics. |
/api/v1/solvers, /api/v1/solvers/{id} | GET | Solver catalogue, formats, features and notes. |
/api/v1/solvers/{id}/methods | POST | List the methods a solver admits for the posted model. |
/api/v1/health, /api/v1/ready, /api/v1/info | GET | Liveness, readiness (heap-based) and server information. |
/api/v1/metrics, /api/v1/metrics/json | GET | Request and solve-time counters, in Prometheus or JSON form. |
/api/v1/models/calibrate, /api/v1/traces/import | POST | Estimate service rates from observed metrics; build a model from traces. |
Solve Request Body
| Field | Type | Default | Description |
|---|---|---|---|
model.format | string | required | Input format: jsim, jsimg, jsimw, lqnx, xml, json |
model.content | string | required | Model document, optionally base64 encoded |
model.base64 | boolean | false | Whether content is base64 encoded |
solver | string | required | Solver: mva, ctmc, fluid, jmt, nc, ssa for networks; ln, lqns, mva, nc for layered models |
analysis | string | all | Analysis type: all, avg, sys |
options | object | solver defaults | Solver options (seed, samples, tolerance, method) |
Example Request
# Solve a JSIMG model using the MVA solver
curl -X POST http://localhost:8080/api/v1/models/solve \
-H "Content-Type: application/json" \
-d "{\"model\":{\"format\":\"jsimg\",\"content\":\"$(base64 -w0 mymodel.jsimg)\",\"base64\":true},
\"solver\":\"mva\",\"analysis\":\"all\"}"
# Health check
curl http://localhost:8080/api/v1/health
import base64
import requests
# Read model file
with open('mymodel.jsimg', 'rb') as f:
content = base64.b64encode(f.read()).decode('ascii')
# Solve using REST API
response = requests.post(
'http://localhost:8080/api/v1/models/solve',
json={
'model': {'format': 'jsimg', 'content': content, 'base64': True},
'solver': 'mva',
'analysis': 'all',
},
)
# Parse results
results = response.json()
print(results['status'], results['runtime'])
print(results['results']['avgTable'])
// Solve a model using fetch API
async function solveModel(modelContent) {
const response = await fetch(
'http://localhost:8080/api/v1/models/solve',
{
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({
model: { format: 'jsimg', content: modelContent, base64: false },
solver: 'mva',
analysis: 'all'
})
}
);
const results = await response.json();
console.log(results);
return results;
}
// Health check
fetch('http://localhost:8080/api/v1/health')
.then(r => r.json())
.then(console.log);
Response Format
A solve returns status (completed or failed), the solver used, the runtime in seconds, and the result tables. Station metrics are indexed by station and then by class; system metrics are indexed by chain. A failed solve carries an error field instead of results.
{
"status": "completed",
"solver": "mva",
"runtime": 0.123,
"results": {
"avgTable": {
"stations": ["Delay", "Queue1"],
"classes": ["Class1"],
"metrics": {
"QLen": [[0.444], [2.5]],
"Util": [[0.8], [0.8]],
"RespT": [[1.0], [3.125]],
"ResidT": [[1.0], [3.125]],
"ArvR": [[0.8], [0.8]],
"Tput": [[0.8], [0.8]]
}
},
"sysTable": {
"chains": ["Chain1"],
"metrics": {
"SysRespT": [4.125],
"SysTput": [0.8]
}
}
}
}