ExoIris 0.19.2__py3-none-any.whl → 0.20.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
@@ -71,28 +71,38 @@ def load_model(fname: Path | str, name: str | None = None):
71
71
  """
72
72
  with pf.open(fname) as hdul:
73
73
  data = TSDataGroup.import_fits(hdul)
74
+ hdr = hdul[0].header
74
75
 
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"))
76
+ # Read the limb darkening model.
77
+ # ==============================
78
+ if hdr['LDMODEL'] == 'ldtk':
79
+ filters, teff, logg, metal, dataset = pickle.loads(codecs.decode(json.loads(hdr['LDTKLD']).encode(), "base64"))
77
80
  ldm = LDTkLD(filters, teff, logg, metal, dataset=dataset)
78
81
  else:
79
- ldm = hdul[0].header['LDMODEL']
82
+ ldm = hdr['LDMODEL']
80
83
 
84
+ # Read the interpolation model.
85
+ # =============================
81
86
  try:
82
- ip = hdul[0].header['INTERP']
87
+ ip = hdr['INTERP']
83
88
  except KeyError:
84
89
  ip = 'bspline'
85
90
 
91
+ # Read the noise model.
92
+ # =====================
86
93
  try:
87
- noise_model = hdul[0].header['NOISE']
94
+ noise_model = hdr['NOISE']
88
95
  except KeyError:
89
96
  noise_model = "white"
90
97
 
91
- a = ExoIris(name or hdul[0].header['NAME'], ldmodel=ldm, data=data, noise_model=noise_model, interpolation=ip)
98
+ # Setup the analysis.
99
+ # ===================
100
+ a = ExoIris(name or hdr['NAME'], ldmodel=ldm, data=data, noise_model=noise_model, interpolation=ip)
92
101
  a.set_radius_ratio_knots(hdul['K_KNOTS'].data.astype('d'))
93
102
  a.set_limb_darkening_knots(hdul['LD_KNOTS'].data.astype('d'))
94
103
 
95
104
  # Read the white light curve models if they exist.
105
+ # ================================================
96
106
  try:
97
107
  tb = Table.read(hdul['WHITE_DATA'])
98
108
  white_ids = tb['id'].data
@@ -101,18 +111,28 @@ def load_model(fname: Path | str, name: str | None = None):
101
111
  a._white_fluxes = [tb['flux_obs'].data[white_ids == i] for i in uids]
102
112
  a._white_errors = [tb['flux_obs_err'].data[white_ids == i] for i in uids]
103
113
  a._white_models = [tb['flux_mod'].data[white_ids == i] for i in uids]
104
-
105
114
  except KeyError:
106
115
  pass
107
116
 
117
+ # Read the ephemeris if it exists.
118
+ # ================================
108
119
  try:
109
- a.period = hdul[0].header['P']
110
- a.zero_epoch = hdul[0].header['T0']
111
- a.transit_duration = hdul[0].header['T14']
120
+ a.period = hdr['P']
121
+ a.zero_epoch = hdr['T0']
122
+ a.transit_duration = hdr['T14']
112
123
  [d.mask_transit(a.zero_epoch, a.period, a.transit_duration) for d in a.data]
113
- except KeyError:
124
+ except (KeyError, ValueError):
114
125
  pass
115
126
 
127
+ # Read the spots if they exist.
128
+ # =============================
129
+ if 'SPOTS' in hdr and hdr['SPOTS'] is True:
130
+ a.initialize_spots(hdr["SP_TSTAR"], hdr["SP_REFWL"], hdr["SP_TLSE"])
131
+ for i in range(hdr['NSPOTS']):
132
+ a.add_spot(hdr[f'SP{i+1:02d}_EG'])
133
+
134
+ # Read the priors.
135
+ # ================
116
136
  priors = pickle.loads(codecs.decode(json.loads(hdul['PRIORS'].header['PRIORS']).encode(), "base64"))
117
137
  a._tsa.ps = ParameterSet([pickle.loads(p) for p in priors])
118
138
  a._tsa.ps.freeze()
@@ -132,7 +152,7 @@ class ExoIris:
132
152
 
133
153
  def __init__(self, name: str, ldmodel, data: TSDataGroup | TSData, nk: int = 50, nldc: int = 10, nthreads: int = 1,
134
154
  tmpars: dict | None = None, noise_model: Literal["white", "fixed_gp", "free_gp"] = 'white',
135
- interpolation: Literal['bspline', 'pchip', 'makima'] = 'bspline'):
155
+ interpolation: Literal['bspline', 'pchip', 'makima', 'nearest', 'linear'] = 'makima'):
136
156
  """
