mcarma 0.1.2__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- mcarma/__init__.py +54 -0
- mcarma/bootstrap.py +178 -0
- mcarma/fit.py +2025 -0
- mcarma/gic.py +150 -0
- mcarma/inference.py +353 -0
- mcarma/jax_loglik.py +494 -0
- mcarma/jax_transitions.py +151 -0
- mcarma/kalman.py +448 -0
- mcarma/model_utils.py +468 -0
- mcarma/observation.py +73 -0
- mcarma/optimizer_utils.py +284 -0
- mcarma/parameters.py +277 -0
- mcarma/pipeline.py +84 -0
- mcarma/preprocess.py +27 -0
- mcarma/priors.py +53 -0
- mcarma/reporting.py +108 -0
- mcarma/rts.py +239 -0
- mcarma/simulate.py +227 -0
- mcarma/smoother.py +345 -0
- mcarma/stage2.py +340 -0
- mcarma/statespace.py +260 -0
- mcarma-0.1.2.dist-info/METADATA +188 -0
- mcarma-0.1.2.dist-info/RECORD +26 -0
- mcarma-0.1.2.dist-info/WHEEL +5 -0
- mcarma-0.1.2.dist-info/licenses/LICENSE +201 -0
- mcarma-0.1.2.dist-info/top_level.txt +1 -0
mcarma/__init__.py
ADDED
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"""mcarma — multivariate CARMA modeling of irregularly sampled light curves.
|
|
2
|
+
|
|
3
|
+
Penalized maximum-likelihood fitting of multiband continuous-time ARMA models
|
|
4
|
+
to astronomical light curves, plus the closed-form multivariate PSD
|
|
5
|
+
and a data simulator. Builds on single-band CARMA (Kelly et al. 2014) and the
|
|
6
|
+
multiband damped random walk (Hu et al. 2020).
|
|
7
|
+
|
|
8
|
+
Quick start
|
|
9
|
+
-----------
|
|
10
|
+
from mcarma import ObservationData, fit, compute_aicc, mcarma_psd
|
|
11
|
+
|
|
12
|
+
data = ObservationData(t, y, band, R, d=5) # scalar (t, band) observations
|
|
13
|
+
res = fit(data, p=2, q=1) # MLE + optional penalties
|
|
14
|
+
aicc = compute_aicc(res["loglik_pure"], k, data.n)
|
|
15
|
+
psd = mcarma_psd(res["F"], res["G"], res["H"], res["Sigma"], freqs)
|
|
16
|
+
|
|
17
|
+
See `examples/sdss_stripe82_demo.py` for a full worked example on real SDSS
|
|
18
|
+
Stripe 82 data.
|
|
19
|
+
|
|
20
|
+
Note on JAX: the analytic-gradient objective (`mcarma.jax_loglik`), the JAX
|
|
21
|
+
transition helpers (`mcarma.jax_transitions`), the RTS smoother
|
|
22
|
+
(`mcarma.smoother`, which uses them), and the smoother-based reconstruction
|
|
23
|
+
helpers (`mcarma.rts`) import JAX at module load. They are **not** imported here,
|
|
24
|
+
so `import mcarma` stays JAX-free and safe on login nodes; import those
|
|
25
|
+
submodules explicitly on a compute node when you need them.
|
|
26
|
+
"""
|
|
27
|
+
import logging as _logging
|
|
28
|
+
|
|
29
|
+
# The library logs progress through the standard logging module and attaches no
|
|
30
|
+
# handler of its own, so an application that has not configured logging sees
|
|
31
|
+
# nothing. The research drivers in fits/ call logging.basicConfig, which is what
|
|
32
|
+
# puts these lines back in the SLURM logs.
|
|
33
|
+
_logging.getLogger(__name__).addHandler(_logging.NullHandler())
|
|
34
|
+
|
|
35
|
+
from .observation import ObservationData
|
|
36
|
+
from .fit import fit
|
|
37
|
+
from .model_utils import compute_aicc
|
|
38
|
+
from .simulate import simulate, mcarma_psd
|
|
39
|
+
from .reporting import sigma_to_var_corr
|
|
40
|
+
|
|
41
|
+
# The one place the version is written. pyproject.toml derives the
|
|
42
|
+
# distribution version from this attribute, so the two cannot drift; they
|
|
43
|
+
# already had, with this file saying 0.1 while everything else said 0.1.1.
|
|
44
|
+
__version__ = "0.1.2"
|
|
45
|
+
|
|
46
|
+
__all__ = [
|
|
47
|
+
"ObservationData",
|
|
48
|
+
"fit",
|
|
49
|
+
"compute_aicc",
|
|
50
|
+
"simulate",
|
|
51
|
+
"mcarma_psd",
|
|
52
|
+
"sigma_to_var_corr",
|
|
53
|
+
"__version__",
|
|
54
|
+
]
|
mcarma/bootstrap.py
ADDED
|
@@ -0,0 +1,178 @@
|
|
|
1
|
+
"""Per-object parametric bootstrap: SE for every parameter + finite-sample
|
|
2
|
+
bias-correction of the MA coefficient b.
|
|
3
|
+
|
|
4
|
+
See docs/METHODS.Rmd sec 8-9 (bias-correction b_bc = 2*b_hat - boot_mean; subsampling SE).
|
|
5
|
+
|
|
6
|
+
Why this exists. The MLE of a moving-average coefficient carries a known
|
|
7
|
+
O(1/n) downward finite-sample bias (Cox & Snell 1968 for the general MLE
|
|
8
|
+
expansion; Cordeiro & Klein 1994 for ARMA specifically; the classic MA(1)
|
|
9
|
+
"pile-up" toward the non-invertibility boundary). The simulation study measured
|
|
10
|
+
it at ~22% (median -0.245 in log b) and a lambda-probe confirmed it is
|
|
11
|
+
estimator-intrinsic, NOT our prior (it persists, and grows, as the MA band prior
|
|
12
|
+
lambda -> 0). The SEs are well-calibrated (bootstrap SE ~ Hessian SE); only the
|
|
13
|
+
point estimate is offset.
|
|
14
|
+
|
|
15
|
+
Crucially the correction is estimated PER OBJECT, not applied as a blanket shift
|
|
16
|
+
from the study's (narrow) parameter region: for each object we simulate from its
|
|
17
|
+
OWN fit on its OWN cadence, refit with the same per-band warm start, and take the
|
|
18
|
+
empirical mean/SD over refits. bias-corrected theta = 2*theta_fit - boot_mean.
|
|
19
|
+
|
|
20
|
+
Mirrors the validated real-data bootstrap in fits/process_quasars/validate_object.py
|
|
21
|
+
(object 1035792). Depends only on the mcarma package (warm start, simulate, fit,
|
|
22
|
+
per-band centering), so it lives here in the core library.
|
|
23
|
+
"""
|
|
24
|
+
import logging
|
|
25
|
+
import warnings
|
|
26
|
+
import numpy as np
|
|
27
|
+
|
|
28
|
+
from mcarma.preprocess import center_bands
|
|
29
|
+
from mcarma.model_utils import build_perband_warm_theta
|
|
30
|
+
from mcarma.observation import ObservationData
|
|
31
|
+
from mcarma.simulate import simulate
|
|
32
|
+
from mcarma.fit import fit
|
|
33
|
+
|
|
34
|
+
_log = logging.getLogger(__name__)
|
|
35
|
+
|
|
36
|
+
|
|
37
|
+
def parametric_bootstrap_se(theta_fit, data, p, q, prior=None, n_boot=24,
|
|
38
|
+
n_restarts_perband=4, maxiter=300,
|
|
39
|
+
use_jax_grad=False, seed0=7000, verbose=False):
|
|
40
|
+
"""Parametric bootstrap of the fit at (p, q) on `data`'s exact design.
|
|
41
|
+
|
|
42
|
+
Simulate `n_boot` CARMA(p,q) light curves from `theta_fit` on the same
|
|
43
|
+
times / bands / per-obs noise as `data`, refit each at (p,q) with a per-band
|
|
44
|
+
warm start (no slopes: the simulated process is trend-free), and summarise
|
|
45
|
+
the refit theta vectors.
|
|
46
|
+
|
|
47
|
+
Returns dict(theta_se, boot_mean, theta_bc, boot_n, boot_thetas) or None if
|
|
48
|
+
fewer than 2 reps succeeded. theta_bc = 2*theta_fit - boot_mean is the
|
|
49
|
+
bias-corrected estimate; theta_se is the per-parameter empirical SD (the
|
|
50
|
+
honest, Hessian-free SE). For q >= 1 the MA coeff is theta[p*d].
|
|
51
|
+
"""
|
|
52
|
+
theta_fit = np.asarray(theta_fit, dtype=float)
|
|
53
|
+
d = data.d
|
|
54
|
+
t = data.t_obs
|
|
55
|
+
band = data.band
|
|
56
|
+
R = np.array([data.R_list[k][0, 0] for k in range(data.n)])
|
|
57
|
+
tmpl = ObservationData(t, np.zeros(data.n), band, R, d=d)
|
|
58
|
+
prior = prior or {}
|
|
59
|
+
|
|
60
|
+
thetas = []
|
|
61
|
+
for m in range(n_boot):
|
|
62
|
+
rng = np.random.default_rng(seed0 + m)
|
|
63
|
+
try:
|
|
64
|
+
sim = simulate(theta_fit, tmpl, p=p, q=q,
|
|
65
|
+
initial_state="stationary",
|
|
66
|
+
seed=int(rng.integers(1 << 31)))
|
|
67
|
+
yb = center_bands(t, sim.data.y_obs, band)
|
|
68
|
+
db = ObservationData(t, yb, band, R, d=d)
|
|
69
|
+
warm = build_perband_warm_theta(
|
|
70
|
+
t, yb, band, R, d=d, p=p, q=q,
|
|
71
|
+
n_restarts=n_restarts_perband,
|
|
72
|
+
seed=int(rng.integers(1 << 31)))
|
|
73
|
+
with warnings.catch_warnings():
|
|
74
|
+
warnings.simplefilter("ignore")
|
|
75
|
+
r = fit(db, p, q, n_restarts=1, warm_theta=warm,
|
|
76
|
+
maxiter=maxiter, use_jax_grad=use_jax_grad, **prior)
|
|
77
|
+
th = np.asarray(r["theta"], dtype=float)
|
|
78
|
+
if th.shape != theta_fit.shape:
|
|
79
|
+
continue
|
|
80
|
+
thetas.append(th)
|
|
81
|
+
if verbose:
|
|
82
|
+
bstr = f" b11_log={th[p * d]:+.3f}" if q >= 1 else ""
|
|
83
|
+
_log.info(f" [boot {m:2d}]{bstr}")
|
|
84
|
+
except Exception as exc:
|
|
85
|
+
_log.warning(f" [boot {m:2d}] failed: {exc}")
|
|
86
|
+
continue
|
|
87
|
+
|
|
88
|
+
if len(thetas) < 2:
|
|
89
|
+
return None
|
|
90
|
+
thetas = np.vstack(thetas)
|
|
91
|
+
theta_se = np.std(thetas, axis=0, ddof=1)
|
|
92
|
+
boot_mean = np.mean(thetas, axis=0)
|
|
93
|
+
theta_bc = 2.0 * theta_fit - boot_mean
|
|
94
|
+
return dict(theta_se=theta_se, boot_mean=boot_mean, theta_bc=theta_bc,
|
|
95
|
+
boot_n=int(thetas.shape[0]), boot_thetas=thetas)
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
def subsampling_se(theta_fit, data, p, q, prior=None, m=None, n_sub=40,
|
|
99
|
+
n_restarts_perband=4, maxiter=300, use_jax_grad=False,
|
|
100
|
+
seed0=9000, verbose=False):
|
|
101
|
+
"""m-out-of-n subsampling SE (Politis-Romano-Wolf), WITHOUT replacement.
|
|
102
|
+
|
|
103
|
+
Independent of the parametric bootstrap above: instead of regenerating data
|
|
104
|
+
from the fitted model, we refit on genuine subsamples of the observed light
|
|
105
|
+
curve, so it does NOT assume the model is correctly specified.
|
|
106
|
+
|
|
107
|
+
Procedure. Draw ``n_sub`` subsamples of ``m = round(n**(2/3))`` epochs
|
|
108
|
+
without replacement, stratified by band (so every band keeps its share and
|
|
109
|
+
stays identifiable), refit each at (p, q) with the same per-band warm start
|
|
110
|
+
and prior, take the per-parameter SD of the subsample estimates, and rescale
|
|
111
|
+
to the full-sample SE by ``sqrt(m/n)``. The rescaling is the subsampling
|
|
112
|
+
identity: if ``sqrt(k)*(theta_k - theta)`` shares one limiting law, then
|
|
113
|
+
``SD(theta_m) ~ sqrt(V/m)`` and the full-sample SE is
|
|
114
|
+
``SD(theta_m)*sqrt(m/n) ~ sqrt(V/n)``. CARMA is continuous-time, so dropping
|
|
115
|
+
epochs just yields a sparser irregular series the Kalman filter handles
|
|
116
|
+
natively.
|
|
117
|
+
|
|
118
|
+
Returns dict(theta_se, m, n, sub_n, theta_sub_std, sub_thetas) or None if
|
|
119
|
+
fewer than 2 subsample fits succeeded. For q >= 1 the MA coeff is
|
|
120
|
+
theta[p*d].
|
|
121
|
+
"""
|
|
122
|
+
theta_fit = np.asarray(theta_fit, dtype=float)
|
|
123
|
+
d = data.d
|
|
124
|
+
t = np.asarray(data.t_obs, dtype=float)
|
|
125
|
+
band = np.asarray(data.band, dtype=int)
|
|
126
|
+
y = np.asarray(data.y_obs, dtype=float)
|
|
127
|
+
R = np.array([data.R_list[k][0, 0] for k in range(data.n)])
|
|
128
|
+
n = int(data.n)
|
|
129
|
+
prior = prior or {}
|
|
130
|
+
if m is None:
|
|
131
|
+
m = int(round(n ** (2.0 / 3.0)))
|
|
132
|
+
m = int(max(d + 2, min(m, n - 1)))
|
|
133
|
+
|
|
134
|
+
band_idx = [np.where(band == b)[0] for b in range(d)]
|
|
135
|
+
thetas = []
|
|
136
|
+
for s in range(n_sub):
|
|
137
|
+
rng = np.random.default_rng(seed0 + s)
|
|
138
|
+
parts = []
|
|
139
|
+
for b in range(d):
|
|
140
|
+
bi = band_idx[b]
|
|
141
|
+
if len(bi) == 0:
|
|
142
|
+
continue
|
|
143
|
+
mb = int(round(len(bi) * m / n))
|
|
144
|
+
mb = min(max(mb, 1), len(bi))
|
|
145
|
+
parts.append(rng.choice(bi, size=mb, replace=False))
|
|
146
|
+
if not parts:
|
|
147
|
+
continue
|
|
148
|
+
idx = np.sort(np.concatenate(parts))
|
|
149
|
+
try:
|
|
150
|
+
ys = center_bands(t[idx], y[idx], band[idx])
|
|
151
|
+
ds = ObservationData(t[idx], ys, band[idx], R[idx], d=d)
|
|
152
|
+
warm = build_perband_warm_theta(
|
|
153
|
+
t[idx], ys, band[idx], R[idx], d=d, p=p, q=q,
|
|
154
|
+
n_restarts=n_restarts_perband,
|
|
155
|
+
seed=int(rng.integers(1 << 31)))
|
|
156
|
+
with warnings.catch_warnings():
|
|
157
|
+
warnings.simplefilter("ignore")
|
|
158
|
+
r = fit(ds, p, q, n_restarts=1, warm_theta=warm,
|
|
159
|
+
maxiter=maxiter, use_jax_grad=use_jax_grad, **prior)
|
|
160
|
+
th = np.asarray(r["theta"], dtype=float)
|
|
161
|
+
if th.shape != theta_fit.shape:
|
|
162
|
+
continue
|
|
163
|
+
thetas.append(th)
|
|
164
|
+
if verbose:
|
|
165
|
+
bstr = f" b11_log={th[p * d]:+.3f}" if q >= 1 else ""
|
|
166
|
+
_log.info(f" [sub {s:2d}] m={idx.size}{bstr}")
|
|
167
|
+
except Exception as exc:
|
|
168
|
+
_log.warning(f" [sub {s:2d}] failed: {exc}")
|
|
169
|
+
continue
|
|
170
|
+
|
|
171
|
+
if len(thetas) < 2:
|
|
172
|
+
return None
|
|
173
|
+
thetas = np.vstack(thetas)
|
|
174
|
+
theta_sub_std = np.std(thetas, axis=0, ddof=1)
|
|
175
|
+
theta_se = theta_sub_std * np.sqrt(m / n)
|
|
176
|
+
return dict(theta_se=theta_se, m=int(m), n=int(n),
|
|
177
|
+
sub_n=int(thetas.shape[0]), theta_sub_std=theta_sub_std,
|
|
178
|
+
sub_thetas=thetas)
|