import re import json import numpy as np from scipy import constants from astropy.io import fits from astropy.coordinates import SkyCoord from .config import cpism_refdata, MAG_SYSTEM from .config import S # pysynphot from .optics import filter_throughput from .io import log PLANK_H = constants.h # ~6.62607015e-34 LIGHT_SPEED = constants.c # ~3e8 DEG2RAD = np.pi / 180 R_JUPITER_METER = 6.99e4 AU_METER = 1.49e8 DEFAULT_FILTER_FILE = f'{cpism_refdata}/throughtput/f661_total.fits' HYBRID_MODEL_FILE = f'{cpism_refdata}/target_model/hybrid_model.fits' def _sptype2num(spectral_type): """ convert spectral type string to number, for interpretation case0: normal case - M1V: 6.1, 5 - O5IV: 0.5, 4 - F3V: 3.3, 5 - K4.5II: 5.45, 2 case 1: star type or subtype missing zero or V will return - M1: 6.1, 5 - M: 6.0, 5 case 2: spectral type + subtype subtype will be ignored - K3Vvar: 5.3, 5 - F6Vbwvar: 3.6, 5 - K0IV SB: 5.0, 4 - F5V+: 3.5, 5 case 3: multi spectral type only the first sptype is used - G5IV/V +K1IV: 4.5, 4 - F7IV-V: 3.7, 4 - O4/O5IV: 0.4, 0 - G3/G5V: 4.3, 0 case 4: illegal input ValueError will be raised """ obafgkm = 'OBAFGKML' spectral_type = spectral_type.upper() # match spectral type such as M1V, O5IV, F3V, K4.5II matched = re.match( RF'^([{obafgkm}])([0-9]\d*\.?\d*)*([IV]*)', spectral_type) if not matched: raise ValueError(f"illegal spectral type input: {spectral_type}") shorttype = obafgkm.find(matched.group(1)) subtype = 0.0 if matched.group(2): subtype = float(matched.group(2)) stlist = ['O', 'I', 'II', 'III', 'IV', 'V'] startype = dict(zip(stlist, range(len(stlist)))).get(matched.group(3), 5) return shorttype + subtype / 10, startype def _spnum2teff(spn, stn): """ interpret of spectral number (by __sptype2num) to get t_eff and log_g look up table from the document of ck04model """ with open(f'{cpism_refdata}/target_model/sptype2teff_lut.json', 'r') as fid: sptype_teff_lut = json.load(fid) def _interp(spn, stn): lut = sptype_teff_lut[f'{stn}'] teff = np.interp(spn, lut[0], lut[1]) logg = np.interp(spn, lut[0], lut[2]) return teff, logg stn = 5 if stn not in [1, 2, 3, 4, 5] else stn if stn in [1, 3, 5]: return _interp(spn, stn) else: teff_low, logg_low = _interp(spn, stn-1) teff_high, logg_high = _interp(spn, stn+1) return (teff_high + teff_low)/2, (logg_high + logg_low)/2 def star_photlam( magnitude: float, sptype: str, is_blackbody: bool = False, mag_input_band: str = 'f661'): """ genrate flux spectrum of a star by its observal magnitude and spectral type Parameters ---------- magnitude: float magnitude of the star sptype: str spectral type of the star is_blackbody: bool if True, use blackbody spectrum instead of ck04model mag_input_band: str bandpass of the input magnitude, default is f661 Returns ------- pysynphot.spectrum.ArraySpectrum spectrum of the star in photlam unit """ spn, stn = _sptype2num(sptype) t_eff, log_g = _spnum2teff(spn, stn) log.debug(f"{sptype} star => [{t_eff=:}, {log_g=:}]; {is_blackbody=:}") filter = filter_throughput(mag_input_band) if not is_blackbody: METALLICITY = 0 spectrum = S.Icat('ck04models', t_eff, METALLICITY, log_g) else: spectrum = S.BlackBody(t_eff) star_sp = spectrum.renorm(magnitude, MAG_SYSTEM, filter) star_sp.convert('photlam') return star_sp def hybrid_albedo_spectrum( coe_b: float, coe_r: float): """ albedo spectrum of planet using hybrid-jupiter-neptune model (Lacy et al. 2018) jupiter and neptune spectrum is from Karkoschka’s 1994 Parameters ---------- coe_b: float coefficient of blue spectrum, 1 for jupiter, 0 for neptune coe_r: float coefficient of red spectrum, 1 for jupiter, 0 for neptune Returns ------- pysynphot.spectrum.ArrayBandpass albedo spectrum of the planet """ log.debug(f"planet hybrid spectrum with {coe_b=:}, {coe_r=:}") model = fits.getdata(HYBRID_MODEL_FILE) spec = model[1, :] * coe_r spec += model[2, :] * coe_b spec += model[3, :] * (1 - coe_r) spec += model[4, :] * (1 - coe_b) albedo = S.ArrayBandpass( wave=model[0, :], throughput=spec, waveunits='nm' ) albedo.convert('angstrom') return albedo def extract_target_x_y( target: dict, ra0: str = None, dec0: str = None): """ extract x, y of target from target dict Parameters ---------- target: dict target dict. must contain either (ra, dec) or (pangle, spearation) ra0: str ra of center star. must be provided if (ra, dec) of target is used dec0: str dec of center star. must be provided if (ra, dec) of target is used Returns ------- x, y: float x, y of target in arcsec Raises ------ ValueError if (ra, dec) of target is used but (ra, dec) of center star is not provided. ValueError one of (ra, dec) or (pangle, spearation) is not provided. """ def _pa2xy(p_angle, separation): p_angle_rad = p_angle * DEG2RAD x = separation * np.sin(p_angle_rad) y = separation * np.cos(p_angle_rad) log.debug(f"({p_angle=:}, {separation=:}) => ({x=:}, {y=:})") return x, y if 'pangle' in target.keys() and 'separation' in target.keys(): return _pa2xy(target['pangle'], target['separation']) if 'ra' not in target.keys() or 'dec' not in target.keys(): raise ValueError( 'either (ra, dec) or (pangle, separation) needed in target dict') if ra0 is None or dec0 is None: raise ValueError( '(ra, dec) of center star must be provided if (ra, dec) of bkstar is used' ) ra, dec = target['ra'], target['dec'] log.debug(f"target: {ra=:}, {dec=:}, center star: {ra0=:}, {dec0=:}") cstar = SkyCoord(ra0, dec0) bkstar = SkyCoord(ra, dec) separation = cstar.separation(bkstar).arcsec p_angle = cstar.position_angle(bkstar).degree x, y = _pa2xy(p_angle, separation) return x, y def planet_contrast( planet_x_au: float, planet_y_au: float, phase_angle: float, radius: float): """ calculate the contrast of a planet Parameters ---------- planet_x_au: float x position of the planet in au planet_y_au: float y position of the planet in au phase_angle: float phase angle of the planet in degree radius: float radius of the planet in jupiter radius Returns ------- contrast: float contrast of the planet """ separation = np.sqrt(planet_x_au**2 + planet_y_au**2) phase_angle = phase_angle * DEG2RAD if np.sin(phase_angle) < 1e-9: raise ValueError('sin(phase_angle) can not be 0') sep_3d = separation / np.sin(phase_angle) # Lambert Scattering phase function # from Madhusudhan and Burrows 2012 equation 33. phase = (np.sin(phase_angle) + (np.pi - phase_angle) * np.cos(phase_angle)) / np.pi log.debug(f'alpha: {phase_angle/np.pi*180} {phase=}') contrast = (radius / sep_3d * R_JUPITER_METER / AU_METER)**2 * phase return contrast def spectrum_generator( targets: dict): """ generate the spectrum due to the input target list Parameters ---------- targets: dict target dictionary which contains keyword 'cstar' (necessary), 'stars'(optional), 'planets' (optional). The values are: - cstar: dict - center star information. must contain keywords ra, dec, distance, magnitude, sptype - stars: list of dict, optional - list of background stars. each dict must contain ra, dec, magnitude, sptype - planets: list of dict, optional - list of planets. each dict must contain pangle, separation, magnitude, radius Returns ------- obj_sp_list: list list of [x, y, spectrum] of each target Examples -------- >>> targets = { ... target = { ... 'name': 'TEST1', ... 'cstar': { ... 'magnitude': 0, ... 'ra': '120d', ... 'dec': '40d', ... 'distance': 10, ... 'sptype': 'G0III', ... }, ... 'stars': [ ... { ... 'magnitude': 15, ... 'ra': '120.001d', ... 'dec': '40.001d', ... 'sptype': 'F0III', ... 'isblackbody': True ... }, ... { ... 'magnitude': 10, ... 'pangle': -60, ... 'separation': 2, ... 'sptype': 'A2.5II', ... 'isblackbody': False ... }, ... ... ], ... 'planets': [ ... { ... 'radius': 2, ... 'pangle': 60, ... 'coe_b': 0.3, ... 'coe_r': 0.7, ... 'separation': 0.5, ... 'phase_angle': 90, ... } ... ] ... } >>> spectrum_generator(targets) """ cstar = targets['cstar'] stars = targets.get('stars', []) planets = targets.get('planets', []) obj_sp_list = [] cstar_spectrum = star_photlam( cstar['magnitude'], cstar['sptype'], is_blackbody=cstar.get('isblackbody', False), mag_input_band=cstar.get('mag_input_band', 'f661') ) cstar_ra = cstar['ra'] cstar_dec = cstar['dec'] cstar_dist = cstar['distance'] obj_sp_list.append([0, 0, cstar_spectrum]) for star in stars: star_x, star_y = extract_target_x_y(star, cstar_ra, cstar_dec) spectrum = star_photlam( star['magnitude'], star['sptype'], is_blackbody=star.get('isblackbody', False), mag_input_band=cstar.get('mag_input_band', 'f661') ) obj_sp_list.append([star_x, star_y, spectrum]) for planet in planets: planet_x, planet_y = extract_target_x_y(planet) # angle between observation direction and star-planet direction phase_angle = planet.get('phase_angle', 90) radius = planet.get('radius', 1) contrast = planet_contrast( planet_x * cstar_dist, planet_y * cstar_dist, phase_angle, radius, ) coe_blue, coe_red = planet.get('coe_b', 1), planet.get('coe_r', 1) albedo_spect = hybrid_albedo_spectrum(coe_blue, coe_red) spectrum = albedo_spect * cstar_spectrum * contrast obj_sp_list.append([planet_x, planet_y, spectrum]) return obj_sp_list