LINE Solver
MATLAB API documentation
Loading...
Searching...
No Matches
pfqn_sens_simverify.m
1function pfqn_sens_simverify()
2%{
3%{
4 % @file pfqn_sens_simverify.m
5 % @brief End-to-end verification of the queue-length second moments produced by
6 % pfqn_sens_mva and pfqn_sens_mvaldmx against discrete-event simulation.
7 %
8 % pfqn_sens_mva_validate and pfqn_sens_mvaldmx_validate check the two
9 % primitives against brute-force enumeration of the product form, i.e.
10 % against the model the formulas assume. This harness instead checks
11 % them against LDES and JMT, i.e. against a simulated sample path of an
12 % actual LINE Network. It therefore closes the remaining gap: that the
13 % product form assumed by the formulas is the process the simulators
14 % realize, and that the demand matrix handed to the API is the one the
15 % model expresses.
16 %
17 % Two independent simulated references are used:
18 % - LDES, through the Markov reward engine. setReward defines the
19 % nonlinear rewards n(i,r) and n(i,r)*n(i,s), which getAvgReward
20 % evaluates on the exact joint-state residence-time histogram
21 % exported by the engine. This is exact given the histogram, so its
22 % only error is the simulation's own statistical error.
23 % - JMT, through the logged sample path. sampleAggr returns the jump
24 % times and the per-class queue lengths, from which the
25 % time-weighted covariance is formed directly.
26 %
27 % Three models exercise the three code paths:
28 % 1. closed multiclass, load independent -> pfqn_sens_mva
29 % 2. closed, multiserver station -> pfqn_sens_mvaldmx (LD)
30 % 3. mixed open and closed -> pfqn_sens_mvaldmx (mixed)
31 %
32 % Tolerances are statistical, not numerical. Variances converge faster
33 % than covariances, which are small differences of larger numbers, so
34 % they are checked against an absolute-plus-relative band rather than a
35 % tight relative one.
36%}
37%}
38samples = 500000;
39tolVar = 0.03; % relative band on variances
40tolCov = 0.05; % absolute-plus-relative band on covariances
41tolMean = 0.02; % relative band on the means, a sanity check on the mapping
42
43fprintf('\n=== pfqn_sens_mva / pfqn_sens_mvaldmx verification against simulation ===\n');
44fprintf(' samples per run: %d\n', samples);
45failures = {};
46
47% =====================================================================
48% Model 1: closed multiclass, load independent
49% =====================================================================
50fprintf('\n--- Model 1: closed multiclass, load independent (pfqn_sens_mva) ---\n');
51model = Network('m1');
52delay = Delay(model,'Think');
53q1 = Queue(model,'Q1',SchedStrategy.PS);
54q2 = Queue(model,'Q2',SchedStrategy.PS);
55c1 = ClosedClass(model,'C1',3,delay,0);
56c2 = ClosedClass(model,'C2',2,delay,0);
57delay.setService(c1,Exp(1/1.0)); delay.setService(c2,Exp(1/0.5));
58q1.setService(c1,Exp(1/0.4)); q1.setService(c2,Exp(1/0.6));
59q2.setService(c1,Exp(1/0.3)); q2.setService(c2,Exp(1/0.2));
60P = model.initRoutingMatrix;
61P{c1} = Network.serialRouting(delay,q1,q2);
62P{c2} = Network.serialRouting(delay,q1,q2);
63model.link(P);
64
65D1 = [0.4 0.6; 0.3 0.2];
66ref1 = pfqn_sens_mva(D1,[3 2],[1.0 0.5]);
67failures = [failures, compare_station('M1/Q1 (LDES)', ref1, ldes_moments(model,{q1,q2},{c1,c2},samples,1), 1, tolMean, tolVar, tolCov)];
68failures = [failures, compare_station('M1/Q2 (LDES)', ref1, ldes_moments(model,{q1,q2},{c1,c2},samples,2), 2, tolMean, tolVar, tolCov)];
69failures = [failures, compare_station('M1/Q1 (JMT)', ref1, jmt_moments(model,q1,samples), 1, tolMean, tolVar, tolCov)];
70failures = [failures, compare_station('M1/Q2 (JMT)', ref1, jmt_moments(model,q2,samples), 2, tolMean, tolVar, tolCov)];
71
72% =====================================================================
73% Model 2: closed, multiserver station (load dependent)
74% =====================================================================
75fprintf('\n--- Model 2: closed, multiserver station (pfqn_sens_mvaldmx, LD) ---\n');
76nserv = 2;
77model2 = Network('m2');
78delay2 = Delay(model2,'Think');
79m1q = Queue(model2,'Q1',SchedStrategy.PS);
80m2q = Queue(model2,'Q2',SchedStrategy.PS);
81m1q.setNumberOfServers(nserv);
82k1 = ClosedClass(model2,'C1',4,delay2,0);
83delay2.setService(k1,Exp(1/1.0));
84m1q.setService(k1,Exp(1/0.4));
85m2q.setService(k1,Exp(1/0.3));
86model2.link(Network.serialRouting(delay2,m1q,m2q));
87
88Nc2 = 4;
89D2 = [0.4; 0.3];
90mu2 = zeros(2,Nc2);
91for n = 1:Nc2
92 mu2(1,n) = min(n,nserv); % multiserver station: rate min(n,c)
93 mu2(2,n) = 1; % single server
94end
95ref2 = pfqn_sens_mvaldmx(0,D2,Nc2,1.0,mu2,[nserv;1]);
96failures = [failures, compare_station('M2/Q1 (LDES)', ref2, ldes_moments(model2,{m1q,m2q},{k1},samples,1), 1, tolMean, tolVar, tolCov)];
97failures = [failures, compare_station('M2/Q2 (LDES)', ref2, ldes_moments(model2,{m1q,m2q},{k1},samples,2), 2, tolMean, tolVar, tolCov)];
98failures = [failures, compare_station('M2/Q1 (JMT)', ref2, jmt_moments(model2,m1q,samples), 1, tolMean, tolVar, tolCov)];
99failures = [failures, compare_station('M2/Q2 (JMT)', ref2, jmt_moments(model2,m2q,samples), 2, tolMean, tolVar, tolCov)];
100
101% =====================================================================
102% Model 3: mixed open and closed
103% =====================================================================
104fprintf('\n--- Model 3: mixed open and closed (pfqn_sens_mvaldmx, mixed) ---\n');
105lambdaOpen = 0.3;
106model3 = Network('m3');
107delay3 = Delay(model3,'Think');
108src = Source(model3,'Source');
109snk = Sink(model3,'Sink');
110x1 = Queue(model3,'Q1',SchedStrategy.PS);
111x2 = Queue(model3,'Q2',SchedStrategy.PS);
112cc = ClosedClass(model3,'C',2,delay3,0);
113oc = OpenClass(model3,'O',0);
114delay3.setService(cc,Exp(1/1.0));
115x1.setService(cc,Exp(1/0.4)); x2.setService(cc,Exp(1/0.3));
116src.setArrival(oc,Exp(lambdaOpen));
117x1.setService(oc,Exp(1/0.5)); x2.setService(oc,Exp(1/0.4));
118P3 = model3.initRoutingMatrix;
119P3{cc} = Network.serialRouting(delay3,x1,x2);
120P3{oc} = Network.serialRouting(src,x1,x2,snk);
121model3.link(P3);
122
123% class order in the API call: 1 = closed, 2 = open
124D3 = [0.4 0.5; 0.3 0.4];
125N3 = [2 Inf];
126lam3 = [0 lambdaOpen];
127Z3 = [1.0 0];
128mu3 = ones(2,2);
129ref3 = pfqn_sens_mvaldmx(lam3,D3,N3,Z3,mu3,[1;1]);
130failures = [failures, compare_station('M3/Q1 (LDES)', ref3, ldes_moments(model3,{x1,x2},{cc,oc},samples,1), 1, tolMean, tolVar, tolCov)];
131failures = [failures, compare_station('M3/Q2 (LDES)', ref3, ldes_moments(model3,{x1,x2},{cc,oc},samples,2), 2, tolMean, tolVar, tolCov)];
132failures = [failures, compare_station('M3/Q1 (JMT)', ref3, jmt_moments(model3,x1,samples), 1, tolMean, tolVar, tolCov)];
133failures = [failures, compare_station('M3/Q2 (JMT)', ref3, jmt_moments(model3,x2,samples), 2, tolMean, tolVar, tolCov)];
134
135% =====================================================================
136% Model 4: higher moments of the station totals (pfqn_sens_mom)
137% =====================================================================
138fprintf('\n--- Model 4: higher moments of station totals (pfqn_sens_mom) ---\n');
139% Reuse model 1, whose demands are D1 and whose populations are [3 2]. The
140% moments here are of the TOTAL queue length at a station, so the LDES reward
141% is (n(i,1)+n(i,2))^t evaluated on the joint-state histogram.
142mom1 = pfqn_sens_mom(D1,[3 2],[1.0 0.5]);
143for ist = 1:2
144 if ist == 1, node = q1; else, node = q2; end
145 sim = ldes_total_moments(model, node, {c1,c2}, samples);
146 fprintf(' M4/Q%d (LDES) E[Q] analytic %8.5f sim %8.5f rel %7.4f\n', ist, mom1.m(ist), sim.m1, relb(mom1.m(ist),sim.m1));
147 fprintf(' M4/Q%d (LDES) E[Q^2] analytic %8.5f sim %8.5f rel %7.4f\n', ist, mom1.M2(ist), sim.m2, relb(mom1.M2(ist),sim.m2));
148 fprintf(' M4/Q%d (LDES) E[Q^3] analytic %8.5f sim %8.5f rel %7.4f\n', ist, mom1.M3(ist), sim.m3, relb(mom1.M3(ist),sim.m3));
149 if relb(mom1.m(ist),sim.m1) > tolMean
150 failures{end+1} = sprintf('M4/Q%d E[Q]: analytic %g sim %g', ist, mom1.m(ist), sim.m1); %#ok<AGROW>
151 end
152 if relb(mom1.M2(ist),sim.m2) > tolVar
153 failures{end+1} = sprintf('M4/Q%d E[Q^2]: analytic %g sim %g', ist, mom1.M2(ist), sim.m2); %#ok<AGROW>
154 end
155 if relb(mom1.M3(ist),sim.m3) > tolVar
156 failures{end+1} = sprintf('M4/Q%d E[Q^3]: analytic %g sim %g', ist, mom1.M3(ist), sim.m3); %#ok<AGROW>
157 end
158end
159
160% =====================================================================
161% Model 5: FCFS sojourn-time moments (pfqn_sens_respt)
162% =====================================================================
163% This is the only place the sojourn-time VARIANCE of Theorem 4.1 is checked
164% against a simulated sample path rather than against the arrival theorem it is
165% derived from. JMT logs the per-visit response times and getCdfRespT returns
166% their empirical CDF, whose moments are formed directly.
167fprintf('\n--- Model 5: FCFS sojourn-time moments (pfqn_sens_respt) ---\n');
168model5 = Network('m5');
169d5 = Delay(model5,'Think');
170f1 = Queue(model5,'F1',SchedStrategy.FCFS);
171f2 = Queue(model5,'F2',SchedStrategy.FCFS);
172f2.setNumberOfServers(2);
173k5 = ClosedClass(model5,'C',3,d5,0);
174d5.setService(k5,Exp(1/1.0));
175f1.setService(k5,Exp(1/0.4));
176f2.setService(k5,Exp(1/0.3));
177model5.link(Network.serialRouting(d5,f1,f2));
178
179ref5 = pfqn_sens_respt([0.4;0.3],[1;1],3,1.0,[1;2],3);
180% RD from getCdfRespT is indexed by STATION, and station 1 is the delay, so the
181% two queues sit at station indices 2 and 3.
182sj5 = SolverJMT(model5,'seed',11,'samples',samples);
183RD5 = sj5.getCdfRespT();
184names5 = model5.getStationNames();
185for ist = 1:2
186 if ist == 1, want = 'F1'; else, want = 'F2'; end
187 sidx = findstring(names5, want);
188 FX = RD5{sidx,1};
189 if isempty(FX)
190 failures{end+1} = sprintf('M5/%s: JMT returned no response-time CDF', want); %#ok<AGROW>
191 continue;
192 end
193 F = FX(:,1); Xv = FX(:,2);
194 dF = diff([0;F]);
195 s1 = sum(dF.*Xv); s2 = sum(dF.*Xv.^2);
196 svar = s2 - s1^2;
197 aW = ref5.W(ist,1); aV = ref5.WVar(ist,1);
198 fprintf(' M5/%s (JMT) E[W] analytic %8.5f sim %8.5f rel %7.4f\n', want, aW, s1, relb(aW,s1));
199 fprintf(' M5/%s (JMT) Var[W] analytic %8.5f sim %8.5f rel %7.4f\n', want, aV, svar, relb(aV,svar));
200 if relb(aW,s1) > tolMean
201 failures{end+1} = sprintf('M5/%s E[W]: analytic %g sim %g', want, aW, s1); %#ok<AGROW>
202 end
203 if relb(aV,svar) > tolVar
204 failures{end+1} = sprintf('M5/%s Var[W]: analytic %g sim %g', want, aV, svar); %#ok<AGROW>
205 end
206end
207
208fprintf('\n=== summary ===\n');
209if isempty(failures)
210 fprintf(' ALL SIMULATION CHECKS PASSED\n');
211else
212 for k = 1:numel(failures)
213 fprintf(' FAIL: %s\n', failures{k});
214 end
215 error('pfqn_sens_simverify:mismatch','%d simulation check(s) outside tolerance', numel(failures));
216end
217end
218
219% =========================================================================
220function bad = compare_station(tag, ref, sim, ist, tolMean, tolVar, tolCov)
221% Compare the analytic moments at station ist against a simulated estimate.
222% SIM has fields .mean (1 x R), .cov (R x R).
223bad = {};
224R = size(sim.cov,1);
225for r = 1:R
226 a = ref.Q(ist,r); s = sim.mean(r);
227 e = abs(a-s)/max(1e-12,abs(a));
228 fprintf(' %-14s mean(r=%d) analytic %8.5f sim %8.5f rel %7.4f\n', tag, r, a, s, e);
229 if e > tolMean
230 bad{end+1} = sprintf('%s mean r=%d: analytic %g sim %g rel %g > %g', tag, r, a, s, e, tolMean); %#ok<AGROW>
231 end
232end
233for r = 1:R
234 a = ref.QVar(ist,r); s = sim.cov(r,r);
235 e = abs(a-s)/max(1e-12,abs(a));
236 fprintf(' %-14s Var (r=%d) analytic %8.5f sim %8.5f rel %7.4f\n', tag, r, a, s, e);
237 if e > tolVar
238 bad{end+1} = sprintf('%s Var r=%d: analytic %g sim %g rel %g > %g', tag, r, a, s, e, tolVar); %#ok<AGROW>
239 end
240end
241for r = 1:R
242 for s2 = (r+1):R
243 a = ref.QCov(ist,r,s2); s = sim.cov(r,s2);
244 e = abs(a-s) / max(0.05, abs(a)); % covariances are small differences
245 fprintf(' %-14s Cov (%d,%d) analytic %8.5f sim %8.5f band %7.4f\n', tag, r, s2, a, s, e);
246 if e > tolCov
247 bad{end+1} = sprintf('%s Cov (%d,%d): analytic %g sim %g band %g > %g', tag, r, s2, a, s, e, tolCov); %#ok<AGROW>
248 end
249 end
250end
251end
252
253% =========================================================================
254function sim = ldes_moments(model, queues, classes, samples, ist)
255% Second moments at station ist via the LDES Markov reward engine. The rewards
256% n(i,r) and n(i,r)*n(i,s) are evaluated on the exact joint-state
257% residence-time histogram exported by the engine, so no sample-path
258% post-processing is needed.
259R = numel(classes);
260node = queues{ist};
261model.clearRewards();
262names = cell(0,1);
263for r = 1:R
264 cr = classes{r};
265 nm = sprintf('mean_%d', r);
266 model.setReward(nm, @(state) state.at(node,cr));
267 names{end+1} = nm; %#ok<AGROW>
268end
269for r = 1:R
270 for s = r:R
271 cr = classes{r}; cs = classes{s};
272 nm = sprintf('prod_%d_%d', r, s);
273 model.setReward(nm, @(state) state.at(node,cr)*state.at(node,cs));
274 names{end+1} = nm; %#ok<AGROW>
275 end
276end
277sl = SolverLDES(model,'seed',7,'samples',samples);
278[Rw,nmOut] = sl.getAvgReward();
279map = containers.Map();
280for k = 1:numel(nmOut)
281 map(nmOut{k}) = Rw(k);
282end
283sim.mean = zeros(1,R);
284for r = 1:R
285 sim.mean(r) = map(sprintf('mean_%d', r));
286end
287sim.cov = zeros(R,R);
288for r = 1:R
289 for s = r:R
290 m2 = map(sprintf('prod_%d_%d', r, s));
291 sim.cov(r,s) = m2 - sim.mean(r)*sim.mean(s);
292 sim.cov(s,r) = sim.cov(r,s);
293 end
294end
295model.clearRewards();
296end
297
298% =========================================================================
299function e = relb(a,s)
300% Relative deviation with a floor, so that a near-zero analytic value does not
301% make the relative error meaningless.
302e = abs(a-s) / max(0.05, abs(a));
303end
304
305% =========================================================================
306function sim = ldes_total_moments(model, node, classes, samples)
307% Moments of the TOTAL queue length at a node, via the LDES reward engine. The
308% reward (sum_r n(node,r))^t is nonlinear, which the joint-state
309% residence-time histogram evaluates exactly.
310model.clearRewards();
311model.setReward('tot1', @(state) state.at(node).total());
312model.setReward('tot2', @(state) state.at(node).total()^2);
313model.setReward('tot3', @(state) state.at(node).total()^3);
314sl = SolverLDES(model,'seed',7,'samples',samples);
315[Rw,nm] = sl.getAvgReward();
316map = containers.Map();
317for k = 1:numel(nm)
318 map(nm{k}) = Rw(k);
319end
320sim.m1 = map('tot1');
321sim.m2 = map('tot2');
322sim.m3 = map('tot3');
323model.clearRewards();
324end
325
326% =========================================================================
327function sim = jmt_moments(model, node, samples)
328% Second moments at a node via the JMT logged sample path. sampleAggr returns
329% the jump times t and the per-class queue lengths, which are piecewise
330% constant on [t(k),t(k+1)), so the stationary moments are the time-weighted
331% averages along the path.
332sj = SolverJMT(model,'seed',3,'samples',samples);
333sa = sj.sampleAggr(node);
334t = sa.t(:);
335st = sa.state;
336dt = diff(t);
337n = st(1:end-1,:);
338keep = dt > 0;
339dt = dt(keep);
340n = n(keep,:);
341% discard a warmup transient: the path starts from the initial state
342nk = numel(dt);
343first = max(1, floor(0.1*nk));
344dt = dt(first:end);
345n = n(first:end,:);
346W = dt / sum(dt);
347R = size(n,2);
348sim.mean = zeros(1,R);
349for r = 1:R
350 sim.mean(r) = sum(W .* n(:,r));
351end
352sim.cov = zeros(R,R);
353for r = 1:R
354 for s = 1:R
355 m2 = sum(W .* n(:,r) .* n(:,s));
356 sim.cov(r,s) = m2 - sim.mean(r)*sim.mean(s);
357 end
358end
359end
Definition Station.m:245