LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_fluid_ratemult.m
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)
3%
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
8% autonomous closure.
9%
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)
18% .class class index
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).
25%
26% Copyright (c) 2012-2026, Imperial College London
27% All rights reserved.
28
29tgrid = [];
30Mmat = [];
31if ~isfield(options,'config') || isempty(options.config)
32 return
33end
34cfg = options.config;
35
36% -- source (1): caller-supplied rate_traj ------------------------------------
37user_tg = [];
38user_M = [];
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));
44 end
45end
46
47% -- source (2): NHPP source intensities --------------------------------------
48nhpp_tg = [];
49nhpp_M = [];
50if isfield(cfg,'nhpp_sched') && ~isempty(cfg.nhpp_sched)
51 sched = cfg.nhpp_sched;
52 % horizon over which to expand a (possibly cyclic) schedule
53 t0 = 0;
54 tend = Inf;
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);
58 end
59 for s = 1:numel(sched)
60 i = sched(s).station;
61 c = sched(s).class;
62 nh = sched(s).nhpp;
63 if ~enabled(i,c)
64 continue % class not served/active at this station in the fluid ODE
65 end
66 nominal = Mu{i}{c}(1);
67 if ~(nominal > 0)
68 continue
69 end
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();
73 if ~isfinite(tend)
74 if isfinite(period) && period > 0
75 thi = t0 + 3*period;
76 else
77 thi = t0 + 1;
78 end
79 else
80 thi = tend;
81 end
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);
86 for kic = 1:Kic(i,c)
87 rows = rows | (eventIdx(:) == (q_indices(i,c) + kic - 1));
88 end
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);
92 end
93end
94
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.
103sched_tg = [];
104sched_M = [];
105if isfield(cfg,'rate_sched') && ~isempty(cfg.rate_sched)
106 rs = cfg.rate_sched;
107 for s = 1:numel(rs)
108 i = rs(s).station;
109 c = rs(s).class;
110 if ~enabled(i,c)
111 continue
112 end
113 if isfield(rs(s),'nominal') && ~isempty(rs(s).nominal)
114 nominal = rs(s).nominal;
115 else
116 nominal = Mu{i}{c}(1);
117 end
118 if ~(nominal > 0)
119 continue
120 end
121 seg_t = rs(s).tgrid(:)';
122 rowmult = rs(s).rates(:)' / nominal;
123 rows = false(numEvents,1);
124 for kic = 1:Kic(i,c)
125 rows = rows | (eventIdx(:) == (q_indices(i,c) + kic - 1));
126 end
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);
130 end
131end
132
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);
136end
137
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();
144bp = bp(:)';
145period = nh.getPeriod();
146if nh.isCyclic() && isfinite(period) && period > 0
147 bounds = [];
148 kmax = ceil((thi - t0)/period) + 2;
149 for k = -1:kmax
150 bounds = [bounds, bp + k*period]; %#ok<AGROW>
151 end
152else
153 bounds = bp;
154end
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);
160for k = 1:nb
161 a = bounds(k);
162 b = bounds(k+1);
163 r = nh.getRateAt((a + b)/2);
164 seg_t(2*k-1) = a;
165 seg_r(2*k-1) = r;
166 seg_t(2*k) = max(a + neps, b - neps);
167 seg_r(2*k) = r;
168end
169end
170
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).
174if isempty(M1)
175 tg = tg2; Mg = M2; return
176end
177if isempty(M2)
178 tg = tg1; Mg = M1; return
179end
180tg = unique([tg1(:)', tg2(:)']);
181ng = numel(tg);
182Mg = ones(numEvents, ng);
183for j = 1:ng
184 Mg(:,j) = fluid_interpcols(tg1, M1, tg(j)) .* fluid_interpcols(tg2, M2, tg(j));
185end
186end
Definition Station.m:245