Tutorial 12: Posterior Analysis with Uncertain Parameters

Propagate parameter uncertainty through a queueing model. The Posterior solver attaches a Prior distribution to a model parameter, evaluates the model under each alternative, and aggregates the results into a posterior distribution of the chosen performance metrics. The example uses an M/M/1 queue with arrival rate λ = 0.5 and an uncertain service rate μ drawn from a Gaussian-shaped prior over [0.7, 2.5] centred at μ = 1.3.

Block 1: Build the model with a Prior service rate

model = Network('UncertainServiceModel');
source = Source(model, 'Source');
queue  = Queue(model, 'Queue', SchedStrategy.FCFS);
sink   = Sink(model, 'Sink');
jobClass = OpenClass(model, 'Jobs');
source.setArrival(jobClass, Exp(0.5));

numAlternatives = 30;
serviceRates = linspace(0.7, 2.5, numAlternatives);
priorMean = 1.3; priorStd = 0.4;
priorProbs = exp(-0.5 * ((serviceRates - priorMean) / priorStd).^2);
priorProbs = priorProbs / sum(priorProbs);

alternatives = cell(1, numAlternatives);
for i = 1:numAlternatives
    alternatives{i} = Exp(serviceRates(i));
end
queue.setService(jobClass, Prior(alternatives, priorProbs));

model.link(Network.serialRouting(source, queue, sink));
Network model = new Network("UncertainServiceModel");
Source source = new Source(model, "Source");
Queue queue   = new Queue(model, "Queue", SchedStrategy.FCFS);
Sink sink     = new Sink(model, "Sink");
OpenClass jobClass = new OpenClass(model, "Jobs", 0);
source.setArrival(jobClass, new Exp(0.5));

int numAlternatives = 30;
double priorMean = 1.3, priorStd = 0.4;
List<Distribution> alternatives = new ArrayList<>();
double[] priorProbs = new double[numAlternatives];
double sum = 0.0;
for (int i = 0; i < numAlternatives; i++) {
    double rate = 0.7 + (2.5 - 0.7) * i / (double) (numAlternatives - 1);
    alternatives.add(new Exp(rate));
    double z = (rate - priorMean) / priorStd;
    priorProbs[i] = Math.exp(-0.5 * z * z);
    sum += priorProbs[i];
}
for (int i = 0; i < numAlternatives; i++) priorProbs[i] /= sum;
queue.setService(jobClass, new Prior(alternatives, priorProbs));

model.link(Network.serialRouting(source, queue, sink));
val model = Network("UncertainServiceModel")
val source = Source(model, "Source")
val queue  = Queue(model, "Queue", SchedStrategy.FCFS)
val sink   = Sink(model, "Sink")
val jobClass = OpenClass(model, "Jobs", 0)
source.setArrival(jobClass, Exp(0.5))

val numAlternatives = 30
val priorMean = 1.3; val priorStd = 0.4
val alternatives = (0 until numAlternatives).map {
    Exp(0.7 + (2.5 - 0.7) * it / (numAlternatives - 1).toDouble())
}
val rawProbs = (0 until numAlternatives).map {
    val rate = 0.7 + (2.5 - 0.7) * it / (numAlternatives - 1).toDouble()
    val z = (rate - priorMean) / priorStd
    Math.exp(-0.5 * z * z)
}
val sum = rawProbs.sum()
val priorProbs = rawProbs.map { it / sum }.toDoubleArray()
queue.setService(jobClass, Prior(alternatives, priorProbs))

model.link(Network.serialRouting(source, queue, sink))
from line_solver import *
import numpy as np

model = Network('UncertainServiceModel')
source = Source(model, 'Source')
queue  = Queue(model, 'Queue', SchedStrategy.FCFS)
sink   = Sink(model, 'Sink')
jobClass = OpenClass(model, 'Jobs')
source.setArrival(jobClass, Exp(0.5))

num_alternatives = 30
service_rates = np.linspace(0.7, 2.5, num_alternatives)
prior_mean, prior_std = 1.3, 0.4
prior_probs = np.exp(-0.5 * ((service_rates - prior_mean) / prior_std) ** 2)
prior_probs = prior_probs / np.sum(prior_probs)

