| """DGP 1: Pure scale heterogeneity in ILR space. |
| |
| Y = ilr⁻¹(ilr(μ(X)) + σ(μ(X))·ε), ε ~ N(0, I) |
| σ(u) = σ_min + c·(1 - H(u)/log K) |
| f̂(X) = μ(X) |
| """ |
| import numpy as np |
| from .base import BaseDGP, DGPSample |
| from ..utils.simplex import ilr, ilr_inv, entropy, aitchison_dist |
|
|
|
|
| def _softmax(z: np.ndarray) -> np.ndarray: |
| z = z - z.max(axis=-1, keepdims=True) |
| e = np.exp(z) |
| return e / e.sum(axis=-1, keepdims=True) |
|
|
|
|
| class PureScaleDGP(BaseDGP): |
| """Pure scale heterogeneity: residual distribution is N(0, σ²(u)I) in ILR space. |
| |
| Args: |
| K: number of simplex components |
| sigma_min: minimum scale |
| c: scale range (σ_max = σ_min + c) |
| d_x: dimension of covariate X |
| """ |
| def __init__(self, K: int = 3, sigma_min: float = 0.1, c: float = 0.5, |
| d_x: int = 2): |
| self.K = K |
| self.sigma_min = sigma_min |
| self.c = c |
| self.d_x = d_x |
| |
| self._W = None |
|
|
| def _init_weights(self, rng: np.random.Generator): |
| """Initialize fixed linear map for μ(X).""" |
| if self._W is None: |
| self._W = rng.standard_normal((self.d_x, self.K)) * 0.8 |
|
|
| def _mu(self, X: np.ndarray) -> np.ndarray: |
| """Map X -> simplex via softmax of linear functions.""" |
| logits = X @ self._W |
| return _softmax(logits) |
|
|
| def _sigma(self, u: np.ndarray) -> np.ndarray: |
| """Scale function: low entropy -> high scale.""" |
| H = entropy(u) |
| return self.sigma_min + self.c * (1.0 - H / np.log(self.K)) |
|
|
| def sample(self, n: int, rng: np.random.Generator) -> DGPSample: |
| self._init_weights(rng) |
|
|
| X = rng.standard_normal((n, self.d_x)) |
| mu = self._mu(X) |
| sigma = self._sigma(mu) |
|
|
| |
| Z_mu = ilr(mu) |
| eps = rng.standard_normal((n, self.K - 1)) |
| Z_y = Z_mu + sigma[:, None] * eps |
| Y = ilr_inv(Z_y, K=self.K) |
|
|
| |
| U = mu |
| R = aitchison_dist(Y, U) |
|
|
| return DGPSample(X=X, Y=Y, U=U, R=R, sigma_true=sigma) |
|
|