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)
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).
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.
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.
22% Copyright (c) 2012-2026, Imperial College London
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
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();
39% relaxation controls (reuse the ensemble iteration budget / tolerance)
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;
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;
51tgrid = linspace(ts(1), ts(2), Ngrid)';
53% Per-layer sn (
for node->station and
class bookkeeping), cached once.
56 layerSn{e} = self.ensemble{e}.getStruct;
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);
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);
82% Assemble the block-diagonal aggregate exactly as getTranAvgDecoupled does.
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;
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).
107 savedTs = s.options.timespan;
108 hadSched = isfield(s.options.config,
'rate_sched');
110 savedSched = s.options.config.rate_sched;
112 s.options.timespan = ts;
113 if ~isempty(schedByLayer{e})
114 s.options.config.rate_sched = schedByLayer{e};
116 s.options.config.rate_sched = [];
118 % Drop any cached transient before re-solving with injected rate_sched.
119 % see _kb/06-solver-catalog.md (LN section)
for rationale
121 [Qe, Ue, Te] = s.getTranAvg();
122 s.options.timespan = savedTs;
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');
128 be = struct(); % avoid struct('Q',{cell}) which builds a
struct ARRAY
129 be.Q = Qe; be.U = Ue; be.T = Te;
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);
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);
142 traj{e} =
struct(
'Q',Q,
'U',U,
'T',T,
'R',R);
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);
154 v = zeros(numel(tgrid),1);
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
167thinkt = containers.Map('KeyType','
double','ValueType','any');
168callservt = containers.Map('KeyType','
double','ValueType','any');
170% Task think times: from the task's own server-layer utilization/throughput.
172 tidx = lqn.tshift + t;
173 if isnan(self.idxhash(tidx)) || lqn.isref(tidx)
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);
181 Uti = Uti + squeeze(traj{e}.U(sIdx,r,:));
182 Tti = Tti + squeeze(traj{e}.T(sIdx,r,:));
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;
190 tk = njobs .* abs(1 - Uti) ./ Tsafe - userthink;
192 tk = max(GlobalConstants.Zero, tk) + userthink; % total mean incl. user think
196% Synchronous-call service demands: callee entry response time * call mean.
197for cidx = 1:lqn.ncalls
198 if lqn.calltype(cidx) ~= CallType.SYNC
201 eidx = lqn.callpair(cidx,2); % callee entry
202 tidx = lqn.parent(eidx); % callee task
203 if isnan(self.idxhash(tidx))
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;
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,:));
218 % fall back to the entry
's activities response time
219 Rc = squeeze(sum(traj{e}.R(sIdx,:,:),2));
222 if ~isempty(lqn.callproc{cidx}) && isa(lqn.callproc{cidx},'Distribution
')
223 callmean = lqn.callproc{cidx}.getMean;
225 callservt(cidx) = Rc * callmean;
228demand = struct('thinkt
', thinkt, 'callservt
', callservt);
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.
240schedByLayer = cell(1,E);
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);
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
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);
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
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);
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);
298if ~(dend > GlobalConstants.FineTol)
299 return % degenerate steady-state demand; skip
this channel
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
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).
309entry = struct('station
', ist, 'class', classidx, 'tgrid
', tgrid(:)', ...
310 'rates', rates,
'nominal', nominal);
314 sched(end+1) = entry; %#ok<AGROW>
318% -------------------------------------------------------------------------
319function gap = local_supnorm_gap(trajPrev, traj)
320% Sup-norm of the queue-length trajectory change across all layers.
323 d = abs(traj{e}.Q(:) - trajPrev{e}.Q(:));
325 gap = max(gap, max(d));