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

nikhil-sarin / redback / 18131194739

30 Sep 2025 01:16PM UTC coverage: 87.629% (-0.04%) from 87.665%
18131194739

Pull #293

github

web-flow
Merge 05c94ad74 into 8141796a4
Pull Request #293: Add a class method to load from LightCurveLynx output

47 of 49 new or added lines in 2 files covered. (95.92%)

15 existing lines in 2 files now uncovered.

14429 of 16466 relevant lines covered (87.63%)

0.88 hits per line

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

90.71
/redback/transient_models/tde_models.py
1
import numpy as np
1✔
2
import redback.interaction_processes as ip
1✔
3
import redback.sed as sed
1✔
4
import redback.photosphere as photosphere
1✔
5
from redback.utils import calc_kcorrected_properties, citation_wrapper, calc_tfb, lambda_to_nu, \
1✔
6
    calc_ABmag_from_flux_density, calc_flux_density_from_ABmag, bands_to_frequency
7
import redback.constants as cc
1✔
8
import redback.transient_models.phenomenological_models as pm
1✔
9

10
from collections import namedtuple
1✔
11
from astropy.cosmology import Planck18 as cosmo  # noqa
1✔
12
import astropy.units as uu
1✔
13
from scipy.interpolate import interp1d
1✔
14

15
def _analytic_fallback(time, l0, t_0):
1✔
16
    """
17
    :param time: time in days
18
    :param l0: bolometric luminosity at 1 second in cgs
19
    :param t_0: turn on time in days (after this time lbol decays as 5/3 powerlaw)
20
    :return: bolometric luminosity
21
    """
22
    mask = time - t_0 > 0.
1✔
23
    lbol = np.zeros(len(time))
1✔
24
    lbol[mask] = l0 / (time[mask] * 86400)**(5./3.)
1✔
25
    lbol[~mask] = l0 / (t_0 * 86400)**(5./3.)
1✔
26
    return lbol
1✔
27

28
def _semianalytical_fallback():
1✔
29
    pass
×
30

31

32
def _cooling_envelope(mbh_6, stellar_mass, eta, alpha, beta, **kwargs):
1✔
33
    """
34
    Sarin and Metzger 24 cooling envelope model.
35
    Also includes partial disruptions by assuming only fraction of stellar mass is disrupted.
36

37
    :param mbh_6: mass of supermassive black hole in units of 10^6 solar mass
38
    :param stellar_mass: stellar mass in units of solar masses
39
    :param eta: SMBH feedback efficiency (typical range: etamin - 0.1)
40
    :param alpha: disk viscosity
41
    :param beta: TDE penetration factor (typical range: 0.1 - beta_max).
42
    :param kwargs: Binding energy constant, zeta, hoverR, t_0_init, f_debris (optional, defaults to 1 unless
43
                   calculate_f_debris=True), calculate_f_debris (optional, defaults to False).
44
    :return: named tuple with bolometric luminosity, photosphere radius, temperature, and other parameters
45
    """
46
    t_0_init = kwargs.get('t_0_init', 1.0)
1✔
47
    binding_energy_const = kwargs.get('binding_energy_const', 0.8)
1✔
48
    zeta = kwargs.get('zeta', 2.0)
1✔
49
    hoverR = kwargs.get('hoverR', 0.3)
1✔
50
    calculate_f_debris = kwargs.get('calculate_f_debris', False)
1✔
51

52
    # Determine debris fraction
53
    if 'f_debris' in kwargs:
1✔
54
        # User explicitly provided f_debris - use that value
55
        f_debris = kwargs['f_debris']
×
56
        f_mbh = None
×
57
        g_mstar = None
×
58
        scaling_factor = None
×
59
    elif calculate_f_debris:
1✔
60
        # Calculate debris fraction from beta using Ryu et al. scaling
61
        # R_t = r_t * f(m_bh) * g(m_star)
62
        # f(m_bh) = 0.80 + 0.26 * (mbh/10^6)^0.5
63
        f_mbh = 0.80 + 0.26 * (mbh_6) ** 0.5
×
64

65
        # g(m_star) = (1.47 + exp[(M_star - 0.669)/0.137]) / (1 + 2.34 * exp[(M_star - 0.669)/0.137])
66
        exp_term = np.exp((stellar_mass - 0.669) / 0.137)
×
67
        g_mstar = (1.47 + exp_term) / (1 + 2.34 * exp_term)
×
68

69
        # Combined scaling factor
70
        scaling_factor = f_mbh * g_mstar
×
71

72
        # f_debris = (beta * scaling_factor)^3
73
        f_debris = (beta * scaling_factor) ** 3
×
74
        f_debris = np.clip(f_debris, 0.0, 1.0)  # Ensure physical bounds
×
75
    else:
76
        # Default behavior: assume complete disruption
77
        f_debris = 1.0
1✔
78
        f_mbh = None
1✔
79
        g_mstar = None
1✔
80
        scaling_factor = None
1✔
81

82
    # gravitational radius
83
    Rg = cc.graviational_constant * mbh_6 * 1.0e6 * (cc.solar_mass / cc.speed_of_light ** (2.0))
1✔
84
    # stellar mass in cgs
85
    Mstar = stellar_mass * cc.solar_mass
1✔
86
    # effective disrupted mass
87
    Mdisrupt = f_debris * Mstar
1✔
88
    # stellar radius in cgs
89
    Rstar = stellar_mass ** (0.8) * cc.solar_radius
1✔
90
    # tidal radius
91
    Rt = Rstar * (mbh_6 * 1.0e6 / stellar_mass) ** (1. / 3.)
1✔
92
    # circularization radius
93
    Rcirc = 2.0 * Rt / beta
1✔
94
    # fall-back time of most tightly bound debris (uses effective disrupted mass)
95
    tfb = calc_tfb(binding_energy_const, mbh_6, stellar_mass * f_debris)
1✔
96
    # Eddington luminosity of SMBH in units of 1e40 erg/s
97
    Ledd40 = 1.4e4 * mbh_6
1✔
98
    time_temp = np.logspace(np.log10(1.0 * tfb), np.log10(5000 * tfb), 5000)
1✔
99
    tdays = time_temp / cc.day_to_s
1✔
100

101
    # set up grids
102
    # mass of envelope in Msun
103
    Me = np.empty_like(tdays)
1✔
104
    # thermal energy of envelope in units of 1e40 ergs
105
    Ee40 = np.empty_like(tdays)
1✔
106
    # virial radius of envelope in cm
107
    Rv = np.empty_like(tdays)
1✔
108
    # accretion stream radius
109
    Racc = np.empty_like(tdays)
1✔
110
    # photosphere radius of envelop in cm
111
    Rph = np.empty_like(tdays)
1✔
112
    # fallback accretion luminosity in units of 1e40 erg/s
113
    Edotfb40 = np.empty_like(tdays)
1✔
114
    # accretion timescale onto SMBH
115
    tacc = np.empty_like(tdays)
1✔
116
    # feedback luminosity from SMBH in units of 1e40 erg/s
117
    Edotbh40 = np.empty_like(tdays)
1✔
118
    # accretion rate onto SMBH in units of g/s
119
    MdotBH = np.empty_like(tdays)
1✔
120
    # effective temperature of envelope emission in K
121
    Teff = np.empty_like(tdays)
1✔
122
    # bolometric luminosity of envelope thermal emission
123
    Lrad = np.empty_like(tdays)
1✔
124
    # nuLnu luminosity of envelope thermal emission at frequency nu
125
    nuLnu = np.empty_like(tdays)
1✔
126
    # characteristic optical depth through envelope
127
    Lamb = np.empty_like(tdays)
1✔
128
    # proxy x-ray luminosity (not used directly in optical light curve calculation)
129
    LX40 = np.empty_like(tdays)
1✔
130

131
    # Modified fallback rate using disrupted mass
132
    Mdotfb = (0.8 * Mdisrupt / (3.0 * tfb)) * (time_temp / tfb) ** (-5. / 3.)
1✔
133

134
    # ** initialize grid quantities at t = t_0_init [grid point 0] **
135
    # initial envelope mass at t_0_init (scaled by disruption fraction)
136
    Me[0] = f_debris * (0.1 * Mstar + (0.4 * Mstar) * (1.0 - t_0_init ** (-2. / 3.)))
1✔
137
    # initial envelope radius determined by energy of TDE process
138
    Rv[0] = (2. * Rt ** (2.0) / (5.0 * binding_energy_const * Rstar)) * (Me[0] / (f_debris * Mstar))
1✔
139
    # initial thermal energy of envelope
140
    Ee40[0] = ((2.0 * cc.graviational_constant * mbh_6 * 1.0e6 * Me[0]) / (5.0 * Rv[0])) * 2.0e-7
1✔
141
    # initial characteristic optical depth
142
    Lamb[0] = 0.38 * Me[0] / (10.0 * np.pi * Rv[0] ** (2.0))
1✔
143
    # initial photosphere radius
144
    Rph[0] = Rv[0] * (1.0 + np.log(Lamb[0]))
1✔
145
    # initial fallback stream accretion radius
146
    Racc[0] = zeta * Rv[0]
1✔
147
    # initial fallback accretion heating rate in 1e40 erg/s
148
    Edotfb40[0] = (cc.graviational_constant * mbh_6 * 1.0e6 * Mdotfb[0] / Racc[0]) * (2.0e-7)
1✔
149
    # initial luminosity of envelope
150
    Lrad[0] = Ledd40 + Edotfb40[0]
1✔
151
    # initial SMBH accretion timescale in s
152
    tacc[0] = 2.2e-17 * (10. / (3. * alpha)) * (Rv[0] ** (2.0)) / (
1✔
153
                cc.graviational_constant * mbh_6 * 1.0e6 * Rcirc) ** (0.5) * (hoverR) ** (
154
                  -2.0)
155
    # initial SMBH accretion rate in g/s
156
    MdotBH[0] = (Me[0] / tacc[0])
1✔
157
    # initial SMBH feedback heating rate in 1e40 erg/s
158
    Edotbh40[0] = eta * cc.speed_of_light ** (2.0) * (Me[0] / tacc[0]) * (1.0e-40)
1✔
159
    # initial photosphere temperature of envelope in K
160
    Teff[0] = 1.0e10 * ((Ledd40 + Edotfb40[0]) / (4.0 * np.pi * cc.sigma_sb * Rph[0] ** (2.0))) ** (0.25)
1✔
161

162
    t = time_temp
1✔
163
    for ii in range(1, len(time_temp)):
1✔
164
        Me[ii] = Me[ii - 1] - (MdotBH[ii - 1] - Mdotfb[ii - 1]) * (t[ii] - t[ii - 1])
1✔
165
        # update envelope energy due to SMBH heating + radiative losses
166
        Ee40[ii] = Ee40[ii - 1] + (Ledd40 - Edotbh40[ii - 1]) * (t[ii] - t[ii - 1])
1✔
167
        # update envelope radius based on its new energy
168
        Rv[ii] = ((2.0 * cc.graviational_constant * mbh_6 * 1.0e6 * Me[ii]) / (5.0 * Ee40[ii])) * (2.0e-7)
1✔
169
        # update envelope optical depth
170
        Lamb[ii] = 0.38 * Me[ii] / (10.0 * np.pi * Rv[ii] ** (2.0))
1✔
171
        # update envelope photosphere radius
172
        Rph[ii] = Rv[ii] * (1.0 + np.log(Lamb[ii]))
1✔
173
        # update accretion radius
174
        Racc[ii] = zeta * Rv[0] * (t[ii] / tfb) ** (2. / 3.)
1✔
175
        # update fall-back heating rate in 1e40 erg/s
176
        Edotfb40[ii] = (cc.graviational_constant * mbh_6 * 1.0e6 * Mdotfb[ii] / Racc[ii]) * (2.0e-7)
1✔
177
        # update total radiated luminosity
178
        Lrad[ii] = Ledd40 + Edotfb40[ii]
1✔
179
        # update photosphere temperature in K
180
        Teff[ii] = 1.0e10 * ((Ledd40 + Edotfb40[ii]) / (4.0 * np.pi * cc.sigma_sb * Rph[ii] ** (2.0))) ** (0.25)
1✔
181
        # update SMBH accretion timescale in seconds
182
        tacc[ii] = 2.2e-17 * (10. / (3.0 * alpha)) * (Rv[ii] ** (2.0)) / (
1✔
183
                    cc.graviational_constant * mbh_6 * 1.0e6 * Rcirc) ** (0.5) * (
184
                       hoverR) ** (-2.0)
185
        # update SMBH accretion rate in g/s
186
        MdotBH[ii] = (Me[ii] / tacc[ii])
1✔
187
        # update proxy X-ray luminosity
188
        LX40[ii] = 0.01 * (MdotBH[ii] / 1.0e20) * (cc.speed_of_light ** (2.0) / 1.0e20)
1✔
189
        # update SMBH feedback heating rate
190
        Edotbh40[ii] = eta * cc.speed_of_light ** (2.0) * (Me[ii] / tacc[ii]) * (1.0e-40)
