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