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% see _kb/03-api-layer.md (pfqn_sens_simverify -- FCFS sojourn moments vs JMT samples)
140mom1 = pfqn_sens_mom(D1,[3 2],[1.0 0.5]);
141for ist = 1:2
142 if ist == 1, node = q1; else, node = q2; end
143 sim = ldes_total_moments(model, node, {c1,c2}, samples);
144 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));
145 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));
146 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));
147 if relb(mom1.m(ist),sim.m1) > tolMean
148 failures{end+1} = sprintf('M4/Q%d E[Q]: analytic %g sim %g', ist, mom1.m(ist), sim.m1); %#ok<AGROW>
149 end
150 if relb(mom1.M2(ist),sim.m2) > tolVar
151 failures{end+1} = sprintf('M4/Q%d E[Q^2]: analytic %g sim %g', ist, mom1.M2(ist), sim.m2); %#ok<AGROW>
152 end
153 if relb(mom1.M3(ist),sim.m3) > tolVar
154 failures{end+1} = sprintf('M4/Q%d E[Q^3]: analytic %g sim %g', ist, mom1.M3(ist), sim.m3); %#ok<AGROW>
155 end
156end
157
158% =====================================================================
159% Model 5: FCFS sojourn-time moments (pfqn_sens_respt)
160% =====================================================================
161% see _kb/03-api-layer.md (pfqn_sens_simverify -- FCFS sojourn moments vs JMT samples)
162fprintf('\n--- Model 5: FCFS sojourn-time moments (pfqn_sens_respt) ---\n');
163model5 = Network('m5');
164d5 = Delay(model5,'Think');
165f1 = Queue(model5,'F1',SchedStrategy.FCFS);
166f2 = Queue(model5,'F2',SchedStrategy.FCFS);
167f2.setNumberOfServers(2);
168k5 = ClosedClass(model5,'C',3,d5,0);
169d5.setService(k5,Exp(1/1.0));
170f1.setService(k5,Exp(1/0.4));
171f2.setService(k5,Exp(1/0.3));
172model5.link(Network.serialRouting(d5,f1,f2));
173
174ref5 = pfqn_sens_respt([0.4;0.3],[1;1],3,1.0,[1;2],3);
175% RD from getCdfRespT is indexed by STATION, and station 1 is the delay, so the
176% two queues sit at station indices 2 and 3.
177sj5 = SolverJMT(model5,'seed',11,'samples',samples);
178RD5 = sj5.getCdfRespT();
179names5 = model5.getStationNames();
180for ist = 1:2
181 if ist == 1, want = 'F1'; else, want = 'F2'; end
182 sidx = findstring(names5, want);
183 FX = RD5{sidx,1};
184 if isempty(FX)
185 failures{end+1} = sprintf('M5/%s: JMT returned no response-time CDF', want); %#ok<AGROW>
186 continue;
187 end
188 F = FX(:,1); Xv = FX(:,2);
189 dF = diff([0;F]);
190 s1 = sum(dF.*Xv); s2 = sum(dF.*Xv.^2);
191 svar = s2 - s1^2;
192 aW = ref5.W(ist,1); aV = ref5.WVar(ist,1);
193 fprintf(' M5/%s (JMT) E[W] analytic %8.5f sim %8.5f rel %7.4f\n', want, aW, s1, relb(aW,s1));
194 fprintf(' M5/%s (JMT) Var[W] analytic %8.5f sim %8.5f rel %7.4f\n', want, aV, svar, relb(aV,svar));
195 if relb(aW,s1) > tolMean
196 failures{end+1} = sprintf('M5/%s E[W]: analytic %g sim %g', want, aW, s1); %#ok<AGROW>
197 end
198 if relb(aV,svar) > tolVar
199 failures{end+1} = sprintf('M5/%s Var[W]: analytic %g sim %g', want, aV, svar); %#ok<AGROW>
200 end
201end
202
203fprintf('\n=== summary ===\n');
204if isempty(failures)
205 fprintf(' ALL SIMULATION CHECKS PASSED\n');
206else
207 for k = 1:numel(failures)
208 fprintf(' FAIL: %s\n', failures{k});
209 end
210 error('pfqn_sens_simverify:mismatch','%d simulation check(s) outside tolerance', numel(failures));
211end
212end
213
214% =========================================================================
215function bad = compare_station(tag, ref, sim, ist, tolMean, tolVar, tolCov)
216% Compare the analytic moments at station ist against a simulated estimate.
217% SIM has fields .mean (1 x R), .cov (R x R).
218bad = {};
219R = size(sim.cov,1);
220for r = 1:R
221 a = ref.Q(ist,r); s = sim.mean(r);
222 e = abs(a-s)/max(1e-12,abs(a));
223 fprintf(' %-14s mean(r=%d) analytic %8.5f sim %8.5f rel %7.4f\n', tag, r, a, s, e);
224 if e > tolMean
225 bad{end+1} = sprintf('%s mean r=%d: analytic %g sim %g rel %g > %g', tag, r, a, s, e, tolMean); %#ok<AGROW>
226 end
227end
228for r = 1:R
229 a = ref.QVar(ist,r); s = sim.cov(r,r);
230 e = abs(a-s)/max(1e-12,abs(a));
231 fprintf(' %-14s Var (r=%d) analytic %8.5f sim %8.5f rel %7.4f\n', tag, r, a, s, e);
232 if e > tolVar
233 bad{end+1} = sprintf('%s Var r=%d: analytic %g sim %g rel %g > %g', tag, r, a, s, e, tolVar); %#ok<AGROW>
234 end
235end
236for r = 1:R
237 for s2 = (r+1):R
238 a = ref.QCov(ist,r,s2); s = sim.cov(r,s2);
239 e = abs(a-s) / max(0.05, abs(a)); % covariances are small differences
240 fprintf(' %-14s Cov (%d,%d) analytic %8.5f sim %8.5f band %7.4f\n', tag, r, s2, a, s, e);
241 if e > tolCov
242 bad{end+1} = sprintf('%s Cov (%d,%d): analytic %g sim %g band %g > %g', tag, r, s2, a, s, e, tolCov); %#ok<AGROW>
243 end
244 end
245end
246end
247
248% =========================================================================
249function sim = ldes_moments(model, queues, classes, samples, ist)
250% Second moments at station ist via the LDES Markov reward engine. The rewards
251% n(i,r) and n(i,r)*n(i,s) are evaluated on the exact joint-state
252% residence-time histogram exported by the engine, so no sample-path
253% post-processing is needed.
254R = numel(classes);
255node = queues{ist};
256model.clearRewards();
257names = cell(0,1);
258for r = 1:R
259 cr = classes{r};
260 nm = sprintf('mean_%d', r);
261 model.setReward(nm, @(state) state.at(node,cr));
262 names{end+1} = nm; %#ok<AGROW>
263end
264for r = 1:R
265 for s = r:R
266 cr = classes{r}; cs = classes{s};
267 nm = sprintf('prod_%d_%d', r, s);
268 model.setReward(nm, @(state) state.at(node,cr)*state.at(node,cs));
269 names{end+1} = nm; %#ok<AGROW>
270 end
271end
272sl = SolverLDES(model,'seed',7,'samples',samples);
273[Rw,nmOut] = sl.getAvgReward();
274map = containers.Map();
275for k = 1:numel(nmOut)
276 map(nmOut{k}) = Rw(k);
277end
278sim.mean = zeros(1,R);
279for r = 1:R
280 sim.mean(r) = map(sprintf('mean_%d', r));
281end
282sim.cov = zeros(R,R);
283for r = 1:R
284 for s = r:R
285 m2 = map(sprintf('prod_%d_%d', r, s));
286 sim.cov(r,s) = m2 - sim.mean(r)*sim.mean(s);
287 sim.cov(s,r) = sim.cov(r,s);
288 end
289end
290model.clearRewards();
291end
292
293% =========================================================================
294function e = relb(a,s)
295% Relative deviation with a floor, so that a near-zero analytic value does not
296% make the relative error meaningless.
297e = abs(a-s) / max(0.05, abs(a));
298end
299
300% =========================================================================
301function sim = ldes_total_moments(model, node, classes, samples)
302% Moments of the TOTAL queue length at a node, via the LDES reward engine. The
303% reward (sum_r n(node,r))^t is nonlinear, which the joint-state
304% residence-time histogram evaluates exactly.
305model.clearRewards();
306model.setReward('tot1', @(state) state.at(node).total());
307model.setReward('tot2', @(state) state.at(node).total()^2);
308model.setReward('tot3', @(state) state.at(node).total()^3);
309sl = SolverLDES(model,'seed',7,'samples',samples);
310[Rw,nm] = sl.getAvgReward();
311map = containers.Map();
312for k = 1:numel(nm)
313 map(nm{k}) = Rw(k);
314end
315sim.m1 = map('tot1');
316sim.m2 = map('tot2');
317sim.m3 = map('tot3');
318model.clearRewards();
319end
320
321% =========================================================================
322function sim = jmt_moments(model, node, samples)
323% Second moments at a node via the JMT logged sample path. sampleAggr returns
324% the jump times t and the per-class queue lengths, which are piecewise
325% constant on [t(k),t(k+1)), so the stationary moments are the time-weighted
326% averages along the path.
327sj = SolverJMT(model,'seed',3,'samples',samples);
328sa = sj.sampleAggr(node);
329t = sa.t(:);
330st = sa.state;
331dt = diff(t);
332n = st(1:end-1,:);
333keep = dt > 0;
334dt = dt(keep);
335n = n(keep,:);
336% discard a warmup transient: the path starts from the initial state
337nk = numel(dt);
338first = max(1, floor(0.1*nk));
339dt = dt(first:end);
340n = n(first:end,:);
341W = dt / sum(dt);
342R = size(n,2);
343sim.mean = zeros(1,R);
344for r = 1:R
345 sim.mean(r) = sum(W .* n(:,r));
346end
347sim.cov = zeros(R,R);
348for r = 1:R
349 for s = 1:R
350 m2 = sum(W .* n(:,r) .* n(:,s));
351 sim.cov(r,s) = m2 - sim.mean(r)*sim.mean(s);
352 end
353end
354end