From 6acb80a2d916a172d3c503d68497369176c53aef Mon Sep 17 00:00:00 2001 From: Kyle A Pearson Date: Tue, 25 Jul 2023 14:11:32 -0700 Subject: [PATCH 1/8] tweaks to plot --- examples/tess.py | 4 +++- exotic/api/nested_linear_fitter.py | 2 +- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/examples/tess.py b/examples/tess.py index 375476ce..7394c1bf 100644 --- a/examples/tess.py +++ b/examples/tess.py @@ -544,7 +544,9 @@ def check_std(time, flux, dt=0.5): # dt = [hr] myfit = lc_fitter(time[tmask]+2457000.0, flux[tmask], phot_std/flux[tmask], airmass, tpars, mybounds, verbose=True) # create plots - myfit.plot_bestfit(title=f"{args.target} Global Fit") + fig,ax = myfit.plot_bestfit(title=f"{args.target} Global Fit") + # set y_limit between 1 and 99 percentile + ax[0].set_ylim([np.percentile(flux, 1)*0.99, np.percentile(flux,99)*1.01]) plt.savefig( os.path.join( planetdir, planetname+"_global_fit.png")) plt.close() diff --git a/exotic/api/nested_linear_fitter.py b/exotic/api/nested_linear_fitter.py index e25c388f..49c02fd1 100644 --- a/exotic/api/nested_linear_fitter.py +++ b/exotic/api/nested_linear_fitter.py @@ -660,4 +660,4 @@ def main(): print("image saved to: periodogram.png") if __name__ == "__main__": - main() + main() \ No newline at end of file From 3f719ed0f35769dd03cfe312eadf5dc88ef88e29 Mon Sep 17 00:00:00 2001 From: Kyle A Pearson Date: Tue, 25 Jul 2023 14:25:23 -0700 Subject: [PATCH 2/8] updated instructions for running --- examples/tess.py | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/examples/tess.py b/examples/tess.py index 7394c1bf..e7b1adb7 100644 --- a/examples/tess.py +++ b/examples/tess.py @@ -2,13 +2,14 @@ # git clone https://github.com/rzellem/EXOTIC.git # cd EXOTIC # git checkout tess -# conda create -n tess python=3.9 -# conda activate tess -# pip install pandas scipy matplotlib astropy statsmodels cython numpy==1.21.6 -# pip install wotan transitleastsquares pylightcurve lightkurve==2.0.6 ultranest==3.5.6 +# conda create -n tess_exotic python=3.9 +# conda activate tess_exotic # pip install . -# cd examples +# pip install lightkurve==2.0.6 statsmodels wotan transitleastsquares +# pip install numpy==1.21.6 +# cd examples/ # python tess.py -t "HAT-P-18 b" + import os import copy import json From bd4c698c0fcd9c0ab693a377eb1cb17dababdc0e Mon Sep 17 00:00:00 2001 From: Kyle A Pearson Date: Tue, 25 Jul 2023 15:24:36 -0700 Subject: [PATCH 3/8] updates to numpy api --- exotic/api/ew.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exotic/api/ew.py b/exotic/api/ew.py index f0da6a59..c179e2cb 100644 --- a/exotic/api/ew.py +++ b/exotic/api/ew.py @@ -128,7 +128,7 @@ def get_data(self): r = urllib.request.urlopen( os.path.join(base_uri,self.files['file_data_json'][2:])) jdata = json.loads(r.read().decode(r.info().get_param('charset') or 'utf-8')) - return np.array(jdata[1:],dtype=np.float).T + return np.array(jdata[1:],dtype=float).T def translate_keys(rdict): """ Translates the keys to a compatible format for EXOTIC/ELCA From cd0aa7d42f55eb66a42b8ff1ef45c062b4870ecc Mon Sep 17 00:00:00 2001 From: Kyle A Pearson Date: Mon, 31 Jul 2023 13:33:53 -0700 Subject: [PATCH 4/8] color coded O-C plot, added 3sigma clip to unittest --- exotic/api/nested_linear_fitter.py | 47 +++++++++++++++++++++++++++--- 1 file changed, 43 insertions(+), 4 deletions(-) diff --git a/exotic/api/nested_linear_fitter.py b/exotic/api/nested_linear_fitter.py index 49c02fd1..21d9d90a 100644 --- a/exotic/api/nested_linear_fitter.py +++ b/exotic/api/nested_linear_fitter.py @@ -55,7 +55,7 @@ class linear_fitter(object): - def __init__(self, data, dataerr, bounds=None, prior=None): + def __init__(self, data, dataerr, bounds=None, prior=None, labels=None): """ Fit a linear model to data using nested sampling. @@ -73,6 +73,7 @@ def __init__(self, data, dataerr, bounds=None, prior=None): self.data = data self.dataerr = dataerr self.bounds = bounds + self.labels = np.array(labels) self.prior = prior.copy() # dict {'m':(0.1,0.5), 'b':(0,1)} if bounds is None: # use +- 3 sigma prior as bounds @@ -134,8 +135,25 @@ def plot_oc(self, savefile=None, ylim='none', show_2sigma=False, prior_name="Pri # set up the figure fig,ax = plt.subplots(1, figsize=(9,6)) - # plot the data/residuals - ax.errorbar(self.epochs, self.residuals*24*60, yerr=self.dataerr*24*60, ls='none', marker='o',color='black') + # check if labels are not None + if self.labels is not None: + # find unique set of labels + ulabels = np.unique(self.labels) + # set up a color/marker cycle + markers = cycle(['o','v','^','<','>','s','*','h','H','D','d','P','X']) + colors = cycle(['black','blue','green','orange','purple','grey','magenta','cyan','lime']) + + # plot each label separately + for i, ulabel in enumerate(ulabels): + # find where the label matches + mask = self.labels == ulabel + # plot the data/residuals + ax.errorbar(self.epochs[mask], self.residuals[mask]*24*60, yerr=self.dataerr[mask]*24*60, + ls='none', marker=next(markers),color=next(colors), label=ulabel) + else: + # plot the data/residuals + ax.errorbar(self.epochs, self.residuals*24*60, yerr=self.dataerr*24*60, ls='none', marker='o',color='black') + ylower = (self.residuals.mean()-3*np.std(self.residuals))*24*60 yupper = (self.residuals.mean()+3*np.std(self.residuals))*24*60 @@ -595,6 +613,19 @@ def main(): 0.000160, 0.000160, 0.000151, 0.000160, 0.000140, 0.000120, 0.000800, 0.000140 ]) + # labels for a legend + labels = np.array([ 'TESS', 'TESS', + 'TESS', 'EpW', 'ExoClock', 'Unistellar', + 'TESS', 'EpW', 'ExoClock', 'Unistellar', + 'TESS', 'EpW', 'ExoClock', 'Unistellar', + 'TESS', 'EpW', 'ExoClock', 'Unistellar', + 'TESS', 'EpW', 'ExoClock', 'Unistellar', + 'TESS', 'EpW', 'ExoClock', 'Unistellar', + 'TESS', 'EpW', 'ExoClock', 'Unistellar', + 'TESS', 'EpW', 'ExoClock', 'Unistellar', + 'TESS', 'EpW', 'ExoClock', 'Unistellar', + 'TESS', 'EpW', 'ExoClock', 'Unistellar']) + P = 1.360029 # orbital period for your target Tc_norm = Tc - Tc.min() #normalize the data to the first observation @@ -617,6 +648,14 @@ def main(): intercept = params[0] intercept_std_dev = std_dev[0] + # 3 sigma clip based on residuals + calculated = orbit*slope + intercept + residuals = (Tc - calculated)/Tc_error + mask = np.abs(residuals) < 3 + Tc = Tc[mask] + Tc_error = Tc_error[mask] + labels = labels[mask] + #print(res.summary()) #print("Params =",params) #print("Error matrix =",res.normalized_cov_params) @@ -638,7 +677,7 @@ def main(): 'b':[intercept, intercept_std_dev] # value from WLS (replace with literature value) } - lf = linear_fitter( Tc, Tc_error, bounds, prior=prior ) + lf = linear_fitter( Tc, Tc_error, bounds, prior=prior, labels=labels ) lf.plot_triangle() plt.subplots_adjust(top=0.9,hspace=0.2,wspace=0.2) From d43e3df9dcbbae69183983d0aeac81fd3ccc3843 Mon Sep 17 00:00:00 2001 From: jpl-jengelke Date: Fri, 4 Aug 2023 17:33:01 -0700 Subject: [PATCH 5/8] Issue #1210: Updated filters to remove redundancy in naming which is auto-generated now as a convenience function. ... --- exotic/api/filters.py | 79 +++++++++++++++++++++---------------------- 1 file changed, 39 insertions(+), 40 deletions(-) diff --git a/exotic/api/filters.py b/exotic/api/filters.py index cf546d7c..d2f5d15a 100644 --- a/exotic/api/filters.py +++ b/exotic/api/filters.py @@ -1,79 +1,78 @@ # Sources for FWHM band wavelengths are referenced below, respectively # note that all below units default to nm -fwhm = { +__fwhm = { # AAVSO, Source(s): AAVSO International Database; https://www.aavso.org/filters # Johnson - "Johnson U": {"name": "U", "desc": "Johnson U", "fwhm": ("333.8", "398.8")}, - "Johnson B": {"name": "B", "desc": "Johnson B", "fwhm": ("391.6", "480.6")}, - "Johnson V": {"name": "V", "desc": "Johnson V", "fwhm": ("502.8", "586.8")}, - "Johnson R": {"name": "RJ", "desc": "Johnson R", "fwhm": ("590.0", "810.0")}, - "Johnson I": {"name": "IJ", "desc": "Johnson I", "fwhm": ("780.0", "1020.0")}, + "Johnson U": {"name": "U", "fwhm": ("333.8", "398.8")}, + "Johnson B": {"name": "B", "fwhm": ("391.6", "480.6")}, + "Johnson V": {"name": "V", "fwhm": ("502.8", "586.8")}, + "Johnson R": {"name": "RJ", "fwhm": ("590.0", "810.0")}, + "Johnson I": {"name": "IJ", "fwhm": ("780.0", "1020.0")}, # Cousins - "Cousins R": {"name": "R", "desc": "Cousins R", "fwhm": ("561.7", "719.7")}, - "Cousins I": {"name": "I", "desc": "Cousins I", "fwhm": ("721.0", "875.0")}, + "Cousins R": {"name": "R", "fwhm": ("561.7", "719.7")}, + "Cousins I": {"name": "I", "fwhm": ("721.0", "875.0")}, # Near-Infrared - "Near-Infrared J": {"name": "J", "desc": "Near-Infrared J", "fwhm": ("1040.0", "1360.0")}, - "Near-Infrared H": {"name": "H", "desc": "Near-Infrared H", "fwhm": ("1420.0", "1780.0")}, - "Near-Infrared K": {"name": "K", "desc": "Near-Infrared K", "fwhm": ("2015.0", "2385.0")}, + "Near-Infrared J": {"name": "J", "fwhm": ("1040.0", "1360.0")}, + "Near-Infrared H": {"name": "H", "fwhm": ("1420.0", "1780.0")}, + "Near-Infrared K": {"name": "K", "fwhm": ("2015.0", "2385.0")}, # Sloan - "Sloan u": {"name": "SU", "desc": "Sloan u", "fwhm": ("321.8", "386.8")}, - "Sloan g": {"name": "SG", "desc": "Sloan g", "fwhm": ("402.5", "551.5")}, - "Sloan r": {"name": "SR", "desc": "Sloan r", "fwhm": ("553.1", "693.1")}, - "Sloan i": {"name": "SI", "desc": "Sloan i", "fwhm": ("697.5", "827.5")}, - "Sloan z": {"name": "SZ", "desc": "Sloan z", "fwhm": ("841.2", "978.2")}, + "Sloan u": {"name": "SU", "fwhm": ("321.8", "386.8")}, + "Sloan g": {"name": "SG", "fwhm": ("402.5", "551.5")}, + "Sloan r": {"name": "SR", "fwhm": ("553.1", "693.1")}, + "Sloan i": {"name": "SI", "fwhm": ("697.5", "827.5")}, + "Sloan z": {"name": "SZ", "fwhm": ("841.2", "978.2")}, # Stromgren - "Stromgren u": {"name": "STU", "desc": "Stromgren u", "fwhm": ("336.3", "367.7")}, - "Stromgren v": {"name": "STV", "desc": "Stromgren v", "fwhm": ("401.5", "418.5")}, - "Stromgren b": {"name": "STB", "desc": "Stromgren b", "fwhm": ("459.55", "478.05")}, - "Stromgren y": {"name": "STY", "desc": "Stromgren y", "fwhm": ("536.7", "559.3")}, - "Stromgren Hbw": {"name": "STHBW", "desc": "Stromgren Hbw", "fwhm": ("481.5", "496.5")}, - "Stromgren Hbn": {"name": "STHBN", "desc": "Stromgren Hbn", "fwhm": ("487.5", "484.5")}, + "Stromgren u": {"name": "STU", "fwhm": ("336.3", "367.7")}, + "Stromgren v": {"name": "STV", "fwhm": ("401.5", "418.5")}, + "Stromgren b": {"name": "STB", "fwhm": ("459.55", "478.05")}, + "Stromgren y": {"name": "STY", "fwhm": ("536.7", "559.3")}, + "Stromgren Hbw": {"name": "STHBW", "fwhm": ("481.5", "496.5")}, + "Stromgren Hbn": {"name": "STHBN", "fwhm": ("487.5", "484.5")}, # Optec - "Optec Wing A": {"name": "MA", "desc": "Optec Wing A", "fwhm": ("706.5", "717.5")}, - "Optec Wing B": {"name": "MB", "desc": "Optec Wing B", "fwhm": ("748.5", "759.5")}, - "Optec Wing C": {"name": "MI", "desc": "Optec Wing C", "fwhm": ("1003.0", "1045.0")}, + "Optec Wing A": {"name": "MA", "fwhm": ("706.5", "717.5")}, + "Optec Wing B": {"name": "MB", "fwhm": ("748.5", "759.5")}, + "Optec Wing C": {"name": "MI", "fwhm": ("1003.0", "1045.0")}, # PanSTARRS - "PanSTARRS z-short": {"name": "ZS", "desc": "PanSTARRS z-short", "fwhm": ("826.0", "920.0")}, - "PanSTARRS Y": {"name": "Y", "desc": "PanSTARRS Y", "fwhm": ("946.4", "1054.4")}, - "PanSTARRS w": {"name": "N/A", "desc": "PanSTARRS w", "fwhm": ("404.2", "845.8")}, + "PanSTARRS z-short": {"name": "ZS", "fwhm": ("826.0", "920.0")}, + "PanSTARRS Y": {"name": "Y", "fwhm": ("946.4", "1054.4")}, + "PanSTARRS w": {"name": "N/A", "fwhm": ("404.2", "845.8")}, # MObs Clear Filter; Source(s): Martin Fowler - "MObs CV": {"name": "CV", "desc": "MObs CV", "fwhm": ("350.0", "850.0")}, + "MObs CV": {"name": "CV", "fwhm": ("350.0", "850.0")}, # Astrodon CBB; Source(s): George Silvis; https://astrodon.com/products/astrodon-exo-planet-filter/ - "Astrodon ExoPlanet-BB": {"name": "CBB", "desc": "Astrodon ExoPlanet-BB", "fwhm": ("500.0", "1000.0")}, + "Astrodon ExoPlanet-BB": {"name": "CBB", "fwhm": ("500.0", "1000.0")}, } +# expose as fwhm and for convenience set 'desc' field equal to key +fwhm = {k: v for k, v in __fwhm.items() if (v.update(desc=k),)} # aliases to back-reference naming standard updates fwhm_alias = { - "J NIR 1.2 micron": "Near-Infrared J", - "J NIR 1.2micron": "Near-Infrared J", - "H NIR 1.6 micron": "Near-Infrared H", - "H NIR 1.6micron": "Near-Infrared H", - "K NIR 2.2 micron": "Near-Infrared K", - "K NIR 2.2micron": "Near-Infrared K", - "LCO Bessell B": "Johnson B", "LCO Bessell V": "Johnson V", "LCO Bessell R": "Cousins R", "LCO Bessell I": "Cousins I", + "J NIR 1.2 micron": "Near-Infrared J", + "H NIR 1.6 micron": "Near-Infrared H", + "K NIR 2.2 micron": "Near-Infrared K", + "LCO SDSS u'": "Sloan u", "LCO SDSS g'": "Sloan g", "LCO SDSS r'": "Sloan r", "LCO SDSS i'": "Sloan i", - "LCO Pan-STARRS Y": "PanSTARRS Y", "LCO Pan-STARRS zs": "PanSTARRS z-short", + "LCO Pan-STARRS Y": "PanSTARRS Y", "LCO Pan-STARRS w": "PanSTARRS w", - "Exop": "Astrodon ExoPlanet-BB", - "Clear (unfiltered) reduced to V sequence": "MObs CV", + + "Exop": "Astrodon ExoPlanet-BB", } From a1e070d4199eeda84d438192a840bd4c219b6df2 Mon Sep 17 00:00:00 2001 From: jpl-jengelke Date: Sun, 6 Aug 2023 16:01:00 -0700 Subject: [PATCH 6/8] Closes #1210: Implement fuller validation techniques on filter specifications. OOP design changes to support class and static methods. ... --- exotic/api/ld.py | 159 +++++++++++++++++++++++++++++++++++------------ exotic/exotic.py | 4 +- 2 files changed, 122 insertions(+), 41 deletions(-) diff --git a/exotic/api/ld.py b/exotic/api/ld.py index 67bf088c..b396c9ef 100644 --- a/exotic/api/ld.py +++ b/exotic/api/ld.py @@ -1,4 +1,6 @@ +import logging import numpy as np +import re try: from .gael_ld import createldgrid @@ -9,68 +11,132 @@ except ImportError: from filters import fwhm, fwhm_alias +log = logging.getLogger(__name__) +ld_re_punct = r'[\W]' +ld_re_punct_p = re.compile(ld_re_punct) + class LimbDarkening: - ld0 = ld1 = ld2 = ld3 = None - filter_desc = filter_type = None - wl_min = wl_max = None + filter_nonspecific_ids = ('N/A',) # used to prevent identification of generic values + # include filter maps as references from this class fwhm = fwhm + fwhm_alias = fwhm_alias + + # lookup table: fwhm_map references filters irrespective of spacing and punctuation + # 1 - join optimized str lookups in lookup table + fwhm_lookup = {''.join(k.strip().lower().split()): k for k in fwhm.keys()} + fwhm_lookup.update({''.join(k.strip().lower().split()): v for k, v in fwhm_alias.items()}) + # 2 - ignore punctuation in lookup table + fwhm_lookup = {re.sub(ld_re_punct_p, '', k): v for k, v in fwhm_lookup.items()} def __init__(self, stellar): + self.filter_name = self.filter_desc = None + self.ld0 = self.ld1 = self.ld2 = self.ld3 = None self.priors = { - 'T*': stellar['teff'], - 'T*_uperr': stellar['teffUncPos'], - 'T*_lowerr': stellar['teffUncNeg'], - 'FEH*': stellar['met'], - 'FEH*_uperr': stellar['metUncPos'], - 'FEH*_lowerr': stellar['metUncNeg'], - 'LOGG*': stellar['logg'], - 'LOGG*_uperr': stellar['loggUncPos'], - 'LOGG*_lowerr': stellar['loggUncNeg'] + 'T*': stellar.get('teff'), + 'T*_uperr': stellar.get('teffUncPos'), + 'T*_lowerr': stellar.get('teffUncNeg'), + 'FEH*': stellar.get('met'), + 'FEH*_uperr': stellar.get('metUncPos'), + 'FEH*_lowerr': stellar.get('metUncNeg'), + 'LOGG*': stellar.get('logg'), + 'LOGG*_uperr': stellar.get('loggUncPos'), + 'LOGG*_lowerr': stellar.get('loggUncNeg') } + self.wl_max = self.wl_min = None + super() - def standard_list(self): + @staticmethod + def standard_list(): print("\n\n***************************") print("Limb Darkening Coefficients") - print("***************************") - print("\nThe standard bands that are available for limb darkening parameters (https://www.aavso.org/filters)" - "\nas well as filters for MObs and LCO (0.4m telescope) datasets:\n") - for val in self.fwhm.values(): - match = next((f" (or {k})" for k, v in fwhm_alias.items() if v.lower() == val['desc'].lower()), '') + print("***************************\n") + print("The standard bands that are available for limb darkening parameters (https://www.aavso.org/filters)\n" + "as well as filters for MObs and LCO (0.4m telescope) datasets:\n") + for val in LimbDarkening.fwhm.values(): + match = next((f" (or {k})" for k, v in LimbDarkening.fwhm_alias.items() if + v.lower() == val['desc'].lower()), '') print(f"\u2022 {val['desc']}{match}:\n\t-Abbreviation: {val['name']}" f"\n\t-FWHM: ({val['fwhm'][0]}-{val['fwhm'][1]}) nm") - - def check_standard(self, filter_): - if filter_['filter'] in fwhm_alias: - filter_['filter'] = fwhm_alias[filter_['filter']] - for val in self.fwhm.values(): - if filter_['filter'].lower() in (val['desc'].lower(), val['name'].lower()): - self.filter_type = val['desc'] - self.set_filter(self.fwhm[self.filter_type]['name'], self.filter_type, - float(self.fwhm[self.filter_type]['fwhm'][0]), - float(self.fwhm[self.filter_type]['fwhm'][1])) - return True - else: - return False - - def set_filter(self, filter_type, filter_desc, wl_min, wl_max): - self.filter_type = filter_type + return + + def check_standard(self, filter_: dict = None): + filter_alias = filter_matcher = None + try: + if not isinstance(filter_, dict): # set sane failure + log.error("Filter not defined according to spec -- parsing failure.") + return False + for k in ('filter', 'name', 'wl_min', 'wl_max'): # set missing values to nonetype + filter_[k] = filter_.get(k) + if isinstance(filter_[k], str): + # eliminate errant spaces on edges + filter_[k] = filter_[k].strip() + if k == 'name' and filter_[k]: # format 'name' (if exists) to uppercase, no spaces + filter_[k] = ''.join(filter_[k].upper().split()) + if filter_['filter']: # remove spaces, remove punctuation and lowercase + filter_matcher = ''.join(filter_['filter'].lower().split()) + filter_matcher = re.sub(ld_re_punct_p, '', filter_matcher) + # identify defined filters via optimized lookup table + if filter_matcher and filter_matcher in LimbDarkening.fwhm_lookup: + filter_['filter'] = LimbDarkening.fwhm_lookup[filter_matcher] # sets to actual filter reference key + for f in LimbDarkening.fwhm.values(): + # match to wavelength values (strict) + if (filter_['wl_min'] and filter_['wl_min'] == f['fwhm'][0] and + filter_['wl_max'] and filter_['wl_max'] == f['fwhm'][1]): + filter_alias = f + break + # match 'filter' (strict) to actual reference key, e.g. 'desc' + elif filter_['filter'] == f['desc']: + filter_alias = f + break + # match 'name' or 'filter' (strict) to actual name abbreviation, e.g. 'name' + elif filter_['name'] == f['name'].strip().upper() or \ + (filter_['filter'] and filter_['filter'][:5].upper() == f['name'].strip().upper()): + if filter_['name'] in LimbDarkening.filter_nonspecific_ids: # exclude unknown vals for 'name' + pass + filter_alias = f + break + # match 'filter' (loose) to any portion of actual reference key, e.g. 'desc' -- if possible + if filter_matcher and not filter_alias: # filter not identified so try another algorithm + f_count = 0 + for f in LimbDarkening.fwhm_lookup: + if filter_matcher in f: + f_count += 1 + filter_alias = LimbDarkening.fwhm[LimbDarkening.fwhm_lookup[f]] + if f_count > 1: # no unique match, reset + filter_alias = None + break + if filter_alias: + filter_['name'] = filter_alias['name'] + filter_['filter'] = filter_alias['desc'] + filter_['wl_min'] = filter_alias['fwhm'][0] + filter_['wl_max'] = filter_alias['fwhm'][1] + self.set_filter(filter_['name'], filter_['filter'], float(filter_['wl_min']), float(filter_['wl_max'])) + except BaseException as be: + log.error(f"Filter matching failed -- {be}") + return filter_alias is not None + + def set_filter(self, filter_name, filter_desc, wl_min, wl_max): + self.filter_name = filter_name self.filter_desc = filter_desc self.wl_min = wl_min self.wl_max = wl_max + return def calculate_ld(self): - ld_params = createldgrid(np.array([self.wl_min / 1000]), np.array([self.wl_max / 1000]), self.priors) + ld_params = createldgrid(np.array([self.wl_min / 1000.]), np.array([self.wl_max / 1000.]), self.priors) self.set_ld((ld_params['LD'][0][0], ld_params['ERR'][0][0]), (ld_params['LD'][1][0], ld_params['ERR'][1][0]), (ld_params['LD'][2][0], ld_params['ERR'][2][0]), (ld_params['LD'][3][0], ld_params['ERR'][3][0])) + return def set_ld(self, ld0, ld1, ld2, ld3): self.ld0 = ld0 self.ld1 = ld1 self.ld2 = ld2 self.ld3 = ld3 + return def output_ld(self): print("\nEXOTIC-calculated nonlinear limb-darkening coefficients: ") @@ -78,15 +144,17 @@ def output_ld(self): print(f"{self.ld1[0]:5f} +/- + {self.ld1[1]:5f}") print(f"{self.ld2[0]:5f} +/- + {self.ld2[1]:5f}") print(f"{self.ld3[0]:5f} +/- + {self.ld3[1]:5f}") + return def test_ld(ld_obj_, filter_): try: ld_obj_.check_standard(filter_) ld_obj_.calculate_ld() - except (KeyError, AttributeError): + except BaseException as be: + log.exception(be) + log.error("Continuing with default operations. ...") filter_['filter'] = "Custom" - if filter_['wl_min'] and filter_['wl_max']: ld_obj_.set_filter('N/A', filter_['filter'], float(filter_['wl_min']), float(filter_['wl_max'])) ld_obj_.calculate_ld() @@ -95,9 +163,10 @@ def test_ld(ld_obj_, filter_): if key in ['u0', 'u1', 'u2', 'u3']] ld_obj_.set_filter('N/A', filter_['filter'], filter_['wl_min'], filter_['wl_max']) ld_obj_.set_ld(ld_[0], ld_[1], ld_[2], ld_[3]) + return -if __name__ == "__main__": +if __name__ == "__main__": # tests stellar_params = { 'teff': 6001.0, 'teffUncPos': 88.0, @@ -154,8 +223,20 @@ def test_ld(ld_obj_, filter_): 'u3': {"value": -1.63, "uncertainty": 0.12} } + filter_info5 = { + 'filter': 'g\'', + 'wl_min': None, + 'wl_max': None, + 'u0': {"value": 2.118, "uncertainty": 0.051}, + 'u1': {"value": -3.88, "uncertainty": 0.21}, + 'u2': {"value": 4.39, "uncertainty": 0.27}, + 'u3': {"value": -1.63, "uncertainty": 0.12} + } + ld_obj = LimbDarkening(stellar_params) - ld_obj.standard_list() + LimbDarkening.standard_list() + + ld_obj.check_standard(filter_info5) test_ld(ld_obj, filter_info1) # test_ld(ld_obj, filter_info2) diff --git a/exotic/exotic.py b/exotic/exotic.py index ae0429e5..9b6dbacc 100644 --- a/exotic/exotic.py +++ b/exotic/exotic.py @@ -526,7 +526,7 @@ def radec_hours_to_degree(ra, dec): def standard_filter(ld, filter_): if not filter_['filter']: - ld.standard_list() + LimbDarkening.standard_list() while True: if not filter_['filter']: @@ -1856,7 +1856,7 @@ def main(): ld_obj = LimbDarkening(pDict) nonlinear_ld(ld_obj, exotic_infoDict) - exotic_infoDict['filter'] = ld_obj.filter_type + exotic_infoDict['filter'] = ld_obj.filter_name exotic_infoDict['filter_desc'] = ld_obj.filter_desc exotic_infoDict['wl_min'] = ld_obj.wl_min exotic_infoDict['wl_max'] = ld_obj.wl_max From 1f90f0ede650e7d49fd767c2328e29dd88a78411 Mon Sep 17 00:00:00 2001 From: jpl-jengelke Date: Sun, 6 Aug 2023 18:18:15 -0700 Subject: [PATCH 7/8] Closes #1210: Implement fuller validation techniques on filter specifications. OOP design changes to support class and static methods. Add new method to check wavelengths. ... --- exotic/api/ld.py | 64 ++++++++++++++++++++++++++++++++++++++++++++++-- 1 file changed, 62 insertions(+), 2 deletions(-) diff --git a/exotic/api/ld.py b/exotic/api/ld.py index b396c9ef..237c76b8 100644 --- a/exotic/api/ld.py +++ b/exotic/api/ld.py @@ -60,7 +60,26 @@ def standard_list(): f"\n\t-FWHM: ({val['fwhm'][0]}-{val['fwhm'][1]}) nm") return - def check_standard(self, filter_: dict = None): + def check_standard(self, filter_: dict = None) -> bool: + """ + Utility method to detect filter from full or partial values and to inject + results into LimbDarkening object. Detection algorithm is loose and + finds alias from unique partial matches on the `filter` key. The input + dict must contain one or more of the required keys to facilitate + matching. + Order of matching operations proceeds as follows: + 1 - 'filter': loose match against name (irrespective of spacing, caps, punct) + 2 - Both supplied min/max wavelength values: precise match + 3 - 'filter': if it precisely matches any filter key (same as 'desc') + 4 - Abbrev. name if it is in the 'filter' or 'name' fields: precise match + 5 - Any portion of filter name that is unique to one filter + @param filter_: A dictionary containing any combination of ('filter', 'name', + 'wl_min', 'wl_max') keys implying a certain visibility filter used + during telescope observations. + @type filter_: dict + @return: True if filter parameters match known aliases and false if any do not. + @rtype: bool + """ filter_alias = filter_matcher = None try: if not isinstance(filter_, dict): # set sane failure @@ -116,6 +135,45 @@ def check_standard(self, filter_: dict = None): log.error(f"Filter matching failed -- {be}") return filter_alias is not None + @staticmethod + def check_fwhm(filter_: dict = None) -> bool: + """ + Validates wavelength values in a filter dict to verify they are in the correct + order (e.g. min, max properly ordered) and are not out of bounds, between + numeric values of 300nm and 4000nm. This mutates min/max value strings to + ensure they end with '.0' and are ordered. NOTE: ASSUMES NANOMETER INPUTS. + @param filter_: A dictionary containing full-width, half-maximum (fwhm) + wavelength values ('wl_min' and 'wl_max') keys implying a certain visibility + filter used during telescope observations. + @type filter_: dict + @return: True if wavelength parameters meet requirements and false if they do not. + Note that the input dict is modified to correct malformed values, including + popping detected bad keys. + @rtype: bool + """ + fwhm_tuple = None + try: + if not isinstance(filter_, dict): # set sane failure + log.error("Filter not defined according to spec (dict required) -- parsing failure.") + return False + for k in ('wl_min', 'wl_max'): # clean inputs + filter_[k] = filter_.get(k) + filter_[k] = ''.join(str(filter_[k]).strip().split()) if filter_[k] else filter_[k] + if not 200. <= float(filter_[k]) <= 4000.: # also fails if nan + raise ValueError(f"FWHM '{k}' is outside of bounds (200., 4000.). ...") + else: # add .0 to end of str to aid literal matching + if not filter_[k].count('.'): + filter_[k] += '.0' + if float(filter_['wl_min']) > float(filter_['wl_max']): # reorder if min > max + fwhm_tuple = (filter_['wl_max'], filter_['wl_min']) + filter_['wl_min'] = fwhm_tuple[0] + filter_['wl_max'] = fwhm_tuple[1] + else: + fwhm_tuple = (filter_['wl_min'], filter_['wl_max']) + except BaseException as be: + log.error(f"FWHM matching failed [{filter_.get('wl_min')},{filter_.get('wl_max')}]-- {be}") + return fwhm_tuple is not None + def set_filter(self, filter_name, filter_desc, wl_min, wl_max): self.filter_name = filter_name self.filter_desc = filter_desc @@ -204,7 +262,7 @@ def test_ld(ld_obj_, filter_): # Test given only FWHM filter_info3 = { 'filter': None, - 'wl_min': "350.0", + 'wl_min': "350", 'wl_max': "850.0", 'u0': {"value": None, "uncertainty": None}, 'u1': {"value": None, "uncertainty": None}, @@ -238,6 +296,8 @@ def test_ld(ld_obj_, filter_): ld_obj.check_standard(filter_info5) + LimbDarkening.check_fwhm(filter_info5) + LimbDarkening.check_fwhm(filter_info3) test_ld(ld_obj, filter_info1) # test_ld(ld_obj, filter_info2) # test_ld(ld_obj, filter_info3) From 6ac1f60f0590220c1d87e68169bee57885033b2c Mon Sep 17 00:00:00 2001 From: jpl-jengelke Date: Sun, 6 Aug 2023 18:23:33 -0700 Subject: [PATCH 8/8] Close #1210: Rev version for next release. ... --- exotic/version.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exotic/version.py b/exotic/version.py index 7f5601d9..573cf70b 100644 --- a/exotic/version.py +++ b/exotic/version.py @@ -1 +1 @@ -__version__ = '3.1.0' +__version__ = '3.2.0'