137
157
  Parameters
138
158
  ----------
@@ -343,6 +363,36 @@ class ExoIris:
343
363
  """
344
364
  self._tsa.set_gp_kernel(kernel)
345
365
 
366
+ def initialize_spots(self, tstar: float, wlref: float, include_tlse: bool = True):
367
+ """Initialize star spot model using given stellar and wavelength reference values.
368
+
369
+ Parameters
370
+ ----------
371
+ tstar
372
+ Effective stellar temperature [K].
373
+ wlref
374
+ Reference wavelength where spot amplitude matches the amplitude parameter.
375
+ """
376
+ self._tsa.initialize_spots(tstar, wlref, include_tlse)
377
+
378
+ def add_spot(self, epoch_group: int) -> None:
379
+ """Add a new star spot and associate it with an epoch group.
380
+
381
+ Parameters
382
+ ----------
383
+ epoch_group
384
+ Identifier for the epoch group to which the spot will be added.
385
+ """
386
+ self._tsa.add_spot(epoch_group)
387
+
388
+ @property
389
+ def nspots(self) -> int:
390
+ """Number of star spots."""
391
+ if self._tsa.spot_model is None:
392
+ return 0
393
+ else:
394
+ return self._tsa.spot_model.nspots
395
+
346
396
  @property
347
397
  def name(self) -> str:
348
398
  """Analysis name."""
@@ -1095,10 +1145,14 @@ class ExoIris:
1095
1145
  pri.header['interp'] = self._tsa.interpolation
1096
1146
  pri.header['noise'] = self._tsa.noise_model
1097
1147
 
1148
+ # Priors
1149
+ # ======
1098
1150
  pr = pf.ImageHDU(name='priors')
1099
1151
  priors = [pickle.dumps(p) for p in self.ps]
1100
1152
  pr.header['priors'] = json.dumps(codecs.encode(pickle.dumps(priors), "base64").decode())
1101
1153
 
1154
+ # Limb darkening
1155
+ # ==============
1102
1156
  if isinstance(self._tsa.ldmodel, LDTkLD):
1103
1157
  ldm = self._tsa.ldmodel
1104
1158
  pri.header['ldmodel'] = 'ldtk'
@@ -1107,11 +1161,15 @@ class ExoIris:
1107
1161
  else:
1108
1162
  pri.header['ldmodel'] = self._tsa.ldmodel
1109
1163
 
1164
+ # Knots
1165
+ # =====
1110
1166
  k_knots = pf.ImageHDU(self._tsa.k_knots, name='k_knots')
1111
1167
  ld_knots = pf.ImageHDU(self._tsa.ld_knots, name='ld_knots')
1112
1168
  hdul = pf.HDUList([pri, k_knots, ld_knots, pr])
1113
1169
  hdul += self.data.export_fits()
1114
1170
 
1171
+ # White light curve analysis
1172
+ # ==========================
1115
1173
  if self._wa is not None and self._wa._local_minimization is not None:
1116
1174
  wa_data = pf.BinTableHDU(
1117
1175
  Table(
@@ -1140,6 +1198,19 @@ class ExoIris:
1140
1198
  wa_params = pf.BinTableHDU(Table(self._wa._local_minimization.x, names=names), name='white_params')
1141
1199
  hdul.append(wa_params)
1142
1200
 
1201
+ # Spots
1202
+ # =====
1203
+ if self._tsa.spot_model is not None:
1204
+ pri.header['spots'] = True
1205
+ pri.header["sp_tstar"] = self._tsa.spot_model.tphot
1206
+ pri.header["sp_refwl"] = self._tsa.spot_model.wlref
1207
+ pri.header["sp_tlse"] = self._tsa.spot_model.include_tlse
1208
+ pri.header["nspots"] = self.nspots
1209
+ for i in range(self.nspots):
1210
+ pri.header[f"sp{i+1:02d}_eg"] = self._tsa.spot_model.spot_epoch_groups[i]
1211
+
1212
+ # Global optimization results
1213
+ # ===========================
1143
1214
  if self._tsa.de is not None:
1144
1215
  de = pf.BinTableHDU(Table(self._tsa._de_population, names=self.ps.names), name='DE')
1145
1216
  de.header['npop'] = self._tsa.de.n_pop
@@ -1147,6 +1218,8 @@ class ExoIris:
1147
1218
  de.header['imin'] = self._tsa.de.minimum_index
1148
1219
  hdul.append(de)
1149
1220
 
1221
+ # MCMC results
1222
+ # ============
1150
1223
  if self._tsa.sampler is not None:
1151
1224
  mc = pf.BinTableHDU(Table(self._tsa.sampler.flatchain, names=self.ps.names), name='MCMC')
1152
1225
  mc.header['npop'] = self._tsa.sampler.nwalkers
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,20 @@ 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
+ FloaterHormannInterpolator,
39
+ )
32
40
 
33
41
  from .tsmodel import TransmissionSpectroscopyModel as TSModel
34
42
  from .tsdata import TSDataGroup
35
43
  from .ldtkld import LDTkLD
44
+ from .spotmodel import SpotModel
36
45
 
37
46
  NM_WHITE = 0
38
47
  NM_GP_FIXED = 1
@@ -81,6 +90,14 @@ def ip_makima(x, xk, yk):
81
90
  return Akima1DInterpolator(xk, yk, method='makima', extrapolate=True)(x)
82
91
 
83
92
 
93
+ def ip_nearest(x, xk, yk):
94
+ return interp1d(xk, yk, kind='nearest', bounds_error=False, fill_value='extrapolate', assume_sorted=True)(x)
95
+
96
+
97
+ def ip_linear(x, xk, yk):
98
+ return interp1d(xk, yk, kind='linear', bounds_error=False, fill_value='extrapolate', assume_sorted=True)(x)
99
+
100
+
84
101
  def add_knots(x_new, x_old):
85
102
  return sort(concatenate([x_new, x_old]))
86
103
 
@@ -126,7 +143,7 @@ def clean_knots(knots, min_distance, lmin=0, lmax=inf):
126
143
  class TSLPF(LogPosteriorFunction):
127
144
  def __init__(self, runner, name: str, ldmodel, data: TSDataGroup, nk: int = 50, nldc: int = 10, nthreads: int = 1,
128
145
  tmpars = None, noise_model: Literal["white", "fixed_gp", "free_gp"] = 'white',
129
- interpolation: Literal['bspline', 'pchip', 'makima'] = 'bspline'):
146
+ interpolation: Literal['bspline', 'pchip', 'makima', 'nearest', 'linear'] = 'makima'):
130
147
  super().__init__(name)
131
148
  self._runner = runner
132
149
  self._original_data: TSDataGroup | None = None
@@ -137,7 +154,8 @@ class TSLPF(LogPosteriorFunction):
137
154
  self._baseline_models: list[ndarray] | None = None
138
155
  self.interpolation: str = interpolation
139
156
 
140
- self._ip = {'bspline': ip_bspline, 'pchip': ip_pchip, 'makima': ip_makima}[interpolation]
157
+ self._ip = {'bspline': ip_bspline, 'pchip': ip_pchip, 'makima': ip_makima,
158
+ 'nearest': ip_nearest, 'linear': ip_linear}[interpolation]
141
159
 
142
160
  self._gp: Optional[list[GP]] = None
143
161
  self._gp_time: Optional[list[ndarray]] = None
@@ -147,6 +165,9 @@ class TSLPF(LogPosteriorFunction):
147
165
  self.set_data(data)
148
166
  self.set_noise_model(noise_model)
149
167
 
168
+ self.spot_model: SpotModel | None = None
169
+ self.spot_model_fluxes = []
170
+
150
171
  self.ldmodel = ldmodel
151
172
  if isinstance(ldmodel, LDTkLD):
152
173
  for tm in self.tms:
@@ -211,6 +232,19 @@ class TSLPF(LogPosteriorFunction):
211
232
  self.ps.freeze()
212
233
  self.ndim = len(self.ps)
213
234
 
235
+ def initialize_spots(self, tstar: float, wlref: float, include_tlse: bool = True) -> None:
236
+ self.spot_model = SpotModel(self, tstar, wlref, include_tlse)
237
+
238
+ def add_spot(self, epoch_group: int) -> None:
239
+ """Adds a new star spot.
240
+
241
+ Parameters
242
+ ----------
243
+ epoch_group : int
244
+ The identifier of the epoch group to which the spot belongs.
245
+ """
246
+ self.spot_model.add_spot(epoch_group)
247
+
214
248
  def set_noise_model(self, noise_model: str) -> None:
215
249
  """Sets the noise model for the analysis.
