|
| 1 | +"""The log-space generator_taylor_coefficients override must agree with the |
| 2 | +linear override (and with jet where jet is still accurate). |
| 3 | +
|
| 4 | +Regression test for ``Copula.log_generator_taylor_coefficients`` and the Bell |
| 5 | +dispatch that prefers it. Uses AMH, whose frailty series is computed in the log |
| 6 | +domain anyway, so the linear override is just the log override exponentiated. |
| 7 | +""" |
| 8 | +import jax |
| 9 | +import jax.numpy as jnp |
| 10 | +import pytest |
| 11 | + |
| 12 | +jax.config.update("jax_enable_x64", True) |
| 13 | + |
| 14 | +from acopula import compile_model, copula, marginal |
| 15 | + |
| 16 | + |
| 17 | +class _Uniform: |
| 18 | + def quantile(self, u): return u |
| 19 | + def cdf(self, x): return x |
| 20 | + def log_prob(self, x): return 0.0 |
| 21 | + |
| 22 | + |
| 23 | +def _amh_sign_logabs(theta, t, k_max, n_terms=300): |
| 24 | + ks = jnp.arange(k_max + 1) |
| 25 | + xs = jnp.arange(1, n_terms + 1, dtype=jnp.float64) |
| 26 | + log_w = jnp.log(1.0 - theta) + (xs - 1.0) * jnp.log(theta) - t * xs |
| 27 | + log_fact_k = jax.scipy.special.gammaln(ks + 1) |
| 28 | + log_term = (ks[:, None] * jnp.log(xs)[None, :] + log_w[None, :] |
| 29 | + - log_fact_k[:, None]) |
| 30 | + log_abs_c = jax.scipy.special.logsumexp(log_term, axis=1) |
| 31 | + signs = jnp.where(ks % 2 == 0, 1.0, -1.0) |
| 32 | + return signs, log_abs_c |
| 33 | + |
| 34 | + |
| 35 | +@copula |
| 36 | +class _AMHLinear: |
| 37 | + theta: float |
| 38 | + def generator(self, t): |
| 39 | + return (1.0 - self.theta) / (jnp.exp(t) - self.theta) |
| 40 | + def generator_taylor_coefficients(self, t, k_max, n_terms=300): |
| 41 | + signs, log_abs = _amh_sign_logabs(self.theta, t, k_max, n_terms) |
| 42 | + return signs * jnp.exp(log_abs) |
| 43 | + |
| 44 | + |
| 45 | +@copula |
| 46 | +class _AMHLog: |
| 47 | + theta: float |
| 48 | + def generator(self, t): |
| 49 | + return (1.0 - self.theta) / (jnp.exp(t) - self.theta) |
| 50 | + def log_generator_taylor_coefficients(self, t, k_max, n_terms=300): |
| 51 | + return _amh_sign_logabs(self.theta, t, k_max, n_terms) |
| 52 | + |
| 53 | + |
| 54 | +def _ll(cls, d, theta_val): |
| 55 | + def model(p, u): |
| 56 | + return cls(p[0])([marginal(_Uniform(), obs=u[i]) for i in range(d)]) |
| 57 | + cm = compile_model(model, template=jnp.array([theta_val]), method="bell") |
| 58 | + u = jnp.arange(1, d + 1, dtype=jnp.float64) / (d + 1) |
| 59 | + return float(cm.eval(u, jnp.array([theta_val]))) |
| 60 | + |
| 61 | + |
| 62 | +@pytest.mark.parametrize("d", [8, 12]) |
| 63 | +def test_log_override_matches_linear(d): |
| 64 | + lin = _ll(_AMHLinear, d, 0.5) |
| 65 | + log = _ll(_AMHLog, d, 0.5) |
| 66 | + assert abs(lin - log) < 1e-9, f"d={d}: linear={lin}, log={log}" |
0 commit comments