1✔
191

192
    output = namedtuple('output', ['bolometric_luminosity', 'photosphere_temperature',
1✔
193
                                   'photosphere_radius', 'lum_xray', 'accretion_radius',
194
                                   'SMBH_accretion_rate', 'time_temp', 'nulnu',
195
                                   'time_since_fb', 'tfb', 'lnu', 'envelope_radius', 'envelope_mass',
196
                                   'rtidal', 'rcirc', 'termination_time', 'termination_time_id', 'f_debris',
197
                                   'f_mbh', 'g_mstar', 'scaling_factor'])
198
    try:
1✔
199
        constraint_1 = np.min(np.where(Rv < Rcirc / 2.))
1✔
200
        if constraint_1 == 0.:
1✔
201
            # ignore constraining on Rv < Rcirc/2. if it is at the first time step
202
            constraint_1 = 5000
×
203
        constraint_2 = np.min(np.where(Me < 0.0))
1✔
204
    except ValueError:
×
205
        constraint_1 = len(time_temp)
×
206
        constraint_2 = len(time_temp)
×
207
    constraint = np.min([constraint_1, constraint_2])
1✔
208
    termination_time_id = np.min([constraint_1, constraint_2])
1✔
209

210
    nu = 6.0e14
1✔
211
    expon = 1. / (np.exp(cc.planck * nu / (cc.boltzmann_constant * Teff)) - 1.0)
1✔
212
    nuLnu40 = (8.0 * np.pi ** (2.0) * Rph ** (2.0) / cc.speed_of_light ** (2.0))
1✔
213
    nuLnu40 = nuLnu40 * ((cc.planck * nu) * (nu ** (2.0))) / 1.0e30
1✔
214
    nuLnu40 = nuLnu40 * expon
1✔
215
    nuLnu40 = nuLnu40 * (nu / 1.0e10)
1✔
216

217
    output.bolometric_luminosity = Lrad[:constraint] * 1e40
1✔
218
    output.photosphere_temperature = Teff[:constraint]
1✔
219
    output.photosphere_radius = Rph[:constraint]
1✔
220
    output.envelope_radius = Rv[:constraint]
1✔
221
    output.envelope_mass = Me[:constraint]
1✔
222
    output.rcirc = Rcirc
1✔
223
    output.rtidal = Rt
1✔
224
    output.lum_xray = LX40[:constraint]
1✔
225
    output.accretion_radius = Racc[:constraint]
1✔
226
    output.SMBH_accretion_rate = MdotBH[:constraint]
1✔
227
    output.time_temp = time_temp[:constraint]
1✔
228
    output.time_since_fb = output.time_temp - output.time_temp[0]
1✔
229
    if constraint == len(time_temp):
1✔
230
        output.termination_time = time_temp[-1] - tfb
×
231
    else:
232
        output.termination_time = time_temp[termination_time_id] - tfb
1✔
233
    output.termination_time_id = termination_time_id
1✔
234
    output.tfb = tfb
1✔
235
    output.nulnu = nuLnu40[:constraint] * 1e40
1✔
236
    output.f_debris = f_debris
1✔
237
    output.f_mbh = f_mbh
1✔
238
    output.g_mstar = g_mstar
1✔
239
    output.scaling_factor = scaling_factor
1✔
240
    return output
1✔
241

242
@citation_wrapper('https://arxiv.org/abs/2307.15121,https://ui.adsabs.harvard.edu/abs/2022arXiv220707136M/abstract')
1✔
243
def cooling_envelope(time, redshift, mbh_6, stellar_mass, eta, alpha, beta, **kwargs):
1✔
244
    """
245
    This model is only valid for time after circulation. Use the gaussianrise_metzgertde model for the full lightcurve
246

247
    :param time: time in observer frame in days
248
    :param redshift: redshift
249
    :param mbh_6: mass of supermassive black hole in units of 10^6 solar mass
250
    :param stellar_mass: stellar mass in units of solar masses
251
    :param eta: SMBH feedback efficiency (typical range: etamin - 0.1)
252
    :param alpha: disk viscosity
253
    :param beta: TDE penetration factor (typical range: 1 - beta_max)
254
    :param kwargs: Additional parameters, check _cooling_envelope for more information
255
    :param frequency: Required if output_format is 'flux_density'.
256
        frequency to calculate - Must be same length as time array or a single number).
257
    :param bands: Required if output_format is 'magnitude' or 'flux'.
258
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
259
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
260
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
261
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
262
    """
263
    output = _cooling_envelope(mbh_6, stellar_mass, eta, alpha, beta, **kwargs)
1✔
264
    cosmology = kwargs.get('cosmology', cosmo)
1✔
265
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
266
    time_obs = time
1✔
267

268
    if kwargs['output_format'] == 'flux_density':
1✔
269
        frequency = kwargs['frequency']
1✔
270
        if isinstance(frequency, float):
1✔
271
            frequency = np.ones(len(time)) * frequency
1✔
272

273
        # convert to source frame time and frequency
274
        time = time * cc.day_to_s
1✔
275
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
276

277
        # interpolate properties onto observation times post tfb
278
        temp_func = interp1d(output.time_since_fb, y=output.photosphere_temperature)
1✔
279
        rad_func = interp1d(output.time_since_fb, y=output.photosphere_radius)
1✔
280

281
        temp = temp_func(time)
1✔
282
        photosphere = rad_func(time)
1✔
283

284
        flux_density = sed.blackbody_to_flux_density(temperature=temp, r_photosphere=photosphere,
1✔
285
                                                 dl=dl, frequency=frequency)
286
        return flux_density.to(uu.mJy).value
1✔
287
    else:
288
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 100))
1✔
289
        time_observer_frame = output.time_since_fb * (1. + redshift)
1✔
290

291
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
292
                                                     redshift=redshift, time=time_observer_frame)
293
        fmjy = sed.blackbody_to_flux_density(temperature=output.photosphere_temperature,
1✔
294
                                             r_photosphere=output.photosphere_radius,
295
                                             frequency=frequency[:, None], dl=dl)
296
        fmjy = fmjy.T
1✔
297
        spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
298
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
299
        if kwargs['output_format'] == 'spectra':
1✔
300
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
301
                                                                          lambdas=lambda_observer_frame,
302
                                                                          spectra=spectra)
303
        else:
304
            return sed.get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame / cc.day_to_s,
1✔
305
                                                              spectra=spectra, lambda_array=lambda_observer_frame,
306
                                                              **kwargs)
307

308
@citation_wrapper('https://arxiv.org/abs/2307.15121,https://ui.adsabs.harvard.edu/abs/2022arXiv220707136M/abstract')
1✔
309
def gaussianrise_cooling_envelope_bolometric(time, peak_time, sigma_t, mbh_6, stellar_mass, eta, alpha, beta,
1✔
310
                                                **kwargs):
311
    """
312
    Full lightcurve, with gaussian rise till xi * fallback time and then the metzger tde model,
313
    bolometric version for fitting the bolometric lightcurve
314

315
    :param time: time in source frame in days
316
    :param peak_time: peak time in days
317
    :param sigma_t: the sharpness of the Gaussian in days
318
    :param mbh_6: mass of supermassive black hole in units of 10^6 solar mass
319
    :param stellar_mass: stellar mass in units of solar masses
320
    :param eta: SMBH feedback efficiency (typical range: etamin - 0.1)
321
    :param alpha: disk viscosity
322
    :param beta: TDE penetration factor (typical range: 1 - beta_max)
323
    :param kwargs: Additional parameters, check _cooling_envelope for more information
324
    :param xi: Optional, default is 1. transition factor - Gaussian transitions to cooling envelope at xi * tfb
325
    :return luminosity in ergs/s
326
    """
327
    output = _cooling_envelope(mbh_6, stellar_mass, eta, alpha, beta, **kwargs)
1✔
328
    kwargs['binding_energy_const'] = kwargs.get('binding_energy_const', 0.8)
1✔
329
    tfb_sf = calc_tfb(kwargs['binding_energy_const'], mbh_6, stellar_mass)  # source frame
1✔
330

331
    # Transition time
332
    xi = kwargs.get('xi', 1.)
1✔
333
    transition_time = xi * tfb_sf
1✔
334

335
    # Find the luminosity value at the transition time by interpolating the cooling envelope model
336
    # Create interpolation function for the cooling envelope model
337
    cooling_envelope_func = interp1d(output.time_temp, output.bolometric_luminosity,
1✔
338
                                     bounds_error=False, fill_value='extrapolate')
339

340
    # Get luminosity at transition time
341
    f1 = pm.gaussian_rise(time=transition_time, a_1=1,
1✔
342
                          peak_time=peak_time * cc.day_to_s,
343
                          sigma_t=sigma_t * cc.day_to_s)
344

345
    # get normalisation
346
    f2 = cooling_envelope_func(transition_time)
1✔
347
    norm = f2 / f1
1✔
348

349
    # evaluate giant array of bolometric luminosities
350
    tt_pre_transition = np.linspace(0, transition_time, 100)
1✔
351
    # Only use cooling envelope times after the transition
352
    tt_post_transition = output.time_temp[output.time_temp >= transition_time]
1✔
353

354
    full_time = np.concatenate([tt_pre_transition, tt_post_transition])
1✔
355

356
    # Gaussian part before transition
357
    f1_array = pm.gaussian_rise(time=tt_pre_transition, a_1=norm,
1✔
358
                                peak_time=peak_time * cc.day_to_s,
359
                                sigma_t=sigma_t * cc.day_to_s)
360

361
    # Cooling envelope part after transition
362
    f2_array = output.bolometric_luminosity[output.time_temp >= transition_time]
1✔
363

364
    full_lbol = np.concatenate([f1_array, f2_array])
1✔
365
    lbol_func = interp1d(full_time, y=full_lbol, fill_value='extrapolate')
1✔
366

367
    return lbol_func(time * cc.day_to_s)
1✔
368

369

370
@citation_wrapper('https://arxiv.org/abs/2307.15121,https://ui.adsabs.harvard.edu/abs/2022arXiv220707136M/abstract')
1✔
371
def smooth_exponential_powerlaw_cooling_envelope_bolometric(time, peak_time, alpha_1, alpha_2, smoothing_factor,
1✔
372
                                                               mbh_6, stellar_mass, eta, alpha, beta, **kwargs):
373
    """
374
    Full lightcurve, with smoothed exponential power law rise till xi * fallback time and then the metzger tde model,
375
    bolometric version for fitting the bolometric lightcurve
376

377
    :param time: time in source frame in days
378
    :param peak_time: peak time in days
379
    :param alpha_1: power law index before peak
380
    :param alpha_2: power law index after peak
381
    :param smoothing_factor: controls transition smoothness at peak (higher = smoother)
382
    :param mbh_6: mass of supermassive black hole in units of 10^6 solar mass
383
    :param stellar_mass: stellar mass in units of solar masses
384
    :param eta: SMBH feedback efficiency (typical range: etamin - 0.1)
385
    :param alpha: disk viscosity
386
    :param beta: TDE penetration factor (typical range: 1 - beta_max)
387
    :param xi: Optional transition factor - smooth exponential power law transitions to cooling envelope at xi * tfb
388
    :param kwargs: Additional parameters, check _cooling_envelope for more information
389
    :return luminosity in ergs/s
390
    """
391
    # Get cooling envelope output
392
    output = _cooling_envelope(mbh_6, stellar_mass, eta, alpha, beta, **kwargs)
1✔
393
    kwargs['binding_energy_const'] = kwargs.get('binding_energy_const', 0.8)
1✔
394
    tfb_sf = calc_tfb(kwargs['binding_energy_const'], mbh_6, stellar_mass)  # source frame
1✔
395

396
    # Transition time
397
    xi = kwargs.get('xi', 1.)
1✔
398
    transition_time = xi * tfb_sf
1✔
399

400
    # Find the luminosity value at the transition time by interpolating the cooling envelope model
401
    # Create interpolation function for the cooling envelope model
402
    cooling_envelope_func = interp1d(output.time_temp, output.bolometric_luminosity,
1✔
403
                                     bounds_error=False, fill_value='extrapolate')
404

405
    # Get luminosity at transition time using smooth exponential power law
406
    f1 = pm.smooth_exponential_powerlaw(np.array([transition_time]), 1.0,
1✔
407
                                     peak_time * cc.day_to_s,
408
                                     alpha_1, alpha_2, smoothing_factor)[0]
409

410
    # get normalisation
411
    f2 = cooling_envelope_func(transition_time)
1✔
412
    norm = f2 / f1
1✔
413

414
    # evaluate giant array of bolometric luminosities
415
    tt_pre_transition = np.linspace(0, transition_time, 100)
1✔
416
    # Only use cooling envelope times after the transition
417
    tt_post_transition = output.time_temp[output.time_temp >= transition_time]
1✔
418

419
    full_time = np.concatenate([tt_pre_transition, tt_post_transition])
1✔
420