216
250
 
@@ -434,6 +468,14 @@ class TSLPF(LogPosteriorFunction):
434
468
  sln = self._sl_rratios
435
469
  ndn = self.ndim
436
470
 
471
+ # Check if we have spots
472
+ # ----------------------
473
+ if self.spot_model is not None:
474
+ spots = self.spot_model
475
+ self.initialize_spots(spots.tphot, spots.wlref, spots.include_tlse)
476
+ for eg in spots.spot_epoch_groups:
477
+ self.spot_model.add_spot(eg)
478
+
437
479
  # Set the priors back as they were
438
480
  # --------------------------------
439
481
  for po in pso:
@@ -566,8 +608,8 @@ class TSLPF(LogPosteriorFunction):
566
608
  ldp = [zeros((pvp.shape[0], npb, 2)) for npb in self.npb]
567
609
  for ids in range(self.data.size):
568
610
  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])
611
+ ldp[ids][ipv, :, 0] = ip_bspline(self.wavelengths[ids], self.ld_knots, ldk[ipv, :, 0])
612
+ ldp[ids][ipv, :, 1] = ip_bspline(self.wavelengths[ids], self.ld_knots, ldk[ipv, :, 1])
571
613
  return ldp
572
614
 
573
615
  def transit_model(self, pv, copy=True):
