Source code for gadfly.scale

import os
import warnings

import numpy as np

import astropy.units as u
from astropy.modeling.models import Voigt1D
from astropy.utils.exceptions import AstropyUserWarning
from astropy.modeling.models import BlackBody

from tynt import FilterGenerator, Filter

__all__ = [
    'p_mode_amplitudes', 'delta_nu', 'nu_max',
    'tau_eff', 'tau_gran',
    'granulation_amplitude', 'c_K',
    'amplitude_with_wavelength'
]

# Solar parameters
_solar_temperature = 5777 * u.K
_solar_mass = 1 * u.M_sun
_solar_radius = 1 * u.R_sun
_solar_luminosity = 1 * u.L_sun

# Huber et al. (2011)
# https://ui.adsabs.harvard.edu/abs/2011ApJ...743..143H/abstract
_solar_nu_max = 3090 * u.uHz
_solar_delta_nu = 135.1 * u.uHz

# Bahng & Schwarzschild (1961)
# https://ui.adsabs.harvard.edu/abs/1961ApJ...134..312B/abstract
_solar_granulation_timescale = 8.6 * u.min

# https://www2.mps.mpg.de/solar-system-school/germerode/Solar-Photosphere-Chromosphere.pdf
_solar_pressure_scale_height = 125 * u.km

# scaling relation parameters from Huber et al. (2011)
# https://ui.adsabs.harvard.edu/abs/2011ApJ...743..143H/abstract
_huber_r = 2
_huber_s = 0.886
_huber_t = 1.89

dirname = os.path.dirname(os.path.abspath(__file__))
default_alpha_table_path = os.path.join(
    dirname, 'data', 'estimate_alpha.json'
)