421
    # Smooth exponential power law part before transition
422
    f1_array = pm.smooth_exponential_powerlaw(tt_pre_transition, norm,
1✔
423
                                           peak_time * cc.day_to_s,
424
                                           alpha_1, alpha_2, smoothing_factor)
425

426
    # Cooling envelope part after transition
427
    f2_array = output.bolometric_luminosity[output.time_temp >= transition_time]
1✔
428

429
    full_lbol = np.concatenate([f1_array, f2_array])
1✔
430
    lbol_func = interp1d(full_time, y=full_lbol, fill_value='extrapolate')
1✔
431

432
    return lbol_func(time * cc.day_to_s)
1✔
433

434

435
@citation_wrapper('https://arxiv.org/abs/2307.15121,https://ui.adsabs.harvard.edu/abs/2022arXiv220707136M/abstract')
1✔
436
def gaussianrise_cooling_envelope(time, redshift, peak_time, sigma_t, mbh_6, stellar_mass, eta, alpha, beta, **kwargs):
1✔
437
    """
438
    Full lightcurve, with gaussian rise till fallback time and then the metzger tde model,
439
    photometric version where each band is fit/joint separately
440

441
    :param time: time in observer frame in days
442
    :param redshift: redshift
443
    :param peak_time: peak time in days
444
    :param sigma_t: the sharpness of the Gaussian in days
445
    :param mbh_6: mass of supermassive black hole in units of 10^6 solar mass
446
    :param stellar_mass: stellar mass in units of solar masses
447
    :param eta: SMBH feedback efficiency (typical range: etamin - 0.1)
448
    :param alpha: disk viscosity
449
    :param beta: TDE penetration factor (typical range: 1 - beta_max)
450
    :param kwargs: Additional parameters, check _cooling_envelope for more information
451
    :param xi: Optional argument (default set to one) to change the point where lightcurve switches from Gaussian rise to cooling envelope.
452
        stitching_point = xi * tfb (where tfb is fallback time). So a xi=1 means the stitching point is at fallback time.
453
    :param frequency: Required if output_format is 'flux_density'.
454
        frequency to calculate - Must be same length as time array or a single number).
455
    :param bands: Required if output_format is 'magnitude' or 'flux'.
456
    :param output_format: 'flux_density', 'magnitude', 'flux'
457
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
458
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
459
    :return: set by output format - 'flux_density', 'magnitude', 'flux'
460
    """
461
    binding_energy_const = kwargs.get('binding_energy_const', 0.8)
1✔
462
    tfb_sf = calc_tfb(binding_energy_const, mbh_6, stellar_mass)  # source frame
1✔
463
    tfb_obf = tfb_sf * (1. + redshift)  # observer frame
1✔
464
    xi = kwargs.get('xi', 1.)
1✔
465
    output = _cooling_envelope(mbh_6, stellar_mass, eta, alpha, beta, **kwargs)
1✔
466
    cosmology = kwargs.get('cosmology', cosmo)
1✔
467
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
468
    stitching_point = xi * tfb_obf
1✔
469

470
    # normalisation term in observer frame
471
    f1 = pm.gaussian_rise(time=stitching_point, a_1=1., peak_time=peak_time * cc.day_to_s, sigma_t=sigma_t * cc.day_to_s)
1✔
472

473
    if kwargs['output_format'] == 'flux_density':
1✔
474
        frequency = kwargs['frequency']
1✔
475
        if isinstance(frequency, float):
1✔
476
            frequency = np.ones(len(time)) * frequency
1✔
477

478
        # convert to source frame time and frequency
479
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
480
        unique_frequency = np.sort(np.unique(frequency))
1✔
481

482
        # source frame
483
        f2 = sed.blackbody_to_flux_density(temperature=output.photosphere_temperature[0],
1✔
484
                                           r_photosphere=output.photosphere_radius[0],
485
                                           dl=dl, frequency=unique_frequency).to(uu.mJy)
486
        norms = f2.value / f1
1✔
487
        norm_dict = dict(zip(unique_frequency, norms))
1✔
488

489
        # build flux density function for each frequency
490
        flux_den_interp_func = {}
1✔
491
        for freq in unique_frequency:
1✔
492
            tt_pre_fb = np.linspace(0, stitching_point / cc.day_to_s, 200) * cc.day_to_s
1✔
493
            tt_post_fb = xi * (output.time_temp * (1 + redshift))
1✔
494
            total_time = np.concatenate([tt_pre_fb, tt_post_fb])
1✔
495
            f1 = pm.gaussian_rise(time=tt_pre_fb, a_1=norm_dict[freq],
1✔
496
                                  peak_time=peak_time * cc.day_to_s, sigma_t=sigma_t * cc.day_to_s)
497
            f2 = sed.blackbody_to_flux_density(temperature=output.photosphere_temperature,
1✔
498
                                               r_photosphere=output.photosphere_radius,
499
                                               dl=dl, frequency=freq).to(uu.mJy)
500
            flux_den = np.concatenate([f1, f2.value])
1✔
501
            flux_den_interp_func[freq] = interp1d(total_time, flux_den, fill_value='extrapolate')
1✔
502

503
        # interpolate onto actual observed frequency and time values
504
        flux_density = []
1✔
505
        for freq, tt in zip(frequency, time):
1✔
506
            flux_density.append(flux_den_interp_func[freq](tt * cc.day_to_s))
1✔
507
        flux_density = flux_density * uu.mJy
1✔
508
        return flux_density.to(uu.mJy).value
1✔
509
    else:
510
        bands = kwargs['bands']
1✔
511
        if isinstance(bands, str):
1✔
512
            bands = [str(bands) for x in range(len(time))]
1✔
513

514
        unique_bands = np.unique(bands)
1✔
515
        temp_kwargs = kwargs.copy()
1✔
516
        temp_kwargs['bands'] = unique_bands
1✔
517
        f2 = cooling_envelope(time=0., redshift=redshift,
1✔
518
                              mbh_6=mbh_6, stellar_mass=stellar_mass, eta=eta, alpha=alpha, beta=beta,
519
                              **temp_kwargs)
520
        if kwargs['output_format'] == 'magnitude':
1✔
521
            # make the normalisation in fmjy to avoid magnitude normalisation problems
522
            _f2mjy = calc_flux_density_from_ABmag(f2).value
1✔
523
            norms = _f2mjy / f1
1✔
524
        else:
525
            norms = f2 / f1
×
526

527
        if isinstance(norms, float):
1✔
528
            norms = np.ones(len(time)) * norms
×
529
        norm_dict = dict(zip(unique_bands, norms))
1✔
530

531
        flux_den_interp_func = {}
1✔
532
        for band in unique_bands:
1✔
533
            tt_pre_fb = np.linspace(0, stitching_point / cc.day_to_s, 100) * cc.day_to_s
1✔
534
            tt_post_fb = output.time_temp * (1 + redshift)
1✔
535
            total_time = np.concatenate([tt_pre_fb, tt_post_fb])
1✔
536
            f1 = pm.gaussian_rise(time=tt_pre_fb, a_1=norm_dict[band],
1✔
537
                                  peak_time=peak_time * cc.day_to_s, sigma_t=sigma_t * cc.day_to_s)
538
            if kwargs['output_format'] == 'magnitude':
1✔
539
                f1 = calc_ABmag_from_flux_density(f1).value
1✔
540
            temp_kwargs = kwargs.copy()
1✔
541
            temp_kwargs['bands'] = band
1✔
542
            f2 = cooling_envelope(time=output.time_since_fb / cc.day_to_s, redshift=redshift,
1✔
543
                                  mbh_6=mbh_6, stellar_mass=stellar_mass, eta=eta, alpha=alpha, beta=beta,
544
                                  **temp_kwargs)
545
            flux_den = np.concatenate([f1, f2])
1✔
546
            flux_den_interp_func[band] = interp1d(total_time, flux_den, fill_value='extrapolate')
1✔
547

548
        # interpolate onto actual observed band and time values
549
        output = []
1✔
550
        for freq, tt in zip(bands, time):
1✔
551
            output.append(flux_den_interp_func[freq](tt * cc.day_to_s))
1✔
552
        return np.array(output)
1✔
553

554
@citation_wrapper('https://arxiv.org/abs/2307.15121,https://ui.adsabs.harvard.edu/abs/2022arXiv220707136M/abstract')
1✔
555
def bpl_cooling_envelope(time, redshift, peak_time, alpha_1, alpha_2, mbh_6, stellar_mass, eta, alpha, beta, **kwargs):
1✔
556
    """
557
    Full lightcurve, with gaussian rise till fallback time and then the metzger tde model,
558
    photometric version where each band is fit/joint separately
559

560
    :param time: time in observer frame in days
561
    :param redshift: redshift
562
    :param peak_time: peak time in days
563
    :param alpha_1: power law index for first power law
564
    :param alpha_2: power law index for second power law (should be positive)
565
    :param mbh_6: mass of supermassive black hole in units of 10^6 solar mass
566
    :param stellar_mass: stellar mass in units of solar masses
567
    :param eta: SMBH feedback efficiency (typical range: etamin - 0.1)
568
    :param alpha: disk viscosity
569
    :param beta: TDE penetration factor (typical range: 1 - beta_max)
570
    :param kwargs: Additional parameters, check _cooling_envelope for more information
571
    :param xi: Optional argument (default set to one) to change the point where lightcurve switches from Gaussian rise to cooling envelope.
572
        stitching_point = xi * tfb (where tfb is fallback time). So a xi=1 means the stitching point is at fallback time.
573
    :param frequency: Required if output_format is 'flux_density'.
574
        frequency to calculate - Must be same length as time array or a single number).
575
    :param bands: Required if output_format is 'magnitude' or 'flux'.
576
    :param output_format: 'flux_density', 'magnitude', 'flux'
577
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
578
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
579
    :return: set by output format - 'flux_density', 'magnitude', 'flux'
580
    """
581
    binding_energy_const = kwargs.get('binding_energy_const', 0.8)
1✔
582
    tfb_sf = calc_tfb(binding_energy_const, mbh_6, stellar_mass)  # source frame
1✔
583
    tfb_obf = tfb_sf * (1. + redshift)  # observer frame
1✔
584
    xi = kwargs.get('xi', 1.)
1✔
585
    output = _cooling_envelope(mbh_6, stellar_mass, eta, alpha, beta, **kwargs)
1✔
586
    cosmology = kwargs.get('cosmology', cosmo)
1✔
587
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
588
    stitching_point = xi * tfb_obf
1✔
589

590
    # normalisation term in observer frame
591
    f1 = pm.exponential_powerlaw(time=stitching_point, a_1=1., tpeak=peak_time * cc.day_to_s,
1✔
592
                                 alpha_1=alpha_1, alpha_2=alpha_2)
593

594
    if kwargs['output_format'] == 'flux_density':
1✔
595
        frequency = kwargs['frequency']
1✔
596
        if isinstance(frequency, float):
1✔
597
            frequency = np.ones(len(time)) * frequency
1✔
598

599
        # convert to source frame time and frequency
600
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
601
        unique_frequency = np.sort(np.unique(frequency))
1✔
602

603
        # source frame
604
        f2 = sed.blackbody_to_flux_density(temperature=output.photosphere_temperature[0],
1✔
605
                                           r_photosphere=output.photosphere_radius[0],
606
                                           dl=dl, frequency=unique_frequency).to(uu.mJy)
607
        norms = f2.value / f1
1✔
608
        norm_dict = dict(zip(unique_frequency, norms))
1✔
609

610
        # build flux density function for each frequency
611
        flux_den_interp_func = {}
1✔
612
        for freq in unique_frequency:
1✔
613
            tt_pre_fb = np.linspace(0, stitching_point / cc.day_to_s, 200) * cc.day_to_s
1✔
614
            tt_post_fb = xi * (output.time_temp * (1 + redshift))
1✔
615
            total_time = np.concatenate([tt_pre_fb, tt_post_fb])
1✔
616
            f1 = pm.exponential_powerlaw(time=tt_pre_fb, a_1=norm_dict[freq],
1✔
617
                                         tpeak=peak_time * cc.day_to_s, alpha_1=alpha_1, alpha_2=alpha_2)
618
            f2 = sed.blackbody_to_flux_density(temperature=output.photosphere_temperature,
1✔
619
                                               r_photosphere=output.photosphere_radius,
620
                                               dl=dl, frequency=freq).to(uu.mJy)
621
            flux_den = np.concatenate([f1, f2.value])
1✔
622
            flux_den_interp_func[freq] = interp1d(total_time, flux_den, fill_value='extrapolate')
1✔
623

624
        # interpolate onto actual observed frequency and time values
625
        flux_density = []
1✔
626
        for freq, tt in zip(frequency, time):
1✔
627
            flux_density.append(flux_den_interp_func[freq](tt * cc.day_to_s))
1✔
628
        flux_density = flux_density * uu.mJy
1✔
629
        return flux_density.to(uu.mJy).value
1✔
630
    else:
631
        bands = kwargs['bands']
