LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_fluid_symodes.m
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:
6%
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.
14%
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.
20%
21% Factor types (form 'J'):
22% 'lin' x_v
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
31%
32% Copyright (c) 2012-2026, Imperial College London
33% All rights reserved.
34
35M = sn.nstations;
36K = sn.nclasses;
37
38method = options.method;
39method = strrep(method, 'fluid.', '');
40switch method
41 case {'default','matrix','pnorm'}
42 form = 'W';
43 if strcmp(method,'default')
44 method = 'matrix';
45 end
46 case {'closing','statedep','softmin'}
47 form = 'J';
48 otherwise
49 line_error(mfilename, sprintf('Symbolic ODE export is unsupported for method ''%s''. Supported methods: default, matrix, pnorm, closing, statedep, softmin.', method));
50end
51
52sys = struct();
53sys.form = form;
54sys.method = method;
55sys.stationNames = cell(M,1);
56sys.classNames = cell(K,1);
57sys.schedNames = cell(M,1);
58for i = 1:M
59 sys.stationNames{i} = sn.nodenames{sn.stationToNode(i)};
60 sys.schedNames{i} = SchedStrategy.toText(sn.sched(i));
61end
62for r = 1:K
63 sys.classNames{r} = sn.classnames{r};
64end
65sys.sched = sn.sched;
66
67switch form
68 case 'W'
69 sys = symodes_wform(sys, sn, options, M, K);
70 case 'J'
71 sys = symodes_jform(sys, sn, M, K, method);
72end
73end
74
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.
78
79pie = sn.pie;
80PH = sn.proc;
81NK = sn.njobs';
82S = sn.nservers;
83S(isinf(S)) = sum(NK);
84nphases = sn.phases;
85
86% station-to-station routing matrix via stochastic complementation
87station_indices = [];
88for ist = 1:M
89 isf = sn.stationToStateful(ist);
90 for r = 1:K
91 station_indices = [station_indices, (isf-1)*K + r]; %#ok<AGROW>
92 end
93end
94P = dtmc_stochcomp(sn.rt, station_indices);
95
96% remove Sink->Source feedback routing for open classes
97for src_ist = 1:M
98 if sn.sched(src_ist) == SchedStrategy.EXT
99 for r = 1:K
100 if ~isnan(sn.rates(src_ist, r)) && sn.rates(src_ist, r) > 0
101 src_col = (src_ist - 1) * K + r;
102 for from_ist = 1:M
103 if from_ist ~= src_ist
104 for from_r = 1:K
105 P((from_ist - 1) * K + from_r, src_col) = 0;
106 end
107 end
108 end
109 end
110 end
111 end
112end
113
114% W = Psi + B*P*A'
115Psi = [];
116A = [];
117B = [];
118for ist = 1:M
119 for r = 1:K
120 if nphases(ist,r) == 0
121 Psi = blkdiag(Psi,0);
122 B = blkdiag(B,0);
123 A = blkdiag(A,NaN);
124 else
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}');
128 end
129 end
130end
131W = Psi + B*P*A';
132
133% exogenous arrival rates into queue phases
134source_arrivals = zeros(M, K);
135for src_ist = 1:M
136 if sn.sched(src_ist) == SchedStrategy.EXT
137 for r = 1:K
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);
140 end
141 end
142 end
143end
144Alambda_full = zeros(size(A,1), 1);
145state = 0;
146for ist = 1:M
147 for r = 1:K
148 if nphases(ist,r) > 0
149 if sn.sched(ist) == SchedStrategy.EXT
150 state = state + nphases(ist,r);
151 else
152 arrival_rate_to_queue = 0;
153 for src_ist = 1:M
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);
156 end
157 end
158 if arrival_rate_to_queue > 0
159 for k = 1:nphases(ist,r)
160 state = state + 1;
161 Alambda_full(state) = pie{ist}{r}(k) * arrival_rate_to_queue;
162 end
163 else
164 state = state + nphases(ist,r);
165 end
166 end
167 else
168 state = state + 1;
169 end
170 end
171end
172
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);
177state = 0;
178for ist = 1:M
179 for r = 1:K
180 if nphases(ist,r) == 0
181 state = state + 1;
182 stateStation(state) = ist;
183 stateClass(state) = r;
184 statePhase(state) = 0; % placeholder, removed by keep filter
185 else
186 for k = 1:nphases(ist,r)
187 state = state + 1;
188 stateStation(state) = ist;
189 stateClass(state) = r;
190 statePhase(state) = k;
191 end
192 end
193 end
194end
195
196keep = find(~isnan(sum(W,1)));
197W = W(keep,:);
198W = W(:,keep);
199sys.keep = keep;
200sys.W = W;
201sys.Alambda = Alambda_full(keep);
202sys.stateStation = stateStation(keep);
203sys.stateClass = stateClass(keep);
204sys.statePhase = statePhase(keep);
205sys.nstates = length(keep);
206sys.S = S;
207sys.isSource = (sn.sched(sys.stateStation) == SchedStrategy.EXT);
208
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));
212if use_pnorm
213 if isfield(options, 'pstar') && ~isempty(options.pstar)
214 pstar_val = options.pstar;
215 else
216 pstar_val = options.config.pstar;
217 end
218 if isscalar(pstar_val)
219 pstar_val = pstar_val * ones(M, 1);
220 end
221 sys.smoothing = 'pnorm';
222 sys.pstar = pstar_val(:);
223else
224 sys.smoothing = 'min';
225 sys.pstar = [];
226end
227end
228
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).
232
233Mu = sn.mu;
234Phi = sn.phi;
235PH = sn.proc;
236sched = sn.sched;
237rt = sn.rt;
238S = sn.nservers;
239N = sn.nclosedjobs;
240for ist = 1:M
241 for k = 1:K
242 if isnan(Mu{ist}{k})
243 Mu{ist}{k} = [];
244 Phi{ist}{k} = [];
245 end
246 end
247 if isinf(S(ist))
248 S(ist) = N;
249 end
250end
251
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));
254end
255
256% state indexing as in solver_fluid_odes
257q_indices = zeros(M,K);
258Kic = zeros(M,K);
259enabled = false(M,K);
260cs = 1;
261for i = 1:M
262 for c = 1:K
263 numphases = length(Mu{i}{c});
264 q_indices(i,c) = cs;
265 enabled(i,c) = numphases > 0;
266 Kic(i,c) = numphases;
267 cs = cs + numphases;
268 end
269end
270nstates = cs - 1;
271
272stateStation = zeros(nstates,1);
273stateClass = zeros(nstates,1);
274statePhase = zeros(nstates,1);
275for i = 1:M
276 for c = 1:K
277 for k = 1:Kic(i,c)
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;
281 end
282 end
283end
284
285% normalized DPS weights (as in ode_rates_closing/ode_statedep/ode_softmin)
286dpsw = ones(M,K);
287for i = 1:M
288 if sched(i) == SchedStrategy.DPS
289 dpsw(i,:) = sn.schedparam(i,:);
290 dpsw(i,:) = dpsw(i,:)/sum(dpsw(i,:));
291 end
292end
293
294% FCFS phase weights used by statedep/softmin: w = -1/D0(k,k)
295fcfsPhaseW = zeros(nstates,1);
296if any(strcmp(method, {'statedep','softmin'}))
297 for i = 1:M
298 if sched(i) == SchedStrategy.FCFS
299 for c = 1:K
300 if enabled(i,c)
301 for k = 1:Kic(i,c)
302 fcfsPhaseW(q_indices(i,c)+k-1) = -1/PH{i}{c}{1}(k,k);
303 end
304 end
305 end
306 end
307 end
308end
309
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];
314
315% event enumeration (departures first, then phase changes) as in
316% ode_jumps_new/ode_rate_base; zero-rate events are dropped
317J = zeros(nstates,0);
318coeff = [];
319eventVar = [];
320factorType = {};
321factorData = {};
322ne = 0;
323for i = 1:M
324 for c = 1:K
325 if enabled(i,c)
326 xic = q_indices(i,c);
327 for j = 1:M
328 for l = 1:K
329 if rt((i-1)*K+c,(j-1)*K+l) > 0
330 if isempty(PH{j}{l})
331 pievec = 1;
332 else
333 pievec = map_pie(PH{j}{l});
334 end
335 xjl = q_indices(j,l);
336 for ki = 1:Kic(i,c)
337 for kj = 1:Kic(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
341 end
342 if ~any(sched(i) == handledSched)
343 continue
344 end
345 end
346 base = Phi{i}{c}(ki) * Mu{i}{c}(ki) * rt((i-1)*K+c,(j-1)*K+l) * pievec(kj);
347 if base > 0
348 ne = ne + 1;
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;
352 J(:,ne) = col;
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>
358 end
359 end
360 end
361 end
362 end
363 end
364 end
365 end
366end
367for i = 1:M
368 for c = 1:K
369 if enabled(i,c)
370 if any(strcmp(method, {'statedep','softmin'})) && ~any(sched(i) == handledSched)
371 continue
372 end
373 xic = q_indices(i,c);
374 for ki = 1:(Kic(i,c) - 1)
375 for kip = 1:Kic(i,c)
376 if ki ~= kip
377 base = PH{i}{c}{1}(ki,kip);
378 if base > 0
379 ne = ne + 1;
380 col = zeros(nstates,1);
381 col(xic+ki-1) = -1;
382 col(xic+kip-1) = 1;
383 J(:,ne) = col;
384 coeff(ne,1) = base;
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);
389 end
390 end
391 end
392 end
393 end
394 end
395end
396
397sys.J = J;
398sys.coeff = coeff;
399sys.eventVar = eventVar;
400sys.factorType = factorType;
401sys.factorData = factorData;
402sys.nevents = ne;
403sys.nstates = nstates;
404sys.stateStation = stateStation;
405sys.stateClass = stateClass;
406sys.statePhase = statePhase;
407sys.S = S;
408sys.dpsw = dpsw;
409sys.fcfsPhaseW = fcfsPhaseW;
410if strcmp(method,'softmin')
411 sys.alpha = 20; % softmin parameter, as in solver_fluid_odes
412end
413end
414
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);
420switch method
421 case 'closing'
422 switch schedi
423 case SchedStrategy.INF
424 ftype = 'lin';
425 case SchedStrategy.EXT
426 if ki == 1
427 ftype = 'ext1';
428 fdata.others = (q_indices(i,c)+1):(q_indices(i,c)+Kic(i,c)-1);
429 else
430 ftype = 'lin';
431 end
432 case {SchedStrategy.PS, SchedStrategy.FCFS}
433 ftype = 'min';
434 case SchedStrategy.DPS
435 % ode_rates_closing uses denominator mean(w) + sum_r w_r*n_r
436 ftype = 'dps';
437 fdata.c0 = mean(dpsw(i,:));
438 coeff = coeff * S(i) * dpsw(i,c);
439 otherwise
440 % strategies without a case in ode_rates_closing keep rates = x
441 ftype = 'lin';
442 end
443 case {'statedep','softmin'}
444 switch schedi
445 case SchedStrategy.INF
446 ftype = 'lin';
447 case SchedStrategy.PS
448 ftype = 'min';
449 case SchedStrategy.FCFS
450 if strcmp(method,'softmin')
451 ftype = 'fcfsws';
452 else
453 ftype = 'fcfsw';
454 end
455 coeff = coeff * fcfsPhaseW(var);
456 case SchedStrategy.DPS
457 ftype = 'dpspw';
458 end
459end
460end
Definition Station.m:245