• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

nikhil-sarin / redback / 17297018917

28 Aug 2025 01:16PM UTC coverage: 86.258% (-1.3%) from 87.575%
17297018917

Pull #287

github

web-flow
Merge 0fcd153f0 into d0b2cb1d3
Pull Request #287: Afterglow optimisation

80 of 338 new or added lines in 1 file covered. (23.67%)

1 existing line in 1 file now uncovered.

13609 of 15777 relevant lines covered (86.26%)

0.86 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

91.6
/redback/transient_models/kilonova_models.py
1
import numpy as np
1✔
2
import pandas as pd
1✔
3

4
from astropy.table import Table, Column
1✔
5
from scipy.interpolate import interp1d, RegularGridInterpolator
1✔
6
from astropy.cosmology import Planck18 as cosmo  # noqa
1✔
7
from scipy.integrate import cumulative_trapezoid
1✔
8
from collections import namedtuple
1✔
9
from redback.photosphere import TemperatureFloor, CocoonPhotosphere
1✔
10
from redback.interaction_processes import Diffusion, AsphericalDiffusion
1✔
11

12
from redback.utils import calc_kcorrected_properties, interpolated_barnes_and_kasen_thermalisation_efficiency, \
1✔
13
    electron_fraction_from_kappa, citation_wrapper, lambda_to_nu, _calculate_rosswogkorobkin24_qdot, \
14
    kappa_from_electron_fraction
15
from redback.eos import PiecewisePolytrope
1✔
16
from redback.sed import blackbody_to_flux_density, get_correct_output_format_from_spectra, Blackbody
1✔
17
from redback.constants import *
1✔
18
import redback.ejecta_relations as ejr
1✔
19

20
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2021MNRAS.505.3016N/abstract')
1✔
21
def _nicholl_bns_get_quantities(mass_1, mass_2, lambda_s, kappa_red, kappa_blue,
1✔
22
                                mtov, epsilon, alpha, cos_theta_open, cos_theta, **kwargs):
23
    """
24
    Calculates quantities for the Nicholl et al. 2021 BNS model
25

26
    :param mass_1: Mass of primary in solar masses
27
    :param mass_2: Mass of secondary in solar masses
28
    :param lambda_s: Symmetric tidal deformability i.e, lambda_s = lambda_1 + lambda_2
29
    :param kappa_red: opacity of the red ejecta
30
    :param kappa_blue: opacity of the blue ejecta
31
    :param mtov: Tolman Oppenheimer-Volkoff mass in solar masses
32
    :param epsilon: fraction of disk that gets unbound/ejected
33
    :param alpha: Enhancement of blue ejecta by NS surface winds if mtotal < prompt collapse,
34
                can turn off by setting alpha=1
35
    :param cos_theta_open: Lanthanide opening angle 
36
    :param cos_theta: Viewing angle of observer
37
    :param kwargs: Additional keyword arguments
38
    :param dynamical_ejecta_error: Error in dynamical ejecta mass, default is 1 i.e., no error in fitting formula
39
    :param disk_ejecta_error: Error in disk ejecta mass, default is 1 i.e., no error in fitting formula
40
    :return: Namedtuple with 'mejecta_blue', 'mejecta_red', 'mejecta_purple',
41
            'vejecta_blue', 'vejecta_red', 'vejecta_purple',
42
            'vejecta_mean', 'kappa_mean', 'mejecta_dyn',
43
            'mejecta_total', 'kappa_purple', 'radius_1', 'radius_2',
44
            'binary_lambda', 'remnant_radius', 'area_blue', 'area_blue_ref',
45
            'area_red', 'area_red_ref' properties. Masses in solar masses and velocities in units of c
46
    """
47
    ckm = 3e10/1e5
1✔
48
    a = 0.07550
1✔
49
    b = np.array([[-2.235, 0.8474], [10.45, -3.251], [-15.70, 13.61]])
1✔
50
    c = np.array([[-2.048, 0.5976], [7.941, 0.5658], [-7.360, -1.320]])
1✔
51
    n_ave = 0.743
1✔
52
    dynamical_ejecta_error = kwargs.get('dynamical_ejecta_error', 1.0)
1✔
53
    disk_ejecta_error = kwargs.get('disk_ejecta_error', 1.0)
1✔
54
    theta_open = np.arccos(cos_theta_open)
1✔
55

56
    fq = (1 - (mass_2 / mass_1) ** (10. / (3 - n_ave))) / (1 + (mass_2 / mass_1) ** (10. / (3 - n_ave)))
1✔
57

58
    nume = a
1✔
59
    denom = a
1✔
60

61
    for i in np.arange(3):
1✔
62
        for j in np.arange(2):
1✔
63
            nume += b[i, j] * (mass_2 / mass_1) ** (j + 1) * lambda_s ** (-(i + 1) / 5)
1✔
64

65
    for i in np.arange(3):
1✔
66
        for j in np.arange(2):
1✔
67
            denom += c[i, j] * (mass_2 / mass_1) ** (j + 1) * lambda_s ** (-(i + 1) / 5)
1✔
68

69
    lambda_a = lambda_s * fq * nume / denom
1✔
70
    lambda_1 = lambda_s - lambda_a
1✔
71
    lambda_2 = lambda_s + lambda_a
1✔
72
    m_total = mass_1 + mass_2
1✔
73

74
    binary_lambda =  16./13 * ((mass_1 + 12*mass_2) * mass_1**4 * lambda_1 +
1✔
75
                (mass_2 + 12*mass_1) * mass_2**4 * lambda_2) / m_total**5
76
    mchirp = (mass_1 * mass_2)**(3./5) / m_total**(1./5)
1✔
77
    remnant_radius = 11.2 * mchirp * (binary_lambda/800)**(1./6.)
1✔
78

79
    compactness_1 = 0.360 - 0.0355 * np.log(lambda_1) + 0.000705 * np.log(lambda_1) ** 2
1✔
80
    compactness_2 = 0.360 - 0.0355 * np.log(lambda_2) + 0.000705 * np.log(lambda_2) ** 2
1✔
81

82
    radius_1 = (graviational_constant * mass_1 * solar_mass / (compactness_1 * speed_of_light ** 2)) / 1e5
1✔
83
    radius_2 = (graviational_constant * mass_2 * solar_mass / (compactness_2 * speed_of_light ** 2)) / 1e5
1✔
84

85
    # Baryonic masses, Gao 2019
86
    mass_baryonic_1 = mass_1 + 0.08 * mass_1 ** 2
1✔
87
    mass_baryonic_2 = mass_2 + 0.08 * mass_2 ** 2
1✔
88

89
    a_1 = -1.35695
1✔
90
    b_1 = 6.11252
1✔
91
    c_1 = -49.43355
1✔
92
    d_1 = 16.1144
1✔
93
    n = -2.5484
1✔
94
    dynamical_ejecta_mass = 1e-3 * (a_1 * ((mass_2 / mass_1) ** (1 / 3) * (1 - 2 * compactness_1) / compactness_2 * mass_baryonic_1 +
1✔
95
                            (mass_1 / mass_2) ** (1 / 3) * (1 - 2 * compactness_2) / compactness_2 * mass_baryonic_2) +
96
                     b_1 * ((mass_2 / mass_1) ** n * mass_baryonic_1 + (mass_1 / mass_2) ** n * mass_baryonic_2) +
97
                     c_1 * (mass_baryonic_1 - mass_1 + mass_baryonic_2 - mass_2) + d_1)
98

99
    if dynamical_ejecta_mass < 0:
1✔
UNCOV
100
        dynamical_ejecta_mass = 0
×
101

102
    dynamical_ejecta_mass *= dynamical_ejecta_error
1✔
103

104
    a_4 = 14.8609
1✔
105
    b_4 = -28.6148
1✔
106
    c_4 = 13.9597
1✔
107

108
    # fraction can't exceed 100%
109
    f_red = min([a_4 * (mass_1 / mass_2) ** 2 + b_4 * (mass_1 / mass_2) + c_4, 1])
1✔
110

111
    mejecta_red = dynamical_ejecta_mass * f_red
1✔
112
    mejecta_blue = dynamical_ejecta_mass * (1 - f_red)
1✔
113

114
    # Velocity of dynamical ejecta
115
    a_2 = -0.219479
1✔
116
    b_2 = 0.444836
1✔
117
    c_2 = -2.67385
1✔
118

119
    vdynp = a_2 * ((mass_1 / mass_2) * (1 + c_2 * compactness_1) + (mass_2 / mass_1) * (1 + c_2 * compactness_2)) + b_2
1✔
120

121
    a_3 = -0.315585
1✔
122
    b_3 = 0.63808
1✔
123
    c_3 = -1.00757
1✔
124

125
    vdynz = a_3 * ((mass_1 / mass_2) * (1 + c_3 * compactness_1) + (mass_2 / mass_1) * (1 + c_3 * compactness_2)) + b_3
1✔
126

127
    dynamical_ejecta_velocity = np.sqrt(vdynp ** 2 + vdynz ** 2)
1✔
128

129
    # average velocity over angular ranges (< and > theta_open)
130

131
    theta1 = np.arange(0, theta_open, 0.01)
1✔
132
    theta2 = np.arange(theta_open, np.pi / 2, 0.01)
1✔
133

134
    vtheta1 = np.sqrt((vdynz * np.cos(theta1)) ** 2 + (vdynp * np.sin(theta1)) ** 2)
1✔
135
    vtheta2 = np.sqrt((vdynz * np.cos(theta2)) ** 2 + (vdynp * np.sin(theta2)) ** 2)
1✔
136

137
    atheta1 = 2 * np.pi * np.sin(theta1)
1✔
138
    atheta2 = 2 * np.pi * np.sin(theta2)
1✔
139

140
    vejecta_blue = np.trapz(vtheta1 * atheta1, x=theta1) / np.trapz(atheta1, x=theta1)
1✔
141
    vejecta_red = np.trapz(vtheta2 * atheta2, x=theta2) / np.trapz(atheta2, x=theta2)
1✔
142

143
    # vejecta_red *= ckm
144
    # vejecta_blue *= ckm
145

146
    # Bauswein 2013, cut-off for prompt collapse to BH
147
    prompt_threshold_mass = (2.38 - 3.606 * mtov / remnant_radius) * mtov
1✔
148

149
    if m_total < prompt_threshold_mass:
1✔
150
        mejecta_blue /= alpha
1✔
151

152
    # Now compute disk ejecta following Coughlin+ 2019
153

154
    a_5 = -31.335
1✔
155
    b_5 = -0.9760
1✔
156
    c_5 = 1.0474
1✔
157
    d_5 = 0.05957
1✔
158

159
    logMdisk = np.max([-3, a_5 * (1 + b_5 * np.tanh((c_5 - m_total / prompt_threshold_mass) / d_5))])
1✔
160

161
    disk_mass = 10 ** logMdisk
1✔
162

163
    disk_mass *= disk_ejecta_error
1✔
164

165
    disk_ejecta_mass = disk_mass * epsilon
1✔
166

167
    mejecta_purple = disk_ejecta_mass
1✔
168

169
    # Fit for disk velocity using Metzger and Fernandez
170
    vdisk_max = 0.15
1✔
171
    vdisk_min = 0.03
1✔
172
    vfit = np.polyfit([mtov, prompt_threshold_mass], [vdisk_max, vdisk_min], deg=1)
1✔
173

174
    # Get average opacity of 'purple' (disk) component
175
    # Mass-averaged Ye as a function of remnant lifetime from Lippuner 2017
176
    # Lifetime related to Mtot using Metzger handbook table 3
177
    if m_total < mtov:
1✔
178
        # stable NS
179
        Ye = 0.38
×
180
        vdisk = vdisk_max
×
181
    elif m_total < 1.2 * mtov:
1✔
182
        # long-lived (>>100 ms) NS remnant Ye = 0.34-0.38,
183
        # smooth interpolation
184
        Yfit = np.polyfit([mtov, 1.2 * mtov], [0.38, 0.34], deg=1)
×
185
        Ye = Yfit[0] * m_total + Yfit[1]
×
186
        vdisk = vfit[0] * m_total + vfit[1]
×
187
    elif m_total < prompt_threshold_mass:
1✔
188
        # short-lived (hypermassive) NS, Ye = 0.25-0.34, smooth interpolation
189
        Yfit = np.polyfit([1.2 * mtov, prompt_threshold_mass], [0.34, 0.25], deg=1)
1✔
190
        Ye = Yfit[0] * m_total + Yfit[1]
1✔
191
        vdisk = vfit[0] * m_total + vfit[1]
1✔
192
    else:
193
        # prompt collapse to BH, disk is red
194
        Ye = 0.25
×
195
        vdisk = vdisk_min
×
196

197
    # Convert Ye to opacity using Tanaka et al 2019 for Ye >= 0.25:
198
    a_6 = 2112.0
1✔
199
    b_6 = -2238.9
1✔
200
    c_6 = 742.35
1✔
201
    d_6 = -73.14
1✔
202

203
    kappa_purple = a_6 * Ye ** 3 + b_6 * Ye ** 2 + c_6 * Ye + d_6
1✔
204

205
    vejecta_purple = vdisk
1✔
206

207
    vejecta_mean = (mejecta_purple * vejecta_purple + vejecta_red * mejecta_red +
1✔
208
                    vejecta_blue * mejecta_blue) / (mejecta_purple + mejecta_red + mejecta_blue)
209

210
    kappa_mean = (mejecta_purple * kappa_purple + kappa_red * mejecta_red +
1✔
211
                  kappa_blue * mejecta_blue) / (mejecta_purple + mejecta_red + mejecta_blue)
212

213
    # Viewing angle and lanthanide-poor opening angle correction from Darbha and Kasen 2020
214
    ct = (1 - cos_theta_open ** 2) ** 0.5
1✔
215

216
    if cos_theta > ct:
1✔
217
        area_projected_top = np.pi * ct * cos_theta
1✔
218
    else:
219
        theta_p = np.arccos(cos_theta_open /
1✔
220
                            (1 - cos_theta ** 2) ** 0.5)
221
        theta_d = np.arctan(np.sin(theta_p) / cos_theta_open *
1✔
222
                            (1 - cos_theta ** 2) ** 0.5 / np.abs(cos_theta))
