1function data = solveCli(self, options, extraFlags)
2% DATA = SOLVECLI(OPTIONS, EXTRAFLAGS)
4% Fully JSON-mediated LDES solve. Serializes
the model natively to model.json
5% (linemodel_save, no in-process Java
object marshalling), runs
the LDES engine
6% as a subprocess exchanging only JSON (
"solve model.json -o result.json"), and
7% returns
the jsondecode
'd result struct. Returns [] on any failure.
9% The engine is either the native GraalVM binary (common/ldes) or, failing that,
10% "java -jar common/ldes.jar" (same shaded engine). No JPype/JLINE path remains.
12% EXTRAFLAGS is an optional cellstr of additional CLI tokens, e.g.
13% {'--timespan
','0,100',
'--trajectory'} or {
'--export-histogram'}.
15if nargin < 3 || isempty(extraFlags)
24cleaner = onCleanup(@() rmdirQuiet(tmpDir)); %#ok<NASGU>
25modelPath = fullfile(tmpDir,
'model.json');
26resultPath = fullfile(tmpDir,
'result.json');
28% Serialize
the model to
the CLI model.json format natively (no LINE2JLINE).
30 linemodel_save(self.model, modelPath);
32 line_debug(
'LDES solveCli: linemodel_save failed: %s', ME.message);
35if exist(modelPath,
'file') ~= 2
39% Resolve
the ordered engine command prefixes (native binary, then jar).
40runners = SolverLDES.getLdesRunners();
42 line_error(mfilename, [
'No runnable LDES engine found (neither the native ' ...
43 'common/ldes binary nor a Java runtime for common/ldes.jar).']);
46% Marked (
MMAP) source arrivals: an engine that predates
the MMAP JSON schema
47% deserializes no arrival process and idles forever instead of failing fast,
48% so
the fall-through never triggers. Prefer
the jar runner (always rebuilt
49% with
the sources) over
the AOT native binary
for such models.
51% Class dependence
beta_{i,r}(n)
is the same hazard and
is worse, because it
52% fails silently rather than by idling: an engine predating
the classDependence
53% JSON schema simply reads no scaling and simulates
the UNSCALED network,
54% returning plausible numbers that are wrong. Prefer
the jar runner for such
58 for ni = 1:numel(self.model.nodes)
59 nd = self.model.
nodes{ni};
60 if isa(nd,
'Source') && ~isempty(nd.markedClasses)
64 if isa(nd, '
Station') && ~isempty(nd.lcdScaling)
70 runners = runners(end:-1:1);
74% Flags shared across runners (mirror
the Python-native flag set).
75flags = sprintf('-s %d --seed %d', round(options.samples), round(options.seed));
76if isfield(options, 'method') && (ischar(options.method) || isstring(options.method)) ...
77 && ~strcmpi(
char(options.method), 'default')
78 flags = sprintf('%s --method %s', flags,
char(options.method));
80% Warm-start placement (station-major vector) via --initsol.
81if isfield(options, 'init_sol') && ~isempty(options.init_sol)
82 v = options.init_sol(:).';
83 flags = sprintf('%s --initsol %s', flags, strjoin(arrayfun(@(x) num2str(x, '%.10g'), v, ...
84 'UniformOutput', false), ','));
86% Discrete-time (slotted) mode. --slotlength implies --slotted on
the CLI side,
87% but both are emitted when
the length
is non-default so
the command line states
90if isfield(options, 'slotted') && ~isempty(options.slotted)
91 slottedOn = logical(options.slotted);
92elseif isfield(options, 'config') && isstruct(options.config) ...
93 && isfield(options.config, 'slotted') && ~isempty(options.config.slotted)
94 slottedOn = logical(options.config.slotted);
97 flags = sprintf('%s --slotted', flags);
99 if isfield(options, 'slotlength') && ~isempty(options.slotlength)
100 slotLen = options.slotlength;
101 elseif isfield(options, 'config') && isstruct(options.config) ...
102 && isfield(options.config, 'slotlength') && ~isempty(options.config.slotlength)
103 slotLen = options.config.slotlength;
106 flags = sprintf('%s --slotlength %.10g', flags, slotLen);
109% Convergence-based stopping, if configured on
the options.
110if isfield(options, 'cnvgon') && ~isempty(options.cnvgon) && options.cnvgon
111 flags = sprintf('%s --cnvgon', flags);
112 if isfield(options, 'iter_tol') && ~isempty(options.iter_tol) && isfinite(options.iter_tol)
113 flags = sprintf('%s --cnvgtol %.10g', flags, options.iter_tol);
116for i = 1:numel(extraFlags)
117 flags = sprintf('%s %s', flags, extraFlags{i});
120% Try each runner in order;
the native binary may lack some reflective features
121% (e.g. fork-join MMT serialization), so fall through to
the JVM jar on failure.
123for ri = 1:numel(runners)
124 if exist(resultPath,
'file') == 2
127 cmd = sprintf(
'%s solve "%s" -o "%s" %s', runners{ri}, modelPath, resultPath, flags);
128 [status, cmdout] = system(cmd);
130 if status == 0 && exist(resultPath,
'file') == 2
132 data = jsondecode(fileread(resultPath));
134 line_error(mfilename, sprintf(
'LDES result JSON parse failed: %s', ME.message));
136 if isstruct(data) && isfield(data,
'error')
137 line_error(mfilename, sprintf('LDES engine error: %s', data.error));
141 line_debug('LDES runner %d/%d failed (status=%d); trying next.', ri, numel(runners), status);
144line_error(mfilename, sprintf('LDES engine failed on all %d runner(s): %s', ...
145 numel(runners), lastOut));
148function rmdirQuiet(d)
149% RMDIRQUIET Remove directory d and contents, ignoring errors.