1function [ode_h,q_indices] = solver_fluid_odes(sn, N, Mu, phi, PH,
P, nservers, sched, schedparam, options)
2% [ODE_H,Q_INDICES] = SOLVER_FLUID_ODES(sn, N, MU, PHI, PH,
P, NSERVERS, SCHED, SCHEDPARAM)
4% Copyright (c) 2012-2026, Imperial College London
7M = length(nservers); % number of stations
8K = length(Mu{1}); % number of classes
11enabled =
false(M,K); % indicates whether a
class is served at a station
14% %enabled(i,c) = sum(
P(:,(i-1)*K+c) ) > 0;
15% %changed to consider rows instead of columns,
for response time
16% %analysis (first articial
class never returns to delay node)
17% enabled(i,c) = sum(
P((i-1)*K+c,:) ) > 0;
21q_indices = zeros(M,K);
29 q_indices(i,c) = cumsum;
30 elseif isempty(Mu{i}{c})
33 q_indices(i,c) = cumsum;
35 numphases = length(Mu{i}{c});
36 q_indices(i,c) = cumsum;
40 cumsum = cumsum + numphases;
44% to speed up convert sched strings in numerical values
46 switch sched(i) % source
47 case SchedStrategy.DPS
48 w(i,:) = schedparam(i,:);
52%% define ODE system to be returned
55 alpha = 20; % softmin parameter
56 ode_sm_h = @(t,x) ode_softmin(x, phi, Mu, PH, M, K, enabled, q_indices,
P, Kic, nservers, w, sched, alpha);
59 % p-norm smoothing method based on Ruuskanen et al., PEVA 151 (2021)
60 if isfield(options,
'pstar') && ~isempty(options.pstar)
61 pstar = options.pstar;
63 pstar = 20; % default p-norm parameter (similar behavior to softmin at alpha=20)
65 ode_pn_h = @(t,x) ode_pnorm(x, phi, Mu, PH, M, K, enabled, q_indices,
P, Kic, nservers, w, sched, pstar);
68 ode_sd_h = @(t,x) ode_statedep(x, phi, Mu, PH, M, K, enabled, q_indices,
P, Kic, nservers, w, sched);
71 % determine all
the jumps, and saves them for later use
72 all_jumps = ode_jumps_new(M, K, enabled, q_indices,
P, Kic);
73 % determines a vector with
the fixed part of
the rates,
74 % and defines
the indexes that correspond to
the events that occur
75 [rateBase, eventIdx] = ode_rate_base(sn, phi, Mu, PH, M, K, enabled, q_indices,
P, Kic, sched, all_jumps);
77 % Eliminate immediate transitions if requested
78 if isfield(options.config, 'hide_immediate') && options.config.hide_immediate
79 if isfield(options, 'verbose') && options.verbose >= VerboseLevel.DEBUG
80 fprintf('[DEBUG] Calling immediate elimination (hide_immediate=true)\n');
82 [all_jumps, rateBase, eventIdx, state_map] = ...
83 ode_eliminate_immediate(all_jumps, rateBase, eventIdx, sn, options);
85 if isfield(options, 'verbose') && options.verbose >= VerboseLevel.DEBUG
86 fprintf('[DEBUG] Skipping immediate elimination (hide_immediate=false)\n');
90 % Optional time-varying event-rate multiplier m(t). Two independent
91 % sources both reduce to a per-event multiplicative
factor applied to
92 %
the closing rate vector, evaluated at time t by clamped
93 % piecewise-linear interpolation (fluid_interpcols):
94 % (1) options.config.rate_traj = {tgrid, Mmat} - a caller-supplied
95 %
event multiplier matrix (numEvents x numel(tgrid));
this is how
96 %
the coupled LN layer transient injects time-varying inter-layer
97 % demand trajectories.
98 % (2) options.config.nhpp_sched - NHPP/non-homogeneous source
99 % intensities; expanded here to per-
event multipliers
using the
100 % station/
class -> event mapping (q_indices, eventIdx).
101 % When both are present they are multiplied on a common grid. Absent
102 % both,
the returned closure
is byte-identical to
the legacy autonomous
103 % form. This makes
the otherwise autonomous closing ODE non-autonomous.
104 numEvents = numel(rateBase);
105 [rt_tgrid, rt_Mmat] = solver_fluid_ratemult(numEvents, M, K, enabled, ...
106 q_indices, Kic, Mu, eventIdx, options);
108 ode_si_h = @(t,x) all_jumps * ode_rates_closing(x, M, K, enabled, q_indices, Kic, nservers, w, sched, rateBase, eventIdx);
110 ode_si_h = @(t,x) all_jumps * ( fluid_interpcols(rt_tgrid, rt_Mmat, t) .* ode_rates_closing(x, M, K, enabled, q_indices, Kic, nservers, w, sched, rateBase, eventIdx) );