ExoIris 0.23.2__py3-none-any.whl → 1.1.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.
@@ -0,0 +1,164 @@
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
+ import io
18
+ import warnings
19
+ from contextlib import redirect_stdout
20
+ import numpy as np
21
+
22
+ from petitRADTRANS.radtrans import Radtrans
23
+ from petitRADTRANS import physical_constants as cst
24
+ from petitRADTRANS.spectral_model import SpectralModel
25
+ from petitRADTRANS.retrieval.data import Data
26
+
27
+ with warnings.catch_warnings():
28
+ warnings.simplefilter('ignore')
29
+ from exoiris.exoiris import ExoIris
30
+ from pytransit.lpf.logposteriorfunction import LogPosteriorFunction
31
+ from pytransit.param import ParameterSet, GParameter, UniformPrior as UP, NormalPrior as NP
32
+
33
+ class PRTRetrieval(LogPosteriorFunction):
34
+ def __init__(self, name: str, line_species, ei: ExoIris, rstar: float, mplanet: float, temperature_profile='isothermal',
35
+ pres: int = 100, r: int | None = None, quiet: bool = False):
36
+ super().__init__(name)
37
+ self._line_names = line_species
38
+ if r is None:
39
+ self.line_species = line_species
40
+ else:
41
+ self.line_species = [ls + f'.R{r}' for ls in line_species]
42
+
43
+ self.rstar = rstar
44
+ self.mplanet = mplanet
45
+ self.temperature_profile = temperature_profile
46
+ self.pres = pres
47
+ self.ei = ei
48
+ self.r = r
49
+
50
+ with warnings.catch_warnings():
51
+ warnings.simplefilter("ignore")
52
+ if quiet:
53
+ buf = io.StringIO()
54
+ with redirect_stdout(buf):
55
+ self._create_model()
56
+ else:
57
+ self._create_model()
58
+
59
+ self._init_parameters()
60
+
61
+ self.wavelengths = self.model()[0]
62
+ self._loglike = ei.create_loglikelihood_function(self.wavelengths, 'radius_ratio', method='svd')
63
+
64
+ def _init_parameters(self):
65
+ self.ps = ParameterSet([])
66
+ self._init_p_planet()
67
+ self._init_p_temperature()
68
+ self._init_p_clouds()
69
+ self._init_p_species()
70
+
71
+ def _init_p_planet(self):
72
+ self.ps.thaw()
73
+ ps = [GParameter('radius', 'Planet radius', 'R_Jup', NP(1.3, 0.05), (0.0, 2.5)),
74
+ GParameter('logg', 'Planet log g', '', UP(2, 4), (2, 4))]
75
+ self.ps.add_global_block('planet', ps)
76
+ self.ps.freeze()
77
+ self._sl_planet = self.ps.blocks[-1].slice
78
+ self._st_planet = self.ps.blocks[-1].start
79
+
80
+ def _init_p_clouds(self):
81
+ self.ps.thaw()
82
+ ps = [GParameter('pc', 'Cloud top pressure', '', UP(-6, 2), (-6, 2))]
83
+ self.ps.add_global_block('clouds', ps)
84
+ self.ps.freeze()
85
+ self._sl_clouds = self.ps.blocks[-1].slice
86
+ self._st_clouds = self.ps.blocks[-1].start
87
+
88
+ def _init_p_temperature(self):
89
+ self.ps.thaw()
90
+ if self.temperature_profile == 'isothermal':
91
+ ps = [GParameter('tp_teq', 'equilibrium temperature', 'K', UP(800, 1200), (500, 3500))]
92
+ elif self.temperature_profile == 'guillot':
93
+ ps = [GParameter('tp_teq', 'equilibrium temperature', 'K', UP(800, 1200), (500, 3500)),
94
+ GParameter('tp_tin', 'intrinsinc temperature', 'K', UP(800, 1200), (500, 3500)),
95
+ GParameter('tp_mo', 'guillot profile mean opacity', 'K', UP(0, 1), (0, 1)),
96
+ GParameter('tp_g', 'guillot profile gamma', 'K', UP(0, 1), (0, 1))]
97
+ self.ps.add_global_block('temperature_profile', ps)
98
+ self._sl_tprof = self.ps.blocks[-1].slice
99
+ self._st_tprof = self.ps.blocks[-1].start
100
+
101
+ def _init_p_species(self):
102
+ self.ps.thaw()
103
+ ps = [GParameter(n, n, '', UP(-12, -0.2), [-12, 0]) for n in self.line_species]
104
+ self.ps.add_global_block('line_species', ps)
105
+ self.ps.freeze()
106
+ self._sl_species = self.ps.blocks[-1].slice
107
+ self._st_species = self.ps.blocks[-1].start
108
+
109
+ def _c_temperature_profile(self, pv) -> dict:
110
+ pv = pv[self._sl_tprof]
111
+ pars = {}
112
+ pars['temperature'] = pv[0]
113
+ if self.temperature_profile == 'isothermal':
114
+ pass
115
+ elif self.temperature_profile == 'guillot':
116
+ pars['intrinsic_temperature'] = pv[1]
117
+ pars['guillot_temperature_profile_infrared_mean_opacity_solar_metallicity'] = pv[2]
118
+ pars['guillot_temperature_profile_gamma'] = pv[3]
119
+ return pars
120
+
121
+ def _create_model(self):
122
+ self.sm = SpectralModel(
123
+ pressures=np.logspace(-6, 2, self.pres),
124
+ line_species=self.line_species,
125
+ rayleigh_species=['H2', 'He'],
126
+ gas_continuum_contributors=['H2--H2', 'H2--He'],
127
+ wavelength_boundaries=[self.ei.data.wlmin, self.ei.data.wlmax],
128
+ star_radius=self.rstar * cst.r_sun,
129
+ planet_radius=1.0 * cst.r_jup_mean,
130
+ planet_mass=self.mplanet * cst.m_jup,
131
+ reference_gravity=391,
132
+ reference_pressure=1e-2,
133
+ temperature_profile=self.temperature_profile,
134
+ temperature=1000,
135
+ # cloud_mode='power_law',
136
+ # power_law_opacity_350nm = 0.01,
137
+ # power_law_opacity_coefficient = -4.0,
138
+ # opaque_cloud_top_pressure=1e-3,
139
+ # cloud_fraction=1.0,
140
+ haze_factor=100.0,
141
+ # co_ratio=1,
142
+ use_equilibrium_chemistry=False,
143
+ imposed_mass_fractions={s: 1e-4 for s in self.line_species},
144
+ filling_species={'H2': 37, 'He': 12}
145
+ )
146
+
147
+ def model(self, pv=None):
148
+ if pv is not None:
149
+
150
+ self.sm.model_parameters.update(self._c_temperature_profile(pv))
151
+ self.sm.model_parameters['planet_radius'] = pv[self._st_planet] * cst.r_jup_mean
152
+ self.sm.model_parameters['reference_gravity'] = 10 ** pv[self._st_planet + 1]
153
+ self.sm.model_parameters['opaque_cloud_top_pressure'] = 10 ** pv[self._st_clouds]
154
+ for i, ls in enumerate(self.line_species):
155
+ self.sm.model_parameters['imposed_mass_fractions'][ls] = 10 ** pv[self._st_species + i]
156
+ self.sm.update_spectral_calculation_parameters(**self.sm.model_parameters)
157
+ wavelengths, radii = self.sm.calculate_spectrum(mode='transmission')
158
+ return wavelengths[0] * 1e4, radii[0]
159
+
160
+ def lnlikelihood(self, pv):
161
+ with warnings.catch_warnings():
162
+ warnings.simplefilter("ignore")
163
+ model_spectrum = self.model(pv)[1]
164
+ return self._loglike(model_spectrum / (self.rstar * cst.r_sun))
exoiris/spotmodel.py CHANGED
@@ -36,13 +36,13 @@ from pytransit.stars import create_bt_settl_interpolator, create_husser2013_inte
36
36
  from pytransit.param import GParameter, UniformPrior as U
