1function pfqn_sens_simverify()
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.
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
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.
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)
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
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
43fprintf('\n=== pfqn_sens_mva / pfqn_sens_mvaldmx verification against simulation ===\n
');
44fprintf(' samples per run: %d\n
', samples);
47% =====================================================================
48% Model 1: closed multiclass, load independent
49% =====================================================================
50fprintf('\n--- Model 1: closed multiclass, load independent (pfqn_sens_mva) ---\n
');
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);
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)];
72% =====================================================================
73% Model 2: closed, multiserver station (load dependent)
74% =====================================================================
75fprintf('\n--- Model 2: closed, multiserver station (pfqn_sens_mvaldmx, LD) ---\n
');
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));
92 mu2(1,n) = min(n,nserv); % multiserver station: rate min(n,c)
93 mu2(2,n) = 1; % single server
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)];
101% =====================================================================
102% Model 3: mixed open and closed
103% =====================================================================
104fprintf('\n--- Model 3: mixed open and closed (pfqn_sens_mvaldmx, mixed) ---\n
');
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);
123% class order in the API call: 1 = closed, 2 = open
124D3 = [0.4 0.5; 0.3 0.4];
126lam3 = [0 lambdaOpen];
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)];
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]);
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>
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>
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>
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));
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();
186 if ist == 1, want = 'F1
'; else, want = 'F2
'; end
187 sidx = findstring(names5, want);
190 failures{end+1} = sprintf('M5/%s: JMT returned no response-time CDF
', want); %#ok<AGROW>
193 F = FX(:,1); Xv = FX(:,2);
195 s1 = sum(dF.*Xv); s2 = sum(dF.*Xv.^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>
203 if relb(aV,svar) > tolVar
204 failures{end+1} = sprintf('M5/%s Var[W]: analytic %g sim %g
', want, aV, svar); %#ok<AGROW>
208fprintf('\n=== summary ===\n
');
210 fprintf(' ALL SIMULATION CHECKS PASSED\n
');
212 for k = 1:numel(failures)
213 fprintf(' FAIL: %s\n
', failures{k});
215 error('pfqn_sens_simverify:mismatch
','%d simulation check(s) outside tolerance
', numel(failures));
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).
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);
230 bad{end+1} = sprintf('%s mean r=%d: analytic %g sim %g rel %g > %g
', tag, r, a, s, e, tolMean); %#ok<AGROW>
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);
238 bad{end+1} = sprintf('%s Var r=%d: analytic %g sim %g rel %g > %g
', tag, r, a, s, e, tolVar); %#ok<AGROW>
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);
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>
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.
265 nm = sprintf('mean_%d
', r);
266 model.setReward(nm, @(state) state.at(node,cr));
267 names{end+1} = nm; %#ok<AGROW>
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>
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);
283sim.mean = zeros(1,R);
285 sim.mean(r) = map(sprintf('mean_%d
', 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);
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));
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.
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();
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);
341% discard a warmup transient: the path starts from the initial state
343first = max(1, floor(0.1*nk));
348sim.mean = zeros(1,R);
350 sim.mean(r) = sum(W .* n(:,r));
355 m2 = sum(W .* n(:,r) .* n(:,s));
356 sim.cov(r,s) = m2 - sim.mean(r)*sim.mean(s);