1✔
632
        if isinstance(bands, str):
1✔
633
            bands = [str(bands) for x in range(len(time))]
1✔
634

635
        unique_bands = np.unique(bands)
1✔
636
        temp_kwargs = kwargs.copy()
1✔
637
        temp_kwargs['bands'] = unique_bands
1✔
638
        f2 = cooling_envelope(time=0., redshift=redshift,
1✔
639
                              mbh_6=mbh_6, stellar_mass=stellar_mass, eta=eta, alpha=alpha, beta=beta,
640
                              **temp_kwargs)
641
        if kwargs['output_format'] == 'magnitude':
1✔
642
            # make the normalisation in fmjy to avoid magnitude normalisation problems
643
            _f2mjy = calc_flux_density_from_ABmag(f2).value
1✔
644
            norms = _f2mjy / f1
1✔
645
        else:
646
            norms = f2 / f1
×
647

648
        if isinstance(norms, float):
1✔
649
            norms = np.ones(len(time)) * norms
×
650
        norm_dict = dict(zip(unique_bands, norms))
1✔
651

652
        flux_den_interp_func = {}
1✔
653
        for band in unique_bands:
1✔
654
            tt_pre_fb = np.linspace(0, stitching_point / cc.day_to_s, 100) * cc.day_to_s
1✔
655
            tt_post_fb = output.time_temp * (1 + redshift)
1✔
656
            total_time = np.concatenate([tt_pre_fb, tt_post_fb])
1✔
657
            f1 = pm.exponential_powerlaw(time=tt_pre_fb, a_1=norm_dict[band],
1✔
658
                                         tpeak=peak_time * cc.day_to_s, alpha_1=alpha_1, alpha_2=alpha_2)
659
            if kwargs['output_format'] == 'magnitude':
1✔
660
                f1 = calc_ABmag_from_flux_density(f1).value
1✔
661
            temp_kwargs = kwargs.copy()
1✔
662
            temp_kwargs['bands'] = band
1✔
663
            f2 = cooling_envelope(time=output.time_since_fb / cc.day_to_s, redshift=redshift,
1✔
664
                                  mbh_6=mbh_6, stellar_mass=stellar_mass, eta=eta, alpha=alpha, beta=beta,
665
                                  **temp_kwargs)
666
            flux_den = np.concatenate([f1, f2])
1✔
667
            flux_den_interp_func[band] = interp1d(total_time, flux_den, fill_value='extrapolate')
1✔
668

669
        # interpolate onto actual observed band and time values
670
        output = []
1✔
671
        for freq, tt in zip(bands, time):
1✔
672
            output.append(flux_den_interp_func[freq](tt * cc.day_to_s))
1✔
673
        return np.array(output)
1✔
674

675
@citation_wrapper('redback')
1✔
676
def tde_analytical_bolometric(time, l0, t_0_turn, **kwargs):
1✔
677
    """
678
    :param time: rest frame time in days
679
    :param l0: bolometric luminosity at 1 second in cgs
680
    :param t_0_turn: turn on time in days (after this time lbol decays as 5/3 powerlaw)
681
    :param interaction_process: Default is Diffusion.
682
            Can also be None in which case the output is just the raw engine luminosity
683
    :param kwargs: Must be all the kwargs required by the specific interaction_process
684
                e.g., for Diffusion: kappa, kappa_gamma, mej (solar masses), vej (km/s), temperature_floor
685
    :return: bolometric_luminosity
686
    """
687
    _interaction_process = kwargs.get("interaction_process", ip.Diffusion)
1✔
688
    lbol = _analytic_fallback(time=time, l0=l0, t_0=t_0_turn)
1✔
689
    if _interaction_process is not None:
1✔
690
        dense_resolution = kwargs.get("dense_resolution", 1000)
1✔
691
        dense_times = np.linspace(0, time[-1] + 100, dense_resolution)
1✔
692
        dense_lbols = _analytic_fallback(time=dense_times, l0=l0, t_0=t_0_turn)
1✔
693
        interaction_class = _interaction_process(time=time, dense_times=dense_times, luminosity=dense_lbols, **kwargs)
1✔
694
        lbol = interaction_class.new_luminosity
1✔
695
    return lbol
1✔
696

697
@citation_wrapper('redback')
1✔
698
def tde_analytical(time, redshift, l0, t_0_turn, **kwargs):
1✔
699
    """
700
    :param time: observer frame time in days
701
    :param l0: bolometric luminosity at 1 second in cgs
702
    :param t_0_turn: turn on time in days (after this time lbol decays as 5/3 powerlaw)
703
    :param kwargs: Must be all the kwargs required by the specific interaction_process
704
     e.g., for Diffusion TemperatureFloor: kappa, kappa_gamma, vej (km/s), temperature_floor
705
    :param interaction_process: Default is Diffusion.
706
            Can also be None in which case the output is just the raw engine luminosity
707
    :param photosphere: TemperatureFloor
708
    :param sed: CutoffBlackbody must have cutoff_wavelength in kwargs or it will default to 3000 Angstrom
709
    :param frequency: Required if output_format is 'flux_density'.
710
        frequency to calculate - Must be same length as time array or a single number).
711
    :param bands: Required if output_format is 'magnitude' or 'flux'.
712
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
713
    :param lambda_array: Optional argument to set your desired wavelength array (in Angstroms) to evaluate the SED on.
714
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
715
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
716
    """
717
    kwargs['interaction_process'] = kwargs.get("interaction_process", ip.Diffusion)
1✔
718
    kwargs['photosphere'] = kwargs.get("photosphere", photosphere.TemperatureFloor)
1✔
719
    kwargs['sed'] = kwargs.get("sed", sed.CutoffBlackbody)
1✔
720
    cutoff_wavelength = kwargs.get('cutoff_wavelength', 3000)
1✔
721
    cosmology = kwargs.get('cosmology', cosmo)
1✔
722
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
723

724
    if kwargs['output_format'] == 'flux_density':
1✔
725
        frequency = kwargs['frequency']
1✔
726
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
727
        lbol = tde_analytical_bolometric(time=time, l0=l0, t_0_turn=t_0_turn, **kwargs)
1✔
728
        photo = kwargs['photosphere'](time=time, luminosity=lbol, **kwargs)
1✔
729
        sed_1 = kwargs['sed'](time=time, temperature=photo.photosphere_temperature, r_photosphere=photo.r_photosphere,
1✔
730
                     frequency=frequency, luminosity_distance=dl, cutoff_wavelength=cutoff_wavelength, luminosity=lbol)
731

732
        flux_density = sed_1.flux_density
1✔
733
        flux_density = np.nan_to_num(flux_density)
1✔
734
        return flux_density.to(uu.mJy).value
1✔
735
    else:
736
        time_obs = time
1✔
737
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 100))
1✔
738
        time_temp = np.geomspace(0.1, 1000, 300) # in days
1✔
739
        time_observer_frame = time_temp * (1. + redshift)
1✔
740
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
741
                                                     redshift=redshift, time=time_observer_frame)
742
        lbol = tde_analytical_bolometric(time=time, l0=l0, t_0_turn=t_0_turn, **kwargs)
1✔
743
        photo = kwargs['photosphere'](time=time, luminosity=lbol, **kwargs)
1✔
744
        full_sed = np.zeros((len(time), len(frequency)))
1✔
745
        for ii in range(len(frequency)):
1✔
746
            ss = kwargs['sed'](time=time,temperature=photo.photosphere_temperature,
1✔
747
                                r_photosphere=photo.r_photosphere,frequency=frequency[ii],
748
                                luminosity_distance=dl, cutoff_wavelength=cutoff_wavelength, luminosity=lbol)
749
            full_sed[:, ii] = ss.flux_density.to(uu.mJy).value
1✔
750
        spectra = (full_sed * uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
751
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
752
        if kwargs['output_format'] == 'spectra':
1✔
753
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
754
                                                                           lambdas=lambda_observer_frame,
755
                                                                           spectra=spectra)
756
        else:
757
            return sed.get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
1✔
758
                                                              spectra=spectra, lambda_array=lambda_observer_frame,
759
                                                              **kwargs)
760

761
def _initialize_mosfit_tde_model():
1✔
762
    """
763
    Initializtion function to load/process data.
764

765
    Loads and interpolates tde simulation data. Simulation data is
766
    from Guillochon 2013 and can be found on astrocrash.net.
767

768
    :return: Named tuple with several outputs
769
    """
770

771
    import os
1✔
772
    dirname = os.path.dirname(__file__)
1✔
773
    data_dir = f"{dirname}/../tables/guillochon_tde_data"
1✔
774
    G_cgs = cc.graviational_constant
1✔
775
    Mhbase = 1.0e6 * cc.solar_mass
1✔
776

777
    gammas = ['4-3', '5-3']
1✔
778

779
    beta_slope = {gammas[0]: [], gammas[1]: []}
1✔
780
    beta_yinter = {gammas[0]: [], gammas[1]: []}
1✔
781
    sim_beta = {gammas[0]: [], gammas[1]: []}
1✔
782
    mapped_time = {gammas[0]: [], gammas[1]: []}
1✔
783
    premaptime = {gammas[0]: [], gammas[1]: []}
1✔
784
    premapdmdt = {gammas[0]: [], gammas[1]: []}
1✔
785

786
    for g in gammas:
1✔
787
        dmdedir = os.path.join(data_dir, g)
1✔
788

789
        sim_beta_files = os.listdir(dmdedir)
1✔
790
        simbeta = [float(b[:-4]) for b in sim_beta_files]
1✔
791
        sortedindices = np.argsort(simbeta)
1✔
792
        simbeta = [simbeta[i] for i in sortedindices]
1✔
793
        sim_beta_files = [sim_beta_files[i] for i in sortedindices]
1✔
794
        sim_beta[g].extend(simbeta)
1✔
795

796
        time = {}
1✔
797
        dmdt = {}
1✔
798
        ipeak = {}
1✔
799
        _mapped_time = {}
1✔
800

801
        e, d = np.loadtxt(os.path.join(dmdedir, sim_beta_files[0]))
1✔
802
        ebound = e[e < 0]
1✔
803
        dmdebound = d[e < 0]
1✔
804

805
        if min(dmdebound) < 0:
1✔
806
            print('beta, gamma, negative dmde bound:', sim_beta[g], g, dmdebound[dmdebound < 0])
×
807

808
        dedt = (1.0 / 3.0) * (-2.0 * ebound) ** (5.0 / 2.0) / (2.0 * np.pi * G_cgs * Mhbase)
1✔
809
        time['lo'] = np.log10((2.0 * np.pi * G_cgs * Mhbase) * (-2.0 * ebound) ** (-3.0 / 2.0))
1✔
810
        dmdt['lo'] = np.log10(dmdebound * dedt)
1✔
811

812
        ipeak['lo'] = np.argmax(dmdt['lo'])
1✔
813

814
        time['lo'] = np.array([time['lo'][:ipeak['lo']], time['lo'][ipeak['lo']:]], dtype=object)
1✔
815
        dmdt['lo'] = np.array([dmdt['lo'][:ipeak['lo']], dmdt['lo'][ipeak['lo']:]], dtype=object)
1✔
816

817
        premaptime[g].append(np.copy(time['lo']))
1✔
818
        premapdmdt[g].append(np.copy(dmdt['lo']))
1✔
819

820
        for i in range(1, len(sim_beta[g])):
1✔
821
            e, d = np.loadtxt(os.path.join(dmdedir, sim_beta_files[i]))
1✔
822
            ebound = e[e < 0]
1✔
823
            dmdebound = d[e < 0]
1✔
824

825
            if min(dmdebound) < 0:
1✔
826
                print('beta, gamma, negative dmde bound:', sim_beta[g], g, dmdebound[dmdebound < 0])
×
827

828
            dedt = (1.0 / 3.0) * (-2.0 * ebound) ** (5.0 / 2.0) / (2.0 * np.pi * G_cgs * Mhbase)
1✔
829
            time['hi'] = np.log10((2.0 * np.pi * G_cgs * Mhbase) * (-2.0 * ebound) ** (-3.0 / 2.0))
1✔
830
            dmdt['hi'] = np.log10(dmdebound * dedt)
1✔
831

832
            ipeak['hi'] = np.argmax(dmdt['hi'])
1✔
833

834
            time['hi'] = np.array([time['hi'][:ipeak['hi']], time['hi'][ipeak['hi']:]], dtype=object)
1✔
835
            dmdt['hi'] = np.array([dmdt['hi'][:ipeak['hi']], dmdt['hi'][ipeak['hi']:]], dtype=object)
1✔
836

837
            premapdmdt[g].append(np.copy(dmdt['hi']))
1✔
838
            premaptime[g].append(np.copy(time['hi']))
1✔
839

840
            _mapped_time['hi'] = []
1✔
841
            _mapped_time['lo'] = []
1✔
842

843
            beta_slope[g].append([])
1✔
844
            beta_yinter[g].append([])
1✔
845
            mapped_time[g].append([])
