1% renv_rotterdam_blending - Exact reproduction of the fyp26 SOQN blending result
3% Reproduces the 24-environment exponential blending accuracy result of the
4% fyp26 Rotterdam container-terminal study (Dhingra 2018),
using the same
5% level-dependent QBD blocks and the cyclic resolvent blending that the
6% SolverENV state-vector analyzer uses internally.
8% Per-environment model (PoissonSOQNSpec): a semi-open SOQN with N tokens, a
9% Poisson arrival rate lambda_h (hour h of the day), and two flow-equivalent
10% server (FES) subnetworks in tandem (upstream S1, downstream S2), each a
11% load-dependent station whose throughput curve mu1(n)/mu2(n)
is calibrated by
12% exact MVA on the underlying closed subnetwork. The LD-QBD state
is (n,k):
13% level n = jobs upstream of S2 (S1 + external backlog), phase k = S2 occupancy.
15% The 24 hourly environments are visited cyclically with exponential sojourns
16% (mean = hour duration). Each visit applies the resolvent s*pi*(sI-Q)^{-1}
17% (exit = time-average, memoryless), entry vectors are chained to an L1 fixed
18% point, and the blend weights stages by their time fraction. Mean external
19% wait W and external queue length Qex are compared to the Dhingra simulation
22% Copyright (c) 2012-2026, Imperial College London
27%% Dhingra 24-hour schedule and simulation ground truth (Table 13)
28dailyRateHr = [ 6, 30, 40, 62, 76, 79, 119, 164, 152, 130, 79, 70, ...
29 57, 57, 113, 130, 162, 202, 148, 118, 92, 62, 36, 8];
30dailyDurHr = [0.51,0.84,0.76,0.98,0.67,2.33,1.80,0.62,0.36,1.30,1.02,0.98, ...
31 0.86,1.56,1.30,1.57,0.34,0.22,0.47,1.33,1.56,0.93,0.71,1.11];
32lambda = dailyRateHr * 0.5 / 60; % arrivals per minute (buildLambdasMin)
33durMin = dailyDurHr * 60; % hour durations in minutes
34fracW = durMin / sum(durMin); % time-fraction blend weights f_i
37simW = [154.0661,118.3936,99.962,88.2379,80.9272,75.0887,70.745,67.4326,64.9719,62.7913,61.2534];
38simQex = [89.8508,63.3970,49.3650,40.8783,35.3546,30.6543,27.4111,24.7218,22.3622,20.3893,18.9252];
40tailFactor = 15; % M_trunc = N*(1+tailFactor) (ACC_TAIL)
41params =
struct(
'numEntryServers',6,
'numStacks',29,
'entryServiceTime',6.0, ...
42 'travelToStackTime',5.6,
'stackServiceTime',6.0,
'numExitServers',6, ...
43 'travelToExitTime',5.6,
'exitServiceTime',6.0);
45%% Reproduce the blending
for a few token counts and compare to simulation
47fprintf(
'Rotterdam SOQN exponential blending vs Dhingra simulation (tailFactor=%d)\n', tailFactor);
48fprintf(
'%4s | %10s %10s %7s | %10s %10s %7s\n',
'N',
'W_blend',
'W_sim',
'err%',
'Qex_bl',
'Qex_sim',
'err%');
50 mu1 = fesCurve(N, params,
'up');
51 mu2 = fesCurve(N, params,
'down');
52 [Wbl, Qexbl] = blendSOQN(N, lambda, durMin, fracW, mu1, mu2, tailFactor);
54 fprintf(
'%4d | %10.4f %10.4f %6.2f | %10.4f %10.4f %6.2f\n', ...
55 N, Wbl, simW(j), 100*abs(Wbl-simW(j))/simW(j), ...
56 Qexbl, simQex(j), 100*abs(Qexbl-simQex(j))/simQex(j));
59%% ---- local functions ------------------------------------------------------
60function mu = fesCurve(N, p, which)
61% Load-dependent FES throughput curve mu(l), l=0..N, by exact MVA on the
62% closed subnetwork at each population l (Norton flow-equivalent).
65 if strcmp(which,
'up')
67 eg = Queue(m,'EntryGates',SchedStrategy.FCFS); eg.setNumberOfServers(p.numEntryServers);
68 tv = Delay(m,'TravelToStack');
69 st = cell(1,p.numStacks);
70 for i=1:p.numStacks, st{i}=Queue(m,sprintf(
'Stack%d',i),SchedStrategy.FCFS); st{i}.setNumberOfServers(1); end
71 cls = ClosedClass(m,
'Trucks',l,eg);
72 eg.setService(cls,Exp(1/p.entryServiceTime));
73 tv.setService(cls,Exp(1/p.travelToStackTime));
74 for i=1:p.numStacks, st{i}.setService(cls,Exp(1/p.stackServiceTime)); end
75 ns = 2 + p.numStacks;
P = m.initRoutingMatrix();
76 P{1}(1,2) = 1; % EntryGates -> TravelToStack
77 P{1}(2,3:ns) = 1/p.numStacks; % TravelToStack -> Stack_i
78 P{1}(3:ns,1) = 1; % Stack_i -> EntryGates
80 T = SolverMVA(m,
'method',
'exact',
'verbose',
false).getAvgTable;
81 mu(l+1) = T.Tput(1); % throughput at EntryGates
84 tv = Delay(m,
'TravelToExit');
85 xg = Queue(m,
'ExitGates',SchedStrategy.FCFS); xg.setNumberOfServers(p.numExitServers);
86 cls = ClosedClass(m,
'Trucks',l,tv);
87 tv.setService(cls,Exp(1/p.travelToExitTime));
88 xg.setService(cls,Exp(1/p.exitServiceTime));
89 P = m.initRoutingMatrix();
P{1}(1,2)=1;
P{1}(2,1)=1; m.link(
P);
90 T = SolverMVA(m,
'method',
'exact',
'verbose',
false).getAvgTable;
91 mu(l+1) = T.Tput(2); % throughput at ExitGates
96function Q = soqnGenerator(N, lam, mu1, mu2, tailFactor)
97% Flat generator of the Poisson SOQN LD-QBD. State (n,k), flat index n*M+k+1.
98% level n=0..Mtr (S1+backlog), phase k=0..N (S2 occupancy).
99M = N + 1; Mtr = N + tailFactor*N; dim = (Mtr+1)*M;
100ri = zeros(0,1); ci = zeros(0,1); vi = zeros(0,1);
105 m1 = mu1(j+1); % mu1.at(min(n,N-k))
106 m2 = mu2(k+1); % mu2.at(k)
108 lamEff = lam * (~atTop); % no arrivals leave the truncated chain at top
110 ri(end+1,1)=row; ci(end+1,1)=row; vi(end+1,1)=-(lamEff + m1 + m2); %#ok<AGROW>
111 % S2 completion within level: (n,k)->(n,k-1) at mu2(k)
113 ri(end+1,1)=row; ci(end+1,1)=n*M+(k-1)+1; vi(end+1,1)=m2; %#ok<AGROW>
115 % arrival up: (n,k)->(n+1,k) at lam
117 ri(end+1,1)=row; ci(end+1,1)=(n+1)*M+k+1; vi(end+1,1)=lam; %#ok<AGROW>
119 % S1 completion down: (n,k)->(n-1,k+1) at mu1(min(n,N-k))
121 ri(end+1,1)=row; ci(end+1,1)=(n-1)*M+(k+1)+1; vi(end+1,1)=m1; %#ok<AGROW>
125Q = sparse(ri, ci, vi, dim, dim);
128function [W, Qex] = blendSOQN(N, lambda, durMin, fracW, mu1, mu2, tailFactor)
129% Cyclic exponential blending over the 24 hourly SOQN environments, via the
130% resolvent s*pi*(sI-Q)^{-1}, to an L1 fixed point; then blend and extract W,Qex.
132M = N + 1; Mtr = N + tailFactor*N; dim = (Mtr+1)*M;
134for h = 1:K, Q{h} = soqnGenerator(N, lambda(h), mu1, mu2, tailFactor); end
137pi0 = sparse(1, dim); pi0(1) = 1; % empty system
139for h = 2:K, piEnter{h} = pi0; end
146 piEnter{mod(h,K)+1} = s * (piEnter{h} / (s*Ssp - Q{h})); % resolvent = exit
149 for h = 1:K, l1 = max(l1, sum(abs(piEnter{h}-prev{h}))); end
150 if l1 < 1e-10,
break; end
153% Blend (exp sojourn: exit = time-average), weighted by time fraction
154piAvg = sparse(1, dim);
157 piEx = s * (piEnter{h} / (s*Ssp - Q{h}));
158 piAvg = piAvg + fracW(h) * piEx;
160piAvg = full(piAvg); piAvg(piAvg<0)=0; piAvg = piAvg/sum(piAvg);
162% Metrics (extractMetricsPoissonSOQN)
163QLex=0; QL1=0; QL2=0; tput=0;
167 if p==0,
continue; end
168 extQ = max(0, n+k-N); s1 = min(n, N-k);
169 QLex = QLex + extQ*p; QL1 = QL1 + s1*p; QL2 = QL2 + k*p;
170 if k>=1, tput = tput + mu2(k+1)*p; end
174W = (QLex + QL1 + QL2) / tput; % external wait + internal time (Little)