ExoIris 0.19.2__py3-none-any.whl → 0.21.0__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.
- exoiris/exoiris.py +125 -13
- exoiris/loglikelihood.py +144 -0
- exoiris/spotmodel.py +176 -0
- exoiris/tsdata.py +3 -3
- exoiris/tslpf.py +59 -5
- exoiris/util.py +46 -3
- exoiris-0.21.0.dist-info/METADATA +118 -0
- exoiris-0.21.0.dist-info/RECORD +17 -0
- exoiris-0.19.2.dist-info/METADATA +0 -81
- exoiris-0.19.2.dist-info/RECORD +0 -15
- {exoiris-0.19.2.dist-info → exoiris-0.21.0.dist-info}/WHEEL +0 -0
- {exoiris-0.19.2.dist-info → exoiris-0.21.0.dist-info}/licenses/LICENSE +0 -0
- {exoiris-0.19.2.dist-info → exoiris-0.21.0.dist-info}/top_level.txt +0 -0
exoiris/exoiris.py
CHANGED
|
@@ -32,7 +32,8 @@ from emcee import EnsembleSampler
|
|
|
32
32
|
from matplotlib.pyplot import subplots, setp, figure, Figure, Axes
|
|
33
33
|
from numpy import (any, where, sqrt, clip, percentile, median, squeeze, floor, ndarray, isfinite,
|
|
34
34
|
array, inf, arange, argsort, concatenate, full, nan, r_, nanpercentile, log10,
|
|
35
|
-
ceil, unique)
|
|
35
|
+
ceil, unique, zeros)
|
|
36
|
+
from numpy.typing import ArrayLike
|
|
36
37
|
from numpy.random import normal
|
|
37
38
|
from pytransit import UniformPrior, NormalPrior
|
|
38
39
|
from pytransit.param import ParameterSet
|
|
@@ -44,6 +45,7 @@ from .ldtkld import LDTkLD
|
|
|
44
45
|
from .tsdata import TSData, TSDataGroup
|
|
45
46
|
from .tslpf import TSLPF
|
|
46
47
|
from .wlpf import WhiteLPF
|
|
48
|
+
from .loglikelihood import LogLikelihood
|
|
47
49
|
|
|
48
50
|
|
|
49
51
|
def load_model(fname: Path | str, name: str | None = None):
|
|
@@ -71,28 +73,38 @@ def load_model(fname: Path | str, name: str | None = None):
|
|
|
71
73
|
"""
|
|
72
74
|
with pf.open(fname) as hdul:
|
|
73
75
|
data = TSDataGroup.import_fits(hdul)
|
|
76
|
+
hdr = hdul[0].header
|
|
74
77
|
|
|
75
|
-
|
|
76
|
-
|
|
78
|
+
# Read the limb darkening model.
|
|
79
|
+
# ==============================
|
|
80
|
+
if hdr['LDMODEL'] == 'ldtk':
|
|
81
|
+
filters, teff, logg, metal, dataset = pickle.loads(codecs.decode(json.loads(hdr['LDTKLD']).encode(), "base64"))
|
|
77
82
|
ldm = LDTkLD(filters, teff, logg, metal, dataset=dataset)
|
|
78
83
|
else:
|
|
79
|
-
ldm =
|
|
84
|
+
ldm = hdr['LDMODEL']
|
|
80
85
|
|
|
86
|
+
# Read the interpolation model.
|
|
87
|
+
# =============================
|
|
81
88
|
try:
|
|
82
|
-
ip =
|
|
89
|
+
ip = hdr['INTERP']
|
|
83
90
|
except KeyError:
|
|
84
91
|
ip = 'bspline'
|
|
85
92
|
|
|
93
|
+
# Read the noise model.
|
|
94
|
+
# =====================
|
|
86
95
|
try:
|
|
87
|
-
noise_model =
|
|
96
|
+
noise_model = hdr['NOISE']
|
|
88
97
|
except KeyError:
|
|
89
98
|
noise_model = "white"
|
|
90
99
|
|
|
91
|
-
|
|
100
|
+
# Setup the analysis.
|
|
101
|
+
# ===================
|
|
102
|
+
a = ExoIris(name or hdr['NAME'], ldmodel=ldm, data=data, noise_model=noise_model, interpolation=ip)
|
|
92
103
|
a.set_radius_ratio_knots(hdul['K_KNOTS'].data.astype('d'))
|
|
93
104
|
a.set_limb_darkening_knots(hdul['LD_KNOTS'].data.astype('d'))
|
|
94
105
|
|
|
95
106
|
# Read the white light curve models if they exist.
|
|
107
|
+
# ================================================
|
|
96
108
|
try:
|
|
97
109
|
tb = Table.read(hdul['WHITE_DATA'])
|
|
98
110
|
white_ids = tb['id'].data
|
|
@@ -101,18 +113,28 @@ def load_model(fname: Path | str, name: str | None = None):
|
|
|
101
113
|
a._white_fluxes = [tb['flux_obs'].data[white_ids == i] for i in uids]
|
|
102
114
|
a._white_errors = [tb['flux_obs_err'].data[white_ids == i] for i in uids]
|
|
103
115
|
a._white_models = [tb['flux_mod'].data[white_ids == i] for i in uids]
|
|
104
|
-
|
|
105
116
|
except KeyError:
|
|
106
117
|
pass
|
|
107
118
|
|
|
119
|
+
# Read the ephemeris if it exists.
|
|
120
|
+
# ================================
|
|
108
121
|
try:
|
|
109
|
-
a.period =
|
|
110
|
-
a.zero_epoch =
|
|
111
|
-
a.transit_duration =
|
|
122
|
+
a.period = hdr['P']
|
|
123
|
+
a.zero_epoch = hdr['T0']
|
|
124
|
+
a.transit_duration = hdr['T14']
|
|
112
125
|
[d.mask_transit(a.zero_epoch, a.period, a.transit_duration) for d in a.data]
|
|
113
|
-
except KeyError:
|
|
126
|
+
except (KeyError, ValueError):
|
|
114
127
|
pass
|
|
115
128
|
|
|
129
|
+
# Read the spots if they exist.
|
|
130
|
+
# =============================
|
|
131
|
+
if 'SPOTS' in hdr and hdr['SPOTS'] is True:
|
|
132
|
+
a.initialize_spots(hdr["SP_TSTAR"], hdr["SP_REFWL"], hdr["SP_TLSE"])
|
|
133
|
+
for i in range(hdr['NSPOTS']):
|
|
134
|
+
a.add_spot(hdr[f'SP{i+1:02d}_EG'])
|
|
135
|
+
|
|
136
|
+
# Read the priors.
|
|
137
|
+
# ================
|
|
116
138
|
priors = pickle.loads(codecs.decode(json.loads(hdul['PRIORS'].header['PRIORS']).encode(), "base64"))
|
|
117
139
|
a._tsa.ps = ParameterSet([pickle.loads(p) for p in priors])
|
|
118
140
|
a._tsa.ps.freeze()
|
|
@@ -132,7 +154,7 @@ class ExoIris:
|
|
|
132
154
|
|
|
133
155
|
def __init__(self, name: str, ldmodel, data: TSDataGroup | TSData, nk: int = 50, nldc: int = 10, nthreads: int = 1,
|
|
134
156
|
tmpars: dict | None = None, noise_model: Literal["white", "fixed_gp", "free_gp"] = 'white',
|
|
135
|
-
interpolation: Literal['bspline', 'pchip', 'makima'] = '
|
|
157
|
+
interpolation: Literal['bspline', 'pchip', 'makima', 'nearest', 'linear'] = 'makima'):
|
|
136
158
|
"""
|
|
137
159
|
Parameters
|
|
138
160
|
----------
|
|
@@ -343,6 +365,36 @@ class ExoIris:
|
|
|
343
365
|
"""
|
|
344
366
|
self._tsa.set_gp_kernel(kernel)
|
|
345
367
|
|
|
368
|
+
def initialize_spots(self, tstar: float, wlref: float, include_tlse: bool = True):
|
|
369
|
+
"""Initialize star spot model using given stellar and wavelength reference values.
|
|
370
|
+
|
|
371
|
+
Parameters
|
|
372
|
+
----------
|
|
373
|
+
tstar
|
|
374
|
+
Effective stellar temperature [K].
|
|
375
|
+
wlref
|
|
376
|
+
Reference wavelength where spot amplitude matches the amplitude parameter.
|
|
377
|
+
"""
|
|
378
|
+
self._tsa.initialize_spots(tstar, wlref, include_tlse)
|
|
379
|
+
|
|
380
|
+
def add_spot(self, epoch_group: int) -> None:
|
|
381
|
+
"""Add a new star spot and associate it with an epoch group.
|
|
382
|
+
|
|
383
|
+
Parameters
|
|
384
|
+
----------
|
|
385
|
+
epoch_group
|
|
386
|
+
Identifier for the epoch group to which the spot will be added.
|
|
387
|
+
"""
|
|
388
|
+
self._tsa.add_spot(epoch_group)
|
|
389
|
+
|
|
390
|
+
@property
|
|
391
|
+
def nspots(self) -> int:
|
|
392
|
+
"""Number of star spots."""
|
|
393
|
+
if self._tsa.spot_model is None:
|
|
394
|
+
return 0
|
|
395
|
+
else:
|
|
396
|
+
return self._tsa.spot_model.nspots
|
|
397
|
+
|
|
346
398
|
@property
|
|
347
399
|
def name(self) -> str:
|
|
348
400
|
"""Analysis name."""
|
|
@@ -1078,6 +1130,22 @@ class ExoIris:
|
|
|
1078
1130
|
median(ar, 0)[ix], ar.std(0)[ix]],
|
|
1079
1131
|
names = ['wavelength', 'radius_ratio', 'radius_ratio_e', 'area_ratio', 'area_ratio_e'])
|
|
1080
1132
|
|
|
1133
|
+
def radius_ratio_spectrum(self, wavelengths: ArrayLike, knot_samples: ArrayLike | None = None) -> ndarray:
|
|
1134
|
+
if knot_samples is None:
|
|
1135
|
+
knot_samples = self.posterior_samples.iloc[:, self._tsa._sl_rratios].values
|
|
1136
|
+
k_posteriors = zeros((knot_samples.shape[0], wavelengths.size))
|
|
1137
|
+
for i, ks in enumerate(knot_samples):
|
|
1138
|
+
k_posteriors[i, :] = self._tsa._ip(wavelengths, self._tsa.k_knots, ks)
|
|
1139
|
+
return k_posteriors
|
|
1140
|
+
|
|
1141
|
+
def area_ratio_spectrum(self, wavelengths: ArrayLike, knot_samples: ArrayLike | None = None) -> ndarray:
|
|
1142
|
+
if knot_samples is None:
|
|
1143
|
+
knot_samples = self.posterior_samples.iloc[:, self._tsa._sl_rratios].values
|
|
1144
|
+
d_posteriors = zeros((knot_samples.shape[0], wavelengths.size))
|
|
1145
|
+
for i, ks in enumerate(knot_samples):
|
|
1146
|
+
d_posteriors[i, :] = self._tsa._ip(wavelengths, self._tsa.k_knots, ks) ** 2
|
|
1147
|
+
return d_posteriors
|
|
1148
|
+
|
|
1081
1149
|
def save(self, overwrite: bool = False) -> None:
|
|
1082
1150
|
"""Save the ExoIris analysis to a FITS file.
|
|
1083
1151
|
|
|
@@ -1095,10 +1163,14 @@ class ExoIris:
|
|
|
1095
1163
|
pri.header['interp'] = self._tsa.interpolation
|
|
1096
1164
|
pri.header['noise'] = self._tsa.noise_model
|
|
1097
1165
|
|
|
1166
|
+
# Priors
|
|
1167
|
+
# ======
|
|
1098
1168
|
pr = pf.ImageHDU(name='priors')
|
|
1099
1169
|
priors = [pickle.dumps(p) for p in self.ps]
|
|
1100
1170
|
pr.header['priors'] = json.dumps(codecs.encode(pickle.dumps(priors), "base64").decode())
|
|
1101
1171
|
|
|
1172
|
+
# Limb darkening
|
|
1173
|
+
# ==============
|
|
1102
1174
|
if isinstance(self._tsa.ldmodel, LDTkLD):
|
|
1103
1175
|
ldm = self._tsa.ldmodel
|
|
1104
1176
|
pri.header['ldmodel'] = 'ldtk'
|
|
@@ -1107,11 +1179,15 @@ class ExoIris:
|
|
|
1107
1179
|
else:
|
|
1108
1180
|
pri.header['ldmodel'] = self._tsa.ldmodel
|
|
1109
1181
|
|
|
1182
|
+
# Knots
|
|
1183
|
+
# =====
|
|
1110
1184
|
k_knots = pf.ImageHDU(self._tsa.k_knots, name='k_knots')
|
|
1111
1185
|
ld_knots = pf.ImageHDU(self._tsa.ld_knots, name='ld_knots')
|
|
1112
1186
|
hdul = pf.HDUList([pri, k_knots, ld_knots, pr])
|
|
1113
1187
|
hdul += self.data.export_fits()
|
|
1114
1188
|
|
|
1189
|
+
# White light curve analysis
|
|
1190
|
+
# ==========================
|
|
1115
1191
|
if self._wa is not None and self._wa._local_minimization is not None:
|
|
1116
1192
|
wa_data = pf.BinTableHDU(
|
|
1117
1193
|
Table(
|
|
@@ -1140,6 +1216,19 @@ class ExoIris:
|
|
|
1140
1216
|
wa_params = pf.BinTableHDU(Table(self._wa._local_minimization.x, names=names), name='white_params')
|
|
1141
1217
|
hdul.append(wa_params)
|
|
1142
1218
|
|
|
1219
|
+
# Spots
|
|
1220
|
+
# =====
|
|
1221
|
+
if self._tsa.spot_model is not None:
|
|
1222
|
+
pri.header['spots'] = True
|
|
1223
|
+
pri.header["sp_tstar"] = self._tsa.spot_model.tphot
|
|
1224
|
+
pri.header["sp_refwl"] = self._tsa.spot_model.wlref
|
|
1225
|
+
pri.header["sp_tlse"] = self._tsa.spot_model.include_tlse
|
|
1226
|
+
pri.header["nspots"] = self.nspots
|
|
1227
|
+
for i in range(self.nspots):
|
|
1228
|
+
pri.header[f"sp{i+1:02d}_eg"] = self._tsa.spot_model.spot_epoch_groups[i]
|
|
1229
|
+
|
|
1230
|
+
# Global optimization results
|
|
1231
|
+
# ===========================
|
|
1143
1232
|
if self._tsa.de is not None:
|
|
1144
1233
|
de = pf.BinTableHDU(Table(self._tsa._de_population, names=self.ps.names), name='DE')
|
|
1145
1234
|
de.header['npop'] = self._tsa.de.n_pop
|
|
@@ -1147,6 +1236,8 @@ class ExoIris:
|
|
|
1147
1236
|
de.header['imin'] = self._tsa.de.minimum_index
|
|
1148
1237
|
hdul.append(de)
|
|
1149
1238
|
|
|
1239
|
+
# MCMC results
|
|
1240
|
+
# ============
|
|
1150
1241
|
if self._tsa.sampler is not None:
|
|
1151
1242
|
mc = pf.BinTableHDU(Table(self._tsa.sampler.flatchain, names=self.ps.names), name='MCMC')
|
|
1152
1243
|
mc.header['npop'] = self._tsa.sampler.nwalkers
|
|
@@ -1155,6 +1246,27 @@ class ExoIris:
|
|
|
1155
1246
|
|
|
1156
1247
|
hdul.writeto(f"{self.name}.fits", overwrite=True)
|
|
1157
1248
|
|
|
1249
|
+
def create_loglikelihood_function(self, wavelengths: ArrayLike, kind: Literal['radius_ratio', 'depth'] = 'depth') -> LogLikelihood:
|
|
1250
|
+
"""Create a reduced-rank Gaussian log-likelihood function for retrieval.
|
|
1251
|
+
|
|
1252
|
+
Parameters
|
|
1253
|
+
----------
|
|
1254
|
+
wavelengths
|
|
1255
|
+
The wavelength grid used in the theoretical transmission spectra.
|
|
1256
|
+
|
|
1257
|
+
kind
|
|
1258
|
+
The transmission spectrum type. Can be either 'radius_ratio' or 'depth'.
|
|
1259
|
+
|
|
1260
|
+
Returns
|
|
1261
|
+
-------
|
|
1262
|
+
LogLikelihood
|
|
1263
|
+
An instance of LogLikelihood for analyzing the consistency of the model
|
|
1264
|
+
with the provided wavelengths and chosen log-likelihood kind.
|
|
1265
|
+
"""
|
|
1266
|
+
if self.mcmc_chains is None:
|
|
1267
|
+
raise ValueError("Cannot create log-likelihood function before running the MCMC sampler.")
|
|
1268
|
+
return LogLikelihood(self, wavelengths, kind)
|
|
1269
|
+
|
|
1158
1270
|
def create_initial_population(self, n: int, source: str, add_noise: bool = True) -> ndarray:
|
|
1159
1271
|
"""Create an initial parameter vector population for the DE optimisation.
|
|
1160
1272
|
|
exoiris/loglikelihood.py
ADDED
|
@@ -0,0 +1,144 @@
|
|
|
1
|
+
# ExoIris: fast, flexible, and easy exoplanet transmission spectroscopy in Python.
|
|
2
|
+
# Copyright (C) 2025 Hannu Parviainen
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
from typing import Literal
|
|
17
|
+
|
|
18
|
+
from numpy import full, cov, sqrt, sum
|
|
19
|
+
from numpy.linalg import eigh
|
|
20
|
+
from numpy.typing import ArrayLike
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
class LogLikelihood:
|
|
24
|
+
def __init__(self, exoiris, wavelength: ArrayLike, kind: Literal['radius_ratio', 'depth'] = 'depth', eps: float = 1e-10):
|
|
25
|
+
"""Reduced-rank Gaussian log-likelihood.
|
|
26
|
+
|
|
27
|
+
This class constructs a statistically correct reduced-rank Gaussian
|
|
28
|
+
log-likelihood function for comparing a theoretical transmission spectrum to the
|
|
29
|
+
posterior distribution inferred by ExoIris. Because the posterior
|
|
30
|
+
samples of the transmission spectrum are generated from a spline with
|
|
31
|
+
far fewer independent parameters than the number of wavelength bins, the
|
|
32
|
+
empirical covariance matrix is rank-deficient or strongly ill-conditioned.
|
|
33
|
+
Direct inversion of the full covariance is therefore numerically unstable
|
|
34
|
+
and produces incorrect likelihoods.
|
|
35
|
+
|
|
36
|
+
Parameters
|
|
37
|
+
----------
|
|
38
|
+
exoiris
|
|
39
|
+
The ExoIris model object from which posterior samples of the knot
|
|
40
|
+
values and spline interpolation functions are obtained.
|
|
41
|
+
|
|
42
|
+
wavelength
|
|
43
|
+
Wavelength grid on which the radius ratio posterior samples and the
|
|
44
|
+
theoretical spectra will be evaluated.
|
|
45
|
+
|
|
46
|
+
kind
|
|
47
|
+
The type of the spectrum. Can be either ``radius_ratio`` for a radius ratio
|
|
48
|
+
spectrum, or ``depth`` for a transit depth spectrum.
|
|
49
|
+
|
|
50
|
+
eps
|
|
51
|
+
Relative tolerance factor used to determine which eigenvalues of
|
|
52
|
+
the covariance matrix are considered significant. Eigenvalues smaller
|
|
53
|
+
than ``eps * max_eigenvalue`` are discarded. Default is ``1e-10``.
|
|
54
|
+
|
|
55
|
+
Attributes
|
|
56
|
+
----------
|
|
57
|
+
k_posteriors : ndarray of shape (n_samples, n_wavelengths)
|
|
58
|
+
Radius-ratio posterior samples evaluated on the wavelength grid.
|
|
59
|
+
|
|
60
|
+
k_mean : ndarray of shape (n_wavelengths,)
|
|
61
|
+
Posterior mean radius-ratio spectrum.
|
|
62
|
+
|
|
63
|
+
k_cov : ndarray of shape (n_wavelengths, n_wavelengths)
|
|
64
|
+
Empirical covariance matrix of the posterior samples.
|
|
65
|
+
|
|
66
|
+
lambda_r : ndarray of shape (k,)
|
|
67
|
+
Significant eigenvalues of the covariance matrix (``k`` = reduced
|
|
68
|
+
dimensionality).
|
|
69
|
+
|
|
70
|
+
u_r : ndarray of shape (n_wavelengths, k)
|
|
71
|
+
Eigenvectors corresponding to the significant eigenvalues.
|
|
72
|
+
|
|
73
|
+
sqrt_inv_lambda_r : ndarray of shape (k,)
|
|
74
|
+
Factors used to whiten the reduced-rank representation.
|
|
75
|
+
|
|
76
|
+
y_data : ndarray of shape (k,)
|
|
77
|
+
Whitened reduced-rank representation of the posterior mean spectrum.
|
|
78
|
+
|
|
79
|
+
Notes
|
|
80
|
+
-----
|
|
81
|
+
The class implements the reduced-rank likelihood method:
|
|
82
|
+
|
|
83
|
+
1. Posterior samples of the spline knot values are evaluated on the
|
|
84
|
+
user-specified wavelength grid to produce a matrix of radius-ratio
|
|
85
|
+
samples, ``k_posteriors``.
|
|
86
|
+
|
|
87
|
+
2. The empirical mean spectrum and covariance matrix are computed over
|
|
88
|
+
these samples.
|
|
89
|
+
|
|
90
|
+
3. An eigendecomposition of the covariance matrix is performed. All
|
|
91
|
+
eigenvalues below ``eps * max(eigenvalue)`` are discarded, ensuring that
|
|
92
|
+
only statistically meaningful directions (i.e., those supported by the
|
|
93
|
+
spline parameterization and the data) are retained.
|
|
94
|
+
|
|
95
|
+
4. The retained eigenvectors form an orthonormal basis for the true
|
|
96
|
+
low-dimensional subspace in which the posterior distribution lives.
|
|
97
|
+
Projection onto this basis, followed by whitening with
|
|
98
|
+
``lambda_r**(-1/2)``, yields a representation where the posterior is a
|
|
99
|
+
standard multivariate normal with identity covariance.
|
|
100
|
+
|
|
101
|
+
5. The log-likelihood of a theoretical spectrum ``x`` is evaluated in this
|
|
102
|
+
reduced space as:
|
|
103
|
+
|
|
104
|
+
log L = -0.5 * sum_i (y_data[i] - y_model[i])^2
|
|
105
|
+
|
|
106
|
+
where ``y_data`` is the whitened reduced-space representation of the
|
|
107
|
+
posterior mean spectrum, and ``y_model`` is the whitened projection of
|
|
108
|
+
the model spectrum.
|
|
109
|
+
|
|
110
|
+
- This reduced-rank formulation is mathematically equivalent to computing
|
|
111
|
+
the Gaussian likelihood in knot space, and avoids the numerical
|
|
112
|
+
instabilities associated with inverting a nearly singular covariance
|
|
113
|
+
matrix in the oversampled wavelength space.
|
|
114
|
+
|
|
115
|
+
- If ``x`` is provided as a scalar, it is broadcast to a constant spectrum
|
|
116
|
+
over the wavelength grid. Otherwise, it must be an array of the same
|
|
117
|
+
wavelength length.
|
|
118
|
+
"""
|
|
119
|
+
self.model = m = exoiris
|
|
120
|
+
self.wavelength = wavelength
|
|
121
|
+
self.eps = eps
|
|
122
|
+
|
|
123
|
+
if kind == 'radius_ratio':
|
|
124
|
+
self.spectrum = m.radius_ratio_spectrum(wavelength)
|
|
125
|
+
elif kind == 'depth':
|
|
126
|
+
self.spectrum = m.area_ratio_spectrum(wavelength)
|
|
127
|
+
else:
|
|
128
|
+
raise ValueError('Unknown spectrum type: {}'.format(kind))
|
|
129
|
+
|
|
130
|
+
self.spmean = self.spectrum.mean(0)
|
|
131
|
+
self.spcov = cov(self.spectrum, rowvar=False)
|
|
132
|
+
|
|
133
|
+
evals, u = eigh(self.spcov)
|
|
134
|
+
tol = eps * evals.max()
|
|
135
|
+
keep = evals > tol
|
|
136
|
+
self.lambda_r, self.u_r = evals[keep], u[:, keep]
|
|
137
|
+
self.sqrt_inv_lambda_r = 1.0 / sqrt(self.lambda_r)
|
|
138
|
+
self.y_data = (self.u_r.T @ self.spmean) * self.sqrt_inv_lambda_r
|
|
139
|
+
|
|
140
|
+
def __call__(self, x):
|
|
141
|
+
if isinstance(x, float):
|
|
142
|
+
x = full(self.wavelength.size, x)
|
|
143
|
+
y_model = (self.u_r.T @ x) * self.sqrt_inv_lambda_r
|
|
144
|
+
return -0.5*sum((self.y_data - y_model)**2)
|
exoiris/spotmodel.py
ADDED
|
@@ -0,0 +1,176 @@
|
|
|
1
|
+
# ExoIris: fast, flexible, and easy exoplanet transmission spectroscopy in Python.
|
|
2
|
+
# Copyright (C) 2025 Hannu Parviainen
|
|
3
|
+
#
|
|
4
|
+
# This program is free software: you can redistribute it and/or modify
|
|
5
|
+
# it under the terms of the GNU General Public License as published by
|
|
6
|
+
# the Free Software Foundation, either version 3 of the License, or
|
|
7
|
+
# (at your option) any later version.
|
|
8
|
+
#
|
|
9
|
+
# This program is distributed in the hope that it will be useful,
|
|
10
|
+
# but WITHOUT ANY WARRANTY; without even the implied warranty of
|
|
11
|
+
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
|
12
|
+
# GNU General Public License for more details.
|
|
13
|
+
#
|
|
14
|
+
# You should have received a copy of the GNU General Public License
|
|
15
|
+
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
|
+
|
|
17
|
+
from copy import deepcopy
|
|
18
|
+
|
|
19
|
+
from numpy import (
|
|
20
|
+
exp,
|
|
21
|
+
fabs,
|
|
22
|
+
log,
|
|
23
|
+
inf,
|
|
24
|
+
array,
|
|
25
|
+
vstack,
|
|
26
|
+
atleast_2d,
|
|
27
|
+
nan,
|
|
28
|
+
unique,
|
|
29
|
+
linspace,
|
|
30
|
+
floor,
|
|
31
|
+
)
|
|
32
|
+
from scipy.interpolate import RegularGridInterpolator
|
|
33
|
+
from numba import njit
|
|
34
|
+
|
|
35
|
+
from pytransit.stars import create_bt_settl_interpolator, create_husser2013_interpolator
|
|
36
|
+
from pytransit.param import GParameter, UniformPrior as U
|
|
37
|
+
|
|
38
|
+
from exoiris.tsdata import TSData
|
|
39
|
+
from exoiris.util import bin2d
|
|
40
|
+
|
|
41
|
+
|
|
42
|
+
@njit
|
|
43
|
+
def spot_model(x, center, amplitude, fwhm, shape):
|
|
44
|
+
c = fwhm / 2*(2*log(2))**(1/shape)
|
|
45
|
+
return amplitude*exp(-(fabs(x-center) / c)**shape)
|
|
46
|
+
|
|
47
|
+
|
|
48
|
+
@njit
|
|
49
|
+
def interpolate_spectrum(teff, values, tgrid):
|
|
50
|
+
t0 = tgrid[0]
|
|
51
|
+
dt = tgrid[1] - tgrid[0]
|
|
52
|
+
k = (teff - t0) / dt
|
|
53
|
+
i = int(floor(k))
|
|
54
|
+
a = k - floor(k)
|
|
55
|
+
return (1.0-a)*values[i] + a*values[i+1]
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
@njit
|
|
59
|
+
def tlse(tphot, tspot, tfac, aspot, afac, spectra, tgrid):
|
|
60
|
+
fphot = interpolate_spectrum(tphot, spectra, tgrid)
|
|
61
|
+
fspot = interpolate_spectrum(tspot, spectra, tgrid)
|
|
62
|
+
ffac = interpolate_spectrum(tfac, spectra, tgrid)
|
|
63
|
+
return 1.0 / (1.0 - aspot*(1.0 - fspot/fphot) - afac*(1.0 - ffac/fphot))
|
|
64
|
+
|
|
65
|
+
|
|
66
|
+
def spot_contrast(tphot, tspot, spectra, spnorm):
|
|
67
|
+
fphot = interpolate_spectrum(tphot, spectra.values, spectra.grid[0])
|
|
68
|
+
fspot = interpolate_spectrum(tspot, spectra.values, spectra.grid[0])
|
|
69
|
+
norm = interpolate_spectrum(tspot, spnorm, spectra.grid[0])
|
|
70
|
+
return (fphot / fspot) / norm
|
|
71
|
+
|
|
72
|
+
|
|
73
|
+
def bin_stellar_spectrum_model(sp: RegularGridInterpolator, data: TSData):
|
|
74
|
+
lrange = array(data.bbox_wl) * 1e3
|
|
75
|
+
ml = (sp.grid[1] >= lrange[0]) & (sp.grid[1] <= lrange[1])
|
|
76
|
+
|
|
77
|
+
teff = sp.grid[0]
|
|
78
|
+
wave = sp.grid[1][ml]
|
|
79
|
+
flux = sp.values[:, ml]
|
|
80
|
+
|
|
81
|
+
wl_l_edges = wave - 0.5
|
|
82
|
+
wl_r_edges = wave + 0.5
|
|
83
|
+
|
|
84
|
+
bflux = bin2d(flux.T, flux.T, wl_l_edges*1e-3, wl_r_edges*1e-3, vstack([data._wl_l_edges, data._wl_r_edges]).T)[0].T
|
|
85
|
+
return RegularGridInterpolator((teff, data.wavelength), bflux, bounds_error=False, fill_value=nan)
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class SpotModel:
|
|
89
|
+
def __init__(self, tsa, tphot: float, wlref: float, include_tlse: bool = True):
|
|
90
|
+
self.tsa = tsa
|
|
91
|
+
self.tphot = tphot
|
|
92
|
+
self.wlref = wlref
|
|
93
|
+
self.include_tlse = include_tlse
|
|
94
|
+
|
|
95
|
+
ms = create_bt_settl_interpolator()
|
|
96
|
+
new_teff_grid = linspace(*ms.grid[0][[0, -1]], 117)
|
|
97
|
+
new_spectrum = array([ms((t, ms.grid[1])) for t in new_teff_grid])
|
|
98
|
+
self.full_spectrum = ms = RegularGridInterpolator((new_teff_grid, ms.grid[1]), new_spectrum, bounds_error=False, fill_value=nan)
|
|
99
|
+
|
|
100
|
+
wave = ms.grid[1] / 1e3
|
|
101
|
+
m = (wave > wlref - 0.025) & (wave < wlref + 0.025)
|
|
102
|
+
spot_norm = ms.values[:, m].mean(1)
|
|
103
|
+
self.spot_norm = interpolate_spectrum(tphot, spot_norm, ms.grid[0]) / spot_norm
|
|
104
|
+
|
|
105
|
+
self.binned_spectra = []
|
|
106
|
+
for d in tsa.data:
|
|
107
|
+
self.binned_spectra.append(bin_stellar_spectrum_model(self.full_spectrum, d))
|
|
108
|
+
|
|
109
|
+
if self.include_tlse:
|
|
110
|
+
self._init_tlse_parameters()
|
|
111
|
+
|
|
112
|
+
self.nspots = 0
|
|
113
|
+
self.spot_epoch_groups = []
|
|
114
|
+
self.spot_data_ids = []
|
|
115
|
+
self.spot_pv_slices = []
|
|
116
|
+
|
|
117
|
+
def use_tlse(self):
|
|
118
|
+
if self.include_tlse is False:
|
|
119
|
+
self.include_tlse = True
|
|
120
|
+
self._init_tlse_parameters()
|
|
121
|
+
|
|
122
|
+
def _init_tlse_parameters(self):
|
|
123
|
+
ps = [GParameter('tlse_tspot', 'Effective temperature of unocculted spots', 'K', U(1200, 7000), (1200, 7000))]
|
|
124
|
+
ps.append(GParameter('tlse_tfac', 'Effective temperature of unocculted faculae', 'K', U(1200, 7000), (1200, 7000)))
|
|
125
|
+
for e in unique(self.tsa.data.epoch_groups):
|
|
126
|
+
ps.append(GParameter(f"tlse_aspot_e{e:02d}", "Area fraction covered by unocculted spots", "", U(0,1), (0,1)))
|
|
127
|
+
ps.append(GParameter(f"tlse_afac_e{e:02d}", "Area fraction covered by unocculted faculae", "", U(0,1), (0,1)))
|
|
128
|
+
self.tsa.ps.thaw()
|
|
129
|
+
self.tsa.ps.add_global_block(f'tlse', ps)
|
|
130
|
+
setattr(self.tsa, "_start_tlse", self.tsa.ps.blocks[-1].start)
|
|
131
|
+
setattr(self.tsa, "_sl_tlse", self.tsa.ps.blocks[-1].slice)
|
|
132
|
+
self.tlse_pv_slice = self.tsa.ps.blocks[-1].slice
|
|
133
|
+
self.tsa.ps.freeze()
|
|
134
|
+
|
|
135
|
+
def add_spot(self, epoch_group: int):
|
|
136
|
+
self.nspots += 1
|
|
137
|
+
self.spot_epoch_groups.append(epoch_group)
|
|
138
|
+
self.spot_data_ids.append([i for i, d in enumerate(self.tsa.data) if d.epoch_group == epoch_group])
|
|
139
|
+
|
|
140
|
+
i = self.nspots
|
|
141
|
+
pspot = [GParameter(f"spc_{i:02d}", 'spot {i:02d} center', "d", U(0, 1), (0, inf)),
|
|
142
|
+
GParameter(f"spa_{i:02d}", 'spot {i:02d} amplitude', "", U(0, 1), (0, inf)),
|
|
143
|
+
GParameter(f"spw_{i:02d}", 'spot {i:02d} FWHM', "d", U(0, 1), (0, inf)),
|
|
144
|
+
GParameter(f"sps_{i:02d}", 'spot {i:02d} shape', "d", U(1, 5), (0, inf)),
|
|
145
|
+
GParameter(f"spt_{i:02d}", 'spot {i:02d} temperature', "K", U(3000, 6000), (0, inf))]
|
|
146
|
+
ps = self.tsa.ps
|
|
147
|
+
ps.thaw()
|
|
148
|
+
ps.add_global_block(f'spot_{i:02d}', pspot)
|
|
149
|
+
setattr(self.tsa, f"_start_spot_{i:02d}", ps.blocks[-1].start)
|
|
150
|
+
setattr(self.tsa, f"_sl_spot_{i:02d}", ps.blocks[-1].slice)
|
|
151
|
+
self.spot_pv_slices.append(ps.blocks[-1].slice)
|
|
152
|
+
ps.freeze()
|
|
153
|
+
|
|
154
|
+
def apply_tlse(self, pvp, models):
|
|
155
|
+
pvp = atleast_2d(pvp)[:, self.tlse_pv_slice]
|
|
156
|
+
npv = pvp.shape[0]
|
|
157
|
+
for d, m, sp in zip(self.tsa.data, models, self.binned_spectra):
|
|
158
|
+
for i in range(npv):
|
|
159
|
+
tspot = pvp[i, 0]
|
|
160
|
+
tfac = pvp[i, 1]
|
|
161
|
+
fspot = pvp[i, 2+d.epoch_group*2]
|
|
162
|
+
ffac = pvp[i, 3+d.epoch_group*2]
|
|
163
|
+
m[i, :, :] = (m[i, :, :] - 1.0) * tlse(self.tphot, tspot, tfac, fspot, ffac, sp.values, sp.grid[0])[:, None] + 1
|
|
164
|
+
|
|
165
|
+
def apply_spots(self, pvp, models):
|
|
166
|
+
pvp = atleast_2d(pvp)
|
|
167
|
+
npv = pvp.shape[0]
|
|
168
|
+
if models[0].shape[0] != npv:
|
|
169
|
+
raise ValueError('The _spot_models array has a wrong shape, it has not been initialized properly.')
|
|
170
|
+
|
|
171
|
+
for isp in range(self.nspots):
|
|
172
|
+
for ipv in range(npv):
|
|
173
|
+
center, amplitude, fwhm, shape, tspot = pvp[ipv, self.spot_pv_slices[isp]]
|
|
174
|
+
for idata in self.spot_data_ids[isp]:
|
|
175
|
+
models[idata][ipv, :, :] += (spot_model(self.tsa.data[idata].time, center, amplitude, fwhm, shape) *
|
|
176
|
+
spot_contrast(self.tphot, tspot, self.binned_spectra[idata], self.spot_norm)[:, None])
|
exoiris/tsdata.py
CHANGED
|
@@ -274,7 +274,7 @@ class TSData:
|
|
|
274
274
|
self.transit_mask = ones(self.fluxes.shape, bool)
|
|
275
275
|
self.transit_mask[:, elims[0]:elims[1]] = False
|
|
276
276
|
else:
|
|
277
|
-
raise ValueError("Transit masking requires either t0,
|
|
277
|
+
raise ValueError("Transit masking requires either t0, p, and t14, ephemeris, or transit limits in exposure indices.")
|
|
278
278
|
return self
|
|
279
279
|
|
|
280
280
|
def estimate_average_uncertainties(self):
|
|
@@ -568,8 +568,8 @@ class TSData:
|
|
|
568
568
|
axx2.set_xlabel('Exposure index')
|
|
569
569
|
axx2.xaxis.set_major_locator(LinearLocator())
|
|
570
570
|
axx2.xaxis.set_major_formatter('{x:.0f}')
|
|
571
|
-
|
|
572
|
-
|
|
571
|
+
ax.axx2 = axx2
|
|
572
|
+
ax.axy2 = axy2
|
|
573
573
|
return fig
|
|
574
574
|
|
|
575
575
|
def plot_white(self, ax: Axes | None = None, figsize: tuple[float, float] | None = None) -> Figure:
|
exoiris/tslpf.py
CHANGED
|
@@ -28,11 +28,19 @@ from pytransit.lpf.logposteriorfunction import LogPosteriorFunction
|
|
|
28
28
|
|
|
29
29
|
from pytransit.orbits import as_from_rhop, i_from_ba, fold, i_from_baew, d_from_pkaiews, epoch
|
|
30
30
|
from pytransit.param import ParameterSet, UniformPrior as UP, NormalPrior as NP, GParameter
|
|
31
|
-
from
|
|
31
|
+
from pytransit.stars import create_bt_settl_interpolator
|
|
32
|
+
from scipy.interpolate import (
|
|
33
|
+
pchip_interpolate,
|
|
34
|
+
splrep,
|
|
35
|
+
splev,
|
|
36
|
+
Akima1DInterpolator,
|
|
37
|
+
interp1d,
|
|
38
|
+
)
|
|
32
39
|
|
|
33
40
|
from .tsmodel import TransmissionSpectroscopyModel as TSModel
|
|
34
41
|
from .tsdata import TSDataGroup
|
|
35
42
|
from .ldtkld import LDTkLD
|
|
43
|
+
from .spotmodel import SpotModel
|
|
36
44
|
|
|
37
45
|
NM_WHITE = 0
|
|
38
46
|
NM_GP_FIXED = 1
|
|
@@ -81,10 +89,25 @@ def ip_makima(x, xk, yk):
|
|
|
81
89
|
return Akima1DInterpolator(xk, yk, method='makima', extrapolate=True)(x)
|
|
82
90
|
|
|
83
91
|
|
|
92
|
+
def ip_nearest(x, xk, yk):
|
|
93
|
+
return interp1d(xk, yk, kind='nearest', bounds_error=False, fill_value='extrapolate', assume_sorted=True)(x)
|
|
94
|
+
|
|
95
|
+
|
|
96
|
+
def ip_linear(x, xk, yk):
|
|
97
|
+
return interp1d(xk, yk, kind='linear', bounds_error=False, fill_value='extrapolate', assume_sorted=True)(x)
|
|
98
|
+
|
|
99
|
+
|
|
84
100
|
def add_knots(x_new, x_old):
|
|
85
101
|
return sort(concatenate([x_new, x_old]))
|
|
86
102
|
|
|
87
103
|
|
|
104
|
+
interpolator_choices = ("bspline", "pchip", "makima", "nearest", "linear")
|
|
105
|
+
|
|
106
|
+
|
|
107
|
+
interpolators = {'bspline': ip_bspline, 'pchip': ip_pchip, 'makima': ip_makima,
|
|
108
|
+
'nearest': ip_nearest, 'linear': ip_linear}
|
|
109
|
+
|
|
110
|
+
|
|
88
111
|
def clean_knots(knots, min_distance, lmin=0, lmax=inf):
|
|
89
112
|
"""Clean the knot table by replacing groups of adjacent knots with a single knot at the group mean.
|
|
90
113
|
|
|
@@ -126,7 +149,7 @@ def clean_knots(knots, min_distance, lmin=0, lmax=inf):
|
|
|
126
149
|
class TSLPF(LogPosteriorFunction):
|
|
127
150
|
def __init__(self, runner, name: str, ldmodel, data: TSDataGroup, nk: int = 50, nldc: int = 10, nthreads: int = 1,
|
|
128
151
|
tmpars = None, noise_model: Literal["white", "fixed_gp", "free_gp"] = 'white',
|
|
129
|
-
interpolation: Literal['bspline', 'pchip', 'makima'] = '
|
|
152
|
+
interpolation: Literal['bspline', 'pchip', 'makima', 'nearest', 'linear'] = 'makima'):
|
|
130
153
|
super().__init__(name)
|
|
131
154
|
self._runner = runner
|
|
132
155
|
self._original_data: TSDataGroup | None = None
|
|
@@ -137,7 +160,10 @@ class TSLPF(LogPosteriorFunction):
|
|
|
137
160
|
self._baseline_models: list[ndarray] | None = None
|
|
138
161
|
self.interpolation: str = interpolation
|
|
139
162
|
|
|
140
|
-
|
|
163
|
+
if interpolation not in interpolator_choices:
|
|
164
|
+
raise ValueError(f'interpolation must be one of {interpolator_choices}')
|
|
165
|
+
self._ip = interpolators[interpolation]
|
|
166
|
+
self._ip_ld = interpolators['bspline']
|
|
141
167
|
|
|
142
168
|
self._gp: Optional[list[GP]] = None
|
|
143
169
|
self._gp_time: Optional[list[ndarray]] = None
|
|
@@ -147,6 +173,9 @@ class TSLPF(LogPosteriorFunction):
|
|
|
147
173
|
self.set_data(data)
|
|
148
174
|
self.set_noise_model(noise_model)
|
|
149
175
|
|
|
176
|
+
self.spot_model: SpotModel | None = None
|
|
177
|
+
self.spot_model_fluxes = []
|
|
178
|
+
|
|
150
179
|
self.ldmodel = ldmodel
|
|
151
180
|
if isinstance(ldmodel, LDTkLD):
|
|
152
181
|
for tm in self.tms:
|
|
@@ -211,6 +240,19 @@ class TSLPF(LogPosteriorFunction):
|
|
|
211
240
|
self.ps.freeze()
|
|
212
241
|
self.ndim = len(self.ps)
|
|
213
242
|
|
|
243
|
+
def initialize_spots(self, tstar: float, wlref: float, include_tlse: bool = True) -> None:
|
|
244
|
+
self.spot_model = SpotModel(self, tstar, wlref, include_tlse)
|
|
245
|
+
|
|
246
|
+
def add_spot(self, epoch_group: int) -> None:
|
|
247
|
+
"""Adds a new star spot.
|
|
248
|
+
|
|
249
|
+
Parameters
|
|
250
|
+
----------
|
|
251
|
+
epoch_group : int
|
|
252
|
+
The identifier of the epoch group to which the spot belongs.
|
|
253
|
+
"""
|
|
254
|
+
self.spot_model.add_spot(epoch_group)
|
|
255
|
+
|
|
214
256
|
def set_noise_model(self, noise_model: str) -> None:
|
|
215
257
|
"""Sets the noise model for the analysis.
|
|
216
258
|
|
|
@@ -434,6 +476,14 @@ class TSLPF(LogPosteriorFunction):
|
|
|
434
476
|
sln = self._sl_rratios
|
|
435
477
|
ndn = self.ndim
|
|
436
478
|
|
|
479
|
+
# Check if we have spots
|
|
480
|
+
# ----------------------
|
|
481
|
+
if self.spot_model is not None:
|
|
482
|
+
spots = self.spot_model
|
|
483
|
+
self.initialize_spots(spots.tphot, spots.wlref, spots.include_tlse)
|
|
484
|
+
for eg in spots.spot_epoch_groups:
|
|
485
|
+
self.spot_model.add_spot(eg)
|
|
486
|
+
|
|
437
487
|
# Set the priors back as they were
|
|
438
488
|
# --------------------------------
|
|
439
489
|
for po in pso:
|
|
@@ -566,8 +616,8 @@ class TSLPF(LogPosteriorFunction):
|
|
|
566
616
|
ldp = [zeros((pvp.shape[0], npb, 2)) for npb in self.npb]
|
|
567
617
|
for ids in range(self.data.size):
|
|
568
618
|
for ipv in range(pvp.shape[0]):
|
|
569
|
-
ldp[ids][ipv, :, 0] = self.
|
|
570
|
-
ldp[ids][ipv, :, 1] = self.
|
|
619
|
+
ldp[ids][ipv, :, 0] = self._ip_ld(self.wavelengths[ids], self.ld_knots, ldk[ipv, :, 0])
|
|
620
|
+
ldp[ids][ipv, :, 1] = self._ip_ld(self.wavelengths[ids], self.ld_knots, ldk[ipv, :, 1])
|
|
571
621
|
return ldp
|
|
572
622
|
|
|
573
623
|
def transit_model(self, pv, copy=True):
|
|
@@ -642,6 +692,10 @@ class TSLPF(LogPosteriorFunction):
|
|
|
642
692
|
def flux_model(self, pv):
|
|
643
693
|
transit_models = self.transit_model(pv)
|
|
644
694
|
baseline_models = self.baseline_model(pv)
|
|
695
|
+
if self.spot_model is not None:
|
|
696
|
+
self.spot_model.apply_spots(pv, transit_models)
|
|
697
|
+
if self.spot_model.include_tlse:
|
|
698
|
+
self.spot_model.apply_tlse(pv, transit_models)
|
|
645
699
|
for i in range(self.data.size):
|
|
646
700
|
transit_models[i][:, :, :] *= baseline_models[i][:, :, newaxis]
|
|
647
701
|
return transit_models
|
exoiris/util.py
CHANGED
|
@@ -15,8 +15,10 @@
|
|
|
15
15
|
# along with this program. If not, see <https://www.gnu.org/licenses/>.
|
|
16
16
|
|
|
17
17
|
from numba import njit
|
|
18
|
-
from numpy import zeros, sum, sqrt, linspace, vstack, concatenate, floor, dot, ndarray, nan
|
|
19
|
-
|
|
18
|
+
from numpy import (zeros, sum, sqrt, linspace, vstack, concatenate, floor, dot, ndarray, nan, asarray, tile)
|
|
19
|
+
from numpy._typing import ArrayLike
|
|
20
|
+
from pytransit import TSModel
|
|
21
|
+
from pytransit.orbits import i_from_ba
|
|
20
22
|
|
|
21
23
|
@njit
|
|
22
24
|
def bin2d(v, e, el, er, bins, estimate_errors: bool = False) -> tuple[ndarray, ndarray]:
|
|
@@ -116,4 +118,45 @@ def create_binning(ranges, bwidths):
|
|
|
116
118
|
n = int(floor((r[1] - r[0]) / w))
|
|
117
119
|
e = linspace(*r, num=n)
|
|
118
120
|
bins.append(vstack([e[:-1], e[1:]]).T)
|
|
119
|
-
return concatenate(bins)
|
|
121
|
+
return concatenate(bins)
|
|
122
|
+
|
|
123
|
+
|
|
124
|
+
def create_mock_model(ks: ArrayLike, times: ArrayLike = None, ldc: ArrayLike = None, t0: float = 0.0, p: float =2.0, a: float =8.0, b: float =0.0) -> ndarray:
|
|
125
|
+
"""Create a mock transmission spectrum observation using given parameters.
|
|
126
|
+
|
|
127
|
+
Parameters
|
|
128
|
+
----------
|
|
129
|
+
ks
|
|
130
|
+
Array of radius ratios, one radius ratio per wavelength.
|
|
131
|
+
times
|
|
132
|
+
Array of time values to set the data points. If None, defaults to a
|
|
133
|
+
linspace of 500 points in the range [-0.1, 0.1].
|
|
134
|
+
ldc
|
|
135
|
+
Array representing the limb darkening coefficients. If None, defaults to
|
|
136
|
+
a tile of [0.4, 0.4] for each wavelength element.
|
|
137
|
+
t0
|
|
138
|
+
Transit center.
|
|
139
|
+
p
|
|
140
|
+
Orbital period.
|
|
141
|
+
a
|
|
142
|
+
Semi-major axis.
|
|
143
|
+
b
|
|
144
|
+
Impact parameter.
|
|
145
|
+
|
|
146
|
+
Returns
|
|
147
|
+
-------
|
|
148
|
+
ndarray
|
|
149
|
+
Mock spectrophotometric light curves.
|
|
150
|
+
|
|
151
|
+
"""
|
|
152
|
+
ks = asarray(ks)
|
|
153
|
+
if times is None:
|
|
154
|
+
times = linspace(-0.1, 0.1, 500)
|
|
155
|
+
if ldc is None:
|
|
156
|
+
ldc = tile([0.4, 0.4], (1, ks.size, 1))
|
|
157
|
+
inc = i_from_ba(b, a)
|
|
158
|
+
|
|
159
|
+
m1 = TSModel('power-2', ng=100, nzin=50, nzlimb=50)
|
|
160
|
+
m1.set_data(times)
|
|
161
|
+
f1 = m1.evaluate(ks, ldc, t0, p, a, inc)[0]
|
|
162
|
+
return f1
|
|
@@ -0,0 +1,118 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ExoIris
|
|
3
|
+
Version: 0.21.0
|
|
4
|
+
Summary: Easy and robust exoplanet transmission spectroscopy.
|
|
5
|
+
Author-email: Hannu Parviainen <hannu@iac.es>
|
|
6
|
+
License: GPLv3
|
|
7
|
+
Project-URL: homepage, https://github.com/hpparvi/ExoIris
|
|
8
|
+
Keywords: astronomy,astrophysics,exoplanets
|
|
9
|
+
Classifier: Topic :: Scientific/Engineering
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
+
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
14
|
+
Classifier: Operating System :: OS Independent
|
|
15
|
+
Classifier: Programming Language :: Python
|
|
16
|
+
Requires-Python: >=3.10
|
|
17
|
+
Description-Content-Type: text/markdown
|
|
18
|
+
License-File: LICENSE
|
|
19
|
+
Requires-Dist: pytransit>=2.6.15
|
|
20
|
+
Requires-Dist: ldtk>=1.8.5
|
|
21
|
+
Requires-Dist: numpy
|
|
22
|
+
Requires-Dist: scipy
|
|
23
|
+
Requires-Dist: numba
|
|
24
|
+
Requires-Dist: emcee
|
|
25
|
+
Requires-Dist: matplotlib
|
|
26
|
+
Requires-Dist: celerite2
|
|
27
|
+
Requires-Dist: pandas
|
|
28
|
+
Requires-Dist: xarray
|
|
29
|
+
Requires-Dist: seaborn
|
|
30
|
+
Requires-Dist: astropy
|
|
31
|
+
Requires-Dist: uncertainties
|
|
32
|
+
Dynamic: license-file
|
|
33
|
+
|
|
34
|
+
# ExoIris: Fast and Flexible Transmission Spectroscopy in Python
|
|
35
|
+
|
|
36
|
+
[](https://exoiris.readthedocs.io)
|
|
37
|
+

|
|
38
|
+
[](CODE_OF_CONDUCT.md)
|
|
39
|
+
[](http://www.gnu.org/licenses/gpl-3.0.html)
|
|
40
|
+
[](https://pypi.org/project/ExoIris/)
|
|
41
|
+
|
|
42
|
+
**ExoIris** is a Python package for modeling exoplanet transmission spectroscopy. ExoIris removes the typical
|
|
43
|
+
limitations of the two-step workflow by modeling the full two-dimensional spectroscopic transit time series *directly*.
|
|
44
|
+
It supports combining transmission spectroscopy datasets from multiple instruments observed in different epochs, yielding
|
|
45
|
+
self-consistent wavelength-independent and wavelength-dependent parameters, simplifying joint analyses, and delivering
|
|
46
|
+
results quickly.
|
|
47
|
+
|
|
48
|
+

|
|
49
|
+
|
|
50
|
+
## Why ExoIris?
|
|
51
|
+
|
|
52
|
+
Transmission spectroscopy is often done following a **two-step workflow**: (1) fit a white light curve to infer
|
|
53
|
+
wavelength-independent parameters; (2) fit each spectroscopic light curve independently, constrained by the white-light
|
|
54
|
+
solution. This split can introduce approximations and inconsistencies.
|
|
55
|
+
|
|
56
|
+
**ExoIris takes a different approach.** It models spectrophotometric time series *end-to-end*, enabling:
|
|
57
|
+
|
|
58
|
+
- Self-consistent inference of shared (wavelength-independent) and spectral (wavelength-dependent) parameters.
|
|
59
|
+
- **Joint** modeling of multiple datasets from different instruments and epochs.
|
|
60
|
+
- Accounting for **transit timing variations** and dataset-dependent offsets within a unified framework.
|
|
61
|
+
|
|
62
|
+
This design is a natural fit for **JWST-class** data, where correlated noise, multi-epoch observations, and
|
|
63
|
+
cross-instrument combinations are the norm.
|
|
64
|
+
|
|
65
|
+
## Documentation
|
|
66
|
+
|
|
67
|
+
Full documentation and tutorials: <https://exoiris.readthedocs.io>
|
|
68
|
+
|
|
69
|
+
## Installation
|
|
70
|
+
|
|
71
|
+
Install from PyPI:
|
|
72
|
+
|
|
73
|
+
```bash
|
|
74
|
+
pip install exoiris
|
|
75
|
+
```
|
|
76
|
+
|
|
77
|
+
Latest development version:
|
|
78
|
+
|
|
79
|
+
```bash
|
|
80
|
+
git clone https://github.com/hpparvi/ExoIris.git
|
|
81
|
+
cd ExoIris
|
|
82
|
+
pip install -e .
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
ExoIris supports Python 3.9+. See the docs for dependency details and optional extras.
|
|
86
|
+
|
|
87
|
+
## Key Features
|
|
88
|
+
|
|
89
|
+
- **Direct modelling of spectroscopic transit time series**
|
|
90
|
+
Built on PyTransit’s `TSModel`, optimised for transmission spectroscopy; scales to hundreds–thousands of light curves simultaneously.
|
|
91
|
+
|
|
92
|
+
- **Flexible limb darkening**
|
|
93
|
+
Use standard analytical laws (quadratic, power-2, non-linear), numerical intensity profiles from stellar atmosphere models, or user-defined radially symmetric functions.
|
|
94
|
+
|
|
95
|
+
- **Robust noise treatment**
|
|
96
|
+
Choose white noise or **time-correlated** noise via a Gaussian Process likelihood, without changing the overall workflow.
|
|
97
|
+
|
|
98
|
+
- **Full control of spectral resolution**
|
|
99
|
+
The transmission spectrum is represented as a cubic spline with user-defined knots, allowing variable resolution across wavelength.
|
|
100
|
+
|
|
101
|
+
- **Reproducible, incremental workflows**
|
|
102
|
+
Save and reload models to refine a low-resolution run into a high-resolution analysis seamlessly.
|
|
103
|
+
|
|
104
|
+
- **Joint multi-dataset analyses**
|
|
105
|
+
Combine instruments and epochs in one fit, with support for transit timing variations and dataset-specific systematics and offsets.
|
|
106
|
+
|
|
107
|
+
## Performance
|
|
108
|
+
|
|
109
|
+
ExoIris is designed for speed and stability:
|
|
110
|
+
|
|
111
|
+
- A transmission spectroscopy analysis of a single JWST/NIRISS dataset at **R ≈ 100** typically runs in **3–5 minutes**
|
|
112
|
+
assuming white noise, or **5–15 minutes** with a GP noise model, on a standard desktop CPU.
|
|
113
|
+
- A high-resolution analysis of the JWST/NIRISS **WASP-39 b** dataset (~3800 spectroscopic light curves; see Feinstein
|
|
114
|
+
et al. 2023) can be optimised and sampled in about **1.5 hours** on an AMD Ryzen 7 5800X (8 cores, ~3-year-old desktop).
|
|
115
|
+
|
|
116
|
+
---
|
|
117
|
+
|
|
118
|
+
© 2025 Hannu Parviainen
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
exoiris/__init__.py,sha256=LU5jAE7_OVPLHFO0UAOGS0e0wuWV6rdSD0Qveet11K8,1147
|
|
2
|
+
exoiris/binning.py,sha256=-Y9hdK0jZj8DOS82keaprneid2lZ4rCx-keWlKi0LP8,6455
|
|
3
|
+
exoiris/ephemeris.py,sha256=dthBkJztT5yAP6VnnO7jGvxikboFUQBUGPUfBCFrA3w,1316
|
|
4
|
+
exoiris/exoiris.py,sha256=YRTieXTSXa4G0HBfKfDlFw52i3nKHa3OKrspfhJj5oE,55721
|
|
5
|
+
exoiris/ldtkld.py,sha256=7H1r1xail3vSKdsNKorMTqivnRKU9WrOVH-uE4Ky2jM,3495
|
|
6
|
+
exoiris/loglikelihood.py,sha256=AMssTs-lIMbnB5p9Y5z5Qb5XimP3TEfUUsHjW_n-MUM,6315
|
|
7
|
+
exoiris/spotmodel.py,sha256=9-DxvVzGzxf6AjQfrzZreyJB4Htw0gsIAD3nWl0tQMc,7160
|
|
8
|
+
exoiris/tsdata.py,sha256=WqId5rfZR08pFZ83UZiyO39-QjX6WcB1GrUYolZsM-4,35323
|
|
9
|
+
exoiris/tslpf.py,sha256=VOMnAJuX_adnipk5T4rZ2V6KIeVrvfhj8agDRCHrYTA,31573
|
|
10
|
+
exoiris/tsmodel.py,sha256=6NaGY48fWHUT_7ti6Ao618PN-LgyoIhfQd8lZQqZ7hU,5160
|
|
11
|
+
exoiris/util.py,sha256=uNv_c3Kuv1lml8MuDAuyElO4s3f1tRIQ1QMlLaI7Yak,5921
|
|
12
|
+
exoiris/wlpf.py,sha256=g6h1cLk2-nKD8u_FzwXNVVGFK4dry8fBr0A70LA5gJw,6281
|
|
13
|
+
exoiris-0.21.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
14
|
+
exoiris-0.21.0.dist-info/METADATA,sha256=En9nJ-OKz5_H7W9XPVXjjPtBZepegbkhBsebaM8lvDQ,5056
|
|
15
|
+
exoiris-0.21.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
16
|
+
exoiris-0.21.0.dist-info/top_level.txt,sha256=EoNxT6c5mQDcM0f_LUQB-ETsYg03lNaV3o2L_Yc6-aE,8
|
|
17
|
+
exoiris-0.21.0.dist-info/RECORD,,
|
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
Metadata-Version: 2.4
|
|
2
|
-
Name: ExoIris
|
|
3
|
-
Version: 0.19.2
|
|
4
|
-
Summary: Easy and robust exoplanet transmission spectroscopy.
|
|
5
|
-
Author-email: Hannu Parviainen <hannu@iac.es>
|
|
6
|
-
License: GPLv3
|
|
7
|
-
Project-URL: homepage, https://github.com/hpparvi/ExoIris
|
|
8
|
-
Keywords: astronomy,astrophysics,exoplanets
|
|
9
|
-
Classifier: Topic :: Scientific/Engineering
|
|
10
|
-
Classifier: Intended Audience :: Science/Research
|
|
11
|
-
Classifier: Intended Audience :: Developers
|
|
12
|
-
Classifier: Development Status :: 5 - Production/Stable
|
|
13
|
-
Classifier: License :: OSI Approved :: GNU General Public License v3 (GPLv3)
|
|
14
|
-
Classifier: Operating System :: OS Independent
|
|
15
|
-
Classifier: Programming Language :: Python
|
|
16
|
-
Requires-Python: >=3.10
|
|
17
|
-
Description-Content-Type: text/markdown
|
|
18
|
-
License-File: LICENSE
|
|
19
|
-
Requires-Dist: pytransit>=2.6.15
|
|
20
|
-
Requires-Dist: ldtk>=1.8.5
|
|
21
|
-
Requires-Dist: numpy
|
|
22
|
-
Requires-Dist: scipy
|
|
23
|
-
Requires-Dist: numba
|
|
24
|
-
Requires-Dist: emcee
|
|
25
|
-
Requires-Dist: matplotlib
|
|
26
|
-
Requires-Dist: celerite2
|
|
27
|
-
Requires-Dist: pandas
|
|
28
|
-
Requires-Dist: xarray
|
|
29
|
-
Requires-Dist: seaborn
|
|
30
|
-
Requires-Dist: astropy
|
|
31
|
-
Requires-Dist: uncertainties
|
|
32
|
-
Dynamic: license-file
|
|
33
|
-
|
|
34
|
-
# ExoIris: Transmission Spectroscopy Made Easy
|
|
35
|
-
|
|
36
|
-
[](https://exoiris.readthedocs.io)
|
|
37
|
-

|
|
38
|
-
[](CODE_OF_CONDUCT.md)
|
|
39
|
-
[](http://www.gnu.org/licenses/gpl-3.0.html)
|
|
40
|
-
[](https://pypi.org/project/ExoIris/)
|
|
41
|
-
|
|
42
|
-
**ExoIris** is a user-friendly Python package designed to simplify and accelerate the analysis of transmission
|
|
43
|
-
spectroscopy data for exoplanets. The package can estimate a self-consistent medium-resolution transmission spectrum
|
|
44
|
-
with uncertainties from JWST NIRISS data in minutes, even when using a Gaussian Process-based noise model.
|
|
45
|
-
|
|
46
|
-

|
|
47
|
-
|
|
48
|
-
## Documentation
|
|
49
|
-
|
|
50
|
-
Read the docs at [exoiris.readthedocs.io](https://exoiris.readthedocs.io).
|
|
51
|
-
|
|
52
|
-
## Key Features
|
|
53
|
-
|
|
54
|
-
- **Fast modelling of spectroscopic transit time series**: ExoIris uses PyTransit's advanced `TSModel` transit
|
|
55
|
-
model that is specially tailored for fast and efficient modelling of spectroscopic transit (or eclipse) time series.
|
|
56
|
-
- **Flexible handling of limb darkening**: The stellar limb darkening can be modelled freely either by any of the standard
|
|
57
|
-
limb darkening laws (quadratic, power-2, non-linear, etc.), by numerical stellar intensity profiles obtained
|
|
58
|
-
directly from stellar atmosphere models, or by an arbitrary ser-defined radially symmetric function.
|
|
59
|
-
- **Handling of Correlated noise**: The noise model can be chosen between white or time-correlated noise, where the
|
|
60
|
-
time-correlated noise is modelled as a Gaussian process.
|
|
61
|
-
- **Model saving and loading**: Seamless model saving and loading allows one to create a high-resolution analysis starting
|
|
62
|
-
from a saved low-resolution analysis.
|
|
63
|
-
- **Full control of resolution**: ExoIris represents the transmission spectrum as a cubic spline, with complete
|
|
64
|
-
flexibility to set and modify the number and placement of spline knots, allowing variable resolution throughout the
|
|
65
|
-
analysis.
|
|
66
|
-
|
|
67
|
-
## Details
|
|
68
|
-
|
|
69
|
-
ExoIris uses PyTransit's `TSModel`, a transit model that is specially optimised for transmission spectroscopy and allows
|
|
70
|
-
for simultaneous modelling of hundreds to thousands of spectroscopic light curves 20-30 times faster than when using
|
|
71
|
-
standard transit models not explicitly designed for transmission spectroscopy.
|
|
72
|
-
|
|
73
|
-
A complete posterior solution for a low-resolution transmission spectrum with a data resolution of R=100
|
|
74
|
-
takes 3-5 minutes to estimate assuming white noise, or 5-15 minutes if using a Gaussian process-based likelihood
|
|
75
|
-
model powered by the celerite2 package. A high-resolution spectrum of the JWST NIRISS WASP-39 b observations
|
|
76
|
-
by [Feinstein et al. (2023)](https://ui.adsabs.harvard.edu/abs/2023Natur.614..670F/abstract) with ~3800
|
|
77
|
-
spectroscopic light curves (as shown above) takes about 1.5 hours to optimise and sample on a three-year-old
|
|
78
|
-
AMD Ryzen 7 5800X with eight cores.
|
|
79
|
-
|
|
80
|
-
---
|
|
81
|
-
© 2024 Hannu Parviainen
|
exoiris-0.19.2.dist-info/RECORD
DELETED
|
@@ -1,15 +0,0 @@
|
|
|
1
|
-
exoiris/__init__.py,sha256=LU5jAE7_OVPLHFO0UAOGS0e0wuWV6rdSD0Qveet11K8,1147
|
|
2
|
-
exoiris/binning.py,sha256=-Y9hdK0jZj8DOS82keaprneid2lZ4rCx-keWlKi0LP8,6455
|
|
3
|
-
exoiris/ephemeris.py,sha256=dthBkJztT5yAP6VnnO7jGvxikboFUQBUGPUfBCFrA3w,1316
|
|
4
|
-
exoiris/exoiris.py,sha256=s-q5QoV2Vj4qDHfocXrdZZo0C99E3iTr3g8OjTTgaug,51312
|
|
5
|
-
exoiris/ldtkld.py,sha256=7H1r1xail3vSKdsNKorMTqivnRKU9WrOVH-uE4Ky2jM,3495
|
|
6
|
-
exoiris/tsdata.py,sha256=jEaoz9EvUtbZ3GS-W9ymp07NR952gfiBPlhs4Olll6I,35326
|
|
7
|
-
exoiris/tslpf.py,sha256=mpzYdubNXkeLAaQdUKKBuKJ6iDN_KEl4cOh4wcBB-g0,29734
|
|
8
|
-
exoiris/tsmodel.py,sha256=6NaGY48fWHUT_7ti6Ao618PN-LgyoIhfQd8lZQqZ7hU,5160
|
|
9
|
-
exoiris/util.py,sha256=5PynwYYHRrzyXJHskBtp2J-pcM59zsA1_VtDxencQm4,4630
|
|
10
|
-
exoiris/wlpf.py,sha256=g6h1cLk2-nKD8u_FzwXNVVGFK4dry8fBr0A70LA5gJw,6281
|
|
11
|
-
exoiris-0.19.2.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
|
|
12
|
-
exoiris-0.19.2.dist-info/METADATA,sha256=rmLHmfpXXera-ZGV_S3CHVi5oakrP317i-oeLrEaoYM,4214
|
|
13
|
-
exoiris-0.19.2.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
14
|
-
exoiris-0.19.2.dist-info/top_level.txt,sha256=EoNxT6c5mQDcM0f_LUQB-ETsYg03lNaV3o2L_Yc6-aE,8
|
|
15
|
-
exoiris-0.19.2.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|