1✔
846

847
            for j in [0, 1]:
1✔
848
                if len(time['lo'][j]) < len(time['hi'][j]):
1✔
849
                    interp = 'lo'
1✔
850
                    nointerp = 'hi'
1✔
851
                else:
852
                    interp = 'hi'
1✔
853
                    nointerp = 'lo'
1✔
854

855
                _mapped_time[nointerp].append(
1✔
856
                    1. / (time[nointerp][j][-1] - time[nointerp][j][0]) *
857
                    (time[nointerp][j] - time[nointerp][j][0]))
858
                _mapped_time[interp].append(
1✔
859
                    1. / (time[interp][j][-1] - time[interp][j][0]) *
860
                    (time[interp][j] - time[interp][j][0]))
861

862
                _mapped_time[interp][j][0] = 0
1✔
863
                _mapped_time[interp][j][-1] = 1
1✔
864
                _mapped_time[nointerp][j][0] = 0
1✔
865
                _mapped_time[nointerp][j][-1] = 1
1✔
866

867
                func = interp1d(_mapped_time[interp][j], dmdt[interp][j])
1✔
868
                dmdtinterp = func(_mapped_time[nointerp][j])
1✔
869

870
                if interp == 'hi':
1✔
871
                    slope = ((dmdtinterp - dmdt['lo'][j]) /
1✔
872
                             (sim_beta[g][i] - sim_beta[g][i - 1]))
873
                else:
874
                    slope = ((dmdt['hi'][j] - dmdtinterp) /
1✔
875
                             (sim_beta[g][i] - sim_beta[g][i - 1]))
876
                beta_slope[g][-1].append(slope)
1✔
877

878
                yinter1 = (dmdt[nointerp][j] - beta_slope[g][-1][j] *
1✔
879
                           sim_beta[g][i - 1])
880
                yinter2 = (dmdtinterp - beta_slope[g][-1][j] *
1✔
881
                           sim_beta[g][i])
882
                beta_yinter[g][-1].append((yinter1 + yinter2) / 2.0)
1✔
883
                mapped_time[g][-1].append(
1✔
884
                    np.array(_mapped_time[nointerp][j]))
885

886
            time['lo'] = np.copy(time['hi'])
1✔
887
            dmdt['lo'] = np.copy(dmdt['hi'])
1✔
888

889
    outs = namedtuple('sim_outputs', ['beta_slope', 'beta_yinter', 'sim_beta', 'mapped_time',
1✔
890
                                      'premaptime', 'premapdmdt'])
891
    outs = outs(beta_slope=beta_slope, beta_yinter=beta_yinter, sim_beta=sim_beta,
1✔
892
                mapped_time=mapped_time,premaptime=premaptime, premapdmdt=premapdmdt)
893
    return outs
1✔
894

895

896
def _tde_mosfit_engine(times, mbh6, mstar, b, efficiency, leddlimit, **kwargs):
1✔
897
    """
898
    Produces the processed outputs from simulation data for the TDE model.
899

900
    :param times: A dense array of times in rest frame in days
901
    :param mbh6: black hole mass in units of 10^6 solar masses
902
    :param mstar: star mass in units of solar masses
903
    :param b: Relates to beta and gamma values for the star that's disrupted
904
    :param efficiency: efficiency of the BH
905
    :param leddlimit: eddington limit for the BH
906
    :param kwargs: Additional keyword arguments
907
    :return: Named tuple with several outputs
908
    """
909
    beta_interp = True
1✔
910

911
    outs = _initialize_mosfit_tde_model()
1✔
912
    beta_slope = outs.beta_slope
1✔
913
    beta_yinter = outs.beta_yinter
1✔
914
    sim_beta = outs.sim_beta
1✔
915
    mapped_time = outs.mapped_time
1✔
916
    premaptime = outs.premaptime
1✔
917
    premapdmdt = outs.premapdmdt
1✔
918

919
    Mhbase = 1.0e6  # in units of Msolar, this is generic Mh used in astrocrash sims
1✔
920
    Mstarbase = 1.0  # in units of Msolar
1✔
921
    Rstarbase = 1.0  # in units of Rsolar
1✔
922
    starmass = mstar
1✔
923

924
    # Calculate beta values
925
    if 0 <= b < 1:
1✔
926
        beta43 = 0.6 + 1.25 * b
1✔
927
        beta53 = 0.5 + 0.4 * b
1✔
928
        betas = {'4-3': beta43, '5-3': beta53}
1✔
929
    elif 1 <= b <= 2:
1✔
930
        beta43 = 1.85 + 2.15 * (b - 1)
1✔
931
        beta53 = 0.9 + 1.6 * (b - 1)
1✔
932
        betas = {'4-3': beta43, '5-3': beta53}
1✔
933
    else:
934
        raise ValueError('b outside range, bmin = 0; bmax = 2')
×
935

936
    # Determine gamma values
937
    gamma_interp = False
1✔
938
    if starmass <= 0.3 or starmass >= 22:
1✔
939
        gammas = ['5-3']
1✔
940
        beta = betas['5-3']
1✔
941
    elif 1 <= starmass <= 15:
1✔
942
        gammas = ['4-3']
1✔
943
        beta = betas['4-3']
1✔
944
    elif 0.3 < starmass < 1:
1✔
945
        gamma_interp = True
1✔
946
        gammas = ['4-3', '5-3']
1✔
947
        gfrac = (starmass - 1.) / (0.3 - 1.)
1✔
948
        beta = betas['5-3'] + (betas['4-3'] - betas['5-3']) * (1. - gfrac)
1✔
UNCOV
949
    elif 15 < starmass < 22:
×
UNCOV
950
        gamma_interp = True
×
UNCOV
951
        gammas = ['4-3', '5-3']
×
UNCOV
952
        gfrac = (starmass - 15.) / (22. - 15.)
×
UNCOV
953
        beta = betas['5-3'] + (betas['4-3'] - betas['5-3']) * (1. - gfrac)
×
954

955
    timedict = {}
1✔
956
    dmdtdict = {}
1✔
957

958
    sim_beta = outs.sim_beta
1✔
959
    for g in gammas:
1✔
960
        for i in range(len(sim_beta[g])):
1✔
961
            if betas[g] == sim_beta[g][i]:
1✔
962
                beta_interp = False
×
963
                interp_index_low = i
×
964
                break
×
965
            if betas[g] < sim_beta[g][i]:
1✔
966
                interp_index_high = i
1✔
967
                interp_index_low = i - 1
1✔
968
                beta_interp = True
1✔
969
                break
1✔
970

971
        if beta_interp:
1✔
972
            dmdt = np.array([
1✔
973
                beta_yinter[g][interp_index_low][0] + beta_slope[g][interp_index_low][0] * betas[g],
974
                beta_yinter[g][interp_index_low][1] + beta_slope[g][interp_index_low][1] * betas[g]
975
            ], dtype=object)
976

977
            time = []
1✔
978
            for i in [0, 1]:
1✔
979
                time_betalo = (mapped_time[g][interp_index_low][i] * (
1✔
980
                            premaptime[g][interp_index_low][i][-1] - premaptime[g][interp_index_low][i][0]) +
981
                               premaptime[g][interp_index_low][i][0])
982
                time_betahi = (mapped_time[g][interp_index_low][i] * (
1✔
983
                            premaptime[g][interp_index_high][i][-1] - premaptime[g][interp_index_high][i][0]) +
984
                               premaptime[g][interp_index_high][i][0])
985
                time.append(time_betalo + (time_betahi - time_betalo) * (betas[g] - sim_beta[g][interp_index_low]) / (
1✔
986
                            sim_beta[g][interp_index_high] - sim_beta[g][interp_index_low]))
987
            time = np.array(time, dtype=object)
1✔
988

989
            timedict[g] = time
1✔
990
            dmdtdict[g] = dmdt
1✔
991
        else:
992
            timedict[g] = np.copy(premaptime[g][interp_index_low])
×
993
            dmdtdict[g] = np.copy(premapdmdt[g][interp_index_low])
×
994

995
    if gamma_interp:
1✔
996
        mapped_time = {'4-3': [], '5-3': []}
1✔
997
        time = []
1✔
998
        dmdt = []
1✔
999
        for j in [0, 1]:
1✔
1000
            if len(timedict['4-3'][j]) < len(timedict['5-3'][j]):
1✔
1001
                interp = '4-3'
1✔
1002
                nointerp = '5-3'
1✔
1003
            else:
1004
                interp = '5-3'
1✔
1005
                nointerp = '4-3'
1✔
1006

1007
            mapped_time[nointerp].append(1. / (timedict[nointerp][j][-1] - timedict[nointerp][j][0]) * (
1✔
1008
                        timedict[nointerp][j] - timedict[nointerp][j][0]))
1009
            mapped_time[interp].append(1. / (timedict[interp][j][-1] - timedict[interp][j][0]) * (
1✔
1010
                        timedict[interp][j] - timedict[interp][j][0]))
1011
            mapped_time[interp][j][0] = 0
1✔
1012
            mapped_time[interp][j][-1] = 1
1✔
1013
            mapped_time[nointerp][j][0] = 0
1✔
1014
            mapped_time[nointerp][j][-1] = 1
1✔
1015

1016
            func = interp1d(mapped_time[interp][j], dmdtdict[interp][j])
1✔
1017
            dmdtdict[interp][j] = func(mapped_time[nointerp][j])
1✔
1018

1019
            if interp == '5-3':
1✔
1020
                time53 = (mapped_time['4-3'][j] * (timedict['5-3'][j][-1] - timedict['5-3'][j][0]) + timedict['5-3'][j][
1✔
1021
                    0])
1022
                time.extend(10 ** (timedict['4-3'][j] + (time53 - timedict['4-3'][j]) * gfrac))
1✔
1023
            else:
1024
                time43 = (mapped_time['5-3'][j] * (timedict['4-3'][j][-1] - timedict['4-3'][j][0]) + timedict['4-3'][j][
1✔
1025
                    0])
1026
                time.extend(10 ** (time43 + (timedict['5-3'][j] - time43) * gfrac))
1✔
1027

1028
            dmdt.extend(10 ** (dmdtdict['4-3'][j] + (dmdtdict['5-3'][j] - dmdtdict['4-3'][j]) * gfrac))
1✔
1029
    else:
1030
        time = np.concatenate((timedict[g][0], timedict[g][1]))
1✔
1031
        time = 10 ** time
1✔
1032
        dmdt = np.concatenate((dmdtdict[g][0], dmdtdict[g][1]))
1✔
1033
        dmdt = 10 ** dmdt
1✔
1034

1035
    time = np.array(time)
1✔
1036
    dmdt = np.array(dmdt)
1✔
1037

1038
    Mh = mbh6 * 1.0e6
1✔
1039

1040
    if starmass < 0.1:
1✔
1041
        Mstar_Tout = 0.1
×
1042
    else:
1043
        Mstar_Tout = starmass
1✔
1044

1045
    Z = 0.0134
1✔
1046
    log10_Z_02 = np.log10(Z / 0.02)
1✔
1047

1048
    Tout_theta = (
1✔
1049
                1.71535900 + 0.62246212 * log10_Z_02 - 0.92557761 * log10_Z_02 ** 2 - 1.16996966 * log10_Z_02 ** 3 - 0.30631491 * log10_Z_02 ** 4)
1050
    Tout_l = (
1✔
1051
                6.59778800 - 0.42450044 * log10_Z_02 - 12.13339427 * log10_Z_02 ** 2 - 10.73509484 * log10_Z_02 ** 3 - 2.51487077 * log10_Z_02 ** 4)
1052
    Tout_kpa = (
1✔
1053
                10.08855000 - 7.11727086 * log10_Z_02 - 31.67119479 * log10_Z_02 ** 2 - 24.24848322 * log10_Z_02 ** 3 - 5.33608972 * log10_Z_02 ** 4)
1054
    Tout_lbda = (
1✔
1055
                1.01249500 + 0.32699690 * log10_Z_02 - 0.00923418 * log10_Z_02 ** 2 - 0.03876858 * log10_Z_02 ** 3 - 0.00412750 * log10_Z_02 ** 4)
1056
    Tout_mu = (
1✔
1057
                0.07490166 + 0.02410413 * log10_Z_02 + 0.07233664 * log10_Z_02 ** 2 + 0.03040467 * log10_Z_02 ** 3 + 0.00197741 * log10_Z_02 ** 4)
1058
    Tout_nu = 0.01077422
1✔
1059
    Tout_eps = (
1✔
1060
                3.08223400 + 0.94472050 * log10_Z_02 - 2.15200882 * log10_Z_02 ** 2 - 2.49219496 * log10_Z_02 ** 3 - 0.63848738 * log10_Z_02 ** 4)
1061
    Tout_o = (
1✔
1062
                17.84778000 - 7.45345690 * log10_Z_02 - 48.9606685 * log10_Z_02 ** 2 - 40.05386135 * log10_Z_02 ** 3 - 9.09331816 * log10_Z_02 ** 4)
