LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
getTranAvgCoupled.m
1function [QNlqn_t, UNlqn_t, TNlqn_t] = getTranAvgCoupled(self, Qt, Ut, Tt) %#ok<INUSD>
2% [QNLQN_T,UNLQN_T,TNLQN_T] = GETTRANAVGCOUPLED(SELF,QT,UT,TT)
3%
4% Coupled layered transient by waveform relaxation over the LQN ensemble.
5% Unlike getTranAvgDecoupled, which freezes inter-layer demands at the
6% converged fixed point, this reconciles the per-layer transients iteratively:
7% each layer's fluid transient is driven by TIME-VARYING inter-layer demand
8% trajectories taken from the other layers' latest transients, and the loop
9% repeats until the trajectories stop changing (sup-norm gap over time). The
10% time-varying demands are injected into each layer's closing ODE through the
11% per-event rate multiplier (options.config.rate_sched -> solver_fluid_ratemult).
12%
13% Iteration 0 uses the frozen equilibrium demands, so it reproduces
14% getTranAvgDecoupled exactly; at convergence every layer relaxes to its fixed
15% point, so the endpoint equals getAvg. The return layout is the same
16% block-diagonal (station x class per layer) as getTranAvgDecoupled.
17%
18% Coupled channels: task think times (client delay) and synchronous-call
19% service demands (caller client station). Both are the dominant inter-layer
20% couplings; intra-layer host service stays at its equilibrium value.
21%
22% Copyright (c) 2012-2026, Imperial College London
23% All rights reserved.
24
25lqn = self.lqn;
26% Capture the transient horizon BEFORE getAvg: the layered fixed-point solve
27% strips options.timespan (each layer getAvg rejects a timespan), so reading it
28% afterwards would spuriously trigger the no-horizon fallback.
29ts = self.options.timespan;
30self.getAvg; % converge the fixed point; layer solvers primed with equilibrium
31
32if ~(numel(ts) >= 2 && all(isfinite(ts)))
33 % No finite transient horizon: nothing to co-evolve, defer to decoupled.
34 [QNlqn_t, UNlqn_t, TNlqn_t] = self.getTranAvgDecoupled();
35 return
36end
37
38E = self.nlayers;
39
40% relaxation controls (reuse the ensemble iteration budget / tolerance)
41maxit = 20;
42if isfield(self.options,'config') && isfield(self.options.config,'ln_transient_iter_max') ...
43 && ~isempty(self.options.config.ln_transient_iter_max)
44 maxit = self.options.config.ln_transient_iter_max;
45end
46tol = 1e-2;
47if isfield(self.options,'config') && isfield(self.options.config,'ln_transient_tol') ...
48 && ~isempty(self.options.config.ln_transient_tol)
49 tol = self.options.config.ln_transient_tol;
50end
51Ngrid = 100;
52tgrid = linspace(ts(1), ts(2), Ngrid)';
53
54% Per-layer sn (for node->station and class bookkeeping), cached once.
55layerSn = cell(1,E);
56for e = 1:E
57 layerSn{e} = self.ensemble{e}.getStruct;
58end
59
60% Iteration 0: decoupled transients (frozen equilibrium demands already set by
61% getAvg). local_run_layers returns per-layer {M,K} structs on the layer's own
62% time grid, plus per-(station,class) trajectories resampled onto tgrid.
63[blocks, traj] = local_run_layers(self, ts, tgrid, cell(1,E), layerSn);
64
65for iter = 1:maxit
66 trajPrev = traj;
67 % 1) recompute inter-layer demand trajectories from the latest layer traj
68 demand = local_recompute_demand(self, traj, tgrid, layerSn);
69 % 2) build the per-layer rate_sched injections from those demands
70 schedByLayer = local_build_rate_sched(self, demand, tgrid, layerSn);
71 % 3) re-run each layer with its injected time-varying demand
72 [blocks, traj] = local_run_layers(self, ts, tgrid, schedByLayer, layerSn);
73 % 4) convergence: sup-norm gap of the queue-length trajectories
74 gap = local_supnorm_gap(trajPrev, traj);
75 if self.options.verbose >= VerboseLevel.STD
76 line_printf('\nLN coupled transient: iter %d, sup-norm gap %.3e', iter, gap);
77 end
78 if gap < tol
79 break
80 end
81end
82
83% Assemble the block-diagonal aggregate exactly as getTranAvgDecoupled does.
84QNlqn_t = cell(0,0);
85UNlqn_t = cell(0,0);
86TNlqn_t = cell(0,0);
87for e = 1:E
88 [crows, ccols] = size(QNlqn_t);
89 Qe = blocks{e}.Q; Ue = blocks{e}.U; Te = blocks{e}.T;
90 QNlqn_t(crows+1:crows+size(Qe,1), ccols+1:ccols+size(Qe,2)) = Qe;
91 UNlqn_t(crows+1:crows+size(Ue,1), ccols+1:ccols+size(Ue,2)) = Ue;
92 TNlqn_t(crows+1:crows+size(Te,1), ccols+1:ccols+size(Te,2)) = Te;
93end
94end
95
96% -------------------------------------------------------------------------
97function [blocks, traj] = local_run_layers(self, ts, tgrid, schedByLayer, layerSn)
98% Run each layer's transient (optionally with an injected rate_sched) and
99% return: blocks{e} = struct with {M,K} cell fields Q,U,T (native handles, for
100% block-diagonal assembly); traj{e} = struct with M x K x numel(tgrid) arrays
101% Q,U,T,R resampled onto tgrid (R = Q./T residence via Little).
102E = self.nlayers;
103blocks = cell(1,E);
104traj = cell(1,E);
105ng = numel(tgrid);
106for e = 1:E
107 s = self.solvers{e};
108 savedTs = s.options.timespan;
109 hadSched = isfield(s.options.config,'rate_sched');
110 if hadSched
111 savedSched = s.options.config.rate_sched;
112 end
113 s.options.timespan = ts;
114 if ~isempty(schedByLayer{e})
115 s.options.config.rate_sched = schedByLayer{e};
116 elseif hadSched
117 s.options.config.rate_sched = [];
118 end
119 % Drop any cached transient: NetworkSolver.getTranAvg returns the stored
120 % self.result.Tran.Avg on repeat calls and does NOT re-solve on an
121 % options.config change, so without this the injected rate_sched would be
122 % silently ignored across waveform-relaxation iterations. reset() only
123 % clears the cached result, not the model's equilibrium service priming.
124 s.reset();
125 [Qe, Ue, Te] = s.getTranAvg();
126 s.options.timespan = savedTs;
127 if hadSched
128 s.options.config.rate_sched = savedSched;
129 elseif isfield(s.options.config,'rate_sched')
130 s.options.config = rmfield(s.options.config,'rate_sched');
131 end
132 be = struct(); % avoid struct('Q',{cell}) which builds a struct ARRAY
133 be.Q = Qe; be.U = Ue; be.T = Te;
134 blocks{e} = be;
135 M = size(Qe,1); K = size(Qe,2);
136 Q = zeros(M,K,ng); U = zeros(M,K,ng); T = zeros(M,K,ng); R = zeros(M,K,ng);
137 for i = 1:M
138 for r = 1:K
139 qv = local_resample(Qe{i,r}, tgrid);
140 uv = local_resample(Ue{i,r}, tgrid);
141 tv = local_resample(Te{i,r}, tgrid);
142 Q(i,r,:) = qv; U(i,r,:) = uv; T(i,r,:) = tv;
143 R(i,r,:) = qv ./ max(tv, GlobalConstants.FineTol);
144 end
145 end
146 traj{e} = struct('Q',Q,'U',U,'T',T,'R',R);
147end
148end
149
150% -------------------------------------------------------------------------
151function v = local_resample(h, tgrid)
152% Resample a transient handle (struct with .t/.metric, or a scalar) onto tgrid.
153if isstruct(h) && isfield(h,'t') && isfield(h,'metric') && numel(h.t) >= 2
154 v = interp1(h.t(:), h.metric(:), tgrid, 'linear', 'extrap');
155elseif isstruct(h) && isfield(h,'metric')
156 v = repmat(h.metric(end), numel(tgrid), 1);
157else
158 v = zeros(numel(tgrid),1);
159end
160end
161
162% -------------------------------------------------------------------------
163function demand = local_recompute_demand(self, traj, tgrid, layerSn)
164% Recompute the time-varying inter-layer demands from the layer trajectories,
165% pointwise in t, mirroring the scalar updateThinkTimes / updateMetricsDefault
166% formulas. Returns struct with:
167% thinkt : containers.Map tidx -> (ng x 1) think-time trajectory
168% callservt : containers.Map cidx -> (ng x 1) call service-time trajectory
169lqn = self.lqn;
170ng = numel(tgrid);
171thinkt = containers.Map('KeyType','double','ValueType','any');
172callservt = containers.Map('KeyType','double','ValueType','any');
173
174% Task think times: from the task's own server-layer utilization/throughput.
175for t = 1:lqn.ntasks
176 tidx = lqn.tshift + t;
177 if isnan(self.idxhash(tidx)) || lqn.isref(tidx)
178 continue
179 end
180 e = self.idxhash(tidx);
181 sIdx = self.ensemble{e}.attribute.serverIdx;
182 K = layerSn{e}.nclasses;
183 Uti = zeros(ng,1); Tti = zeros(ng,1);
184 for r = 1:K
185 Uti = Uti + squeeze(traj{e}.U(sIdx,r,:));
186 Tti = Tti + squeeze(traj{e}.T(sIdx,r,:));
187 end
188 njobs = max(self.njobs(tidx,:));
189 userthink = lqn.think{tidx}.getMean;
190 Tsafe = max(Tti, GlobalConstants.FineTol);
191 if lqn.sched(tidx) == SchedStrategy.INF
192 tk = (njobs - Uti) ./ Tsafe - userthink;
193 else
194 tk = njobs .* abs(1 - Uti) ./ Tsafe - userthink;
195 end
196 tk = max(GlobalConstants.Zero, tk) + userthink; % total mean incl. user think
197 thinkt(tidx) = tk;
198end
199
200% Synchronous-call service demands: callee entry response time * call mean.
201for cidx = 1:lqn.ncalls
202 if lqn.calltype(cidx) ~= CallType.SYNC
203 continue
204 end
205 eidx = lqn.callpair(cidx,2); % callee entry
206 tidx = lqn.parent(eidx); % callee task
207 if isnan(self.idxhash(tidx))
208 continue
209 end
210 e = self.idxhash(tidx);
211 sIdx = self.ensemble{e}.attribute.serverIdx;
212 % response time of the callee at its server, summed over the entry classes
213 K = layerSn{e}.nclasses;
214 Rc = zeros(ng,1);
215 for r = 1:K
216 typ = self.ensemble{e}.classes{r}.attribute(1);
217 if typ == LayeredNetworkElement.ENTRY && self.ensemble{e}.classes{r}.attribute(2) == eidx
218 Rc = Rc + squeeze(traj{e}.R(sIdx,r,:));
219 end
220 end
221 if all(Rc == 0)
222 % fall back to the entry's activities response time
223 Rc = squeeze(sum(traj{e}.R(sIdx,:,:),2));
224 end
225 callmean = 1.0;
226 if ~isempty(lqn.callproc{cidx}) && isa(lqn.callproc{cidx},'Distribution')
227 callmean = lqn.callproc{cidx}.getMean;
228 end
229 callservt(cidx) = Rc * callmean;
230end
231
232demand = struct('thinkt', thinkt, 'callservt', callservt);
233end
234
235% -------------------------------------------------------------------------
236function schedByLayer = local_build_rate_sched(self, demand, tgrid, layerSn)
237% Map the recomputed demand trajectories to per-layer rate_sched injections,
238% using the same update maps updateLayers uses to place setService calls.
239% options.config.ln_transient_channels selects which inter-layer coupling
240% channels are injected: 'both' (default), 'thinkt' (client-delay only), or
241% 'callservt' (synchronous-call service only). Used to isolate each channel's
242% contribution to the coupled transient.
243E = self.nlayers;
244schedByLayer = cell(1,E);
245lqn = self.lqn;
246channels = 'both';
247if isfield(self.options,'config') && isfield(self.options.config,'ln_transient_channels') ...
248 && ~isempty(self.options.config.ln_transient_channels)
249 channels = lower(self.options.config.ln_transient_channels);
250end
251
252% think-time channel (client delay of caller tasks)
253if any(strcmp(channels, {'both','thinkt'}))
254 map = self.thinkt_classes_updmap;
255 for r = 1:size(map,1)
256 idx = map(r,1); aidx = map(r,2); nodeidx = map(r,3); classidx = map(r,4);
257 e = self.idxhash(idx);
258 if isnan(e) || nodeidx ~= self.ensemble{e}.attribute.clientIdx
259 continue
260 end
261 if lqn.type(aidx) == LayeredNetworkElement.TASK && lqn.sched(aidx) ~= SchedStrategy.REF ...
262 && isKey(demand.thinkt, aidx)
263 d = demand.thinkt(aidx);
264 schedByLayer{e} = local_add_sched(schedByLayer{e}, layerSn{e}, nodeidx, classidx, tgrid, d);
265 end
266 end
267end
268
269% call-service channel (client station of caller for each sync call)
270if any(strcmp(channels, {'both','callservt'}))
271 map = self.call_classes_updmap;
272 for c = 1:size(map,1)
273 idx = map(c,1); cidx = map(c,2); nodeidx = map(c,3); classidx = map(c,4);
274 e = self.idxhash(idx);
275 if isnan(e) || nodeidx ~= self.ensemble{e}.attribute.clientIdx
276 continue
277 end
278 if isKey(demand.callservt, cidx)
279 d = demand.callservt(cidx);
280 schedByLayer{e} = local_add_sched(schedByLayer{e}, layerSn{e}, nodeidx, classidx, tgrid, d);
281 end
282 end
283end
284end
285
286% -------------------------------------------------------------------------
287function sched = local_add_sched(sched, sn, nodeidx, classidx, tgrid, demand)
288% Append one rate_sched entry that MODULATES the layer's equilibrium rate by
289% the ratio of the transient demand to its steady-state (end-of-horizon) value:
290% effective_rate(t) = nominal * demand(end)/demand(t).
291% Passing rate = 1/demand(t) and nominal_denominator = 1/demand(end) makes the
292% multiplier = demand(end)/demand(t), which is exactly 1 at the horizon end, so
293% the layer relaxes to its unmodified fixed point (oracle 1: endpoint == getAvg)
294% regardless of any small mismatch between the fluid residence Q/T and the
295% scalar equilibrium demand. During the transient the ratio modulates the rate.
296ist = sn.nodeToStation(nodeidx);
297if isnan(ist)
298 return
299end
300d = demand(:)';
301dend = d(end);
302if ~(dend > GlobalConstants.FineTol)
303 return % degenerate steady-state demand; skip this channel
304end
305% Bound the transient demand to a physical band around its steady-state value.
306% The Little's-law demand estimates (thinkt = njobs*(1-util)/tput, R = Q/T)
307% diverge at early t when the layer transient throughput is still ~0 (no jobs
308% have completed yet), producing unbounded multipliers. An inter-layer demand
309% cannot realistically deviate from its equilibrium by an unbounded factor; the
310% fluid ODE tolerates the spikes but the CTMC uniformization does not (an
311% enormous generator scaling ejects all mass from a state within one segment).
312% Clamping d to [dend/Cap, dend*Cap] keeps the multiplier m=dend/d in
313% [1/Cap, Cap] and is exactly 1 at the horizon end (endpoint preserved).
314Cap = 20;
315d = min(max(d, dend/Cap), dend*Cap);
316% self-normalising rate: nominal is the reciprocal of the steady-state demand,
317% so solver_fluid_ratemult's multiplier = rate/nominal = dend/d(t).
318rates = 1 ./ d;
319nominal = 1 / dend;
320entry = struct('station', ist, 'class', classidx, 'tgrid', tgrid(:)', ...
321 'rates', rates, 'nominal', nominal);
322if isempty(sched)
323 sched = entry;
324else
325 sched(end+1) = entry; %#ok<AGROW>
326end
327end
328
329% -------------------------------------------------------------------------
330function gap = local_supnorm_gap(trajPrev, traj)
331% Sup-norm of the queue-length trajectory change across all layers.
332gap = 0;
333for e = 1:numel(traj)
334 d = abs(traj{e}.Q(:) - trajPrev{e}.Q(:));
335 if ~isempty(d)
336 gap = max(gap, max(d));
337 end
338end
339end
Definition Station.m:287
Definition Station.m:245