223
        area_projected_top = (theta_p - np.sin(theta_p) * np.cos(theta_p)) - (ct *
1✔
224
                                                                     cos_theta * (theta_d - np.sin(theta_d) * np.cos(
225
                            theta_d) - np.pi))
226

227
    minus_cos_theta = -1 * cos_theta
1✔
228

229
    if minus_cos_theta < -1 * ct:
1✔
230
        area_projected_bottom = 0
1✔
231
    else:
232
        theta_p2 = np.arccos(cos_theta_open /
1✔
233
                             (1 - minus_cos_theta ** 2) ** 0.5)
234
        theta_d2 = np.arctan(np.sin(theta_p2) / cos_theta_open *
1✔
235
                             (1 - minus_cos_theta ** 2) ** 0.5 / np.abs(minus_cos_theta))
236

237
        Aproj_bot1 = (theta_p2 - np.sin(theta_p2) * np.cos(theta_p2)) + (ct *
1✔
238
                                                                         minus_cos_theta * (theta_d2 - np.sin(
239
                    theta_d2) * np.cos(theta_d2)))
240
        area_projected_bottom = np.max([Aproj_bot1, 0])
1✔
241

242
    area_projected = area_projected_top + area_projected_bottom
1✔
243

244
    # Compute reference areas for this opening angle to scale luminosity
245

246
    cos_theta_ref = 0.5
1✔
247

248
    if cos_theta_ref > ct:
1✔
249
        area_ref_top = np.pi * ct * cos_theta_ref
×
250
    else:
251
        theta_p_ref = np.arccos(cos_theta_open /
1✔
252
                                (1 - cos_theta_ref ** 2) ** 0.5)
253
        theta_d_ref = np.arctan(np.sin(theta_p_ref) / cos_theta_open *
1✔
254
                                (1 - cos_theta_ref ** 2) ** 0.5 / np.abs(cos_theta_ref))
255
        area_ref_top = (theta_p_ref - np.sin(theta_p_ref) *
1✔
256
                    np.cos(theta_p_ref)) - (ct * cos_theta_ref *
257
                                            (theta_d_ref - np.sin(theta_d_ref) *
258
                                             np.cos(theta_d_ref) - np.pi))
259

260
    minus_cos_theta_ref = -1 * cos_theta_ref
1✔
261

262
    if minus_cos_theta_ref < -1 * ct:
1✔
263
        area_ref_bottom = 0
×
264
    else:
265
        theta_p2_ref = np.arccos(cos_theta_open /
1✔
266
                                 (1 - minus_cos_theta_ref ** 2) ** 0.5)
267
        theta_d2_ref = np.arctan(np.sin(theta_p2_ref) /
1✔
268
                                 cos_theta_open * (1 - minus_cos_theta_ref ** 2) ** 0.5 /
269
                                 np.abs(minus_cos_theta_ref))
270

271
        area_ref_bottom = (theta_p2_ref - np.sin(theta_p2_ref) *
1✔
272
                    np.cos(theta_p2_ref)) + (ct * minus_cos_theta_ref *
273
                                             (theta_d2_ref - np.sin(theta_d2_ref) *
274
                                              np.cos(theta_d2_ref)))
275

276
    area_ref = area_ref_top + area_ref_bottom
1✔
277

278
    area_blue = area_projected
1✔
279
    area_blue_ref = area_ref
1✔
280

281
    area_red = np.pi - area_blue
1✔
282
    area_red_ref = np.pi - area_blue_ref
1✔
283

284
    output = namedtuple('output', ['mejecta_blue', 'mejecta_red', 'mejecta_purple',
1✔
285
                                   'vejecta_blue', 'vejecta_red', 'vejecta_purple',
286
                                   'vejecta_mean', 'kappa_mean', 'mejecta_dyn',
287
                                   'mejecta_total', 'kappa_purple', 'radius_1', 'radius_2',
288
                                   'binary_lambda', 'remnant_radius', 'area_blue', 'area_blue_ref',
289
                                   'area_red', 'area_red_ref'])
290
    output.mejecta_blue = mejecta_blue
1✔
291
    output.mejecta_red = mejecta_red
1✔
292
    output.mejecta_purple = mejecta_purple
1✔
293
    output.vejecta_blue = vejecta_blue
1✔
294
    output.vejecta_red = vejecta_red
1✔
295
    output.vejecta_purple = vejecta_purple
1✔
296
    output.vejecta_mean = vejecta_mean
1✔
297
    output.kappa_mean = kappa_mean
1✔
298
    output.mejecta_dyn = dynamical_ejecta_mass
1✔
299
    output.mejecta_total = dynamical_ejecta_mass + mejecta_purple
1✔
300
    output.kappa_purple = kappa_purple
1✔
301
    output.radius_1 = radius_1
1✔
302
    output.radius_2 = radius_2
1✔
303
    output.binary_lambda = binary_lambda
1✔
304
    output.remnant_radius = remnant_radius
1✔
305
    output.area_blue = area_blue
1✔
306
    output.area_blue_ref = area_blue_ref
1✔
307
    output.area_red = area_red
1✔
308
    output.area_red_ref = area_red_ref
1✔
309
    return output
1✔
310

311
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2021MNRAS.505.3016N/abstract')
1✔
312
def nicholl_bns(time, redshift, mass_1, mass_2, lambda_s, kappa_red, kappa_blue,
1✔
313
                mtov, epsilon, alpha, cos_theta, cos_theta_open, cos_theta_cocoon, temperature_floor_1,
314
                temperature_floor_2, temperature_floor_3, **kwargs):
315
    """
316
    Kilonova model from Nicholl et al. 2021, inclides three kilonova components
317
    + shock heating from cocoon + disk winds from remnant
318

319
    :param time: time in days in observer frame
320
    :param redshift: redshift
321
    :param mass_1: Mass of primary in solar masses
322
    :param mass_2: Mass of secondary in solar masses
323
    :param lambda_s: Symmetric tidal deformability i.e, lambda_s = lambda_1 + lambda_2
324
    :param kappa_red: opacity of the red ejecta
325
    :param kappa_blue: opacity of the blue ejecta
326
    :param mtov: Tolman Oppenheimer-Volkoff mass in solar masses
327
    :param epsilon: fraction of disk that gets unbound/ejected
328
    :param alpha: Enhancement of blue ejecta by NS surface winds if mtotal < prompt collapse,
329
                can turn off by setting alpha=1
330
    :param cos_theta: Viewing angle of observer
331
    :param cos_theta_open: Lanthanide opening angle
332
    :param cos_theta_cocoon: Opening angle of shocked cocoon
333
    :param temperature_floor_1: Temperature floor of first (blue) component
334
    :param temperature_floor_2: Temperature floor of second (purple) component
335
    :param temperature_floor_3: Temperature floor of third (red) component
336
    :param kwargs: Additional keyword arguments
337
    :param dynamical_ejecta_error: Error in dynamical ejecta mass, default is 1 i.e., no error in fitting formula
338
    :param disk_ejecta_error: Error in disk ejecta mass, default is 1 i.e., no error in fitting formula
339
    :param shocked_fraction: Fraction of ejecta that is shocked by jet, default is 0.2 i.e., 20% of blue ejecta is shocked.
340
        Use 0. if you want to turn off cocoon emission.
341
    :param nn: ejecta power law density profile, default is 1.
342
    :param tshock: time for shock in source frame in seconds, default is 1.7s (see Nicholl et al. 2021)
343
    :param frequency: Required if output_format is 'flux_density'.
344
        frequency to calculate - Must be same length as time array or a single number).
345
    :param bands: Required if output_format is 'magnitude' or 'flux'.
346
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
347
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
348
    :param dense_resolution: resolution of the grid that the model is actually evaluated on, default is 300
349
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
350
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
351
    """
352
    from redback.transient_models.shock_powered_models import _shocked_cocoon_nicholl
1✔
353
    cosmology = kwargs.get('cosmology', cosmo)
1✔
354
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
355
    dense_resolution = kwargs.get('dense_resolution', 100)
1✔
356
    time_temp = np.geomspace(0.01, 30, dense_resolution)  # in source frame and days
1✔
357
    kappa_gamma = kwargs.get('kappa_gamma', 10)
1✔
358
    ckm = 3e10/1e5
1✔
359

360
    if np.max(time) > 20: # in source frame and days
1✔
361
        time_temp = np.geomspace(0.01, np.max(time) + 5, dense_resolution)
×
362

363
    time_obs = time
1✔
364
    shocked_fraction = kwargs.get('shocked_fraction', 0.2)
1✔
365
    nn = kwargs.get('nn', 1)
1✔
366
    tshock = kwargs.get('tshock', 1.7)
1✔
367

368
    output = _nicholl_bns_get_quantities(mass_1=mass_1, mass_2=mass_2, lambda_s=lambda_s,
1✔
369
                                         kappa_red=kappa_red, kappa_blue=kappa_blue, mtov=mtov,
370
                                         epsilon=epsilon, alpha=alpha, cos_theta_open=cos_theta_open, 
371
                                         cos_theta=cos_theta, **kwargs)
372
    cocoon_output = _shocked_cocoon_nicholl(time=time_temp, kappa=kappa_blue, mejecta=output.mejecta_blue,
1✔
373
                                  vejecta=output.vejecta_blue, cos_theta_cocoon=cos_theta_cocoon,
374
                                  shocked_fraction=shocked_fraction, nn=nn, tshock=tshock)
375
    cocoon_photo = CocoonPhotosphere(time=time_temp, luminosity=cocoon_output.lbol,
1✔
376
                                     tau_diff=cocoon_output.taudiff, t_thin=cocoon_output.tthin,
377
                                     vej=output.vejecta_blue*ckm, nn=nn)
378
    mejs = [output.mejecta_blue, output.mejecta_purple, output.mejecta_red]
1✔
379
    vejs = [output.vejecta_blue, output.vejecta_purple, output.vejecta_red]
1✔
380
    area_projs = [output.area_blue, output.area_blue, output.area_red]
1✔
381
    area_refs = [output.area_blue_ref, output.area_blue_ref, output.area_red_ref]
1✔
382
    temperature_floors = [temperature_floor_1, temperature_floor_2, temperature_floor_3]
1✔
383
    kappas = [kappa_blue, output.kappa_purple, kappa_red]
1✔
384

385
    if kwargs['output_format'] == 'flux_density':
1✔
386
        frequency = kwargs['frequency']
1✔
387
        # interpolate properties onto observation times
388
        temp_func = interp1d(time_temp, y=cocoon_photo.photosphere_temperature)
1✔
389
        rad_func = interp1d(time_temp, y=cocoon_photo.r_photosphere)
1✔
390
        # convert to source frame time and frequency
391
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
392
        temp = temp_func(time)
1✔
393
        photosphere = rad_func(time)
1✔
394
        flux_density = blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
395
                                                 dl=dl, frequency=frequency)
396
        ff = flux_density.value
1✔
397
        ff = np.nan_to_num(ff)
1✔
398
        for x in range(3):
1✔
399
            lbols = _mosfit_kilonova_one_component_lbol(time=time_temp*day_to_s, mej=mejs[x], vej=vejs[x])
1✔
400
            interaction_class = AsphericalDiffusion(time=time_temp, dense_times=time_temp,
1✔
401
                                                    luminosity=lbols, kappa=kappas[x], kappa_gamma=kappa_gamma,
402
                                                    mej=mejs[x], vej=vejs[x]*ckm, area_projection=area_projs[x],
403
                                                    area_reference=area_refs[x])
404
            lbols = interaction_class.new_luminosity
1✔
405
            lbols = np.nan_to_num(lbols)
1✔
406
            photo = TemperatureFloor(time=time_temp, luminosity=lbols,
1✔
407
                                     temperature_floor=temperature_floors[x], vej=vejs[x]*ckm)
408
            temp_func = interp1d(time_temp, y=photo.photosphere_temperature)
1✔
409
            rad_func = interp1d(time_temp, y=photo.r_photosphere)
1✔
410
            temp = temp_func(time)
1✔
411
            photosphere = rad_func(time)
1✔
412
            flux_density = blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
413
                                                     dl=dl, frequency=frequency)
414
            flux_density = np.nan_to_num(flux_density)
1✔
415
            units = flux_density.unit
1✔
416
            ff += flux_density.value
1✔
417
        ff = ff * units
1✔
418
        return ff.to(uu.mJy).value
1✔
419
    else:
420
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 200))
1✔
421
        time_observer_frame = time_temp * (1. + redshift) #in days
1✔
422
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
423
                                                     redshift=redshift, time=time_observer_frame)
424
        fmjy = blackbody_to_flux_density(temperature=cocoon_photo.photosphere_temperature,
1✔
425
                                         r_photosphere=cocoon_photo.r_photosphere,dl=dl,
426
                                         frequency=frequency[:,None]).T
427
        cocoon_spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
428
                                         equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
429
        cocoon_spectra = np.nan_to_num(cocoon_spectra)
1✔
430
        full_spec = cocoon_spectra.value
1✔
431
        for x in range(3):
1✔
432
            lbols = _mosfit_kilonova_one_component_lbol(time=time_temp*day_to_s, mej=mejs[x], vej=vejs[x])
1✔
433
            interaction_class = AsphericalDiffusion(time=time_temp, dense_times=time_temp,
1✔
434
                                                    luminosity=lbols, kappa=kappas[x], kappa_gamma=kappa_gamma,
435
                                                    mej=mejs[x], vej=vejs[x]*ckm, area_projection=area_projs[x],
436
                                                    area_reference=area_refs[x])
437
            lbols = interaction_class.new_luminosity
1✔
438
            photo = TemperatureFloor(time=time_temp, luminosity=lbols,
1✔
439
                                     temperature_floor=temperature_floors[x], vej=vejs[x]*ckm)
440
            fmjy = blackbody_to_flux_density(temperature=photo.photosphere_temperature,
1✔
441
                                              r_photosphere=photo.r_photosphere, dl=dl,
442
                                              frequency=frequency[:, None])
443
            fmjy = fmjy.T
1✔
444
            spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
445
                                         equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
446
            spectra = np.nan_to_num(spectra)
1✔
447
            units = spectra.unit
1✔
448
            full_spec += spectra.value
1✔
449

450
        full_spec = full_spec * units
1✔
451
        if kwargs['output_format'] == 'spectra':