1063
    Tout_pi = (
1✔
1064
                0.00022582 - 0.00186899 * log10_Z_02 + 0.00388783 * log10_Z_02 ** 2 + 0.00142402 * log10_Z_02 ** 3 - 0.00007671 * log10_Z_02 ** 4)
1065

1066
    Rstar = ((
1✔
1067
                         Tout_theta * Mstar_Tout ** 2.5 + Tout_l * Mstar_Tout ** 6.5 + Tout_kpa * Mstar_Tout ** 11 + Tout_lbda * Mstar_Tout ** 19 + Tout_mu * Mstar_Tout ** 19.5) / (
1068
                         Tout_nu + Tout_eps * Mstar_Tout ** 2 + Tout_o * Mstar_Tout ** 8.5 + Mstar_Tout ** 18.5 + Tout_pi * Mstar_Tout ** 19.5))
1069

1070
    dmdt = (dmdt * np.sqrt(Mhbase / Mh) * (starmass / Mstarbase) ** 2.0 * (Rstarbase / Rstar) ** 1.5)
1✔
1071
    time = (time * np.sqrt(Mh / Mhbase) * (Mstarbase / starmass) * (Rstar / Rstarbase) ** 1.5)
1✔
1072

1073
    DAY_CGS = 86400
1✔
1074
    time = time / DAY_CGS
1✔
1075
    tfallback = np.copy(time[0])
1✔
1076

1077
    time = time - tfallback
1✔
1078
    tpeak = time[np.argmax(dmdt)]
1✔
1079

1080
    timeinterpfunc = interp1d(time, dmdt)
1✔
1081
    lengthpretimes = len(np.where(times < time[0])[0])
1✔
1082
    lengthposttimes = len(np.where(times > time[-1])[0])
1✔
1083

1084
    dmdt2 = timeinterpfunc(times[lengthpretimes:(len(times) - lengthposttimes)])
1✔
1085
    dmdt1 = np.zeros(lengthpretimes)
1✔
1086
    dmdt3 = np.zeros(lengthposttimes)
1✔
1087

1088
    dmdtnew = np.append(dmdt1, dmdt2)
1✔
1089
    dmdtnew = np.append(dmdtnew, dmdt3)
1✔
1090
    dmdtnew[dmdtnew < 0] = 0
1✔
1091

1092
    kappa_t = 0.2 * (1 + 0.74)
1✔
1093
    Ledd = (4 * np.pi * cc.graviational_constant * Mh * cc.solar_mass * cc.speed_of_light / kappa_t)
1✔
1094

1095
    luminosities = (efficiency * dmdtnew * cc.speed_of_light * cc.speed_of_light)
1✔
1096
    luminosities = (luminosities * leddlimit * Ledd / (luminosities + leddlimit * Ledd))
1✔
1097
    luminosities = [0.0 if np.isnan(x) else x for x in luminosities]
1✔
1098

1099
    ProcessedData = namedtuple('ProcessedData', [
1✔
1100
        'luminosities', 'Rstar', 'tpeak', 'beta', 'starmass', 'dmdt', 'Ledd', 'tfallback'])
1101
    ProcessedData = ProcessedData(luminosities=luminosities, Rstar=Rstar, tpeak=tpeak, beta=beta, starmass=starmass,
1✔
1102
                                  dmdt=dmdtnew, Ledd=Ledd, tfallback=float(tfallback))
1103
    return ProcessedData
1✔
1104

1105
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2019ApJ...872..151M/abstract, https://ui.adsabs.harvard.edu/abs/2013ApJ...767...25G/abstract, https://ui.adsabs.harvard.edu/abs/2018ApJS..236....6G/abstract')
1✔
1106
def _tde_fallback_all_outputs(time, mbh6, mstar, tvisc, bb, eta, leddlimit, **kwargs):
1✔
1107
    """
1108
    Identical to the Mosfit model following Guillochon+ 2013 apart from doing the fallback rate fudge in mosfit.
1109

1110
    :param time: A dense array of times in rest frame in days
1111
    :param mbh6: black hole mass in units of 10^6 solar masses
1112
    :param mstar: star mass in units of solar masses
1113
    :param tvisc: viscous timescale in days
1114
    :param bb: Relates to beta and gamma values for the star that's disrupted
1115
    :param eta: efficiency of the BH
1116
    :param leddlimit: eddington limit for the BH
1117
    :param kwargs: Additional keyword arguments
1118
    :return: bolometric luminosity
1119
    """
1120
    _interaction_process = kwargs.get("interaction_process", ip.Viscous)
1✔
1121
    dense_resolution = kwargs.get("dense_resolution", 1000)
1✔
1122
    dense_times = np.linspace(0, time[-1] + 100, dense_resolution)
1✔
1123
    outs = _tde_mosfit_engine(times=dense_times, mbh6=mbh6, mstar=mstar, b=bb, efficiency=eta,
1✔
1124
                              leddlimit=leddlimit, **kwargs)
1125
    dense_lbols = outs.luminosities
1✔
1126
    interaction_class = _interaction_process(time=time, dense_times=dense_times, luminosity=dense_lbols, t_viscous=tvisc, **kwargs)
1✔
1127
    lbol = interaction_class.new_luminosity
1✔
1128
    return lbol, outs
1✔
1129

1130
def tde_fallback_bolometric(time, mbh6, mstar, tvisc, bb, eta, leddlimit, **kwargs):
1✔
1131
    """
1132
    Identical to the Mosfit model following Guillochon+ 2013 apart from doing the fallback rate fudge in mosfit.
1133

1134
    :param time: A dense array of times in rest frame in days
1135
    :param mbh6: black hole mass in units of 10^6 solar masses
1136
    :param mstar: star mass in units of solar masses
1137
    :param tvisc: viscous timescale in days
1138
    :param bb: Relates to beta and gamma values for the star that's disrupted
1139
    :param eta: efficiency of the BH
1140
    :param leddlimit: eddington limit for the BH
1141
    :param kwargs: Additional keyword arguments
1142
    :return: bolometric luminosity
1143
    """
1144
    lbol, _ = _tde_fallback_all_outputs(time=time, mbh6=mbh6, mstar=mstar, tvisc=tvisc, bb=bb, eta=eta,
1✔
1145
                                       leddlimit=leddlimit, **kwargs)
1146
    return lbol
1✔
1147

1148
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2019ApJ...872..151M/abstract, https://ui.adsabs.harvard.edu/abs/2013ApJ...767...25G/abstract, https://ui.adsabs.harvard.edu/abs/2018ApJS..236....6G/abstract')
1✔
1149
def tde_fallback(time, redshift, mbh6, mstar, tvisc, bb, eta, leddlimit, rph0, lphoto, **kwargs):
1✔
1150
    """
1151
    Identical to the Mosfit model following Guillochon+ 2013 apart from doing the fallback rate fudge in mosfit.
1152

1153
    :param time: Times in observer frame in days
1154
    :param redshift: redshift of the transient
1155
    :param mbh6: black hole mass in units of 10^6 solar masses
1156
    :param mstar: star mass in units of solar masses
1157
    :param tvisc: viscous timescale in days
1158
    :param bb: Relates to beta and gamma values for the star that's disrupted
1159
    :param eta: efficiency of the BH
1160
    :param leddlimit: eddington limit for the BH
1161
    :param rph0: initial photosphere radius
1162
    :param lphoto: photosphere luminosity
1163
    :param kwargs: Additional keyword arguments
1164
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1165
    """
1166

1167
    kwargs['interaction_process'] = kwargs.get("interaction_process", ip.Viscous)
1✔
1168
    kwargs['photosphere'] = kwargs.get("photosphere", photosphere.TDEPhotosphere)
1✔
1169
    kwargs['sed'] = kwargs.get("sed", sed.Blackbody)
1✔
1170
    cosmology = kwargs.get('cosmology', cosmo)
1✔
1171
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
1172

1173
    if kwargs['output_format'] == 'flux_density':
1✔
1174
        frequency = kwargs['frequency']
1✔
1175
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1176
        lbol, outs = _tde_fallback_all_outputs(time=time, mbh6=mbh6, mstar=mstar, tvisc=tvisc, bb=bb, eta=eta,
1✔
1177
                                               leddlimit=leddlimit, **kwargs)
1178
        photo = kwargs['photosphere'](time=time, luminosity=lbol, mass_bh=mbh6*1e6,
1✔
1179
                                      mass_star=mstar, star_radius=outs.Rstar,
1180
                                      tpeak=outs.tpeak, rph_0=rph0, lphoto=lphoto, beta=outs.beta, **kwargs)
1181
        sed_1 = kwargs['sed'](time=time, temperature=photo.photosphere_temperature, r_photosphere=photo.r_photosphere,
1✔
1182
                     frequency=frequency, luminosity_distance=dl)
1183
        flux_density = sed_1.flux_density
1✔
1184
        flux_density = np.nan_to_num(flux_density)
1✔
1185
        return flux_density.to(uu.mJy).value
1✔
1186
    else:
1187
        time_obs = time
1✔
1188
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 100))
1✔
1189
        time_temp = np.geomspace(0.1, 1000, 300)
1✔
1190
        time_observer_frame = time_temp * (1. + redshift)
1✔
1191
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
1192
                                                     redshift=redshift, time=time_observer_frame)
1193
        lbol, outs = _tde_fallback_all_outputs(time=time, mbh6=mbh6, mstar=mstar, tvisc=tvisc, bb=bb, eta=eta,
1✔
1194
                                               leddlimit=leddlimit, **kwargs)
1195
        photo = kwargs['photosphere'](time=time, luminosity=lbol, mass_bh=mbh6*1e6, mass_star=mstar,
1✔
1196
                                      star_radius=outs.Rstar, tpeak=outs.tpeak, rph_0=rph0, lphoto=lphoto,
1197
                                      beta=outs.beta,**kwargs)
1198
        sed_1 = kwargs['sed'](time=time, temperature=photo.photosphere_temperature, r_photosphere=photo.r_photosphere,
1✔
1199
                     frequency=frequency[:, None], luminosity_distance=dl)
1200
        fmjy = sed_1.flux_density.T
1✔
1201
        spectra = fmjy.to(uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
1202
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))
1203
        if kwargs['output_format'] == 'spectra':
1✔
1204
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
1205
                                                                          lambdas=lambda_observer_frame,
1206
                                                                          spectra=spectra)
1207
        else:
1208
            return sed.get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
1✔
1209
                                                              spectra=spectra, lambda_array=lambda_observer_frame,
1210
                                                              **kwargs)
1211
                                                              
1212
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2024arXiv240815048M/abstract')   
1✔
1213
def fitted(time, redshift, log_mh, a_bh, m_disc, r0, tvi, t_form, incl, **kwargs):
1✔
1214
    """
1215
    An import of FitTeD to model the plateau phase
1216
    
1217
    :param time: observer frame time in days
1218
    :param redshift: redshift
1219
    :param log_mh: log of the black hole mass (solar masses)
1220
    :param a_bh: black hole spin parameter (dimensionless)
1221
    :param m_disc: initial mass of disc ring (solar masses)
1222
    :param r0: initial radius of disc ring (gravitational radii)
1223
    :param tvi: viscous timescale of disc evolution (days)
1224
    :param t_form: time of ring formation prior to t = 0 (days)
1225
    :param incl: disc-observer inclination angle (radians)    
1226
    :param kwargs: Must be all the kwargs required by the specific output_format 
1227
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'  
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 cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1232
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1233
    """
1234
    import fitted #user needs to have downloaded and compiled FitTeD in order to run this model
1✔
1235
    cosmology = kwargs.get('cosmology', cosmo)
1✔
1236
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
1237
    ang = 180.0/np.pi*incl
1✔
1238
    m = fitted.models.GR_disc()
1✔
1239

1240
    if kwargs['output_format'] == 'flux_density':
1✔
1241
        frequency = kwargs['frequency']
1✔
1242
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1243
        freqs_un = np.unique(frequency)
1✔
1244
        nulnus = np.zeros(len(time))
1✔
1245
        if len(freqs_un) == 1:
1✔
1246
            nulnus = m.model_UV(time, log_mh, a_bh, m_disc, r0, tvi, t_form, ang, frequency)
1✔
1247
        else:
1248
            for i in range(0,len(freqs_un)):
1✔
1249
                inds = np.where(frequency == freqs_un[i])[0]
1✔
1250
                nulnus[inds] = m.model_UV([time[j] for j in inds], log_mh, a_bh, m_disc, r0, tvi, t_form, ang, freqs_un[i])
1✔
1251
        flux_density = nulnus/(4.0 * np.pi * dl**2 * frequency)   
1✔
1252
        return flux_density/1.0e-26   
1✔
1253

1254
    else:
1255
        time_obs = time
1✔
1256
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 100))
1✔
1257
        time_temp = np.geomspace(0.1, 3000, 300) # in days
1✔
1258
        time_observer_frame = time_temp * (1. + redshift)
1✔
1259
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
1260
                                                     redshift=redshift, time=time_observer_frame)
