LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
solver_fluid_tbi_iteration.m
1function [xvec_it, xvec_t, t, iter] = solver_fluid_tbi_iteration(sn, N, Mu, Phi, PH, P, S, xvec_it, ydefault, slowrate, Tstart, max_time, options)
2% [XVEC_IT, XVEC_T, T, ITER] = SOLVER_FLUID_TBI_ITERATION(SN, N, MU, PHI, PH, P, S, XVEC_IT, YDEFAULT, SLOWRATE, TSTART, MAX_TIME, OPTIONS)
3%
4% Trajectory-based iteration (TBI) for the transient fluid solution.
5% The station set is partitioned into cells (tbi_partition). On each time
6% segment, the IVP of every cell is solved with the state of the other
7% cells frozen at the trajectory computed in the previous iteration (Gauss-Seidel or Jacobi
8% waveform relaxation); iterations repeat until the trajectory sup-norm gap
9% falls below options.config.tbi_tol. Cross-cell inflows are therefore
10% evaluated on frozen trajectories, interior flows on the live cell state,
11% matching the decomposed ODEs of the TBI method.
12%
13% Reference: Sheldon, Tuncer, Casale, "TBI: Transient Hierarchical
14% Modeling of Large-Scale Vehicle Sharing Systems", IEEE T-ITS.
15%
16% Copyright (c) 2012-2026, Imperial College London
17% All rights reserved.
18
19iter_max = options.iter_max;
20tol = options.tol;
21timespan = options.timespan;
22stiff = options.stiff;
23
24% TBI configuration
25if isfield(options.config,'tbi_tol') && ~isempty(options.config.tbi_tol)
26 tbi_tol = options.config.tbi_tol;
27else
28 tbi_tol = 1e-3; % absolute sup-norm tolerance on state trajectories
29end
30if isfield(options.config,'tbi_iter_max') && ~isempty(options.config.tbi_iter_max)
31 tbi_iter_max = options.config.tbi_iter_max;
32else
33 tbi_iter_max = 50; % maximum TBI iterations per time segment
34end
35if isfield(options.config,'hide_immediate') && options.config.hide_immediate
36 line_warning(mfilename,'The tbi method does not support hide_immediate, ignoring it.\n');
37end
38if isfield(options.config,'tbi_parallel') && ~isempty(options.config.tbi_parallel)
39 tbi_parallel = options.config.tbi_parallel && license('test','Distrib_Computing_Toolbox');
40 if options.config.tbi_parallel && ~tbi_parallel
41 line_warning(mfilename,'tbi_parallel requested but Parallel Computing Toolbox is unavailable, running serially.\n');
42 end
43else
44 tbi_parallel = false;
45end
46% iteration ordering: Gauss-Seidel (paper Alg. 1, cells see the trajectories of
47% cells already solved in the same iteration) or Jacobi (all cells see the
48% previous iteration; required by the parallel path)
49if isfield(options.config,'tbi_iteration') && ~isempty(options.config.tbi_iteration)
50 tbi_gs = strcmpi(options.config.tbi_iteration,'gs') || strcmpi(options.config.tbi_iteration,'gauss-seidel');
51else
52 tbi_gs = true; % default
53end
54if tbi_parallel && tbi_gs
55 tbi_gs = false; % Gauss-Seidel is sequential by construction
56end
57
58M = sn.nstations;
59K = sn.nclasses;
60sched = sn.sched;
61schedparam = sn.schedparam;
62
63% state indexing, mirrors solver_fluid_odes
64w = ones(M,K);
65enabled = false(M,K);
66q_indices = zeros(M,K);
67Kic = zeros(M,K);
68csum = 1;
69for i = 1:M
70 for c = 1:K
71 if isnan(Mu{i}{c})
72 numphases = 0;
73 q_indices(i,c) = csum;
74 elseif isempty(Mu{i}{c})
75 numphases = 0;
76 q_indices(i,c) = csum;
77 else
78 numphases = length(Mu{i}{c});
79 q_indices(i,c) = csum;
80 enabled(i,c) = true;
81 end
82 Kic(i,c) = numphases;
83 csum = csum + numphases;
84 end
85 if sched(i) == SchedStrategy.DPS
86 w(i,:) = schedparam(i,:);
87 end
88end
89ndim = csum - 1;
90
91% closing-method event machinery, built once on the full model
92all_jumps = ode_jumps_new(M, K, enabled, q_indices, P, Kic);
93[rateBase, eventIdx] = ode_rate_base(sn, Phi, Mu, PH, M, K, enabled, q_indices, P, Kic, sched, all_jumps);
94rates_h = @(x) ode_rates_closing(x, M, K, enabled, q_indices, Kic, S, w, sched, rateBase, eventIdx);
95
96% partition stations into cells and build per-cell state masks; the
97% restriction of the jump matrix to a cell's rows retains interior and
98% boundary events only through their effect on the cell state, so inbound
99% events read the frozen complement while outbound mass leaves the cell
100cells = tbi_partition(sn, options);
101ncells = numel(cells);
102cellmask = cell(1,ncells);
103Jint = cell(1,ncells); % interior/outbound jumps restricted to cell rows
104Jext = cell(1,ncells); % inbound jumps sourced at complement stations
105Eext = cell(1,ncells); % events feeding Jext (rates frozen per iteration)
106Eint = cell(1,ncells); % events sourced inside the cell (global indices)
107Mk = zeros(1,ncells);
108enabled_k = cell(1,ncells); q_idx_k = cell(1,ncells); Kic_k = cell(1,ncells);
109S_k = cell(1,ncells); w_k = cell(1,ncells); sched_k = cell(1,ncells);
110rateBase_k = cell(1,ncells); eventIdx_k = cell(1,ncells);
111for kc = 1:ncells
112 cs = sort(cells{kc}(:)');
113 % local station-restricted structures; the local state layout follows
114 % ascending global index order, so it coincides with the sorted mask
115 Mk(kc) = numel(cs);
116 enabled_k{kc} = enabled(cs,:);
117 Kic_k{kc} = Kic(cs,:);
118 S_k{kc} = S(cs);
119 w_k{kc} = w(cs,:);
120 sched_k{kc} = sched(cs);
121 q_idx_k{kc} = zeros(Mk(kc),K);
122 lsum = 1;
123 for a = 1:Mk(kc)
124 for c = 1:K
125 q_idx_k{kc}(a,c) = lsum;
126 lsum = lsum + Kic_k{kc}(a,c);
127 end
128 end
129 mask = false(ndim,1);
130 for i = cs
131 for c = 1:K
132 if Kic(i,c) > 0
133 mask(q_indices(i,c):(q_indices(i,c)+Kic(i,c)-1)) = true;
134 end
135 end
136 end
137 idx = find(mask);
138 cellmask{kc} = idx;
139 glob2loc = zeros(ndim,1);
140 glob2loc(idx) = 1:numel(idx);
141 % events sourced inside the cell: rates depend only on the live cell
142 % state because the closing scaling is local to the source station
143 isInt = glob2loc(eventIdx) > 0;
144 Eint{kc} = find(isInt);
145 rateBase_k{kc} = rateBase(Eint{kc});
146 eventIdx_k{kc} = glob2loc(eventIdx(Eint{kc}));
147 Jint{kc} = all_jumps(idx, Eint{kc});
148 % inbound events: sourced at complement stations but move mass into the
149 % cell; their rates are frozen per iteration and precomputed on the grid
150 Ee = find(~isInt);
151 Je = all_jumps(idx, Ee);
152 keep = any(Je ~= 0, 1);
153 Eext{kc} = Ee(keep);
154 Jext{kc} = Je(:, keep);
155end
156
157% horizon growth heuristic, mirrors solver_fluid_iteration
158nonZeroRates = slowrate(:);
159nonZeroRates = nonZeroRates(nonZeroRates > tol);
160nonZeroRates = nonZeroRates(isfinite(nonZeroRates));
161if isempty(nonZeroRates)
162 nonZeroRates = 1; % fallback when all rates are zero or infinite
163end
164
165goon = true;
166iter = 0;
167t = [];
168xvec_t = [];
169T0 = timespan(1);
170T = 0;
171while (isfinite(timespan(2)) && T < timespan(2)) || (goon && iter < iter_max)
172 iter = iter + 1;
173 if toc(Tstart) > max_time
174 goon = false;
175 break;
176 end
177
178 y0 = xvec_it{iter-1 +1}(:)';
179 if iter == 1 % first iteration
180 T = min(timespan(2),abs(10/min(nonZeroRates)));
181 else
182 T = min(timespan(2),abs(10*iter/min(nonZeroRates)));
183 end
184 trange = [T0, T];
185
186 % frozen trajectory on this segment, initialized constant at the
187 % segment entry state (warm start of the waveform relaxation)
188 tprev = [T0; T];
189 Yprev = [y0; y0];
190
191 delta = Inf;
192 for itn = 1:tbi_iter_max
193 % precompute the full closing-rate vector on the frozen grid once
194 % per iteration (shared by all cells), then collapse the inbound events
195 % of each cell to a drift time series of cell dimension
196 ngrid = numel(tprev);
197 Rfull = zeros(numel(rateBase), ngrid);
198 for jg = 1:ngrid
199 Rfull(:,jg) = rates_h(Yprev(jg,:)');
200 end
201 cell_t = cell(1,ncells);
202 cell_y = cell(1,ncells);
203 if tbi_parallel
204 parfor kc = 1:ncells
205 [cell_t{kc}, cell_y{kc}] = tbi_solve_cell(kc, tprev, Yprev, Rfull, trange, y0, ydefault, ...
206 cellmask, Jint, Jext, Eext, Mk, K, enabled_k, q_idx_k, Kic_k, S_k, w_k, sched_k, ...
207 rateBase_k, eventIdx_k, tol, stiff, options);
208 end
209 else
210 for kc = 1:ncells
211 [cell_t{kc}, cell_y{kc}] = tbi_solve_cell(kc, tprev, Yprev, Rfull, trange, y0, ydefault, ...
212 cellmask, Jint, Jext, Eext, Mk, K, enabled_k, q_idx_k, Kic_k, S_k, w_k, sched_k, ...
213 rateBase_k, eventIdx_k, tol, stiff, options);
214 if tbi_gs
215 % Gauss-Seidel: refresh this cell's event rates on the
216 % exchange grid so later cells see the new trajectory
217 tc = cell_t{kc};
218 yl = interp1(tc, cell_y{kc}, min(max(tprev, tc(1)), tc(end)));
219 for jg = 1:ngrid
220 Rfull(Eint{kc}, jg) = ode_rates_closing(yl(jg,:)', Mk(kc), K, enabled_k{kc}, q_idx_k{kc}, Kic_k{kc}, S_k{kc}, w_k{kc}, sched_k{kc}, rateBase_k{kc}, eventIdx_k{kc});
221 end
222 end
223 end
224 end
225 tgrid = tprev;
226 for kc = 1:ncells
227 tgrid = union(tgrid, cell_t{kc});
228 end
229 % assemble the new full trajectory on the union time grid
230 Ynew = zeros(numel(tgrid), ndim);
231 for kc = 1:ncells
232 tc = cell_t{kc};
233 tq = min(max(tgrid, tc(1)), tc(end));
234 Ynew(:,cellmask{kc}) = interp1(tc, cell_y{kc}, tq);
235 end
236 % sup-norm gap against the previous iteration trajectory
237 tq = min(max(tgrid, tprev(1)), tprev(end));
238 Yold = interp1(tprev, Yprev, tq);
239 delta = max(abs(Ynew(:) - Yold(:)));
240 tprev = tgrid;
241 Yprev = Ynew;
242 if delta < tbi_tol || toc(Tstart) > max_time
243 break
244 end
245 end
246 if delta >= tbi_tol && options.verbose > 0
247 line_warning(mfilename,'TBI iterations did not converge within tbi_iter_max=%d on segment [%g,%g], residual gap %g.\n', tbi_iter_max, T0, T, delta);
248 end
249 if options.verbose >= 2
250 line_printf('\nTBI segment %d [%g,%g]: %d iterations, %d cell solves, grid %d points, gap %g', iter, T0, T, itn, itn*ncells, numel(tprev), delta);
251 end
252
253 xvec_t(end+1:end+size(Yprev,1),:) = Yprev;
254 t(end+1:end+numel(tprev),:) = tprev;
255 xvec_it{iter +1} = xvec_t(end,:);
256 if options.verbose >= 2 && isfinite(N)
257 % closed-population conservation check; waveform relaxation loses
258 % mass transiently, at convergence the total must return to N
259 massgap = abs(sum(xvec_it{iter +1}) - N);
260 if massgap > max(0.01*N, 10*tbi_tol)
261 line_warning(mfilename,'TBI mass conservation gap %g at t=%g (N=%g).\n', massgap, T, N);
262 end
263 end
264 T0 = T; % for next segment
265
266 if T >= timespan(2)
267 goon = false;
268 end
269end
270end
271
272function [tc, yc] = tbi_solve_cell(kc, tprev, Yprev, Rfull, trange, y0, ydefault, ...
273 cellmask, Jint, Jext, Eext, Mk, K, enabled_k, q_idx_k, Kic_k, S_k, w_k, sched_k, ...
274 rateBase_k, eventIdx_k, tol, stiff, options)
275% solve one cell IVP against the frozen complement (one relaxation step)
276idx = cellmask{kc};
277Bk = Jext{kc} * Rfull(Eext{kc},:); % inbound drift on the frozen grid
278ode_c = @(tt,xc) Jint{kc} * ode_rates_closing(xc, Mk(kc), K, enabled_k{kc}, q_idx_k{kc}, Kic_k{kc}, S_k{kc}, w_k{kc}, sched_k{kc}, rateBase_k{kc}, eventIdx_k{kc}) ...
279 + fluid_interpcols(tprev, Bk, tt);
280odeopt_c = odeset('AbsTol', tol, 'RelTol', tol, 'NonNegative', 1:numel(idx));
281try
282 if stiff
283 [tc, yc] = ode_solve_stiff(ode_c, trange, y0(idx), odeopt_c, options);
284 else
285 [tc, yc] = ode_solve(ode_c, trange, y0(idx), odeopt_c, options);
286 end
287catch
288 line_printf('\nThe initial point is invalid, Fluid solver switching to default initialization.');
289 [tc, yc] = ode_solve(ode_c, trange, ydefault(idx), odeopt_c, options);
290end
291end
292
293% Cross-cell inflow interpolation is provided by the shared helper
294% fluid_interpcols (formerly the local tbi_interpcols).
Definition Station.m:245