1✔
452
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
453
                                                                        lambdas=lambda_observer_frame,
454
                                                                        spectra=full_spec)
455
        else:
456
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
1✔
457
                                                          spectra=full_spec, lambda_array=lambda_observer_frame,
458
                                                          **kwargs)
459

460

461
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2017ApJ...851L..21V/abstract')
1✔
462
def mosfit_rprocess(time, redshift, mej, vej, kappa, kappa_gamma, temperature_floor, **kwargs):
1✔
463
    """
464
    A single component kilonova model that *should* be exactly the same as mosfit's r-process model.
465
    Effectively the only difference to the redback one_component model is the inclusion of gamma-ray opacity in diffusion.
466

467
    :param time: observer frame time in days
468
    :param redshift: redshift
469
    :param mej: ejecta mass in solar masses of first component
470
    :param vej: minimum initial velocity of first component in units of c
471
    :param kappa: gray opacity of first component
472
    :param temperature_floor: floor temperature of first component
473
    :param kappa_gamma: gamma-ray opacity
474
    :param kwargs: Additional keyword arguments
475
    :param frequency: Required if output_format is 'flux_density'.
476
        frequency to calculate - Must be same length as time array or a single number).
477
    :param bands: Required if output_format is 'magnitude' or 'flux'.
478
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
479
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
480
    :param dense_resolution: resolution of the grid that the model is actually evaluated on, default is 300
481
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
482
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
483
    """
484
    ckm = 3e10/1e5
1✔
485
    cosmology = kwargs.get('cosmology', cosmo)
1✔
486
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
487
    dense_resolution = kwargs.get('dense_resolution', 300)
1✔
488
    time_temp = np.geomspace(1e-2, 7e6, dense_resolution) # in source frame in seconds
1✔
489
    time_obs = time
1✔
490
    lbols = _mosfit_kilonova_one_component_lbol(time=time_temp,
1✔
491
                                                mej=mej, vej=vej)
492
    interaction_class = Diffusion(time=time_temp / day_to_s, dense_times=time_temp / day_to_s, luminosity=lbols,
1✔
493
                                  kappa=kappa, kappa_gamma=kappa_gamma, mej=mej, vej=vej*ckm)
494
    lbols = interaction_class.new_luminosity
1✔
495
    photo = TemperatureFloor(time=time_temp / day_to_s, luminosity=lbols, vej=vej*ckm,
1✔
496
                             temperature_floor=temperature_floor)
497

498
    if kwargs['output_format'] == 'flux_density':
1✔
499
        #time = time_obs * day_to_s
500
        frequency = kwargs['frequency']
1✔
501
        # interpolate properties onto observation times
502
        temp_func = interp1d(time_temp / day_to_s, y=photo.photosphere_temperature)
1✔
503
        rad_func = interp1d(time_temp / day_to_s, y=photo.r_photosphere)
1✔
504
        # convert to source frame time and frequency
505
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
506
        temp = temp_func(time)
1✔
507
        photosphere = rad_func(time)
1✔
508
        flux_density = blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
509
                                                 dl=dl, frequency=frequency)
510
        return flux_density.to(uu.mJy).value
1✔
511
    else:
512
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 200))
1✔
513
        time_observer_frame = time_temp / day_to_s * (1. + redshift) # in days
1✔
514
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
515
                                                     redshift=redshift, time=time_observer_frame)
516
        fmjy = blackbody_to_flux_density(temperature=photo.photosphere_temperature,
1✔
517
                                         r_photosphere=photo.r_photosphere, frequency=frequency[:, None], dl=dl)
518
        fmjy = fmjy.T
1✔
519
        spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
520
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
521
        if kwargs['output_format'] == 'spectra':
1✔
522
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
523
                                                                        lambdas=lambda_observer_frame,
524
                                                                        spectra=spectra)
525
        else:
526
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
1✔
527
                                                          spectra=spectra, lambda_array=lambda_observer_frame,
528
                                                          **kwargs)
529

530
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2017ApJ...851L..21V/abstract')
1✔
531
def mosfit_kilonova(time, redshift, mej_1, vej_1, temperature_floor_1, kappa_1,
1✔
532
                    mej_2, vej_2, temperature_floor_2, kappa_2,
533
                    mej_3, vej_3, temperature_floor_3, kappa_3, kappa_gamma, **kwargs):
534
    """
535
    A three component kilonova model that *should* be exactly the same as mosfit's kilonova model or Villar et al. 2017.
536
    Effectively the only difference to the redback three_component model is the inclusion of gamma-ray opacity in diffusion.
537
    Note: Villar et al. fix the kappa's of each component to get the desired red/blue/purple components. This is not done here.
538

539
    :param time: observer frame time in days
540
    :param redshift: redshift
541
    :param mej_1: ejecta mass in solar masses of first component
542
    :param vej_1: minimum initial velocity of first component in units of c
543
    :param kappa_1: gray opacity of first component
544
    :param temperature_floor_1: floor temperature of first component
545
    :param mej_2: ejecta mass in solar masses of second component
546
    :param vej_2: minimum initial velocity of second component in units of c
547
    :param temperature_floor_2: floor temperature of second component
548
    :param kappa_2: gray opacity of second component
549
    :param mej_3: ejecta mass in solar masses of third component
550
    :param vej_3: minimum initial velocity of third component in units of c
551
    :param temperature_floor_3: floor temperature of third component
552
    :param kappa_3: gray opacity of third component
553
    :param kappa_gamma: gamma-ray opacity
554
    :param kwargs: Additional keyword arguments
555
    :param frequency: Required if output_format is 'flux_density'.
556
        frequency to calculate - Must be same length as time array or a single number).
557
    :param bands: Required if output_format is 'magnitude' or 'flux'.
558
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
559
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
560
    :param dense_resolution: resolution of the grid that the model is actually evaluated on, default is 300
561
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
562
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
563
    """
564
    ckm = 3e10/1e5
1✔
565
    cosmology = kwargs.get('cosmology', cosmo)
1✔
566
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
567
    dense_resolution = kwargs.get('dense_resolution', 300)
1✔
568
    time_temp = np.geomspace(1e-2, 7e6, dense_resolution)  # in source frame in s
1✔
569
    time_obs = time
1✔
570
    mej = [mej_1, mej_2, mej_3]
1✔
571
    vej = [vej_1, vej_2, vej_3]
1✔
572
    temperature_floor = [temperature_floor_1, temperature_floor_2, temperature_floor_3]
1✔
573
    kappa = [kappa_1, kappa_2, kappa_3]
1✔
574
    if kwargs['output_format'] == 'flux_density':
1✔
575
        #time = time_obs * day_to_s
576
        frequency = kwargs['frequency']
1✔
577
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
578

579
        ff = np.zeros(len(time))
1✔
580
        for x in range(3):
1✔
581
            lbols = _mosfit_kilonova_one_component_lbol(time=time_temp,
1✔
582
                                                        mej=mej[x], vej=vej[x])
583
            interaction_class = Diffusion(time=time_temp / day_to_s, dense_times=time_temp / day_to_s, luminosity=lbols,
1✔
584
                                          kappa=kappa[x], kappa_gamma=kappa_gamma, mej=mej[x], vej=vej[x]*ckm)
585
            lbols = interaction_class.new_luminosity
1✔
586
            photo = TemperatureFloor(time=time_temp / day_to_s, luminosity=lbols, vej=vej[x]*ckm,
1✔
587
                                     temperature_floor=temperature_floor[x])
588
            temp_func = interp1d(time_temp / day_to_s, y=photo.photosphere_temperature)
1✔
589
            rad_func = interp1d(time_temp / day_to_s, y=photo.r_photosphere)
1✔
590
            # convert to source frame time and frequency
591
            temp = temp_func(time)
1✔
592
            photosphere = rad_func(time)
1✔
593
            flux_density = blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
594
                                                     dl=dl, frequency=frequency)
595
            units = flux_density.unit
1✔
596
            ff += flux_density.value
1✔
597
        ff = ff * units
1✔
598
        return ff.to(uu.mJy).value
1✔
599
    else:
600
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 200))
1✔
601
        time_observer_frame = time_temp / day_to_s * (1. + redshift) # in days
1✔
602
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
603
                                                     redshift=redshift, time=time_observer_frame)
604
        full_spec = np.zeros((len(time), len(frequency)))
1✔
605
        for x in range(3):
1✔
606
            lbols = _mosfit_kilonova_one_component_lbol(time=time_temp,
1✔
607
                                                        mej=mej[x], vej=vej[x])
608
            interaction_class = Diffusion(time=time_temp / day_to_s, dense_times=time_temp / day_to_s, luminosity=lbols,
1✔
609
                                          kappa=kappa[x], kappa_gamma=kappa_gamma, mej=mej[x], vej=vej[x]*ckm)
610
            lbols = interaction_class.new_luminosity
1✔
611
            photo = TemperatureFloor(time=time_temp / day_to_s, luminosity=lbols, vej=vej[x]*ckm,
1✔
612
                                     temperature_floor=temperature_floor[x])
613
            fmjy = blackbody_to_flux_density(temperature=photo.photosphere_temperature,
1✔
614
                                             r_photosphere=photo.r_photosphere, frequency=frequency[:, None], dl=dl).T
615
            spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
616
                                         equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
617
            units = spectra.unit
1✔
618
            full_spec += spectra.value
1✔
619

620
        full_spec = full_spec * units
1✔
621
        if kwargs['output_format'] == 'spectra':
1✔
622
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
623
                                                                        lambdas=lambda_observer_frame,
624
                                                                        spectra=full_spec)
625
        else:
626
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
1✔
627
                                                          spectra=full_spec, lambda_array=lambda_observer_frame,
628
                                                          **kwargs)
629

630
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2017ApJ...851L..21V/abstract')
1✔
631
def _mosfit_kilonova_one_component_lbol(time, mej, vej):
1✔
632
    """
633

634
    :param time: time in seconds in source frame
635
    :param mej: mass in solar masses
636
    :param vej: velocity in units of c
637
    :return: lbol in erg/s
638
    """
639
    tdays = time/day_to_s
1✔
640

641
    # set up kilonova physics
642
    av, bv, dv = interpolated_barnes_and_kasen_thermalisation_efficiency(mej, vej)
1✔
643
    # thermalisation from Barnes+16
644
    e_th = 0.36 * (np.exp(-av * tdays) + np.log1p(2.0 * bv * tdays ** dv) / (2.0 * bv * tdays ** dv))
1✔
645
    t0 = 1.3 #seconds
1✔
646
    sig = 0.11  #seconds
1✔
647

648
    m0 = mej * solar_mass
1✔
649
    lum_in = 4.0e18 * (m0) * (0.5 - np.arctan((time - t0) / sig) / np.pi)**1.3
1✔
650
    lbol = lum_in * e_th
1✔
651
    return lbol
1✔
652

653
@citation_wrapper("redback,https://ui.adsabs.harvard.edu/abs/2020ApJ...891..152H/abstract")
1✔
654
def power_law_stratified_kilonova(time, redshift, mej, voffset, v0, alpha,
1✔
655
                                  kappa_offset, kappa_0, zeta, beta, **kwargs):
656
    """
657
    Assumes a power law distribution of ejecta velocities
658
    and a power law distribution of kappa corresponding to the ejecta velocity layers for a total of 10 "shells"
659
    and calculates the kilonova light curve, using kilonova heating rate.
660
    Velocity distribution is flipped so that faster material is the outermost layer as expected for homologous expansion.
661

662
    Must be used with a constraint to avoid prior draws that predict nonsensical luminosities. Or a sensible prior.
663

664
    :param time: time in days in observer frame
665
    :param redshift: redshift
666
    :param mej: total ejecta mass in solar masses
667
    :param voffset: minimum ejecta velocity in units of c
668
    :param v0: velocity normalization in units of c of the power law
669
    :param alpha: power-law index of the velocity distribution i.e., vel = (xs/v0)^-alpha + voffset.
670
    :param kappa_offset: minimum kappa value
671
    :param kappa_0: kappa normalization
672
    :param zeta: power law index of the kappa distribution i.e., kappa = (xs/kappa_0)^-zeta + kappa_offset
673
    :param beta: power law index of density profile
674
    :param kwargs: Additional keyword arguments
675
    :param frequency: Required if output_format is 'flux_density'.
676
        frequency to calculate - Must be same length as time array or a single number).
677
    :param bands: Required if output_format is 'magnitude' or 'flux'.
678
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
679
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
680
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
681
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
682
    """
683
    xs = np.linspace(0.2, 0.5, 10)
1✔
684
    velocity_array = np.flip(xs/v0) ** -alpha + voffset
1✔
685
    xs = np.linspace(0.2, 0.5, 9)
1✔
686
    kappa_array = (xs/kappa_0) ** -zeta + kappa_offset
1✔
687
    output = _kilonova_hr(time=time, redshift=redshift, mej=mej, velocity_array=velocity_array,
1✔
688
                          kappa_array=kappa_array, beta=beta, **kwargs)
689
    return output
1✔
690

691
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2022MNRAS.516.1137L/abstract')
1✔
692
def bulla_bns_kilonova(time, redshift, mej_dyn, mej_disk, phi, costheta_obs, **kwargs):
1✔
693
    """
694
    Kilonovanet model based on Bulla BNS merger simulations
695

696
    :param time: time in days in observer frame
697
    :param redshift: redshift
698
    :param mej_dyn: dynamical mass of ejecta in solar masses
699
    :param mej_disk: disk mass of ejecta in solar masses
700
    :param phi: half-opening angle of the lanthanide-rich tidal dynamical ejecta in degrees
701
    :param costheta_obs: cosine of the observers viewing angle
702
    :param kwargs: Additional keyword arguments
703
    :param frequency: Required if output_format is 'flux_density'.
704
        frequency to calculate - Must be same length as time array or a single number).
705
    :param bands: Required if output_format is 'magnitude' or 'flux'.
706
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
707
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
708
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
709
    """
710
    from redback_surrogates.kilonovamodels import bulla_bns_kilonovanet_spectra as function
1✔
711

712
    cosmology = kwargs.get('cosmology', cosmo)
1✔
713
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
714

715
    if kwargs['output_format'] == 'flux_density':
1✔
716
        frequency = kwargs['frequency']
