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.

Hostlang valuesBridgeTransport
MATLABmatlab (default), java, python, cppJLINE.m, PYLINE.m, CPPLINE.mJVM objects in process; MATLAB py.* in process; line-cli subprocess over model.json
Pythonpython (default), java, cppjar_dispatch.py, cpp_dispatch.pySubprocess over model.json in both cases, so an install that never sets lang needs no JVM
Java, C++not applicablenot applicableEach 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 transformMATLABJavaPythonC++
LINE JSON, writelinemodel_saveLineModelIO.savesave_modelio::linemodel_save
LINE JSON, readlinemodel_loadLineModelIO.loadload_modelio::linemodel_load
JMT .jsimg, importJSIM2LINEnew M2M().JSIM2LINEJSIM2LINEio::jsim2line
JMT .jsimg, exportQN2JSIMGQN2JSIMG.writeJSIMQN2JSIMGio::qn2jsimg
JMT .jmva, importJMVA2LINEnew M2M().JMVA2LINEJMVA2LINEio::jmva2line
JMT .jmva, exportSolverJMT.writeJMVASolverJMT.writeJMVASolverJMT.writeJMVAio::write_jmva
JMT, any format by extensionJMT2LINEnew M2M().JMT2LINEJMT2LINEio::jmt2line
LQN XML, importLayeredNetwork.parseXMLLayeredNetwork.parseXMLLayeredNetwork.parseXMLio::read_lqnx_model
LQN XML, exportwriteXMLwriteXMLwriteXMLio::write_lqnx
PNML (ISO/IEC 15909-2), importpnml_loadPnmlIO.loadpnml_loadio::pnml_load
PNML, exportpnml_savePnmlIO.savepnml_saveio::pnml_save
Network → layered networkQN2LQNQN2LQN.convertQN2LQNio::qn2lqn
Layered network → NetworkLQN2QNLQN2QN.convertLQN2QNio::lqn2qn
MAP network → random environmentmap2renvMAPQN2RENV.map2renvmap2renvio::map2renv
WfCommons workflow, importWfCommonsLoader.loadWfCommonsLoader.loadWfCommonsLoader.loadio::wfcommons_load
Network → MATLAB scriptQN2MATLABQN2MATLAB.generateqn2matlabio::qn2matlab
Network → Java programQN2JAVAQN2JAVA.generateqn2javaio::qn2java
Layered network → MATLAB scriptLQN2MATLABLQN2MATLAB.generatelqn2matlabio::lqn2matlab
Layered network → Java programLQN2JAVALQN2JAVA.generatelqn2javaio::lqn2java
Any model → MATLAB or Java, by typeLINE2MATLAB, LINE2JAVALINE2MATLAB.write, LINE2JAVA.writeline2matlab, line2javaio::line2matlab, io::line2java
Network → TikZ diagramtoTikZ, exportTikZtoTikZ, exportTikZtoTikZ, exportTikZio::to_tikz, io::export_tikz
MATLAB ↔ JAR objectsJLINE.line_to_jline, JLINE.jline_to_linenot applicablenot applicablenot 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 endCommandReadsWrites
C++common/line-clijson, jsim, jsimg, jsimw, lqnx, xml, pnmlreadable, json
Javajava -jar common/jline.jar (jline.cli.LineCLI)json, jsim, jsimg, jsimw, lqnx, xml, pnmlreadable, json
Pythonpython3 -m line_solver.clijson, jsim, jsimg, jsimw, lqnx, xml, pnml, mat, pklreadable, json, csv, pickle, mat
MATLABmatlab/cli/linemcr.m (MATLAB Compiler / Docker)json, jsim, jsimg, jsimw, lqnx, xml, pnmljson, 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.

EndpointMethodDescription
/api/v1/models/solvePOSTSolve a model synchronously. Model, solver and options travel in the JSON body.
/api/v1/models/solve/asyncPOSTSubmit a solve as a background job, returning a job identifier.
/api/v1/models/validatePOSTCheck model syntax and structure without solving.
/api/v1/models/convertPOSTConvert a model between supported formats.
/api/v1/models/describePOSTReturn the model as the LINE JSON interchange document.
/api/v1/jobs, /api/v1/jobs/{id}GET, DELETEList, inspect and cancel asynchronous jobs.
/api/v1/jobs/{id}/streamGETStream job progress as server-sent events.
/api/v1/analysis/whatifPOSTSweep one parameter and return the metric trajectory.
/api/v1/analysis/sensitivityPOSTRank parameters by the sensitivity of a chosen metric.
/api/v1/analysis/bottleneckPOSTIdentify saturated stations and report per-station metrics.
/api/v1/solvers, /api/v1/solvers/{id}GETSolver catalogue, formats, features and notes.
/api/v1/solvers/{id}/methodsPOSTList the methods a solver admits for the posted model.
/api/v1/health, /api/v1/ready, /api/v1/infoGETLiveness, readiness (heap-based) and server information.
/api/v1/metrics, /api/v1/metrics/jsonGETRequest and solve-time counters, in Prometheus or JSON form.
/api/v1/models/calibrate, /api/v1/traces/importPOSTEstimate service rates from observed metrics; build a model from traces.

Solve Request Body

FieldTypeDefaultDescription
model.formatstringrequiredInput format: jsim, jsimg, jsimw, lqnx, xml, json
model.contentstringrequiredModel document, optionally base64 encoded
model.base64booleanfalseWhether content is base64 encoded
solverstringrequiredSolver: mva, ctmc, fluid, jmt, nc, ssa for networks; ln, lqns, mva, nc for layered models
analysisstringallAnalysis type: all, avg, sys
optionsobjectsolver defaultsSolver 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]
      }
    }
  }
}