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)
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.
13% Reference: Sheldon, Tuncer, Casale,
"TBI: Transient Hierarchical
14% Modeling of Large-Scale Vehicle Sharing Systems", IEEE T-ITS.
16% Copyright (c) 2012-2026, Imperial College London
19iter_max = options.iter_max;
21timespan = options.timespan;
25if isfield(options.config,
'tbi_tol') && ~isempty(options.config.tbi_tol)
26 tbi_tol = options.config.tbi_tol;
28 tbi_tol = 1e-3; % absolute sup-norm tolerance on state trajectories
30if isfield(options.config,'tbi_iter_max') && ~isempty(options.config.tbi_iter_max)
31 tbi_iter_max = options.config.tbi_iter_max;
33 tbi_iter_max = 50; % maximum TBI iterations per time segment
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');
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');
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');
52 tbi_gs = true; % default
54if tbi_parallel && tbi_gs
55 tbi_gs = false; % Gauss-Seidel
is sequential by construction
61schedparam = sn.schedparam;
63% state indexing, mirrors solver_fluid_odes
66q_indices = zeros(M,K);
73 q_indices(i,c) = csum;
74 elseif isempty(Mu{i}{c})
76 q_indices(i,c) = csum;
78 numphases = length(Mu{i}{c});
79 q_indices(i,c) = csum;
83 csum = csum + numphases;
85 if sched(i) == SchedStrategy.DPS
86 w(i,:) = schedparam(i,:);
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);
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)
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);
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
116 enabled_k{kc} = enabled(cs,:);
117 Kic_k{kc} = Kic(cs,:);
120 sched_k{kc} = sched(cs);
121 q_idx_k{kc} = zeros(Mk(kc),K);
125 q_idx_k{kc}(a,c) = lsum;
126 lsum = lsum + Kic_k{kc}(a,c);
129 mask =
false(ndim,1);
133 mask(q_indices(i,c):(q_indices(i,c)+Kic(i,c)-1)) = true;
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
151 Je = all_jumps(idx, Ee);
152 keep = any(Je ~= 0, 1);
154 Jext{kc} = Je(:, keep);
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
171while (isfinite(timespan(2)) && T < timespan(2)) || (goon && iter < iter_max)
173 if toc(Tstart) > max_time
178 y0 = xvec_it{iter-1 +1}(:)
';
179 if iter == 1 % first iteration
180 T = min(timespan(2),abs(10/min(nonZeroRates)));
182 T = min(timespan(2),abs(10*iter/min(nonZeroRates)));
186 % frozen trajectory on this segment, initialized constant at the
187 % segment entry state (warm start of the waveform relaxation)
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);
199 Rfull(:,jg) = rates_h(Yprev(jg,:)');
201 cell_t = cell(1,ncells);
202 cell_y = cell(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);
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);
215 % Gauss-Seidel: refresh
this cell
's event rates on the
216 % exchange grid so later cells see the new trajectory
218 yl = interp1(tc, cell_y{kc}, min(max(tprev, tc(1)), tc(end)));
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});
227 tgrid =
union(tgrid, cell_t{kc});
229 % assemble
the new full trajectory on
the union time grid
230 Ynew = zeros(numel(tgrid), ndim);
233 tq = min(max(tgrid, tc(1)), tc(end));
234 Ynew(:,cellmask{kc}) = interp1(tc, cell_y{kc}, tq);
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(:)));
242 if delta < tbi_tol || toc(Tstart) > max_time
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);
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);
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);
264 T0 = T; %
for next segment
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)
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));
283 [tc, yc] = ode_solve_stiff(ode_c, trange, y0(idx), odeopt_c, options);
285 [tc, yc] = ode_solve(ode_c, trange, y0(idx), odeopt_c, options);
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);
293% Cross-cell inflow interpolation
is provided by
the shared helper
294% fluid_interpcols (formerly
the local tbi_interpcols).