1✔
717
        frequency, time = calc_kcorrected_properties(frequency=frequency, time=time, redshift=redshift)
1✔
718
        output = function(time_source_frame=time, redshift=redshift, mej_dyn=mej_dyn,
1✔
719
                          mej_disk=mej_disk, phi=phi, costheta_obs=costheta_obs)
720
        spectra = output.spectra / (4 * np.pi * dl ** 2)  # to erg/s/cm^2/Angstrom
1✔
721
        spectra = spectra * uu.erg / (uu.s * uu.cm ** 2 * uu.Angstrom)
1✔
722
        fmjy = spectra.to(uu.mJy, equivalencies=uu.spectral_density(wav=output.lambdas * uu.Angstrom)).value
1✔
723
        nu_array = lambda_to_nu(output.lambdas)
1✔
724
        fmjy_func = RegularGridInterpolator((np.unique(time), nu_array), fmjy, bounds_error=False)
1✔
725
        if type(frequency) == float:
1✔
726
            frequency = np.ones(len(time)) * frequency
1✔
727
        points = np.array([time, frequency]).T
1✔
728
        return fmjy_func(points)
1✔
729
    else:
730
        time_source_frame = np.linspace(0.1, 20, 200)
1✔
731
        output = function(time_source_frame=time_source_frame, redshift=redshift, mej_dyn=mej_dyn,
1✔
732
                          mej_disk=mej_disk, phi=phi, costheta_obs=costheta_obs)
733
        if kwargs['output_format'] == 'spectra':
1✔
734
            return output
×
735
        else:
736
            time_observer_frame = output.time
1✔
737
            lambda_observer_frame = output.lambdas
1✔
738
            spectra = output.spectra / (4 * np.pi * dl ** 2) # to erg/s/cm^2/Angstrom
1✔
739
            spectra = spectra * uu.erg / (uu.s * uu.cm ** 2 * uu.Angstrom)
1✔
740
            time_obs = time
1✔
741
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
1✔
742
                                                   spectra=spectra, lambda_array=lambda_observer_frame,
743
                                                   **kwargs)
744

745
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2022MNRAS.516.1137L/abstract')
1✔
746
def bulla_nsbh_kilonova(time, redshift, mej_dyn, mej_disk, costheta_obs, **kwargs):
1✔
747
    """
748
    Kilonovanet model based on Bulla NSBH merger simulations
749

750
    :param time: time in observer frame in days
751
    :param redshift: redshift
752
    :param mej_dyn: dynamical mass of ejecta in solar masses
753
    :param mej_disk: disk mass of ejecta in solar masses
754
    :param costheta_obs: cosine of the observers viewing angle
755
    :param kwargs: Additional keyword arguments
756
    :param frequency: Required if output_format is 'flux_density'.
757
        frequency to calculate - Must be same length as time array or a single number).
758
    :param bands: Required if output_format is 'magnitude' or 'flux'.
759
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
760
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
761
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
762
    """
763
    from redback_surrogates.kilonovamodels import bulla_nsbh_kilonovanet_spectra as function
1✔
764

765
    cosmology = kwargs.get('cosmology', cosmo)
1✔
766
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
767

768
    if kwargs['output_format'] == 'flux_density':
1✔
769
        frequency = kwargs['frequency']
1✔
770
        frequency, time = calc_kcorrected_properties(frequency=frequency, time=time, redshift=redshift)
1✔
771
        output = function(time_source_frame=time, redshift=redshift, mej_dyn=mej_dyn,
1✔
772
                          mej_disk=mej_disk, costheta_obs=costheta_obs)
773
        spectra = output.spectra / (4 * np.pi * dl ** 2)  # to erg/s/cm^2/Angstrom
1✔
774
        spectra = spectra * uu.erg / (uu.s * uu.cm ** 2 * uu.Angstrom)
1✔
775
        fmjy = spectra.to(uu.mJy, equivalencies=uu.spectral_density(wav=output.lambdas * uu.Angstrom)).value
1✔
776
        nu_array = lambda_to_nu(output.lambdas)
1✔
777
        fmjy_func = RegularGridInterpolator((np.unique(time), nu_array), fmjy, bounds_error=False)
1✔
778
        if type(frequency) == float:
1✔
779
            frequency = np.ones(len(time)) * frequency
1✔
780
        points = np.array([time, frequency]).T
1✔
781
        return fmjy_func(points)
1✔
782
    else:
783
        time_source_frame = np.linspace(0.1, 20, 200)
1✔
784
        output = function(time_source_frame=time_source_frame, redshift=redshift, mej_dyn=mej_dyn,
1✔
785
                          mej_disk=mej_disk, costheta_obs=costheta_obs)
786
        if kwargs['output_format'] == 'spectra':
1✔
787
            return output
×
788
        else:
789
            time_observer_frame = output.time
1✔
790
            lambda_observer_frame = output.lambdas
1✔
791
            spectra = output.spectra / (4 * np.pi * dl ** 2) # to erg/s/cm^2/Angstrom
1✔
792
            spectra = spectra * uu.erg / (uu.s * uu.cm ** 2 * uu.Angstrom)
1✔
793
            time_obs = time
1✔
794
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
1✔
795
                                                   spectra=spectra, lambda_array=lambda_observer_frame,
796
                                                   **kwargs)
797

798
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2022MNRAS.516.1137L/abstract')
1✔
799
def kasen_bns_kilonova(time, redshift, mej, vej, chi, **kwargs):
1✔
800
    """
801
    Kilonovanet model based on Kasen BNS simulations
802

803
    :param time: time in days in observer frame
804
    :param redshift: redshift
805
    :param mej: ejecta mass in solar masses
806
    :param vej: ejecta velocity in units of c
807
    :param chi: lanthanide fraction
808
    :param kwargs: Additional keyword arguments
809
    :param frequency: Required if output_format is 'flux_density'.
810
        frequency to calculate - Must be same length as time array or a single number).
811
    :param bands: Required if output_format is 'magnitude' or 'flux'.
812
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
813
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
814
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
815
    """
816
    from redback_surrogates.kilonovamodels import kasen_bns_kilonovanet_spectra as function
1✔
817

818
    cosmology = kwargs.get('cosmology', cosmo)
1✔
819
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
820

821
    if kwargs['output_format'] == 'flux_density':
1✔
822
        frequency = kwargs['frequency']
1✔
823
        frequency, time = calc_kcorrected_properties(frequency=frequency, time=time, redshift=redshift)
1✔
824
        output = function(time_source_frame=time,redshift=redshift, mej=mej, vej=vej, chi=chi)
1✔
825
        spectra = output.spectra / (4 * np.pi * dl ** 2) # to erg/s/cm^2/Angstrom
1✔
826
        spectra = spectra * uu.erg / (uu.s * uu.cm ** 2 * uu.Angstrom)
1✔
827
        fmjy = spectra.to(uu.mJy, equivalencies=uu.spectral_density(wav=output.lambdas * uu.Angstrom)).value
1✔
828
        nu_array = lambda_to_nu(output.lambdas)
1✔
829
        fmjy_func = RegularGridInterpolator((np.unique(time), nu_array), fmjy, bounds_error=False)
1✔
830
        if type(frequency) == float or type(frequency) == np.float64:
1✔
831
            frequency = np.ones(len(time)) * frequency
1✔
832
        points = np.array([time, frequency]).T
1✔
833
        return fmjy_func(points)
1✔
834
    else:
835
        time_source_frame = np.linspace(0.1, 20, 200)
1✔
836
        output = function(time_source_frame=time_source_frame, redshift=redshift, mej=mej, vej=vej, chi=chi)
1✔
837
        if kwargs['output_format'] == 'spectra':
1✔
838
            return output
×
839
        else:
840
            time_observer_frame = output.time
1✔
841
            lambda_observer_frame = output.lambdas
1✔
842
            spectra = output.spectra / (4 * np.pi * dl ** 2) # to erg/s/cm^2/Angstrom
1✔
843
            spectra = spectra * uu.erg / (uu.s * uu.cm ** 2 * uu.Angstrom)
1✔
844
            time_obs = time
1✔
845
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
1✔
846
                                                   spectra=spectra, lambda_array=lambda_observer_frame,
847
                                                   **kwargs)
848
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2020ApJ...891..152H/abstract')
1✔
849
def two_layer_stratified_kilonova(time, redshift, mej, vej_1, vej_2, kappa, beta, **kwargs):
1✔
850
    """
851
    Uses kilonova_heating_rate module to model a two layer stratified kilonova
852

853
    :param time: observer frame time in days
854
    :param redshift: redshift
855
    :param mej: ejecta mass in solar masses
856
    :param vej_1: velocity of inner shell in c
857
    :param vej_2: velocity of outer shell in c
858
    :param kappa: constant gray opacity
859
    :param beta: power law index of density profile
860
    :param kwargs: Additional keyword arguments
861
    :param frequency: Required if output_format is 'flux_density'.
862
        frequency to calculate - Must be same length as time array or a single number).
863
    :param bands: Required if output_format is 'magnitude' or 'flux'.
864
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
865
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
866
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
867
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
868
    """
869
    velocity_array = np.array([vej_1, vej_2])
1✔
870
    output = _kilonova_hr(time, redshift, mej, velocity_array, kappa, beta, **kwargs)
1✔
871
    return output
1✔
872

873

874
def _kilonova_hr(time, redshift, mej, velocity_array, kappa_array, beta, **kwargs):
1✔
875
    """
876
    Uses kilonova_heating_rate module
877

878
    :param time: observer frame time in days
879
    :param redshift: redshift
880
    :param frequency: frequency to calculate - Must be same length as time array or a single number
881
    :param mej: ejecta mass
882
    :param velocity_array: array of ejecta velocities; length >=2
883
    :param kappa_array: opacities of each shell, length = 1 less than velocity
884
    :param beta: power law index of density profile
885
    :param kwargs: Additional keyword arguments
886
    :param frequency: Required if output_format is 'flux_density'.
887
        frequency to calculate - Must be same length as time array or a single number).
888
    :param bands: Required if output_format is 'magnitude' or 'flux'.
889
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
890
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
891
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
892
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
893
    """
894
    cosmology = kwargs.get('cosmology', cosmo)
1✔
895
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
896
    time_obs = time
1✔
897

898
    if kwargs['output_format'] == 'flux_density':
1✔
899
        frequency = kwargs['frequency']
1✔
900
        time = time * day_to_s
1✔
901
        # convert to source frame time and frequency
902
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
903
        if (isinstance(frequency, (float, int)) == False):
1✔
904
            radio_mask = frequency < 10e10
×
905
            frequency[radio_mask]=10e50
×
906
        elif frequency < 10e10:
1✔
907
            frequency =10e50
×
908

909
        _, temperature, r_photosphere = _kilonova_hr_sourceframe(time, mej, velocity_array, kappa_array, beta)
1✔
910

911
        flux_density = blackbody_to_flux_density(temperature=temperature.value, r_photosphere=r_photosphere.value,
1✔
912
                                                 dl=dl, frequency=frequency)
913
        return flux_density.to(uu.mJy).value
1✔
914
    else:
915
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 200))
1✔
916
        time_observer_frame = np.geomspace(0.03, 10, 100) * day_to_s
1✔
917
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
918
                                                     redshift=redshift, time=time_observer_frame)
919
        _, temperature, r_photosphere = _kilonova_hr_sourceframe(time, mej, velocity_array, kappa_array, beta)
1✔
920
        fmjy = blackbody_to_flux_density(temperature=temperature.value,
1✔
921
                                         r_photosphere=r_photosphere.value, frequency=frequency[:, None], dl=dl)
922
        fmjy = fmjy.T
1✔
923
        spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
924
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
925
        if kwargs['output_format'] == 'spectra':
1✔
926
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
927
                                                                          lambdas=lambda_observer_frame,
928
                                                                          spectra=spectra)
929
        else:
930
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame / day_to_s,
1✔
931
                                                          spectra=spectra, lambda_array=lambda_observer_frame,
932
                                                          **kwargs)
933

934
def _kilonova_hr_sourceframe(time, mej, velocity_array, kappa_array, beta):
1✔
935
    """
936
    Uses kilonova_heating_rate module
937

938
    :param time: source frame time in seconds
939
    :param mej: ejecta mass
940
    :param velocity_array: array of ejecta velocities; length >=2
941
    :param kappa_array: opacities of each shell, length = 1 less than velocity
942
    :param beta: power law index of density profile
943
    :return: bolometric_luminosity, temperature, photosphere
944
    """
945
    if len(velocity_array) < 2:
1✔
946
        raise ValueError("velocity_array must be of length >=2")
×
947

948
    from kilonova_heating_rate import lightcurve
1✔
949

950
    mej = mej * uu.M_sun
1✔
951
    velocity_array = velocity_array * cc.c
1✔
952
    kappa_array = kappa_array * uu.cm**2 / uu.g
1✔
953
    time = time * uu.s
1✔
954
    time = time.to(uu.day)
1✔
955
    if time.value[0] < 0.02:
1✔
956
        raise ValueError("time in source frame must be larger than 0.02 days for this model")
×
957

958
    bolometric_luminosity, temperature, r_photosphere = lightcurve(time, mass=mej, velocities=velocity_array,
1✔
959
                                                                   opacities=kappa_array, n=beta)
960
    return bolometric_luminosity, temperature, r_photosphere
1✔
961

962
@citation_wrapper('redback')
1✔
963
def three_component_kilonova_model(time, redshift, mej_1, vej_1, temperature_floor_1, kappa_1,
1✔
964
                                 mej_2, vej_2, temperature_floor_2, kappa_2,
965
                                   mej_3, vej_3, temperature_floor_3, kappa_3, **kwargs):
966
    """
967
    :param time: observer frame time in days
968
    :param redshift: redshift
969
    :param mej_1: ejecta mass in solar masses of first component
970
    :param vej_1: minimum initial velocity of first component
971
    :param kappa_1: gray opacity of first component
972
    :param temperature_floor_1: floor temperature of first component
973
    :param mej_2: ejecta mass in solar masses of second component
974
    :param vej_2: minimum initial velocity of second component
975
    :param temperature_floor_2: floor temperature of second component
976
    :param kappa_2: gray opacity of second component
977
    :param mej_3: ejecta mass in solar masses of third component
978
    :param vej_3: minimum initial velocity of third component
979
    :param temperature_floor_3: floor temperature of third component
980
    :param kappa_3: gray opacity of third component
981
    :param kwargs: Additional keyword arguments
982
    :param frequency: Required if output_format is 'flux_density'.
983
        frequency to calculate - Must be same length as time array or a single number).
984
    :param bands: Required if output_format is 'magnitude' or 'flux'.
985
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
986
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
987
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
988
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
989
    """