@@ -642,6 +684,10 @@ class TSLPF(LogPosteriorFunction):
642
684
  def flux_model(self, pv):
643
685
  transit_models = self.transit_model(pv)
644
686
  baseline_models = self.baseline_model(pv)
687
+ if self.spot_model is not None:
688
+ self.spot_model.apply_spots(pv, transit_models)
689
+ if self.spot_model.include_tlse:
690
+ self.spot_model.apply_tlse(pv, transit_models)
645
691
  for i in range(self.data.size):
646
692
  transit_models[i][:, :, :] *= baseline_models[i][:, :, newaxis]
647
693
  return transit_models
@@ -0,0 +1,118 @@
1
+ Metadata-Version: 2.4
2
+ Name: ExoIris
3
+ Version: 0.20.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,16 @@
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=POqkyc9hM5YmARDRtVN6k0l6N63hk6g_ecOR8Y4Z_2o,53838
5
+ exoiris/ldtkld.py,sha256=7H1r1xail3vSKdsNKorMTqivnRKU9WrOVH-uE4Ky2jM,3495
6
+ exoiris/spotmodel.py,sha256=9-DxvVzGzxf6AjQfrzZreyJB4Htw0gsIAD3nWl0tQMc,7160
7
+ exoiris/tsdata.py,sha256=WqId5rfZR08pFZ83UZiyO39-QjX6WcB1GrUYolZsM-4,35323
8
+ exoiris/tslpf.py,sha256=yFyMjvx8P9t55wYGGfoEZ8u3Yw1H7T4Ot-dsnT2oQEw,31316
9
+ exoiris/tsmodel.py,sha256=6NaGY48fWHUT_7ti6Ao618PN-LgyoIhfQd8lZQqZ7hU,5160
10
+ exoiris/util.py,sha256=5PynwYYHRrzyXJHskBtp2J-pcM59zsA1_VtDxencQm4,4630
11
+ exoiris/wlpf.py,sha256=g6h1cLk2-nKD8u_FzwXNVVGFK4dry8fBr0A70LA5gJw,6281
12
+ exoiris-0.20.0.dist-info/licenses/LICENSE,sha256=OXLcl0T2SZ8Pmy2_dmlvKuetivmyPd5m1q-Gyd-zaYY,35149
13
+ exoiris-0.20.0.dist-info/METADATA,sha256=LfJuP-1bFOckme0rDdzOcohvnUeuvspSNQ_m9_yNrbc,5056
14
+ exoiris-0.20.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
15
+ exoiris-0.20.0.dist-info/top_level.txt,sha256=EoNxT6c5mQDcM0f_LUQB-ETsYg03lNaV3o2L_Yc6-aE,8
16
+ exoiris-0.20.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,,