Source code for line_solver.api.pfqn.pam

"""
Hsieh-Lam Proportional Approximation Methods (PAMB/PAMI/PAMT), Hsieh and
Lam (1988).

Demand matrices follow the LINE convention L[station, class]; the cited papers
index them the other way round as D_ck.
"""

from typing import Tuple

import numpy as np

from .utils import _amva_prep

__all__ = ['pfqn_pam']


[docs] def pfqn_pam(L, N, Z=None, variant: str = 'pamb' ) -> Tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: """Hsieh-Lam Proportional Approximation Methods (PAMB/PAMI/PAMT). C. T. Hsieh, S. S. Lam, "PAM - A noniterative approximate solution method for closed multichain queueing networks", ACM SIGMETRICS Perform. Eval. Rev. 16(1), 1988. The three variants are NONITERATIVE: the queue lengths are seeded by the proportion of a class demand that falls at each centre, E_ck = D_ck / sum_i D_ci, Q_ck(N) = E_ck N_c, and the MVA equations are then unrolled a fixed number of times. 'pamb' applies the last MVA step; 'pami' additionally scales a class down wherever it would drive a centre past full utilization; 'pamt' seeds at N - 1_i - 1_j and applies the last TWO MVA steps before that capping. The seed spreads the whole class population over the queueing centres and ignores Z, exactly as published: PAM buys speed, not accuracy. Returns (XN, QN, UN, RN). """ L, N, Z, M, R = _amva_prep(L, N, Z) variant = (variant or 'pamb').lower() E = np.zeros((M, R)) Ltot = L.sum(axis=0) for r in range(R): if Ltot[r] > 0: E[:, r] = L[:, r] / Ltot[r] Q = E * np.tile(N, (M, 1)) RN = np.zeros((M, R)) XN = np.zeros(R) if variant == 'pamt': for i in range(R): Qmi = np.zeros((M, R)) for j in range(R): # Q_ck(N - 1_i - 1_j) = Q_ck(N) - E_ck [(c==i) + (c==j)] Qij = Q.copy() Qij[:, i] -= E[:, i] Qij[:, j] -= E[:, j] Rj = L[:, j] * (1 + Qij.sum(axis=1)) nj = N[j] - (1.0 if i == j else 0.0) Xj = nj / (Rj.sum() + Z[j]) if nj > 0 else 0.0 Qmi[:, j] = Xj * Rj RN[:, i] = L[:, i] * (1 + Qmi.sum(axis=1)) if N[i] > 0: XN[i] = N[i] / (RN[:, i].sum() + Z[i]) else: for r in range(R): # Q_jk(N - 1_r) = Q_jk(N) - E_jk [j == r] Qmr = Q.copy() Qmr[:, r] -= E[:, r] RN[:, r] = L[:, r] * (1 + Qmr.sum(axis=1)) if N[r] > 0: XN[r] = N[r] / (RN[:, r].sum() + Z[r]) if variant in ('pami', 'pamt'): # scale a class down when it would drive a centre it visits past U = 1 U = (L * np.tile(XN, (M, 1))).sum(axis=1) for r in range(R): visited = L[:, r] != 0 if np.any(visited): S = float(np.max(U[visited])) if S > 1: XN[r] = XN[r] / S QN = np.tile(XN, (M, 1)) * RN UN = np.tile(XN, (M, 1)) * L return XN.reshape(1, -1), QN, UN, RN