990
    cosmology = kwargs.get('cosmology', cosmo)
1✔
991
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
992
    time_temp = np.geomspace(1e-2, 7e6, 300) # in source frame
1✔
993
    time_obs = time
1✔
994

995
    mej = [mej_1, mej_2, mej_3]
1✔
996
    vej = [vej_1, vej_2, vej_3]
1✔
997
    temperature_floor = [temperature_floor_1, temperature_floor_2, temperature_floor_3]
1✔
998
    kappa = [kappa_1, kappa_2, kappa_3]
1✔
999

1000
    if kwargs['output_format'] == 'flux_density':
1✔
1001
        time = time * day_to_s
1✔
1002
        frequency = kwargs['frequency']
1✔
1003

1004
        # convert to source frame time and frequency
1005
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1006

1007
        ff = np.zeros(len(time))
1✔
1008
        for x in range(3):
1✔
1009
            temp_kwargs = {}
1✔
1010
            temp_kwargs['temperature_floor'] = temperature_floor[x]
1✔
1011
            _, temperature, r_photosphere = _one_component_kilonova_model(time_temp, mej[x], vej[x], kappa[x],
1✔
1012
                                                                          **temp_kwargs)
1013
            # interpolate properties onto observation times
1014
            temp_func = interp1d(time_temp, y=temperature)
1✔
1015
            rad_func = interp1d(time_temp, y=r_photosphere)
1✔
1016
            temp = temp_func(time)
1✔
1017
            photosphere = rad_func(time)
1✔
1018
            flux_density = blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
1019
                                                     dl=dl, frequency=frequency)
1020
            units = flux_density.unit
1✔
1021
            ff += flux_density.value
1✔
1022

1023
        ff = ff * units
1✔
1024
        return ff.to(uu.mJy).value
1✔
1025

1026
    else:
1027
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 200))
1✔
1028
        time_observer_frame = time_temp * (1. + redshift)
1✔
1029
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
1030
                                                     redshift=redshift, time=time_observer_frame)
1031
        full_spec = np.zeros((len(time), len(frequency)))
1✔
1032
        for x in range(3):
1✔
1033
            temp_kwargs = {}
1✔
1034
            temp_kwargs['temperature_floor'] = temperature_floor[x]
1✔
1035
            _, temperature, r_photosphere = _one_component_kilonova_model(time_temp, mej[x], vej[x], kappa[x],
1✔
1036
                                                                          **temp_kwargs)
1037
            fmjy = blackbody_to_flux_density(temperature=temperature,
1✔
1038
                                             r_photosphere=r_photosphere, frequency=frequency[:, None], dl=dl)
1039
            fmjy = fmjy.T
1✔
1040
            spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
1041
                                         equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
1042
            units = spectra.unit
1✔
1043
            full_spec += spectra.value
1✔
1044

1045
        full_spec = full_spec * units
1✔
1046
        if kwargs['output_format'] == 'spectra':
1✔
1047
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
1048
                                                                          lambdas=lambda_observer_frame,
1049
                                                                          spectra=full_spec)
1050
        else:
1051
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame / day_to_s,
1✔
1052
                                                          spectra=full_spec, lambda_array=lambda_observer_frame,
1053
                                                          **kwargs)
1054

1055

1056
@citation_wrapper('redback')
1✔
1057
def two_component_kilonova_model(time, redshift, mej_1, vej_1, temperature_floor_1, kappa_1,
1✔
1058
                                 mej_2, vej_2, temperature_floor_2, kappa_2, **kwargs):
1059
    """
1060
    :param time: observer frame time in days
1061
    :param redshift: redshift
1062
    :param mej_1: ejecta mass in solar masses of first component
1063
    :param vej_1: minimum initial velocity of first component
1064
    :param kappa_1: gray opacity of first component
1065
    :param temperature_floor_1: floor temperature of first component
1066
    :param mej_2: ejecta mass in solar masses of second component
1067
    :param vej_2: minimum initial velocity of second component
1068
    :param temperature_floor_2: floor temperature of second component
1069
    :param kappa_2: gray opacity of second component
1070
    :param kwargs: Additional keyword arguments
1071
    :param frequency: Required if output_format is 'flux_density'.
1072
        frequency to calculate - Must be same length as time array or a single number).
1073
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1074
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1075
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1076
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1077
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1078
    """
1079
    cosmology = kwargs.get('cosmology', cosmo)
1✔
1080
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
1081
    time_temp = np.geomspace(1e-2, 7e6, 300) # in source frame
1✔
1082
    time_obs = time
1✔
1083

1084
    mej = [mej_1, mej_2]
1✔
1085
    vej = [vej_1, vej_2]
1✔
1086
    temperature_floor = [temperature_floor_1, temperature_floor_2]
1✔
1087
    kappa = [kappa_1, kappa_2]
1✔
1088

1089
    if kwargs['output_format'] == 'flux_density':
1✔
1090
        time = time * day_to_s
1✔
1091
        frequency = kwargs['frequency']
1✔
1092

1093
        # convert to source frame time and frequency
1094
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1095

1096
        ff = np.zeros(len(time))
1✔
1097
        for x in range(2):
1✔
1098
            temp_kwargs = {}
1✔
1099
            temp_kwargs['temperature_floor'] = temperature_floor[x]
1✔
1100
            _, temperature, r_photosphere = _one_component_kilonova_model(time_temp, mej[x], vej[x], kappa[x],
1✔
1101
                                                                          **temp_kwargs)
1102
            # interpolate properties onto observation times
1103
            temp_func = interp1d(time_temp, y=temperature)
1✔
1104
            rad_func = interp1d(time_temp, y=r_photosphere)
1✔
1105
            temp = temp_func(time)
1✔
1106
            photosphere = rad_func(time)
1✔
1107
            flux_density = blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
1108
                                                     dl=dl, frequency=frequency)
1109
            units = flux_density.unit
1✔
1110
            ff += flux_density.value
1✔
1111

1112
        ff = ff * units
1✔
1113
        return ff.to(uu.mJy).value
1✔
1114

1115
    else:
1116
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 200))
1✔
1117
        time_observer_frame = time_temp * (1. + redshift)
1✔
1118
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
1119
                                                     redshift=redshift, time=time_observer_frame)
1120
        full_spec = np.zeros((len(time), len(frequency)))
1✔
1121

1122
        for x in range(2):
1✔
1123
            temp_kwargs = {}
1✔
1124
            temp_kwargs['temperature_floor'] = temperature_floor[x]
1✔
1125
            _, temperature, r_photosphere = _one_component_kilonova_model(time_temp, mej[x], vej[x], kappa[x],
1✔
1126
                                                                          **temp_kwargs)
1127
            fmjy = blackbody_to_flux_density(temperature=temperature,
1✔
1128
                                             r_photosphere=r_photosphere, frequency=frequency[:, None], dl=dl)
1129
            fmjy = fmjy.T
1✔
1130
            spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
1131
                                         equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
1132
            units = spectra.unit
1✔
1133
            full_spec += spectra.value
1✔
1134

1135
        full_spec = full_spec * units
1✔
1136
        if kwargs['output_format'] == 'spectra':
1✔
1137
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
1138
                                                                           lambdas=lambda_observer_frame,
1139
                                                                           spectra=full_spec)
1140
        else:
1141
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame/day_to_s,
1✔
1142
                                                          spectra=full_spec, lambda_array=lambda_observer_frame,
1143
                                                          **kwargs)
1144

1145
@citation_wrapper('redback')
1✔
1146
def one_component_ejecta_relation(time, redshift, mass_1, mass_2,
1✔
1147
                                  lambda_1, lambda_2, kappa, **kwargs):
1148
    """
1149
    Assumes no velocity projection in the ejecta velocity ejecta relation
1150

1151
    :param time: observer frame time in days
1152
    :param redshift: redshift
1153
    :param mass_1: mass of primary in solar masses
1154
    :param mass_2: mass of secondary in solar masses
1155
    :param lambda_1: dimensionless tidal deformability of primary
1156
    :param lambda_2: dimensionless tidal deformability of secondary
1157
    :param kappa: gray opacity
1158
    :param kwargs: Additional keyword arguments
1159
    :param temperature_floor: Temperature floor in K (default 4000)
1160
    :param frequency: Required if output_format is 'flux_density'.
1161
        frequency to calculate - Must be same length as time array or a single number).
1162
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1163
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1164
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1165
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1166
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1167
    """
1168
    ejecta_relation = kwargs.get('ejecta_relation', ejr.OneComponentBNSNoProjection)
1✔
1169
    ejecta_relation = ejecta_relation(mass_1, mass_2, lambda_1, lambda_2)
1✔
1170
    mej = ejecta_relation.ejecta_mass
1✔
1171
    vej = ejecta_relation.ejecta_velocity
1✔
1172
    flux_density = one_component_kilonova_model(time, redshift, mej, vej, kappa, **kwargs)
1✔
1173
    return flux_density
1✔
1174

1175
@citation_wrapper('redback')
1✔
1176
def one_component_ejecta_relation_projection(time, redshift, mass_1, mass_2,
1✔
1177
                                             lambda_1, lambda_2, kappa, **kwargs):
1178
    """
1179
    Assumes a velocity projection between the orthogonal and orbital plane
1180

1181
    :param time: observer frame time in days
1182
    :param redshift: redshift
1183
    :param mass_1: mass of primary in solar masses
1184
    :param mass_2: mass of secondary in solar masses
1185
    :param lambda_1: dimensionless tidal deformability of primary
1186
    :param lambda_2: dimensionless tidal deformability of secondary
1187
    :param kappa: gray opacity
1188
    :param kwargs: Additional keyword arguments
1189
    :param temperature_floor: Temperature floor in K (default 4000)
1190
    :param frequency: Required if output_format is 'flux_density'.
1191
        frequency to calculate - Must be same length as time array or a single number).
1192
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1193
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1194
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1195
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1196
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1197
    """
1198
    ejecta_relation = kwargs.get('ejecta_relation', ejr.OneComponentBNSProjection)
1✔
1199
    ejecta_relation = ejecta_relation(mass_1, mass_2, lambda_1, lambda_2)
1✔
1200
    mej = ejecta_relation.ejecta_mass
1✔
1201
    vej = ejecta_relation.ejecta_velocity
1✔
1202
    flux_density = one_component_kilonova_model(time, redshift, mej, vej, kappa, **kwargs)
1✔
1203
    return flux_density
1✔
1204

1205
@citation_wrapper('redback')
1✔
1206
def two_component_bns_ejecta_relation(time, redshift, mass_1, mass_2,
1✔
1207
                                        lambda_1, lambda_2, mtov, zeta, vej_2, kappa_1, kappa_2, tf_1, tf_2, **kwargs):
1208
    """
1209
    Assumes two kilonova components corresponding to dynamical and disk wind ejecta with properties
1210
    derived using ejecta relation specified by keyword argument.
1211

1212
    :param time: observer frame time in days
1213
    :param redshift: redshift
1214
    :param mass_1: mass of primary in solar masses
1215
    :param mass_2: mass of secondary in solar masses
1216
    :param lambda_1: dimensionless tidal deformability of primary
1217
    :param lambda_2: dimensionless tidal deformability of secondary
1218
    :param mtov: Tolman Oppenheimer Volkoff mass in solar masses
1219
    :param zeta: fraction of disk that gets unbound
1220
    :param vej_2: disk wind velocity in c
1221
    :param kappa_1: gray opacity of first component
1222
    :param kappa_2: gracy opacity of second component
1223
    :param tf_1: floor temperature of first component
1224
    :param tf_2: floor temperature of second component
1225
    :param kwargs: additional keyword arguments
1226
    :param ejecta_relation: a class that relates the instrinsic parameters to the kilonova parameters
1227
            default is TwoComponentBNS
1228
    :param frequency: Required if output_format is 'flux_density'.
1229
        frequency to calculate - Must be same length as time array or a single number).
1230
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1231
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1232
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1233
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1234
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1235
    """
1236
    ejecta_relation = kwargs.get('ejecta_relation', ejr.TwoComponentBNS)
1✔
1237
    ejecta_relation = ejecta_relation(mass_1=mass_1, mass_2=mass_2, lambda_1=lambda_1,
1✔
1238
                                      lambda_2=lambda_2, mtov=mtov, zeta=zeta)
1239
    mej_1 = ejecta_relation.dynamical_mej
1✔
1240
    mej_2 = ejecta_relation.disk_wind_mej
1✔
1241
    vej_1 = ejecta_relation.ejecta_velocity
1✔
1242

1243
    output = two_component_kilonova_model(time=time, redshift=redshift, mej_1=mej_1,
1✔
1244
                                                vej_1=vej_1, temperature_floor_1=tf_1,
1245
                                                kappa_1=kappa_1, mej_2=mej_2, vej_2=vej_2,
1246
                                                temperature_floor_2=tf_2, kappa_2=kappa_2, **kwargs)
1247
    return output
1✔
1248

1249
@citation_wrapper('redback')
1✔
1250
def polytrope_eos_two_component_bns(time, redshift, mass_1, mass_2,  log_p, gamma_1, gamma_2, gamma_3,
1✔
1251
                                    zeta, vej_2, kappa_1, kappa_2, tf_1, tf_2, **kwargs):