alternatives = [Exp(rate) for rate in service_rates]
queue.setService(jobClass, Prior(alternatives, prior_probs.tolist()))

model.link(Network.serial_routing([source, queue, sink]))

Block 2: Run Posterior over a back-end solver

Posterior wraps an arbitrary back-end solver (here MVA), runs it once per alternative, and aggregates the per-alternative results into prior-weighted means and per-alternative tables.

post = Posterior(model, @SolverMVA);
post.runAnalyzer();

avgTable  = post.getAvgTable()         % prior-weighted means
postTable = post.getPosteriorTable()   % per-alternative results

respDist  = post.getPosteriorDist('R', queue, jobClass);
qlenDist  = post.getPosteriorDist('Q', queue, jobClass);
utilDist  = post.getPosteriorDist('U', queue, jobClass);
Posterior post = new Posterior(model, m -> new MVA(m));
post.runAnalyzer();

post.getAvgTable().print();         // prior-weighted means
post.getPosteriorTable().print();   // per-alternative results

SolverPosterior.EmpiricalCDF respDist = post.getPosteriorDist("R", queue, jobClass);
SolverPosterior.EmpiricalCDF qlenDist = post.getPosteriorDist("Q", queue, jobClass);
SolverPosterior.EmpiricalCDF utilDist = post.getPosteriorDist("U", queue, jobClass);
val post = Posterior(model) { m -> MVA(m) }
post.runAnalyzer()

post.getAvgTable().print()
post.getPosteriorTable().print()

val respDist = post.getPosteriorDist("R", queue, jobClass)
val qlenDist = post.getPosteriorDist("Q", queue, jobClass)
val utilDist = post.getPosteriorDist("U", queue, jobClass)
post = Posterior(model, MVA)
post.run_analyzer()

avg_table  = post.get_avg_table()         # prior-weighted means
post_table = post.get_posterior_table()   # per-alternative results

resp_dist  = post.get_posterior_dist('R', queue, jobClass)
qlen_dist  = post.get_posterior_dist('Q', queue, jobClass)
util_dist  = post.get_posterior_dist('U', queue, jobClass)

Block 3: Extract statistics from the posterior CDFs

Each posterior distribution returned by getPosteriorDist/get_posterior_dist is an empirical CDF. In MATLAB and Python the underlying data is exposed as an n×2 matrix (cumulative probability, metric value); in Java and Kotlin the same information is reachable via parallel values, probabilities, and cdf arrays sorted by value. Standard summary statistics follow directly:

respValues = respDist.data(:, 2);
respCdf    = respDist.data(:, 1);
respProbs  = [respCdf(1); diff(respCdf)];
expectedR  = sum(respValues .* respProbs);
medianR    = respValues(find(respCdf >= 0.5, 1, 'first'));
fprintf('E[R] = %.4f, median(R) = %.4f\n', expectedR, medianR);
double expectedR = respDist.getMean();
double medianR = respDist.values[0];
for (int i = 0; i < respDist.cdf.length; i++) {
    if (respDist.cdf[i] >= 0.5) { medianR = respDist.values[i]; break; }
}
System.out.printf("E[R] = %.4f, median(R) = %.4f%n", expectedR, medianR);
val expectedR = respDist.getMean()
val medianR = respDist.values[respDist.cdf.indexOfFirst { it >= 0.5 }]
println("E[R] = $expectedR, median(R) = $medianR")
resp_values = resp_dist.data[:, 1]
resp_cdf    = resp_dist.data[:, 0]
resp_probs  = np.concatenate([[resp_cdf[0]], np.diff(resp_cdf)])
expected_R  = np.sum(resp_values * resp_probs)
median_R    = resp_values[np.searchsorted(resp_cdf, 0.5)]
print(f'E[R] = {expected_R:.4f}, median(R) = {median_R:.4f}')

The same recipe applies to queue length, utilization, throughput, or any other metric exposed by the back-end solver. Plotting respValues against respProbs yields a posterior PDF; plotting against respCdf yields the posterior CDF.