1261
        nulnus = m.model_SEDs(time, log_mh, a_bh, m_disc, r0, tvi, t_form, ang, frequency)
1✔
1262
        flux_density = (nulnus/(4.0 * np.pi * dl**2 * frequency[:,np.newaxis] * 1.0e-26)) 
1✔
1263
        fmjy = flux_density.T           
1✔
1264
        spectra = (fmjy * uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
1265
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))  
1266
        if kwargs['output_format'] == 'spectra':
1✔
1267
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
1✔
1268
                                                                          lambdas=lambda_observer_frame,
1269
                                                                          spectra=spectra)
1270
        else:
1271
            return sed.get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
1✔
1272
                                                              spectra=spectra, lambda_array=lambda_observer_frame,
1273
                                                              **kwargs)
1274
                                                              
1275
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2024arXiv240815048M/abstract')   
1✔
1276
def fitted_pl_decay(time, redshift, log_mh, a_bh, m_disc, r0, tvi, t_form, incl, log_L, t_decay, p, log_T, sigma_t, t_peak, **kwargs):
1✔
1277
    """
1278
    An import of FitTeD to model the plateau phase, with a gaussian rise and power-law decay
1279
    
1280
    :param time: observer frame time in days
1281
    :param redshift: redshift
1282
    :param log_mh: log of the black hole mass (solar masses)
1283
    :param a_bh: black hole spin parameter (dimensionless)
1284
    :param m_disc: initial mass of disc ring (solar masses)
1285
    :param r0: initial radius of disc ring (gravitational radii)
1286
    :param tvi: viscous timescale of disc evolution (days)
1287
    :param t_form: time of ring formation prior to t = 0 (days)
1288
    :param incl: disc-observer inclination angle (radians)    
1289
    :param log_L: single temperature blackbody amplitude for decay model (log_10 erg/s)
1290
    :param t_decay: fallback timescale (days)
1291
    :param p: power-law decay index
1292
    :param log_T: single temperature blackbody temperature for decay model (log_10 Kelvin)
1293
    :param sigma_t: gaussian rise timescale (days)
1294
    :param t_peak: time of light curve peak (days)
1295
    :param kwargs: Must be all the kwargs required by the specific output_format 
1296
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'  
1297
    :param frequency: Required if output_format is 'flux_density'.
1298
        frequency to calculate - Must be same length as time array or a single number).
1299
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1300
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1301
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1302
    """
1303
    import fitted #user needs to have downloaded and compiled FitTeD in order to run this model
1✔
1304
    cosmology = kwargs.get('cosmology', cosmo)
1✔
1305
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
1306
    ang = 180.0/np.pi*incl
1✔
1307
    m = fitted.models.GR_disc(decay_type='pl', rise=True)
1✔
1308

1309
    if kwargs['output_format'] == 'flux_density':
1✔
1310
        frequency = kwargs['frequency']
1✔
1311
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1312
        freqs_un = np.unique(frequency)
1✔
1313
        
1314
        #initialize arrays
1315
        nulnus_plateau = np.zeros(len(time))
1✔
1316
        nulnus_rise = np.zeros(len(time))
1✔
1317
        nulnus_decay = np.zeros(len(time))
1✔
1318
        
1319
        if len(freqs_un) == 1:
1✔
1320
            nulnus_plateau = m.model_UV(time, log_mh, a_bh, m_disc, r0, tvi, t_form, ang, v=freqs_un[0])
1✔
1321
            nulnus_decay = m.decay_model(time, log_L, t_decay, p, t_peak, log_T, v=freqs_un[0])
1✔
1322
            nulnus_rise = m.rise_model(time, log_L, sigma_t, t_peak, log_T, v=freqs_un[0])
1✔
1323
        else:
1324
            for i in range(0,len(freqs_un)):
×
1325
                inds = np.where(frequency == freqs_un[i])[0]
×
1326
                nulnus[inds] = m.model_UV([time[j] for j in inds], log_mh, a_bh, m_disc, r0, tvi, t_form, ang, freqs_un[i])
×
1327
                nulnus_decay[inds] = m.decay_model([time[j] for j in inds], log_L, t_decay, p, t_peak, log_T, v=freqs_un[i]) 
×
1328
                nulnus_rise[inds] = m.rise_model([time[j] for j in inds], log_L, sigma_t, t_peak, log_T, v=freqs_un[i])                                             
×
1329
        nulnus = nulnus_plateau + nulnus_rise + nulnus_decay    
1✔
1330
        flux_density = nulnus/(4.0 * np.pi * dl**2 * frequency)   
1✔
1331
        return flux_density/1.0e-26   
1✔
1332

1333
    else:
1334
        time_obs = time
×
1335
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 100))
×
1336
        time_temp = np.geomspace(0.1, 3000, 300) # in days
×
1337
        time_observer_frame = time_temp * (1. + redshift)
×
1338
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
×
1339
                                                     redshift=redshift, time=time_observer_frame)
1340
        nulnus_plateau = m.model_SEDs(time, log_mh, a_bh, m_disc, r0, tvi, t_form, ang, frequency)
×
1341

1342
        freq_0 = 6e14
×
1343
        l_e_amp = (model.decay_model(time, log_L, t_decay, t_peak, log_T, freq_0) + model.rise_model(time, log_L, sigma_t, t_peak, log_T, freq_0))
×
1344
        nulnus_risedecay = ((l_e_amp[:, None] * (frequency/freq_0)**4 * 
×
1345
                        (np.exp(cc.planck * freq_0/(cc.boltzmann_constant * 10**log_T)) - 1)/(np.exp(cc.planck * frequency/(cc.boltzmann_constant * 10**log_T)) - 1)).T)  
1346
        flux_density = ((nulnus_risedecay + nulnus_plateau)/(4.0 * np.pi * dl**2 * frequency[:,np.newaxis] * 1.0e-26))  
×
1347
        fmjy = flux_density.T           
×
1348
        spectra = (fmjy * uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
×
1349
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))  
1350
        if kwargs['output_format'] == 'spectra':
×
1351
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
1352
                                                                          lambdas=lambda_observer_frame,
1353
                                                                          spectra=spectra)
1354
        else:
1355
            return sed.get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
×
1356
                                                              spectra=spectra, lambda_array=lambda_observer_frame,
1357
                                                              **kwargs)  
1358
                                                              
1359
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2024arXiv240815048M/abstract')   
1✔
1360
def fitted_exp_decay(time, redshift, log_mh, a_bh, m_disc, r0, tvi, t_form, incl, log_L, t_decay, log_T, sigma_t, t_peak, **kwargs):
1✔
1361
    """
1362
    An import of FitTeD to model the plateau phase, with a gaussian rise and exponential decay
1363
    
1364
    :param time: observer frame time in days
1365
    :param redshift: redshift
1366
    :param log_mh: log of the black hole mass (solar masses)
1367
    :param a_bh: black hole spin parameter (dimensionless)
1368
    :param m_disc: initial mass of disc ring (solar masses)
1369
    :param r0: initial radius of disc ring (gravitational radii)
1370
    :param tvi: viscous timescale of disc evolution (days)
1371
    :param t_form: time of ring formation prior to t = 0 (days)
1372
    :param incl: disc-observer inclination angle (radians)    
1373
    :param log_L: single temperature blackbody amplitude for decay model (log_10 erg/s)
1374
    :param t_decay: fallback timescale (days)
1375
    :param log_T: single temperature blackbody temperature for decay model (log_10 Kelvin)
1376
    :param sigma_t: gaussian rise timescale (days)
1377
    :param t_peak: time of light curve peak (days)
1378
    :param kwargs: Must be all the kwargs required by the specific output_format 
1379
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'  
1380
    :param frequency: Required if output_format is 'flux_density'.
1381
        frequency to calculate - Must be same length as time array or a single number).
1382
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1383
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1384
    :return: set by output format - 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'
1385
    """
1386
    import fitted #user needs to have downloaded and compiled FitTeD in order to run this model
1✔
1387
    cosmology = kwargs.get('cosmology', cosmo)
1✔
1388
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
1389
    ang = 180.0/np.pi*incl
1✔
1390
    m = fitted.models.GR_disc(decay_type='exp', rise=True)
1✔
1391

1392
    if kwargs['output_format'] == 'flux_density':
1✔
1393
        frequency = kwargs['frequency']
1✔
1394
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1395
        freqs_un = np.unique(frequency)
1✔
1396
        
1397
        #initialize arrays
1398
        nulnus_plateau = np.zeros(len(time))
1✔
1399
        nulnus_rise = np.zeros(len(time))
1✔
1400
        nulnus_decay = np.zeros(len(time))
1✔
1401
        
1402
        if len(freqs_un) == 1:
1✔
1403
            nulnus_plateau = m.model_UV(time, log_mh, a_bh, m_disc, r0, tvi, t_form, ang, v=freqs_un[0])
1✔
1404
            nulnus_decay = m.decay_model(time, log_L, t_decay, t_peak, log_T, v=freqs_un[0])
1✔
1405
            nulnus_rise = m.rise_model(time, log_L, sigma_t, t_peak, log_T, v=freqs_un[0])
1✔
1406
        else:
1407
            for i in range(0,len(freqs_un)):
×
1408
                inds = np.where(frequency == freqs_un[i])[0]
×
1409
                nulnus[inds] = m.model_UV([time[j] for j in inds], log_mh, a_bh, m_disc, r0, tvi, t_form, ang, freqs_un[i])
×
1410
                nulnus_decay[inds] = m.decay_model([time[j] for j in inds], log_L, t_decay, t_peak, log_T, v=freqs_un[i]) 
×
1411
                nulnus_rise[inds] = m.rise_model([time[j] for j in inds], log_L, sigma_t, t_peak, log_T, v=freqs_un[i]) 
×
1412
        nulnus = nulnus_plateau + nulnus_rise + nulnus_decay        
1✔
1413
        flux_density = nulnus/(4.0 * np.pi * dl**2 * frequency)   
1✔
1414
        return flux_density/1.0e-26   
1✔
1415

1416
    else:
1417
        time_obs = time
×
1418
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 100))
×
1419
        time_temp = np.geomspace(0.1, 3000, 300) # in days
×
1420
        time_observer_frame = time_temp * (1. + redshift)
×
1421
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
×
1422
                                                     redshift=redshift, time=time_observer_frame)
1423
        nulnus_plateau = m.model_SEDs(time, log_mh, a_bh, m_disc, r0, tvi, t_form, ang, frequency)
×
1424

1425
        freq_0 = 6e14
×
1426
        l_e_amp = (m.decay_model(time, log_L, t_decay, t_peak, log_T, freq_0) + m.rise_model(time, log_L, sigma_t, t_peak, log_T, freq_0))
×
1427
        nulnus_risedecay = ((l_e_amp[:, None] * (frequency/freq_0)**4 * 
×
1428
                        (np.exp(cc.planck * freq_0/(cc.boltzmann_constant * 10**log_T)) - 1)/(np.exp(cc.planck * frequency/(cc.boltzmann_constant * 10**log_T)) - 1)).T) 
1429
        flux_density = ((nulnus_risedecay + nulnus_plateau)/(4.0 * np.pi * dl**2 * frequency[:,np.newaxis] * 1.0e-26))  
×
1430
        fmjy = flux_density.T           
×
1431
        spectra = (fmjy * uu.mJy).to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
×
1432
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom))  
1433
        if kwargs['output_format'] == 'spectra':
×
1434
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
1435
                                                                          lambdas=lambda_observer_frame,
1436
                                                                          spectra=spectra)
1437
        else:
1438
            return sed.get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame,
×
1439
                                                              spectra=spectra, lambda_array=lambda_observer_frame,
1440
                                                              **kwargs)               
1441

1442
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2015ApJ...806..164P/abstract, https://ui.adsabs.harvard.edu/abs/2020ApJ...904...73R/abstract')
1✔
1443
def _stream_stream_collision(mbh_6, mstar, c1, f, h_r, inc_tcool, del_omega):
1✔
1444
    """
1445
    A TDE model based on stream-stream collisions.  Used as input for the bolometric and broadband versions.
1446
    
1447
    :param mbh_6: black hole mass (10^6 solar masses)
1448
    :param mstar: mass of the disrupted star (solar masses)
1449
    :param c1: characteristic distance scale of the emission region in units of the apocenter distance of the most tightly bound debris
1450
    :param f: fraction of the bound mass within the semimajor axis of the most tightly bound debris at peak mass return time
1451
    :param h_r: aspect ratio used to calculate t_cool. This is only used when include_tcool_tdyn_ratio = 1
1452
    :param inc_tcool: if include_tcool_tdyn_ratio = 1, the luminosity is limited by the Eddington luminosity if t_cool / t_dyn < 1.0
1453
    :param del_omega: solid angle (in units of pi) of radiation from the emission region
1454
    :return: physical outputs
1455
    """
1456
    kappa = 0.34
1✔
1457
    t_ratio = 0.0
1✔
1458
    factor = 1.0