1252
    """
1253
    Assumes two kilonova components corresponding to dynamical and disk wind ejecta with properties
1254
    derived using ejecta relation specified by keyword argument and lambda set by polytropic EOS.
1255

1256
    :param time: observer frame time in days
1257
    :param redshift: redshift
1258
    :param mass_1: mass of primary in solar masses
1259
    :param mass_2: mass of secondary in solar masses
1260
    :param log_p: log central pressure in SI units
1261
    :param gamma_1: polytrope index 1
1262
    :param gamma_2: polytrope index 2
1263
    :param gamma_3: polytrope index 3
1264
    :param zeta: fraction of disk that gets unbound
1265
    :param vej_2: disk wind velocity in c
1266
    :param kappa_1: gray opacity of first component
1267
    :param kappa_2: gracy opacity of second component
1268
    :param tf_1: floor temperature of first component
1269
    :param tf_2: floor temperature of second component
1270
    :param kwargs: additional keyword arguments
1271
    :param ejecta_relation: a class that relates the instrinsic parameters to the kilonova parameters
1272
            default is TwoComponentBNS
1273
    :param frequency: Required if output_format is 'flux_density'.
1274
        frequency to calculate - Must be same length as time array or a single number).
1275
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1276
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1277
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1278
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1279
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1280
    """
1281
    central_pressure = np.logspace(np.log10(4e32), np.log10(2.5e35), 70)
1✔
1282
    eos = PiecewisePolytrope(log_p=log_p, gamma_1=gamma_1, gamma_2=gamma_2, gamma_3=gamma_3)
1✔
1283
    mtov = eos.maximum_mass()
1✔
1284
    masses = np.array([mass_1, mass_2])
1✔
1285
    tidal_deformability, _ = eos.lambda_of_mass(central_pressure=central_pressure, mass=masses)
1✔
1286
    lambda_1, lambda_2 = tidal_deformability[0], tidal_deformability[1]
1✔
1287
    ejecta_relation = kwargs.get('ejecta_relation', ejr.TwoComponentBNS)
1✔
1288
    ejecta_relation = ejecta_relation(mass_1=mass_1, mass_2=mass_2, lambda_1=lambda_1,
1✔
1289
                                      lambda_2=lambda_2, mtov=mtov, zeta=zeta)
1290
    mej_1 = ejecta_relation.dynamical_mej
1✔
1291
    mej_2 = ejecta_relation.disk_wind_mej
1✔
1292
    vej_1 = ejecta_relation.ejecta_velocity
1✔
1293

1294
    output = two_component_kilonova_model(time=time, redshift=redshift, mej_1=mej_1,
1✔
1295
                                                vej_1=vej_1, temperature_floor_1=tf_1,
1296
                                                kappa_1=kappa_1, mej_2=mej_2, vej_2=vej_2,
1297
                                                temperature_floor_2=tf_2, kappa_2=kappa_2, **kwargs)
1298
    return output
1✔
1299

1300
@citation_wrapper('redback')
1✔
1301
def one_component_nsbh_ejecta_relation(time, redshift, mass_bh, mass_ns,
1✔
1302
                                        chi_bh, lambda_ns, kappa, **kwargs):
1303
    """
1304
    One component NSBH model
1305

1306
    :param time: observer frame time in days
1307
    :param redshift: redshift
1308
    :param mass_bh: mass of black hole
1309
    :param mass_2: mass of neutron star
1310
    :param chi_bh: spin of black hole along z axis
1311
    :param lambda_ns: tidal deformability of neutron star
1312
    :param kappa: opacity
1313
    :param kwargs: additional keyword arguments
1314
    :param temperature_floor: floor temperature
1315
    :param ejecta_relation: a class that relates the instrinsic parameters to the kilonova parameters
1316
            default is OneComponentNSBH
1317
    :param frequency: Required if output_format is 'flux_density'.
1318
        frequency to calculate - Must be same length as time array or a single number).
1319
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1320
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1321
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1322
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1323
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1324
    """
1325
    ejecta_relation = kwargs.get('ejecta_relation', ejr.OneComponentNSBH)
1✔
1326
    ejecta_relation = ejecta_relation(mass_bh=mass_bh, mass_ns=mass_ns, chi_bh=chi_bh, lambda_ns=lambda_ns)
1✔
1327
    mej = ejecta_relation.ejecta_mass
1✔
1328
    vej = ejecta_relation.ejecta_velocity
1✔
1329
    output = one_component_kilonova_model(time, redshift, mej, vej, kappa, **kwargs)
1✔
1330
    return output
1✔
1331

1332
@citation_wrapper('redback')
1✔
1333
def two_component_nsbh_ejecta_relation(time, redshift,  mass_bh, mass_ns,
1✔
1334
                                        chi_bh, lambda_ns, zeta, vej_2, kappa_1, kappa_2, tf_1, tf_2, **kwargs):
1335
    """
1336
    Two component NSBH model with dynamical and disk wind ejecta
1337

1338
    :param time: observer frame time in days
1339
    :param redshift: redshift
1340
    :param mass_bh: mass of black hole
1341
    :param mass_2: mass of neutron star
1342
    :param chi_bh: spin of black hole along z axis
1343
    :param lambda_ns: tidal deformability of neutron star
1344
    :param zeta: fraction of disk that gets unbound
1345
    :param vej_2: disk wind velocity in c
1346
    :param kappa_1: gray opacity of first component
1347
    :param kappa_2: gracy opacity of second component
1348
    :param tf_1: floor temperature of first component
1349
    :param tf_2: floor temperature of second component
1350
    :param kwargs: additional keyword arguments
1351
    :param ejecta_relation: a class that relates the instrinsic parameters to the kilonova parameters
1352
            default is TwoComponentNSBH
1353
    :param frequency: Required if output_format is 'flux_density'.
1354
        frequency to calculate - Must be same length as time array or a single number).
1355
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1356
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1357
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1358
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1359
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1360
    """
1361
    ejecta_relation = kwargs.get('ejecta_relation', ejr.TwoComponentNSBH)
1✔
1362
    ejecta_relation = ejecta_relation(mass_bh=mass_bh, mass_ns=mass_ns, chi_bh=chi_bh,
1✔
1363
                                      lambda_ns=lambda_ns, zeta=zeta)
1364
    mej_1 = ejecta_relation.dynamical_mej
1✔
1365
    mej_2 = ejecta_relation.disk_wind_mej
1✔
1366
    vej_1 = ejecta_relation.ejecta_velocity
1✔
1367

1368
    output = two_component_kilonova_model(time=time, redshift=redshift, mej_1=mej_1,
1✔
1369
                                                vej_1=vej_1, temperature_floor_1=tf_1,
1370
                                                kappa_1=kappa_1, mej_2=mej_2, vej_2=vej_2,
1371
                                                temperature_floor_2=tf_2, kappa_2=kappa_2, **kwargs)
1372
    return output
1✔
1373

1374
@citation_wrapper('redback')
1✔
1375
def one_component_kilonova_model(time, redshift, mej, vej, kappa, **kwargs):
1✔
1376
    """
1377
    :param time: observer frame time in days
1378
    :param redshift: redshift
1379
    :param mej: ejecta mass in solar masses
1380
    :param vej: minimum initial velocity
1381
    :param kappa: gray opacity
1382
    :param kwargs: Additional keyword arguments
1383
    :param temperature_floor: Temperature floor in K (default 4000)
1384
    :param frequency: Required if output_format is 'flux_density'.
1385
        frequency to calculate - Must be same length as time array or a single number).
1386
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1387
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1388
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1389
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1390
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1391
    """
1392
    cosmology = kwargs.get('cosmology', cosmo)
1✔
1393
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
1394
    time_temp = np.geomspace(1e-3, 7e6, 300) # in source frame
1✔
1395
    time_obs = time
1✔
1396
    _, temperature, r_photosphere = _one_component_kilonova_model(time_temp, mej, vej, kappa, **kwargs)
1✔
1397

1398
    if kwargs['output_format'] == 'flux_density':
1✔
1399
        time = time_obs * day_to_s
1✔
1400
        frequency = kwargs['frequency']
1✔
1401
        # interpolate properties onto observation times
1402
        temp_func = interp1d(time_temp, y=temperature)
1✔
1403
        rad_func = interp1d(time_temp, y=r_photosphere)
1✔
1404
        # convert to source frame time and frequency
1405
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1406

1407
        temp = temp_func(time)
1✔
1408
        photosphere = rad_func(time)
1✔
1409

1410
        flux_density = blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
1411
                                                 dl=dl, frequency=frequency)
1412

1413
        return flux_density.to(uu.mJy).value
1✔
1414

1415
    else:
1416
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 200))
1✔
1417
        time_observer_frame = time_temp * (1. + redshift)
1✔
1418
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
1419
                                                     redshift=redshift, time=time_observer_frame)
1420
        fmjy = blackbody_to_flux_density(temperature=temperature,
1✔
1421
                                         r_photosphere=r_photosphere, frequency=frequency[:, None], dl=dl)
1422
        fmjy = fmjy.T
1✔
1423
        spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
1424
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
1425
        if kwargs['output_format'] == 'spectra':
1✔
1426
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
1427
                                                                           lambdas=lambda_observer_frame,
1428
                                                                           spectra=spectra)
1429
        else:
1430
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame/day_to_s,
1✔
1431
                                                          spectra=spectra, lambda_array=lambda_observer_frame,
1432
                                                          **kwargs)
1433

1434

1435
def _calc_new_heating_rate(time, mej, electron_fraction, ejecta_velocity, **kwargs):
1✔
1436
    """
1437
    Heating rate prescription following Rosswog and Korobkin 2024
1438

1439
    :param time: time in seconds
1440
    :param mej: ejecta mass in solar masses
1441
    :param electron_fraction: electron fraction
1442
    :param ejecta_velocity: ejecta velocity in c
1443
    :param kwargs: Additional keyword arguments
1444
    :param heating_rate_perturbation: A fudge factor for heating rate to account for uncertainties in the heating rate. Default is 1.0 i.e., no perturbation.
1445
    :param heating_rate_fudge: A fudge factor for each of the terms in the heating rate. Default to 1. i.e., no uncertainty
1446
    Default is 1.0 i.e., no perturbation.
1447
    :return: heating rate in erg/s
1448
    """
1449
    heating_rate_perturbation = kwargs.get('heating_rate_perturbation', 1.0)
1✔
1450
    # rescale
1451
    m0 = mej * solar_mass
1✔
1452
    qdot_in = _calculate_rosswogkorobkin24_qdot(time, ejecta_velocity, electron_fraction)
1✔
1453
    lum_in = qdot_in * m0
1✔
1454
    return lum_in * heating_rate_perturbation
1✔
1455

1456
def _calculate_rosswogkorobkin24_qdot_formula(time_array, e0, alp, t0, sig, alp1,
1✔
1457
                            t1, sig1, c1, tau1, c2, tau2, c3, tau3):
1458
    time = time_array
×
1459
    c1 = np.exp(c1)
×
1460
    c2 = np.exp(c2)
×
1461
    c3 = np.exp(c3)
×
1462
    tau1 = 1e3 * tau1
×
1463
    tau2 = 1e5 * tau2
×
1464
    tau3 = 1e5 * tau3
×
1465
    term1 = 10. ** (e0 + 18) * (0.5 - np.arctan((time - t0) / sig) / np.pi) ** alp
×
1466
    term2 = (0.5 + np.arctan((time - t1) / sig1) / np.pi) ** alp1
×
1467
    term3 = c1 * np.exp(-time / tau1)
×
1468
    term4 = c2 * np.exp(-time / tau2)
×
1469
    term5 = c3 * np.exp(-time / tau3)
×
1470
    lum_in = term1 * term2 + term3 + term4 + term5
×
1471
    return lum_in
×
1472

1473
def _one_component_kilonova_rosswog_heatingrate(time, mej, vej, electron_fraction, **kwargs):
1✔
1474
    tdays = time/day_to_s
1✔
1475
    # set up kilonova physics
1476
    av, bv, dv = interpolated_barnes_and_kasen_thermalisation_efficiency(mej, vej)
1✔
1477
    # thermalisation from Barnes+16
1478
    e_th = 0.36 * (np.exp(-av * tdays) + np.log1p(2.0 * bv * tdays ** dv) / (2.0 * bv * tdays ** dv))
1✔
1479
    temperature_floor = kwargs.get('temperature_floor', 4000)  # kelvin
1✔
1480
    beta = 13.7
1✔
1481

1482
    v0 = vej * speed_of_light
1✔
1483
    m0 = mej * solar_mass
1✔
1484
    kappa = kappa_from_electron_fraction(electron_fraction)
1✔
1485
    tdiff = np.sqrt(2.0 * kappa * (m0) / (beta * v0 * speed_of_light))
1✔
1486

1487
    lum_in = _calc_new_heating_rate(time, mej, electron_fraction, vej, **kwargs)
1✔
1488
    integrand = lum_in * e_th * (time / tdiff) * np.exp(time ** 2 / tdiff ** 2)
1✔
1489

1490
    bolometric_luminosity = np.zeros(len(time))
1✔
1491
    bolometric_luminosity[1:] = cumulative_trapezoid(integrand, time)
1✔
1492
    bolometric_luminosity[0] = bolometric_luminosity[1]
1✔
1493
    bolometric_luminosity = bolometric_luminosity * np.exp(-time ** 2 / tdiff ** 2) / tdiff
1✔
1494

1495
    temperature = (bolometric_luminosity / (4.0 * np.pi * sigma_sb * v0 ** 2 * time ** 2)) ** 0.25
1✔
1496
    r_photosphere = (bolometric_luminosity / (4.0 * np.pi * sigma_sb * temperature_floor ** 4)) ** 0.5
1✔
1497

1498
    # check temperature floor conditions
1499
    mask = temperature <= temperature_floor
1✔
1500
    temperature[mask] = temperature_floor
1✔
1501
    mask = np.logical_not(mask)
1✔
1502
    r_photosphere[mask] = v0 * time[mask]
1✔
1503
    return bolometric_luminosity, temperature, r_photosphere
1✔
1504

1505
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2024arXiv240407271S/abstract, https://ui.adsabs.harvard.edu/abs/2024AnP...53600306R/abstract')
1✔
1506
def one_comp_kne_rosswog_heatingrate(time, redshift, mej, vej, ye, **kwargs):
1✔
1507
    """
1508
    :param time: observer frame time in days
1509
    :param redshift: redshift
1510
    :param mej: ejecta mass in solar masses
1511
    :param vej: minimum initial velocity
1512
    :param kappa: gray opacity
1513
    :param kwargs: Additional keyword arguments
1514
    :param temperature_floor: Temperature floor in K (default 4000)
1515
    :param heating_rate_perturbation: A fudge factor for heating rate to account for uncertainties in the heating rate.
1516
    Default is 1.0 i.e., no perturbation.
1517
    :param heating_rate_fudge: A fudge factor for each of the terms in the heating rate. Default to 1. i.e., no uncertainty
1518
    :param frequency: Required if output_format is 'flux_density'.
1519
        frequency to calculate - Must be same length as time array or a single number).
1520
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1521
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1522
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1523
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1524
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1525
    """