37
37
 
38
38
  from exoiris.tsdata import TSData
39
- from exoiris.util import bin2d
39
+ from exoiris.bin1d import bin1d
40
40
 
41
41
 
42
42
  @njit
43
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)
44
+ c = log(4)**(1/shape) * fwhm
45
+ return amplitude*exp(-(2*fabs(x-center) / c)**shape)
46
46
 
47
47
 
48
48
  @njit
@@ -81,7 +81,7 @@ def bin_stellar_spectrum_model(sp: RegularGridInterpolator, data: TSData):
81
81
  wl_l_edges = wave - 0.5
82
82
  wl_r_edges = wave + 0.5
83
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
84
+ bflux = bin1d(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
85
  return RegularGridInterpolator((teff, data.wavelength), bflux, bounds_error=False, fill_value=nan)
86
86
 
87
87
 
exoiris/tsdata.py CHANGED
@@ -56,12 +56,14 @@ from numpy import (
56
56
  ascontiguousarray,
57
57
  vstack,
58
58
  ones_like,
59
+ average,
59
60
  )
60
61
  from pytransit.orbits import fold
61
62
 
62
63
  from .binning import Binning, CompoundBinning
63
64
  from .ephemeris import Ephemeris
64
- from .util import bin2d
65
+ from .bin1d import bin1d
66
+ from .bin2d import bin2d
65
67
 
66
68
 
67
69
  class TSData:
@@ -603,6 +605,14 @@ class TSData:
603
605
  ax.axy2 = axy2
604
606
  return fig
605
607
 
608
+ def create_white_light_curve(self, data=None) -> ndarray:
609
+ """Create a white light curve."""
610
+ if data is not None and data.shape != self.fluxes.shape:
611
+ raise ValueError("The data must have the same shape as the 2D flux array.")
612
+ data = data if data is not None else self.fluxes
613
+ weights = where(isfinite(data) & isfinite(self.errors), 1/self.errors**2, 0.0)
614
+ return average(where(isfinite(data), data, 0), axis=0, weights=weights)
615
+
606
616
  def plot_white(self, ax: Axes | None = None, figsize: tuple[float, float] | None = None) -> Figure:
607
617
  """Plot a white light curve.
608
618
 
@@ -623,7 +633,7 @@ class TSData:
623
633
  fig = ax.figure
624
634
  tref = floor(self.time.min())
625
635
 
626
- ax.plot(self.time, nanmean(self.fluxes, 0))
636
+ ax.plot(self.time, self.create_white_light_curve())
627
637
  if self.ephemeris is not None:
628
638
  [ax.axvline(tl, ls='--', c='k') for tl in self.ephemeris.transit_limits(self.time.mean())]
629
639
 
@@ -677,6 +687,76 @@ class TSData:
677
687
  else:
678
688
  return TSDataGroup([self]) + other
679
689
 
690
+ def bin(self,
691
+ wave_binning: Optional[Union[Binning, CompoundBinning]] = None,
692
+ time_binning: Optional[Union[Binning, CompoundBinning]] = None,
693
+ wave_nb: Optional[int] = None, wave_bw: Optional[float] = None, wave_r: Optional[float] = None,
694
+ time_nb: Optional[int] = None, time_bw: Optional[float] = None,
695
+ estimate_errors: bool = False) -> 'TSData':
696
+ """Bin the data along the wavelength axis.
697
+
698
+ Bin the data along the wavelength and/or time axes. If binning is not specified, a Binning object is created using the
699
+ minimum and maximum time and wavelength values.
700
+
701
+ Parameters
702
+ ----------
703
+ binning
704
+ The binning method to use.
705
+ nb
706
+ Number of bins.
707
+ bw
708
+ Bin width.
709
+ r
710
+ Bin resolution.
711
+ estimate_errors
712
+ Should the uncertainties be estimated from the data.
713
+
714
+ Returns
715
+ -------
716
+ TSData
717
+ """
718
+
719
+ if wave_binning is None and wave_nb is None and wave_bw is None and wave_r is None:
720
+ return self.bin_time(time_binning, time_nb, time_bw, estimate_errors=estimate_errors)
721
+ if time_binning is None and time_nb is None and time_bw is None:
722
+ return self.bin_wavelength(wave_binning, wave_nb, wave_bw, wave_r, estimate_errors=estimate_errors)
723
+
724
+ with warnings.catch_warnings():
725
+ warnings.simplefilter('ignore', numba.NumbaPerformanceWarning)
726
+ if wave_binning is None:
727
+ wave_binning = Binning(self.bbox_wl[0], self.bbox_wl[1], nb=wave_nb, bw=wave_bw, r=wave_r)
728
+ if time_binning is None:
729
+ time_binning = Binning(self.time.min(), self.time.max(), nb=time_nb, bw=time_bw/(24*60*60) if time_bw is not None else None)
730
+
731
+ bf, be, bn = bin2d(self.fluxes, self.errors,
732
+ self._wl_l_edges, self._wl_r_edges,
733
+ self._tm_l_edges, self._tm_r_edges,
734
+ wave_binning.bins, time_binning.bins,
735
+ estimate_errors=estimate_errors)
736
+
737
+ bc, _ = bin1d(self.covs, ones_like(self.covs),
738
+ self._tm_l_edges, self._tm_r_edges,
739
+ time_binning.bins,
740
+ estimate_errors=False)
741
+
742
+ if not all(isfinite(be)):
743
+ warnings.warn('Error estimation failed for some bins, check the error array.')
744
+
745
+ d = TSData(time_binning.bins.mean(1), wave_binning.bins.mean(1), bf, be,
746
+ name=self.name,
747
+ wl_edges=(wave_binning.bins[:, 0], wave_binning.bins[:, 1]),
748
+ tm_edges=(time_binning.bins[:, 0], time_binning.bins[:, 1]),
749
+ noise_group=self.noise_group,
750
+ epoch_group=self.epoch_group,
751
+ offset_group=self.offset_group,
752
+ ephemeris=self.ephemeris,
753
+ n_baseline=self.n_baseline,
754
+ covs=bc)
755
+ if self.ephemeris is not None:
756
+ d.mask_transit(ephemeris=self.ephemeris)
757
+ return d
758
+
759
+
680
760
  def bin_wavelength(self, binning: Optional[Union[Binning, CompoundBinning]] = None,
681
761
  nb: Optional[int] = None, bw: Optional[float] = None, r: Optional[float] = None,
682
762
  estimate_errors: bool = False) -> 'TSData':
@@ -706,7 +786,7 @@ class TSData:
706
786
  warnings.simplefilter('ignore', numba.NumbaPerformanceWarning)
707
787
  if binning is None:
708
788
  binning = Binning(self.bbox_wl[0], self.bbox_wl[1], nb=nb, bw=bw, r=r)
709
- bf, be = bin2d(self.fluxes, self.errors, self._wl_l_edges, self._wl_r_edges,
789
+ bf, be = bin1d(self.fluxes, self.errors, self._wl_l_edges, self._wl_r_edges,
710
790
  binning.bins, estimate_errors=estimate_errors)
711
791
  if not all(isfinite(be)):
712
792
  warnings.warn('Error estimation failed for some bins, check the error array.')
@@ -749,9 +829,9 @@ class TSData:
749
829
  warnings.simplefilter('ignore', numba.NumbaPerformanceWarning)
750
830
  if binning is None:
751
831
  binning = Binning(self.time.min(), self.time.max(), nb=nb, bw=bw/(24*60*60) if bw is not None else None)
752
- bf, be = bin2d(self.fluxes.T, self.errors.T, self._tm_l_edges, self._tm_r_edges,
832
+ bf, be = bin1d(self.fluxes.T, self.errors.T, self._tm_l_edges, self._tm_r_edges,
753
833
  binning.bins, estimate_errors=estimate_errors)
754
- bc, _ = bin2d(self.covs, ones_like(self.covs), self._tm_l_edges, self._tm_r_edges, binning.bins, False)
834
+ bc, _ = bin1d(self.covs, ones_like(self.covs), self._tm_l_edges, self._tm_r_edges, binning.bins, False)
755
835
  d = TSData(binning.bins.mean(1), self.wavelength, bf.T, be.T,
756
836
  wl_edges=(self._wl_l_edges, self._wl_r_edges),
757
837
  tm_edges=(binning.bins[:,0], binning.bins[:,1]),
exoiris/tslpf.py CHANGED
@@ -55,17 +55,19 @@ from scipy.interpolate import (
55
55
  interp1d,
56
56
  )
57
57
 
58
+ from .lmlikelihood import marginalized_loglike_mbl2d
58
59
  from .ldtkld import LDTkLD
59
60
  from .spotmodel import SpotModel
60
61
  from .tsdata import TSDataGroup
61
62
  from .tsmodel import TransmissionSpectroscopyModel as TSModel
62
63
 
63
- NM_WHITE = 0
64
+ NM_WHITE_MARGINALIZED = 0
64
65
  NM_GP_FIXED = 1
65
66
  NM_GP_FREE = 2
67
+ NM_WHITE_PROFILED = 3
66
68
 
67
- noise_models = dict(white=NM_WHITE, fixed_gp=NM_GP_FIXED, free_gp=NM_GP_FREE)
68
-
69
+ noise_models = dict(white=NM_WHITE_PROFILED, white_profiled=NM_WHITE_PROFILED, white_marginalized=NM_WHITE_MARGINALIZED,
70
+ fixed_gp=NM_GP_FIXED, free_gp=NM_GP_FREE)
69
71
 
70
72
  @njit
71
73
  def nlstsq(covs, res, mask, wlmask, with_nans):
@@ -74,7 +76,10 @@ def nlstsq(covs, res, mask, wlmask, with_nans):
74
76
  x = zeros((nc, nwl))
75
77
  x[:, wlmask] = lstsq(covs, ascontiguousarray(res[wlmask].T))[0]
76
78
  for i in with_nans:
77
- x[:, i] = lstsq(covs[mask[i]], res[i, mask[i]])[0]
79
+ try:
80
+ x[:, i] = lstsq(covs[mask[i]], res[i, mask[i]])[0]
81
+ except:
82
+ x[:, i] = nan
78
83
  return x
79
84
 
80
85
 
@@ -181,7 +186,7 @@ def clean_knots(knots, min_distance, lmin=0, lmax=inf):
181
186
 
182
187
  class TSLPF(LogPosteriorFunction):
183
188
  def __init__(self, runner, name: str, ldmodel, data: TSDataGroup, nk: int = 50, nldc: int = 10, nthreads: int = 1,
184
- tmpars = None, noise_model: Literal["white", "fixed_gp", "free_gp"] = 'white',
189
+ tmpars = None, noise_model: Literal["white_profiled", "white_marginalized", "fixed_gp", "free_gp"] = 'white_profiled',
185
190
  interpolation: Literal['nearest', 'linear', 'pchip', 'makima', 'bspline', 'bspline-quadratic', 'bspline-cubic'] = 'linear'):
186
191
  super().__init__(name)
187
192
  self._runner = runner
@@ -191,12 +196,13 @@ class TSLPF(LogPosteriorFunction):
191
196
  self.npt: list[int] | None = None
192
197
  self._baseline_models: list[ndarray] | None = None
193
198
  self.interpolation: str = interpolation
199
+ self.ld_interpolation: str = 'bspline-quadratic'
194
200
 
195
201
  if interpolation not in interpolator_choices:
196
202
  raise ValueError(f'interpolation must be one of {interpolator_choices}')
197
203
 
198
204
  self._ip = interpolators[interpolation]
199
- self._ip_ld = interpolators['bspline']
205
+ self._ip_ld = interpolators[self.ld_interpolation]
200
206
 
201
207
  self._gp: Optional[list[GP]] = None
202
208
  self._gp_time: Optional[list[ndarray]] = None
@@ -295,7 +301,7 @@ class TSLPF(LogPosteriorFunction):
295
301
  Parameters
296
302
  ----------
297
303
  noise_model : str
298
- The noise model to be used. Must be one of the following: white, fixed_gp, free_gp.
304
+ The noise model to be used. Must be one of the following: white_profiled, white_marginalized, fixed_gp, free_gp.
299
305
 
300
306
  Raises
301
307
  ------
@@ -303,7 +309,7 @@ class TSLPF(LogPosteriorFunction):
303
309
  If noise_model is not one of the specified options.
304
310
  """
305
311
  if noise_model not in noise_models.keys():
306
- raise ValueError('noise_model must be one of: white, fixed_gp, free_gp')
312
+ raise ValueError('noise_model must be one of: white_profiled, white_marginalized, fixed_gp, free_gp')
307
313
  self.noise_model = noise_model
308
314
  self._nm = noise_models[noise_model]
309
315
  if self._nm in (NM_GP_FIXED, NM_GP_FREE):
@@ -468,6 +474,13 @@ class TSLPF(LogPosteriorFunction):
468
474
  """
469
475
  self.set_k_knots(concatenate([self.k_knots, knot_wavelengths]))
470
476
 
477
+ def set_k_interpolator(self, interpolator: str) -> None:
478
+ """Set the interpolator for the radius ratio (k) model."""
479
+ if interpolator not in interpolators.keys():
480
+ raise ValueError(f"Interpolator {interpolator} not recognized.")
481
+ self.interpolation = interpolator
482
+ self._ip = interpolators[interpolator]
483
+
471
484
  def set_k_knots(self, knot_wavelengths) -> None:
472
485
  """Set the radius ratio (k) knot wavelengths for the model.
473
486
 
@@ -607,6 +620,12 @@ class TSLPF(LogPosteriorFunction):
607
620
  return logp
608
621
  self._additional_log_priors.append(k_knot_order_prior)
609
622
 
623
+ def set_ld_interpolator(self, interpolator: str) -> None:
624
+ """Set the interpolator for the limb darkening model."""
625
+ if interpolator not in interpolators.keys():
626
+ raise ValueError(f"Interpolator {interpolator} not recognized.")
627
+ self.ld_interpolation = interpolator
628
+ self._ip_ld = interpolators[interpolator]
610
629
 
611
630
  def add_ld_knots(self, knot_wavelengths) -> None:
612
631
  """Add limb darkening knots to the model.
@@ -626,36 +645,80 @@ class TSLPF(LogPosteriorFunction):
626
645
  knot_wavelengths : array-like
627
646
  Array of knot wavelengths.
628
647
  """
648
+
649
+ # Save the old variables
650
+ # ----------------------
629
651
  xo = self.ld_knots
652
+ pso = self.ps
653
+ deo = self._de_population
654
+ mco = self._mc_chains
655
+ slo = self._sl_ld
656
+ ndo = self.ndim
657
+
630
658
  xn = self.ld_knots = sort(knot_wavelengths)
631
659
  self.nldc = self.ld_knots.size
632
660
 
633
- pvpo = self.de.population.copy() if self.de is not None else None
634
- pso = self.ps
635
- sldo = self._sl_ld
636
661
  self._init_parameters()
637
662
  psn = self.ps
638
- sldn = self._sl_ld
663
+ sln = self._sl_ld
664
+ ndn = self.ndim
665
+
666
+ # Check if we have spots
667
+ # ----------------------
668
+ if self.spot_model is not None:
669
+ spots = self.spot_model
670
+ self.initialize_spots(spots.tphot, spots.wlref, spots.include_tlse)
671
+ for eg in spots.spot_epoch_groups:
672
+ self.spot_model.add_spot(eg)
673
+
674
+ # Set the priors back as they were
675
+ # --------------------------------
639
676
  for po in pso:
640
677
  if po.name in psn.names:
641
678
  self.set_prior(po.name, po.prior)
642
679
 
643
- if self.de is not None:
644
- pvpn = self.create_pv_population(pvpo.shape[0])
680
+ # Resample the DE parameter population
681
+ # ------------------------------------
682
+ if self._de_population is not None:
683
+ den = zeros((deo.shape[0], ndn))
684
+
645
685
  # Copy the old parameter values
646
686
  # -----------------------------
647
687
  for pid_old, p in enumerate(pso):
648
- if p.name in psn:
688
+ if p.name in psn.names:
689
+ pid_new = psn.find_pid(p.name)
690
+ den[:, pid_new] = deo[:, pid_old]
691
+
692
+ # Resample the limb darkening coefficients
693
+ # ----------------------------------------
694
+ for i in range(den.shape[0]):
695
+ den[i, sln][0::2] = self._ip_ld(xn, xo, deo[i, slo][0::2])
696
+ den[i, sln][1::2] = self._ip_ld(xn, xo, deo[i, slo][1::2])
697
+
698
+ self._de_population = den
699
+ self.de = None
700
+
701
+ # Resample the MCMC parameter population
702
+ # --------------------------------------
703
+ if self._mc_chains is not None:
704
+ fmco = mco.reshape([-1, ndo])
705
+ fmcn = zeros((fmco.shape[0], ndn))
706
+
707
+ # Copy the old parameter values
708
+ # -----------------------------
709
+ for pid_old, p in enumerate(pso):
710
+ if p.name in psn.names:
649
711
  pid_new = psn.find_pid(p.name)
650
- pvpn[:, pid_new] = pvpo[:, pid_old]
712
+ fmcn[:, pid_new] = fmco[:, pid_old]
651
713
 
652
714
  # Resample the radius ratios
653
715
  # --------------------------
654
- for i in range(pvpn.shape[0]):
655
- pvpn[i, sldn] = self._ip(xn, xo, pvpo[i, sldo])
716
+ for i in range(fmcn.shape[0]):
717
+ fmcn[i, sln][0::2] = self._ip_ld(xn, xo, fmco[i, slo][0::2])
718
+ fmcn[i, sln][1::2] = self._ip_ld(xn, xo, fmco[i, slo][1::2])
656
719
 
657
- self.de = None
658
- self._de_population = pvpn
720
+ self._mc_chains = fmcn.reshape([mco.shape[0], mco.shape[1], ndn])
721
+ self.sampler = None
659
722
 
660
723
  def _eval_k(self, pvp) -> list[ndarray]:
661
724
  """Evaluate the radius ratio model.
@@ -764,15 +827,16 @@ class TSLPF(LogPosteriorFunction):
764
827
  self._baseline_models[i][ipv, :, :] = nan
765
828
  return self._baseline_models
766
829
 
767
- def flux_model(self, pv):
830
+ def flux_model(self, pv, include_baseline: bool = True):
768
831
  transit_models = self.transit_model(pv)
769
- baseline_models = self.baseline_model(transit_models)
770
832
  if self.spot_model is not None:
771
833
  self.spot_model.apply_spots(pv, transit_models)
772
834
  if self.spot_model.include_tlse:
773
835
  self.spot_model.apply_tlse(pv, transit_models)
774
- for i in range(self.data.size):
775
- transit_models[i][:, :, :] *= baseline_models[i][:, :, :]
836
+ if include_baseline:
837
+ baseline_models = self.baseline_model(transit_models)
838
+ for i in range(self.data.size):
839
+ transit_models[i][:, :, :] *= baseline_models[i][:, :, :]
776
840
  return transit_models
777
841
 
778
842
  def create_pv_population(self, npop: int = 50) -> ndarray:
@@ -809,13 +873,22 @@ class TSLPF(LogPosteriorFunction):
809
873
  """
810
874
  pv = atleast_2d(pv)
811
875
  npv = pv.shape[0]
812
- fmod = self.flux_model(pv)
813
876
  wn_multipliers = pv[:, self._sl_wnm]
814
877
  lnl = zeros(npv)
815
- if self._nm == NM_WHITE:
878
+ if self._nm == NM_WHITE_MARGINALIZED:
879
+ fmod = self.flux_model(pv, include_baseline=False)
880
+ for ipv in range(npv):
881
+ try:
882
+ for i, d in enumerate(self.data):
883
+ lnl[ipv] += marginalized_loglike_mbl2d(d.fluxes, fmod[i][ipv], d.errors*wn_multipliers[ipv, d.noise_group], d.covs, d.mask)
884
+ except LinAlgError:
885
+ lnl[ipv] = -inf
886
+ elif self._nm == NM_WHITE_PROFILED:
887
+ fmod = self.flux_model(pv, include_baseline=True)
816
888
  for i, d in enumerate(self.data):
817
889
  lnl += lnlike_normal(d.fluxes, fmod[i], d.errors, wn_multipliers[:, d.noise_group], d.mask)
818
890
  else:
891
+ fmod = self.flux_model(pv)
819
892
  for j in range(npv):
820
893
  if self._nm == NM_GP_FREE:
821
894
  self.set_gp_hyperparameters(*pv[j, self._sl_gp])
@@ -823,7 +896,7 @@ class TSLPF(LogPosteriorFunction):
823
896
  lnl[j] += self._gp[i].log_likelihood(self._gp_flux[i] - fmod[i][j][self.data[i].mask])
824
897
  return lnl if npv > 1 else lnl[0]
825
898
 
826
- def create_initial_population(self, n: int, source: str, add_noise: bool = True) -> ndarray:
899
+ def create_initial_population(self, n: int, source: str, add_noise: bool = False) -> ndarray:
827
900
  """Create an initial parameter vector population for DE.
828
901
 
829
902
  Parameters
@@ -833,7 +906,7 @@ class TSLPF(LogPosteriorFunction):
833
906
  source : str
834
907
  Source of the initial population. Must be either 'fit' or 'mcmc'.
835
908
  add_noise : bool, optional
836
- Flag indicating whether to add noise to the initial population. Default is True.
909
+ Flag indicating whether to add noise to the initial population. Default is False.
837
910
 
838
911
  Returns
839
912
  -------
@@ -863,12 +936,9 @@ class TSLPF(LogPosteriorFunction):
863
936
  else:
864
937
  pvp = rng.choice(pvs.reshape([-1, self.ndim]), size=n)
865
938
 
866
- if pvp[0, self._sl_baseline][0] < 0.5:
867
- pvp[:, self._sl_baseline] = rng.normal(1.0, 1e-6, size=(n, sum(self.n_baselines)))
868
-
869
939
  if add_noise:
870
- pvp[:, self._sl_rratios] += rng.normal(0, 1, pvp[:, self._sl_rratios].shape) * 0.002 * pvp[:, self._sl_rratios]
871
- pvp[:, self._sl_ld] += rng.normal(0, 1, pvp[:, self._sl_ld].shape) * 0.002 * pvp[:, self._sl_ld]
940
+ pvp[:, self._sl_rratios] += rng.normal(0, 1e-4, pvp[:, self._sl_rratios].shape)
941
+ pvp[:, self._sl_ld] += rng.normal(0, 1e-3, pvp[:, self._sl_ld].shape)
872
942
  return pvp
873
943
 
874
944
  def optimize_global(self, niter=200, npop=50, population=None, pool=None, lnpost=None, vectorize=True,
exoiris/util.py CHANGED
@@ -14,79 +14,11 @@
14
14
  # You should have received a copy of the GNU General Public License
15
15
  # along with this program. If not, see <https://www.gnu.org/licenses/>.
16
16
 
17
- from numba import njit
18
- from numpy import (zeros, sum, sqrt, linspace, vstack, concatenate, floor, dot, ndarray, nan, asarray, tile)
17
+ from numpy import (linspace, vstack, concatenate, floor, ndarray, asarray, tile)
19
18
  from numpy._typing import ArrayLike
20
19
  from pytransit import TSModel
21
20
  from pytransit.orbits import i_from_ba
22
21
 
23
- @njit
24
- def bin2d(v, e, el, er, bins, estimate_errors: bool = False) -> tuple[ndarray, ndarray]:
25
- """Bin 2D exoplanet transmission spectrum data with its uncertainties into predefined bins in wavelength.
26
-
27
- Parameters
28
- ----------
29
- v : ndarray
30
- A 2D array of the exoplanet transmission spectrum data with a shape (n_wavelength, n_exposure).
31
- e : ndarray
32
- A 2D array of uncertainties associated with the spectrum data in `v`, matching the shape of `v`.
33
- el : ndarray
34
- A 1D array containing the left wavelength edges of the integration ranges for each spectral data point.
35
- er : ndarray
36
- A 1D array containing the right wavelength edges of the integration ranges for each spectral data point.
37
- bins : ndarray
38
- A 2D array containing the edges of the wavelength bins. These should be sorted in ascending order.
39
- estimate_errors: bool, optional.
40
- Should the uncertainties be estimated from the data. Default value is False.
41
-
42
- Returns
43
- -------
44
- tuple of ndarrays
45
- A tuple containing two 2D arrays:
46
- - The first array (`bv`) contains the binned values of the transmission spectrum.
47
- - The second array (`be`) contains the binned uncertainties.
48
- """
49
- nbins = len(bins)
50
- ndata = v.shape[0]
51
- bv = zeros((nbins, v.shape[1]))
52
- be = zeros((nbins, v.shape[1]))
53
- e2 = e**2
54
- weights = zeros(ndata)
55
- i = 0
56
- for ibin in range(nbins):
57
- weights[:] = 0.0
58
- npt = 0
59
- bel, ber = bins[ibin]
60
- for i in range(i, ndata - 1):
61
- if el[i + 1] > bel:
62
- break
63
- il = i
64
- if er[i] > ber:
65
- weights[i] = ber - bel
66
- npt += 1
67
- else:
68
- weights[i] = er[i] - bel
69
- npt += 1
70
- for i in range(i + 1, ndata):
71
- if er[i] < ber:
72
- weights[i] = er[i] - el[i]
73
- npt += 1
74
- else:
75
- weights[i] = ber - el[i]
76
- npt += 1
77
- break
78
- ir = i
79
- ws = sum(weights)
80
- bv[ibin] = vmean = dot(weights[il:ir+1], v[il:ir+1,:]) / ws
81
- if estimate_errors:
82
- if npt > 1:
83
- be[ibin] = sqrt(dot(weights[il:ir+1], (v[il:ir+1,:] - vmean)**2) / ws) / sqrt(npt)
84
- else:
85
- be[ibin] = nan
86
- else:
87
- be[ibin] = sqrt(dot(weights[il:ir+1], e2[il:ir+1,:])) / ws
88
- return bv, be
89
-
90
22
 
91
23
  def create_binning(ranges, bwidths):
92
24
  """