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.
- exoiris/bin1d.py +97 -0
- exoiris/bin2d.py +218 -0
- exoiris/exoiris.py +30 -42
- exoiris/lmlikelihood.py +172 -0
- exoiris/prtretrieval.py +164 -0
- exoiris/spotmodel.py +4 -4
- exoiris/tsdata.py +85 -5
- exoiris/tslpf.py +103 -33
- exoiris/util.py +1 -69
- {exoiris-0.23.2.dist-info → exoiris-1.1.0.dist-info}/METADATA +1 -1
- exoiris-1.1.0.dist-info/RECORD +21 -0
- {exoiris-0.23.2.dist-info → exoiris-1.1.0.dist-info}/WHEEL +1 -1
- exoiris-0.23.2.dist-info/RECORD +0 -17
- {exoiris-0.23.2.dist-info → exoiris-1.1.0.dist-info}/licenses/LICENSE +0 -0
- {exoiris-0.23.2.dist-info → exoiris-1.1.0.dist-info}/top_level.txt +0 -0
exoiris/prtretrieval.py
ADDED
|
@@ -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.
|
|
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 =
|
|
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 =
|
|
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 .
|
|
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,
|
|
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 =
|
|
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 =
|
|
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, _ =
|
|
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
|
-
|
|
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=
|
|
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
|
-
|
|
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["
|
|
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[
|
|
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:
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
644
|
-
|
|
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
|
-
|
|
712
|
+
fmcn[:, pid_new] = fmco[:, pid_old]
|
|
651
713
|
|
|
652
714
|
# Resample the radius ratios
|
|
653
715
|
# --------------------------
|
|
654
|
-
for i in range(
|
|
655
|
-
|
|
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.
|
|
658
|
-
self.
|
|
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
|
-
|
|
775
|
-
|
|
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 ==
|
|
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 =
|
|
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
|
|
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,
|
|
871
|
-
pvp[:, self._sl_ld] += rng.normal(0,
|
|
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
|
|
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
|
"""
|