1526
    cosmology = kwargs.get('cosmology', cosmo)
1✔
1527
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
1528
    time_temp = np.geomspace(1e-3, 7e6, 300) # in source frame
1✔
1529
    time_obs = time
1✔
1530
    _, temperature, r_photosphere = _one_component_kilonova_rosswog_heatingrate(time_temp, mej, vej, ye, **kwargs)
1✔
1531

1532
    if kwargs['output_format'] == 'flux_density':
1✔
1533
        time = time_obs * day_to_s
1✔
1534
        frequency = kwargs['frequency']
1✔
1535
        # interpolate properties onto observation times
1536
        temp_func = interp1d(time_temp, y=temperature)
1✔
1537
        rad_func = interp1d(time_temp, y=r_photosphere)
1✔
1538
        # convert to source frame time and frequency
1539
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1540

1541
        temp = temp_func(time)
1✔
1542
        photosphere = rad_func(time)
1✔
1543

1544
        flux_density = blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
1545
                                                 dl=dl, frequency=frequency)
1546

1547
        return flux_density.to(uu.mJy).value
1✔
1548

1549
    else:
1550
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 200))
1✔
1551
        time_observer_frame = time_temp * (1. + redshift)
1✔
1552
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
1553
                                                     redshift=redshift, time=time_observer_frame)
1554
        fmjy = blackbody_to_flux_density(temperature=temperature,
1✔
1555
                                         r_photosphere=r_photosphere, frequency=frequency[:, None], dl=dl)
1556
        fmjy = fmjy.T
1✔
1557
        spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
1558
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
1559
        if kwargs['output_format'] == 'spectra':
1✔
1560
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
1561
                                                                           lambdas=lambda_observer_frame,
1562
                                                                           spectra=spectra)
1563
        else:
1564
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame/day_to_s,
1✔
1565
                                                          spectra=spectra, lambda_array=lambda_observer_frame,
1566
                                                          **kwargs)
1567

1568
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2024arXiv240407271S/abstract, https://ui.adsabs.harvard.edu/abs/2024AnP...53600306R/abstract')
1✔
1569
def two_comp_kne_rosswog_heatingrate(time, redshift, mej_1, vej_1, temperature_floor_1, ye_1,
1✔
1570
                                 mej_2, vej_2, temperature_floor_2, ye_2, **kwargs):
1571
    """
1572
    :param time: observer frame time in days
1573
    :param redshift: redshift
1574
    :param mej_1: ejecta mass in solar masses of first component
1575
    :param vej_1: minimum initial velocity of first component
1576
    :param kappa_1: gray opacity of first component
1577
    :param temperature_floor_1: floor temperature of first component
1578
    :param mej_2: ejecta mass in solar masses of second component
1579
    :param vej_2: minimum initial velocity of second component
1580
    :param temperature_floor_2: floor temperature of second component
1581
    :param kappa_2: gray opacity of second component
1582
    :param kwargs: Additional keyword arguments
1583
    :param heating_rate_perturbation: A fudge factor for heating rate to account for uncertainties in the heating rate.
1584
    Default is 1.0 i.e., no perturbation.
1585
    :param heating_rate_fudge: A fudge factor for each of the terms in the heating rate. Default to 1. i.e., no uncertainty
1586
    :param frequency: Required if output_format is 'flux_density'.
1587
        frequency to calculate - Must be same length as time array or a single number).
1588
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1589
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1590
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1591
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1592
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1593
    """
1594
    cosmology = kwargs.get('cosmology', cosmo)
1✔
1595
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
1596
    time_temp = np.geomspace(1e-2, 7e6, 300) # in source frame
1✔
1597
    time_obs = time
1✔
1598

1599
    mej = [mej_1, mej_2]
1✔
1600
    vej = [vej_1, vej_2]
1✔
1601
    temperature_floor = [temperature_floor_1, temperature_floor_2]
1✔
1602
    ye = [ye_1, ye_2]
1✔
1603

1604
    if kwargs['output_format'] == 'flux_density':
1✔
1605
        time = time * day_to_s
1✔
1606
        frequency = kwargs['frequency']
1✔
1607

1608
        # convert to source frame time and frequency
1609
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1610

1611
        ff = np.zeros(len(time))
1✔
1612
        for x in range(2):
1✔
1613
            temp_kwargs = {}
1✔
1614
            if 'heating_rate_fudge' in kwargs:
1✔
1615
                temp_kwargs['heating_rate_fudge'] = kwargs['heating_rate_fudge']
×
1616
            if 'heating_rate_perturbation' in kwargs:
1✔
1617
                temp_kwargs['heating_rate_perturbation'] = kwargs['heating_rate_perturbation']
×
1618
            temp_kwargs['temperature_floor'] = temperature_floor[x]
1✔
1619
            _, temperature, r_photosphere = _one_component_kilonova_rosswog_heatingrate(time_temp, mej[x], vej[x], ye[x],
1✔
1620
                                                                          **temp_kwargs)
1621
            # interpolate properties onto observation times
1622
            temp_func = interp1d(time_temp, y=temperature)
1✔
1623
            rad_func = interp1d(time_temp, y=r_photosphere)
1✔
1624
            temp = temp_func(time)
1✔
1625
            photosphere = rad_func(time)
1✔
1626
            flux_density = blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
1627
                                                     dl=dl, frequency=frequency)
1628
            units = flux_density.unit
1✔
1629
            ff += flux_density.value
1✔
1630

1631
        ff = ff * units
1✔
1632
        return ff.to(uu.mJy).value
1✔
1633

1634
    else:
1635
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 200))
1✔
1636
        time_observer_frame = time_temp * (1. + redshift)
1✔
1637
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
1638
                                                     redshift=redshift, time=time_observer_frame)
1639
        full_spec = np.zeros((len(time), len(frequency)))
1✔
1640

1641
        for x in range(2):
1✔
1642
            temp_kwargs = {}
1✔
1643
            if 'heating_rate_fudge' in kwargs:
1✔
1644
                temp_kwargs['heating_rate_fudge'] = kwargs['heating_rate_fudge']
×
1645
            if 'heating_rate_perturbation' in kwargs:
1✔
1646
                temp_kwargs['heating_rate_perturbation'] = kwargs['heating_rate_perturbation']
×
1647
            temp_kwargs['temperature_floor'] = temperature_floor[x]
1✔
1648
            _, temperature, r_photosphere = _one_component_kilonova_rosswog_heatingrate(time_temp, mej[x], vej[x], ye[x],
1✔
1649
                                                                          **temp_kwargs)
1650
            fmjy = blackbody_to_flux_density(temperature=temperature,
1✔
1651
                                             r_photosphere=r_photosphere, frequency=frequency[:, None], dl=dl)
1652
            fmjy = fmjy.T
1✔
1653
            spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
1654
                                         equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
1655
            units = spectra.unit
1✔
1656
            full_spec += spectra.value
1✔
1657

1658
        full_spec = full_spec * units
1✔
1659
        if kwargs['output_format'] == 'spectra':
1✔
1660
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
1661
                                                                           lambdas=lambda_observer_frame,
1662
                                                                           spectra=full_spec)
1663
        else:
1664
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame/day_to_s,
1✔
1665
                                                          spectra=full_spec, lambda_array=lambda_observer_frame,
1666
                                                          **kwargs)
1667

1668
def _one_component_kilonova_model(time, mej, vej, kappa, **kwargs):
1✔
1669
    """
1670
    :param time: source frame time in seconds
1671
    :param redshift: redshift
1672
    :param mej: ejecta mass in solar masses
1673
    :param vej: minimum initial velocity
1674
    :param kappa_r: gray opacity
1675
    :param kwargs: temperature_floor
1676
    :return: bolometric_luminosity, temperature, r_photosphere
1677
    """
1678
    tdays = time/day_to_s
1✔
1679

1680
    # set up kilonova physics
1681
    av, bv, dv = interpolated_barnes_and_kasen_thermalisation_efficiency(mej, vej)
1✔
1682
    # thermalisation from Barnes+16
1683
    e_th = 0.36 * (np.exp(-av * tdays) + np.log1p(2.0 * bv * tdays ** dv) / (2.0 * bv * tdays ** dv))
1✔
1684
    t0 = 1.3 #seconds
1✔
1685
    sig = 0.11  #seconds
1✔
1686
    temperature_floor = kwargs.get('temperature_floor', 4000) #kelvin
1✔
1687

1688
    beta = 13.7
1✔
1689

1690
    v0 = vej * speed_of_light
1✔
1691
    m0 = mej * solar_mass
1✔
1692
    tdiff = np.sqrt(2.0 * kappa * (m0) / (beta * v0 * speed_of_light))
1✔
1693
    lum_in = 4.0e18 * (m0) * (0.5 - np.arctan((time - t0) / sig) / np.pi)**1.3
1✔
1694
    integrand = lum_in * e_th * (time/tdiff) * np.exp(time**2/tdiff**2)
1✔
1695
    bolometric_luminosity = np.zeros(len(time))
1✔
1696
    bolometric_luminosity[1:] = cumulative_trapezoid(integrand, time)
1✔
1697
    bolometric_luminosity[0] = bolometric_luminosity[1]
1✔
1698
    bolometric_luminosity = bolometric_luminosity * np.exp(-time**2/tdiff**2) / tdiff
1✔
1699

1700
    temperature = (bolometric_luminosity / (4.0 * np.pi * sigma_sb * v0**2 * time**2))**0.25
1✔
1701
    r_photosphere = (bolometric_luminosity / (4.0 * np.pi * sigma_sb * temperature_floor**4))**0.5
1✔
1702

1703
    # check temperature floor conditions
1704
    mask = temperature <= temperature_floor
1✔
1705
    temperature[mask] = temperature_floor
1✔
1706
    mask = np.logical_not(mask)
1✔
1707
    r_photosphere[mask] = v0 * time[mask]
1✔
1708
    return bolometric_luminosity, temperature, r_photosphere
1✔
1709

1710
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2017LRR....20....3M/abstract')
1✔
1711
def metzger_kilonova_model(time, redshift, mej, vej, beta, kappa, **kwargs):
1✔
1712
    """
1713
    :param time: observer frame time in days
1714
    :param redshift: redshift
1715
    :param mej: ejecta mass in solar masses
1716
    :param vej: minimum initial velocity
1717
    :param beta: velocity power law slope (M=v^-beta)
1718
    :param kappa: gray opacity
1719
    :param kwargs: Additional keyword arguments
1720
    :param neutron_precursor_switch: Whether to include neutron precursor emission
1721
    :param frequency: Required if output_format is 'flux_density'.
1722
        frequency to calculate - Must be same length as time array or a single number).
1723
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1724
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1725
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
1726
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1727
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1728
    """
1729
    cosmology = kwargs.get('cosmology', cosmo)
1✔
1730
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
1731
    time_temp = np.geomspace(1e-4, 7e6, 300) # in source frame
1✔
1732
    time_obs = time
1✔
1733
    bolometric_luminosity, temperature, r_photosphere = _metzger_kilonova_model(time_temp, mej, vej, beta,
1✔
1734
                                                                                kappa, **kwargs)
1735

1736
    if kwargs['output_format'] == 'flux_density':
1✔
1737
        time = time * day_to_s
1✔
1738
        frequency = kwargs['frequency']
1✔
1739

1740
        # interpolate properties onto observation times
1741
        temp_func = interp1d(time_temp, y=temperature)
1✔
1742
        rad_func = interp1d(time_temp, y=r_photosphere)
1✔
1743
        # convert to source frame time and frequency
1744
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1745

1746
        temp = temp_func(time)
1✔
1747
        photosphere = rad_func(time)
1✔
1748

1749
        flux_density = blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
1750
                                                 dl=dl, frequency=frequency)
1751
        return flux_density.to(uu.mJy).value
1✔
1752

1753
    else:
1754
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 200))
1✔
1755
        time_observer_frame = time_temp * (1. + redshift)
1✔
1756
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
1757
                                                     redshift=redshift, time=time_observer_frame)
1758
        fmjy = blackbody_to_flux_density(temperature=temperature,
1✔
1759
                                         r_photosphere=r_photosphere, frequency=frequency[:, None], dl=dl)
1760
        fmjy = fmjy.T
1✔
1761
        spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
1762
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
1763
        if kwargs['output_format'] == 'spectra':
1✔
1764
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
1765
                                                                           lambdas=lambda_observer_frame,
1766
                                                                           spectra=spectra)
1767
        else:
1768
            return get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame / day_to_s,
1✔
1769
                                                          spectra=spectra, lambda_array=lambda_observer_frame,
1770
                                                          **kwargs)
1771

1772

1773
def _metzger_kilonova_model(time, mej, vej, beta, kappa, **kwargs):
1✔
1774
    """
1775
    :param time: time array to evaluate model on in source frame in seconds
1776
    :param redshift: redshift
1777
    :param mej: ejecta mass in solar masses
1778
    :param vej: minimum initial velocity
1779
    :param beta: velocity power law slope (M=v^-beta)
1780
    :param kappa: gray opacity
1781
    :param kwargs: Additional keyword arguments
1782
    :param neutron_precursor_switch: Whether to include neutron precursor emission
1783
    :return: bolometric_luminosity, temperature, photosphere_radius
1784
    """
1785
    neutron_precursor_switch = kwargs.get('neutron_precursor_switch', True)
1✔
1786

1787
    time = time
1✔
1788
    tdays = time/day_to_s
1✔
1789
    time_len = len(time)
1✔
1790
    mass_len = 200
1✔
1791

1792
    # set up kilonova physics
1793
    av, bv, dv = interpolated_barnes_and_kasen_thermalisation_efficiency(mej, vej)
1✔
1794
    # thermalisation from Barnes+16
1795
    e_th = 0.36 * (np.exp(-av * tdays) + np.log1p(2.0 * bv * tdays ** dv) / (2.0 * bv * tdays ** dv))
1✔
1796
    electron_fraction = electron_fraction_from_kappa(kappa)
