"""
Limited Joint-Dependence (LJD) Indexing Functions.
Native Python implementations of linearization/delinearization functions
for LJD scaling tables and multi-linear interpolation.
Key functions:
ljd_linearize: Convert population vector to linear index
ljd_delinearize: Convert linear index to population vector
ljcd_interpolate: Multi-linear interpolation for scaling tables
References:
Original MATLAB: matlab/src/api/pfqn/ljd_*.m, ljcd_interpolate.m
"""
import numpy as np
from typing import Union
[docs]
def ljd_linearize(nvec: np.ndarray, cutoffs: np.ndarray) -> int:
"""
Convert per-class population vector to linearized index.
Maps a multi-dimensional population vector to a single linear index
for efficient table lookups in LJD scaling tables.
Index formula: idx = 1 + n1 + n2*(N1+1) + n3*(N1+1)*(N2+1) + ...
Args:
nvec: Per-class populations [n1, n2, ..., nK]
cutoffs: Per-class cutoffs [N1, N2, ..., NK]
Returns:
1-based linearized index
References:
Original MATLAB: matlab/src/api/pfqn/ljd_linearize.m
"""
nvec = np.asarray(nvec, dtype=int).flatten()
cutoffs = np.asarray(cutoffs, dtype=int).flatten()
K = len(nvec)
idx = 1 # 1-indexed for MATLAB compatibility
multiplier = 1
for k in range(K):
nk = min(nvec[k], cutoffs[k]) # Clamp to cutoff
idx += nk * multiplier
multiplier *= (cutoffs[k] + 1)
return idx
[docs]
def ljd_delinearize(idx: int, cutoffs: np.ndarray) -> np.ndarray:
"""
Convert linearized index back to per-class population vector.
Inverse of ljd_linearize: recovers the multi-dimensional population
vector from a linear index.
Args:
idx: 1-based linearized index
cutoffs: Per-class cutoffs [N1, N2, ..., NK]
Returns:
Per-class populations [n1, n2, ..., nK]
References:
Original MATLAB: matlab/src/api/pfqn/ljd_delinearize.m
"""
cutoffs = np.asarray(cutoffs, dtype=int).flatten()
K = len(cutoffs)
nvec = np.zeros(K, dtype=int)
idx = idx - 1 # Convert to 0-based for computation
for k in range(K):
base = cutoffs[k] + 1
nvec[k] = idx % base
idx = idx // base
return nvec
[docs]
def ljcd_interpolate(nvec: np.ndarray, cutoffs: np.ndarray,
table: np.ndarray, K: int) -> float:
"""
Multi-linear interpolation for LJCD scaling tables.
Performs multi-linear interpolation of a throughput value from an LJCD
scaling table for non-integer population vectors.
For K classes, interpolates between 2^K corner points of the hypercube
containing the population vector using multi-linear interpolation.
Args:
nvec: Continuous population vector [n1, n2, ..., nK] (clamped to cutoffs)
cutoffs: Per-class cutoffs [N1, N2, ..., NK]
table: Linearized throughput table (1-D array indexed by ljd_linearize)
K: Number of classes
Returns:
Interpolated throughput value
References:
Original MATLAB: matlab/src/api/pfqn/ljcd_interpolate.m
"""
nvec = np.asarray(nvec, dtype=float).flatten()
cutoffs = np.asarray(cutoffs, dtype=int).flatten()
table = np.asarray(table, dtype=float).flatten()
# Get floor and ceiling for each dimension
nFloor = np.floor(nvec).astype(int)
nCeil = np.ceil(nvec).astype(int)
# Clamp to valid range
nFloor = np.maximum(0, np.minimum(nFloor, cutoffs))
nCeil = np.maximum(0, np.minimum(nCeil, cutoffs))
# Compute fractional parts (weights)
frac = nvec - nFloor
# Handle edge case: all integer values
if np.all(np.abs(frac) < 1e-10):
idx = ljd_linearize(nFloor, cutoffs)
if idx <= len(table):
return table[idx - 1] # Convert to 0-based
else:
return 0.0
# Multi-linear interpolation over 2^K corners
Xval = 0.0
numCorners = 2 ** K
for corner in range(numCorners):
# Build corner point: bit i determines floor (0) or ceil (1) for class i
cornerPoint = nFloor.copy()
weight = 1.0
for i in range(K):
if (corner >> i) & 1:
# Use ceiling for this dimension
cornerPoint[i] = nCeil[i]
weight *= frac[i]
else:
# Use floor for this dimension
weight *= (1 - frac[i])
# Look up table value at corner point
idx = ljd_linearize(cornerPoint, cutoffs)
if idx <= len(table):
Xval += weight * table[idx - 1] # Convert to 0-based
return Xval
[docs]
def infradius_h(x: np.ndarray, L: np.ndarray, N: np.ndarray,
alpha: np.ndarray) -> np.ndarray:
"""
Helper function for infinite radius computation with logistic transformation.
Used in normalizing constant computation via integration methods.
Args:
x: Logistic transformation parameters
L: Service demand matrix (M x R)
N: Population vector (R,)
alpha: Load-dependent rate matrix
Returns:
Evaluated function value for integration
References:
Original MATLAB: matlab/src/api/pfqn/infradius_h.m
"""
# Import here to avoid circular dependency
from .ncld import pfqn_gld
L = np.atleast_2d(np.asarray(L, dtype=float))
N = np.asarray(N, dtype=float).flatten()
x = np.atleast_2d(np.asarray(x, dtype=float))
M = L.shape[0]
Nt = int(np.sum(N))
beta = N / Nt if Nt > 0 else np.zeros_like(N)
y = np.zeros(x.shape[0])
for i in range(x.shape[0]):
xi = x[i, :]
# Logistic transformation
t = np.exp(xi) / (1 + np.exp(xi))
tb = np.sum(beta * t)
# Evaluate h function
z = np.sum(L * np.tile(np.exp(2 * np.pi * 1j * (t - tb)), (M, 1)), axis=1)
gld_result = pfqn_gld(z, Nt, alpha)
# Extract scalar G value from PfqnNcResult
gld_value = gld_result.G if hasattr(gld_result, 'G') else gld_result
# Jacobian of transformation
jacobian = np.prod(np.exp(xi) / (1 + np.exp(xi)) ** 2)
y[i] = np.real(gld_value * jacobian)
return y
[docs]
def infradius_hnorm(x: np.ndarray, L: np.ndarray, N: np.ndarray,
alpha: np.ndarray, logNormConstScale: float) -> np.ndarray:
"""
Normalized helper function for infinite radius computation.
Like infradius_h but with normalization for numerical stability.
Args:
x: Logistic transformation parameters
L: Service demand matrix (M x R)
N: Population vector (R,)
alpha: Load-dependent rate matrix
logNormConstScale: Logarithm of normalizing scale factor
Returns:
Normalized evaluated function value
References:
Original MATLAB: matlab/src/api/pfqn/infradius_hnorm.m
"""
y = infradius_h(x, L, N, alpha)
# Normalize
if logNormConstScale != 0:
y = y / np.exp(logNormConstScale)
return y
__all__ = [
'ljd_linearize',
'ljd_delinearize',
'ljcd_interpolate',
'infradius_h',
'infradius_hnorm',
]