1function [tgrid, Mmat] = solver_fluid_ratemult(numEvents, M, K, enabled, q_indices, Kic, Mu, eventIdx, options)
2% [TGRID, MMAT] = SOLVER_FLUID_RATEMULT(NUMEVENTS, M, K, ENABLED, Q_INDICES, KIC, MU, EVENTIDX, OPTIONS)
4% Build
the time-varying per-
event rate multiplier
for the closing fluid ODE.
5% Returns TGRID (1 x ngrid, strictly increasing) and MMAT (numEvents x ngrid);
6%
event e
is scaled at time t by fluid_interpcols(TGRID, MMAT, t)(e). Returns
7% [] when no time-varying source
is configured, so
the caller keeps
the legacy
10% Two independent, composable sources are honoured (both reduce to a per-event
11% multiplicative
factor because
the closing rate
is rate = rateBase .* theta(x)
12% and rateBase
is linear in
the station-
class service/arrival rate):
13% (1) options.config.rate_traj = {tgrid, Mmat} - a caller-supplied
event
14% multiplier matrix (used by
the coupled LN layer transient).
15% (2) options.config.nhpp_sched - a
struct array of non-homogeneous
16% (NHPP) source intensities, each with fields:
17% .station station index in
the sn station space (must be EXT/source)
19% .nhpp
the process handle exposing getRateAt(t) and,
for grid
20% construction, getBreakpoints/getPeriod/isCyclic
21% The nominal (time-average) rate baked into rateBase
for that
22% station-
class is Mu{station}{
class}(1);
the multiplier
is
23% getRateAt(t)/nominal, applied to every
event sourced at that station
24% and
class (eventIdx == q_indices(station,
class) + kic - 1).
26% Copyright (c) 2012-2026, Imperial College London
31if ~isfield(options,
'config') || isempty(options.config)
36% -- source (1): caller-supplied rate_traj ------------------------------------
39if isfield(cfg,'rate_traj') && ~isempty(cfg.rate_traj)
40 user_tg = cfg.rate_traj{1}(:)
';
41 user_M = cfg.rate_traj{2};
42 if size(user_M,1) ~= numEvents
43 line_error(mfilename, sprintf('rate_traj multiplier matrix has %d rows but
the closing ODE has %d events.
', size(user_M,1), numEvents));
47% -- source (2): NHPP source intensities --------------------------------------
50if isfield(cfg,'nhpp_sched
') && ~isempty(cfg.nhpp_sched)
51 sched = cfg.nhpp_sched;
52 % horizon over which to expand a (possibly cyclic) schedule
55 if isfield(options,'timespan
') && numel(options.timespan) >= 2
56 if isfinite(options.timespan(1)), t0 = options.timespan(1); end
57 tend = options.timespan(2);
59 for s = 1:numel(sched)
64 continue % class not served/active at this station in the fluid ODE
66 nominal = Mu{i}{c}(1);
70 % horizon: for a non-finite timespan use a few periods so a cyclic
71 % schedule is represented rather than clamped after one segment
72 period = nh.getPeriod();
74 if isfinite(period) && period > 0
82 [seg_t, seg_r] = local_nhpp_steps(nh, t0, thi);
83 rowmult = seg_r / nominal;
84 % rows: all events sourced at (i,c) across its phases
85 rows = false(numEvents,1);
87 rows = rows | (eventIdx(:) == (q_indices(i,c) + kic - 1));
89 thisM = ones(numEvents, numel(seg_t));
90 thisM(rows,:) = repmat(rowmult, sum(rows), 1);
91 [nhpp_tg, nhpp_M] = local_merge(nhpp_tg, nhpp_M, seg_t, thisM, numEvents);
95% -- source (3): explicit per-(station,class) rate trajectories ---------------
96% options.config.rate_sched is a struct array with fields:
97% .station, .class station/class in the sn space
98% .tgrid, .rates the time-varying rate (same length)
99% .nominal (opt) nominal rate baked into rateBase (default Mu{i}{c}(1))
100% Used by the coupled LN layer transient to inject time-varying inter-layer
101% demand (think-time / service) trajectories, reusing the same station-class
102% -> event expansion as the NHPP path.
105if isfield(cfg,'rate_sched
') && ~isempty(cfg.rate_sched)
113 if isfield(rs(s),'nominal
') && ~isempty(rs(s).nominal)
114 nominal = rs(s).nominal;
116 nominal = Mu{i}{c}(1);
121 seg_t = rs(s).tgrid(:)';
122 rowmult = rs(s).rates(:)
' / nominal;
123 rows = false(numEvents,1);
125 rows = rows | (eventIdx(:) == (q_indices(i,c) + kic - 1));
127 thisM = ones(numEvents, numel(seg_t));
128 thisM(rows,:) = repmat(rowmult, sum(rows), 1);
129 [sched_tg, sched_M] = local_merge(sched_tg, sched_M, seg_t, thisM, numEvents);
133% -- compose all sources ------------------------------------------------------
134[tgrid, Mmat] = local_merge(user_tg, user_M, nhpp_tg, nhpp_M, numEvents);
135[tgrid, Mmat] = local_merge(tgrid, Mmat, sched_tg, sched_M, numEvents);
138function [seg_t, seg_r] = local_nhpp_steps(nh, t0, thi)
139% Build a step-faithful (time, rate) sampling of a piecewise-constant NHPP
140% intensity over [t0, thi]. Each segment contributes two samples at its start
141% and just before its end, so clamped-linear interpolation reproduces the step
142% with a negligible transition ramp.
143bp = nh.getBreakpoints();
145period = nh.getPeriod();
146if nh.isCyclic() && isfinite(period) && period > 0
148 kmax = ceil((thi - t0)/period) + 2;
150 bounds = [bounds, bp + k*period]; %#ok<AGROW>
155bounds = unique([t0, bounds(bounds > t0 & bounds < thi), thi]);
156neps = max(1e-9, 1e-6*(thi - t0));
157nb = numel(bounds) - 1;
158seg_t = zeros(1, 2*nb);
159seg_r = zeros(1, 2*nb);
163 r = nh.getRateAt((a + b)/2);
166 seg_t(2*k) = max(a + neps, b - neps);
171function [tg, Mg] = local_merge(tg1, M1, tg2, M2, numEvents)
172% Merge two
event-multiplier trajectories onto
the union time grid by
173% elementwise product (identity where a source
is silent).
175 tg = tg2; Mg = M2;
return
178 tg = tg1; Mg = M1;
return
180tg = unique([tg1(:)
', tg2(:)']);
182Mg = ones(numEvents, ng);
184 Mg(:,j) = fluid_interpcols(tg1, M1, tg(j)) .* fluid_interpcols(tg2, M2, tg(j));