redback 1.0.31__py3-none-any.whl → 1.12.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.
- redback/__init__.py +3 -2
- redback/analysis.py +321 -4
- redback/filters.py +57 -23
- redback/get_data/directory.py +18 -0
- redback/likelihoods.py +260 -0
- redback/model_library.py +12 -2
- redback/plotting.py +335 -4
- redback/priors/blackbody_spectrum_with_absorption_and_emission_lines.prior +9 -0
- redback/priors/csm_shock_and_arnett_two_rphots.prior +11 -0
- redback/priors/exp_rise_powerlaw_decline.prior +6 -0
- redback/priors/powerlaw_spectrum_with_absorption_and_emission_lines.prior +8 -0
- redback/priors/salt2.prior +6 -0
- redback/priors/shock_cooling_and_arnett_bolometric.prior +11 -0
- redback/priors/shockcooling_morag.prior +6 -0
- redback/priors/shockcooling_morag_and_arnett.prior +10 -0
- redback/priors/shockcooling_morag_and_arnett_bolometric.prior +9 -0
- redback/priors/shockcooling_morag_bolometric.prior +5 -0
- redback/priors/shockcooling_sapirandwaxman.prior +6 -0
- redback/priors/shockcooling_sapirandwaxman_bolometric.prior +5 -0
- redback/priors/shockcooling_sapirwaxman_and_arnett.prior +10 -0
- redback/priors/shockcooling_sapirwaxman_and_arnett_bolometric.prior +9 -0
- redback/priors/shocked_cocoon_and_arnett.prior +13 -0
- redback/priors/synchrotron_ism.prior +6 -0
- redback/priors/synchrotron_massloss.prior +6 -0
- redback/priors/synchrotron_pldensity.prior +7 -0
- redback/priors/thermal_synchrotron_v2_fluxdensity.prior +8 -0
- redback/priors/thermal_synchrotron_v2_lnu.prior +7 -0
- redback/priors.py +10 -3
- redback/result.py +9 -1
- redback/sampler.py +46 -4
- redback/sed.py +48 -1
- redback/simulate_transients.py +5 -1
- redback/tables/filters.csv +265 -254
- redback/transient/__init__.py +2 -3
- redback/transient/transient.py +648 -10
- redback/transient_models/__init__.py +3 -2
- redback/transient_models/extinction_models.py +3 -2
- redback/transient_models/gaussianprocess_models.py +45 -0
- redback/transient_models/general_synchrotron_models.py +296 -6
- redback/transient_models/phenomenological_models.py +154 -7
- redback/transient_models/shock_powered_models.py +503 -40
- redback/transient_models/spectral_models.py +82 -0
- redback/transient_models/supernova_models.py +405 -31
- redback/transient_models/tde_models.py +57 -41
- redback/utils.py +302 -51
- {redback-1.0.31.dist-info → redback-1.12.0.dist-info}/METADATA +8 -6
- {redback-1.0.31.dist-info → redback-1.12.0.dist-info}/RECORD +50 -29
- {redback-1.0.31.dist-info → redback-1.12.0.dist-info}/WHEEL +1 -1
- {redback-1.0.31.dist-info → redback-1.12.0.dist-info/licenses}/LICENCE.md +0 -0
- {redback-1.0.31.dist-info → redback-1.12.0.dist-info}/top_level.txt +0 -0
redback/transient/transient.py
CHANGED
|
@@ -8,8 +8,119 @@ import pandas as pd
|
|
|
8
8
|
|
|
9
9
|
import redback
|
|
10
10
|
from redback.plotting import \
|
|
11
|
-
LuminosityPlotter, FluxDensityPlotter, IntegratedFluxPlotter, MagnitudePlotter,
|
|
11
|
+
LuminosityPlotter, FluxDensityPlotter, IntegratedFluxPlotter, MagnitudePlotter, \
|
|
12
|
+
IntegratedFluxOpticalPlotter, SpectrumPlotter, LuminosityOpticalPlotter
|
|
13
|
+
from redback.model_library import all_models_dict
|
|
14
|
+
from collections import namedtuple
|
|
15
|
+
|
|
16
|
+
class Spectrum(object):
|
|
17
|
+
def __init__(self, angstroms: np.ndarray, flux_density: np.ndarray, flux_density_err: np.ndarray,
|
|
18
|
+
time: str = None, name: str = '', **kwargs) -> None:
|
|
19
|
+
"""
|
|
20
|
+
A class to store spectral data.
|
|
21
|
+
|
|
22
|
+
:param angstroms: Wavelength in angstroms.
|
|
23
|
+
:param flux_density: flux density in ergs/s/cm^2/angstrom.
|
|
24
|
+
:param flux_density_err: flux density error in ergs/s/cm^2/angstrom.
|
|
25
|
+
:param time: Time of the spectrum. Could be a phase or time since burst. Only used for plotting.
|
|
26
|
+
:param name: Name of the spectrum.
|
|
27
|
+
"""
|
|
28
|
+
|
|
29
|
+
self.angstroms = angstroms
|
|
30
|
+
self.flux_density = flux_density
|
|
31
|
+
self.flux_density_err = flux_density_err
|
|
32
|
+
self.time = time
|
|
33
|
+
self.name = name
|
|
34
|
+
if self.time is None:
|
|
35
|
+
self.plot_with_time_label = False
|
|
36
|
+
else:
|
|
37
|
+
self.plot_with_time_label = True
|
|
38
|
+
self.directory_structure = redback.get_data.directory.spectrum_directory_structure(transient=name)
|
|
39
|
+
self.data_mode = 'spectrum'
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def xlabel(self) -> str:
|
|
43
|
+
"""
|
|
44
|
+
:return: xlabel used in plotting functions
|
|
45
|
+
:rtype: str
|
|
46
|
+
"""
|
|
47
|
+
return r'Wavelength [$\mathrm{\AA}$]'
|
|
48
|
+
|
|
49
|
+
@property
|
|
50
|
+
def ylabel(self) -> str:
|
|
51
|
+
"""
|
|
52
|
+
:return: ylabel used in plotting functions
|
|
53
|
+
:rtype: str
|
|
54
|
+
"""
|
|
55
|
+
return r'Flux ($10^{-17}$ erg s$^{-1}$ cm$^{-2}$ $\mathrm{\AA}$)'
|
|
56
|
+
|
|
57
|
+
def plot_data(self, axes: matplotlib.axes.Axes = None, filename: str = None, outdir: str = None, save: bool = True,
|
|
58
|
+
show: bool = True, color: str = 'k', **kwargs) -> matplotlib.axes.Axes:
|
|
59
|
+
"""Plots the Transient data and returns Axes.
|
|
60
|
+
|
|
61
|
+
:param axes: Matplotlib axes to plot the lightcurve into. Useful for user specific modifications to the plot.
|
|
62
|
+
:param filename: Name of the file to be plotted in.
|
|
63
|
+
:param outdir: The directory in which to save the file in.
|
|
64
|
+
:param save: Whether to save the plot. (Default value = True)
|
|
65
|
+
:param show: Whether to show the plot. (Default value = True)
|
|
66
|
+
:param color: Color of the data.
|
|
67
|
+
:param kwargs: Additional keyword arguments to pass in the Plotter methods.
|
|
68
|
+
Available in the online documentation under at `redback.plotting.Plotter`.
|
|
69
|
+
`print(Transient.plot_data.__doc__)` to see all options!
|
|
70
|
+
:return: The axes with the plot.
|
|
71
|
+
"""
|
|
12
72
|
|
|
73
|
+
plotter = SpectrumPlotter(spectrum=self, color=color, filename=filename, outdir=outdir, **kwargs)
|
|
74
|
+
return plotter.plot_data(axes=axes, save=save, show=show)
|
|
75
|
+
|
|
76
|
+
def plot_spectrum(
|
|
77
|
+
self, model: callable, filename: str = None, outdir: str = None, axes: matplotlib.axes.Axes = None,
|
|
78
|
+
save: bool = True, show: bool = True, random_models: int = 100, posterior: pd.DataFrame = None,
|
|
79
|
+
model_kwargs: dict = None, **kwargs: None) -> matplotlib.axes.Axes:
|
|
80
|
+
"""
|
|
81
|
+
:param model: The model used to plot the lightcurve.
|
|
82
|
+
:param filename: The output filename. Otherwise, use default which starts with the name
|
|
83
|
+
attribute and ends with *lightcurve.png.
|
|
84
|
+
:param axes: Axes to plot in if given.
|
|
85
|
+
:param save:Whether to save the plot.
|
|
86
|
+
:param show: Whether to show the plot.
|
|
87
|
+
:param random_models: Number of random posterior samples plotted faintly. (Default value = 100)
|
|
88
|
+
:param posterior: Posterior distribution to which to draw samples from. Is optional but must be given.
|
|
89
|
+
:param outdir: Out directory in which to save the plot. Default is the current working directory.
|
|
90
|
+
:param model_kwargs: Additional keyword arguments to be passed into the model.
|
|
91
|
+
:param kwargs: Additional keyword arguments to pass in the Plotter methods.
|
|
92
|
+
Available in the online documentation under at `redback.plotting.Plotter`.
|
|
93
|
+
`print(Transient.plot_lightcurve.__doc__)` to see all options!
|
|
94
|
+
:return: The axes.
|
|
95
|
+
"""
|
|
96
|
+
plotter = SpectrumPlotter(
|
|
97
|
+
spectrum=self, model=model, filename=filename, outdir=outdir,
|
|
98
|
+
posterior=posterior, model_kwargs=model_kwargs, random_models=random_models, **kwargs)
|
|
99
|
+
return plotter.plot_spectrum(axes=axes, save=save, show=show)
|
|
100
|
+
|
|
101
|
+
def plot_residual(self, model: callable, filename: str = None, outdir: str = None, axes: matplotlib.axes.Axes = None,
|
|
102
|
+
save: bool = True, show: bool = True, posterior: pd.DataFrame = None,
|
|
103
|
+
model_kwargs: dict = None, **kwargs: None) -> matplotlib.axes.Axes:
|
|
104
|
+
"""
|
|
105
|
+
:param model: The model used to plot the lightcurve.
|
|
106
|
+
:param filename: The output filename. Otherwise, use default which starts with the name
|
|
107
|
+
attribute and ends with *lightcurve.png.
|
|
108
|
+
:param axes: Axes to plot in if given.
|
|
109
|
+
:param save:Whether to save the plot.
|
|
110
|
+
:param show: Whether to show the plot.
|
|
111
|
+
:param posterior: Posterior distribution to which to draw samples from. Is optional but must be given.
|
|
112
|
+
:param outdir: Out directory in which to save the plot. Default is the current working directory.
|
|
113
|
+
:param model_kwargs: Additional keyword arguments to be passed into the model.
|
|
114
|
+
:param kwargs: Additional keyword arguments to pass in the Plotter methods.
|
|
115
|
+
Available in the online documentation under at `redback.plotting.Plotter`.
|
|
116
|
+
`print(Transient.plot_residual.__doc__)` to see all options!
|
|
117
|
+
:return: The axes.
|
|
118
|
+
"""
|
|
119
|
+
plotter = SpectrumPlotter(
|
|
120
|
+
spectrum=self, model=model, filename=filename, outdir=outdir,
|
|
121
|
+
posterior=posterior, model_kwargs=model_kwargs, **kwargs)
|
|
122
|
+
return plotter.plot_residuals(axes=axes, save=save, show=show)
|
|
123
|
+
LuminosityPlotter, FluxDensityPlotter, IntegratedFluxPlotter, MagnitudePlotter, IntegratedFluxOpticalPlotter
|
|
13
124
|
|
|
14
125
|
class Transient(object):
|
|
15
126
|
DATA_MODES = ['luminosity', 'flux', 'flux_density', 'magnitude', 'counts', 'ttes']
|
|
@@ -164,6 +275,7 @@ class Transient(object):
|
|
|
164
275
|
:return: Six elements when querying magnitude or flux_density data, Eight for 'all'.
|
|
165
276
|
:rtype: tuple
|
|
166
277
|
"""
|
|
278
|
+
DATA_MODES = ['luminosity', 'flux', 'flux_density', 'magnitude', 'counts', 'ttes', 'all']
|
|
167
279
|
df = pd.read_csv(processed_file_path)
|
|
168
280
|
time_days = np.array(df["time (days)"])
|
|
169
281
|
time_mjd = np.array(df["time"])
|
|
@@ -172,6 +284,8 @@ class Transient(object):
|
|
|
172
284
|
bands = np.array(df["band"])
|
|
173
285
|
flux_density = np.array(df["flux_density(mjy)"])
|
|
174
286
|
flux_density_err = np.array(df["flux_density_error"])
|
|
287
|
+
if data_mode not in DATA_MODES:
|
|
288
|
+
raise ValueError(f"Data mode {data_mode} not in {DATA_MODES}")
|
|
175
289
|
if data_mode == "magnitude":
|
|
176
290
|
return time_days, time_mjd, magnitude, magnitude_err, bands
|
|
177
291
|
elif data_mode == "flux_density":
|
|
@@ -372,7 +486,7 @@ class Transient(object):
|
|
|
372
486
|
if self.use_phase_model:
|
|
373
487
|
return r"Time [MJD]"
|
|
374
488
|
else:
|
|
375
|
-
return r"Time since
|
|
489
|
+
return r"Time since explosion [days]"
|
|
376
490
|
|
|
377
491
|
@property
|
|
378
492
|
def ylabel(self) -> str:
|
|
@@ -576,7 +690,11 @@ class Transient(object):
|
|
|
576
690
|
else:
|
|
577
691
|
plotter = IntegratedFluxPlotter(transient=self, color=color, filename=filename, outdir=outdir, **kwargs)
|
|
578
692
|
elif self.luminosity_data:
|
|
579
|
-
|
|
693
|
+
if self.optical_data:
|
|
694
|
+
plotter = LuminosityOpticalPlotter(transient=self, color=color, filename=filename, outdir=outdir,
|
|
695
|
+
**kwargs)
|
|
696
|
+
else:
|
|
697
|
+
plotter = LuminosityPlotter(transient=self, color=color, filename=filename, outdir=outdir, **kwargs)
|
|
580
698
|
elif self.flux_density_data:
|
|
581
699
|
plotter = FluxDensityPlotter(transient=self, color=color, filename=filename, outdir=outdir,
|
|
582
700
|
plot_others=plot_others, **kwargs)
|
|
@@ -655,9 +773,13 @@ class Transient(object):
|
|
|
655
773
|
transient=self, model=model, filename=filename, outdir=outdir,
|
|
656
774
|
posterior=posterior, model_kwargs=model_kwargs, random_models=random_models, **kwargs)
|
|
657
775
|
elif self.luminosity_data:
|
|
658
|
-
|
|
659
|
-
transient=self, model=model, filename=filename, outdir=outdir,
|
|
660
|
-
|
|
776
|
+
if self.optical_data:
|
|
777
|
+
plotter = LuminosityOpticalPlotter(transient=self, model=model, filename=filename, outdir=outdir,
|
|
778
|
+
posterior=posterior, model_kwargs=model_kwargs, random_models=random_models, **kwargs)
|
|
779
|
+
else:
|
|
780
|
+
plotter = LuminosityPlotter(
|
|
781
|
+
transient=self, model=model, filename=filename, outdir=outdir,
|
|
782
|
+
posterior=posterior, model_kwargs=model_kwargs, random_models=random_models, **kwargs)
|
|
661
783
|
elif self.flux_density_data:
|
|
662
784
|
plotter = FluxDensityPlotter(
|
|
663
785
|
transient=self, model=model, filename=filename, outdir=outdir,
|
|
@@ -693,13 +815,145 @@ class Transient(object):
|
|
|
693
815
|
transient=self, model=model, filename=filename, outdir=outdir,
|
|
694
816
|
posterior=posterior, model_kwargs=model_kwargs, **kwargs)
|
|
695
817
|
elif self.luminosity_data:
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
818
|
+
if self.optical_data:
|
|
819
|
+
plotter = LuminosityOpticalPlotter(
|
|
820
|
+
transient=self, model=model, filename=filename, outdir=outdir,
|
|
821
|
+
posterior=posterior, model_kwargs=model_kwargs, **kwargs)
|
|
822
|
+
else:
|
|
823
|
+
plotter = LuminosityPlotter(
|
|
824
|
+
transient=self, model=model, filename=filename, outdir=outdir,
|
|
825
|
+
posterior=posterior, model_kwargs=model_kwargs, **kwargs)
|
|
699
826
|
else:
|
|
700
827
|
raise ValueError("Residual plotting not implemented for this data mode")
|
|
701
828
|
return plotter.plot_residuals(axes=axes, save=save, show=show)
|
|
702
829
|
|
|
830
|
+
|
|
831
|
+
def fit_gp(self, mean_model, kernel, prior=None, use_frequency=True):
|
|
832
|
+
"""
|
|
833
|
+
Fit a GP to the data using george and scipy minimization.
|
|
834
|
+
|
|
835
|
+
:param mean_model: Mean model to use in the GP fit. Can be a string to refer to a redback model, a callable, or None
|
|
836
|
+
:param kernel: George GP to use. User must ensure this is set up correctly.
|
|
837
|
+
:param prior: Prior to use when fitting with a mean model.
|
|
838
|
+
:param use_frequency: Whether to use the effective frequency in a 2D GP fit. Cannot be used with most mean models.
|
|
839
|
+
:return: Named tuple with George GP object and additional useful data.
|
|
840
|
+
"""
|
|
841
|
+
try:
|
|
842
|
+
import george
|
|
843
|
+
import george.kernels as kernels
|
|
844
|
+
except ImportError:
|
|
845
|
+
redback.utils.logger.warning("George must be installed to use GP fitting.")
|
|
846
|
+
import scipy.optimize as op
|
|
847
|
+
from bilby.core.likelihood import function_to_george_mean_model
|
|
848
|
+
|
|
849
|
+
output = namedtuple("gp_out", ["gp", "scaled_y", "y_scaler", 'use_frequency', 'mean_model'])
|
|
850
|
+
output.use_frequency = use_frequency
|
|
851
|
+
output.mean_model = mean_model
|
|
852
|
+
|
|
853
|
+
if self.data_mode == 'luminosity':
|
|
854
|
+
x = self.time_rest_frame
|
|
855
|
+
y = self.y
|
|
856
|
+
try:
|
|
857
|
+
y_err = np.max(self.y_err, axis=0)
|
|
858
|
+
except IndexError:
|
|
859
|
+
y_err = self.y_err
|
|
860
|
+
else:
|
|
861
|
+
x, x_err, y, y_err = self.get_filtered_data()
|
|
862
|
+
redback.utils.logger.info("Rescaling data for GP fitting.")
|
|
863
|
+
gp_y_err = y_err / np.max(y)
|
|
864
|
+
gp_y = y / np.max(y)
|
|
865
|
+
output.scaled_y = gp_y
|
|
866
|
+
output.y_scaler = np.max(y)
|
|
867
|
+
|
|
868
|
+
def nll(p):
|
|
869
|
+
gp.set_parameter_vector(p)
|
|
870
|
+
ll = gp.log_likelihood(gp_y, quiet=True)
|
|
871
|
+
return -ll if np.isfinite(ll) else 1e25
|
|
872
|
+
|
|
873
|
+
def grad_nll(p):
|
|
874
|
+
gp.set_parameter_vector(p)
|
|
875
|
+
return -gp.grad_log_likelihood(gp_y, quiet=True)
|
|
876
|
+
|
|
877
|
+
if use_frequency:
|
|
878
|
+
redback.utils.logger.info("Using frequencies and time in the GP fit.")
|
|
879
|
+
redback.utils.logger.info("Kernel used: " + str(kernel))
|
|
880
|
+
redback.utils.logger.info("Ensure that the kernel is set up correctly for 2D GP.")
|
|
881
|
+
redback.utils.logger.info("You will be returned a single GP object with frequency as a parameter")
|
|
882
|
+
freqs = self.filtered_frequencies
|
|
883
|
+
X = np.column_stack((freqs, x))
|
|
884
|
+
else:
|
|
885
|
+
redback.utils.logger.info("Using time in GP fit.")
|
|
886
|
+
redback.utils.logger.info("Kernel used: " + str(kernel))
|
|
887
|
+
redback.utils.logger.info("Ensure that the kernel is set up correctly for 1D GP.")
|
|
888
|
+
redback.utils.logger.info("You will be returned a GP object unique to a band/frequency"
|
|
889
|
+
" in the data if working with multiband data")
|
|
890
|
+
X = x
|
|
891
|
+
|
|
892
|
+
if mean_model is None:
|
|
893
|
+
redback.utils.logger.info("Mean model not given, fitting GP with no mean model.")
|
|
894
|
+
gp = george.GP(kernel)
|
|
895
|
+
gp.compute(X, gp_y_err + 1e-8)
|
|
896
|
+
p0 = gp.get_parameter_vector()
|
|
897
|
+
results = op.minimize(nll, p0, jac=grad_nll)
|
|
898
|
+
gp.set_parameter_vector(results.x)
|
|
899
|
+
redback.utils.logger.info(f"GP final loglikelihood: {gp.log_likelihood(gp_y)}")
|
|
900
|
+
redback.utils.logger.info(f"GP final parameters: {gp.get_parameter_dict()}")
|
|
901
|
+
output.gp = gp
|
|
902
|
+
else:
|
|
903
|
+
if isinstance(mean_model, str):
|
|
904
|
+
mean_model_func = all_models_dict[mean_model]
|
|
905
|
+
redback.utils.logger.info("Using inbuilt redback function {} as a mean model.".format(mean_model))
|
|
906
|
+
if prior is None:
|
|
907
|
+
redback.utils.logger.warning("No prior given for mean model. Using default prior.")
|
|
908
|
+
prior = redback.priors.get_priors(mean_model)
|
|
909
|
+
else:
|
|
910
|
+
mean_model_func = mean_model
|
|
911
|
+
redback.utils.logger.info("Using user-defined python function as a mean model.")
|
|
912
|
+
|
|
913
|
+
if prior is None:
|
|
914
|
+
redback.utils.logger.warning("Prior must be specified for GP fit with a mean model")
|
|
915
|
+
raise ValueError("No prior specified")
|
|
916
|
+
|
|
917
|
+
if self.data_mode in ['flux_density', 'magnitude', 'flux']:
|
|
918
|
+
redback.utils.logger.info("Setting up GP version of mean model.")
|
|
919
|
+
gp_dict = {}
|
|
920
|
+
scaled_y_dict = {}
|
|
921
|
+
for ii in range(len(self.unique_bands)):
|
|
922
|
+
scaled_y_dict[self.unique_bands[ii]] = gp_y[self.list_of_band_indices[ii]]
|
|
923
|
+
redback.utils.logger.info("Fitting for band {}".format(self.unique_bands[ii]))
|
|
924
|
+
gp_x = X[self.list_of_band_indices[ii]]
|
|
925
|
+
|
|
926
|
+
def nll(p):
|
|
927
|
+
gp.set_parameter_vector(p)
|
|
928
|
+
ll = gp.log_likelihood(gp_y[self.list_of_band_indices[ii]], quiet=True)
|
|
929
|
+
return -ll if np.isfinite(ll) else 1e25
|
|
930
|
+
|
|
931
|
+
mean_model_class = function_to_george_mean_model(mean_model_func)
|
|
932
|
+
mm = mean_model_class(**prior.sample())
|
|
933
|
+
gp = george.GP(kernel, mean=mm, fit_mean=True)
|
|
934
|
+
gp.compute(gp_x, gp_y_err[self.list_of_band_indices[ii]] + 1e-8)
|
|
935
|
+
p0 = gp.get_parameter_vector()
|
|
936
|
+
results = op.minimize(nll, p0)
|
|
937
|
+
gp.set_parameter_vector(results.x)
|
|
938
|
+
redback.utils.logger.info(f"GP final loglikelihood: {gp.log_likelihood(gp_y[self.list_of_band_indices[ii]])}")
|
|
939
|
+
redback.utils.logger.info(f"GP final parameters: {gp.get_parameter_dict()}")
|
|
940
|
+
gp_dict[self.unique_bands[ii]] = gp
|
|
941
|
+
del gp
|
|
942
|
+
output.gp = gp_dict
|
|
943
|
+
output.scaled_y = scaled_y_dict
|
|
944
|
+
else:
|
|
945
|
+
mean_model_class = function_to_george_mean_model(mean_model_func)
|
|
946
|
+
mm = mean_model_class(**prior.sample())
|
|
947
|
+
gp = george.GP(kernel, mean=mm, fit_mean=True)
|
|
948
|
+
gp.compute(X, gp_y_err + 1e-8)
|
|
949
|
+
p0 = gp.get_parameter_vector()
|
|
950
|
+
results = op.minimize(nll, p0)
|
|
951
|
+
gp.set_parameter_vector(results.x)
|
|
952
|
+
redback.utils.logger.info(f"GP final loglikelihood: {gp.log_likelihood(gp_y)}")
|
|
953
|
+
redback.utils.logger.info(f"GP final parameters: {gp.get_parameter_dict()}")
|
|
954
|
+
output.gp = gp
|
|
955
|
+
return output
|
|
956
|
+
|
|
703
957
|
def plot_multiband_lightcurve(
|
|
704
958
|
self, model: callable, filename: str = None, outdir: str = None,
|
|
705
959
|
figure: matplotlib.figure.Figure = None, axes: matplotlib.axes.Axes = None,
|
|
@@ -871,7 +1125,8 @@ class OpticalTransient(Transient):
|
|
|
871
1125
|
use_phase_model=use_phase_model, optical_data=optical_data,
|
|
872
1126
|
system=system, bands=bands, plotting_order=plotting_order,
|
|
873
1127
|
active_bands=active_bands, **kwargs)
|
|
874
|
-
self.directory_structure =
|
|
1128
|
+
self.directory_structure = redback.get_data.directory.DirectoryStructure(
|
|
1129
|
+
directory_path=".", raw_file_path=".", processed_file_path=".")
|
|
875
1130
|
|
|
876
1131
|
@classmethod
|
|
877
1132
|
def from_open_access_catalogue(
|
|
@@ -944,3 +1199,386 @@ class OpticalTransient(Transient):
|
|
|
944
1199
|
transient_dir, _, _ = redback.get_data.directory.open_access_directory_structure(
|
|
945
1200
|
transient=self.name, transient_type=self.__class__.__name__.lower())
|
|
946
1201
|
return transient_dir
|
|
1202
|
+
|
|
1203
|
+
def estimate_bb_params(self, distance: float = 1e27, bin_width: float = 1.0, min_filters: int = 3, **kwargs):
|
|
1204
|
+
"""
|
|
1205
|
+
Estimate the blackbody temperature and photospheric radius as functions of time by fitting
|
|
1206
|
+
a blackbody SED to the multi‑band photometry.
|
|
1207
|
+
|
|
1208
|
+
The method groups the photometric data into time bins (epochs) of width bin_width (in the
|
|
1209
|
+
same units as self.x, typically days). For each epoch with at least min_filters measurements
|
|
1210
|
+
(from distinct filters), it fits a blackbody model to the data. When working with photometry
|
|
1211
|
+
provided in an effective flux density format (data_mode == "flux_density") the effective–wavelength
|
|
1212
|
+
approximation is used. When the data_mode is "flux" (or "magnitude") users have the option
|
|
1213
|
+
(via use_eff_wavelength=True) to instead use the effective wavelength approximation by converting AB
|
|
1214
|
+
magnitudes to flux density (using redback.utils.calc_flux_density_from_ABmag). If this flag is not
|
|
1215
|
+
provided (or is False) then the full bandpass integration is applied.
|
|
1216
|
+
|
|
1217
|
+
Parameters
|
|
1218
|
+
----------
|
|
1219
|
+
distance : float, optional
|
|
1220
|
+
Distance to the transient in centimeters. Default is 1e27 cm.
|
|
1221
|
+
bin_width : float, optional
|
|
1222
|
+
Width of the time bins (in days) used to group the photometric data. Default is 1.0.
|
|
1223
|
+
min_filters : int, optional
|
|
1224
|
+
Minimum number of measurements (from distinct filters) required in a bin to perform the fit.
|
|
1225
|
+
Default is 3.
|
|
1226
|
+
kwargs : Additional keyword arguments
|
|
1227
|
+
maxfev : int, optional, default is 1000
|
|
1228
|
+
T_init : float, optional, default is 1e4, used as the initial guess for the fit.
|
|
1229
|
+
R_init : float, optional, default is 1e15, used as the initial guess for the fit.
|
|
1230
|
+
use_eff_wavelength : bool, optional, default is False.
|
|
1231
|
+
If True, then even for photometry provided as magnitudes (or bandpass fluxes),
|
|
1232
|
+
the effective wavelength approximation is used. In that case the AB magnitudes are
|
|
1233
|
+
converted to flux densities via redback.utils.calc_flux_density_from_ABmag.
|
|
1234
|
+
If False, full bandpass integration is used.
|
|
1235
|
+
|
|
1236
|
+
Returns
|
|
1237
|
+
-------
|
|
1238
|
+
df_bb : pandas.DataFrame or None
|
|
1239
|
+
A DataFrame containing columns:
|
|
1240
|
+
- epoch_times : binned epoch times,
|
|
1241
|
+
- temperature : best-fit blackbody temperatures (Kelvin),
|
|
1242
|
+
- radius : best-fit photospheric radii (cm),
|
|
1243
|
+
- temp_err : 1σ uncertainties on the temperatures,
|
|
1244
|
+
- radius_err : 1σ uncertainties on the radii.
|
|
1245
|
+
Returns None if insufficient data are available.
|
|
1246
|
+
"""
|
|
1247
|
+
from scipy.optimize import curve_fit
|
|
1248
|
+
import astropy.units as uu
|
|
1249
|
+
import numpy as np
|
|
1250
|
+
import pandas as pd
|
|
1251
|
+
|
|
1252
|
+
# Get the filtered photometry.
|
|
1253
|
+
# Assumes self.get_filtered_data() returns (time, time_err, y, y_err)
|
|
1254
|
+
time_data, _, flux_data, flux_err_data = self.get_filtered_data()
|
|
1255
|
+
|
|
1256
|
+
redback.utils.logger.info("Estimating blackbody parameters for {}.".format(self.name))
|
|
1257
|
+
redback.utils.logger.info("Using data mode = {}".format(self.data_mode))
|
|
1258
|
+
|
|
1259
|
+
# Determine whether we are in bandpass mode.
|
|
1260
|
+
use_bandpass = False
|
|
1261
|
+
if hasattr(self, "data_mode") and self.data_mode in ['flux', 'magnitude']:
|
|
1262
|
+
use_bandpass = True
|
|
1263
|
+
# Assume self.filtered_sncosmo_bands contains the (string) band names.
|
|
1264
|
+
band_data = self.filtered_sncosmo_bands
|
|
1265
|
+
else:
|
|
1266
|
+
# Otherwise the flux data and frequencies are assumed to be given.
|
|
1267
|
+
redback.utils.logger.info("Using effective wavelength approximation for {}".format(self.data_mode))
|
|
1268
|
+
freq_data = self.filtered_frequencies
|
|
1269
|
+
|
|
1270
|
+
# Option: force effective wavelength approximation even if data_mode is bandpass.
|
|
1271
|
+
force_eff = kwargs.get('use_eff_wavelength', False)
|
|
1272
|
+
if use_bandpass and force_eff:
|
|
1273
|
+
redback.utils.logger.warning("Using effective wavelength approximation for {}".format(self.data_mode))
|
|
1274
|
+
|
|
1275
|
+
if self.data_mode == 'magnitude':
|
|
1276
|
+
# Convert the AB magnitudes to flux density using the redback function.
|
|
1277
|
+
from redback.utils import abmag_to_flux_density_and_error_inmjy
|
|
1278
|
+
flux_data, flux_err_data = abmag_to_flux_density_and_error_inmjy(flux_data, flux_err_data)
|
|
1279
|
+
freq_data = redback.utils.bands_to_frequency(band_data)
|
|
1280
|
+
else:
|
|
1281
|
+
# Convert the bandpass fluxes to flux density using the redback function.
|
|
1282
|
+
from redback.utils import bandpass_flux_to_flux_density, bands_to_effective_width
|
|
1283
|
+
redback.utils.logger.warning("Ensure filters.csv has the correct bandpass effective widths for your filter.")
|
|
1284
|
+
effective_widths = bands_to_effective_width(band_data)
|
|
1285
|
+
freq_data = redback.utils.bands_to_frequency(band_data)
|
|
1286
|
+
flux_data, flux_err_data = bandpass_flux_to_flux_density(flux_data, flux_err_data, effective_widths)
|
|
1287
|
+
# Use the effective frequency approach.
|
|
1288
|
+
use_bandpass = False
|
|
1289
|
+
|
|
1290
|
+
# Get initial guesses.
|
|
1291
|
+
T_init = kwargs.get('T_init', 1e4)
|
|
1292
|
+
R_init = kwargs.get('R_init', 1e15)
|
|
1293
|
+
maxfev = kwargs.get('maxfev', 1000)
|
|
1294
|
+
|
|
1295
|
+
# Sort photometric data by time.
|
|
1296
|
+
sort_idx = np.argsort(time_data)
|
|
1297
|
+
time_data = time_data[sort_idx]
|
|
1298
|
+
flux_data = flux_data[sort_idx]
|
|
1299
|
+
flux_err_data = flux_err_data[sort_idx]
|
|
1300
|
+
if use_bandpass:
|
|
1301
|
+
band_data = np.array(band_data)[sort_idx]
|
|
1302
|
+
else:
|
|
1303
|
+
freq_data = np.array(freq_data)[sort_idx]
|
|
1304
|
+
|
|
1305
|
+
# Retrieve redshift.
|
|
1306
|
+
redshift = np.nan_to_num(self.redshift)
|
|
1307
|
+
if redshift <= 0.:
|
|
1308
|
+
raise ValueError("Redshift must be provided to perform K-correction.")
|
|
1309
|
+
|
|
1310
|
+
# For effective frequency mode, K-correct frequencies.
|
|
1311
|
+
if not use_bandpass:
|
|
1312
|
+
freq_data, _ = redback.utils.calc_kcorrected_properties(frequency=freq_data,
|
|
1313
|
+
redshift=redshift, time=0.)
|
|
1314
|
+
|
|
1315
|
+
# Define the model functions.
|
|
1316
|
+
if not use_bandpass:
|
|
1317
|
+
# --- Effective-wavelength model ---
|
|
1318
|
+
def bb_model(freq, logT, logR):
|
|
1319
|
+
T = 10 ** logT
|
|
1320
|
+
R = 10 ** logR
|
|
1321
|
+
# Compute the model flux density in erg/s/cm^2/Hz.
|
|
1322
|
+
model_flux_cgs = redback.sed.blackbody_to_flux_density(T, R, distance, freq)
|
|
1323
|
+
# Convert to mJy. (1 Jy = 1e-23 erg/s/cm^2/Hz; 1 mJy = 1e-3 Jy = 1e-26 erg/s/cm^2/Hz)
|
|
1324
|
+
model_flux_mjy = (model_flux_cgs / (1e-26 * uu.erg / uu.s / uu.cm**2 / uu.Hz)).value
|
|
1325
|
+
return model_flux_mjy
|
|
1326
|
+
|
|
1327
|
+
model_func = bb_model
|
|
1328
|
+
else:
|
|
1329
|
+
# --- Full bandpass integration model ---
|
|
1330
|
+
# In this branch we do NOT want to pass strings to curve_fit.
|
|
1331
|
+
# Instead, we will dummy-encode the independent variable as indices.
|
|
1332
|
+
# We also capture the band names in a closure variable.
|
|
1333
|
+
def bb_model_bandpass_from_index(x, logT, logR):
|
|
1334
|
+
# Ensure x is a numpy array and convert indices to integers.
|
|
1335
|
+
i_idx = np.round(x).astype(int)
|
|
1336
|
+
# Retrieve all corresponding band names in one step.
|
|
1337
|
+
bands = np.array(epoch_bands)[i_idx]
|
|
1338
|
+
# Call bb_model_bandpass with the entire array of bands.
|
|
1339
|
+
return bb_model_bandpass(bands, logT, logR, redshift, distance, output_format=self.data_mode)
|
|
1340
|
+
|
|
1341
|
+
def bb_model_bandpass(band, logT, logR, redshift, distance, output_format='magnitude'):
|
|
1342
|
+
from redback.utils import calc_kcorrected_properties, lambda_to_nu, bandpass_magnitude_to_flux
|
|
1343
|
+
# Create a wavelength grid (in Å) from 100 to 80,000 Å.
|
|
1344
|
+
lambda_obs = np.geomspace(100, 80000, 300)
|
|
1345
|
+
# Convert to frequency (Hz) and apply K-correction.
|
|
1346
|
+
frequency, _ = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_obs),
|
|
1347
|
+
redshift=redshift, time=0.)
|
|
1348
|
+
T = 10 ** logT
|
|
1349
|
+
R = 10 ** logR
|
|
1350
|
+
# Compute the model SED (flux density in erg/s/cm^2/Hz).
|
|
1351
|
+
model_flux = redback.sed.blackbody_to_flux_density(T, R, distance, frequency)
|
|
1352
|
+
# Convert the SED to per-Å units.
|
|
1353
|
+
_spectra = model_flux.to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
|
|
1354
|
+
equivalencies=uu.spectral_density(wav=lambda_obs * uu.Angstrom))
|
|
1355
|
+
spectra = np.zeros((5, 300))
|
|
1356
|
+
spectra[:, :] = _spectra.value
|
|
1357
|
+
# Create a source object from the spectrum.
|
|
1358
|
+
source = redback.sed.RedbackTimeSeriesSource(phase=np.array([0, 1, 2, 3, 4]),
|
|
1359
|
+
wave=lambda_obs, flux=spectra)
|
|
1360
|
+
if output_format == 'flux':
|
|
1361
|
+
# Convert bandpass magnitude to flux.
|
|
1362
|
+
mag = source.bandmag(phase=0, band=band, magsys='ab')
|
|
1363
|
+
return bandpass_magnitude_to_flux(magnitude=mag, bands=band)
|
|
1364
|
+
elif output_format == 'magnitude':
|
|
1365
|
+
mag = source.bandmag(phase=0, band=band, magsys='ab')
|
|
1366
|
+
return mag
|
|
1367
|
+
else:
|
|
1368
|
+
raise ValueError("Unknown output_format in bb_model_bandpass.")
|
|
1369
|
+
|
|
1370
|
+
# Our wrapper for curve_fit uses dummy x-values.
|
|
1371
|
+
model_func = bb_model_bandpass_from_index
|
|
1372
|
+
|
|
1373
|
+
# Initialize lists to store fit results.
|
|
1374
|
+
epoch_times = []
|
|
1375
|
+
temperatures = []
|
|
1376
|
+
radii = []
|
|
1377
|
+
temp_errs = []
|
|
1378
|
+
radius_errs = []
|
|
1379
|
+
|
|
1380
|
+
t_min = np.min(time_data)
|
|
1381
|
+
t_max = np.max(time_data)
|
|
1382
|
+
bins = np.arange(t_min, t_max + bin_width, bin_width)
|
|
1383
|
+
redback.utils.logger.info("Number of bins: {}".format(len(bins)))
|
|
1384
|
+
|
|
1385
|
+
# Ensure at least one bin has enough points.
|
|
1386
|
+
bins_with_enough = [i for i in range(len(bins) - 1)
|
|
1387
|
+
if np.sum((time_data >= bins[i]) & (time_data < bins[i + 1])) >= min_filters]
|
|
1388
|
+
if len(bins_with_enough) == 0:
|
|
1389
|
+
redback.utils.logger.warning("No time bins have at least {} measurements. Fitting cannot proceed.".format(min_filters))
|
|
1390
|
+
redback.utils.logger.warning("Try generating more data through GPs, increasing bin widths, or using fewer filters.")
|
|
1391
|
+
return None
|
|
1392
|
+
|
|
1393
|
+
# Loop over bins (epochs): for each with enough data perform the fit.
|
|
1394
|
+
for i in range(len(bins) - 1):
|
|
1395
|
+
mask = (time_data >= bins[i]) & (time_data < bins[i + 1])
|
|
1396
|
+
if np.sum(mask) < min_filters:
|
|
1397
|
+
continue
|
|
1398
|
+
t_epoch = np.mean(time_data[mask])
|
|
1399
|
+
try:
|
|
1400
|
+
if not use_bandpass:
|
|
1401
|
+
# Use effective frequency array (numeric).
|
|
1402
|
+
xdata = freq_data[mask]
|
|
1403
|
+
else:
|
|
1404
|
+
# For full bandpass integration mode, we dummy encode xdata.
|
|
1405
|
+
# We ignore the value and simply use indices [0, 1, 2, ...].
|
|
1406
|
+
epoch_bands = list(band_data[mask]) # capture the list of bands for this epoch
|
|
1407
|
+
xdata = np.arange(len(epoch_bands))
|
|
1408
|
+
popt, pcov = curve_fit(
|
|
1409
|
+
model_func,
|
|
1410
|
+
xdata,
|
|
1411
|
+
flux_data[mask],
|
|
1412
|
+
sigma=flux_err_data[mask],
|
|
1413
|
+
p0=[np.log10(T_init), np.log10(R_init)],
|
|
1414
|
+
absolute_sigma=True,
|
|
1415
|
+
maxfev=maxfev
|
|
1416
|
+
)
|
|
1417
|
+
except Exception as e:
|
|
1418
|
+
redback.utils.logger.warning(f"Fit failed for epoch {i}: {e}")
|
|
1419
|
+
redback.utils.logger.warning(f"Skipping epoch {i} with time {t_epoch:.2f} days.")
|
|
1420
|
+
continue
|
|
1421
|
+
|
|
1422
|
+
logT_fit, logR_fit = popt
|
|
1423
|
+
T_fit = 10 ** logT_fit
|
|
1424
|
+
R_fit = 10 ** logR_fit
|
|
1425
|
+
perr = np.sqrt(np.diag(pcov))
|
|
1426
|
+
T_err = np.log(10) * T_fit * perr[0]
|
|
1427
|
+
R_err = np.log(10) * R_fit * perr[1]
|
|
1428
|
+
|
|
1429
|
+
epoch_times.append(t_epoch)
|
|
1430
|
+
temperatures.append(T_fit)
|
|
1431
|
+
radii.append(R_fit)
|
|
1432
|
+
temp_errs.append(T_err)
|
|
1433
|
+
radius_errs.append(R_err)
|
|
1434
|
+
|
|
1435
|
+
if len(epoch_times) == 0:
|
|
1436
|
+
redback.utils.logger.warning("No epochs with sufficient data yielded a successful fit.")
|
|
1437
|
+
return None
|
|
1438
|
+
|
|
1439
|
+
df_bb = pd.DataFrame({
|
|
1440
|
+
'epoch_times': epoch_times,
|
|
1441
|
+
'temperature': temperatures,
|
|
1442
|
+
'radius': radii,
|
|
1443
|
+
'temp_err': temp_errs,
|
|
1444
|
+
'radius_err': radius_errs
|
|
1445
|
+
})
|
|
1446
|
+
|
|
1447
|
+
redback.utils.logger.info('Masking epochs with likely wrong extractions')
|
|
1448
|
+
df_bb = df_bb[df_bb['temp_err'] / df_bb['temperature'] < 1]
|
|
1449
|
+
df_bb = df_bb[df_bb['radius_err'] / df_bb['radius'] < 1]
|
|
1450
|
+
return df_bb
|
|
1451
|
+
|
|
1452
|
+
|
|
1453
|
+
def estimate_bolometric_luminosity(self, distance: float = 1e27, bin_width: float = 1.0,
|
|
1454
|
+
min_filters: int = 3, **kwargs):
|
|
1455
|
+
"""
|
|
1456
|
+
Estimate the bolometric luminosity as a function of time by fitting the blackbody SED
|
|
1457
|
+
to the multi‑band photometry and then integrating that spectrum. For each epoch the bolometric
|
|
1458
|
+
luminosity is computed using the Stefan–Boltzmann law evaluated at the source:
|
|
1459
|
+
|
|
1460
|
+
L_bol = 4 π R² σ_SB T⁴
|
|
1461
|
+
|
|
1462
|
+
Uncertainties in T and R are propagated assuming
|
|
1463
|
+
|
|
1464
|
+
(ΔL_bol / L_bol)² = (2 ΔR / R)² + (4 ΔT / T)².
|
|
1465
|
+
|
|
1466
|
+
Optionally, two corrections can be applied:
|
|
1467
|
+
|
|
1468
|
+
1. A boost–factor to “restore” missing blue flux. If a cutoff wavelength is provided via
|
|
1469
|
+
the keyword 'lambda_cut' (in angstroms), it is converted to centimeters and a boost factor is
|
|
1470
|
+
calculated as:
|
|
1471
|
+
|
|
1472
|
+
Boost = (F_tot / F_red)
|
|
1473
|
+
|
|
1474
|
+
where F_tot = σ_SB T⁴ and F_red is computed by numerically integrating π * B_λ(T)
|
|
1475
|
+
from the cutoff wavelength (in cm) to infinity. The final (boosted) luminosity becomes:
|
|
1476
|
+
|
|
1477
|
+
L_boosted = Boost × (4π R² σ_SB T⁴).
|
|
1478
|
+
|
|
1479
|
+
2. An extinction correction. If the bolometric extinction (A_ext, in magnitudes) is supplied via
|
|
1480
|
+
the keyword 'A_ext', the luminosity will be reduced by a factor of 10^(–0.4·A_ext) to account
|
|
1481
|
+
for dust extinction. (A_ext defaults to 0.)
|
|
1482
|
+
|
|
1483
|
+
Parameters
|
|
1484
|
+
----------
|
|
1485
|
+
distance : float, optional
|
|
1486
|
+
Distance to the transient in centimeters. (Default is 1e27 cm.)
|
|
1487
|
+
bin_width : float, optional
|
|
1488
|
+
Width of the time bins (in days) used for grouping photometry. (Default is 1.0.)
|
|
1489
|
+
min_filters : int, optional
|
|
1490
|
+
Minimum number of independent filters required in a bin to perform a fit. (Default is 3.)
|
|
1491
|
+
kwargs : dict, optional
|
|
1492
|
+
Additional keyword arguments to pass to `estimate_bb_params` (e.g., maxfev, T_init, R_init,
|
|
1493
|
+
use_eff_wavelength, etc.). Additionally:
|
|
1494
|
+
- 'lambda_cut': If provided (in angstroms), the bolometric luminosity will be “boosted”
|
|
1495
|
+
to account for missing blue flux.
|
|
1496
|
+
- 'A_ext': Bolometric extinction in magnitudes. The observed luminosity is increased by a factor
|
|
1497
|
+
10^(+0.4·A_ext). (Default is 0.)
|
|
1498
|
+
|
|
1499
|
+
Returns
|
|
1500
|
+
-------
|
|
1501
|
+
df_bol : pandas.DataFrame or None
|
|
1502
|
+
A DataFrame containing columns:
|
|
1503
|
+
- epoch_times: Mean time of the bin (days).
|
|
1504
|
+
- temperature: Fitted blackbody temperature (K).
|
|
1505
|
+
- radius: Fitted photospheric radius (cm).
|
|
1506
|
+
- lum_bol: Derived bolometric luminosity (1e50 erg/s) computed as 4π R² σ_SB T⁴
|
|
1507
|
+
(boosted and extinction-corrected if requested).
|
|
1508
|
+
- lum_bol_bb: Derived bolometric blackbody luminosity (1e50 erg/s) computed as 4π R² σ_SB T⁴,
|
|
1509
|
+
before applying either the boost or extinction correction.
|
|
1510
|
+
- lum_bol_err: 1σ uncertainty on L_bol (1e50 erg/s) from error propagation.
|
|
1511
|
+
- time_rest_frame: Epoch time divided by (1+redshift), i.e., the rest-frame time in days.
|
|
1512
|
+
Returns None if no valid blackbody fits were obtained.
|
|
1513
|
+
"""
|
|
1514
|
+
from redback.sed import boosted_bolometric_luminosity
|
|
1515
|
+
|
|
1516
|
+
# Retrieve optional lambda_cut (in angstroms) for the boost correction.
|
|
1517
|
+
lambda_cut_angstrom = kwargs.pop('lambda_cut', None)
|
|
1518
|
+
if lambda_cut_angstrom is not None:
|
|
1519
|
+
redback.utils.logger.info("Including effects of missing flux due to line blanketing.")
|
|
1520
|
+
redback.utils.logger.info(
|
|
1521
|
+
"Using lambda_cut = {} Å for bolometric luminosity boost.".format(lambda_cut_angstrom))
|
|
1522
|
+
# Convert lambda_cut from angstroms to centimeters (1 Å = 1e-8 cm)
|
|
1523
|
+
lambda_cut = lambda_cut_angstrom * 1e-8
|
|
1524
|
+
else:
|
|
1525
|
+
redback.utils.logger.info("No lambda_cut provided; no correction applied. Assuming a pure blackbody SED.")
|
|
1526
|
+
lambda_cut = None
|
|
1527
|
+
|
|
1528
|
+
# Retrieve optional extinction in magnitudes.
|
|
1529
|
+
A_ext = kwargs.pop('A_ext', 0.0)
|
|
1530
|
+
if A_ext != 0.0:
|
|
1531
|
+
redback.utils.logger.info("Applying extinction correction with A_ext = {} mag.".format(A_ext))
|
|
1532
|
+
extinction_factor = 10 ** (0.4 * A_ext)
|
|
1533
|
+
|
|
1534
|
+
# Retrieve blackbody parameters via your existing method.
|
|
1535
|
+
df_bb = self.estimate_bb_params(distance=distance, bin_width=bin_width, min_filters=min_filters, **kwargs)
|
|
1536
|
+
if df_bb is None or len(df_bb) == 0:
|
|
1537
|
+
redback.utils.logger.warning("No valid blackbody fits were obtained; cannot estimate bolometric luminosity.")
|
|
1538
|
+
return None
|
|
1539
|
+
|
|
1540
|
+
# Compute L_bol (or L_boosted) for each epoch and propagate uncertainties.
|
|
1541
|
+
L_bol = []
|
|
1542
|
+
L_bol_err = []
|
|
1543
|
+
L_bol_bb = []
|
|
1544
|
+
L_bol_bb_err = []
|
|
1545
|
+
for index, row in df_bb.iterrows():
|
|
1546
|
+
temp = row['temperature']
|
|
1547
|
+
radius = row['radius']
|
|
1548
|
+
T_err = row['temp_err']
|
|
1549
|
+
R_err = row['radius_err']
|
|
1550
|
+
|
|
1551
|
+
# Use boosted luminosity if lambda_cut is provided.
|
|
1552
|
+
if lambda_cut is not None:
|
|
1553
|
+
lum, lum_bb = boosted_bolometric_luminosity(temp, radius, lambda_cut)
|
|
1554
|
+
else:
|
|
1555
|
+
lum = 4 * np.pi * (radius ** 2) * redback.constants.sigma_sb * (temp ** 4)
|
|
1556
|
+
lum_bb = lum
|
|
1557
|
+
|
|
1558
|
+
# Apply extinction correction to both luminosities.
|
|
1559
|
+
lum *= extinction_factor
|
|
1560
|
+
lum_bb *= extinction_factor
|
|
1561
|
+
|
|
1562
|
+
# Propagate uncertainties using:
|
|
1563
|
+
# (ΔL/L)² = (2 ΔR / R)² + (4 ΔT / T)².
|
|
1564
|
+
rel_err = np.sqrt((2 * R_err / radius) ** 2 + (4 * T_err / temp) ** 2)
|
|
1565
|
+
L_err = lum * rel_err
|
|
1566
|
+
L_err_bb = lum_bb * rel_err
|
|
1567
|
+
|
|
1568
|
+
L_bol.append(lum)
|
|
1569
|
+
L_bol_bb.append(lum_bb)
|
|
1570
|
+
L_bol_err.append(L_err)
|
|
1571
|
+
L_bol_bb_err.append(L_err_bb)
|
|
1572
|
+
|
|
1573
|
+
df_bol = df_bb.copy()
|
|
1574
|
+
df_bol['lum_bol'] = np.array(L_bol) / 1e50
|
|
1575
|
+
df_bol['lum_bol_err'] = np.array(L_bol_err) / 1e50
|
|
1576
|
+
df_bol['lum_bol_bb'] = np.array(L_bol_bb) / 1e50
|
|
1577
|
+
df_bol['lum_bol_bb_err'] = np.array(L_bol_bb_err) / 1e50
|
|
1578
|
+
df_bol['time_rest_frame'] = df_bol['epoch_times'] / (1 + self.redshift)
|
|
1579
|
+
|
|
1580
|
+
redback.utils.logger.info('Masking bolometric estimates with likely wrong extractions')
|
|
1581
|
+
df_bol = df_bol[df_bol['lum_bol_err'] / df_bol['lum_bol'] < 1]
|
|
1582
|
+
redback.utils.logger.info(
|
|
1583
|
+
"Estimated bolometric luminosity using blackbody integration (with boost and extinction corrections if specified).")
|
|
1584
|
+
return df_bol
|