Loading CpicImgSim/target.py 0 → 100644 +395 −0 Original line number Diff line number Diff line 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 Loading
CpicImgSim/target.py 0 → 100644 +395 −0 Original line number Diff line number Diff line 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