[docs]@u.quantity_input(temperature=u.K) def c_K(temperature): """ Bolometric correction factor as a function of effective temperature. Given in the abstract of Ballot et al. (2011) [1]_, and Eqn 8 of Huber et al. (2011) [2]_. Parameters ---------- temperature : ~astropy.units.Quantity Effective temperature References ---------- .. [1] `Ballot et al. (2011) <https://ui.adsabs.harvard.edu/abs/2011A%26A...531A.124B/abstract>`_ .. [2] `Huber et al. (2011) <https://ui.adsabs.harvard.edu/abs/2011ApJ...743..143H/abstract>`_ """ return float( (temperature / (5934 * u.K)) ** 0.8 )
def _amplitudes_huber(mass, temperature, luminosity): return ( luminosity ** _huber_s / (mass ** _huber_t * temperature ** (_huber_r - 1) * c_K(temperature)) ) @u.quantity_input(mass=u.g, temperature=u.K, luminosity=u.L_sun) def _p_mode_amplitudes_huber(mass, temperature, luminosity): """ p-mode oscillation power amplitudes. Huber et al. (2011), Eqn 9 [1]_. Parameters ---------- mass : ~astropy.units.Quantity Stellar mass temperature : ~astropy.units.Quantity Effective temperature luminosity : ~astropy.units.Quantity Stellar luminosity References ---------- .. [1] `Huber et al. (2011) <https://ui.adsabs.harvard.edu/abs/2011ApJ...743..143H/abstract>`_ """ return float( _amplitudes_huber(mass, temperature, luminosity) / _amplitudes_huber(_solar_mass, _solar_temperature, _solar_luminosity) ) def tau_osc(temperature): # mode lifetime return 1 / (np.pi * _lifetimes_lund(temperature.value)) def _p_mode_amplitudes_kb2011( mass, radius, temperature, luminosity, nu, wavelength, r=2.0 ): # Kjeldsen & Bedding (2011), Eqn 20 return ( luminosity * tau_osc(temperature) ** 0.5 / (wavelength * mass ** 1.5 * temperature ** (2.25 - r)) )
[docs]@u.quantity_input( mass=u.g, radius=u.m, temperature=u.K, luminosity=u.L_sun, wavelength=u.nm, nu=u.Hz, nu_solar=u.Hz ) def p_mode_amplitudes( mass, radius, temperature, luminosity, nu, nu_solar, wavelength=550*u.nm ): """ p-mode oscillation power amplitudes. Kjeldsen & Bedding (2011), Eqn 20 [1]_. Parameters ---------- mass : ~astropy.units.Quantity Stellar mass temperature : ~astropy.units.Quantity Effective temperature luminosity : ~astropy.units.Quantity Stellar luminosity References ---------- .. [1] `Huber et al. (2011) <https://ui.adsabs.harvard.edu/abs/2011A%26A...529L...8K/abstract>`_ """ num = _p_mode_amplitudes_kb2011( mass, radius, temperature, luminosity, nu, wavelength ) denom = _p_mode_amplitudes_kb2011( _solar_mass, _solar_radius, _solar_temperature, _solar_luminosity, nu_solar, 550 * u.nm ) return (num / denom).to(u.dimensionless_unscaled).value
[docs]@u.quantity_input(mass=u.g, radius=u.m) def delta_nu(mass, radius): """ Large frequency separation scaling. Huber et al. (2012) Eqn 3 [1]_. Parameters ---------- mass : ~astropy.units.Quantity Stellar mass radius : ~astropy.units.Quantity Stellar radius References ---------- .. [1] `Huber et al. (2012) <https://ui.adsabs.harvard.edu/abs/2012ApJ...760...32H/abstract>`_ """ return float( (mass / _solar_mass) ** 0.5 * (radius / _solar_radius) ** (-3 / 2) )
[docs]@u.quantity_input(mass=u.g, temperature=u.K, radius=u.m) def nu_max(mass, temperature, radius): """ Frequency of maximum power. Huber et al. (2012) Eqn 4 [1]_. Parameters ---------- mass : ~astropy.units.Quantity Stellar mass temperature : ~astropy.units.Quantity Effective temperature radius : ~astropy.units.Quantity Stellar radius References ---------- .. [1] `Huber et al. (2012) <https://ui.adsabs.harvard.edu/abs/2012ApJ...760...32H/abstract>`_ """ return float( (mass / _solar_mass) * (radius / _solar_radius) ** -2 * (temperature / _solar_temperature) ** -0.5 )
[docs]@u.quantity_input(nu=u.Hz) def tau_eff(nu_max): """ Characteristic granulation timescale. Relation from Kallinger et al. (2014) [1]_, described in the last sentence in the paragraph below Eqn 4. Parameters ---------- nu_max : ~astropy.units.Quantity p-mode maximum frequency References ---------- .. [1] `Kallinger et al. (2014) <https://ui.adsabs.harvard.edu/abs/2014A%26A...570A..41K/abstract>`_ """ return float( (nu_max / _solar_nu_max) ** -0.89 )
def _fwhm_corsaro(temperature): temperature_kelvin = temperature.to(u.K).value return np.exp( 1463.49 - 1.03503 * temperature_kelvin + 0.000271565 * temperature_kelvin ** 2 - 3.14139e-08 * temperature_kelvin ** 3 + 1.35524e-12 * temperature_kelvin ** 4 ) @u.quantity_input(temperature=u.K) def _fwhm_deprecated(temperature, quiet=False): """ Scale the FWHM of p-mode oscillation peaks in power. From the fit in Figure 7 of Corsaro et al. (2015) [1]_. Actual parameters for the fit from Enrico Corsaro (private communication). Parameters ---------- temperature : ~astropy.units.Quantity Stellar temperature quiet : bool Set to True to raise a warning if the effective temperature is outside of the range where Corsaro's fit is valid. References ---------- .. [1] `Corsaro et al. (2015) <https://ui.adsabs.harvard.edu/abs/2015A%26A...579A..83C/abstract>`_ """ if temperature < 4900 * u.K: if not quiet: message = ( "The p-mode FWHM scaling relations from Corsaro " "et al. (2015) are only valid for " "effective temperatures >4900 K, but you " f"gave temperature={temperature}. The algorithm will proceed " f"by fixing temperature=4900 K within the calculation " f"for the p-mode FWHM scaling (only)." ) warnings.warn(message, AstropyUserWarning) temperature = 4900 * u.K return float( _fwhm_corsaro(_solar_temperature) / _fwhm_corsaro(temperature) ) def _lifetimes_lund(temperature): # Lund 2017 Eqn 32 Gamma_0 = 0.07 alpha = 0.91 beta = 15.3 return ( Gamma_0 + alpha * (temperature / 5777) ** beta ) * u.uHz def quality(nu_max, temperature): """ Scale the mode lifetime of p-mode oscillation peaks in power. Gets the mode lifetime scaling as a function of the p-mode central frequency before and after scaling, as shown in Figure 20 of Lund et al. (2017) [1]_, and the scaling with frequency described by Eqn 30 of the same paper. Parameters ---------- nu_max : ~astropy.units.Quantity Frequency of the maximum p-mode oscillations. temperature : ~astropy.units.Quantity Stellar temperature References ---------- .. [1] `Lund et al. (2017) <https://ui.adsabs.harvard.edu/abs/2017ApJ...835..172L/abstract>`_ """ Gamma_sun = _lifetimes_lund(_solar_temperature.value) solar_Q = (_solar_nu_max / Gamma_sun).to(u.dimensionless_unscaled).value Gamma_star = _lifetimes_lund(temperature.value) star_Q = (nu_max / Gamma_star).to(u.dimensionless_unscaled).value scale_factor_Q = star_Q / solar_Q return scale_factor_Q def _tau_gran(mass, temperature, luminosity): return luminosity / (mass * temperature ** 3.5)
[docs]@u.quantity_input(mass=u.g, temperature=u.K, luminosity=u.L_sun) def tau_gran(mass, temperature, luminosity): """ Granulation timescale scaling. Kjeldsen & Bedding (2011) Eqn 9 [1]_. Parameters ---------- mass : ~astropy.units.Quantity Stellar mass temperature : ~astropy.units.Quantity Effective temperature luminosity : ~astropy.units.Quantity Stellar luminosity References ---------- .. [1] `Kjeldsen & Bedding (2011) <https://ui.adsabs.harvard.edu/abs/2011A%26A...529L...8K/abstract>`_ """ return float( _tau_gran(mass, temperature, luminosity) / _tau_gran(_solar_mass, _solar_temperature, _solar_luminosity) )
@u.quantity_input(radius=u.m) def n(mass, radius, temperature, luminosity): """ Proportion of granules on the stellar surface. From Kjeldsen & Bedding (2011) Eqn 13 [1]_. References ---------- .. [1] `Kjeldsen & Bedding (2011) <https://ui.adsabs.harvard.edu/abs/2011A%26A...529L...8K/abstract>`_ """ return (radius / H_P(mass, temperature, luminosity)) ** 2 @u.quantity_input(mass=u.g, temperature=u.K, luminosity=u.L_sun) def H_P(mass, temperature, luminosity): """ Pressure scale height of stellar photosphere. Kjeldsen & Bedding (2011) Eqn 8 [1]_. Parameters ---------- nu_max : ~astropy.units.Quantity p-mode maximum frequency mass : ~astropy.units.Quantity Stellar mass temperature : ~astropy.units.Quantity Effective temperature luminosity : ~astropy.units.Quantity Stellar luminosity References ---------- .. [1] `Kjeldsen & Bedding (2011) <https://ui.adsabs.harvard.edu/abs/2011A%26A...529L...8K/abstract>`_ """ stellar_H_factor = luminosity / (mass * temperature ** 3) solar_H_factor = _solar_luminosity / (_solar_mass * _solar_temperature ** 3) return ( _solar_pressure_scale_height * stellar_H_factor / solar_H_factor ) def _granulation_power_factor(mass, temperature, luminosity): return luminosity ** 2 / (mass ** 3 * temperature ** 5.5)
[docs]@u.quantity_input(mass=u.g, temperature=u.K, luminosity=u.L_sun) def granulation_amplitude(mass, temperature, luminosity): """ Granulation amplitude scaling. Kjeldsen & Bedding (2011) Eqn 24 [1]_. Parameters ---------- mass : ~astropy.units.Quantity Stellar mass temperature : ~astropy.units.Quantity Effective temperature luminosity : ~astropy.units.Quantity Stellar luminosity References ---------- .. [1] `Kjeldsen & Bedding (2011) <https://ui.adsabs.harvard.edu/abs/2011A%26A...529L...8K/abstract>`_ """ return float( _granulation_power_factor(mass, temperature, luminosity) / _granulation_power_factor( _solar_mass, _solar_temperature, _solar_luminosity ) )
@u.quantity_input(freq=u.uHz) def _v_osc_kiefer(freq): """ Velocity spectral power from Kiefer et al. (2018) [1]_. Parameters ---------- freq : ~astropy.units.Quantity Frequency (not angular) References ---------- .. [1] See Eqns 1-5 and the parameter estimates in Table 1 of `Kiefer et al. (2018) <https://ui.adsabs.harvard.edu/abs/2018SoPh..293..151K/abstract>`_ """ nu_max = 3079.76 * u.uHz # peak p-mode frequency sigma = 181.8 * u.uHz # stddev of Gaussian gamma = 150.9 * u.uHz # HWHM of Lorentzian Sigma = 611.8 * u.uHz # FWHM of Voigt S = -0.100 # asymmetry parameter a = 3299 * 1e4 * u.m**2 / u.s**2 / u.Hz # height factor b = -581 * u.m**2 / u.s**2 / u.Hz # offset factor A = 1 / np.pi * (np.arctan(S * (freq - nu_max) / Sigma).to(u.rad).value + 0.5) voigt_profile = Voigt1D(x_0=nu_max, amplitude_L=a, fwhm_L=2*gamma, fwhm_G=2.355 * sigma) return (A * (b + voigt_profile(freq)) * u.uHz).to(u.m**2/u.s**2).value @u.quantity_input(freq=u.uHz, nu_max=u.uHz, delta_nu=u.uHz) def _v_osc_kiefer_scaled(freq, nu_max, delta_nu): """ Velocity spectral power from Kiefer et al. (2018) [1]_. Parameters ---------- freq : ~astropy.units.Quantity Frequency (not angular) References ---------- .. [1] See Eqns 1-5 and the parameter estimates in Table 1 of `Kiefer et al. (2018) <https://ui.adsabs.harvard.edu/abs/2018SoPh..293..151K/abstract>`_ """ sigma = 181.8 / (_solar_delta_nu / delta_nu) * u.uHz # stddev of Gaussian gamma = 150.9 / (_solar_delta_nu / delta_nu) * u.uHz # HWHM of Lorentzian Sigma = 611.8 / (_solar_delta_nu / delta_nu) * u.uHz # FWHM of Voigt S = -0.1 # asymmetry parameter a = 3299 * 1e4 * u.m**2 / u.s**2 / u.Hz # height factor b = -581 * u.m**2 / u.s**2 / u.Hz # offset factor A = 1 / np.pi * (np.arctan(S * (freq - nu_max) / Sigma).to(u.rad).value + 0.5) voigt_profile = Voigt1D(x_0=nu_max, amplitude_L=a, fwhm_L=2*gamma, fwhm_G=2.355 * sigma) return (A * (b + voigt_profile(freq)) * u.uHz).to(u.m**2/u.s**2).value @u.quantity_input(freq=u.uHz, wavelength=u.nm, temperature=u.K) def _p_mode_intensity_kjeldsen_bedding( freq, wavelength=550 * u.nm, temperature=5777 * u.K ): """ Power spectrum intensity scaling for p-modes. Computes the amplitudes of p-mode oscillations in the velocity power spectrum from Kiefer et al. (2018) [1]_, and converts the velocity power spectrum to an intensity estimate using Kjeldsen & Bedding (1995) [1]_ Parameters ---------- freq : ~astropy.units.Quantity Frequency (not angular) wavelength : ~astropy.units.Quantity Wavelength of observations temperature : ~astropy.units.Quantity Effective temperature References ---------- .. [1] See Eqns 1-5 and the parameter estimates in Table 1 of `Kiefer et al. (2018) <https://ui.adsabs.harvard.edu/abs/2018SoPh..293..151K/abstract>`_ .. [2] See Eqn 5 of `Kjeldsen & Bedding (1995) <https://ui.adsabs.harvard.edu/abs/1995A%26A...293...87K/abstract>`_ """ # in units of power spectral density: velocity_to_intensity = ( _v_osc_kiefer(freq) / (wavelength / (550 * u.nm)) / (temperature / (5777 * u.K)) ** 2 ).decompose() return 20.1 * velocity_to_intensity * u.cds.ppm**2 / u.uHz @u.quantity_input(wavelength=u.nm, temperature=u.K) def _velocity_to_intensity( velocity_power_spectrum, temperature, wavelength=550 * u.nm ): # Kjeldsen & Bedding (1995) return 20.1 * ( velocity_power_spectrum / (wavelength / (550 * u.nm)) / (temperature / (5777 * u.K)) ** 2 ) # units: ppm @u.quantity_input(freq=u.uHz, wavelength=u.nm, temperature=u.K) def p_mode_intensity( temperature, freq, nu_max, delta_nu, wavelength=550 * u.nm ): """ Power spectrum intensity scaling for p-modes. Computes the amplitudes of p-mode oscillations in the velocity power spectrum from Kiefer et al. (2018) [1]_, and converts the velocity power spectrum to an intensity estimate using Kjeldsen & Bedding (1995) [2]_. Parameters ---------- freq : ~astropy.units.Quantity Frequency (not angular) wavelength : ~astropy.units.Quantity Wavelength of observations temperature : ~astropy.units.Quantity Effective temperature References ---------- .. [1] See Eqns 1-5 and the parameter estimates in Table 1 of `Kiefer et al. (2018) <https://ui.adsabs.harvard.edu/abs/2018SoPh..293..151K/abstract>`_ .. [2] See Eqn 5 of `Kjeldsen & Bedding (1995) <https://ui.adsabs.harvard.edu/abs/1995A%26A...293...87K/abstract>`_ """ intensity_stellar_freq = _velocity_to_intensity( _v_osc_kiefer_scaled(freq, nu_max, delta_nu), temperature, wavelength ) intensity_stellar_numax = _velocity_to_intensity( _v_osc_kiefer_scaled(nu_max, nu_max, delta_nu), temperature, wavelength ) # normalized so that at nu_max the relative amplitude is unity: relative_intensity = ( intensity_stellar_freq / intensity_stellar_numax ).to(u.dimensionless_unscaled).value return relative_intensity
[docs]@u.quantity_input(temperature=u.K) def amplitude_with_wavelength(filter, temperature, n_wavelengths=10_000, **kwargs): """ Scale power spectral feature amplitudes with wavelength and stellar effective temperature, compared to their amplitudes when observed with SOHO VIRGO/PMO6. Follows the Taylor expansion argument in Sect 5.1 of Morris et al. (2020), see Eqn 11 [1]_. Makes use of the ``tynt`` package to retrieve filter transmittance curves. To see available filters via ``tynt``, run: >>> from tynt import FilterGenerator >>> f = FilterGenerator() >>> print(f.available_filters()) # doctest: +SKIP Parameters ---------- filter : str or ~tynt.Filter Either the SVO FPS name for a filter bandpass available via the ``tynt`` package, or an instance of the :py:class:`~tynt.Filter` object itself. temperature : ~astropy.units.Quantity Stellar effective temperature n_wavelengths : int Number of wavelengths included in the calculation. **kwargs : dict Passed on to :py:meth:`~tynt.FilterGenerator.reconstruct`. Returns ------- alpha : float Scaling for the amplitude of intensity features relative to the SOHO VIRGO amplitudes References ---------- .. [1] `Morris et al. (2020) <https://ui.adsabs.harvard.edu/abs/2020MNRAS.493.5489M/abstract>`_ """ wavelength = np.logspace(-1.5, 1.5, n_wavelengths) * u.um if isinstance(filter, Filter): selected_filter = filter else: f = FilterGenerator() if filter != 'SOHO VIRGO' and filter in f.available_filters(): # reset the filter selected_filter = f.reconstruct(filter, **kwargs) elif isinstance(filter, Filter): selected_filter = filter elif filter == 'SOHO VIRGO': # just in case you don't want to transform amplitudes with wavelength, # this will have no effect. # SOHO VIRGO "filter profile" described here: # https://adsabs.harvard.edu/full/1995ASPC...76..408A # The SOHO VIRGO PMO6 radiometer measures *bolometric* fluxes: selected_filter = Filter(wavelength, np.ones_like(wavelength.value)) else: raise ValueError(f"filter must be available via the ``tynt`` package or " f"be an instance of the tynt Filter object, but got: {filter}") # Following Morris+ 2020, eqn 11: dT = np.atleast_2d([-10, 10]).T * u.K temperatures = dT + temperature I_nu = BlackBody(temperature)(wavelength) dI_dT = np.diff(BlackBody(temperatures)(wavelength), axis=0)[0] / dT.ptp() wl_micron = wavelength.to(u.um).value filt0_transmittance = np.ones_like(wl_micron) filt1_transmittance = np.interp( wl_micron, selected_filter.wavelength.to(u.um).value, selected_filter.transmittance, left=0, right=0 ) ratio_0 = ( np.trapz(dI_dT * wl_micron * filt1_transmittance, wl_micron) / np.trapz(dI_dT * wl_micron * filt0_transmittance, wl_micron) ) ratio_1 = ( np.trapz(I_nu * wl_micron * filt0_transmittance, wl_micron) / np.trapz(I_nu * wl_micron * filt1_transmittance, wl_micron) ) return (ratio_0 * ratio_1).to(u.dimensionless_unscaled).value