1function sys = solver_fluid_symodes(sn, options)
2% SYS = SOLVER_FLUID_SYMODES(SN, OPTIONS)
3% Build a symbolic description of
the mean-field ODE system integrated by
4% SolverFLD
for the ODE-based methods. Two representations are produced,
5% mirroring
the numerical solver code paths:
7% form =
'W': dx/dt = W
'*theta(x) + lambda (methods: default, matrix, pnorm)
8% W (nstates x nstates) phase-transition rate matrix
9% Alambda (nstates x 1) exogenous arrival rate vector
10% theta_s = x_s * min(n_i, S_i)/n_i ('min
' smoothing), or
11% = x_s * (1+(n_i/S_i)^p_i)^(-1/p_i) ('pnorm
' smoothing)
12% with theta_s = 0 for states of Source (EXT) stations, and n_i the
13% total mass at the station i owning state s.
15% form = 'J
': dx/dt = J*r(x) (methods: closing, statedep, softmin)
16% J (nstates x nevents) stoichiometry (jump) matrix
17% r_e(x) = coeff(e) * factor_e(x), where factor_e depends on the
18% scheduling strategy of the station owning the state
19% variable that drives event e.
21% Factor types (form 'J
'):
23% 'min
' x_v * min(n_i, S_i)/n_i
24% 'ext1
' 1 - sum of x over phases 2..end of the class at the source
25% 'dps
' x_v / (c0 + ntilde_i), scaled weights folded into coeff;
26% ntilde_i = sum_r w_ir n_ir (normalized DPS weights)
27% 'dpspw
' piecewise: x_v if n_i <= S_i, else S_i*w_ir*x_v/ntilde_i
28% 'fcfsw
' x_v * min(n_i, S_i)/nhat_i, phase weight folded into coeff;
29% nhat_i = sum_u w_u x_u with w_u = -1/D0(k,k) (mean phase time)
30% 'fcfsws
' x_v * softmin(n_i, S_i; alpha)/nhat_i, phase weight in coeff
32% Copyright (c) 2012-2026, Imperial College London
38method = options.method;
39method = strrep(method, 'fluid.
', '');
41 case {'default
','matrix
','pnorm
'}
43 if strcmp(method,'default')
46 case {'closing
','statedep
','softmin
'}
49 line_error(mfilename, sprintf('Symbolic ODE
export is unsupported
for method
''%s
''. Supported methods:
default, matrix, pnorm, closing, statedep, softmin.
', method));
55sys.stationNames = cell(M,1);
56sys.classNames = cell(K,1);
57sys.schedNames = cell(M,1);
59 sys.stationNames{i} = sn.nodenames{sn.stationToNode(i)};
60 sys.schedNames{i} = SchedStrategy.toText(sn.sched(i));
63 sys.classNames{r} = sn.classnames{r};
69 sys = symodes_wform(sys, sn, options, M, K);
71 sys = symodes_jform(sys, sn, M, K, method);
75function sys = symodes_wform(sys, sn, options, M, K)
76% Mirror the ODE construction of solver_fluid_matrix (Ruuskanen et al.,
77% PEVA 151 (2021)): dx/dt = W'*theta(x) + Alambda.
86% station-to-station routing matrix via stochastic complementation
89 isf = sn.stationToStateful(ist);
91 station_indices = [station_indices, (isf-1)*K + r]; %#ok<AGROW>
94P = dtmc_stochcomp(sn.rt, station_indices);
96% remove Sink->Source feedback routing for open classes
98 if sn.sched(src_ist) == SchedStrategy.EXT
100 if ~isnan(sn.rates(src_ist, r)) && sn.rates(src_ist, r) > 0
101 src_col = (src_ist - 1) * K + r;
103 if from_ist ~= src_ist
105 P((from_ist - 1) * K + from_r, src_col) = 0;
120 if nphases(ist,r) == 0
121 Psi = blkdiag(Psi,0);
125 Psi = blkdiag(Psi,PH{ist}{r}{1});
126 B = blkdiag(B,sum(PH{ist}{r}{2},2));
127 A = blkdiag(A,pie{ist}{r}
');
133% exogenous arrival rates into queue phases
134source_arrivals = zeros(M, K);
136 if sn.sched(src_ist) == SchedStrategy.EXT
138 if ~isnan(sn.rates(src_ist, r)) && sn.rates(src_ist, r) > 0
139 source_arrivals(src_ist, r) = sn.rates(src_ist, r);
144Alambda_full = zeros(size(A,1), 1);
148 if nphases(ist,r) > 0
149 if sn.sched(ist) == SchedStrategy.EXT
150 state = state + nphases(ist,r);
152 arrival_rate_to_queue = 0;
154 if source_arrivals(src_ist, r) > 0
155 arrival_rate_to_queue = arrival_rate_to_queue + source_arrivals(src_ist, r) *
P((src_ist - 1) * K + r, (ist - 1) * K + r);
158 if arrival_rate_to_queue > 0
159 for k = 1:nphases(ist,r)
161 Alambda_full(state) = pie{ist}{r}(k) * arrival_rate_to_queue;
164 state = state + nphases(ist,r);
173% state metadata over
the same enumeration, then keep-filter
174stateStation = zeros(size(A,1),1);
175stateClass = zeros(size(A,1),1);
176statePhase = zeros(size(A,1),1);
180 if nphases(ist,r) == 0
182 stateStation(state) = ist;
183 stateClass(state) = r;
184 statePhase(state) = 0; % placeholder, removed by keep filter
186 for k = 1:nphases(ist,r)
188 stateStation(state) = ist;
189 stateClass(state) = r;
190 statePhase(state) = k;
196keep = find(~isnan(sum(W,1)));
201sys.Alambda = Alambda_full(keep);
202sys.stateStation = stateStation(keep);
203sys.stateClass = stateClass(keep);
204sys.statePhase = statePhase(keep);
205sys.nstates = length(keep);
207sys.isSource = (sn.sched(sys.stateStation) == SchedStrategy.EXT);
209% smoothing selection mirrors use_pnorm in solver_fluid_matrix
210use_pnorm = isfield(options,
'pstar') && ~isempty(options.pstar) || ...
211 (isfield(options.config,
'pstar') && ~isempty(options.config.pstar));
213 if isfield(options,
'pstar') && ~isempty(options.pstar)
214 pstar_val = options.pstar;
216 pstar_val = options.config.pstar;
218 if isscalar(pstar_val)
219 pstar_val = pstar_val * ones(M, 1);
221 sys.smoothing = 'pnorm';
222 sys.pstar = pstar_val(:);
224 sys.smoothing = 'min';
229function sys = symodes_jform(sys, sn, M, K, method)
230% Mirror
the event enumeration of ode_jumps_new/ode_rate_base and
the rate
231% scaling of ode_rates_closing/ode_statedep/ode_softmin: dx/dt = J*r(x).
252if any(sched == SchedStrategy.EXT) && any(strcmp(method, {
'statedep',
'softmin'}))
253 line_error(mfilename, sprintf('The ''%s'' ODE method does not support open models, so their ODE system cannot be exported. Use
the ''matrix'' or ''closing'' method instead.', method));
256% state indexing as in solver_fluid_odes
257q_indices = zeros(M,K);
263 numphases = length(Mu{i}{c});
265 enabled(i,c) = numphases > 0;
266 Kic(i,c) = numphases;
272stateStation = zeros(nstates,1);
273stateClass = zeros(nstates,1);
274statePhase = zeros(nstates,1);
278 stateStation(q_indices(i,c)+k-1) = i;
279 stateClass(q_indices(i,c)+k-1) = c;
280 statePhase(q_indices(i,c)+k-1) = k;
285% normalized DPS weights (as in ode_rates_closing/ode_statedep/ode_softmin)
288 if sched(i) == SchedStrategy.DPS
289 dpsw(i,:) = sn.schedparam(i,:);
290 dpsw(i,:) = dpsw(i,:)/sum(dpsw(i,:));
294% FCFS phase weights used by statedep/softmin: w = -1/D0(k,k)
295fcfsPhaseW = zeros(nstates,1);
296if any(strcmp(method, {
'statedep',
'softmin'}))
298 if sched(i) == SchedStrategy.FCFS
302 fcfsPhaseW(q_indices(i,c)+k-1) = -1/PH{i}{c}{1}(k,k);
310% under statedep/softmin, stations with an unmatched scheduling strategy
311% contribute no outgoing events (their
switch has no otherwise branch)
312handledSched = [SchedStrategy.INF, SchedStrategy.EXT, SchedStrategy.PS, ...
313 SchedStrategy.FCFS, SchedStrategy.DPS];
315%
event enumeration (departures first, then phase changes) as in
316% ode_jumps_new/ode_rate_base; zero-rate events are dropped
326 xic = q_indices(i,c);
329 if rt((i-1)*K+c,(j-1)*K+l) > 0
333 pievec = map_pie(PH{j}{l});
335 xjl = q_indices(j,l);
338 if any(strcmp(method, {
'statedep',
'softmin'}))
339 if sched(i) == SchedStrategy.INF && j == i
340 continue % self-loop departures skipped at INF stations
342 if ~any(sched(i) == handledSched)
346 base = Phi{i}{c}(ki) * Mu{i}{c}(ki) * rt((i-1)*K+c,(j-1)*K+l) * pievec(kj);
349 col = zeros(nstates,1);
350 col(xic+ki-1) = col(xic+ki-1) - 1;
351 col(xjl+kj-1) = col(xjl+kj-1) + 1;
353 coeff(ne,1) = base; %#ok<AGROW>
354 eventVar(ne,1) = xic+ki-1; %#ok<AGROW>
355 [factorType{ne,1}, factorData{ne,1}, coeff(ne,1)] = ...
356 symodes_factor(method, sched(i), i, c, ki, coeff(ne,1), ...
357 q_indices, Kic, S, dpsw, fcfsPhaseW, xic+ki-1); %#ok<AGROW>
370 if any(strcmp(method, {
'statedep',
'softmin'})) && ~any(sched(i) == handledSched)
373 xic = q_indices(i,c);
374 for ki = 1:(Kic(i,c) - 1)
377 base = PH{i}{c}{1}(ki,kip);
380 col = zeros(nstates,1);
385 eventVar(ne,1) = xic+ki-1;
386 [factorType{ne,1}, factorData{ne,1}, coeff(ne,1)] = ...
387 symodes_factor(method, sched(i), i, c, ki, coeff(ne,1), ...
388 q_indices, Kic, S, dpsw, fcfsPhaseW, xic+ki-1);
399sys.eventVar = eventVar;
400sys.factorType = factorType;
401sys.factorData = factorData;
403sys.nstates = nstates;
404sys.stateStation = stateStation;
405sys.stateClass = stateClass;
406sys.statePhase = statePhase;
409sys.fcfsPhaseW = fcfsPhaseW;
410if strcmp(method,
'softmin')
411 sys.alpha = 20; % softmin parameter, as in solver_fluid_odes
415function [ftype, fdata, coeff] = symodes_factor(method, schedi, i, c, ki, coeff, q_indices, Kic, S, dpsw, fcfsPhaseW, var)
416% Scaling
factor applied to
the driving state variable of an event whose
417% source station
is i (class c, phase ki), matching
the per-strategy rate
418% scaling in ode_rates_closing (closing), ode_statedep and ode_softmin.
419fdata = struct('station', i, 'class', c);
423 case SchedStrategy.INF
425 case SchedStrategy.EXT
428 fdata.others = (q_indices(i,c)+1):(q_indices(i,c)+Kic(i,c)-1);
432 case {SchedStrategy.PS, SchedStrategy.FCFS}
434 case SchedStrategy.DPS
435 % ode_rates_closing uses denominator mean(w) + sum_r w_r*n_r
437 fdata.c0 = mean(dpsw(i,:));
438 coeff = coeff * S(i) * dpsw(i,c);
440 % strategies without a
case in ode_rates_closing keep rates = x
443 case {
'statedep',
'softmin'}
445 case SchedStrategy.INF
447 case SchedStrategy.PS
449 case SchedStrategy.FCFS
450 if strcmp(method,
'softmin')
455 coeff = coeff * fcfsPhaseW(var);
456 case SchedStrategy.DPS