use crate::error::GeomError;
use crate::linalg::lu;
use crate::linalg::matrix::Matrix;
use crate::monte_carlo::Rng;
const STABILITY_TOL: f64 = 1e-12;
#[derive(Debug, Clone, Copy, PartialEq)]
pub enum QueueModel {
MMc { c: usize, a: f64 },
MMcK { c: usize, k: usize, a: f64 },
MMInf { a: f64 },
MeanValueOnly,
}
#[derive(Debug, Clone, Copy, PartialEq)]
pub struct QueueMetrics {
pub rho: f64,
pub l: f64,
pub lq: f64,
pub w: f64,
pub wq: f64,
pub p0: f64,
pub lambda_eff: f64,
pub model: QueueModel,
}
impl QueueMetrics {
#[must_use]
pub fn pn(&self, n: usize) -> f64 {
match self.model {
QueueModel::MMc { c, a } => birth_death_pn(self.p0, a, c, n),
QueueModel::MMcK { c, k, a } => {
if n > k {
0.0
} else {
birth_death_pn(self.p0, a, c, n)
}
}
QueueModel::MMInf { a } => birth_death_pn((-a).exp(), a, usize::MAX, n),
QueueModel::MeanValueOnly => f64::NAN,
}
}
}
fn birth_death_pn(p0: f64, a: f64, c: usize, n: usize) -> f64 {
let mut p = p0;
for k in 1..=n.min(c) {
p *= a / k as f64;
}
if n > c {
p *= (a / c as f64).powi((n - c) as i32);
}
p
}
#[must_use]
pub fn mm1(lambda: f64, mu: f64) -> QueueMetrics {
mmc(lambda, mu, 1)
}
#[must_use]
pub fn mmc(lambda: f64, mu: f64, c: usize) -> QueueMetrics {
assert!(lambda > 0.0, "mmc requires lambda > 0");
assert!(mu > 0.0, "mmc requires mu > 0");
assert!(c >= 1, "mmc requires at least one server");
let a = lambda / mu;
let rho = a / c as f64;
if rho >= 1.0 - STABILITY_TOL {
return QueueMetrics {
rho,
l: f64::INFINITY,
lq: f64::INFINITY,
w: f64::INFINITY,
wq: f64::INFINITY,
p0: 0.0,
lambda_eff: lambda,
model: QueueModel::MMc { c, a },
};
}
let mut term = 1.0f64;
let mut sum = 0.0f64;
for k in 0..c {
sum += term;
term *= a / (k + 1) as f64;
}
let tail = term / (1.0 - rho);
let p0 = 1.0 / (sum + tail);
let c_erlang = tail * p0;
let lq = c_erlang * rho / (1.0 - rho);
let wq = lq / lambda;
let w = wq + 1.0 / mu;
let l = lambda * w;
QueueMetrics { rho, l, lq, w, wq, p0, lambda_eff: lambda, model: QueueModel::MMc { c, a } }
}
#[must_use]
pub fn mm1k(lambda: f64, mu: f64, k: usize) -> QueueMetrics {
mmck(lambda, mu, 1, k)
}
#[must_use]
pub fn mmck(lambda: f64, mu: f64, c: usize, k: usize) -> QueueMetrics {
assert!(lambda > 0.0, "mmck requires lambda > 0");
assert!(mu > 0.0, "mmck requires mu > 0");
assert!(c >= 1, "mmck requires at least one server");
assert!(k >= c, "mmck requires capacity k >= server count c");
let a = lambda / mu;
let mut weights = Vec::with_capacity(k + 1);
let mut w = 1.0f64;
weights.push(w);
for n in 1..=k {
w *= if n <= c { a / n as f64 } else { a / c as f64 };
weights.push(w);
}
let total: f64 = weights.iter().sum();
let p0 = 1.0 / total;
let mut l = 0.0;
let mut lq = 0.0;
for n in 0..=k {
let p = weights[n] * p0;
l += n as f64 * p;
lq += (n.saturating_sub(c)) as f64 * p;
}
let p_block = weights[k] * p0;
let lambda_eff = lambda * (1.0 - p_block);
let w = l / lambda_eff;
let wq = lq / lambda_eff;
let rho = lambda_eff / (c as f64 * mu);
QueueMetrics { rho, l, lq, w, wq, p0, lambda_eff, model: QueueModel::MMcK { c, k, a } }
}
#[must_use]
pub fn mm_inf(lambda: f64, mu: f64) -> QueueMetrics {
assert!(lambda > 0.0, "mm_inf requires lambda > 0");
assert!(mu > 0.0, "mm_inf requires mu > 0");
let a = lambda / mu;
QueueMetrics {
rho: 0.0,
l: a,
lq: 0.0,
w: 1.0 / mu,
wq: 0.0,
p0: (-a).exp(),
lambda_eff: lambda,
model: QueueModel::MMInf { a },
}
}
#[must_use]
pub fn erlang_b(offered_load: f64, c: usize) -> f64 {
assert!(offered_load >= 0.0, "erlang_b requires a non-negative load");
let mut b = 1.0;
for k in 1..=c {
b = offered_load * b / (k as f64 + offered_load * b);
}
b
}
#[must_use]
pub fn erlang_c(load: f64, c: usize) -> f64 {
assert!(load >= 0.0, "erlang_c requires a non-negative load");
assert!(c >= 1, "erlang_c requires at least one server");
let rho = load / c as f64;
if rho >= 1.0 - STABILITY_TOL {
return 1.0;
}
let b = erlang_b(load, c);
b / (1.0 - rho * (1.0 - b))
}
#[must_use]
pub fn erlang_b_inverse_capacity(load: f64, blocking_target: f64) -> usize {
assert!(load >= 0.0, "erlang_b_inverse_capacity requires a non-negative load");
assert!(
blocking_target > 0.0 && blocking_target <= 1.0,
"erlang_b_inverse_capacity requires a target in (0, 1]"
);
let mut b = 1.0;
let mut c = 0usize;
while b > blocking_target {
c += 1;
b = load * b / (c as f64 + load * b);
}
c
}
#[must_use]
pub fn mg1_pollaczek_khinchine(lambda: f64, service_mean: f64, service_var: f64) -> QueueMetrics {
assert!(lambda > 0.0, "mg1_pollaczek_khinchine requires lambda > 0");
assert!(service_mean > 0.0, "mg1_pollaczek_khinchine requires a positive service mean");
assert!(service_var >= 0.0, "mg1_pollaczek_khinchine requires a non-negative variance");
let rho = lambda * service_mean;
if rho >= 1.0 - STABILITY_TOL {
return QueueMetrics {
rho,
l: f64::INFINITY,
lq: f64::INFINITY,
w: f64::INFINITY,
wq: f64::INFINITY,
p0: 0.0,
lambda_eff: lambda,
model: QueueModel::MeanValueOnly,
};
}
let second_moment = service_var + service_mean * service_mean;
let lq = lambda * lambda * second_moment / (2.0 * (1.0 - rho));
let wq = lq / lambda;
let w = wq + service_mean;
let l = lq + rho;
QueueMetrics {
rho,
l,
lq,
w,
wq,
p0: 1.0 - rho,
lambda_eff: lambda,
model: QueueModel::MeanValueOnly,
}
}
#[must_use]
pub fn gg1_kingman_approx(lambda: f64, mu: f64, ca2: f64, cs2: f64) -> f64 {
assert!(lambda > 0.0 && mu > 0.0, "gg1_kingman_approx requires positive rates");
assert!(ca2 >= 0.0 && cs2 >= 0.0, "gg1_kingman_approx requires non-negative variability");
let rho = lambda / mu;
if rho >= 1.0 - STABILITY_TOL {
return f64::INFINITY;
}
(rho / (1.0 - rho)) * ((ca2 + cs2) / 2.0) / mu
}
#[must_use]
pub fn littles_law_check(l: f64, lambda: f64, w: f64) -> f64 {
l - lambda * w
}
pub fn jackson_network(
routing: &Matrix,
external: &[f64],
service: &[f64],
servers: &[usize],
) -> Result<Vec<QueueMetrics>, GeomError> {
let n = external.len();
if !routing.is_square() || routing.rows != n {
return Err(GeomError::InvalidArgument("jackson_network: routing must be n x n"));
}
if service.len() != n || servers.len() != n {
return Err(GeomError::InvalidArgument("jackson_network: rate/server length mismatch"));
}
for i in 0..n {
let row: f64 = (0..n).map(|j| routing.get(i, j)).sum();
if !(row <= 1.0 + 1e-9) {
return Err(GeomError::InvalidArgument(
"jackson_network: a routing row sums past one",
));
}
for j in 0..n {
if !(routing.get(i, j) >= 0.0) {
return Err(GeomError::InvalidArgument(
"jackson_network: routing probabilities must be non-negative",
));
}
}
}
let mut a = Matrix::zeros(n, n);
for i in 0..n {
for j in 0..n {
let delta = if i == j { 1.0 } else { 0.0 };
a.set(i, j, delta - routing.get(j, i));
}
}
let rates = lu::solve(&a, external)
.map_err(|_| GeomError::Degenerate("jackson_network: traffic equations are singular"))?;
rates
.iter()
.zip(service.iter().zip(servers.iter()))
.map(|(&lam, (&mu, &c))| {
if !(lam > 0.0) {
return Err(GeomError::InvalidArgument(
"jackson_network: a node has non-positive total arrival rate",
));
}
Ok(mmc(lam, mu, c))
})
.collect()
}
#[derive(Debug, Clone, PartialEq)]
pub struct QueueSimResult {
pub l: f64,
pub lq: f64,
pub w: f64,
pub wq: f64,
pub rho: f64,
pub lambda_eff: f64,
pub served: usize,
}
#[must_use]
pub fn queue_simulate(
arrival: &dyn Fn(&mut Rng) -> f64,
service: &dyn Fn(&mut Rng) -> f64,
c: usize,
t_end: f64,
rng: &mut Rng,
) -> QueueSimResult {
assert!(c >= 1, "queue_simulate requires at least one server");
assert!(t_end > 0.0, "queue_simulate requires a positive horizon");
let mut free_at = vec![0.0f64; c];
let mut busy_time = vec![0.0f64; c];
let mut arrivals: Vec<f64> = Vec::new();
let mut departures: Vec<f64> = Vec::new();
let mut starts: Vec<f64> = Vec::new();
let mut t = 0.0f64;
loop {
t += arrival(rng).max(0.0);
if t > t_end {
break;
}
let mut which = 0usize;
for s in 1..c {
if free_at[s] < free_at[which] {
which = s;
}
}
let start = t.max(free_at[which]);
let duration = service(rng).max(0.0);
free_at[which] = start + duration;
busy_time[which] += duration;
arrivals.push(t);
starts.push(start);
departures.push(start + duration);
}
let served = arrivals.len();
if served == 0 {
return QueueSimResult {
l: 0.0,
lq: 0.0,
w: 0.0,
wq: 0.0,
rho: 0.0,
lambda_eff: 0.0,
served: 0,
};
}
let w: f64 = departures.iter().zip(&arrivals).map(|(d, a)| d - a).sum::<f64>() / served as f64;
let wq: f64 = starts.iter().zip(&arrivals).map(|(s, a)| s - a).sum::<f64>() / served as f64;
let mut sorted_dep = departures.clone();
sorted_dep.sort_by(|a, b| a.partial_cmp(b).unwrap_or(std::cmp::Ordering::Equal));
let horizon = sorted_dep[served - 1].max(t_end);
let (mut i, mut j) = (0usize, 0usize);
let (mut n, mut last, mut area, mut area_q) = (0usize, 0.0f64, 0.0f64, 0.0f64);
while i < served || j < served {
let next_arr = if i < served { arrivals[i] } else { f64::INFINITY };
let next_dep = if j < served { sorted_dep[j] } else { f64::INFINITY };
let next = next_arr.min(next_dep);
area += n as f64 * (next - last);
area_q += n.saturating_sub(c) as f64 * (next - last);
last = next;
if next_dep <= next_arr {
n -= 1;
j += 1;
} else {
n += 1;
i += 1;
}
}
let busy: f64 = busy_time.iter().sum();
QueueSimResult {
l: area / horizon,
lq: area_q / horizon,
w,
wq,
rho: busy / (c as f64 * horizon),
lambda_eff: served as f64 / horizon,
served,
}
}
#[must_use]
pub fn priority_queue_simulate(
lambdas: &[f64],
mus: &[f64],
c: usize,
t_end: f64,
rng: &mut Rng,
) -> Vec<QueueSimResult> {
assert!(c >= 1, "priority_queue_simulate requires at least one server");
assert!(t_end > 0.0, "priority_queue_simulate requires a positive horizon");
assert!(
!lambdas.is_empty() && lambdas.len() == mus.len(),
"priority_queue_simulate requires one service rate per class"
);
assert!(
lambdas.iter().all(|&l| l > 0.0) && mus.iter().all(|&m| m > 0.0),
"priority_queue_simulate requires positive rates"
);
let classes = lambdas.len();
let total_lambda: f64 = lambdas.iter().sum();
let mut pending: Vec<(f64, usize)> = Vec::new();
let mut t = 0.0f64;
loop {
t += -rng.next_f64().max(1e-300).ln() / total_lambda;
if t > t_end {
break;
}
let u = rng.next_f64() * total_lambda;
let mut acc = 0.0;
let mut k = classes - 1;
for (idx, &l) in lambdas.iter().enumerate() {
acc += l;
if u < acc {
k = idx;
break;
}
}
pending.push((t, k));
}
let mut waiting: Vec<Vec<f64>> = vec![Vec::new(); classes];
let mut free_at = vec![0.0f64; c];
let mut busy_time = vec![0.0f64; c];
let mut arrivals: Vec<Vec<f64>> = vec![Vec::new(); classes];
let mut starts: Vec<Vec<f64>> = vec![Vec::new(); classes];
let mut departures: Vec<Vec<f64>> = vec![Vec::new(); classes];
let mut next_arrival = 0usize;
loop {
let earliest_free = free_at.iter().copied().fold(f64::INFINITY, f64::min);
let queued = waiting.iter().any(|q| !q.is_empty());
let next_arr =
if next_arrival < pending.len() { pending[next_arrival].0 } else { f64::INFINITY };
if !queued && next_arr.is_infinite() {
break;
}
if queued && earliest_free <= next_arr {
let which = (0..c).min_by(|&x, &y| {
free_at[x].partial_cmp(&free_at[y]).unwrap_or(std::cmp::Ordering::Equal)
});
let Some(which) = which else { break };
let k = waiting.iter().position(|q| !q.is_empty()).unwrap_or(0);
let arrived = waiting[k].remove(0);
let start = earliest_free.max(arrived);
let duration = -rng.next_f64().max(1e-300).ln() / mus[k];
free_at[which] = start + duration;
busy_time[which] += duration;
arrivals[k].push(arrived);
starts[k].push(start);
departures[k].push(start + duration);
} else if next_arrival < pending.len() {
let (at, k) = pending[next_arrival];
waiting[k].push(at);
next_arrival += 1;
} else {
break;
}
}
let horizon = free_at.iter().copied().fold(t_end, f64::max);
(0..classes)
.map(|k| {
let served = arrivals[k].len();
if served == 0 {
return QueueSimResult {
l: 0.0,
lq: 0.0,
w: 0.0,
wq: 0.0,
rho: 0.0,
lambda_eff: 0.0,
served: 0,
};
}
let w = departures[k]
.iter()
.zip(&arrivals[k])
.map(|(d, a)| d - a)
.sum::<f64>()
/ served as f64;
let wq = starts[k].iter().zip(&arrivals[k]).map(|(s, a)| s - a).sum::<f64>()
/ served as f64;
let lambda_eff = served as f64 / horizon;
QueueSimResult {
l: lambda_eff * w,
lq: lambda_eff * wq,
w,
wq,
rho: lambda_eff / mus[k],
lambda_eff,
served,
}
})
.collect()
}
#[derive(Debug, Clone, PartialEq)]
pub struct Ctmc {
pub q: Matrix,
}
impl Ctmc {
pub fn new(q: Matrix) -> Result<Self, GeomError> {
if !q.is_square() || q.rows == 0 {
return Err(GeomError::InvalidArgument("Ctmc: generator must be square and non-empty"));
}
for i in 0..q.rows {
let mut sum = 0.0;
for j in 0..q.rows {
let v = q.get(i, j);
if i != j && !(v >= 0.0) {
return Err(GeomError::InvalidArgument(
"Ctmc: off-diagonal rates must be non-negative",
));
}
sum += v;
}
if sum.abs() > 1e-9 {
return Err(GeomError::InvalidArgument("Ctmc: generator rows must sum to zero"));
}
}
Ok(Self { q })
}
#[must_use]
pub fn n(&self) -> usize {
self.q.rows
}
#[must_use]
pub fn mean_holding_times(&self) -> Vec<f64> {
(0..self.n())
.map(|i| {
let rate = -self.q.get(i, i);
if rate <= 0.0 {
f64::INFINITY
} else {
1.0 / rate
}
})
.collect()
}
pub fn embedded_chain(&self) -> Result<crate::stochastic::markov::MarkovChain, GeomError> {
let n = self.n();
let mut p = Matrix::zeros(n, n);
for i in 0..n {
let out = -self.q.get(i, i);
if out <= 0.0 {
p.set(i, i, 1.0);
continue;
}
for j in 0..n {
if i != j {
p.set(i, j, self.q.get(i, j) / out);
}
}
}
crate::stochastic::markov::MarkovChain::new(p)
}
pub fn stationary(&self) -> Result<Vec<f64>, GeomError> {
let n = self.n();
let mut a = Matrix::zeros(n, n);
for i in 0..n - 1 {
for j in 0..n {
a.set(i, j, self.q.get(j, i));
}
}
for j in 0..n {
a.set(n - 1, j, 1.0);
}
let mut b = vec![0.0; n];
b[n - 1] = 1.0;
lu::solve(&a, &b)
.map_err(|_| GeomError::Degenerate("Ctmc::stationary: balance equations are singular"))
}
#[must_use]
pub fn simulate(&self, start: usize, t_end: f64, rng: &mut Rng) -> Vec<(f64, usize)> {
assert!(start < self.n(), "Ctmc::simulate: start state out of range");
assert!(t_end > 0.0, "Ctmc::simulate requires a positive horizon");
let mut out = vec![(0.0, start)];
let mut t = 0.0f64;
let mut s = start;
loop {
let rate = -self.q.get(s, s);
if rate <= 0.0 {
return out;
}
t += -rng.next_f64().max(1e-300).ln() / rate;
if t > t_end {
return out;
}
let mut u = rng.next_f64() * rate;
let mut next = s;
for j in 0..self.n() {
if j == s {
continue;
}
u -= self.q.get(s, j);
if u <= 0.0 {
next = j;
break;
}
}
s = next;
out.push((t, s));
}
}
pub fn first_passage(&self, from: usize, to: usize) -> Result<f64, GeomError> {
let n = self.n();
if from >= n || to >= n {
return Err(GeomError::InvalidArgument("Ctmc::first_passage: state out of range"));
}
if from == to {
return Ok(0.0);
}
let mut a = Matrix::zeros(n, n);
let mut b = vec![0.0; n];
for i in 0..n {
if i == to {
a.set(i, i, 1.0);
continue;
}
let out = -self.q.get(i, i);
if out <= 0.0 {
return Ok(f64::INFINITY);
}
a.set(i, i, 1.0);
for j in 0..n {
if j != i && j != to {
a.set(i, j, -self.q.get(i, j) / out);
}
}
b[i] = 1.0 / out;
}
let m = lu::solve(&a, &b)
.map_err(|_| GeomError::Degenerate("Ctmc::first_passage: system is singular"))?;
Ok(m[from])
}
}
pub fn uniformization(
q_matrix: &Matrix,
p0: &[f64],
t: f64,
eps: f64,
) -> Result<Vec<f64>, GeomError> {
let n = q_matrix.rows;
if !q_matrix.is_square() || p0.len() != n || n == 0 {
return Err(GeomError::InvalidArgument("uniformization: shape mismatch"));
}
if !(t > 0.0) || !(eps > 0.0) {
return Err(GeomError::InvalidArgument("uniformization: t and eps must be positive"));
}
let mass: f64 = p0.iter().sum();
if (mass - 1.0).abs() > 1e-9 || p0.iter().any(|&x| x < 0.0) {
return Err(GeomError::InvalidArgument("uniformization: p0 must be a distribution"));
}
let mut lambda = 0.0f64;
for i in 0..n {
lambda = lambda.max(-q_matrix.get(i, i));
}
if lambda <= 0.0 {
return Ok(p0.to_vec());
}
let mut p = Matrix::zeros(n, n);
for i in 0..n {
for j in 0..n {
let delta = if i == j { 1.0 } else { 0.0 };
p.set(i, j, delta + q_matrix.get(i, j) / lambda);
}
}
let lt = lambda * t;
let mut out = vec![0.0; n];
let mut vec_k = p0.to_vec();
let ln_lt = lt.ln();
let mut log_weight = -lt;
let mut accumulated = 0.0f64;
for k in 0..MAX_UNIFORMIZATION_TERMS {
let past_mode = (k as f64) > lt;
if log_weight > LOG_UNDERFLOW {
let weight = log_weight.exp();
for i in 0..n {
out[i] += weight * vec_k[i];
}
accumulated += weight;
} else if past_mode {
break;
}
if past_mode && 1.0 - accumulated < eps {
break;
}
let mut next = vec![0.0; n];
for i in 0..n {
let v = vec_k[i];
if v == 0.0 {
continue;
}
for j in 0..n {
next[j] += v * p.get(i, j);
}
}
vec_k = next;
log_weight += ln_lt - ((k + 1) as f64).ln();
}
let total: f64 = out.iter().sum();
if total > 0.0 {
for v in &mut out {
*v /= total;
}
}
Ok(out)
}
const MAX_UNIFORMIZATION_TERMS: usize = 1_000_000;
const LOG_UNDERFLOW: f64 = -745.0;
pub fn queue_transient_mm1(
lambda: f64,
mu: f64,
n0: usize,
t: f64,
) -> Result<Vec<f64>, GeomError> {
if !(lambda > 0.0) || !(mu > 0.0) {
return Err(GeomError::InvalidArgument("queue_transient_mm1 requires positive rates"));
}
if !(t > 0.0) {
return Err(GeomError::InvalidArgument("queue_transient_mm1 requires t > 0"));
}
let rho = lambda / mu;
let net = lambda - mu;
let reach = if rho < 1.0 {
let tail = 41.5 / (1.0 / rho).ln().max(1e-3);
n0 as f64 + tail + 10.0 * (n0 as f64).sqrt() + 20.0
} else {
n0 as f64 + net * t + 10.0 * ((lambda + mu) * t).sqrt()
};
let cap = (reach.ceil() as usize).clamp(n0 + 20, 2000);
let n = cap + 1;
let mut q = Matrix::zeros(n, n);
for i in 0..n {
let up = if i + 1 < n { lambda } else { 0.0 };
let down = if i > 0 { mu } else { 0.0 };
if up > 0.0 {
q.set(i, i + 1, up);
}
if down > 0.0 {
q.set(i, i - 1, down);
}
q.set(i, i, -(up + down));
}
let mut p0 = vec![0.0; n];
p0[n0.min(cap)] = 1.0;
uniformization(&q, &p0, t, 1e-12)
}
#[cfg(test)]
mod tests {
use super::*;
fn exponential(rate: f64) -> impl Fn(&mut Rng) -> f64 {
move |rng: &mut Rng| -rng.next_f64().max(1e-300).ln() / rate
}
fn close(a: f64, b: f64, tol: f64) -> bool {
(a - b).abs() <= tol * (1.0 + a.abs().max(b.abs()))
}
#[test]
fn mm1_state_distribution_is_the_geometric_law() {
let (lambda, mu) = (0.6, 1.0);
let q = mm1(lambda, mu);
let rho = lambda / mu;
for n in 0..40 {
let expected = (1.0 - rho) * rho.powi(n);
assert!(
(q.pn(n as usize) - expected).abs() < 1e-12,
"p_{n} = {} but the geometric law gives {expected}",
q.pn(n as usize)
);
}
}
#[test]
fn closed_form_means_are_the_moments_of_the_reported_distribution() {
let cases: Vec<(QueueMetrics, usize, usize)> = vec![
(mm1(0.6, 1.0), 1, 400),
(mmc(2.4, 1.0, 3), 3, 400),
(mmc(7.0, 1.0, 9), 9, 400),
(mm1k(0.8, 1.0, 12), 1, 13),
(mmck(3.0, 1.0, 2, 9), 2, 10),
(mm_inf(4.0, 1.0), usize::MAX, 200),
];
for (q, c, terms) in cases {
let mut mass = 0.0;
let mut l = 0.0;
let mut lq = 0.0;
for n in 0..terms {
let p = q.pn(n);
mass += p;
l += n as f64 * p;
if c != usize::MAX {
lq += n.saturating_sub(c) as f64 * p;
}
}
assert!((mass - 1.0).abs() < 1e-9, "probabilities summed to {mass}, not one");
assert!(close(l, q.l, 1e-8), "sum n p_n = {l} but L = {}", q.l);
if c != usize::MAX {
assert!(close(lq, q.lq, 1e-8), "sum (n-c)+ p_n = {lq} but Lq = {}", q.lq);
}
}
}
#[test]
fn littles_law_holds_for_every_closed_form_model() {
let models = [
mm1(0.7, 1.0),
mmc(3.5, 1.0, 4),
mm1k(1.3, 1.0, 8),
mmck(5.0, 1.0, 3, 11),
mm_inf(2.5, 1.0),
mg1_pollaczek_khinchine(0.5, 1.0, 0.0),
mg1_pollaczek_khinchine(0.5, 1.0, 4.0),
];
for q in models {
assert!(
littles_law_check(q.l, q.lambda_eff, q.w).abs() < 1e-9,
"L = {} but lambda W = {}",
q.l,
q.lambda_eff * q.w
);
assert!(
littles_law_check(q.lq, q.lambda_eff, q.wq).abs() < 1e-9,
"Lq = {} but lambda Wq = {}",
q.lq,
q.lambda_eff * q.wq
);
}
}
#[test]
fn service_time_is_exactly_the_gap_between_sojourn_and_wait() {
for (lambda, mu, c, k) in
[(0.7, 1.0, 1, 0usize), (3.5, 1.1, 4, 0), (1.3, 1.0, 1, 8), (5.0, 1.0, 3, 11)]
{
let q = if k == 0 { mmc(lambda, mu, c) } else { mmck(lambda, mu, c, k) };
assert!(close(q.w - q.wq, 1.0 / mu, 1e-9), "W - Wq = {}", q.w - q.wq);
assert!(
close(q.l - q.lq, q.lambda_eff / mu, 1e-9),
"L - Lq = {} but lambda_eff / mu = {}",
q.l - q.lq,
q.lambda_eff / mu
);
}
}
#[test]
fn one_server_specialisations_agree() {
let (lambda, mu) = (0.55, 1.3);
let a = mm1(lambda, mu);
let b = mmc(lambda, mu, 1);
assert_eq!(a, b);
let k = mm1k(lambda, mu, 7);
let k2 = mmck(lambda, mu, 1, 7);
assert_eq!(k, k2);
let g = mg1_pollaczek_khinchine(lambda, 1.0 / mu, 1.0 / (mu * mu));
assert!(close(g.lq, a.lq, 1e-9), "P-K Lq = {} but M/M/1 Lq = {}", g.lq, a.lq);
assert!(close(g.l, a.l, 1e-9), "P-K L = {} but M/M/1 L = {}", g.l, a.l);
assert!(close(g.wq, a.wq, 1e-9));
assert!(close(g.p0, a.p0, 1e-9));
}
#[test]
fn finite_capacity_converges_to_the_infinite_queue() {
let (lambda, mu) = (0.6, 1.0);
let unbounded = mm1(lambda, mu);
let mut previous = f64::INFINITY;
for k in [5usize, 10, 20, 40, 80] {
let bounded = mm1k(lambda, mu, k);
let gap = (bounded.l - unbounded.l).abs();
assert!(gap < previous, "capacity {k} did not improve on the previous truncation");
assert!(bounded.l <= unbounded.l + 1e-12);
previous = gap;
}
assert!(previous < 1e-12, "K = 80 still differs from M/M/1 by {previous}");
}
#[test]
fn deterministic_service_halves_the_exponential_queue() {
for lambda in [0.2, 0.5, 0.8, 0.95] {
let mu = 1.0;
let md1 = mg1_pollaczek_khinchine(lambda, 1.0 / mu, 0.0);
let mm1_ = mm1(lambda, mu);
assert!(
close(md1.lq, 0.5 * mm1_.lq, 1e-9),
"at lambda = {lambda}, M/D/1 Lq = {} against half of {}",
md1.lq,
mm1_.lq
);
}
}
#[test]
fn kingman_is_exact_for_markovian_arrivals_and_service() {
for (lambda, mu) in [(0.3, 1.0), (0.5, 0.9), (0.85, 1.0)] {
let approx = gg1_kingman_approx(lambda, mu, 1.0, 1.0);
let exact = mm1(lambda, mu).wq;
assert!(close(approx, exact, 1e-12), "Kingman {approx} against exact {exact}");
}
let base = gg1_kingman_approx(0.7, 1.0, 1.0, 1.0);
assert!(gg1_kingman_approx(0.7, 1.0, 1.0, 0.0) < base);
assert!(gg1_kingman_approx(0.7, 1.0, 1.0, 4.0) > base);
}
#[test]
fn erlang_b_recursion_matches_the_factorial_ratio() {
for &a in &[0.5, 1.0, 3.0, 7.5] {
for c in 1..=15usize {
let mut terms = Vec::with_capacity(c + 1);
let mut term = 1.0f64;
terms.push(term);
for k in 1..=c {
term *= a / k as f64;
terms.push(term);
}
let direct = terms[c] / terms.iter().sum::<f64>();
let recursive = erlang_b(a, c);
assert!(
(direct - recursive).abs() < 1e-12,
"a = {a}, c = {c}: direct {direct} against recursion {recursive}"
);
}
}
}
#[test]
fn erlang_b_survives_a_trunk_count_that_overflows_the_direct_form() {
let b = erlang_b(180.0, 200);
assert!(b.is_finite() && b > 0.0 && b < 1.0, "B(200, 180) = {b}");
assert!(erlang_b(180.0, 220) < b);
}
#[test]
fn erlang_b_is_monotone_in_both_arguments() {
let a = 4.0;
let mut previous = 1.0;
for c in 1..=30usize {
let b = erlang_b(a, c);
assert!(b < previous, "adding a trunk did not reduce blocking at c = {c}");
assert!((0.0..=1.0).contains(&b));
previous = b;
}
let c = 6;
let mut previous = 0.0;
for step in 1..=20 {
let b = erlang_b(step as f64 * 0.5, c);
assert!(b > previous, "more load did not raise blocking at step {step}");
previous = b;
}
}
#[test]
fn erlang_c_is_the_tail_mass_of_the_mmc_distribution() {
for (lambda, mu, c) in [(0.4, 1.0, 1usize), (2.4, 1.0, 3), (7.0, 1.0, 9), (11.0, 2.0, 7)] {
let q = mmc(lambda, mu, c);
let tail: f64 = (c..3000).map(|n| q.pn(n)).sum();
let formula = erlang_c(lambda / mu, c);
assert!(
close(tail, formula, 1e-9),
"c = {c}: tail mass {tail} against Erlang C {formula}"
);
}
}
#[test]
fn erlang_c_exceeds_erlang_b_and_reduces_to_rho_for_one_server() {
for &a in &[0.3, 1.0, 4.0] {
if a < 1.0 {
assert!(close(erlang_c(a, 1), a, 1e-12), "C(1, {a}) = {}", erlang_c(a, 1));
}
for c in 1..=12usize {
if a / c as f64 >= 1.0 {
continue;
}
let (b, cc) = (erlang_b(a, c), erlang_c(a, c));
assert!(cc >= b - 1e-12, "c = {c}, a = {a}: C = {cc} below B = {b}");
assert!((0.0..=1.0).contains(&cc));
}
}
}
#[test]
fn inverse_capacity_returns_the_smallest_sufficient_trunk_count() {
for &(load, target) in &[(1.0, 0.01), (5.0, 0.02), (20.0, 0.001), (0.5, 0.5)] {
let c = erlang_b_inverse_capacity(load, target);
assert!(erlang_b(load, c) <= target, "c = {c} does not meet the target");
assert!(
c == 0 || erlang_b(load, c - 1) > target,
"c = {c} is not minimal: c - 1 already meets the target"
);
}
}
#[test]
fn mm_inf_is_poisson_and_nobody_waits() {
let (lambda, mu) = (3.5, 0.7);
let a = lambda / mu;
let q = mm_inf(lambda, mu);
assert_eq!(q.lq, 0.0);
assert_eq!(q.wq, 0.0);
assert!(close(q.l, a, 1e-12));
assert!(close(q.w, 1.0 / mu, 1e-12));
let mut term = (-a).exp();
for n in 0..60 {
assert!((q.pn(n) - term).abs() < 1e-12, "p_{n} is not the Poisson mass");
term *= a / (n + 1) as f64;
}
}
#[test]
fn saturated_queues_report_infinite_means() {
let q = mmc(2.0, 1.0, 2);
assert!(q.l.is_infinite() && q.w.is_infinite());
assert!(q.rho >= 1.0);
assert!(gg1_kingman_approx(1.0, 1.0, 1.0, 1.0).is_infinite());
assert!(mg1_pollaczek_khinchine(1.0, 1.0, 0.5).lq.is_infinite());
let bounded = mm1k(5.0, 1.0, 4);
assert!(bounded.l.is_finite() && bounded.l <= 4.0);
assert!(bounded.rho < 1.0);
}
#[test]
fn jackson_rates_solve_the_traffic_equations_and_conserve_flow() {
let routing = Matrix::from_rows(&[&[0.0, 0.5], &[0.25, 0.0]]).unwrap();
let external = [1.0, 0.5];
let service = [4.0, 3.0];
let servers = [1usize, 1];
let out = jackson_network(&routing, &external, &service, &servers).unwrap();
let rates: Vec<f64> = out.iter().map(|q| q.lambda_eff).collect();
for j in 0..2 {
let inflow: f64 =
external[j] + (0..2).map(|i| rates[i] * routing.get(i, j)).sum::<f64>();
assert!(
close(rates[j], inflow, 1e-9),
"node {j}: rate {} against inflow {inflow}",
rates[j]
);
}
let entered: f64 = external.iter().sum();
let departed: f64 = (0..2)
.map(|i| rates[i] * (1.0 - (0..2).map(|j| routing.get(i, j)).sum::<f64>()))
.sum();
assert!(close(entered, departed, 1e-9), "{entered} in against {departed} out");
for (j, q) in out.iter().enumerate() {
assert_eq!(*q, mmc(rates[j], service[j], servers[j]));
}
}
#[test]
fn a_jackson_tandem_passes_its_arrival_rate_straight_through() {
let routing = Matrix::from_rows(&[&[0.0, 1.0], &[0.0, 0.0]]).unwrap();
let out = jackson_network(&routing, &[2.0, 0.0], &[5.0, 3.0], &[1, 1]).unwrap();
assert!(close(out[0].lambda_eff, 2.0, 1e-12));
assert!(close(out[1].lambda_eff, 2.0, 1e-12));
let total = out[0].w + out[1].w;
assert!(close(total, 1.0 / (5.0 - 2.0) + 1.0 / (3.0 - 2.0), 1e-12), "total W = {total}");
}
#[test]
fn jackson_rejects_malformed_input() {
let square = Matrix::from_rows(&[&[0.0, 0.5], &[0.25, 0.0]]).unwrap();
assert!(jackson_network(&square, &[1.0], &[1.0], &[1]).is_err());
assert!(jackson_network(&square, &[1.0, 1.0], &[1.0], &[1, 1]).is_err());
let overfull = Matrix::from_rows(&[&[0.7, 0.7], &[0.0, 0.0]]).unwrap();
assert!(jackson_network(&overfull, &[1.0, 1.0], &[9.0, 9.0], &[1, 1]).is_err());
let negative = Matrix::from_rows(&[&[0.0, -0.5], &[0.0, 0.0]]).unwrap();
assert!(jackson_network(&negative, &[1.0, 1.0], &[9.0, 9.0], &[1, 1]).is_err());
}
#[test]
fn simulated_mm1_reproduces_its_closed_form() {
let (lambda, mu) = (0.6, 1.0);
let mut rng = Rng::new(0x51DE_0001);
let sim =
queue_simulate(&exponential(lambda), &exponential(mu), 1, 400_000.0, &mut rng);
let exact = mm1(lambda, mu);
assert!(sim.served > 200_000, "only {} customers served", sim.served);
assert!(close(sim.w, exact.w, 0.03), "W {} against {}", sim.w, exact.w);
assert!(close(sim.wq, exact.wq, 0.04), "Wq {} against {}", sim.wq, exact.wq);
assert!(close(sim.l, exact.l, 0.03), "L {} against {}", sim.l, exact.l);
assert!(close(sim.lq, exact.lq, 0.05), "Lq {} against {}", sim.lq, exact.lq);
assert!(close(sim.rho, exact.rho, 0.02), "rho {} against {}", sim.rho, exact.rho);
}
#[test]
fn the_simulated_time_average_and_customer_average_satisfy_littles_law() {
let mut rng = Rng::new(0x51DE_0002);
let sim = queue_simulate(&exponential(1.4), &exponential(2.0), 2, 200_000.0, &mut rng);
let residual = littles_law_check(sim.l, sim.lambda_eff, sim.w);
assert!(
residual.abs() < 0.02 * sim.l,
"L = {} but lambda W = {}",
sim.l,
sim.lambda_eff * sim.w
);
let residual_q = littles_law_check(sim.lq, sim.lambda_eff, sim.wq);
assert!(residual_q.abs() < 0.02 * sim.l, "Lq = {} against lambda Wq", sim.lq);
}
#[test]
fn simulated_md1_matches_pollaczek_khinchine() {
let (lambda, service) = (0.6, 1.0);
let mut rng = Rng::new(0x51DE_0003);
let sim = queue_simulate(
&exponential(lambda),
&move |_: &mut Rng| service,
1,
400_000.0,
&mut rng,
);
let pk = mg1_pollaczek_khinchine(lambda, service, 0.0);
let exponential_service = mm1(lambda, 1.0 / service);
assert!(close(sim.wq, pk.wq, 0.04), "simulated Wq {} against P-K {}", sim.wq, pk.wq);
assert!(
(sim.wq - exponential_service.wq).abs() > 0.3 * exponential_service.wq,
"M/D/1 came out indistinguishable from M/M/1"
);
}
#[test]
fn simulated_mmc_reproduces_the_multi_server_closed_form() {
let (lambda, mu, c) = (2.4, 1.0, 3usize);
let mut rng = Rng::new(0x51DE_0004);
let sim = queue_simulate(&exponential(lambda), &exponential(mu), c, 150_000.0, &mut rng);
let exact = mmc(lambda, mu, c);
assert!(close(sim.w, exact.w, 0.04), "W {} against {}", sim.w, exact.w);
assert!(close(sim.lq, exact.lq, 0.07), "Lq {} against {}", sim.lq, exact.lq);
assert!(close(sim.rho, exact.rho, 0.02), "rho {} against {}", sim.rho, exact.rho);
}
#[test]
fn an_idle_horizon_produces_an_empty_but_well_formed_result() {
let mut rng = Rng::new(7);
let sim = queue_simulate(&|_: &mut Rng| 100.0, &exponential(1.0), 1, 1.0, &mut rng);
assert_eq!(sim.served, 0);
assert_eq!(sim.l, 0.0);
assert_eq!(sim.rho, 0.0);
}
#[test]
fn non_preemptive_priority_matches_the_cobham_formula() {
let lambdas = [0.2, 0.3, 0.15];
let mu = 1.0;
let mus = [mu; 3];
let mut rng = Rng::new(0x51DE_0005);
let sim = priority_queue_simulate(&lambdas, &mus, 1, 600_000.0, &mut rng);
let w0: f64 = lambdas.iter().map(|&l| l * 2.0 / (mu * mu) / 2.0).sum();
let mut sigma_prev = 0.0;
for k in 0..3 {
let sigma = sigma_prev + lambdas[k] / mu;
let expected = w0 / ((1.0 - sigma_prev) * (1.0 - sigma));
assert!(
close(sim[k].wq, expected, 0.06),
"class {k}: simulated Wq {} against Cobham {expected}",
sim[k].wq
);
sigma_prev = sigma;
}
}
#[test]
fn priority_ordering_shortens_the_top_class_and_lengthens_the_bottom() {
let lambdas = [0.25, 0.25, 0.2];
let mus = [1.0; 3];
let mut rng = Rng::new(0x51DE_0006);
let sim = priority_queue_simulate(&lambdas, &mus, 1, 400_000.0, &mut rng);
assert!(sim[0].wq < sim[1].wq, "class 0 did not beat class 1");
assert!(sim[1].wq < sim[2].wq, "class 1 did not beat class 2");
let total: f64 = lambdas.iter().sum();
let fifo = mm1(total, 1.0);
let weighted: f64 = (0..3).map(|k| lambdas[k] / mus[k] * sim[k].wq).sum();
let reference = total / 1.0 * fifo.wq;
assert!(
close(weighted, reference, 0.06),
"priority weighted wait {weighted} against FIFO {reference}"
);
}
fn birth_death_generator(lambda: f64, mu: f64, k: usize) -> Matrix {
let n = k + 1;
let mut q = Matrix::zeros(n, n);
for i in 0..n {
let up = if i + 1 < n { lambda } else { 0.0 };
let down = if i > 0 { mu } else { 0.0 };
if up > 0.0 {
q.set(i, i + 1, up);
}
if down > 0.0 {
q.set(i, i - 1, down);
}
q.set(i, i, -(up + down));
}
q
}
#[test]
fn ctmc_rejects_matrices_that_are_not_generators() {
assert!(Ctmc::new(Matrix::zeros(2, 3)).is_err());
let bad_row = Matrix::from_rows(&[&[-1.0, 1.0], &[1.0, 0.0]]).unwrap();
assert!(Ctmc::new(bad_row).is_err());
let negative_rate = Matrix::from_rows(&[&[1.0, -1.0], &[1.0, -1.0]]).unwrap();
assert!(Ctmc::new(negative_rate).is_err());
assert!(Ctmc::new(birth_death_generator(1.0, 2.0, 3)).is_ok());
}
#[test]
fn ctmc_stationary_satisfies_global_balance() {
let chain = Ctmc::new(birth_death_generator(1.5, 2.0, 6)).unwrap();
let pi = chain.stationary().unwrap();
assert!(close(pi.iter().sum::<f64>(), 1.0, 1e-12));
assert!(pi.iter().all(|&p| p >= -1e-12));
for j in 0..chain.n() {
let flow: f64 = (0..chain.n()).map(|i| pi[i] * chain.q.get(i, j)).sum();
assert!(flow.abs() < 1e-10, "state {j} has net probability flow {flow}");
}
}
#[test]
fn the_ctmc_of_a_finite_queue_has_the_analytic_stationary_distribution() {
let (lambda, mu, k) = (1.5, 2.0, 9usize);
let chain = Ctmc::new(birth_death_generator(lambda, mu, k)).unwrap();
let pi = chain.stationary().unwrap();
let analytic = mm1k(lambda, mu, k);
for n in 0..=k {
assert!(
(pi[n] - analytic.pn(n)).abs() < 1e-10,
"state {n}: solver {} against product form {}",
pi[n],
analytic.pn(n)
);
}
}
#[test]
fn stationary_is_the_jump_chain_reweighted_by_holding_time() {
let chain = Ctmc::new(birth_death_generator(1.0, 1.7, 5)).unwrap();
let pi = chain.stationary().unwrap();
let nu = chain.embedded_chain().unwrap().stationary();
let h = chain.mean_holding_times();
let unnormalised: Vec<f64> = nu.iter().zip(&h).map(|(&v, &t)| v * t).collect();
let total: f64 = unnormalised.iter().sum();
for i in 0..chain.n() {
let predicted = unnormalised[i] / total;
assert!(
(pi[i] - predicted).abs() < 1e-9,
"state {i}: {} against reweighted jump chain {predicted}",
pi[i]
);
}
}
#[test]
fn embedded_chain_is_stochastic_and_holds_no_self_transitions() {
let chain = Ctmc::new(birth_death_generator(1.0, 1.7, 4)).unwrap();
let jump = chain.embedded_chain().unwrap();
for i in 0..chain.n() {
let row: f64 = (0..chain.n()).map(|j| jump.p.get(i, j)).sum();
assert!((row - 1.0).abs() < 1e-12, "row {i} sums to {row}");
assert_eq!(jump.p.get(i, i), 0.0, "state {i} has a spurious self-loop");
}
let absorbing = Matrix::from_rows(&[&[-1.0, 1.0], &[0.0, 0.0]]).unwrap();
let jump = Ctmc::new(absorbing).unwrap().embedded_chain().unwrap();
assert_eq!(jump.p.get(1, 1), 1.0);
}
#[test]
fn first_passage_up_a_pure_birth_chain_is_the_sum_of_holding_times() {
let n = 6usize;
let rates = [1.0, 2.0, 0.5, 3.0, 1.5];
let mut q = Matrix::zeros(n, n);
for i in 0..n - 1 {
q.set(i, i + 1, rates[i]);
q.set(i, i, -rates[i]);
}
let chain = Ctmc::new(q).unwrap();
let expected: f64 = rates.iter().map(|r| 1.0 / r).sum();
let got = chain.first_passage(0, n - 1).unwrap();
assert!(close(got, expected, 1e-9), "{got} against {expected}");
assert_eq!(chain.first_passage(3, 3).unwrap(), 0.0);
assert!(chain.first_passage(9, 0).is_err());
assert!(chain.first_passage(4, 1).unwrap().is_infinite());
}
#[test]
fn first_passage_matches_a_long_simulation() {
let chain = Ctmc::new(birth_death_generator(1.0, 1.5, 4)).unwrap();
let target = 4usize;
let predicted = chain.first_passage(0, target).unwrap();
let mut rng = Rng::new(0x51DE_0007);
let trials = 4000;
let mut total = 0.0;
for _ in 0..trials {
let path = chain.simulate(0, predicted * 60.0, &mut rng);
let hit = path.iter().find(|&&(_, s)| s == target).map(|&(t, _)| t);
total += hit.unwrap_or(predicted * 60.0);
}
let measured = total / trials as f64;
assert!(close(measured, predicted, 0.06), "simulated {measured} against {predicted}");
}
#[test]
fn simulated_occupancy_matches_the_stationary_distribution() {
let chain = Ctmc::new(birth_death_generator(1.2, 1.8, 5)).unwrap();
let pi = chain.stationary().unwrap();
let horizon = 300_000.0;
let mut rng = Rng::new(0x51DE_0008);
let path = chain.simulate(0, horizon, &mut rng);
let mut time_in = vec![0.0; chain.n()];
for w in path.windows(2) {
time_in[w[0].1] += w[1].0 - w[0].0;
}
if let Some(&(t, s)) = path.last() {
time_in[s] += horizon - t;
}
for i in 0..chain.n() {
let fraction = time_in[i] / horizon;
assert!(
(fraction - pi[i]).abs() < 0.01,
"state {i}: occupied {fraction} of the time against pi = {}",
pi[i]
);
}
}
#[test]
fn uniformization_matches_the_two_state_closed_form() {
let (a, b) = (0.7, 1.3);
let q = Matrix::from_rows(&[&[-a, a], &[b, -b]]).unwrap();
for &t in &[0.05, 0.5, 2.0, 10.0] {
let p = uniformization(&q, &[1.0, 0.0], t, 1e-14).unwrap();
let exact = b / (a + b) + a / (a + b) * (-(a + b) * t).exp();
assert!(
(p[0] - exact).abs() < 1e-9,
"t = {t}: uniformization {} against exp(Qt) {exact}",
p[0]
);
assert!((p.iter().sum::<f64>() - 1.0).abs() < 1e-12);
}
}
#[test]
fn uniformization_relaxes_to_the_stationary_distribution() {
let chain = Ctmc::new(birth_death_generator(1.1, 1.9, 6)).unwrap();
let pi = chain.stationary().unwrap();
let n = chain.n();
let mut start = vec![0.0; n];
start[n - 1] = 1.0;
let mut previous = f64::INFINITY;
for &t in &[0.5, 2.0, 8.0, 40.0, 120.0] {
let p = uniformization(&chain.q, &start, t, 1e-14).unwrap();
assert!(p.iter().all(|&x| x >= -1e-12), "a probability went negative at t = {t}");
assert!((p.iter().sum::<f64>() - 1.0).abs() < 1e-12);
let distance: f64 =
p.iter().zip(&pi).map(|(a, b)| (a - b).abs()).sum::<f64>() / 2.0;
assert!(distance < previous, "distance to stationary grew at t = {t}");
previous = distance;
}
assert!(previous < 1e-12, "still {previous} away from stationary at t = 120");
}
#[test]
fn uniformization_is_stable_on_a_stiff_generator() {
let q = Matrix::from_rows(&[
&[-1000.0, 1000.0, 0.0],
&[0.0, -1000.5, 1000.5],
&[0.5, 0.0, -0.5],
])
.unwrap();
let p = uniformization(&q, &[1.0, 0.0, 0.0], 1.0, 1e-12).unwrap();
assert!(p.iter().all(|&x| x >= 0.0), "negative probability: {p:?}");
assert!((p.iter().sum::<f64>() - 1.0).abs() < 1e-12);
let pi = Ctmc::new(q).unwrap().stationary().unwrap();
let distance: f64 = p.iter().zip(&pi).map(|(a, b)| (a - b).abs()).sum::<f64>() / 2.0;
assert!(distance < 0.35, "distance {distance} is implausibly large");
}
#[test]
fn uniformization_survives_a_horizon_whose_poisson_mass_underflows() {
let chain = Ctmc::new(birth_death_generator(3.0, 4.0, 8)).unwrap();
let pi = chain.stationary().unwrap();
let n = chain.n();
let mut start = vec![0.0; n];
start[n - 1] = 1.0;
let rate = (0..n).map(|i| -chain.q.get(i, i)).fold(0.0f64, f64::max);
for &t in &[200.0f64, 800.0, 4000.0] {
assert!(rate * t > 745.0, "Lt = {} is not past the underflow point", rate * t);
let p = uniformization(&chain.q, &start, t, 1e-14).unwrap();
assert!(p.iter().all(|v| v.is_finite()), "t = {t} produced {p:?}");
assert!((p.iter().sum::<f64>() - 1.0).abs() < 1e-12, "t = {t} is not a distribution");
assert!(p.iter().all(|&v| v >= -1e-15));
for i in 0..n {
assert!(
(p[i] - pi[i]).abs() < 1e-9,
"t = {t}, state {i}: {} against stationary {}",
p[i],
pi[i]
);
}
}
}
#[test]
fn uniformization_rejects_malformed_input() {
let q = birth_death_generator(1.0, 1.0, 2);
assert!(uniformization(&q, &[1.0, 0.0], 1.0, 1e-9).is_err());
assert!(uniformization(&q, &[0.5, 0.5, 0.5], 1.0, 1e-9).is_err());
assert!(uniformization(&q, &[1.0, 0.0, 0.0], -1.0, 1e-9).is_err());
assert!(uniformization(&q, &[1.0, 0.0, 0.0], 1.0, 0.0).is_err());
let frozen = Matrix::zeros(2, 2);
assert_eq!(uniformization(&frozen, &[0.3, 0.7], 5.0, 1e-9).unwrap(), vec![0.3, 0.7]);
}
#[test]
fn transient_mm1_starts_where_it_was_put_and_relaxes_to_the_geometric() {
let (lambda, mu, n0) = (1.0, 2.0, 3usize);
let short = queue_transient_mm1(lambda, mu, n0, 1e-6).unwrap();
assert!(short[n0] > 0.999, "at t = 1e-6 the mass had already left state {n0}");
let long = queue_transient_mm1(lambda, mu, n0, 400.0).unwrap();
let stationary = mm1(lambda, mu);
for n in 0..25 {
assert!(
(long[n] - stationary.pn(n)).abs() < 1e-8,
"state {n}: transient {} against stationary {}",
long[n],
stationary.pn(n)
);
}
let mean = |p: &[f64]| p.iter().enumerate().map(|(n, &q)| n as f64 * q).sum::<f64>();
let mut previous = n0 as f64;
for &t in &[0.1, 0.5, 2.0, 10.0, 400.0] {
let m = mean(&queue_transient_mm1(lambda, mu, n0, t).unwrap());
assert!(m < previous + 1e-9, "the mean rose at t = {t}");
previous = m;
}
assert!(close(previous, stationary.l, 1e-6), "settled at {previous}, not {}", stationary.l);
}
#[test]
fn transient_mm1_rejects_bad_arguments() {
assert!(queue_transient_mm1(0.0, 1.0, 0, 1.0).is_err());
assert!(queue_transient_mm1(1.0, 0.0, 0, 1.0).is_err());
assert!(queue_transient_mm1(1.0, 1.0, 0, 0.0).is_err());
}
}