LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solveCli.m
1function data = solveCli(self, options, extraFlags)
2% DATA = SOLVECLI(OPTIONS, EXTRAFLAGS)
3%
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.
8%
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.
11%
12% EXTRAFLAGS is an optional cellstr of additional CLI tokens, e.g.
13% {'--timespan','0,100','--trajectory'} or {'--export-histogram'}.
14
15if nargin < 3 || isempty(extraFlags)
16 extraFlags = {};
17end
18data = [];
19
20tmpDir = tempname;
21if ~mkdir(tmpDir)
22 return;
23end
24cleaner = onCleanup(@() rmdirQuiet(tmpDir)); %#ok<NASGU>
25modelPath = fullfile(tmpDir, 'model.json');
26resultPath = fullfile(tmpDir, 'result.json');
27
28% Serialize the model to the CLI model.json format natively (no LINE2JLINE).
29try
30 linemodel_save(self.model, modelPath);
31catch ME
32 line_debug('LDES solveCli: linemodel_save failed: %s', ME.message);
33 return;
34end
35if exist(modelPath, 'file') ~= 2
36 return;
37end
38
39% Resolve the ordered engine command prefixes (native binary, then jar).
40runners = SolverLDES.getLdesRunners();
41if isempty(runners)
42 line_error(mfilename, ['No runnable LDES engine found (neither the native ' ...
43 'common/ldes binary nor a Java runtime for common/ldes.jar).']);
44end
45
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.
50%
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
55% models too.
56if numel(runners) > 1
57 preferJar = false;
58 for ni = 1:numel(self.model.nodes)
59 nd = self.model.nodes{ni};
60 if isa(nd, 'Source') && ~isempty(nd.markedClasses)
61 preferJar = true;
62 break;
63 end
64 if isa(nd, 'Station') && ~isempty(nd.lcdScaling)
65 preferJar = true;
66 break;
67 end
68 end
69 if preferJar
70 runners = runners(end:-1:1);
71 end
72end
73
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));
79end
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), ','));
85end
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
88% the intent explicitly.
89slottedOn = false;
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);
95end
96if slottedOn
97 flags = sprintf('%s --slotted', flags);
98 slotLen = 1;
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;
104 end
105 if slotLen ~= 1
106 flags = sprintf('%s --slotlength %.10g', flags, slotLen);
107 end
108end
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);
114 end
115end
116for i = 1:numel(extraFlags)
117 flags = sprintf('%s %s', flags, extraFlags{i});
118end
119
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.
122lastOut = '';
123for ri = 1:numel(runners)
124 if exist(resultPath, 'file') == 2
125 delete(resultPath);
126 end
127 cmd = sprintf('%s solve "%s" -o "%s" %s', runners{ri}, modelPath, resultPath, flags);
128 [status, cmdout] = system(cmd);
129 lastOut = cmdout;
130 if status == 0 && exist(resultPath, 'file') == 2
131 try
132 data = jsondecode(fileread(resultPath));
133 catch ME
134 line_error(mfilename, sprintf('LDES result JSON parse failed: %s', ME.message));
135 end
136 if isstruct(data) && isfield(data, 'error')
137 line_error(mfilename, sprintf('LDES engine error: %s', data.error));
138 end
139 return;
140 end
141 line_debug('LDES runner %d/%d failed (status=%d); trying next.', ri, numel(runners), status);
142end
143
144line_error(mfilename, sprintf('LDES engine failed on all %d runner(s): %s', ...
145 numel(runners), lastOut));
146end
147
148function rmdirQuiet(d)
149% RMDIRQUIET Remove directory d and contents, ignoring errors.
150try
151 if exist(d, 'dir')
152 rmdir(d, 's');
153 end
154catch
155end
156end
Definition fjtag.m:157
Definition Station.m:245