File size: 2,127 Bytes
fc329a3
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
"""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
        # Fixed linear map X -> logits, generated once
        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)

        # Generate Y in ILR space
        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)

        # Predictor = oracle mean
        U = mu
        R = aitchison_dist(Y, U)

        return DGPSample(X=X, Y=Y, U=U, R=R, sigma_true=sigma)