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 and QNS wrap external binaries 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, and converts between queueing formalisms. The table gives the entry point in each implementation; a dash marks what is not ported there.
| Document or transform | MATLAB | Java | Python | C++ |
|---|---|---|---|---|
| LINE JSON, write | linemodel_save | LineModelIO.save | save_model | io::write_network_json |
| LINE JSON, read | linemodel_load | LineModelIO.load | load_model | io::read_network_json |
JMT .jsimg, import | JSIM2LINE | M2M.JSIM2LINE | M2M.JSIM2LINE | io::read_jsim |
JMT .jsimg, export | QN2JSIMG | QN2JSIMG.writeJSIM | QN2JSIMG | io::jmt_write_jsim |
| LQN XML, import | LayeredNetwork.parseXML | LayeredNetwork.parseXML | LayeredNetwork.parse_xml | lqn::read_lqnx |
| LQN XML, export | writeXML | writeXML | write_xml | lqn::write_lqnx |
| PNML (ISO/IEC 15909-2), import | pnml_load | PnmlIO.load | load_pnml | io::pnml_load |
| PNML, export | pnml_save | PnmlIO.save | save_pnml | io::pnml_save |
| Network → layered network | QN2LQN | QN2LQN.convert | qn2lqn | io::qn2lqn |
| Layered network → Network | LQN2QN | LQN2QN.convert | LQN2QN | not ported |
| MAP network → random environment | map2renv | MAPQN2RENV.mapqn2renv | map2renv | io::map2renv |
| WfCommons workflow, import | WfCommonsLoader.load | WfCommonsLoader.load | Workflow.fromWfCommons | via model.json |
| Model → source code | QN2MATLAB, QN2JAVA | not ported | qn2python, qn2java | not ported |
| MATLAB ↔ JAR objects | JLINE.line_to_jline, JLINE.jline_to_line | not applicable | not applicable | not applicable |
% 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 a script
lqn = QN2LQN(model);
QN2MATLAB(model, 'rebuild_model.m');
% 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.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, and save the interchange document
LayeredNetwork asLayered = QN2LQN.convert(model);
LineModelIO.save(model, "model.json");
# Python: model transformations
from line_solver import LayeredNetwork, MVA, save_model
from line_solver.io import M2M, QN2JSIMG, LQN2QN
from line_solver.io.pnml_io import save_pnml
from line_solver.api.io.code_gen import qn2python
# Import a JMT model
model = M2M().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')
save_pnml(model, 'model.pnml')
# Generate a script, and save the interchange document
print(qn2python(model, 'my_model'))
save_model(model, 'model.json')
// C++: model transformations
#include "line/io/jsim_reader.h"
#include "line/io/network_reader.h"
#include "line/io/network_writer.h"
#include "line/io/pnml.h"
#include "line/io/qn2lqn.h"
using namespace line;
// Import a JMT model, or the interchange document the other three write
qn::Network<double> model = io::read_jsim<double>("mymodel.jsimg");
qn::Network<double> same = io::read_network_json<double>("model.json");
// Export to PNML, convert to a layered model, write the interchange back out
io::pnml_save(model.get_struct(), "model.pnml");
lqn::LqnModel<double> layered = io::qn2lqn(model);
io::write_network_json(model.get_struct(), "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]
}
}
}
}