1function [tex, sys] = exportODEs(self, filename, notation)
2% [TEX, SYS] = EXPORTODES(FILENAME, NOTATION)
3% Export
the system of ODEs integrated by
the mean-field methods of
4% SolverFLD as a standalone LaTeX document, in a symbolic form that
is
5% both human and machine readable. The exported system corresponds to
6%
the solver method set in
the solver options (
default/matrix, pnorm,
7% closing, statedep, softmin).
10% filename - path of
the .tex file to write;
if empty or omitted
the
11% LaTeX source
is returned without writing a file
12% notation -
'scalar' (
default): one expanded ODE per state variable
13%
'matrix': compact matrix notation, dx/dt = W
'*theta(x) +
14% lambda for the matrix/pnorm methods and dx/dt = J*r(x) for
15% the closing/statedep/softmin methods
18% tex - LaTeX source (char)
19% sys - structural description of the ODE system (see solver_fluid_symodes)
21% The document header also carries the state-variable mapping as LaTeX
22% comments (lines starting with % STATE) for machine parsing.
24% Copyright (c) 2012-2026, Imperial College London
34 case {'scalar
','matrix
'}
37 line_error(mfilename, sprintf('Unknown notation
''%s
''. Valid notations: scalar, matrix.
', notation));
40options = self.getOptions;
42sn = sn_nonmarkov_toph(sn, options);
43sys = solver_fluid_symodes(sn, options);
44sys.x0 = build_x0(sys, sn, options);
46modelName = self.model.getName;
49%% header comments (machine readable)
51L{end+1} = '% Mean-field fluid ODE system exported by LINE SolverFLD
';
52L{end+1} = sprintf('%% model: %s
', modelName);
53L{end+1} = sprintf('%% method: %s
', sys.method);
56 L{end+1} = '% form: dx/dt = W^T*theta(x) + lambda
';
58 L{end+1} = '% form: dx/dt = J*r(x)
';
60L{end+1} = sprintf('%% notation: %s
', notation);
61L{end+1} = sprintf('%% nstates: %d
', n);
62if strcmp(sys.form,'J
')
63 L{end+1} = sprintf('%% nevents: %d
', sys.nevents);
66 L{end+1} = sprintf('%% STATE %d station=%s class=%s phase=%d
', s, ...
67 sys.stationNames{sys.stateStation(s)}, sys.classNames{sys.stateClass(s)}, sys.statePhase(s)); %#ok<AGROW>
69if strcmp(sys.form,'J
')
71 L{end+1} = sprintf('%% EVENT %d var=%d type=%s coeff=%.15g
', e, ...
72 sys.eventVar(e), sys.factorType{e}, sys.coeff(e)); %#ok<AGROW>
77L{end+1} = '\documentclass{article}
';
78L{end+1} = '\usepackage{amsmath}
';
79L{end+1} = '\usepackage[margin=2.5cm]{geometry}
';
80L{end+1} = '\allowdisplaybreaks
';
81L{end+1} = '\setcounter{MaxMatrixCols}{500}
';
82L{end+1} = '\begin{document}
';
83L{end+1} = '\section*{Mean-field fluid ODE system}
';
84L{end+1} = sprintf('\\noindent Model: \\texttt{%s}. Solver: \\texttt{SolverFLD}, method \\texttt{%s}, %s notation.
', ...
85 texesc(modelName), texesc(sys.method), notation);
88 L{end+1} = sprintf('The system has %d state variables and reads $\\frac{\\mathrm{d}\\mathbf{x}}{\\mathrm{d}t} = W^{\\top}\\theta(\\mathbf{x}) + \\boldsymbol{\\lambda}$.
', n);
90 L{end+1} = sprintf('The system has %d state variables and %d events and reads $\\frac{\\mathrm{d}\\mathbf{x}}{\\mathrm{d}t} = J\\,r(\\mathbf{x})$.
', n, sys.nevents);
93%% state variable legend
94L{end+1} = '\subsection*{State variables}
';
95L{end+1} = 'Each state variable $x_{s}$
is the mean number of jobs of a
class in a service phase at a station:';
96L{end+1} =
'\begin{center}';
99 s1 = min(n, s0+chunk-1);
100 L{end+1} =
'\begin{tabular}{rlll}'; %#ok<AGROW>
101 L{end+1} =
'\hline'; %#ok<AGROW>
102 L{end+1} =
'$s$ & station & class & phase\\'; %#ok<AGROW>
103 L{end+1} =
'\hline'; %#ok<AGROW>
105 L{end+1} = sprintf(
'%d & \\texttt{%s} & \\texttt{%s} & %d\\\\', s, ...
106 texesc(sys.stationNames{sys.stateStation(s)}), ...
107 texesc(sys.classNames{sys.stateClass(s)}), sys.statePhase(s)); %#ok<AGROW>
109 L{end+1} =
'\hline'; %#ok<AGROW>
110 L{end+1} =
'\end{tabular}'; %#ok<AGROW>
112 L{end+1} =
'\par\medskip'; %#ok<AGROW>
115L{end+1} =
'\end{center}';
118usedStations = unique(sys.stateStation(:))
';
119L{end+1} = '\begin{center}
';
120L{end+1} = '\begin{tabular}{rlll}
';
122L{end+1} = '$i$ & station & scheduling & $S_{i}$\\
';
125 L{end+1} = sprintf('%d & \\texttt{%s} & %s & $%s$\\\\
', i, ...
126 texesc(sys.stationNames{i}), texesc(sys.schedNames{i}), fmtnum(sys.S(i))); %#ok<AGROW>
129L{end+1} = '\end{tabular}
';
130L{end+1} = '\end{center}
';
132%% definitions of station-level auxiliary functions
133[T, varFactor, constTerm] = build_terms(sys);
134defs = build_defs(sys, varFactor);
136 L{end+1} = '\subsection*{Definitions}
';
137 L{end+1} = '\begin{align*}
';
138 for d = 1:length(defs)
139 L{end+1} = defs{d}; %#ok<AGROW>
141 L{end+1} = '\end{align*}
';
147 L{end+1} = '\subsection*{ODE system (scalar notation)}
';
148 L{end+1} = '\begin{align}
';
150 L{end+1} = render_equation(sys, s, T, varFactor, constTerm, s==n); %#ok<AGROW>
152 L{end+1} = '\end{align}
';
154 L{end+1} = '\subsection*{ODE system (matrix notation)}
';
157 haveLambda = any(sys.Alambda ~= 0);
159 L{end+1} = '\begin{equation}
';
160 L{end+1} = '\frac{\mathrm{d}\mathbf{x}}{\mathrm{d}t} = W^{\top}\,\theta(\mathbf{x}) + \boldsymbol{\lambda}
';
161 L{end+1} = '\end{equation}
';
163 L{end+1} = '\begin{equation}
';
164 L{end+1} = '\frac{\mathrm{d}\mathbf{x}}{\mathrm{d}t} = W^{\top}\,\theta(\mathbf{x})
';
165 L{end+1} = '\end{equation}
';
167 L{end+1} = 'with $\theta_{s}(\mathbf{x})$ given componentwise by
';
168 L{end+1} = '\begin{equation*}
';
169 L{end+1} = ['\theta(\mathbf{x}) =
', render_theta_vector(sys, varFactor)];
170 L{end+1} = '\end{equation*}
';
172 L{end+1} = '\begin{equation*}
';
173 L{end+1} = ['W^{\top} =
', render_num_matrix(sys.W')];
174 L{end+1} =
'\end{equation*}';
176 L{end+1} =
'\begin{equation*}';
177 L{end+1} = [
'\boldsymbol{\lambda} = ', render_num_vector(sys.Alambda),
'^{\top}'];
178 L{end+1} =
'\end{equation*}';
181 L{end+1} =
'\begin{equation}';
182 L{end+1} =
'\frac{\mathrm{d}\mathbf{x}}{\mathrm{d}t} = J\,r(\mathbf{x})';
183 L{end+1} =
'\end{equation}';
184 L{end+1} =
'with stoichiometry matrix';
185 L{end+1} =
'\begin{equation*}';
186 L{end+1} = [
'J = ', render_num_matrix(sys.J)];
187 L{end+1} =
'\end{equation*}';
188 L{end+1} =
'and event rate functions';
189 L{end+1} =
'\begin{align*}';
190 for e = 1:sys.nevents
191 fd = sys.factorData{e};
192 fstr = factor_tex(sys.eventVar(e), sys.factorType{e}, fd);
193 L{end+1} = sprintf(
'r_{%d}(\\mathbf{x}) &= %s%s', e, ...
194 term_tex(sys.coeff(e), fstr), tern(e<sys.nevents,
'\\',
'')); %#ok<AGROW>
196 L{end+1} =
'\end{align*}';
202 L{end+1} =
'\subsection*{Initial condition}';
203 L{end+1} =
'\begin{equation*}';
204 L{end+1} = [
'\mathbf{x}(0) = ', render_num_vector(sys.x0),
'^{\top}'];
205 L{end+1} =
'\end{equation*}';
209L{end+1} =
'\subsection*{Remarks}';
210L{end+1} =
'\begin{itemize}';
211L{end+1} = sprintf(
'\\item For each station $i$, $n_{i}(\\mathbf{x})$ denotes the total mass at the station and $S_{i}$ the number of servers (infinite-server stations use the closed job population, $%s$ denotes infinity).',
'\infty');
212L{end+1} =
'\item The numerical solver regularizes vanishing denominators with a small positive constant; these regularizations are omitted here.';
213if strcmp(sys.form,
'J') && any(strcmp(
'fcfsw', sys.factorType) | strcmp(
'fcfsws', sys.factorType))
214 L{end+1} =
'\item At FCFS stations, the mean phase residence times $w_{u} = -1/[D_{0}]_{kk}$ weight the backlog $\hat{n}_{i}$; the factors $w_{u}$ of the departing phases are folded into the rate coefficients.';
216if strcmp(sys.form,
'J') && any(strcmp(
'dps', sys.factorType))
217 L{end+1} =
'\item At DPS stations, weights are normalized to sum to one and the products $S_{i} w_{ir}$ are folded into the rate coefficients; the constant added to $\tilde{n}_{i}$ in the denominator mirrors the implementation in \texttt{ode\_rates\_closing}.';
219if any(sys.sched(unique(sys.stateStation)) == SchedStrategy.FCFS) && any(strcmp(sys.method, {
'matrix',
'closing'}))
220 L{end+1} =
'\item For FCFS stations with non-exponential service, the solver may iteratively re-fit the service distributions (non-exponential approximation); the exported system uses the nominal model parameters.';
222if isfield(options.config,
'hide_immediate') && options.config.hide_immediate
223 L{end+1} =
'\item \texttt{hide\_immediate} is enabled in the solver options: the numerical integration may further eliminate immediate transitions by state-space reduction; the exported system is the unreduced one.';
225L{end+1} =
'\end{itemize}';
226L{end+1} =
'\end{document}';
228tex = strjoin(L, newline);
229tex = sprintf(
'%s\n', tex);
232 fid = fopen(filename,
'w');
234 line_error(mfilename, sprintf(
'Cannot open file ''%s'' for writing.', filename));
236 fprintf(fid,
'%s', tex);
243function x0 = build_x0(sys, sn, options)
244% initial condition in
the exported state space
245init_sol = solver_fluid_initsol(sn, options);
246init_sol = init_sol(:);
259 if nphases(ist,r) == 0
261 x0_build(state,1) = 0; %#ok<AGROW>
263 for k = 1:nphases(ist,r)
265 if isnan(sn.rates(ist,r))
266 x0_build(state,1) = 0; %#ok<AGROW>
268 init_idx = init_idx + 1;
269 x0_build(state,1) = init_sol(init_idx); %#ok<AGROW>
275 x0 = x0_build(sys.keep);
276 x0(sys.isSource) = 0;
280function [T, varFactor, constTerm] = build_terms(sys)
281% T(s,v): constant coefficient of
the term driven by state variable v in
282%
the equation of state s; varFactor{v}:
factor descriptor of variable v;
283% constTerm(s): additive constant.
286varFactor = cell(n,1);
290 T(:, sys.isSource) = 0; % theta of Source states is identically zero
293 i = sys.stateStation(v);
295 varFactor{v} = struct('type
','lin
','station
',i,'class',sys.stateClass(v));
297 varFactor{v} = struct('type
',sys.smoothing,'station
',i,'class',sys.stateClass(v));
301 constTerm = sys.Alambda;
303 for e = 1:sys.nevents
305 T(:,v) = T(:,v) + sys.J(:,e) * sys.coeff(e);
306 if isempty(varFactor{v})
307 fd = sys.factorData{e};
308 fd.type = sys.factorType{e};
312 constTerm = zeros(n,1);
316function defs = build_defs(sys, varFactor)
317% station-level auxiliary definitions used by the factors
320M = length(sys.stationNames);
322needNT = false(M,1); % ntilde (DPS-weighted mass)
323needNH = false(M,1); % nhat (FCFS backlog)
333 % no auxiliary quantity
336 gdef{i} = sprintf('g_{%d}(\\mathbf{x}) &= \\frac{\\min(n_{%d}(\\mathbf{x}),\\, %s)}{n_{%d}(\\mathbf{x})}
', i, i, fmtnum(sys.S(i)), i);
339 gdef{i} = sprintf('g_{%d}(\\mathbf{x}) &= \\Bigl(1 + \\bigl(n_{%d}(\\mathbf{x})/%s\\bigr)^{%s}\\Bigr)^{-1/%s}
', ...
340 i, i, fmtnum(sys.S(i)), fmtnum(sys.pstar(i)), fmtnum(sys.pstar(i)));
343 gdef{i} = sprintf('g_{%d}(\\mathbf{x}) &= \\frac{1}{%s + \\tilde{n}_{%d}(\\mathbf{x})}
', i, fmtnum(f.c0), i);
350 % one definition per enabled class at the station, built below
351 case {'fcfsw
','fcfsws
'}
354 if strcmp(f.type,'fcfsw
')
355 gdef{i} = sprintf('g_{%d}(\\mathbf{x}) &= \\frac{\\min(n_{%d}(\\mathbf{x}),\\, %s)}{\\hat{n}_{%d}(\\mathbf{x})}
', i, i, fmtnum(sys.S(i)), i);
357 gdef{i} = sprintf('g_{%d}(\\mathbf{x}) &= \\frac{\\mathrm{softmin}\\bigl(n_{%d}(\\mathbf{x}),\\, %s\\bigr)}{\\hat{n}_{%d}(\\mathbf{x})}
', i, i, fmtnum(sys.S(i)), i);
364 vlist = find(sys.stateStation == i);
365 defs{end+1} = sprintf('n_{%d}(\\mathbf{x}) &= %s\\\\
', i, strjoin(arrayfun(@(v) sprintf('x_{%d}
',v), vlist(:)',
'UniformOutput',
false),
' + ')); %#ok<AGROW>
368% ntilde_i definitions (DPS-weighted station mass)
372 K = length(sys.classNames);
374 vlist = find(sys.stateStation == i & sys.stateClass == r);
376 parts{end+1} = sprintf(
'%s\\,(%s)', fmtnum(sys.dpsw(i,r)), strjoin(arrayfun(@(v) sprintf(
'x_{%d}',v), vlist(:)
', 'UniformOutput
', false), ' +
')); %#ok<AGROW>
379 defs{end+1} = sprintf('\\tilde{n}_{%d}(\\mathbf{x}) &= %s\\\\
', i, strjoin(parts, ' +
')); %#ok<AGROW>
382% nhat_i definitions (FCFS backlog weighted by mean phase residence times)
385 vlist = find(sys.stateStation == i);
386 parts = arrayfun(@(v) sprintf('%s\\,x_{%d}
', fmtnum(sys.fcfsPhaseW(v)), v), vlist(:)',
'UniformOutput', false);
387 defs{end+1} = sprintf(
'\\hat{n}_{%d}(\\mathbf{x}) &= %s\\\\', i, strjoin(parts,
' + ')); %#ok<AGROW>
393 defs{end+1} = sprintf(
'%s\\\\', gdef{i}); %#ok<AGROW>
396% per-
class piecewise DPS definitions
399 if ~isempty(f) && strcmp(f.type,
'dpspw')
402 d = sprintf('g_{%d,%d}(\\mathbf{x}) &= \\begin{cases} 1 & n_{%d}(\\mathbf{x}) \\le %s\\\\ \\dfrac{%s}{\\tilde{n}_{%d}(\\mathbf{x})} & n_{%d}(\\mathbf{x}) > %s \\end{cases}\\\\
', ...
403 i, r, i, fmtnum(sys.S(i)), fmtnum(sys.S(i)*sys.dpsw(i,r)), i, i, fmtnum(sys.S(i)));
404 if ~any(strcmp(defs, d))
405 defs{end+1} = d; %#ok<AGROW>
410if isfield(sys,'alpha
') && any(cellfun(@(f) ~isempty(f) && strcmp(f.type,'fcfsws
'), varFactor))
411 defs{end+1} = sprintf('\\mathrm{softmin}(a,b) &= \\frac{a\\,e^{-\\alpha a} + b\\,e^{-\\alpha b}}{e^{-\\alpha a} + e^{-\\alpha b}}, \\qquad \\alpha = %s\\\\
', fmtnum(sys.alpha));
413% strip the trailing line break of the last definition
415 defs{end} = regexprep(defs{end}, '\\\\$
', '');
419function s = render_equation(sys, sidx, T, varFactor, constTerm, isLast)
420% one align row for the ODE of state variable sidx
425 fstr = factor_tex(v, varFactor{v}.type, varFactor{v});
426 terms{end+1} = {c, fstr}; %#ok<AGROW>
429if constTerm(sidx) ~= 0
430 terms{end+1} = {constTerm(sidx), ''};
436 for k = 1:length(terms)
438 body = term_tex(abs(c), terms{k}{2});
441 parts{end+1} = ['-
', body]; %#ok<AGROW>
443 parts{end+1} = body; %#ok<AGROW>
447 parts{end+1} = [' -
', body]; %#ok<AGROW>
449 parts{end+1} = [' +
', body]; %#ok<AGROW>
452 % break long equations every four terms
453 if mod(k,4) == 0 && k < length(terms)
454 parts{end+1} = sprintf('\\nonumber\\\\\n&\\quad
'); %#ok<AGROW>
457 rhs = strjoin(parts, '');
459s = sprintf('\\frac{\\mathrm{d}x_{%d}}{\\mathrm{d}t} &= %s%s
', sidx, rhs, tern(~isLast,'\\
',''));
462function fstr = factor_tex(v, ftype, fdata)
463% LaTeX of the state-dependent factor of a term driven by variable v
466 fstr = sprintf('x_{%d}
', v);
467 case {'min
','pnorm
','dps
','fcfsw
','fcfsws
'}
468 fstr = sprintf('x_{%d}\\,g_{%d}(\\mathbf{x})
', v, fdata.station);
470 fstr = sprintf('x_{%d}\\,g_{%d,%d}(\\mathbf{x})
', v, fdata.station, fdata.class);
472 if isempty(fdata.others)
473 fstr = ''; % single-phase source class: constant unit mass
475 fstr = sprintf('\\bigl(1 - %s\\bigr)
', strjoin(arrayfun(@(u) sprintf('x_{%d}
',u), fdata.others(:)',
'UniformOutput',
false),
' - '));
480function s = term_tex(c, fstr)
481% LaTeX of a term with positive coefficient c and
factor fstr
487 s = sprintf(
'%s\\,%s', fmtnum(c), fstr);
491function s = render_theta_vector(sys, varFactor)
492% componentwise theta vector
for the matrix notation of
the W form
493rows = cell(sys.nstates,1);
498 rows{v} = factor_tex(v, varFactor{v}.type, varFactor{v});
501s = sprintf(
'\\begin{bmatrix} %s \\end{bmatrix}', strjoin(rows,
' \\\\ '));
504function s = render_num_matrix(A)
505% numeric matrix as a LaTeX bmatrix
506[m, ncol] = size(A); %#ok<ASGLU>
509 rows{r} = strjoin(arrayfun(@fmtnum, A(r,:),
'UniformOutput', false),
' & ');
511body = strjoin(rows,
' \\\\ ');
513 s = sprintf(
'{\\scriptsize\\begin{bmatrix} %s \\end{bmatrix}}', body);
515 s = sprintf(
'\\begin{bmatrix} %s \\end{bmatrix}', body);
519function s = render_num_vector(v)
520% numeric
column vector as a transposed LaTeX row
521s = sprintf(
'\\begin{pmatrix} %s \\end{pmatrix}', strjoin(arrayfun(@fmtnum, v(:)
', 'UniformOutput
', false), ' &
'));
524function s = fmtnum(v)
525% compact LaTeX-safe number formatting
532elseif v == round(v) && abs(v) < 1e15
533 s = sprintf('%d
', v);
535 s = sprintf('%.8g
', v);
539function s = texesc(s)
540% escape LaTeX special characters in identifiers
541s = regexprep(s, '([_%&#])
', '\\$1
');
544function out = tern(cond, a, b)