1✔
1459
    tcool = 0.0
1✔
1460

1461
    rstar = 0.93 * mstar ** (8.0 / 9.0)
1✔
1462
    mstar_max = 15.0
1✔
1463
    Xi = (1.27 - 0.3 *(mbh_6)**0.242 )*((0.620 + np.exp((min(mstar_max,mstar) - 0.674)/0.212)) 
1✔
1464
            / (1.0 + 0.553 *np.exp((min(mstar,mstar_max) - 0.674)/0.212)))
1465
    r_tidal = (mbh_6 * 1e6/ mstar)**(1.0/3.0) * rstar * cc.solar_radius
1✔
1466

1467
    epsilon = cc.graviational_constant * (mbh_6 * 1e6 * cc.solar_mass) * (rstar * cc.solar_radius) / r_tidal ** 2.0
1✔
1468
    a0 = cc.graviational_constant * (mbh_6 * 1e6 * cc.solar_mass)/ (Xi * epsilon)
1✔
1469

1470
    t_dyn = np.pi / np.sqrt(2.0) * a0 ** 1.5 / np.sqrt(cc.graviational_constant * (mbh_6 * 1e6 * cc.solar_mass))
1✔
1471
    t_peak = (3.0/2.0)*t_dyn
1✔
1472
    mdotmax = mstar * cc.solar_mass / t_dyn / 3.0
1✔
1473
    factor_denom = del_omega * cc.sigma_sb * c1**2 * a0**2
1✔
1474

1475
    if inc_tcool == 1:
1✔
1476
        semi = a0 / 2.0     #semimajor axis of the most bound debris
1✔
1477
        area = np.pi * ( c1 * semi ) **2 # emitting area
1✔
1478
        tau = kappa * (f * mstar * cc.solar_mass / 2.0) / area / 2.0  # the characteristic vertical optical depth to the midplane of a circular disk with radius ∼ semi. The first "/2.0" comes from the fact that we consider only the bound mass = mstar / 2. The second "/2.0" comes from the fact that the optical depth was integrated to the mid-plane.
1✔
1479
        tcool = tau * (h_r) * c1 * semi / cc.speed_of_light
1✔
1480
        t_ratio = tcool / t_dyn
1✔
1481
        factor = 2.0 / (1.0 + t_ratio)
1✔
1482
        factor_denom *= (1.0 + 2.0 * h_r) / 4.0
1✔
1483
    
1484
    t_output = np.linspace(t_peak, 1500*cc.day_to_s, 1000)    
1✔
1485
    Lmax = mdotmax * (Xi * epsilon) / c1    
1✔
1486
    Lobs = Lmax * (t_output / t_peak)**(-5.0/3.0) * factor
1✔
1487
    Tobs = (Lobs / factor_denom )**(1.0/4.0)
1✔
1488
    
1489
    output = namedtuple('output', ['bolometric_luminosity', 'photosphere_temperature',
1✔
1490
                                   'Smbh_6_accretion_rate_max', 'time_temp', 'cooling_time',
1491
                                   'dynamical_time', 'r_tidal','debris_energy'])
1492
    output.bolometric_luminosity = Lobs
1✔
1493
    output.photosphere_temperature = Tobs
1✔
1494
    output.Smbh_6_accretion_rate_max = mdotmax
1✔
1495
    output.time_temp = t_output
1✔
1496
    output.cooling_time = tcool
1✔
1497
    output.dynamical_time = t_dyn
1✔
1498
    output.r_tidal = r_tidal
1✔
1499
    output.debris_energy = Xi * epsilon
1✔
1500
    return output
1✔
1501

1502
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2015ApJ...806..164P/abstract, https://ui.adsabs.harvard.edu/abs/2020ApJ...904...73R/abstract')    
1✔
1503
def stream_stream_tde_bolometric(time, mbh_6, mstar, c1, f, h_r, inc_tcool, del_omega, sigma_t, peak_time, **kwargs): 
1✔
1504
    """
1505
    A bolometric TDE model based on stream-stream collisions.  The early emission follows a gaussian rise.
1506
    
1507
    :param time: observer frame time in days
1508
    :param mbh_6: black hole mass (10^6 solar masses)
1509
    :param mstar: mass of the disrupted star (solar masses)
1510
    :param c1: characteristic distance scale of the emission region in units of the apocenter distance of the most tightly bound debris
1511
    :param f: fraction of the bound mass within the semimajor axis of the most tightly bound debris at peak mass return time
1512
    :param h_r: aspect ratio used to calculate t_cool. This is only used when include_tcool_tdyn_ratio = 1
1513
    :param inc_tcool: if include_tcool_tdyn_ratio = 1, the luminosity is limited by the Eddington luminosity if t_cool / t_dyn < 1.0
1514
    :param del_omega: solid angle (in units of pi) of radiation from the emission region
1515
    :param peak_time: peak time in days
1516
    :param sigma_t: the sharpness of the Gaussian in days
1517
    :return: bolometric luminosity         
1518
    """
1519
    output = _stream_stream_collision(mbh_6, mstar, c1, f, h_r, inc_tcool, del_omega)    
1✔
1520
    f1 = pm.gaussian_rise(time=output.time_temp[0] / cc.day_to_s, a_1=1, peak_time=peak_time, sigma_t=sigma_t)
1✔
1521
    norm = output.bolometric_luminosity[0] / f1
1✔
1522

1523
    #evaluate giant array of bolometric luminosities
1524
    tt_pre_fb = np.linspace(0, output.time_temp[0]-0.001, 100)
1✔
1525
    tt_post_fb = output.time_temp
1✔
1526
    full_time = np.concatenate([tt_pre_fb, tt_post_fb])
1✔
1527
    f1 = norm * np.exp(-(tt_pre_fb - (peak_time * cc.day_to_s))**2.0 / (2 * (sigma_t * cc.day_to_s) **2.0))
1✔
1528
    f2 = output.bolometric_luminosity
1✔
1529
    full_lbol = np.concatenate([f1, f2])
1✔
1530
    lbol_func = interp1d(full_time, y=full_lbol, fill_value='extrapolate')
1✔
1531
    return lbol_func(time*cc.day_to_s)
1✔
1532

1533
@citation_wrapper('https://ui.adsabs.harvard.edu/abs/2015ApJ...806..164P/abstract, https://ui.adsabs.harvard.edu/abs/2020ApJ...904...73R/abstract')
1✔
1534
def stream_stream_tde(time, redshift, mbh_6, mstar, c1, f, h_r, inc_tcool, del_omega, sigma_t, peak_time, **kwargs):
1✔
1535
    """
1536
    A TDE model based on stream-stream collisions.  The early emission follows a constant temperature gaussian rise.
1537
    
1538
    :param time: observer frame time in days
1539
    :param redshift: redshift
1540
    :param mbh_6: black hole mass (10^6 solar masses)
1541
    :param mstar: mass of the disrupted star (solar masses)
1542
    :param c1: characteristic distance scale of the emission region in units of the apocenter distance of the most tightly bound debris
1543
    :param f: fraction of the bound mass within the semimajor axis of the most tightly bound debris at peak mass return time
1544
    :param h_r: aspect ratio used to calculate t_cool. This is only used when include_tcool_tdyn_ratio = 1
1545
    :param inc_tcool: if include_tcool_tdyn_ratio = 1, the luminosity is limited by the Eddington luminosity if t_cool / t_dyn < 1.0
1546
    :param del_omega: solid angle (in units of pi) of radiation from the emission region
1547
    :param peak_time: peak time in days
1548
    :param sigma_t: the sharpness of the Gaussian in days
1549
    :param kwargs: Must be all the kwargs required by the specific output_format 
1550
    :param output_format: 'flux_density', 'magnitude', 'spectra', 'flux', 'sncosmo_source'  
1551
    :param frequency: Required if output_format is 'flux_density'.
1552
        frequency to calculate - Must be same length as time array or a single number).
1553
    :param bands: Required if output_format is 'magnitude' or 'flux'.
1554
    :param cosmology: Cosmology to use for luminosity distance calculation. Defaults to Planck18. Must be a astropy.cosmology object.
1555
    :return: set by output format - 'flux_density' or 'magnitude'     
1556
    """
1557

1558
    cosmology = kwargs.get('cosmology', cosmo)
1✔
1559
    dl = cosmology.luminosity_distance(redshift).cgs.value
1✔
1560
    output = _stream_stream_collision(mbh_6, mstar, c1, f, h_r, inc_tcool, del_omega)
1✔
1561

1562
    #get bolometric and temperature info
1563
    f1 = pm.gaussian_rise(time=output.time_temp[0] / cc.day_to_s, a_1=1, peak_time=peak_time, sigma_t=sigma_t)
1✔
1564
    norm = output.bolometric_luminosity[0] / f1    
1✔
1565
    tt_pre_fb = np.linspace(0, output.time_temp[0]-0.001, 100)
1✔
1566
    tt_post_fb = output.time_temp
1✔
1567
    full_time = np.concatenate([tt_pre_fb, tt_post_fb])
1✔
1568
    f1_src = pm.gaussian_rise(time=tt_pre_fb, a_1=norm,
1✔
1569
                          peak_time=peak_time * cc.day_to_s, sigma_t=sigma_t * cc.day_to_s)
1570
    f2_src = output.bolometric_luminosity
1✔
1571
    full_lbol = np.concatenate([f1_src, f2_src])
1✔
1572
    
1573
    temp1 = np.ones(100) * output.photosphere_temperature[0]
1✔
1574
    temp2 = output.photosphere_temperature
1✔
1575
    full_temp = np.concatenate([temp1, temp2])
1✔
1576
    r_eff = np.sqrt(full_lbol / (np.pi * cc.sigma_sb * full_temp**4.0))            
1✔
1577
        
1578
    if kwargs['output_format'] == 'flux_density':
1✔
1579
        frequency = kwargs['frequency']
1✔
1580
        if isinstance(frequency, float):
1✔
1581
            frequency = np.ones(len(time)) * frequency           
1✔
1582
    
1583
        # convert to source frame time and frequency
1584
        frequency, time = calc_kcorrected_properties(frequency=frequency, redshift=redshift, time=time)
1✔
1585
        unique_frequency = np.sort(np.unique(frequency))
1✔
1586

1587
        # build flux density function for each frequency
1588
        flux_den_interp_func = {}
1✔
1589
        total_time = full_time * (1 + redshift)
1✔
1590
        for freq in unique_frequency:           
1✔
1591
            flux_den = sed.blackbody_to_flux_density(temperature=full_temp,
1✔
1592
                                           r_photosphere=r_eff,
1593
                                           dl=dl, frequency=freq).to(uu.mJy)
1594
            flux_den_interp_func[freq] = interp1d(total_time, flux_den, fill_value='extrapolate')
1✔
1595

1596
        # interpolate onto actual observed frequency and time values
1597
        flux_density = []
1✔
1598
        for freq, tt in zip(frequency, time):
1✔
1599
            flux_density.append(flux_den_interp_func[freq](tt * cc.day_to_s))
1✔
1600
        flux_density = flux_density * uu.mJy
1✔
1601
        return flux_density.to(uu.mJy).value    
1✔
1602
        
1603
    else:
1604
        time_obs = time
1✔
1605
        lambda_observer_frame = kwargs.get('lambda_array', np.geomspace(100, 60000, 100))
1✔
1606
        time_observer_frame = full_time * (1. + redshift)
1✔
1607
        frequency, time = calc_kcorrected_properties(frequency=lambda_to_nu(lambda_observer_frame),
1✔
1608
                                                     redshift=redshift, time=time_observer_frame)
1609
        freq_0 = 6e14
1✔
1610
        flux_den = sed.blackbody_to_flux_density(temperature=full_temp,
1✔
1611
                            r_photosphere=r_eff,
1612
                            dl=dl, frequency=freq_0).to(uu.mJy)           
1613
        fmjy = ((flux_den[:, None] * (frequency/freq_0)**4 * 
1✔
1614
                        (np.exp(cc.planck * freq_0/(cc.boltzmann_constant * full_temp[:, None])) - 1) / (np.exp(cc.planck * frequency/(cc.boltzmann_constant * full_temp[:, None])) - 1)).T)                
1615
        spectra = fmjy.T.to(uu.erg / uu.cm ** 2 / uu.s / uu.Angstrom,
1✔
1616
                                     equivalencies=uu.spectral_density(wav=lambda_observer_frame * uu.Angstrom)) 
1617
        if kwargs['output_format'] == 'spectra':
1✔
1618
            return namedtuple('output', ['time', 'lambdas', 'spectra'])(time=time_observer_frame,
×
1619
                                                                          lambdas=lambda_observer_frame,
1620
                                                                          spectra=spectra)
1621
        else:
1622
            return sed.get_correct_output_format_from_spectra(time=time_obs, time_eval=time_observer_frame/cc.day_to_s,
1✔
1623
                                                              spectra=spectra, lambda_array=lambda_observer_frame,
1624
                                                              **kwargs) 
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