1✔
1797
    t0 = 1.3 #seconds
1✔
1798
    sig = 0.11  #seconds
1✔
1799
    tau_neutron = 900  #seconds
1✔
1800

1801
    # convert to astrophysical units
1802
    m0 = mej * solar_mass
1✔
1803
    v0 = vej * speed_of_light
1✔
1804

1805
    # set up mass and velocity layers
1806
    vmin = vej
1✔
1807
    vmax = kwargs.get('vmax', 0.7)
1✔
1808
    vel = np.linspace(vmin, vmax, mass_len)
1✔
1809
    m_array = mej * (vel/vmin)**(-beta)
1✔
1810
    v_m = vel * speed_of_light
1✔
1811

1812
    # set up arrays
1813
    time_array = np.tile(time, (mass_len, 1))
1✔
1814
    e_th_array = np.tile(e_th, (mass_len, 1))
1✔
1815
    edotr = np.zeros((mass_len, time_len))
1✔
1816

1817
    time_mask = time > t0
1✔
1818
    time_1 = time_array[:, time_mask]
1✔
1819
    time_2 = time_array[:, ~time_mask]
1✔
1820
    edotr[:,time_mask] = 2.1e10 * e_th_array[:, time_mask] * ((time_1/ (3600. * 24.)) ** (-1.3))
1✔
1821
    edotr[:, ~time_mask] = 4.0e18 * (0.5 - (1. / np.pi) * np.arctan((time_2 - t0) / sig)) ** (1.3) * e_th_array[:,~time_mask]
1✔
1822

1823
    # set up empty arrays
1824
    energy_v = np.zeros((mass_len, time_len))
1✔
1825
    lum_rad = np.zeros((mass_len, time_len))
1✔
1826
    qdot_rp = np.zeros((mass_len, time_len))
1✔
1827
    td_v = np.zeros((mass_len, time_len))
1✔
1828
    tau = np.zeros((mass_len, time_len))
1✔
1829
    v_photosphere = np.zeros(time_len)
1✔
1830
    r_photosphere = np.zeros(time_len)
1✔
1831

1832
    if neutron_precursor_switch == True:
1✔
1833
        neutron_mass = 1e-8 * solar_mass
1✔
1834
        neutron_mass_fraction = 1 - 2*electron_fraction * 2 * np.arctan(neutron_mass / m_array / solar_mass) / np.pi
1✔
1835
        rprocess_mass_fraction = 1.0 - neutron_mass_fraction
1✔
1836
        initial_neutron_mass_fraction_array = np.tile(neutron_mass_fraction, (time_len, 1)).T
1✔
1837
        rprocess_mass_fraction_array = np.tile(rprocess_mass_fraction, (time_len, 1)).T
1✔
1838
        neutron_mass_fraction_array = initial_neutron_mass_fraction_array*np.exp(-time_array / tau_neutron)
1✔
1839
        edotn = 3.2e14 * neutron_mass_fraction_array
1✔
1840
        edotn = edotn * neutron_mass_fraction_array
1✔
1841
        edotr = edotn + edotr
1✔
1842
        kappa_n = 0.4 * (1.0 - neutron_mass_fraction_array - rprocess_mass_fraction_array)
1✔
1843
        kappa = kappa * rprocess_mass_fraction_array
1✔
1844
        kappa = kappa_n + kappa
1✔
1845

1846
    dt = np.diff(time)
1✔
1847
    dm = np.abs(np.diff(m_array))
1✔
1848

1849
    #initial conditions
1850
    energy_v[:, 0] = 0.5 * m_array*v_m**2
1✔
1851
    lum_rad[:, 0] = 0
1✔
1852
    qdot_rp[:, 0] = 0
1✔
1853

1854
    # solve ODE using euler method for all mass shells v
1855
    for ii in range(time_len - 1):
1✔
1856
        if neutron_precursor_switch:
1✔
1857
            td_v[:-1, ii] = (kappa[:-1, ii] * m_array[:-1] * solar_mass * 3) / (
1✔
1858
                    4 * np.pi * v_m[:-1] * speed_of_light * time[ii] * beta)
1859
            tau[:-1, ii] = (m_array[:-1] * solar_mass * kappa[:-1, ii] / (4 * np.pi * (time[ii] * v_m[:-1]) ** 2))
1✔
1860
        else:
1861
            td_v[:-1, ii] = (kappa * m_array[:-1] * solar_mass * 3) / (
×
1862
                        4 * np.pi * v_m[:-1] * speed_of_light * time[ii] * beta)
1863
            tau[:-1, ii] = (m_array[:-1] * solar_mass * kappa / (4 * np.pi * (time[ii] * v_m[:-1]) ** 2))
×
1864
        lum_rad[:-1, ii] = energy_v[:-1, ii] / (td_v[:-1, ii] + time[ii] * (v_m[:-1] / speed_of_light))
1✔
1865
        energy_v[:-1, ii + 1] = (edotr[:-1, ii] - (energy_v[:-1, ii] / time[ii]) - lum_rad[:-1, ii]) * dt[ii] + energy_v[:-1, ii]
1✔
1866
        lum_rad[:-1, ii] = lum_rad[:-1, ii] * dm * solar_mass
1✔
1867

1868
        tau[mass_len - 1, ii] = tau[mass_len - 2, ii]
1✔
1869
        photosphere_index = np.argmin(np.abs(tau[:, ii] - 1))
1✔
1870
        v_photosphere[ii] = v_m[photosphere_index]
1✔
1871
        r_photosphere[ii] = v_photosphere[ii] * time[ii]
1✔
1872

1873
    bolometric_luminosity = np.sum(lum_rad, axis=0)
1✔
1874

1875
    temperature = (bolometric_luminosity / (4.0 * np.pi * (r_photosphere) ** (2.0) * sigma_sb)) ** (0.25)
1✔
1876

1877
    return bolometric_luminosity, temperature, r_photosphere
1✔
1878

1879
def _generate_single_lightcurve(model, t_ini, t_max, dt, **parameters):
1✔
1880
    """
1881
    Generates a single lightcurve for a given `gwemlightcurves` model
1882

1883
    Parameters
1884
    ----------
1885
    model: str
1886
        The `gwemlightcurve` model, e.g. 'DiUj2017'
1887
    t_ini: float
1888
        Starting time of the time array `gwemlightcurves` will calculate values at.
1889
    t_max: float
1890
        End time of the time array `gwemlightcurves` will calculate values at.
1891
    dt: float
1892
        Spacing of time uniform time steps.
1893
    parameters: dict
1894
        Function parameters for the given model.
1895
    Returns
1896
    ----------
1897
    func, func: A bolometric function and the magnitude function.
1898
    """
1899
    from gwemlightcurves.KNModels.table import KNTable
×
1900

1901
    t = Table()
×
1902
    for key in parameters.keys():
×
1903
        val = parameters[key]
×
1904
        t.add_column(Column(data=[val], name=key))
×
1905
    t.add_column(Column(data=[t_ini], name="tini"))
×
1906
    t.add_column(Column(data=[t_max], name="tmax"))
×
1907
    t.add_column(Column(data=[dt], name="dt"))
×
1908
    model_table = KNTable.model(model, t)
×
1909
    return model_table["t"][0], model_table["lbol"][0], model_table["mag"][0]
×
1910

1911

1912
def _generate_single_lightcurve_at_times(model, times, **parameters):
1✔
1913
    """
1914
    Generates a single lightcurve for a given `gwemlightcurves` model
1915

1916
    Parameters
1917
    ----------
1918
    model: str
1919
        The `gwemlightcurve` model, e.g. 'DiUj2017'
1920
    times: array_like
1921
        Times at which we interpolate the `gwemlightcurves` values, in days
1922
    parameters: dict
1923
        Function parameters for the given model.
1924
    Returns
1925
    ----------
1926
    array_like, array_like: bolometric and magnitude arrays.
1927
    """
1928

1929
    tini = times[0]
×
1930
    tmax = times[-1]
×
1931
    dt = (tmax - tini)/(len(times) - 1)
×
1932
    gwem_times, lbol, mag = _generate_single_lightcurve(model=model, t_ini=times[0], t_max=times[-1],
×
1933
                                                        dt=dt, **parameters)
1934

1935
    lbol = interp1d(gwem_times, lbol)(times)
×
1936
    new_mag = []
×
1937
    for m in mag:
×
1938
        new_mag.append(interp1d(gwem_times, m)(times))
×
1939
    return lbol, np.array(new_mag)
×
1940

1941

1942
def _gwemlightcurve_interface_factory(model):
1✔
1943
    """
1944
    Generates `bilby`-compatible functions from `gwemlightcurve` models.
1945

1946
    Parameters
1947
    ----------
1948
    model: str
1949
        The `gwemlightcurve` model, e.g. 'DiUj2017'
1950

1951
    Returns
1952
    ----------
1953
    func, func: A bolometric function and the magnitude function.
1954
    """
1955

1956
    default_filters = ['u', 'g', 'r', 'i', 'z', 'y', 'J', 'H', 'K']
1✔
1957

1958
    def interface_bolometric(times, **parameters):
1✔
1959
        return _generate_single_lightcurve_at_times(model=model, times=times, **parameters)[0]
×
1960

1961
    def interface_all_magnitudes(times, **parameters):
1✔
1962
        magnitudes = _generate_single_lightcurve_at_times(model=model, times=times, **parameters)[1]
×
1963
        return pd.DataFrame(magnitudes.T, columns=default_filters)
×
1964

1965
    def interface_filtered_magnitudes(times, **parameters):
1✔
1966
        filters = parameters.get('filters', default_filters)
×
1967
        all_magnitudes = interface_all_magnitudes(times, **parameters)
×
1968
        if len(filters) == 1:
×
1969
            return all_magnitudes[filters[0]]
×
1970

1971
        filtered_magnitudes = np.zeros(len(times))
×
1972
        for i, f in enumerate(filters):
×
1973
            filtered_magnitudes[i] = all_magnitudes[f][i]
×
1974

1975
        return filtered_magnitudes
×
1976

1977
    return interface_bolometric, interface_filtered_magnitudes
1✔
1978

1979

1980
gwem_DiUj2017_bolometric, gwem_DiUj2017_magnitudes = _gwemlightcurve_interface_factory("DiUj2017")
1✔
1981
gwem_SmCh2017_bolometric, gwem_SmCh2017_magnitudes = _gwemlightcurve_interface_factory("SmCh2017")
1✔
1982
gwem_Me2017_bolometric, gwem_Me2017_magnitudes = _gwemlightcurve_interface_factory("Me2017")
1✔
1983
gwem_KaKy2016_bolometric, gwem_KaKy2016_magnitudes = _gwemlightcurve_interface_factory("KaKy2016")
1✔
1984
gwem_WoKo2017_bolometric, gwem_WoKo2017_magnitudes = _gwemlightcurve_interface_factory("WoKo2017")
1✔
1985
gwem_BaKa2016_bolometric, gwem_BaKa2016_magnitudes = _gwemlightcurve_interface_factory("BaKa2016")
1✔
1986
gwem_Ka2017_bolometric, gwem_Ka2017_magnitudes = _gwemlightcurve_interface_factory("Ka2017")
1✔
1987
gwem_Ka2017x2_bolometric, gwem_Ka2017x2_magnitudes = _gwemlightcurve_interface_factory("Ka2017x2")
1✔
1988
gwem_Ka2017inc_bolometric, gwem_Ka2017inc_magnitudes = _gwemlightcurve_interface_factory("Ka2017inc")
1✔
1989
gwem_Ka2017x2inc_bolometric, gwem_Ka2017x2inc_magnitudes = _gwemlightcurve_interface_factory("Ka2017x2inc")
1✔
1990
gwem_RoFe2017_bolometric, gwem_RoFe2017_magnitudes = _gwemlightcurve_interface_factory("RoFe2017")
1✔
1991
gwem_Bu2019_bolometric, gwem_Bu2019_magnitudes = _gwemlightcurve_interface_factory("Bu2019")
1✔
1992
gwem_Bu2019inc_bolometric, gwem_Bu2019inc_magnitudes = _gwemlightcurve_interface_factory("Bu2019inc")
1✔
1993
gwem_Bu2019lf_bolometric, gwem_Bu2019lf_magnitudes = _gwemlightcurve_interface_factory("Bu2019lf")
1✔
1994
gwem_Bu2019lr_bolometric, gwem_Bu2019lr_magnitudes = _gwemlightcurve_interface_factory("Bu2019lr")
1✔
1995
gwem_Bu2019lm_bolometric, gwem_Bu2019lm_magnitudes = _gwemlightcurve_interface_factory("Bu2019lm")
1✔
1996
gwem_Bu2019lw_bolometric, gwem_Bu2019lw_magnitudes = _gwemlightcurve_interface_factory("Bu2019lw")
1✔
1997
gwem_Bu2019re_bolometric, gwem_Bu2019re_magnitudes = _gwemlightcurve_interface_factory("Bu2019re")
1✔
1998
gwem_Bu2019bc_bolometric, gwem_Bu2019bc_magnitudes = _gwemlightcurve_interface_factory("Bu2019bc")
1✔
1999
gwem_Bu2019op_bolometric, gwem_Bu2019op_magnitudes = _gwemlightcurve_interface_factory("Bu2019op")
1✔
2000
gwem_Bu2019ops_bolometric, gwem_Bu2019ops_magnitudes = _gwemlightcurve_interface_factory("Bu2019ops")
1✔
2001
gwem_Bu2019rp_bolometric, gwem_Bu2019rp_magnitudes = _gwemlightcurve_interface_factory("Bu2019rp")
1✔
2002
gwem_Bu2019rps_bolometric, gwem_Bu2019rps_magnitudes = _gwemlightcurve_interface_factory("Bu2019rps")
1✔
2003
gwem_Wo2020dyn_bolometric, gwem_Wo2020dyn_magnitudes = _gwemlightcurve_interface_factory("Wo2020dyn")
1✔
2004
gwem_Wo2020dw_bolometric, gwem_Wo2020dw_magnitudes = _gwemlightcurve_interface_factory("Wo2020dw")
1✔
2005
gwem_Bu2019nsbh_bolometric, gwem_Bu2019nsbh_magnitudes = _gwemlightcurve_interface_factory("Bu2019nsbh")
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc