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 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
- if hdul[0].header['LDMODEL'] == 'ldtk':
76
- filters, teff, logg, metal, dataset = pickle.loads(codecs.decode(json.loads(hdul[0].header['LDTKLD']).encode(), "base64"))
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 = hdul[0].header['LDMODEL']
84
+ ldm = hdr['LDMODEL']
80
85
 
86
+ # Read the interpolation model.
87
+ # =============================
81
88
  try:
82
- ip = hdul[0].header['INTERP']
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 = hdul[0].header['NOISE']
96
+ noise_model = hdr['NOISE']
88
97
  except KeyError:
89
98
  noise_model = "white"
90
99
 
91
- a = ExoIris(name or hdul[0].header['NAME'], ldmodel=ldm, data=data, noise_model=noise_model, interpolation=ip)
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 = hdul[0].header['P']
110
- a.zero_epoch = hdul[0].header['T0']
111
- a.transit_duration = hdul[0].header['T14']
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'] = 'bspline'):
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
 
@@ -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, pp, and t14, ephemeris, or transit limits in exposure indices.")
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
- fig.axx2 = axx2
572
- fig.axy2 = axy2
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 scipy.interpolate import pchip_interpolate, splrep, splev, Akima1DInterpolator
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'] = 'bspline'):
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
- self._ip = {'bspline': ip_bspline, 'pchip': ip_pchip, 'makima': ip_makima}[interpolation]
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._ip(self.wavelengths[ids], self.ld_knots, ldk[ipv, :, 0])
570
- ldp[ids][ipv, :, 1] = self._ip(self.wavelengths[ids], self.ld_knots, ldk[ipv, :, 1])
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
+ [![Docs](https://readthedocs.org/projects/exoiris/badge/)](https://exoiris.readthedocs.io)
37
+ ![Python package](https://github.com/hpparvi/ExoIris/actions/workflows/python-package.yml/badge.svg)
38
+ [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](CODE_OF_CONDUCT.md)
39
+ [![Licence](http://img.shields.io/badge/license-GPLv3-blue.svg?style=flat)](http://www.gnu.org/licenses/gpl-3.0.html)
40
+ [![PyPI version](https://badge.fury.io/py/exoiris.svg)](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
+ ![](doc/source/examples/e01/example1.png)
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
- [![Docs](https://readthedocs.org/projects/exoiris/badge/)](https://exoiris.readthedocs.io)
37
- ![Python package](https://github.com/hpparvi/ExoIris/actions/workflows/python-package.yml/badge.svg)
38
- [![Contributor Covenant](https://img.shields.io/badge/Contributor%20Covenant-2.0-4baaaa.svg)](CODE_OF_CONDUCT.md)
39
- [![Licence](http://img.shields.io/badge/license-GPLv3-blue.svg?style=flat)](http://www.gnu.org/licenses/gpl-3.0.html)
40
- [![PyPI version](https://badge.fury.io/py/exoiris.svg)](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
- ![](doc/source/examples/e01/example1.png)
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
- &copy; 2024 Hannu Parviainen
@@ -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,,