From 299f35e8d64a51a1d16879d93a34d50982fa382f Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Sat, 29 May 2021 10:35:18 -0400 Subject: [PATCH 01/20] PEP8 fixes for contam_visibility and atmopsretrievals --- exoctk/atmospheric_retrievals/examples.py | 44 +- exoctk/contam_visibility/astro_funcx.py | 60 +-- .../contam_visibility/contamination_figure.py | 405 ++++++++-------- exoctk/contam_visibility/ephemeris_old2x.py | 436 +++++++++--------- .../contam_visibility/f_visibilityPeriods.py | 288 ++++++------ exoctk/contam_visibility/field_simulator.py | 1 + 6 files changed, 605 insertions(+), 629 deletions(-) diff --git a/exoctk/atmospheric_retrievals/examples.py b/exoctk/atmospheric_retrievals/examples.py index 14450a60..c7e7bbe1 100644 --- a/exoctk/atmospheric_retrievals/examples.py +++ b/exoctk/atmospheric_retrievals/examples.py @@ -117,20 +117,20 @@ def example(method): # Fit for the stellar radius and planetary mass using Gaussian priors. This # is a way to account for the uncertainties in the published values - pw.fit_info.add_gaussian_fit_param('Rs', 0.02*R_sun) - pw.fit_info.add_gaussian_fit_param('Mp', 0.04*M_jup) + pw.fit_info.add_gaussian_fit_param('Rs', 0.02 * R_sun) + pw.fit_info.add_gaussian_fit_param('Mp', 0.04 * M_jup) # Fit for other parameters using uniform priors - pw.fit_info.add_uniform_fit_param('Rp', 0.9*(1.4 * R_jup), 1.1*(1.4 * R_jup)) - pw.fit_info.add_uniform_fit_param('T', 0.5*1200, 1.5*1200) + pw.fit_info.add_uniform_fit_param('Rp', 0.9 * (1.4 * R_jup), 1.1 * (1.4 * R_jup)) + pw.fit_info.add_uniform_fit_param('T', 0.5 * 1200, 1.5 * 1200) pw.fit_info.add_uniform_fit_param("log_scatt_factor", 0, 1) pw.fit_info.add_uniform_fit_param("logZ", -1, 3) pw.fit_info.add_uniform_fit_param("log_cloudtop_P", -0.99, 5) pw.fit_info.add_uniform_fit_param("error_multiple", 0.5, 5) # Define bins, depths, and errors - pw.wavelengths = 1e-6*np.array([1.119, 1.1387]) - pw.bins = [[w-0.0095e-6, w+0.0095e-6] for w in pw.wavelengths] + pw.wavelengths = 1e-6 * np.array([1.119, 1.1387]) + pw.bins = [[w - 0.0095e-6, w + 0.0095e-6] for w in pw.wavelengths] pw.depths = 1e-6 * np.array([14512.7, 14546.5]) pw.errors = 1e-6 * np.array([50.6, 35.5]) @@ -176,20 +176,20 @@ def example_aws_short(method): # Fit for the stellar radius and planetary mass using Gaussian priors. This # is a way to account for the uncertainties in the published values - pw.fit_info.add_gaussian_fit_param('Rs', 0.02*R_sun) - pw.fit_info.add_gaussian_fit_param('Mp', 0.04*M_jup) + pw.fit_info.add_gaussian_fit_param('Rs', 0.02 * R_sun) + pw.fit_info.add_gaussian_fit_param('Mp', 0.04 * M_jup) # Fit for other parameters using uniform priors - pw.fit_info.add_uniform_fit_param('Rp', 0.9*(1.4 * R_jup), 1.1*(1.4 * R_jup)) - pw.fit_info.add_uniform_fit_param('T', 0.5*1200, 1.5*1200) + pw.fit_info.add_uniform_fit_param('Rp', 0.9 * (1.4 * R_jup), 1.1 * (1.4 * R_jup)) + pw.fit_info.add_uniform_fit_param('T', 0.5 * 1200, 1.5 * 1200) pw.fit_info.add_uniform_fit_param("log_scatt_factor", 0, 1) pw.fit_info.add_uniform_fit_param("logZ", -1, 3) pw.fit_info.add_uniform_fit_param("log_cloudtop_P", -0.99, 5) pw.fit_info.add_uniform_fit_param("error_multiple", 0.5, 5) # Define bins, depths, and errors - pw.wavelengths = 1e-6*np.array([1.119, 1.1387]) - pw.bins = [[w-0.0095e-6, w+0.0095e-6] for w in pw.wavelengths] + pw.wavelengths = 1e-6 * np.array([1.119, 1.1387]) + pw.bins = [[w - 0.0095e-6, w + 0.0095e-6] for w in pw.wavelengths] pw.depths = 1e-6 * np.array([14512.7, 14546.5]) pw.errors = 1e-6 * np.array([50.6, 35.5]) @@ -228,19 +228,19 @@ def example_aws_long(method): pw.set_parameters(params) if method == 'multinest': - pw.fit_info.add_gaussian_fit_param('Rs', 0.02*R_sun) - pw.fit_info.add_gaussian_fit_param('Mp', 0.04*M_jup) - pw.fit_info.add_uniform_fit_param('Rp', 0.9*(1.39 * R_jup), 1.1*(1.39 * R_jup)) + pw.fit_info.add_gaussian_fit_param('Rs', 0.02 * R_sun) + pw.fit_info.add_gaussian_fit_param('Mp', 0.04 * M_jup) + pw.fit_info.add_uniform_fit_param('Rp', 0.9 * (1.39 * R_jup), 1.1 * (1.39 * R_jup)) pw.fit_info.add_uniform_fit_param('T', 300, 3000) pw.fit_info.add_uniform_fit_param("log_scatt_factor", 0, 2) pw.fit_info.add_uniform_fit_param("logZ", -1, 3) pw.fit_info.add_uniform_fit_param("log_cloudtop_P", -0.99, 7) pw.fit_info.add_uniform_fit_param("error_multiple", 0.5, 5) elif method == 'emcee': - pw.fit_info.add_gaussian_fit_param('Rs', 0.02*R_sun) - pw.fit_info.add_gaussian_fit_param('Mp', 0.04*M_jup) - pw.fit_info.add_uniform_fit_param('Rp', 0, np.inf, 0.9*(1.39 * R_jup), 1.1*(1.39 * R_jup)) - pw.fit_info.add_uniform_fit_param('T', 300, 3000, 0.5*1476.81, 1.5*1476.81) + pw.fit_info.add_gaussian_fit_param('Rs', 0.02 * R_sun) + pw.fit_info.add_gaussian_fit_param('Mp', 0.04 * M_jup) + pw.fit_info.add_uniform_fit_param('Rp', 0, np.inf, 0.9 * (1.39 * R_jup), 1.1 * (1.39 * R_jup)) + pw.fit_info.add_uniform_fit_param('T', 300, 3000, 0.5 * 1476.81, 1.5 * 1476.81) pw.fit_info.add_uniform_fit_param("log_scatt_factor", 0, 5, 0, 2) pw.fit_info.add_uniform_fit_param("logZ", -1, 3) pw.fit_info.add_uniform_fit_param("log_cloudtop_P", -0.99, 7) @@ -287,11 +287,11 @@ def get_example_data(object_name): df = pandas.read_csv(data_file, names=['wavelengths', 'bin_sizes', 'depths', 'errors']) # Remove and rows outside of wavelength range (3e-7 to 3e-5) - df = df.loc[(1e-6*df['wavelengths'] - 1e-6*df['bin_sizes'] >= 3e-7) & (1e-6*df['wavelengths'] + 1e-6*df['bin_sizes'] <= 3e-5)] + df = df.loc[(1e-6 * df['wavelengths'] - 1e-6 * df['bin_sizes'] >= 3e-7) & (1e-6 * df['wavelengths'] + 1e-6 * df['bin_sizes'] <= 3e-5)] # Parse the data - wavelengths = 1e-6*np.array(df['wavelengths']) - bin_sizes = 1e-6*np.array(df['bin_sizes']) + wavelengths = 1e-6 * np.array(df['wavelengths']) + bin_sizes = 1e-6 * np.array(df['bin_sizes']) depths = np.array(df['depths']) errors = np.array(df['errors']) diff --git a/exoctk/contam_visibility/astro_funcx.py b/exoctk/contam_visibility/astro_funcx.py index 0a66f423..366a34d8 100755 --- a/exoctk/contam_visibility/astro_funcx.py +++ b/exoctk/contam_visibility/astro_funcx.py @@ -14,36 +14,6 @@ epsilon = 23.43929 * D2R # obliquity of the ecliptic J2000 -def pa(tgt_c1, tgt_c2, obj_c1, obj_c2): - """Calculates position angle of object at tgt position. - - Parameters - ---------- - tgt_c1: float - The RA of the target. - tgt_c2: float - The Dec of the target. - obj_c1: float - The RA of the reference. - obj_c2: float - The Dec of the reference. - - Returns - ------- - float - The position angle. - """ - y = cos(obj_c2) * sin(obj_c1 - tgt_c1) - c = cos(obj_c2) * sin(tgt_c2) * cos(obj_c1 - tgt_c1) - x = sin(obj_c2) * cos(tgt_c2) - c - p = atan2(y, x) - if p < 0.: - p += PI2 - if p >= PI2: - p -= PI2 - return p - - def delta_pa_no_roll(pos1_c1, pos1_c2, pos2_c1, pos2_c2): """Calculates the change in position angle between two positions with no roll about V1 @@ -124,6 +94,36 @@ def JWST_same_ori(tgt0_c1, tgt0_c2, p0, tgt_c1, tgt_c2): return pp +def pa(tgt_c1, tgt_c2, obj_c1, obj_c2): + """Calculates position angle of object at tgt position. + + Parameters + ---------- + tgt_c1: float + The RA of the target. + tgt_c2: float + The Dec of the target. + obj_c1: float + The RA of the reference. + obj_c2: float + The Dec of the reference. + + Returns + ------- + float + The position angle. + """ + y = cos(obj_c2) * sin(obj_c1 - tgt_c1) + c = cos(obj_c2) * sin(tgt_c2) * cos(obj_c1 - tgt_c1) + x = sin(obj_c2) * cos(tgt_c2) - c + p = atan2(y, x) + if p < 0.: + p += PI2 + if p >= PI2: + p -= PI2 + return p + + def unit_limit(x): """ Forces value to be in [-1, 1] diff --git a/exoctk/contam_visibility/contamination_figure.py b/exoctk/contam_visibility/contamination_figure.py index d7542561..d213acb7 100755 --- a/exoctk/contam_visibility/contamination_figure.py +++ b/exoctk/contam_visibility/contamination_figure.py @@ -1,13 +1,11 @@ import os -import pkg_resources import sys from astropy.io import fits from bokeh.layouts import gridplot -from bokeh.plotting import figure -from bokeh.models import Range1d, LinearColorMapper, Label -from bokeh.models.widgets import Panel, Tabs +from bokeh.models import Range1d, LinearColorMapper from bokeh.palettes import PuBu +from bokeh.plotting import figure import numpy as np from . import visibilityPA as vpa @@ -15,7 +13,8 @@ EXOCTK_DATA = os.environ.get('EXOCTK_DATA') if not EXOCTK_DATA: print( - 'WARNING: The $EXOCTK_DATA environment variable is not set. Contamination overlap will not work. Please set the ' + 'WARNING: The $EXOCTK_DATA environment variable is not set. ' + 'Contamination overlap will not work. Please set the ' 'value of this variable to point to the location of the exoctk_data ' 'download folder. Users may retreive this folder by clicking the ' '"ExoCTK Data Download" button on the ExoCTK website, or by using ' @@ -24,199 +23,11 @@ else: TRACES_PATH = os.path.join(EXOCTK_DATA, 'exoctk_contam', 'traces') -disp_nircam = 0.001 # microns -lam0_nircam322w2 = 2.369 -lam1_nircam322w2 = 4.417 -lam0_nircam444w = 3.063 -lam1_nircam444w = 5.111 - - -def nirissContam(cube, paRange=[0, 360]): - """ Generates the contamination figure that will be plotted on the website - for NIRISS SOSS. - """ - # Get data from FITS file - if isinstance(cube, str): - hdu = fits.open(cubeName) - cube = hdu[0].data - hdu.close() - - # Pull out the target trace and cube of neighbor traces - trace1 = cube[0, :, :] - trace2 = cube[1, :, :] - cube = cube[2:, :, :] - - plotPAmin, plotPAmax = paRange - - # Start calculations - if not TRACES_PATH: - return None - lam_file = os.path.join(TRACES_PATH, 'NIRISS', 'lambda_order1-2.txt') - ypix, lamO1, lamO2 = np.loadtxt(lam_file, unpack=True) - - nPA = cube.shape[0] - rows = cube.shape[1] - cols = cube.shape[2] - print('cols ', cols) - dPA = 360 // nPA - PA = np.arange(nPA) * dPA - - contamO1 = np.zeros([rows, nPA]) - contamO2 = np.zeros([rows, nPA]) - - low_lim_col = 20 - high_lim_col = 41 - - for row in np.arange(rows): - # Contamination for order 1 of target trace - i = np.argmax(trace1[row, :]) - tr = trace1[row, i - low_lim_col:i + high_lim_col] - w = tr / np.sum(tr**2) - ww = np.tile(w, nPA).reshape([nPA, tr.size]) - contamO1[row, :] = np.sum( - cube[:, row, i - low_lim_col:i + high_lim_col] * ww, axis=1) - - # Contamination for order 2 of target trace - if lamO2[row] < 0.6: - continue - i = np.argmax(trace2[row, :]) - tr = trace2[row, i - 20:i + 41] - w = tr / np.sum(tr**2) - ww = np.tile(w, nPA).reshape([nPA, tr.size]) - contamO2[row, :] = np.sum(cube[:, row, i - 20:i + 41] * ww, axis=1) - - return contamO1, contamO2 - - -def nircamContam(cube, instrument, paRange=[0, 360]): - """ Generates the contamination figure that will be plotted on the website - for NIRCam Grism Time Series mode. - - PARAMETERS - ---------- - cube : arr or str - A 3D array of the simulated field at every Aperture Position Angle (APA). - The shape of the cube is (361, subY, subX). - or - The name of an HDU .fits file sthat has the cube. - - instrument : str - The name of the instrument + what filter is being used. For NIRCam the - options are: 'NIRCam F322W2', 'NIRCam F444W' - - RETURNS - ------- - bokeh plot - """ - # Get data from FITS file - if isinstance(cube, str): - hdu = fits.open(cubeName) - cube = hdu[0].data - hdu.close() - - # Pull out the target trace and cube of neighbor traces - targ = cube[0, :, :] # target star order 1 trace - # neighbor star order 1 and 2 traces in all the angles - cube = cube[1:, :, :] - - # Remove background values < 1 as it can blow up contamination - targ = np.where(targ < 1, 0, targ) - - PAmin, PAmax = paRange[0], paRange[1] - PArange = np.arange(PAmin, PAmax, 1) - - nPA, rows, cols = cube.shape[0], cube.shape[1], cube.shape[2] - - contamO1 = np.zeros([nPA, cols]) - - # the width of the trace (in Y-direction for NIRCam GTS) - peak = targ.max() - low_lim_row = np.where(targ > 0.0001 * peak)[0].min() - high_lim_row = np.where(targ > 0.0001 * peak)[0].max() - - # the length of the trace (in X-direction for NIRCam GTS) - targ_trace_start = np.where(targ > 0.0001 * peak)[1].min() - targ_trace_stop = np.where(targ > 0.0001 * peak)[1].max() - - # Begin contam calculation at each channel (column) X - for X in np.arange(cols): - if (X < targ_trace_start) or (X > targ_trace_stop): - continue - - peakY = np.argmax(targ[:, X]) - TOP, BOT = peakY + high_lim_row, peakY - low_lim_row - - tr = targ[BOT:TOP, X] - - # calculate weights - wt = tr / np.sum(tr**2) - ww = np.tile(wt, nPA).reshape([nPA, tr.size]) - - contamO1[:, X] = np.sum(cube[:, BOT:TOP, X] * ww, axis=1) - - contamO1 = contamO1[:, targ_trace_start:targ_trace_stop] - return contamO1 - - -def miriContam(cube, paRange=[0, 360]): - """ Generates the contamination figure that will be plotted on the website - for MIRI LRS. - """ - # Get data from FITS file - if isinstance(cube, str): - hdu = fits.open(cubeName) - cube = hdu[0].data - hdu.close() - - # Pull out the target trace and cube of neighbor traces - targ = cube[0, :, :] # target star order 1 trace - # neighbor star order 1 and 2 traces in all the angles - cube = cube[1:, :, :] - - # Remove background values < 1 as it can blow up contamination - targ = np.where(targ < 1, 0, targ) - - PAmin, PAmax = paRange[0], paRange[1] - PArange = np.arange(PAmin, PAmax, 1) - - nPA, rows, cols = cube.shape[0], cube.shape[1], cube.shape[2] - - contamO1 = np.zeros([rows, nPA]) - - # the width of the trace (in Y-direction for NIRCam GTS) - peak = targ.max() - - low_lim_col = np.where(targ > 0.0001 * peak)[1].min() - high_lim_col = np.where(targ > 0.0001 * peak)[1].max() - - # the length of the trace (in X-direction for NIRCam GTS) - targ_trace_start = np.where(targ > 0.0001 * peak)[0].min() - targ_trace_stop = np.where(targ > 0.0001 * peak)[0].max() - # Begin contam calculation at each channel (row) Y - for Y in np.arange(rows): - if (Y < targ_trace_start) or (Y > targ_trace_stop): - continue - - peakX = np.argmax(targ[Y, :]) - LEFT, RIGHT = peakX - low_lim_col, peakX + high_lim_col - - tr = targ[Y, LEFT:RIGHT] - - # calculate weights - wt = tr / np.sum(tr**2) - ww = np.tile(wt, nPA).reshape([nPA, tr.size]) - - contamO1[Y, :] = np.sum(cube[:, Y, LEFT:RIGHT] * wt, - where=~np.isnan(cube[:, Y, LEFT:RIGHT] * wt), - axis=1) - - #target = np.sum(cube[0, Y, LEFT:RIGHT], axis=0) - - # contamO1[Y, :] = np.sum(cube[:, Y, LEFT:RIGHT]*ww, - # where=~np.isnan(cube[:, Y, LEFT:RIGHT]), - # axis=1)#/target - contamO1 = contamO1[targ_trace_start:targ_trace_stop, :] - return contamO1 +DISP_NIRCAM = 0.001 # microns +LAM0_NIRCAM322W2 = 2.369 +LAM1_NIRCAM322W2 = 4.417 +LAM0_NIRCAM444W = 3.063 +LAM1_NIRCAM444W = 5.111 def contam(cube, instrument, targetName='noName', paRange=[0, 360], @@ -225,7 +36,7 @@ def contam(cube, instrument, targetName='noName', paRange=[0, 360], lam_file = os.path.join(TRACES_PATH, 'NIRISS', 'lambda_order1-2.txt') ypix, lamO1, lamO2 = np.loadtxt(lam_file, unpack=True) - nPA, rows, cols = cube.shape[0], cube.shape[1], cube.shape[2] + rows, cols = cube.shape[1], cube.shape[2] PAmin, PAmax = paRange[0], paRange[1] PA = np.arange(PAmin, PAmax, 1) @@ -240,11 +51,6 @@ def contam(cube, instrument, targetName='noName', paRange=[0, 360], TOOLS = 'pan, box_zoom, crosshair, reset, hover' - y = np.array([0., 0.]) - y1 = 0.07 - y2 = 0.12 - y3 = 0.17 - y4 = 0.23 bad_PA_color = '#dddddd' bad_PA_alpha = 0.7 dPA = 1 @@ -256,11 +62,11 @@ def contam(cube, instrument, targetName='noName', paRange=[0, 360], xlim0 = lamO1.min() xlim1 = lamO1.max() elif instrument == 'NIRCam F322W2': - xlim0 = lam0_nircam322w2 - xlim1 = lam1_nircam322w2 + xlim0 = LAM0_NIRCAM322W2 + xlim1 = LAM1_NIRCAM322W2 elif instrument == 'NIRCam F444W': - xlim0 = lam0_nircam444w - xlim1 = lam1_nircam444w + xlim0 = LAM0_NIRCAM444W + xlim1 = LAM1_NIRCAM444W elif instrument == 'MIRI': xlim0 = 5 xlim1 = 12 @@ -289,11 +95,6 @@ def contam(cube, instrument, targetName='noName', paRange=[0, 360], # do w the choppiness # of o1 in all instruments - X = xlim1 if (instrument == 'MIRI') or ( - instrument == 'NIRCam F322W2') else xlim0 - DW = xlim0 - xlim1 if (instrument == 'MIRI') or (instrument == - 'NIRCam F322W2') else xlim1 - xlim0 - # Begin plotting ~~~~~~~~~~~~~~~~~~~~~~~~ s2.image([fig_data], x=xlim0, y=ylim0, dw=xlim1 - xlim0, dh=ylim1 - ylim0, @@ -324,7 +125,7 @@ def contam(cube, instrument, targetName='noName', paRange=[0, 360], color=bad_PA_color, alpha=bad_PA_alpha) # Line plot - #ax = 1 if 'NIRCam' in instrument else 0 + # ax = 1 if 'NIRCam' in instrument else 0 channels = cols if 'NIRCam' in instrument else rows s3 = figure(tools=TOOLS, width=150, height=500, x_range=Range1d(0, 100), y_range=s2.y_range, title=None) @@ -370,7 +171,7 @@ def contam(cube, instrument, targetName='noName', paRange=[0, 360], dw=xlim1 - xlim0, dh=ylim1 - ylim0, color_mapper=color_mapper) - #s5.yaxis.major_label_text_font_size = '0pt' + # s5.yaxis.major_label_text_font_size = '0pt' s5.xaxis.axis_label = 'Wavelength (um)' s5.yaxis.axis_label = 'Aperture Position Angle (degrees)' @@ -440,6 +241,180 @@ def contam(cube, instrument, targetName='noName', paRange=[0, 360], return fig # , contamO1 +def miriContam(cube, paRange=[0, 360]): + """ Generates the contamination figure that will be plotted on the website + for MIRI LRS. + """ + # Get data from FITS file + if isinstance(cube, str): + hdu = fits.open(cubeName) + cube = hdu[0].data + hdu.close() + + # Pull out the target trace and cube of neighbor traces + targ = cube[0, :, :] # target star order 1 trace + # neighbor star order 1 and 2 traces in all the angles + cube = cube[1:, :, :] + + # Remove background values < 1 as it can blow up contamination + targ = np.where(targ < 1, 0, targ) + nPA, rows = cube.shape[0], cube.shape[1] + contamO1 = np.zeros([rows, nPA]) + + # the width of the trace (in Y-direction for NIRCam GTS) + peak = targ.max() + + low_lim_col = np.where(targ > 0.0001 * peak)[1].min() + high_lim_col = np.where(targ > 0.0001 * peak)[1].max() + + # the length of the trace (in X-direction for NIRCam GTS) + targ_trace_start = np.where(targ > 0.0001 * peak)[0].min() + targ_trace_stop = np.where(targ > 0.0001 * peak)[0].max() + # Begin contam calculation at each channel (row) Y + for Y in np.arange(rows): + if (Y < targ_trace_start) or (Y > targ_trace_stop): + continue + + peakX = np.argmax(targ[Y, :]) + LEFT, RIGHT = peakX - low_lim_col, peakX + high_lim_col + + tr = targ[Y, LEFT:RIGHT] + + # calculate weights + wt = tr / np.sum(tr**2) + contamO1[Y, :] = np.sum(cube[:, Y, LEFT:RIGHT] * wt, + where=~np.isnan(cube[:, Y, LEFT:RIGHT] * wt), + axis=1) + + # target = np.sum(cube[0, Y, LEFT:RIGHT], axis=0) + + # contamO1[Y, :] = np.sum(cube[:, Y, LEFT:RIGHT]*ww, + # where=~np.isnan(cube[:, Y, LEFT:RIGHT]), + # axis=1)#/target + contamO1 = contamO1[targ_trace_start:targ_trace_stop, :] + return contamO1 + + +def nircamContam(cube, instrument, paRange=[0, 360]): + """ Generates the contamination figure that will be plotted on the website + for NIRCam Grism Time Series mode. + + PARAMETERS + ---------- + cube : arr or str + A 3D array of the simulated field at every Aperture Position Angle (APA). + The shape of the cube is (361, subY, subX). + or + The name of an HDU .fits file sthat has the cube. + + instrument : str + The name of the instrument + what filter is being used. For NIRCam the + options are: 'NIRCam F322W2', 'NIRCam F444W' + + RETURNS + ------- + bokeh plot + """ + # Get data from FITS file + if isinstance(cube, str): + hdu = fits.open(cubeName) + cube = hdu[0].data + hdu.close() + + # Pull out the target trace and cube of neighbor traces + targ = cube[0, :, :] # target star order 1 trace + # neighbor star order 1 and 2 traces in all the angles + cube = cube[1:, :, :] + + # Remove background values < 1 as it can blow up contamination + targ = np.where(targ < 1, 0, targ) + nPA, cols = cube.shape[0], cube.shape[2] + contamO1 = np.zeros([nPA, cols]) + + # the width of the trace (in Y-direction for NIRCam GTS) + peak = targ.max() + low_lim_row = np.where(targ > 0.0001 * peak)[0].min() + high_lim_row = np.where(targ > 0.0001 * peak)[0].max() + + # the length of the trace (in X-direction for NIRCam GTS) + targ_trace_start = np.where(targ > 0.0001 * peak)[1].min() + targ_trace_stop = np.where(targ > 0.0001 * peak)[1].max() + + # Begin contam calculation at each channel (column) X + for X in np.arange(cols): + if (X < targ_trace_start) or (X > targ_trace_stop): + continue + + peakY = np.argmax(targ[:, X]) + TOP, BOT = peakY + high_lim_row, peakY - low_lim_row + + tr = targ[BOT:TOP, X] + + # calculate weights + wt = tr / np.sum(tr**2) + ww = np.tile(wt, nPA).reshape([nPA, tr.size]) + + contamO1[:, X] = np.sum(cube[:, BOT:TOP, X] * ww, axis=1) + + contamO1 = contamO1[:, targ_trace_start:targ_trace_stop] + return contamO1 + + +def nirissContam(cube, paRange=[0, 360]): + """ Generates the contamination figure that will be plotted on the website + for NIRISS SOSS. + """ + # Get data from FITS file + if isinstance(cube, str): + hdu = fits.open(cubeName) + cube = hdu[0].data + hdu.close() + + # Pull out the target trace and cube of neighbor traces + trace1 = cube[0, :, :] + trace2 = cube[1, :, :] + cube = cube[2:, :, :] + + plotPAmin, plotPAmax = paRange + + # Start calculations + if not TRACES_PATH: + return None + lam_file = os.path.join(TRACES_PATH, 'NIRISS', 'lambda_order1-2.txt') + ypix, lamO1, lamO2 = np.loadtxt(lam_file, unpack=True) + + nPA = cube.shape[0] + rows = cube.shape[1] + cols = cube.shape[2] + print('cols ', cols) + + contamO1 = np.zeros([rows, nPA]) + contamO2 = np.zeros([rows, nPA]) + + low_lim_col = 20 + high_lim_col = 41 + + for row in np.arange(rows): + # Contamination for order 1 of target trace + i = np.argmax(trace1[row, :]) + tr = trace1[row, i - low_lim_col:i + high_lim_col] + w = tr / np.sum(tr**2) + ww = np.tile(w, nPA).reshape([nPA, tr.size]) + contamO1[row, :] = np.sum( + cube[:, row, i - low_lim_col:i + high_lim_col] * ww, axis=1) + + # Contamination for order 2 of target trace + if lamO2[row] < 0.6: + continue + i = np.argmax(trace2[row, :]) + tr = trace2[row, i - 20:i + 41] + w = tr / np.sum(tr**2) + ww = np.tile(w, nPA).reshape([nPA, tr.size]) + contamO2[row, :] = np.sum(cube[:, row, i - 20:i + 41] * ww, axis=1) + + return contamO1, contamO2 + + if __name__ == "__main__": # arguments RA & DEC, conversion to radians argv = sys.argv diff --git a/exoctk/contam_visibility/ephemeris_old2x.py b/exoctk/contam_visibility/ephemeris_old2x.py index 34646997..cb84de17 100755 --- a/exoctk/contam_visibility/ephemeris_old2x.py +++ b/exoctk/contam_visibility/ephemeris_old2x.py @@ -5,8 +5,8 @@ import time from . import astro_funcx as astro_func -from . import quaternionx as qx from . import time_extensionsx as time2 +from . import quaternionx as qx D2R = math.pi / 180. # degrees to radians R2D = 180. / math.pi # radians to degrees @@ -83,168 +83,106 @@ def __init__(self, ephem_file, cnvrt=False): self.amax = adate fin.close() - def report_ephemeris(self, limit=100000, pathname=None): - """Prints a formatted report of the ephemeris. - - Parameters - ---------- - limit: int, optional - The number of records to report. - pathname: str, optional - The path to a file to hold the report. - """ - num_to_report = min(limit, len(self.datelist)) - - if (pathname): - dest = open(pathname, 'w') - print(('# Generated %s\n' % (time.ctime())), file=dest) - else: - dest = sys.stdout # defaults to standard output - - print(('%17s %14s %14s %14s\n' % - ('DATE ', 'X (KM) ', 'Y (KM) ', 'Z (KM) ')), file=dest) - - for num in range(num_to_report): - date = self.datelist[num] - x = self.xlist[num] - y = self.ylist[num] - z = self.zlist[num] - - fmt = time2.display_date(date), x, y, z - print(('%17s %14.3f %14.3f %14.3f' % fmt), file=dest) - - if (pathname): - dest.close() # Clean up - - def pos(self, adate): - """Computes the position of the telescope at a given date using the - grid of positions of the ephemeris as a starting point and - applying a linear interpolation between the ephemeris grid points - - Parameters - ---------- - adate: datetime.datetime object - The date of the observation. - - Returns - ------- - qx.Vector object - The position of the telescope as a Vector object - """ - cal_days = adate - self.datelist[0] - index = int(cal_days) - if (index == len(self.datelist) - 1): - index = index - 1 - frac = cal_days - index - x = (self.xlist[index + 1] - self.xlist[index]) * \ - frac + self.xlist[index] - y = (self.ylist[index + 1] - self.ylist[index]) * \ - frac + self.ylist[index] - z = (self.zlist[index + 1] - self.zlist[index]) * \ - frac + self.zlist[index] - return qx.Vector(x, y, z) - - def Vsun_pos(self, adate): - """The vector of the sun at the given date - - Parameters - ---------- - adate: datetime - The date of the observation - - Returns - ------- - Vector - The position of the sun as a Vector object - """ - Vsun = -1. * self.pos(adate) - Vsun = Vsun / Vsun.length() - return Vsun - - def sun_pos(self, adate): - """The coordinates of the sun at the given date + def bisect_by_attitude(self, in_date, out_date, ngc_1, ngc_2, pa): + """Find the midpoint in time between in and out of FOR, + assumes only one "root" in interval Parameters ---------- - adate: datetime.datetime object - The date of the observation. + in_date: float + The in date of the observation. + out_date: float + The out date of the observation. + ngc_1: flaot + The RA of the reference in radians. + ngc_2: float + The Dec of the reference in radians. + pa: float + The position angle. Returns ------- - tuple - The coordinates of the sun. + float + The midpoint in time. """ - Vsun = -1. * self.pos(adate) - Vsun = Vsun / Vsun.length() - coord2 = math.asin(unit_limit(Vsun.z)) - coord1 = math.atan2(Vsun.y, Vsun.x) - if coord1 < 0.: - coord1 += PI2 - return (coord1, coord2) + icount = 0 + delta_days = 200. + mid_date = (in_date + out_date) / 2. + # print "bisect >", in_date, out_date, abs(in_date-out_date ) + while delta_days > 0.000001: + if self.is_valid(mid_date, ngc_1, ngc_2, pa): + in_date = mid_date + else: + out_date = mid_date + mid_date = (in_date + out_date) / 2. + delta_days = abs(in_date - out_date) / 2. + # print "UU", mid_date + icount = icount + 1 + # print " bisected >", icount + return mid_date - def normal_pa(self, adate, tgt_c1, tgt_c2): - """Calculate the V3 position + def bisect_by_FOR(self, in_date, out_date, ngc_1, ngc_2): + """Find the midpoint in time between in and out of FOR, + assumes only one "root" in interval Parameters ---------- - adate: datetime.datetime object - The date of the observation. - tgt_c1: float - The RA in radians. - tgt_c2: float - The Dec in radians. + in_date: float + The in date of the observation. + out_date: float + The out date of the observation. + ngc_1: flaot + The RA of the reference in radians. + ngc_2: float + The Dec of the reference in radians. Returns ------- float - The V3 position. + The midpoint in time. """ - (sun_c1, sun_c2) = self.sun_pos(adate) - sun_pa = astro_func.pa(tgt_c1, tgt_c2, sun_c1, sun_c2) - V3_pa = sun_pa + math.pi # We want -V3 pointed towards sun. - if V3_pa < 0.: - V3_pa += PI2 - if V3_pa >= PI2: - V3_pa -= PI2 - return V3_pa + delta_days = 200. + mid_date = (in_date + out_date) / 2. + while delta_days > 0.000001: + (sun_1, sun_2) = self.sun_pos(mid_date) + d = astro_func.dist(ngc_1, ngc_2, sun_1, sun_2) + if (d > MAX_SUN_ANGLE or d < MIN_SUN_ANGLE): + out_date = mid_date + else: + in_date = mid_date + mid_date = (in_date + out_date) / 2. + delta_days = abs(in_date - out_date) / 2. + # print "UU", mid_date + # ensure returned date always in FOR + if in_date > out_date: + mid_date = mid_date + 0.000001 + else: + mid_date = mid_date - 0.000001 + return mid_date - def long_term_attitude(self, date): - """Defines a long-term safe attitude as of a given date. + def in_FOR(self, date, ngc_1, ngc_2): + """Test if in the FOR Parameters ---------- date: float - The date of computation, as an mjd. + The date of the observation. + ngc_1: flaot + The RA of the reference in radians. + ngc_2: float + The Dec of the reference in radians. Returns ------- - Attitude - The Attitude object at the given date. + bool + Is it in the FOR. """ - # Retrieve Sun's position and transform to ecliptic coordinates. - (sun_ra, sun_dec) = self.sun_pos(date) # RA range 0-PI2 - vSun = qx.CelestialVector(sun_ra, sun_dec, degrees=False) - vSun = vSun.transform_frame('ec') - - # Now subtract the minimum Sun angle plus a pad from the - # ecliptic longitude. - # Sun angle steadily decreases as the Earth (and JWST with it) - # revolve counterclockwise, so this should maximize the duration - # when the attitude is within the FOR. Set the latitude to - # 0 (ecliptic). - # Normalize to 0-360 degrees, and convert back into equatorial - # coordinates. - # Then set the normal PA, which should be valid at the ecliptic for - # the duration of the visibility window. - longitude = vSun.ra - MIN_SUN_ANGLE - SUN_ANGLE_PAD - - if (longitude < 0): - longitude = longitude + PI2 - - vec1 = qx.CelestialVector(longitude, 0.0, frame='ec', degrees=False) - vec1 = vec1.transform_frame('eq') - pa = self.normal_pa(date, vec1.ra, vec1.dec) - return(qx.Attitude(vec1.ra, vec1.dec, pa, degrees=False)) + (sun_1, sun_2) = self.sun_pos(date) + d = astro_func.dist(ngc_1, ngc_2, sun_1, sun_2) + # sun pitch is always equal or greater than sun angle (V1 to sun) + if (d < MIN_SUN_ANGLE or d > MAX_SUN_ANGLE): + return False + return True def is_valid(self, date, ngc_1, ngc_2, V3pa): """Indicates whether an attitude is valid at a given date. @@ -286,106 +224,69 @@ def is_valid(self, date, ngc_1, ngc_2, V3pa): return True return False - def in_FOR(self, date, ngc_1, ngc_2): - """Test if in the FOR + def long_term_attitude(self, date): + """Defines a long-term safe attitude as of a given date. Parameters ---------- date: float - The date of the observation. - ngc_1: flaot - The RA of the reference in radians. - ngc_2: float - The Dec of the reference in radians. + The date of computation, as an mjd. Returns ------- - bool - Is it in the FOR. + Attitude + The Attitude object at the given date. """ - (sun_1, sun_2) = self.sun_pos(date) - d = astro_func.dist(ngc_1, ngc_2, sun_1, sun_2) - # sun pitch is always equal or greater than sun angle (V1 to sun) - if (d < MIN_SUN_ANGLE or d > MAX_SUN_ANGLE): - return False - return True + # Retrieve Sun's position and transform to ecliptic coordinates. + (sun_ra, sun_dec) = self.sun_pos(date) # RA range 0-PI2 + vSun = qx.CelestialVector(sun_ra, sun_dec, degrees=False) + vSun = vSun.transform_frame('ec') - def bisect_by_FOR(self, in_date, out_date, ngc_1, ngc_2): - """Find the midpoint in time between in and out of FOR, - assumes only one "root" in interval + # Now subtract the minimum Sun angle plus a pad from the + # ecliptic longitude. + # Sun angle steadily decreases as the Earth (and JWST with it) + # revolve counterclockwise, so this should maximize the duration + # when the attitude is within the FOR. Set the latitude to + # 0 (ecliptic). + # Normalize to 0-360 degrees, and convert back into equatorial + # coordinates. + # Then set the normal PA, which should be valid at the ecliptic for + # the duration of the visibility window. + longitude = vSun.ra - MIN_SUN_ANGLE - SUN_ANGLE_PAD - Parameters - ---------- - in_date: float - The in date of the observation. - out_date: float - The out date of the observation. - ngc_1: flaot - The RA of the reference in radians. - ngc_2: float - The Dec of the reference in radians. + if (longitude < 0): + longitude = longitude + PI2 - Returns - ------- - float - The midpoint in time. - """ - delta_days = 200. - mid_date = (in_date + out_date) / 2. - while delta_days > 0.000001: - (sun_1, sun_2) = self.sun_pos(mid_date) - d = astro_func.dist(ngc_1, ngc_2, sun_1, sun_2) - if (d > MAX_SUN_ANGLE or d < MIN_SUN_ANGLE): - out_date = mid_date - else: - in_date = mid_date - mid_date = (in_date + out_date) / 2. - delta_days = abs(in_date - out_date) / 2. - # print "UU", mid_date - # ensure returned date always in FOR - if in_date > out_date: - mid_date = mid_date + 0.000001 - else: - mid_date = mid_date - 0.000001 - return mid_date + vec1 = qx.CelestialVector(longitude, 0.0, frame='ec', degrees=False) + vec1 = vec1.transform_frame('eq') + pa = self.normal_pa(date, vec1.ra, vec1.dec) + return(qx.Attitude(vec1.ra, vec1.dec, pa, degrees=False)) - def bisect_by_attitude(self, in_date, out_date, ngc_1, ngc_2, pa): - """Find the midpoint in time between in and out of FOR, - assumes only one "root" in interval + def normal_pa(self, adate, tgt_c1, tgt_c2): + """Calculate the V3 position Parameters ---------- - in_date: float - The in date of the observation. - out_date: float - The out date of the observation. - ngc_1: flaot - The RA of the reference in radians. - ngc_2: float - The Dec of the reference in radians. - pa: float - The position angle. + adate: datetime.datetime object + The date of the observation. + tgt_c1: float + The RA in radians. + tgt_c2: float + The Dec in radians. Returns ------- float - The midpoint in time. + The V3 position. """ - icount = 0 - delta_days = 200. - mid_date = (in_date + out_date) / 2. - # print "bisect >", in_date, out_date, abs(in_date-out_date ) - while delta_days > 0.000001: - if self.is_valid(mid_date, ngc_1, ngc_2, pa): - in_date = mid_date - else: - out_date = mid_date - mid_date = (in_date + out_date) / 2. - delta_days = abs(in_date - out_date) / 2. - # print "UU", mid_date - icount = icount + 1 - # print " bisected >", icount - return mid_date + (sun_c1, sun_c2) = self.sun_pos(adate) + sun_pa = astro_func.pa(tgt_c1, tgt_c2, sun_c1, sun_c2) + V3_pa = sun_pa + math.pi # We want -V3 pointed towards sun. + if V3_pa < 0.: + V3_pa += PI2 + if V3_pa >= PI2: + V3_pa -= PI2 + return V3_pa def OP_window(self, adate, ngc_1, ngc_2, pa, mdelta, pdelta): """Attitude at adate must be valid, else returns (0, 0). @@ -428,6 +329,105 @@ def OP_window(self, adate, ngc_1, ngc_2, pa, mdelta, pdelta): OP_max = 0. return (OP_min, OP_max) + def pos(self, adate): + """Computes the position of the telescope at a given date using the + grid of positions of the ephemeris as a starting point and + applying a linear interpolation between the ephemeris grid points + + Parameters + ---------- + adate: datetime.datetime object + The date of the observation. + + Returns + ------- + qx.Vector object + The position of the telescope as a Vector object + """ + cal_days = adate - self.datelist[0] + index = int(cal_days) + if (index == len(self.datelist) - 1): + index = index - 1 + frac = cal_days - index + x = (self.xlist[index + 1] - self.xlist[index]) * \ + frac + self.xlist[index] + y = (self.ylist[index + 1] - self.ylist[index]) * \ + frac + self.ylist[index] + z = (self.zlist[index + 1] - self.zlist[index]) * \ + frac + self.zlist[index] + return qx.Vector(x, y, z) + + def report_ephemeris(self, limit=100000, pathname=None): + """Prints a formatted report of the ephemeris. + + Parameters + ---------- + limit: int, optional + The number of records to report. + pathname: str, optional + The path to a file to hold the report. + """ + num_to_report = min(limit, len(self.datelist)) + + if (pathname): + dest = open(pathname, 'w') + print(('# Generated %s\n' % (time.ctime())), file=dest) + else: + dest = sys.stdout # defaults to standard output + + print(('%17s %14s %14s %14s\n' % + ('DATE ', 'X (KM) ', 'Y (KM) ', 'Z (KM) ')), file=dest) + + for num in range(num_to_report): + date = self.datelist[num] + x = self.xlist[num] + y = self.ylist[num] + z = self.zlist[num] + + fmt = time2.display_date(date), x, y, z + print(('%17s %14.3f %14.3f %14.3f' % fmt), file=dest) + + if (pathname): + dest.close() # Clean up + + def sun_pos(self, adate): + """The coordinates of the sun at the given date + + Parameters + ---------- + adate: datetime.datetime object + The date of the observation. + + Returns + ------- + tuple + The coordinates of the sun. + """ + Vsun = -1. * self.pos(adate) + Vsun = Vsun / Vsun.length() + coord2 = math.asin(unit_limit(Vsun.z)) + coord1 = math.atan2(Vsun.y, Vsun.x) + if coord1 < 0.: + coord1 += PI2 + return (coord1, coord2) + + def Vsun_pos(self, adate): + """The vector of the sun at the given date + + Parameters + ---------- + adate: datetime + The date of the observation + + Returns + ------- + Vector + The position of the sun as a Vector object + """ + Vsun = -1. * self.pos(adate) + Vsun = Vsun / Vsun.length() + return Vsun + def unit_limit(x): """ Forces value to be in [-1, 1]. diff --git a/exoctk/contam_visibility/f_visibilityPeriods.py b/exoctk/contam_visibility/f_visibilityPeriods.py index ca29b560..e6337f89 100755 --- a/exoctk/contam_visibility/f_visibilityPeriods.py +++ b/exoctk/contam_visibility/f_visibilityPeriods.py @@ -19,6 +19,150 @@ PI2 = 2. * math.pi # 2 pi +def f_computeDurationOfVisibilityPeriodWithPA(ephemeris, mjdmin, mjdmax, + ra, dec, pa, mjdc): + """Computes the duration of a specific visibility period associated to a + given (RA,DEC), a given PA and given date + + flag = 0 visibility period fully in the search interval + flag = -1 start of the visibility period truncated by + the start of the search interval + flag = -2 end of the visibility period truncated by + the end of the search interval + flag = +1 the search interval is fully included in + the visibility period + + Parameters + ---------- + ephemeris: Ephemeris + The input ephemeris object. + mjdmin: float + The beginning of the search interval (modified + Julian date). It must be covered by the ephemeris. + mjdmax: float + The end of the search interval (modified + Julian date). It must be covered by the ephemeris. + ra: float + The input RA coordinate (equatorial coordinate, in rad). + dec: float + The input DEC coordinate (equatorial coordinate, in rad). + pa: float + The position angle. + + Example + ------- + >>> f_computeDurationOfVisibilityPeriodWithPA(ephemeris, mjdmin, mjdmax, + ra, dec, pa, mjdc) + + Returns + ------- + tuple + The lists of visibility period starts and ends with flags. + """ + if (ephemeris.amin > mjdmin): + print("""f_computeDurationOfVisibilityPeriodWithPA(): the start of\ + thesearch interval is not covered by the ephemeris.""") + print("""Ephemeris start date (modified Julian date):\ + {:8.5f}""".format(ephemeris.amin)) + print("""Search interval start date (modified Julian date):\ + {:8.5f}""".format(mjdmin)) + raise ValueError + + if (ephemeris.amax < mjdmax): + print("""f_computeDurationOfVisibilityPeriodWithPA(): the end of the\ + search interval is not covered by the ephemeris.""") + print("""Ephemeris end date (modified Julian date):\ + {:8.5f}""".format(ephemeris.amax)) + print("""Search interval end date (modified Julian date):\ + {:8.5f}""".format(mjdmax)) + raise ValueError + + if (mjdmin > mjdc): + print("""f_computeDurationOfVisibilityPeriodWithPA():\ + initial date is not included in the search interval.""") + print("""Search interval start date (modified Julian date):\ + {:8.5f}""".format(mjdmin)) + print("Initial date (modified Julian date): {:8.5f}".format(mjdc)) + raise ValueError + + if (mjdmax < mjdc): + print("""f_computeDurationOfVisibilityPeriodWithPA(): initial date is\ + not included in the search interval.""") + print("""Search interval end date (modified Julian date):\ + {:8.5f}""".format(mjdmax)) + print("Initial date (modified Julian date): {:8.5f}".format(mjdc)) + raise ValueError + + iflag = ephemeris.is_valid(mjdc, ra, dec, pa) + if (not iflag): + print("""f_computeDurationOfVisibilityPeriodWithPA(): invalid date\ + (not in a vsibility period).""") + print("Date (modified Julian date): {:8.5f}".format(mjdc)) + raise ValueError + + # =========================================================== + # Looking for the start of the visibility period + # =========================================================== + scanningStepSize = 0.1 + iflipLeft = False + currentmjd = mjdc + continueFlag = True + boundaryFlag = False + while (continueFlag): + currentmjd -= scanningStepSize + + if (currentmjd < mjdmin): + currentmjd = mjdmin + boundaryFlag = True + continueFlag = False + iflag = ephemeris.is_valid(currentmjd, ra, dec, pa) + + if (not iflag): + wstart = ephemeris.bisect_by_attitude( + currentmjd, currentmjd + scanningStepSize, ra, dec, pa) + iflipLeft = True + continueFlag = False + elif (boundaryFlag): + wstart = mjdmin + + iflipRight = False + currentmjd = mjdc + boundaryFlag = False + continueFlag = True + while (continueFlag): + + currentmjd += scanningStepSize + if (currentmjd > mjdmax): + currentmjd = mjdmax + boundaryFlag = True + continueFlag = False + + iflag = ephemeris.is_valid(currentmjd, ra, dec, pa) + if (not iflag): + wend = ephemeris.bisect_by_attitude(currentmjd - scanningStepSize, + currentmjd, ra, dec, pa) + iflipRight = True + continueFlag = False + + elif (boundaryFlag): + wend = mjdmax + + if ((not iflipLeft) and (not iflipRight)): + status = 1 + + elif (not iflipLeft): + status = -1 + + elif (not iflipRight): + status = -2 + + else: + status = 0 + + # End of the function + return wstart, wend, status + + def f_computeVisibilityPeriods(ephemeris, mjdmin, mjdmax, ra, dec): """Returns two lists containing the start end end of each visibility period and a list containing a status flag @@ -288,147 +432,3 @@ def f_computeVisibilityPeriodsWithPA(ephemeris, mjdmin, mjdmax, ra, dec, pa): # End of the function return startList, endList, statusList - - -def f_computeDurationOfVisibilityPeriodWithPA(ephemeris, mjdmin, mjdmax, - ra, dec, pa, mjdc): - """Computes the duration of a specific visibility period associated to a - given (RA,DEC), a given PA and given date - - flag = 0 visibility period fully in the search interval - flag = -1 start of the visibility period truncated by - the start of the search interval - flag = -2 end of the visibility period truncated by - the end of the search interval - flag = +1 the search interval is fully included in - the visibility period - - Parameters - ---------- - ephemeris: Ephemeris - The input ephemeris object. - mjdmin: float - The beginning of the search interval (modified - Julian date). It must be covered by the ephemeris. - mjdmax: float - The end of the search interval (modified - Julian date). It must be covered by the ephemeris. - ra: float - The input RA coordinate (equatorial coordinate, in rad). - dec: float - The input DEC coordinate (equatorial coordinate, in rad). - pa: float - The position angle. - - Example - ------- - >>> f_computeDurationOfVisibilityPeriodWithPA(ephemeris, mjdmin, mjdmax, - ra, dec, pa, mjdc) - - Returns - ------- - tuple - The lists of visibility period starts and ends with flags. - """ - if (ephemeris.amin > mjdmin): - print("""f_computeDurationOfVisibilityPeriodWithPA(): the start of\ - thesearch interval is not covered by the ephemeris.""") - print("""Ephemeris start date (modified Julian date):\ - {:8.5f}""".format(ephemeris.amin)) - print("""Search interval start date (modified Julian date):\ - {:8.5f}""".format(mjdmin)) - raise ValueError - - if (ephemeris.amax < mjdmax): - print("""f_computeDurationOfVisibilityPeriodWithPA(): the end of the\ - search interval is not covered by the ephemeris.""") - print("""Ephemeris end date (modified Julian date):\ - {:8.5f}""".format(ephemeris.amax)) - print("""Search interval end date (modified Julian date):\ - {:8.5f}""".format(mjdmax)) - raise ValueError - - if (mjdmin > mjdc): - print("""f_computeDurationOfVisibilityPeriodWithPA():\ - initial date is not included in the search interval.""") - print("""Search interval start date (modified Julian date):\ - {:8.5f}""".format(mjdmin)) - print("Initial date (modified Julian date): {:8.5f}".format(mjdc)) - raise ValueError - - if (mjdmax < mjdc): - print("""f_computeDurationOfVisibilityPeriodWithPA(): initial date is\ - not included in the search interval.""") - print("""Search interval end date (modified Julian date):\ - {:8.5f}""".format(mjdmax)) - print("Initial date (modified Julian date): {:8.5f}".format(mjdc)) - raise ValueError - - iflag = ephemeris.is_valid(mjdc, ra, dec, pa) - if (not iflag): - print("""f_computeDurationOfVisibilityPeriodWithPA(): invalid date\ - (not in a vsibility period).""") - print("Date (modified Julian date): {:8.5f}".format(mjdc)) - raise ValueError - - # =========================================================== - # Looking for the start of the visibility period - # =========================================================== - scanningStepSize = 0.1 - iflipLeft = False - currentmjd = mjdc - continueFlag = True - boundaryFlag = False - while (continueFlag): - currentmjd -= scanningStepSize - - if (currentmjd < mjdmin): - currentmjd = mjdmin - boundaryFlag = True - continueFlag = False - iflag = ephemeris.is_valid(currentmjd, ra, dec, pa) - - if (not iflag): - wstart = ephemeris.bisect_by_attitude( - currentmjd, currentmjd + scanningStepSize, ra, dec, pa) - iflipLeft = True - continueFlag = False - elif (boundaryFlag): - wstart = mjdmin - - iflipRight = False - currentmjd = mjdc - boundaryFlag = False - continueFlag = True - while (continueFlag): - - currentmjd += scanningStepSize - if (currentmjd > mjdmax): - currentmjd = mjdmax - boundaryFlag = True - continueFlag = False - - iflag = ephemeris.is_valid(currentmjd, ra, dec, pa) - if (not iflag): - wend = ephemeris.bisect_by_attitude(currentmjd - scanningStepSize, - currentmjd, ra, dec, pa) - iflipRight = True - continueFlag = False - - elif (boundaryFlag): - wend = mjdmax - - if ((not iflipLeft) and (not iflipRight)): - status = 1 - - elif (not iflipLeft): - status = -1 - - elif (not iflipRight): - status = -2 - - else: - status = 0 - - # End of the function - return wstart, wend, status diff --git a/exoctk/contam_visibility/field_simulator.py b/exoctk/contam_visibility/field_simulator.py index 28087694..2d3e6ba2 100755 --- a/exoctk/contam_visibility/field_simulator.py +++ b/exoctk/contam_visibility/field_simulator.py @@ -14,6 +14,7 @@ from exoctk.utils import get_env_variables from pysiaf.utils import rotations +EXOCTK_DATA = os.environ.get('EXOCTK_DATA') TRACES_PATH = os.path.join(os.environ.get('EXOCTK_DATA'), 'exoctk_contam', 'traces') def sossFieldSim(ra, dec, binComp='', dimX=256): From 04bbbe07aad834e7e07fc151c37133e2c3dabb0d Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Thu, 3 Jun 2021 10:08:29 -0400 Subject: [PATCH 02/20] PEP8 fixes for contam visibility modules --- exoctk/contam_visibility/field_simulator.py | 497 ++-- exoctk/contam_visibility/make_contam_plot.py | 21 +- exoctk/contam_visibility/math_extensionsx.py | 1571 +++++------ exoctk/contam_visibility/miniTools.py | 156 +- exoctk/contam_visibility/quaternionx.py | 2510 +++++++++--------- exoctk/contam_visibility/time_extensionsx.py | 537 ++-- exoctk/contam_visibility/visibilityPA.py | 6 +- 7 files changed, 2624 insertions(+), 2674 deletions(-) diff --git a/exoctk/contam_visibility/field_simulator.py b/exoctk/contam_visibility/field_simulator.py index 2d3e6ba2..5256baeb 100755 --- a/exoctk/contam_visibility/field_simulator.py +++ b/exoctk/contam_visibility/field_simulator.py @@ -3,211 +3,66 @@ import pysiaf import astropy.coordinates as crd +from astropy.io import fits +from astroquery.irsa import Irsa import astropy.units as u -import matplotlib.pyplot as plt import numpy as np -from astroquery.irsa import Irsa -from matplotlib import cm +from pysiaf.utils import rotations from scipy.io import readsav -from astropy.io import fits + from exoctk import utils -from exoctk.utils import get_env_variables -from pysiaf.utils import rotations EXOCTK_DATA = os.environ.get('EXOCTK_DATA') TRACES_PATH = os.path.join(os.environ.get('EXOCTK_DATA'), 'exoctk_contam', 'traces') -def sossFieldSim(ra, dec, binComp='', dimX=256): - """ Produce a SOSS field simulation for a target. + +def fieldSim(ra, dec, instrument, binComp='', testing=False): + """ Wraps ``sossFieldSim``, ``gtsFieldSim``, and ``lrsFieldSim`` together. + Produces a field simulation for a target using any instrument (NIRISS, + NIRCam, or MIRI). Parameters ---------- - ra: float + ra : float The RA of the target. - dec: float + dec : float The Dec of the target. - binComp: sequence + instrument : str + The instrument the contamination is being calculated for. + Can either be (case-sensitive): + 'NIRISS', 'NIRCam F322W2', 'NIRCam F444W', 'MIRI' + binComp : sequence The parameters of a binary companion. - dimX: int - The subarray size. + testing : bool + Shoud be ``True`` if running fieldSim for testing / troubleshooting + purposes. This will generate a matplotlib figure showing the target + FOV. The neighboring stars in this FOV will be included in the + contamination calculation (contamFig.py). Returns ------- - simuCub : np.ndarray - The simulated data cube. + simuCube : np.ndarray + The simulated data cube. Index 0 and 1 (axis=0) show the trace of + the target for orders 1 and 2 (respectively). Index 2-362 show the trace + of the target at every position angle (PA) of the instrument. + plt.plot() : matplotlib object + A plot. Only if `testing` parameter is set to True. """ + utils.check_for_data('exoctk_contam') - # STEP 1 - # Pulling stars from IRSA point-source catalog - targetcrd = crd.SkyCoord(ra=ra, dec=dec, unit=(u.hour, u.deg)) - targetRA = targetcrd.ra.value - targetDEC = targetcrd.dec.value - info = Irsa.query_region(targetcrd, - catalog='fp_psc', - spatial='Cone', - radius=2.5 * u.arcmin) - - # Coordinates of all stars in FOV, including target - allRA = info['ra'].data.data - allDEC = info['dec'].data.data - Jmag = info['j_m'].data.data - Hmag = info['h_m'].data.data - Kmag = info['k_m'].data.data - - # J-H band, H-K band. This will be used to derive the stellar Temps later - J_Hobs = Jmag - Hmag - H_Kobs = Hmag - Kmag - - # Determining target index by calculating the relative distance between - # each source and the target. The target will have the smallest distance - # from itself (oof) so whatever that index is will be the targetIndex - aa = ((targetRA - allRA) * np.cos(targetDEC)) - distance = np.sqrt(aa**2 + (targetDEC - allDEC)**2) - targetIndex = np.argmin(distance) - - # Add any missing companion - if binComp != '': - binComp = [float(i) for i in binComp.split(',')] - - deg2rad = np.pi / 180 - bb = binComp[0] / 3600 / np.cos(allDEC[targetIndex] * deg2rad) - allRA = np.append(allRA, (allRA[targetIndex] + bb)) - allDEC = np.append(allDEC, (allDEC[targetIndex] + binComp[1] / 3600)) - Jmag = np.append(Jmag, binComp[2]) - Hmag = np.append(Kmag, binComp[3]) - Kmag = np.append(Kmag, binComp[4]) - J_Hobs = Jmag - Hmag - H_Kobs = Hmag - Kmag - - # Number of stars - nStars = allRA.size - - # Restoring model parameters - modelParam = readsav(os.path.join(TRACES_PATH, 'NIRISS', 'modelsInfo.sav'), - verbose=False) - models = modelParam['models'] - modelPadX = modelParam['modelpadx'] - modelPadY = modelParam['modelpady'] - dimXmod = modelParam['dimxmod'] - dimYmod = modelParam['dimymod'] - jhMod = modelParam['jhmod'] - hkMod = modelParam['hkmod'] - teffMod = modelParam['teffmod'] - - # Find/assign Teff of each star - starsT = np.empty(nStars) - for j in range(nStars): - color_separation = (J_Hobs[j] - jhMod)**2 + (H_Kobs[j] - hkMod)**2 - min_separation_ind = np.argmin(color_separation) - starsT[j] = teffMod[min_separation_ind] - - sweetSpot = dict(x=856, y=107, RA=allRA[targetIndex], - DEC=allDEC[targetIndex], jmag=Jmag[targetIndex]) - - radeg = 180 / np.pi - niriss_pixel_scale = 0.065 # arcsec - # offset between all stars and target - dRA = (allRA - sweetSpot['RA']) * np.cos(sweetSpot['DEC'] / radeg) * 3600 - dDEC = (allDEC - sweetSpot['DEC']) * 3600 - - # Put field stars positions and magnitudes in structured array - _ = dict(RA=allRA, DEC=allDEC, dRA=dRA, dDEC=dDEC, jmag=Jmag, T=starsT, - x=np.empty(nStars), y=np.empty(nStars), dx=np.empty(nStars), - dy=np.empty(nStars)) - stars = np.empty(nStars, - dtype=[(key, val.dtype) for key, val in _.items()]) - for key, val in _.items(): - stars[key] = val - - # Initialize final fits cube that contains the modelled traces - # with contamination - PAmin = 0 # instrument PA, degrees - PAmax = 360 - dPA = 1 # degrees - - # Set of IPA values to cover - PAtab = np.arange(PAmin, PAmax, dPA) # degrees - nPA = len(PAtab) - - dimY = 2048 - # cube of trace simulation at every degree of field rotation, - # +target at O1 and O2 - simuCube = np.zeros([nPA + 2, dimY, dimX]) - - saveFiles = glob.glob( - os.path.join( - TRACES_PATH, - 'NIRISS', - '*modelOrder12*.sav')) - - # Big loop to generate a simulation at each instrument PA - - for kPA in range(PAtab.size): - APA = PAtab[kPA] - print('Generating field at APA : {}'.format(str(APA))) - - V3PA = APA + 0.57 # from APT - - sindx = np.sin((np.pi / 2) + APA / radeg) * stars['dDEC'] - cosdx = np.cos((np.pi / 2) + APA / radeg) * stars['dDEC'] - nps = niriss_pixel_scale - stars['dx'] = (np.cos((np.pi / 2) + APA / radeg) - * stars['dRA'] - sindx) / nps - stars['dy'] = (np.sin((np.pi / 2) + APA / radeg) - * stars['dRA'] + cosdx) / nps - stars['x'] = stars['dx'] + sweetSpot['x'] - stars['y'] = stars['dy'] + sweetSpot['y'] - - # Retain stars that are within the Direct Image NIRISS POM FOV - ind, = np.where((stars['x'] >= -162) & (stars['x'] <= 2047 + 185) & - (stars['y'] >= -154) & (stars['y'] <= 2047 + 174)) - starsInFOV = stars[ind] - - for i in range(len(ind)): - intx = round(starsInFOV['dx'][i]) - inty = round(starsInFOV['dy'][i]) - - k = np.where(teffMod == starsInFOV['T'][i])[0][0] - - fluxscale = 10.0**(-0.4 * - (starsInFOV['jmag'][i] - sweetSpot['jmag'])) - - # deal with subection sizes. - # these variables will determine where the - # trace will land on the array based on the - # neighbor's position relative to the target's position - mx0 = int(modelPadX - intx) - mx1 = int(modelPadX - intx + dimX) - my0 = int(modelPadY - inty) - my1 = int(modelPadY - inty + dimY) - - if (mx0 > dimXmod) or (my0 > dimYmod): - continue - if (mx1 < 0) or (my1 < 0): - continue + # Calling the variables which depend on what instrument you use + if instrument == 'NIRISS': + simuCube = sossFieldSim(ra, dec, binComp) - x0 = (mx0 < 0) * (-mx0) - y0 = (my0 < 0) * (-my0) - mx0 *= (mx0 >= 0) - mx1 = dimXmod if mx1 > dimXmod else mx1 - my0 *= (my0 >= 0) - my1 = dimYmod if my1 > dimYmod else my1 + elif instrument == 'NIRCam F444W': + simuCube = gtsFieldSim(ra, dec, 'F444W', binComp) - # if target and first kPA, add target traces of order 1 and 2 - # in output cube - if (intx == 0) & (inty == 0) & (kPA == 0): - fNameModO12 = saveFiles[k] + elif instrument == 'NIRCam F322W2': + simuCube = gtsFieldSim(ra, dec, 'F322W2', binComp) - modelO12 = readsav(fNameModO12, verbose=False)['modelo12'] - ord1 = modelO12[0, my0:my1, mx0:mx1] * fluxscale - ord2 = modelO12[1, my0:my1, mx0:mx1] * fluxscale - simuCube[0, y0:y0 + my1 - my0, x0:x0 + mx1 - mx0] = ord1 - simuCube[1, y0:y0 + my1 - my0, x0:x0 + mx1 - mx0] = ord2 + elif instrument == 'MIRI': + simuCube = lrsFieldSim(ra, dec, binComp) - if (intx != 0) or (inty != 0): - mod = models[k, my0:my1, mx0:mx1] - simuCube[kPA + 2, y0:y0 + my1 - my0, - x0:x0 + mx1 - mx0] += mod * fluxscale return simuCube @@ -245,7 +100,6 @@ def gtsFieldSim(ra, dec, filter, binComp=''): deg2rad = np.pi / 180 subX, subY = aper.XSciSize, aper.YSciSize rad = 2.5 # arcmins - pixel_scale = 0.063 # arsec/pixel V3PAs = np.arange(0, 360, 1) nPA = len(V3PAs) # Generate cube of field simulation at every degree of APA rotation @@ -257,8 +111,7 @@ def gtsFieldSim(ra, dec, filter, binComp=''): minrow, maxrow = rows.min(), rows.max() mincol, maxcol = cols.min(), cols.max() - #############################STEP 1##################################### - ######################################################################## + # STEP 1 # Converting to degrees targetcrd = crd.SkyCoord(ra=ra, dec=dec, unit=(u.hour, u.deg)) targetRA = targetcrd.ra.value @@ -276,12 +129,11 @@ def gtsFieldSim(ra, dec, filter, binComp=''): stars = {} stars['RA'], stars['DEC'] = allRA, allDEC - #############################STEP 2##################################### - ######################################################################## + # STEP 2 sindRA = (targetRA - stars['RA']) * np.cos(targetDEC) cosdRA = targetDEC - stars['DEC'] - distance = np.sqrt(sindRA**2 + cosdRA**2) - if np.min(distance) > 1.0*(10**-4): + distance = np.sqrt(sindRA ** 2 + cosdRA ** 2) + if np.min(distance) > 1.0 * (10 ** -4): coords = crd.SkyCoord(ra=ra, dec=dec, unit=(u.hour, u.deg)).to_string('decimal') ra, dec = coords.split(' ')[0], coords.split(' ')[1] raise Exception('Unable to detect a source with coordinates [RA: {}, DEC: {}] within IRSA`s 2MASS Point-Source Catalog. Please enter different coordinates or contact the JWST help desk.'.format(str(ra), str(dec))) @@ -291,17 +143,11 @@ def gtsFieldSim(ra, dec, filter, binComp=''): # Restoring model parameters modelParam = readsav(os.path.join(TRACES_PATH, 'NIRISS', 'modelsInfo.sav'), verbose=False) - models = modelParam['models'] - modelPadX = modelParam['modelpadx'] - modelPadY = modelParam['modelpady'] - dimXmod = modelParam['dimxmod'] - dimYmod = modelParam['dimymod'] jhMod = modelParam['jhmod'] hkMod = modelParam['hkmod'] teffMod = modelParam['teffmod'] - #############################STEP 3##################################### - ######################################################################## + # STEP 3 # JHK bands of all stars in FOV, including target Jmag = info['j_m'].data.data Hmag = info['h_m'].data.data @@ -335,8 +181,7 @@ def gtsFieldSim(ra, dec, filter, binComp=''): stars['Temp'] = starsT stars['Jmag'] = Jmag - #############################STEP 4##################################### - ######################################################################## + # STEP 4 # Calculate corresponding V2/V3 (TEL) coordinates for Sweetspot v2targ, v3targ = aper.det_to_tel(xSweet, ySweet) @@ -344,9 +189,9 @@ def gtsFieldSim(ra, dec, filter, binComp=''): # Get APA from V3PA APA = V3PA + add_to_v3pa if APA > 360: - APA = APA-360 + APA = APA - 360 elif APA < 0: - APA = APA+360 + APA = APA + 360 print('Generating field at APA : {}'.format(str(APA))) @@ -378,8 +223,7 @@ def gtsFieldSim(ra, dec, filter, binComp=''): sci_targx, sci_targy = stars['xsci'][targetIndex],\ stars['ysci'][targetIndex] - #############################STEP 5##################################### - ######################################################################## + # STEP 5 inFOV = [] for star in range(0, nStars): @@ -389,8 +233,7 @@ def gtsFieldSim(ra, dec, filter, binComp=''): inFOV = np.array(inFOV) - #############################STEP 6##################################### - ######################################################################## + # STEP 6 nircam_path = 'NIRCam_F444W' if filter == 'F444W' else 'NIRCam_F322W2' fitsFiles = glob.glob( os.path.join( @@ -409,8 +252,7 @@ def gtsFieldSim(ra, dec, filter, binComp=''): if str(temp) in file: trace = fits.getdata(file, 1)[0] - fluxscale = 10.0**(-0.4 * \ - (stars['Jmag'][idx] - stars['Jmag'][targetIndex])) + fluxscale = 10.0**(-0.4 * (stars['Jmag'][idx] - stars['Jmag'][targetIndex])) # Padding array pad_trace = np.pad(trace, pad_width=5000, mode='constant', @@ -488,8 +330,7 @@ def lrsFieldSim(ra, dec, binComp=''): the target for orders 1 and 2 (respectively). Index 2-362 show the trace of the target at every position angle (PA) of the instrument. """ - #############################INSTRUMENT PARAMETERS###################### - ######################################################################## + # INSTRUMENT PARAMETERS # Instantiate a pySIAF object siaf = pysiaf.Siaf('MIRI') aper = siaf.apertures['MIRIM_SLITLESSPRISM'] @@ -499,7 +340,6 @@ def lrsFieldSim(ra, dec, binComp=''): deg2rad = np.pi / 180 subX, subY = aper.XSciSize, aper.YSciSize rad = 2.0 # arcmins - pixel_scale = 0.11 # arsec/pixel V3PAs = np.arange(0, 360, 1) nPA = len(V3PAs) # Generate cube of field simulation at every degree of APA rotation @@ -511,8 +351,7 @@ def lrsFieldSim(ra, dec, binComp=''): minrow, maxrow = rows.min(), rows.max() mincol, maxcol = cols.min(), cols.max() - #############################STEP 1##################################### - ######################################################################## + # STEP 1 # Converting to degrees targetcrd = crd.SkyCoord(ra=ra, dec=dec, unit=(u.hour, u.deg)) targetRA = targetcrd.ra.value @@ -530,12 +369,11 @@ def lrsFieldSim(ra, dec, binComp=''): stars = {} stars['RA'], stars['DEC'] = allRA, allDEC - #############################STEP 2##################################### - ######################################################################## + # STEP 2 sindRA = (targetRA - stars['RA']) * np.cos(targetDEC) cosdRA = targetDEC - stars['DEC'] - distance = np.sqrt(sindRA**2 + cosdRA**2) - if np.min(distance) > 1.0*(10**-4): + distance = np.sqrt(sindRA ** 2 + cosdRA ** 2) + if np.min(distance) > 1.0 * (10 ** -4): coords = crd.SkyCoord(ra=ra, dec=dec, unit=(u.hour, u.deg)).to_string('decimal') ra, dec = coords.split(' ')[0], coords.split(' ')[1] raise Exception('Unable to detect a source with coordinates [RA: {}, DEC: {}] within IRSA`s 2MASS Point-Source Catalog. Please enter different coordinates or contact the JWST help desk.'.format(str(ra), str(dec))) @@ -545,17 +383,11 @@ def lrsFieldSim(ra, dec, binComp=''): # Restoring model parameters modelParam = readsav(os.path.join(TRACES_PATH, 'NIRISS', 'modelsInfo.sav'), verbose=False) - models = modelParam['models'] - modelPadX = modelParam['modelpadx'] - modelPadY = modelParam['modelpady'] - dimXmod = modelParam['dimxmod'] - dimYmod = modelParam['dimymod'] jhMod = modelParam['jhmod'] hkMod = modelParam['hkmod'] teffMod = modelParam['teffmod'] - #############################STEP 3##################################### - ######################################################################## + # STEP 3 # JHK bands of all stars in FOV, including target Jmag = info['j_m'].data.data Hmag = info['h_m'].data.data @@ -589,8 +421,7 @@ def lrsFieldSim(ra, dec, binComp=''): stars['Temp'] = starsT stars['Jmag'] = Jmag - #############################STEP 4##################################### - ######################################################################## + # STEP 4 # Calculate corresponding V2/V3 (TEL) coordinates for Sweetspot v2targ, v3targ = aper.det_to_tel(xSweet, ySweet) @@ -598,9 +429,9 @@ def lrsFieldSim(ra, dec, binComp=''): # Get APA from V3PA APA = V3PA + add_to_v3pa if APA > 360: - APA = APA-360 + APA = APA - 360 elif APA < 0: - APA = APA+360 + APA = APA + 360 print('Generating field at APA : {}'.format(str(APA))) @@ -631,8 +462,7 @@ def lrsFieldSim(ra, dec, binComp=''): sci_targx, sci_targy = stars['xsci'][targetIndex],\ stars['ysci'][targetIndex] - #############################STEP 5##################################### - ######################################################################## + # STEP 5 inFOV = [] for star in range(0, nStars): @@ -642,8 +472,7 @@ def lrsFieldSim(ra, dec, binComp=''): inFOV = np.array(inFOV) - #############################STEP 6##################################### - ######################################################################## + # STEP 6 fitsFiles = glob.glob(os.path.join(TRACES_PATH, 'MIRI', 'LOW*.fits')) fitsFiles = np.sort(fitsFiles) @@ -658,8 +487,7 @@ def lrsFieldSim(ra, dec, binComp=''): if str(temp) in file: trace = fits.getdata(file)[0] - fluxscale = 10.0**(-0.4 * \ - (stars['Jmag'][idx] - stars['Jmag'][targetIndex])) + fluxscale = 10.0**(-0.4 * (stars['Jmag'][idx] - stars['Jmag'][targetIndex])) # Padding array pad_trace = np.pad(trace, pad_width=5000, mode='constant', @@ -720,58 +548,197 @@ def lrsFieldSim(ra, dec, binComp=''): return simuCube -def fieldSim(ra, dec, instrument, binComp='', testing=False): - """ Wraps ``sossFieldSim``, ``gtsFieldSim``, and ``lrsFieldSim`` together. - Produces a field simulation for a target using any instrument (NIRISS, - NIRCam, or MIRI). +def sossFieldSim(ra, dec, binComp='', dimX=256): + """ Produce a SOSS field simulation for a target. Parameters ---------- - ra : float + ra: float The RA of the target. - dec : float + dec: float The Dec of the target. - instrument : str - The instrument the contamination is being calculated for. - Can either be (case-sensitive): - 'NIRISS', 'NIRCam F322W2', 'NIRCam F444W', 'MIRI' - binComp : sequence + binComp: sequence The parameters of a binary companion. - testing : bool - Shoud be ``True`` if running fieldSim for testing / troubleshooting - purposes. This will generate a matplotlib figure showing the target - FOV. The neighboring stars in this FOV will be included in the - contamination calculation (contamFig.py). + dimX: int + The subarray size. Returns ------- - simuCube : np.ndarray - The simulated data cube. Index 0 and 1 (axis=0) show the trace of - the target for orders 1 and 2 (respectively). Index 2-362 show the trace - of the target at every position angle (PA) of the instrument. - plt.plot() : matplotlib object - A plot. Only if `testing` parameter is set to True. + simuCub : np.ndarray + The simulated data cube. """ - utils.check_for_data('exoctk_contam') - # Calling the variables which depend on what instrument you use - if instrument == 'NIRISS': - simuCube = sossFieldSim(ra, dec, binComp) + # STEP 1 + # Pulling stars from IRSA point-source catalog + targetcrd = crd.SkyCoord(ra=ra, dec=dec, unit=(u.hour, u.deg)) + targetRA = targetcrd.ra.value + targetDEC = targetcrd.dec.value + info = Irsa.query_region(targetcrd, + catalog='fp_psc', + spatial='Cone', + radius=2.5 * u.arcmin) - elif instrument == 'NIRCam F444W': - simuCube = gtsFieldSim(ra, dec, 'F444W', binComp) + # Coordinates of all stars in FOV, including target + allRA = info['ra'].data.data + allDEC = info['dec'].data.data + Jmag = info['j_m'].data.data + Hmag = info['h_m'].data.data + Kmag = info['k_m'].data.data - elif instrument == 'NIRCam F322W2': - simuCube = gtsFieldSim(ra, dec, 'F322W2', binComp) + # J-H band, H-K band. This will be used to derive the stellar Temps later + J_Hobs = Jmag - Hmag + H_Kobs = Hmag - Kmag - elif instrument == 'MIRI': - simuCube = lrsFieldSim(ra, dec, binComp) + # Determining target index by calculating the relative distance between + # each source and the target. The target will have the smallest distance + # from itself (oof) so whatever that index is will be the targetIndex + aa = ((targetRA - allRA) * np.cos(targetDEC)) + distance = np.sqrt(aa**2 + (targetDEC - allDEC)**2) + targetIndex = np.argmin(distance) + + # Add any missing companion + if binComp != '': + binComp = [float(i) for i in binComp.split(',')] + deg2rad = np.pi / 180 + bb = binComp[0] / 3600 / np.cos(allDEC[targetIndex] * deg2rad) + allRA = np.append(allRA, (allRA[targetIndex] + bb)) + allDEC = np.append(allDEC, (allDEC[targetIndex] + binComp[1] / 3600)) + Jmag = np.append(Jmag, binComp[2]) + Hmag = np.append(Kmag, binComp[3]) + Kmag = np.append(Kmag, binComp[4]) + J_Hobs = Jmag - Hmag + H_Kobs = Hmag - Kmag + + # Number of stars + nStars = allRA.size + + # Restoring model parameters + modelParam = readsav(os.path.join(TRACES_PATH, 'NIRISS', 'modelsInfo.sav'), + verbose=False) + models = modelParam['models'] + modelPadX = modelParam['modelpadx'] + modelPadY = modelParam['modelpady'] + dimXmod = modelParam['dimxmod'] + dimYmod = modelParam['dimymod'] + jhMod = modelParam['jhmod'] + hkMod = modelParam['hkmod'] + teffMod = modelParam['teffmod'] + + # Find/assign Teff of each star + starsT = np.empty(nStars) + for j in range(nStars): + color_separation = (J_Hobs[j] - jhMod)**2 + (H_Kobs[j] - hkMod)**2 + min_separation_ind = np.argmin(color_separation) + starsT[j] = teffMod[min_separation_ind] + + sweetSpot = dict(x=856, y=107, RA=allRA[targetIndex], + DEC=allDEC[targetIndex], jmag=Jmag[targetIndex]) + + radeg = 180 / np.pi + niriss_pixel_scale = 0.065 # arcsec + # offset between all stars and target + dRA = (allRA - sweetSpot['RA']) * np.cos(sweetSpot['DEC'] / radeg) * 3600 + dDEC = (allDEC - sweetSpot['DEC']) * 3600 + + # Put field stars positions and magnitudes in structured array + _ = dict(RA=allRA, DEC=allDEC, dRA=dRA, dDEC=dDEC, jmag=Jmag, T=starsT, + x=np.empty(nStars), y=np.empty(nStars), dx=np.empty(nStars), + dy=np.empty(nStars)) + stars = np.empty(nStars, + dtype=[(key, val.dtype) for key, val in _.items()]) + for key, val in _.items(): + stars[key] = val + + # Initialize final fits cube that contains the modelled traces + # with contamination + PAmin = 0 # instrument PA, degrees + PAmax = 360 + dPA = 1 # degrees + + # Set of IPA values to cover + PAtab = np.arange(PAmin, PAmax, dPA) # degrees + nPA = len(PAtab) + + dimY = 2048 + # cube of trace simulation at every degree of field rotation, + # +target at O1 and O2 + simuCube = np.zeros([nPA + 2, dimY, dimX]) + + saveFiles = glob.glob( + os.path.join( + TRACES_PATH, + 'NIRISS', + '*modelOrder12*.sav')) + + # Big loop to generate a simulation at each instrument PA + + for kPA in range(PAtab.size): + APA = PAtab[kPA] + print('Generating field at APA : {}'.format(str(APA))) + + sindx = np.sin((np.pi / 2) + APA / radeg) * stars['dDEC'] + cosdx = np.cos((np.pi / 2) + APA / radeg) * stars['dDEC'] + nps = niriss_pixel_scale + stars['dx'] = (np.cos((np.pi / 2) + APA / radeg) * stars['dRA'] - sindx) / nps + stars['dy'] = (np.sin((np.pi / 2) + APA / radeg) * stars['dRA'] + cosdx) / nps + stars['x'] = stars['dx'] + sweetSpot['x'] + stars['y'] = stars['dy'] + sweetSpot['y'] + + # Retain stars that are within the Direct Image NIRISS POM FOV + ind, = np.where((stars['x'] >= -162) & (stars['x'] <= 2047 + 185) & (stars['y'] >= -154) & (stars['y'] <= 2047 + 174)) + starsInFOV = stars[ind] + + for i in range(len(ind)): + intx = round(starsInFOV['dx'][i]) + inty = round(starsInFOV['dy'][i]) + + k = np.where(teffMod == starsInFOV['T'][i])[0][0] + + fluxscale = 10.0**(-0.4 * (starsInFOV['jmag'][i] - sweetSpot['jmag'])) + + # deal with subection sizes. + # these variables will determine where the + # trace will land on the array based on the + # neighbor's position relative to the target's position + mx0 = int(modelPadX - intx) + mx1 = int(modelPadX - intx + dimX) + my0 = int(modelPadY - inty) + my1 = int(modelPadY - inty + dimY) + + if (mx0 > dimXmod) or (my0 > dimYmod): + continue + if (mx1 < 0) or (my1 < 0): + continue + + x0 = (mx0 < 0) * (-mx0) + y0 = (my0 < 0) * (-my0) + mx0 *= (mx0 >= 0) + mx1 = dimXmod if mx1 > dimXmod else mx1 + my0 *= (my0 >= 0) + my1 = dimYmod if my1 > dimYmod else my1 + + # if target and first kPA, add target traces of order 1 and 2 + # in output cube + if (intx == 0) & (inty == 0) & (kPA == 0): + fNameModO12 = saveFiles[k] + + modelO12 = readsav(fNameModO12, verbose=False)['modelo12'] + ord1 = modelO12[0, my0:my1, mx0:mx1] * fluxscale + ord2 = modelO12[1, my0:my1, mx0:mx1] * fluxscale + simuCube[0, y0:y0 + my1 - my0, x0:x0 + mx1 - mx0] = ord1 + simuCube[1, y0:y0 + my1 - my0, x0:x0 + mx1 - mx0] = ord2 + + if (intx != 0) or (inty != 0): + mod = models[k, my0:my1, mx0:mx1] + simuCube[kPA + 2, y0:y0 + my1 - my0, + x0:x0 + mx1 - mx0] += mod * fluxscale return simuCube if __name__ == '__main__': + ra, dec = "04 25 29.0162", "-30 36 01.603" # Wasp 79 - #sossFieldSim(ra, dec) + # sossFieldSim(ra, dec) if EXOCTK_DATA: fieldSim(ra, dec, instrument='NIRISS') diff --git a/exoctk/contam_visibility/make_contam_plot.py b/exoctk/contam_visibility/make_contam_plot.py index 590abd51..95f4a2ff 100644 --- a/exoctk/contam_visibility/make_contam_plot.py +++ b/exoctk/contam_visibility/make_contam_plot.py @@ -1,16 +1,15 @@ -import numpy as np - from astropy.coordinates import SkyCoord -from bokeh.layouts import gridplot -from bokeh.plotting import figure, show +from bokeh.plotting import show +import numpy as np -from exoctk.contam_visibility import visibilityPA as vpa -from exoctk.contam_visibility import field_simulator as fs from exoctk.contam_visibility import contamination_figure as cf +from exoctk.contam_visibility import field_simulator as fs +from exoctk.contam_visibility import visibilityPA as vpa + def main(): - """ Wrapper to the field simulator and contamination figure generator. - """ + """ Wrapper to the field simulator and contamination figure + generator.""" # User inputs ra = input('Please input the Right Ascension of your target in decimal degrees (The more decimal places the better) : \n') @@ -27,9 +26,9 @@ def main(): companion = input('Any companion not in IRSA`s 2MASS Point-Source Catalog that should be considered for contamination? If no, just press Enter. If yes, please enter the following (comma-separated, no spaces): \n RA offset ("), DEC offset ("), 2MASS J (mag), H (mag) and Ks (mag) \n ').split(',') print(len(companion)) - if len(companion)==5: + if len(companion) == 5: binComp = [int(param) for param in companion] - elif len(companion)==1: + elif len(companion) == 1: binComp = '' elif (len(companion) < 5) & (len(companion) > 1): print('Companion information is incomplete. Starting over...') @@ -60,5 +59,7 @@ def main(): show(plot) + if __name__ == "__main__": + main() diff --git a/exoctk/contam_visibility/math_extensionsx.py b/exoctk/contam_visibility/math_extensionsx.py index bcaee3ce..d8732cec 100755 --- a/exoctk/contam_visibility/math_extensionsx.py +++ b/exoctk/contam_visibility/math_extensionsx.py @@ -14,379 +14,289 @@ OBLIQUITY = 23.43929 * D2R # Obliquity of Earth's orbit, in radians -def sind(x): - """Return the sin in degrees. - - Parameters - ---------- - x: float - The evaluand. +# Some classes need to be defined first so they are available to other classes +class Histogram(object): + """Class to represent a histogram.""" - Returns - ------- - float - The sin of x in degrees. - """ - return sin(radians(x)) + def __str__(self): + """Returns a printed representation of the histogram. + """ + # Don't assume a type for the total, as it may not be an integer + # if normalized. + v = (len(self.bins), self.num_items()) + result = 'Histogram: % d bins, % s items\n' % v + for bin in self.bins: + result = result + '%s\n' % (bin.__str__()) -def cosd(x): - """Return the cos in degrees + return(result) - Parameters - ---------- - x: float - The evaluand. + def normalize(self, total=None): + """Takes a histogram and returns a new histogram that normalizes all + its values. - Returns - ------- - float - The cos of x in degrees. - """ - return cos(radians(x)) + Parameters + ---------- + total: int + Number of items to divide each bin by for the normalization. + If not supplied, it defaults to the total in the histogram. + Returns + ------- + new_histogram : Histogram + The new histogram. + """ + if (total is None): + total = self.num_items() -def atan2d(x): - """Return the arctan in degrees + new_histogram = deepcopy(self) # make a deep copy - Parameters - ---------- - x: float - The evaluand. + for bin in new_histogram.bins: + bin.count = (float(bin.count)) / total - Returns - ------- - float - The arctan of x in degrees. - """ - return atan2(radians(x)) + return(new_histogram) + def num_items(self): + """Returns the total number of items stored in the histogram + """ + return(sum([bin.count for bin in self.bins])) -def really_less_than(x, y): - """Safe less-than function that returns true if and only if x is - "significantly" less than y. + def retrieve_count(self, bin_index): + """Returns the number of items stored in a given bin of the histogram. - Parameters - ---------- - x: float - The first number. - y: float - The second number. + Parameters + ---------- + bin_index: int + The index to use (starts with 1). - Returns - ------- - bool - True if x is less, else False. - """ - return(x < y - EPSILON) + Returns + ------- + int + The number of items in the bin. + """ + return(self.bins[bin_index - 1].count) -def really_greater_than(x, y): - """Safe greater-than function that returns true if and only if x is - "significantly" greater than y +class HistogramBin(object): + """Class to represent a bin within a histogram.""" - Parameters - ---------- - x: float - The first number. - y: float - The second number. + def store_items(self, num_items=1): + """Stores a given number of items in the bin. - Returns - ------- - bool - True if x is greater, else False. - """ - return(x > y + EPSILON) + Parameters + ---------- + num_items: int + Number of items to store (default 1). + """ + self.count += num_items -def asin2(val): - """Safe version of asin that handles invalid arguments. +class Polynomial(object): + """Class to represent a polynomial.""" - Arguments greater than 1 are truncated to 1; arguments less than -1 are - set to -1. + def __init__(self, coefficients): + """Constructor for a polynomial. + Coefficients = a list of coefficients, starting with order 0 and + increasing. - Parameters - ---------- - val: float - The evaluand. + Parameters + ---------- + coefficients: sequence + The list of coefficients, starting with order 0 and increasing. + """ + self.coefficients = coefficients - Returns - ------- - float - The arcsin of the value. - """ - return(asin(max(-1.0, min(1.0, val)))) + def __str__(self): + """Returns a string representation of a polynomial.""" + order = len(self.coefficients) - 1 + return_string = 'Polynomial: order % d' % (order) + for index in range(order + 1): # print list of coefficients + c = index, self.coefficients[index] + return_string = return_string + '\nCoefficient % d = % .3f' % c -def acos2(val): - """Safe version of acos that handles invalid arguments in the same way as - asin2 + return(return_string) - Parameters - ---------- - val: float - The evaluand. + def apply(self, value): + """Returns the result of applying a polynomial to an input value - Returns - ------- - float - The arccos of the value. - """ - return(acos(max(-1.0, min(1.0, val)))) + Parameters + ---------- + value: float + The evaluand. + Returns + ------- + float + The result of the evaluated equation. + """ + result = 0 -def avg(l): - """Returns the average of a list of numbers + # For each index, raise the input to the power given by that index + # and multiply by the coefficient + for index in range(len(self.coefficients)): + result = result + self.coefficients[index] * value**index - Parameters - ---------- - l: sequence - The list of numbers. + return (result) - Returns - ------- - float - The average. - """ - return(sum(l) / float(len(l))) +class Rectangle(object): + """Class to represent a rectangle.""" -def avg2(num1, num2): - """Returns the average of two numbers + def __init__(self, length, width): + """Initialize a rectangle with a specified length and width. - Parameters - ---------- - num1: float - The first number. - num2: float - The second number. + Parameters + ---------- + length: float + The length of the rectangle. + width: float + The width of the rectangle. + """ + self.length = length + self.width = width - Returns - ------- - float - The average. - """ - return((num1 + num2) / 2.0) + def __str__(self): + """Inspector method for the rectangle. + """ + dims = (self.length, self.width) + return('Rectangle: length = % .2f, width = % .2f' % dims) + def area(self): + """Returns the area of the rectangle. + """ + return(self.length * self.width) -def output_as_percentage(num, fractional_digits=1): - """Output a percentage neatly. + def motion_tolerant_area(self, motion_length, motion_angle): + """Returns the area within a rectangle that can tolerate a motion in + a known direction while remaining within the rectangle. - Parameters - ---------- - num: float - The number to make into a percentage. - fractional_digits: int - Number of digits to output as fractions of a percent. - If not supplied, there is no reduction in precision. + Parameters + ---------- + motion_length: float + Distance of motion (same units as rectangle length and width). + motion_angle: float + Angle in radians between the direction of motion and long + direction of rectangle. - Returns - ------- - str - The percentage. - """ - if (fractional_digits is not None): - format_str = '%%.%.df' % (fractional_digits) # creates format string - else: - format_str = '%f' - - return('%s%%' % (format_str % (num))) - - -def percent_str(num, fractional_digits=1): - """Output a number as a percentage. - - Parameters - ---------- - num: float - The number to make into a percentage. - fractional_digits: int - Number of digits to output as fractions of a percent. - If not supplied, there is no reduction in precision. - - Returns - ------- - str - The percentage. - """ - return(output_as_percentage(100 * num, fractional_digits)) - - -def variance(l): - """Variance of a list of numbers that represent sample values - - Parameters - ---------- - l: sequence - The list to take the variance of. - - Returns - ------- - float - The variance. - """ - # Returns the sample variance (n-1 formula). - mean = avg(l) - sumsq = sum([(i - mean)**2 for i in l]) - return(sumsq / float(len(l) - 1)) - - -def stdev(l): - """Standard deviation of a list of numbers that represent sample values - - Parameters - ---------- - l: sequence - The list to take the standard deviation of. - - Returns - ------- - float - The standard deviation. - """ - # Simply take the square root of the sample variance. - return(sqrt(variance(l))) - - -def factorial(num): - """Returns the factorial of a nonnegative integer. - This function is provided in math module starting with Python 2.6, - but implement anyway for compatibility with older systems. - - Parameters - ---------- - num: int - The number to factorialize. - - Returns - ------- - int - The factorial. - """ - result = 1 # factorial of 0 is defined as 1 - - for i in range(2, num + 1): - result = result * i - return(result) - - -def conditional_probability(p_joint, p_B): - """Returns probability of event A given event B. - - Parameters - ---------- - p_joint: float - P(A,B). - p_B: float - Probability of event B. - - Returns - ------- - float - The probability. - """ - return(p_joint / p_B) + Returns + ------- + float + The area. + """ + # Compute the x and y distances to the edge. A position + # within the rectangle is only motion-tolerant if it exceeds + # both of these distances. The effective length is thus reduced + # by delta_x and the effective width by delta_y. + delta_x = motion_length * cos(motion_angle) # lengthwise direction + delta_y = motion_length * sin(motion_angle) + return ((self.length - delta_x) * (self.width - delta_y)) -class Polynomial(object): - """Class to represent a polynomial.""" +class Circle(object): + """Class to represent a circle.""" - def __init__(self, coefficients): - """Constructor for a polynomial. - Coefficients = a list of coefficients, starting with order 0 and - increasing. + def __init__(self, radius): + """Initialize a circle with a specified radius. Parameters ---------- - coefficients: sequence - The list of coefficients, starting with order 0 and increasing. + radius: float + The radius of the circle. """ - self.coefficients = coefficients + self.radius = radius def __str__(self): - """Returns a string representation of a polynomial.""" - order = len(self.coefficients) - 1 - return_string = 'Polynomial: order % d' % (order) + """Inspector method for the circle. + """ + return('Circle: radius = % .2f' % (self.radius)) - for index in range(order + 1): # print list of coefficients - c = index, self.coefficients[index] - return_string = return_string + '\nCoefficient % d = % .3f' % c + def area(self): + """Returns the area of the circle. + """ + return(pi * self.radius**2) - return(return_string) - def apply(self, value): - """Returns the result of applying a polynomial to an input value +class ContinuousHistogram(Histogram): + """Class to represent a histogram with continuous values.""" + + def __init__(self, boundaries, highest_inclusive=False): + """Initializes a continuous histogram. + + Default behavior with highest_inclusive = False: + Bin 0 is defined by x <= boundaries[0]. + For i > 0, bin i is defined by boundaries[i-1] < x <= boundaries[i]. + Bin n+1 is defined by x > boundaries[n-1]. + + Behavior with highest_exclusive = True: + Bins below n are defined in the same way as above. + Bin n is defined by boundaries[n-2] < x < boundaries[n-1]. + Bin n+1 is defined by x >= boundaries[n-1]. Parameters ---------- - value: float - The evaluand. - - Returns - ------- - float - The result of the evaluated equation. + boundaries: sequence + List of numbers that separate the bins, in increasing order. + highest_inclusive: bool + True if highest bin includes the last boundary, + False (default) otherwise. """ - result = 0 - - # For each index, raise the input to the power given by that index - # and multiply by the coefficient - for index in range(len(self.coefficients)): - result = result + self.coefficients[index] * value**index + self.bins = [] + self.highest_inclusive = highest_inclusive + lower_lim = None - return (result) + # A histogram is a list of bins. + # For the first bin, the lower limit is None (unbounded). + # For the last bin, the upper limit is None. + # Rely on default limits behavior in HistogramBin constructor. + for index in range(len(boundaries)): + upper_lim = boundaries[index] + self.bins.append(RangeBin(min_value=lower_lim, + max_value=upper_lim)) + lower_lim = upper_lim -class LinearEquation(Polynomial): - """Subclass of Polynomial for linear equations. - This implementation is three times faster, so Polynomial should be reserved - for higher orders.""" + self.bins.append(RangeBin(min_value=lower_lim)) # Add highest bin - def __init__(self, coeff0, coeff1): - """Constructor for a linear equation to provide a more 'natural' - interface without using a list. + # If highest_inclusive is set, change limit behavior of two + # highest bins. + if (highest_inclusive): + self.bins[-2].upper_inclusive = False + self.bins[-1].lower_inclusive = True - Parameters - ---------- - coeff0: float - Additive constant. - coeff1: float - Multiplicative coefficient. + def retrieve_boundaries(self): + """Returns the list of boundaries of a continuous histogram. """ - self.coefficients = [coeff0, coeff1] - - def apply(self, value): - """Applies a linear equation to an input value. + # Return upper limits of all bins except the last. + return([bin.max_value for bin in self.bins[:-1]]) - This is intended to be faster than the more general method with - Polynomial. + def store_items(self, value, count=1): + """Stores a value in the continuous histogram. Parameters ---------- value: float - The evaluand. - - Returns - ------- - float - The result of the evaluated equation. + The value to store. + count: int + Number of items with that value to store (default 1). """ - return(value * self.coefficients[1] + self.coefficients[0]) + bin_index = 0 + found = False + # Search all bins up to the next-highest. If value is not too high + # for the bin, store the items there. + while ((not found) and (bin_index < len(self.bins) - 1)): + bin = self.bins[bin_index] -class HistogramBin(object): - """Class to represent a bin within a histogram.""" + if (not (bin.istoo_high(value))): + found = True + bin.store_items(count) - def store_items(self, num_items=1): - """Stores a given number of items in the bin. + bin_index += 1 - Parameters - ---------- - num_items: int - Number of items to store (default 1). - """ - self.count += num_items + # The value must belong in the highest bin if not found earlier. + if (not found): + self.bins[-1].store_items(count) class DiscreteBin(HistogramBin): @@ -426,168 +336,8 @@ def ismatch(self, value): return(value == self.bin_value) -class RangeBin(HistogramBin): - """Class to represent a bin with a range.""" - - def __init__(self, min_value=None, max_value=None, lower_inclusive=False, - upper_inclusive=True): - """Constructor for a range bin. - - Parameters - ---------- - min_value: float - Minimum value for the bin. - max_value: float - Maximum value for the bin. - lower_inclusive: bool - True if min_value is inclusive, else False (default). - upper_inclusive: bool - True if max_value is inclusive, else False (default). - """ - self.min_value = min_value - self.max_value = max_value - self.lower_inclusive = lower_inclusive - self.upper_inclusive = upper_inclusive - self.count = 0 - - def describe_limits(self, precision=2): - """Returns a printed representation of the limits of the bin. - - Parameters - ---------- - precision: int - Number of digits to print after the decimal point. - - Returns - ------- - str - The limits of the bin. - """ - if(self.min_value is None): - if(self.upper_inclusive): - result = '<= % . * f:' % (precision, self.max_value) - else: - result = '< % . * f:' % (precision, self.max_value) - elif(self.max_value is None): - if(self.lower_inclusive): - result = '>= % . * f:' % (precision, self.min_value) - else: - result = '> % . * f:' % (precision, self.min_value) - else: - v = (precision, self.min_value, precision, self.max_value) - result = '%. * f to % . * f:' % v - - return(result) - - def __str__(self): - """Returns a printed representation of the bin - """ - # Don't assume a type for the count, as it may not be an integer - # if normalized. - return('%s % s items' % (self.describe_limits(), self.count)) - - def istoo_high(self, value): - """Returns True if the specified value is too high for the bin. - Assumes the bin has an upper limit. - - Parameters - ---------- - value: float - The value to compare. - - Returns - ------- - bool - True if too high, else False. - """ - # value equal to limit not consideredtoo high - if(self.upper_inclusive): - result = (value > self.max_value) - else: - result = (value >= self.max_value) - - return(result) - - def ismatch(self, value): - """Indicates whether the bin matches the value. - - Parameters - ---------- - value: float - The value to compare. - - Returns - ------- - bool - True if matches, else False. - """ - return(False) # not really applicable, so always fail - - -class Histogram(object): - """Class to represent a histogram.""" - - def retrieve_count(self, bin_index): - """Returns the number of items stored in a given bin of the histogram. - - Parameters - ---------- - bin_index: int - The index to use (starts with 1). - - Returns - ------- - int - The number of items in the bin. - """ - return(self.bins[bin_index - 1].count) - - def num_items(self): - """Returns the total number of items stored in the histogram - """ - return(sum([bin.count for bin in self.bins])) - - def __str__(self): - """Returns a printed representation of the histogram. - """ - # Don't assume a type for the total, as it may not be an integer - # if normalized. - v = (len(self.bins), self.num_items()) - result = 'Histogram: % d bins, % s items\n' % v - - for bin in self.bins: - result = result + '%s\n' % (bin.__str__()) - - return(result) - - def normalize(self, total=None): - """Takes a histogram and returns a new histogram that normalizes all - its values. - - Parameters - ---------- - total: int - Number of items to divide each bin by for the normalization. - If not supplied, it defaults to the total in the histogram. - - Returns - ------- - new_histogram : Histogram - The new histogram. - """ - if (total is None): - total = self.num_items() - - new_histogram = deepcopy(self) # make a deep copy - - for bin in new_histogram.bins: - bin.count = (float(bin.count)) / total - - return(new_histogram) - - -class DiscreteHistogram(Histogram): - """Class to represent a histogram with discrete values.""" +class DiscreteHistogram(Histogram): + """Class to represent a histogram with discrete values.""" def __init__(self, values): """Initializes a histogram with discrete values. @@ -603,11 +353,6 @@ def __init__(self, values): for value in values: self.bins.append(DiscreteBin(value)) - def retrieve_values(self): - """Returns the list of bin values of a discrete histogram - """ - return([bin.bin_value for bin in self.bins]) - def retrieve_count_by_value(self, value): """Returns the count matching a certain value. If not found, return None @@ -630,6 +375,11 @@ def retrieve_count_by_value(self, value): return(result) + def retrieve_values(self): + """Returns the list of bin values of a discrete histogram + """ + return([bin.bin_value for bin in self.bins]) + def store_items(self, value, count=1): """Stores a value in the discrete histogram if it matches one of the bin values. @@ -666,167 +416,93 @@ def store_items(self, value, count=1): return(found) -class ContinuousHistogram(Histogram): - """Class to represent a histogram with continuous values.""" - - def __init__(self, boundaries, highest_inclusive=False): - """Initializes a continuous histogram. - - Default behavior with highest_inclusive = False: - Bin 0 is defined by x <= boundaries[0]. - For i > 0, bin i is defined by boundaries[i-1] < x <= boundaries[i]. - Bin n+1 is defined by x > boundaries[n-1]. +class LinearEquation(Polynomial): + """Subclass of Polynomial for linear equations. + This implementation is three times faster, so Polynomial should be reserved + for higher orders.""" - Behavior with highest_exclusive = True: - Bins below n are defined in the same way as above. - Bin n is defined by boundaries[n-2] < x < boundaries[n-1]. - Bin n+1 is defined by x >= boundaries[n-1]. + def __init__(self, coeff0, coeff1): + """Constructor for a linear equation to provide a more 'natural' + interface without using a list. Parameters ---------- - boundaries: sequence - List of numbers that separate the bins, in increasing order. - highest_inclusive: bool - True if highest bin includes the last boundary, - False (default) otherwise. + coeff0: float + Additive constant. + coeff1: float + Multiplicative coefficient. """ - self.bins = [] - self.highest_inclusive = highest_inclusive - lower_lim = None - - # A histogram is a list of bins. - # For the first bin, the lower limit is None (unbounded). - # For the last bin, the upper limit is None. - # Rely on default limits behavior in HistogramBin constructor. - - for index in range(len(boundaries)): - upper_lim = boundaries[index] - self.bins.append(RangeBin(min_value=lower_lim, - max_value=upper_lim)) - lower_lim = upper_lim - - self.bins.append(RangeBin(min_value=lower_lim)) # Add highest bin - - # If highest_inclusive is set, change limit behavior of two - # highest bins. - if (highest_inclusive): - self.bins[-2].upper_inclusive = False - self.bins[-1].lower_inclusive = True + self.coefficients = [coeff0, coeff1] - def retrieve_boundaries(self): - """Returns the list of boundaries of a continuous histogram. - """ - # Return upper limits of all bins except the last. - return([bin.max_value for bin in self.bins[:-1]]) + def apply(self, value): + """Applies a linear equation to an input value. - def store_items(self, value, count=1): - """Stores a value in the continuous histogram. + This is intended to be faster than the more general method with + Polynomial. Parameters ---------- value: float - The value to store. - count: int - Number of items with that value to store (default 1). + The evaluand. + + Returns + ------- + float + The result of the evaluated equation. """ - bin_index = 0 - found = False + return(value * self.coefficients[1] + self.coefficients[0]) - # Search all bins up to the next-highest. If value is not too high - # for the bin, store the items there. - while ((not found) and (bin_index < len(self.bins) - 1)): - bin = self.bins[bin_index] - if (not (bin.istoo_high(value))): - found = True - bin.store_items(count) +class PoissonDistribution(DiscreteHistogram): + """Class to represent a Poisson distribution.""" - bin_index += 1 + def __init__(self, mean, max_boundary): + """Constructor function for the Poisson distribution. - # The value must belong in the highest bin if not found earlier. - if (not found): - self.bins[-1].store_items(count) - - -def combine_histograms(histograms): - """Takes a list of histograms and returns a new Histogram object that sums - the values in each bin. - - All histograms in the list must be identical except for the count. - - Parameters - ---------- - histograms: sequence - A lst of Histogram objects to combine. - - Returns - ------- - Histogram - The combined histogram. - """ - # Initialize the new histogram with properties of the first histogram - # in the list. - if (isinstance(histograms[0], ContinuousHistogram)): - hist_bounds = histograms[0].retrieve_boundaries() - hist_high = histograms[0].highest_inclusive - new_histogram = ContinuousHistogram(hist_bounds, hist_high) - else: # assume discrete histogram - new_histogram = DiscreteHistogram(histograms[0].retrieve_values()) - - for bin_index in range(len(new_histogram.bins)): - total_items = sum([hist.bins[bin_index].count for hist in histograms]) - new_histogram.bins[bin_index].store_items(total_items) - - return(new_histogram) - - -def average_histograms(histograms): - """Takes a list of histogram objects and simply averages all the bin values. - - All histograms in the list must be identical except for the count. - - Parameters - ---------- - histograms: sequence - A lst of Histogram objects to combine. - - Returns - ------- - new_histogram : Histogram - The averaged histogram. - """ - # Make a copy of the first histogram in the list. - new_histogram = deepcopy(histograms[0]) + Parameters + ---------- + mean: float + Mean parameter for the Poisson distribution. + max_boundary: float + The largest parameter for which the probability is to + be computed. All values larger than max_boundary will be lumped + into the highest bin. + """ + self.mean = mean + self.bins = [] - for bin_index in range(len(new_histogram.bins)): - new_histogram.bins[bin_index].count = avg([hist.bins[bin_index].count - for hist in histograms]) + # Create discrete bins for values from 0 to max_boundary, inclusive. + for value in range(max_boundary + 1): + self.bins.append(DiscreteBin(value)) - return(new_histogram) + # Use a RangeBin object for the highest bin. + self.bins.append(RangeBin(min_value=max_boundary)) + # Now generate the distribution. + self.generate_distribution() -class PoissonDistribution(DiscreteHistogram): - """Class to represent a Poisson distribution.""" + def __str__(self): + """Inspector function for Poisson distribution.""" + poisson_info = 'PoissonDistribution: Mean: % .2f\n' % (self.mean) + generic_info = super(self.__class__, self).__str__() - def probability(self, k): - """Computes the probability that the Poisson distribution takes on - the value k. + return(poisson_info + generic_info) - Value must be a nonnegative integer. + def cumulative_probability(self, value): + """Returns the probability that a random variable will have a value no + greater than the one specified. Parameters ---------- - k: float - The value to compute. + value: float + The value between 0 and the max_boundary of the distribution. Returns ------- float The probability. """ - u = self.mean - - return((u**k * exp(-u)) / factorial(k)) + return(sum([self.bins[i].count for i in range(value + 1)])) def generate_distribution(self): """Populates a Poisson distribution up to the maximum bin. @@ -843,41 +519,25 @@ def generate_distribution(self): # remainder of distribution goes in the last bin self.bins[-1].count = 1.0 - cum - def __init__(self, mean, max_boundary): - """Constructor function for the Poisson distribution. + def probability(self, k): + """Computes the probability that the Poisson distribution takes on + the value k. + + Value must be a nonnegative integer. Parameters ---------- - mean: float - Mean parameter for the Poisson distribution. - max_boundary: float - The largest parameter for which the probability is to - be computed. All values larger than max_boundary will be lumped - into the highest bin. - """ - self.mean = mean - self.bins = [] - - # Create discrete bins for values from 0 to max_boundary, inclusive. - for value in range(max_boundary + 1): - self.bins.append(DiscreteBin(value)) - - # Use a RangeBin object for the highest bin. - self.bins.append(RangeBin(min_value=max_boundary)) - - # Now generate the distribution. - self.generate_distribution() - - def __str__(self): - """Inspector function for Poisson distribution.""" - poisson_info = 'PoissonDistribution: Mean: % .2f\n' % (self.mean) - generic_info = super(self.__class__, self).__str__() + k: float + The value to compute. - return(poisson_info + generic_info) + Returns + ------- + float + The probability. + """ + u = self.mean - def retrieve_values(self): - """Returns the list of bin values for the Poisson distribution.""" - return(list(range(len(self.bins) - 1))) # leave out last bin + return((u**k * exp(-u)) / factorial(k)) def retrieve_count_by_value(self, value): """Returns the number of items in the histogram that have the @@ -904,78 +564,203 @@ def retrieve_count_by_value(self, value): return(result) - def cumulative_probability(self, value): - """Returns the probability that a random variable will have a value no - greater than the one specified. - - Parameters - ---------- - value: float - The value between 0 and the max_boundary of the distribution. - - Returns - ------- - float - The probability. - """ - return(sum([self.bins[i].count for i in range(value + 1)])) + def retrieve_values(self): + """Returns the list of bin values for the Poisson distribution.""" + return(list(range(len(self.bins) - 1))) # leave out last bin -class StatisticalList(list): - """Numeric list class with statistical attributes.""" +class RangeBin(HistogramBin): + """Class to represent a bin with a range.""" - def __init__(self, data=None): - """Initializes a statistical list. + def __init__(self, min_value=None, max_value=None, lower_inclusive=False, + upper_inclusive=True): + """Constructor for a range bin. Parameters ---------- - data: sequence - List of inputs to list. + min_value: float + Minimum value for the bin. + max_value: float + Maximum value for the bin. + lower_inclusive: bool + True if min_value is inclusive, else False (default). + upper_inclusive: bool + True if max_value is inclusive, else False (default). """ - # If data were provided, copy into the list. - if (data is not None): - for i in range(len(data)): - self.append(data[i]) + self.min_value = min_value + self.max_value = max_value + self.lower_inclusive = lower_inclusive + self.upper_inclusive = upper_inclusive + self.count = 0 - def compute_variance(self): - """Computes the variance of a statistical list. + def __str__(self): + """Returns a printed representation of the bin """ - mean = self.mean + # Don't assume a type for the count, as it may not be an integer + # if normalized. + return('%s % s items' % (self.describe_limits(), self.count)) - # Variance is defined as the sum of the squares of the differences - # between each data point and the mean, divided by the number of - # degrees of freedom. For now assume the list is a sample, so - # DOF = n-1. - return(sum([(n - mean)**2 for n in self]) / (len(self) - 1)) + def describe_limits(self, precision=2): + """Returns a printed representation of the limits of the bin. - def compute_rms(self): - """Computes the rms value of a statistical list. + Parameters + ---------- + precision: int + Number of digits to print after the decimal point. + + Returns + ------- + str + The limits of the bin. """ - # Simply sum the squares, divide by n, and take the square root. - return(sqrt(avg([n**2 for n in self]))) + if(self.min_value is None): + if(self.upper_inclusive): + result = '<= % . * f:' % (precision, self.max_value) + else: + result = '< % . * f:' % (precision, self.max_value) + elif(self.max_value is None): + if(self.lower_inclusive): + result = '>= % . * f:' % (precision, self.min_value) + else: + result = '> % . * f:' % (precision, self.min_value) + else: + v = (precision, self.min_value, precision, self.max_value) + result = '%. * f to % . * f:' % v - def compute_statistics(self, min_value=None, max_value=None, - max_bins=None): - """Computes statistics for a StatisticalList object; must contain at - least one element. + return(result) + + def ismatch(self, value): + """Indicates whether the bin matches the value. Parameters ---------- - min_value: float - Minimum value for cutoff of histogram (defaults to - minimum in list). - max_value: float - Maximum value for cutoff of histogram (defaults to - maximum in list). - max_bins: int - Maximum number of bins in histogram. + value: float + The value to compare. + + Returns + ------- + bool + True if matches, else False. """ - # first sort the list in increasing order -- note this is destructive - self.sort() + return(False) # not really applicable, so always fail - # Compute min, max, mean, median, variance, standard deviation, - # and rms value. - num_elements = len(self) + def istoo_high(self, value): + """Returns True if the specified value is too high for the bin. + Assumes the bin has an upper limit. + + Parameters + ---------- + value: float + The value to compare. + + Returns + ------- + bool + True if too high, else False. + """ + # value equal to limit not consideredtoo high + if(self.upper_inclusive): + result = (value > self.max_value) + else: + result = (value >= self.max_value) + + return(result) + + +class Square(Rectangle): + """Class to represent a square.""" + + def __init__(self, side): + """Initialize a square with a specified side length. + + Parameters + ---------- + side: float + The length of the square side. + """ + self.side = side + + # call superclass method with length and width + super(self.__class__, self).__init__(side, side) + + def __str__(self): + """Inspector method for the square. + """ + return('Square: side = % .2f' % (self.side)) + + def inner_area(self, excluded_width): + """Returns the area of the square after removing a strip of specified + width along each edge. + + Parameters + ---------- + excluded_width: float + The width of the strip to remove. + + Returns + ------- + float + The area. + """ + # Return the area of a square that is reduced in side length by twice + # the specified width, because both sides are affected. + return(Square(self.side - 2 * excluded_width).area()) + + +class StatisticalList(list): + """Numeric list class with statistical attributes.""" + + def __init__(self, data=None): + """Initializes a statistical list. + + Parameters + ---------- + data: sequence + List of inputs to list. + """ + # If data were provided, copy into the list. + if (data is not None): + for i in range(len(data)): + self.append(data[i]) + + def __str__(self): + """Prints data on a statistical list after statistics are generated.""" + vals = [len(self), self.min, self.max, self.mean, self.median] + vals += [self.variance, self.stdev, self.rms_value] + string = """StatisticalList: % d elements, min = % .4f, max = % .4f, + mean = % .4f, median = % .4f, variance = % .4f, + stdev = % .4f, rms = % .4f""" % vals + + return string + + def compute_rms(self): + """Computes the rms value of a statistical list. + """ + # Simply sum the squares, divide by n, and take the square root. + return(sqrt(avg([n**2 for n in self]))) + + def compute_statistics(self, min_value=None, max_value=None, + max_bins=None): + """Computes statistics for a StatisticalList object; must contain at + least one element. + + Parameters + ---------- + min_value: float + Minimum value for cutoff of histogram (defaults to + minimum in list). + max_value: float + Maximum value for cutoff of histogram (defaults to + maximum in list). + max_bins: int + Maximum number of bins in histogram. + """ + # first sort the list in increasing order -- note this is destructive + self.sort() + + # Compute min, max, mean, median, variance, standard deviation, + # and rms value. + num_elements = len(self) self.min = self[0] self.max = self[-1] self.mean = avg(self) @@ -1031,129 +816,345 @@ def compute_statistics(self, min_value=None, max_value=None, for value in self: self.histogram.store_items(value) - def __str__(self): - """Prints data on a statistical list after statistics are generated.""" - vals = [len(self), self.min, self.max, self.mean, self.median] - vals += [self.variance, self.stdev, self.rms_value] - string = """StatisticalList: % d elements, min = % .4f, max = % .4f, - mean = % .4f, median = % .4f, variance = % .4f, - stdev = % .4f, rms = % .4f""" % vals + def compute_variance(self): + """Computes the variance of a statistical list. + """ + mean = self.mean - return string + # Variance is defined as the sum of the squares of the differences + # between each data point and the mean, divided by the number of + # degrees of freedom. For now assume the list is a sample, so + # DOF = n-1. + return(sum([(n - mean)**2 for n in self]) / (len(self) - 1)) -class Circle(object): - """Class to represent a circle.""" +def acos2(val): + """Safe version of acos that handles invalid arguments in the same way as + asin2 - def __init__(self, radius): - """Initialize a circle with a specified radius. + Parameters + ---------- + val: float + The evaluand. - Parameters - ---------- - radius: float - The radius of the circle. - """ - self.radius = radius + Returns + ------- + float + The arccos of the value. + """ + return(acos(max(-1.0, min(1.0, val)))) - def __str__(self): - """Inspector method for the circle. - """ - return('Circle: radius = % .2f' % (self.radius)) - def area(self): - """Returns the area of the circle. - """ - return(pi * self.radius**2) +def asin2(val): + """Safe version of asin that handles invalid arguments. + Arguments greater than 1 are truncated to 1; arguments less than -1 are + set to -1. -class Rectangle(object): - """Class to represent a rectangle.""" + Parameters + ---------- + val: float + The evaluand. - def __init__(self, length, width): - """Initialize a rectangle with a specified length and width. + Returns + ------- + float + The arcsin of the value. + """ + return(asin(max(-1.0, min(1.0, val)))) - Parameters - ---------- - length: float - The length of the rectangle. - width: float - The width of the rectangle. - """ - self.length = length - self.width = width - def __str__(self): - """Inspector method for the rectangle. - """ - dims = (self.length, self.width) - return('Rectangle: length = % .2f, width = % .2f' % dims) +def atan2d(x): + """Return the arctan in degrees - def area(self): - """Returns the area of the rectangle. - """ - return(self.length * self.width) + Parameters + ---------- + x: float + The evaluand. - def motion_tolerant_area(self, motion_length, motion_angle): - """Returns the area within a rectangle that can tolerate a motion in - a known direction while remaining within the rectangle. + Returns + ------- + float + The arctan of x in degrees. + """ + return atan2(radians(x)) - Parameters - ---------- - motion_length: float - Distance of motion (same units as rectangle length and width). - motion_angle: float - Angle in radians between the direction of motion and long - direction of rectangle. - Returns - ------- - float - The area. - """ - # Compute the x and y distances to the edge. A position - # within the rectangle is only motion-tolerant if it exceeds - # both of these distances. The effective length is thus reduced - # by delta_x and the effective width by delta_y. - delta_x = motion_length * cos(motion_angle) # lengthwise direction - delta_y = motion_length * sin(motion_angle) - return ((self.length - delta_x) * (self.width - delta_y)) +def avg(numbers): + """Returns the average of a list of numbers + Parameters + ---------- + numbers: sequence + The list of numbers. -class Square(Rectangle): - """Class to represent a square.""" + Returns + ------- + float + The average. + """ + return(sum(numbers) / float(len(numbers))) - def __init__(self, side): - """Initialize a square with a specified side length. - Parameters - ---------- - side: float - The length of the square side. - """ - self.side = side +def avg2(num1, num2): + """Returns the average of two numbers - # call superclass method with length and width - super(self.__class__, self).__init__(side, side) + Parameters + ---------- + num1: float + The first number. + num2: float + The second number. - def __str__(self): - """Inspector method for the square. - """ - return('Square: side = % .2f' % (self.side)) + Returns + ------- + float + The average. + """ + return((num1 + num2) / 2.0) - def inner_area(self, excluded_width): - """Returns the area of the square after removing a strip of specified - width along each edge. - Parameters - ---------- - excluded_width: float - The width of the strip to remove. +def combine_histograms(histograms): + """Takes a list of histograms and returns a new Histogram object that sums + the values in each bin. - Returns - ------- - float - The area. - """ - # Return the area of a square that is reduced in side length by twice - # the specified width, because both sides are affected. - return(Square(self.side - 2 * excluded_width).area()) + All histograms in the list must be identical except for the count. + + Parameters + ---------- + histograms: sequence + A lst of Histogram objects to combine. + + Returns + ------- + Histogram + The combined histogram. + """ + # Initialize the new histogram with properties of the first histogram + # in the list. + if (isinstance(histograms[0], ContinuousHistogram)): + hist_bounds = histograms[0].retrieve_boundaries() + hist_high = histograms[0].highest_inclusive + new_histogram = ContinuousHistogram(hist_bounds, hist_high) + else: # assume discrete histogram + new_histogram = DiscreteHistogram(histograms[0].retrieve_values()) + + for bin_index in range(len(new_histogram.bins)): + total_items = sum([hist.bins[bin_index].count for hist in histograms]) + new_histogram.bins[bin_index].store_items(total_items) + + return(new_histogram) + + +def conditional_probability(p_joint, p_B): + """Returns probability of event A given event B. + + Parameters + ---------- + p_joint: float + P(A,B). + p_B: float + Probability of event B. + + Returns + ------- + float + The probability. + """ + return(p_joint / p_B) + + +def cosd(x): + """Return the cos in degrees + + Parameters + ---------- + x: float + The evaluand. + + Returns + ------- + float + The cos of x in degrees. + """ + return cos(radians(x)) + + +def factorial(num): + """Returns the factorial of a nonnegative integer. + This function is provided in math module starting with Python 2.6, + but implement anyway for compatibility with older systems. + + Parameters + ---------- + num: int + The number to factorialize. + + Returns + ------- + int + The factorial. + """ + result = 1 # factorial of 0 is defined as 1 + + for i in range(2, num + 1): + result = result * i + return(result) + + +def output_as_percentage(num, fractional_digits=1): + """Output a percentage neatly. + + Parameters + ---------- + num: float + The number to make into a percentage. + fractional_digits: int + Number of digits to output as fractions of a percent. + If not supplied, there is no reduction in precision. + + Returns + ------- + str + The percentage. + """ + if (fractional_digits is not None): + format_str = '%%.%.df' % (fractional_digits) # creates format string + else: + format_str = '%f' + + return('%s%%' % (format_str % (num))) + + +def percent_str(num, fractional_digits=1): + """Output a number as a percentage. + + Parameters + ---------- + num: float + The number to make into a percentage. + fractional_digits: int + Number of digits to output as fractions of a percent. + If not supplied, there is no reduction in precision. + + Returns + ------- + str + The percentage. + """ + return(output_as_percentage(100 * num, fractional_digits)) + + +def really_greater_than(x, y): + """Safe greater-than function that returns true if and only if x is + "significantly" greater than y + + Parameters + ---------- + x: float + The first number. + y: float + The second number. + + Returns + ------- + bool + True if x is greater, else False. + """ + return(x > y + EPSILON) + + +def really_less_than(x, y): + """Safe less-than function that returns true if and only if x is + "significantly" less than y. + + Parameters + ---------- + x: float + The first number. + y: float + The second number. + + Returns + ------- + bool + True if x is less, else False. + """ + return(x < y - EPSILON) + + +def sind(x): + """Return the sin in degrees. + + Parameters + ---------- + x: float + The evaluand. + + Returns + ------- + float + The sin of x in degrees. + """ + return sin(radians(x)) + + +def stdev(numbers): + """Standard deviation of a list of numbers that represent sample values + + Parameters + ---------- + numbers: sequence + The list to take the standard deviation of. + + Returns + ------- + float + The standard deviation. + """ + # Simply take the square root of the sample variance. + return(sqrt(variance(numbers))) + + +def variance(numbers): + """Variance of a list of numbers that represent sample values + + Parameters + ---------- + numbers: sequence + The list to take the variance of. + + Returns + ------- + float + The variance. + """ + # Returns the sample variance (n-1 formula). + mean = avg(numbers) + sumsq = sum([(i - mean)**2 for i in numbers]) + return(sumsq / float(len(numbers) - 1)) + + +def average_histograms(histograms): + """Takes a list of histogram objects and simply averages all the bin values. + + All histograms in the list must be identical except for the count. + + Parameters + ---------- + histograms: sequence + A lst of Histogram objects to combine. + + Returns + ------- + new_histogram : Histogram + The averaged histogram. + """ + # Make a copy of the first histogram in the list. + new_histogram = deepcopy(histograms[0]) + + for bin_index in range(len(new_histogram.bins)): + new_histogram.bins[bin_index].count = avg([hist.bins[bin_index].count + for hist in histograms]) + + return(new_histogram) diff --git a/exoctk/contam_visibility/miniTools.py b/exoctk/contam_visibility/miniTools.py index f155675b..7aaadc8b 100644 --- a/exoctk/contam_visibility/miniTools.py +++ b/exoctk/contam_visibility/miniTools.py @@ -13,18 +13,17 @@ Jennifer V. Medina, 2020 """ + import astropy.coordinates as crd +from astropy.io import fits import astropy.units as u +from astroquery.irsa import Irsa +from matplotlib import cm +from matplotlib.backends.backend_pdf import PdfPages import matplotlib.pyplot as plt import numpy as np import os import pysiaf - -from astropy.io import fits -from astroquery.irsa import Irsa -from matplotlib import cm -from matplotlib.backends.backend_pdf import FigureCanvasPdf, PdfPages -from matplotlib.figure import Figure from scipy.io import readsav EXOCTK_DATA = os.environ.get('EXOCTK_DATA') @@ -40,70 +39,6 @@ TRACES_PATH = os.path.join(EXOCTK_DATA, 'exoctk_contam', 'traces') -def plotTemps(TEMPS, allRA, allDEC): - """ The stars' colors in the plot will be a function of effective stellar - temperatures when plotting with this function. """ - - # Getting the color palette - colors = cm.get_cmap('viridis', len(TEMPS)) - colors_0 = np.asarray(colors.colors) - - # Assigning index arrays to TEMPS array - i = TEMPS.argsort() - ii = TEMPS.argsort().argsort() - - # Matching the colors to the corresponding magnitude - colors = colors_0 - starsx, starsy = allRA[i], allDEC[i] - - plt.style.use('dark_background') - for x, y, c in zip(starsx, starsy, colors): - plt.scatter( - x, - y, - marker='*', - s=150, - color=c, - picker=True, - lw=0.5, - edgecolor='white') - - # Colorbar - sm = plt.cm.ScalarMappable(cmap=plt.cm.viridis, - norm=plt.Normalize(vmin=TEMPS.min(), - vmax=TEMPS.max())) - sm._A = [] - - cbar = plt.colorbar(sm, fraction=0.046, pad=0.04) - cbar.set_label('effective Temperature (K)', fontsize=20) - - -def traceLength(inst): - """ For fine-tuning the trace lengths in the contamVerify output figures """ - - # Getting example trace to calculate rough estimate of trace lengths - if 'NIRCam' in inst: - FILE = 'rot_o1_6000.0.fits' - elif 'MIRI' in inst: - FILE = 'LOWbg_6000.0.fits' - elif 'NIRISS' in inst: - FILE = 'modelOrder12_teff6000.sav' - - trFile = os.path.join(TRACES_PATH, inst.replace(' ', '_'), FILE) - trData = readsav(trFile)['modelo12'] if 'NIRISS' in inst \ - else fits.getdata(trFile, 1) - trData = trData[0] - print(np.shape(trData)) - ax = 1 if 'NIRCam' in inst else 0 - peak = trData.max() - - # the length of the trace - targ_trace_start = np.where(trData > 0.0001 * peak)[ax].min() - targ_trace_stop = np.where(trData > 0.0001 * peak)[ax].max() - - return targ_trace_start, targ_trace_stop - - def contamVerify(RA, DEC, INSTRUMENT, APAlist, binComp=[], PDF='', web=False): """ Generates a PDF file of figures displaying a simulation of the science image for any given observation using the parameters provided. @@ -194,11 +129,6 @@ def contamVerify(RA, DEC, INSTRUMENT, APAlist, binComp=[], PDF='', web=False): # Restoring model parameters modelParam = readsav(os.path.join(TRACES_PATH, 'NIRISS', 'modelsInfo.sav'), verbose=False) - models = modelParam['models'] - modelPadX = modelParam['modelpadx'] - modelPadY = modelParam['modelpady'] - dimXmod = modelParam['dimxmod'] - dimYmod = modelParam['dimymod'] jhMod = modelParam['jhmod'] hkMod = modelParam['hkmod'] teffMod = modelParam['teffmod'] @@ -245,8 +175,6 @@ def contamVerify(RA, DEC, INSTRUMENT, APAlist, binComp=[], PDF='', web=False): v2targ, v3targ = aper.det_to_tel(xSweet, ySweet) - contam = {} - if not web: filename = 'contam_{}_{}_{}.pdf'.format(RA, DEC, INSTRUMENT) defaultPDF = os.path.join(os.getcwd(), filename).replace(' ', '_') @@ -344,16 +272,7 @@ def contamVerify(RA, DEC, INSTRUMENT, APAlist, binComp=[], PDF='', web=False): XSCI[targetIndex])), str( round( YSCI[targetIndex])) - plt.title( - 'The FOV in SCIENCE coordinates at APA {}$^o$'.format( - str(APA)) + - '\n' + - '{}'.format(aperstr) + - '\n' + - 'Target (X,Y): {}, {}'.format( - tx, - ty), - fontsize=20) + plt.title('The FOV in SCIENCE coordinates at APA {}$^o$'.format(str(APA)) + '\n' + '{}'.format(aperstr) + '\n' + 'Target (X,Y): {}, {}'.format(tx, ty), fontsize=20) # Adding to PDF pdfobj.savefig(fig, bbox_inches='tight') @@ -362,3 +281,66 @@ def contamVerify(RA, DEC, INSTRUMENT, APAlist, binComp=[], PDF='', web=False): if web: return PDF + + +def plotTemps(TEMPS, allRA, allDEC): + """ The stars' colors in the plot will be a function of effective stellar + temperatures when plotting with this function. """ + + # Getting the color palette + colors = cm.get_cmap('viridis', len(TEMPS)) + colors_0 = np.asarray(colors.colors) + + # Assigning index arrays to TEMPS array + i = TEMPS.argsort() + + # Matching the colors to the corresponding magnitude + colors = colors_0 + starsx, starsy = allRA[i], allDEC[i] + + plt.style.use('dark_background') + for x, y, c in zip(starsx, starsy, colors): + plt.scatter( + x, + y, + marker='*', + s=150, + color=c, + picker=True, + lw=0.5, + edgecolor='white') + + # Colorbar + sm = plt.cm.ScalarMappable(cmap=plt.cm.viridis, + norm=plt.Normalize(vmin=TEMPS.min(), + vmax=TEMPS.max())) + sm._A = [] + + cbar = plt.colorbar(sm, fraction=0.046, pad=0.04) + cbar.set_label('effective Temperature (K)', fontsize=20) + + +def traceLength(inst): + """ For fine-tuning the trace lengths in the contamVerify output figures """ + + # Getting example trace to calculate rough estimate of trace lengths + if 'NIRCam' in inst: + FILE = 'rot_o1_6000.0.fits' + elif 'MIRI' in inst: + FILE = 'LOWbg_6000.0.fits' + elif 'NIRISS' in inst: + FILE = 'modelOrder12_teff6000.sav' + + trFile = os.path.join(TRACES_PATH, inst.replace(' ', '_'), FILE) + trData = readsav(trFile)['modelo12'] if 'NIRISS' in inst \ + else fits.getdata(trFile, 1) + trData = trData[0] + print(np.shape(trData)) + ax = 1 if 'NIRCam' in inst else 0 + peak = trData.max() + + # the length of the trace + targ_trace_start = np.where(trData > 0.0001 * peak)[ax].min() + targ_trace_stop = np.where(trData > 0.0001 * peak)[ax].max() + + return targ_trace_start, targ_trace_stop diff --git a/exoctk/contam_visibility/quaternionx.py b/exoctk/contam_visibility/quaternionx.py index 9056e077..b61a05ec 100755 --- a/exoctk/contam_visibility/quaternionx.py +++ b/exoctk/contam_visibility/quaternionx.py @@ -37,1533 +37,1606 @@ PI2 = 2. * pi -class GalacticPole: - """Represents coordinates of galactic pole.""" +class Vector: + "Class to encapsulate vector data and operations." - def __init__(self, latitude, longitude, ascending_node): - """Initializes the coordinates of the galactic pole. + def __add__(self, rs): + """Implements Vector + Vector Parameters ---------- - latitude: float - Latitude of pole, in degrees. - longitude: float - Longitude of pole, in degrees. - ascending_node: float - Ascending node of pole, in degrees. - """ - # Arguments specified in degrees, but values represented in radians. - self.latitude = radians(latitude) - self.longitude = radians(longitude) - self.anode = radians(ascending_node) - - def __str__(self): - """Returns string representation of the galactic pole.""" - text = (degrees(self.latitude), degrees(self.longitude), - degrees(self.anode)) - # Convert attributes back into degrees for readability. - return """GalacticPole: latitude: %.3fD, longitude: %.3fD,\ - anode: %.3fD""" % text + rs: float + The scalar to add. + Returns + ------- + Vector + The resultant vector. + """ + x = self.x + rs.x + y = self.y + rs.y + z = self.z + rs.z + return (Vector(x, y, z)) -# supports transformation to galactic coordinates -NGP = GalacticPole(192.859508, 27.128336, 32.932) + def __iadd__(self, rs): + """Implements Vector += vector. + Parameters + ---------- + rs: float + The scalar to add. -def QX(angle): - """Creates rotation quaternion about X axis, rotates a vector about - this axis. + Returns + ------- + Vector + The resultant vector. + """ + self.x += rs.x + self.y += rs.y + self.z += rs.z + return (self) - Parameters - ---------- - angle: float - The angle to rotate by. + def __idiv__(self, rs): + """Implements Vector /= float. - Result - ------ - Quarternion - The rotated quaternion. - """ - return Quaternion(Vector(sin(angle / 2.), 0., 0.), cos(angle / 2.)) + Parameters + ---------- + rs: float + The scalar to divide. + Returns + ------- + Vector + The resultant vector. + """ + self.x /= rs + self.y /= rs + self.z /= rs + return (self) -def QY(angle): - """Creates rotation quaternion about Y axis, rotates a vector about - this axis. + def __imul__(self, rs): + """Implements Vector *= float - Parameters - ---------- - angle: float - The angle to rotate by. + Parameters + ---------- + rs: float + The scalar to multiply. - Result - ------ - Quarternion - The rotated quaternion. - """ - return Quaternion(Vector(0., sin(angle / 2.), 0.), cos(angle / 2.)) + Returns + ------- + Vector + The resultant vector. + """ + self.x *= rs + self.y *= rs + self.z *= rs + return (self) + def __init__(self, x=0.0, y=0.0, z=0.0): + """Constructor for a three-dimensional vector. -def QZ(angle): - """Creates rotation quaternion about Z axis, rotates a vector about - this axis + Note that two-dimensional vectors can be constructed by omitting one + of the coordinates, which will default to 0. - Parameters - ---------- - angle: float - The angle to rotate by. + Parameters + ---------- + x: float + The x coordinate. + y: float + The y coordinate. + z: float + The z coordinate. + """ + self.x = x # Cartesian x coordinate + self.y = y # Cartesian y coordinate + self.z = z # Cartesian z coordinate - Result - ------ - Quarternion - The rotated quaternion. - """ - return Quaternion(Vector(0., 0., sin(angle / 2.)), cos(angle / 2.)) + def __isub__(self, rs): + """Implements Vector -= vector. + Parameters + ---------- + rs: float + The scalar to subtract. -def Qmake_a_point(V): - """Creates a pure Q, i.e. defines a pointing not a rotation + Returns + ------- + Vector + The resultant vector. + """ + self.x -= rs.x + self.y -= rs.y + self.z -= rs.z + return (self) - Parameters - ---------- - V: Vector - The vector. + def __mul__(self, rs): + """Implements Vector * scalar. Can then use '*' syntax in multiplying + a vector by a scalar rs - Returns - ------- - Quaternion - The point as a quaternion. - """ - return Quaternion(V, 0.) + Parameters + ---------- + rs: float + The scalar to multiply. + Returns + ------- + Vector + The resultant vector. + """ + x = self.x * rs + y = self.y * rs + z = self.z * rs + return (Vector(x, y, z)) -def cvt_pt_Q_to_V(Q): - """Converts a pure (pointing) Q to a unit position Vector + def __rmul__(self, ls): + """Implements float * Vector. - Parameters - ---------- - Q: Quaternion - The quaternion to convert to a Vector. + Parameters + ---------- + ls: float + The scalar to multiply. - Returns - ------- - Vector - The point as a vector. - """ - return Vector(Q.q1, Q.q2, Q.q3) + Returns + ------- + Vector + The resultant vector. + """ + x = self.x * ls + y = self.y * ls + z = self.z * ls + return (Vector(x, y, z)) + def __str__(self): + """Returns a string representation of the vector.""" + return('Vector: x: %.3f, y: %.3f, z: %.3f' % (self.x, self.y, self.z)) -# The following functions are dependent upon the spacecraft definitions -# and perhaps should be moved to that module -def Qmake_body2inertial(coord1, coord2, V3pa): - """Creates a rotation Q, going from the body frame to inertial. + def __sub__(self, rs): + """Implements Vector - Vector. - Parameters - ---------- - coord1: float - The first coordinate. - coord2: float - The second coordinate. - V3pa: float - The V3 position. + Parameters + ---------- + rs: float + The scalar to subtract. - Returns - ------- - Quaternion - The rotation quaternion - """ - return QZ(coord1) * QY(-coord2) * QX(-V3pa) + Returns + ------- + Vector + The resultant vector. + """ + x = self.x - rs.x + y = self.y - rs.y + z = self.z - rs.z + return (Vector(x, y, z)) + def __truediv__(self, rs): + """Implements Vector / float. -def Qmake_v2v3_2body(v2, v3): - """Creates a rotation Q, going from v2 and v3 in the body frame to - inertial. + Parameters + ---------- + rs: float + The scalar to divide. - Parameters - ---------- - v2: float - The V2 position. - V3: float - The V3 position. + Returns + ------- + Vector + The resultant vector. + """ + x = self.x / rs + y = self.y / rs + z = self.z / rs + return (Vector(x, y, z)) - Returns - ------- - Quaternion - The rotation quaternion. - """ - return QY(v3) * QZ(-v2) + # Replace by separation - RLH + def angle(self, V2): + """Returns angle between the two vectors in degrees. + Parameters + ---------- + V2: Vector + The vector to measure. -def Qmake_v2v3_2inertial(coord1, coord2, V3pa, v2, v3): - """Creates a rotation Q, going from v2 and v3 in the body frame to - inertial + Returns + ------- + float + The angle between the two vectors. + """ + R1 = self.length() + R2 = V2.length() + adot = dot(self, V2) + adot = adot / R1 / R2 + adot = min(1., adot) + adot = max(-1., adot) + return math2.acosd(adot) - Parameters - ---------- - coord1: float - The first coordinate. - coord2: float - The second coordinate. - V3pa: float - The V3 position. - v2: float - The V2 position. - v3: float - The V3 position. + def create_matrix(self): + """Converts a Vector into a single-column matrix.""" - Returns - ------- - Quaternion - The rotation quaternion. - """ - return QZ(coord1) * QY(-coord2) * QX(-V3pa) * QY(v3) * QZ(-v2) + column = [self.x, self.y, self.z] + return(Matrix([[element] for element in column])) # singleton list + # Recommend deletion in favor of non-method version. + def cross(self, V1, V2): + """returns cross product of two vectors -def Qmake_aperture2inertial(coord1, coord2, APA, xoff, yoff, s, YapPA, - V3ref, V2ref): - """Creates a rotation Q, going from the target in aperture frame to body. + Parameters + ---------- + V1: Vector + The vector to cross. + V2: Vector + The vector to cross. - Parameters - ---------- - coord1: float - The first coordinate. - coord2: float - The second coordinate. - APA: float - The apature position. - xoff: float - The x offset. - yoff: float - The y offset. - s: float - The multiplicative factor. - V2ref: float - The V2 position. - V3ref: float - The V3 position. + Returns + ------- + Vector + The resultant vector. + """ + x = self.y * V2.z - V1.z * V2.y + y = self.z * V2.x - V1.x * V2.z + z = self.x * V2.y - V1.y * V2.x + return Vector(x, y, z) - Returns - ------- - Quaternion - The rotation quaternion. - """ - term1 = QZ(coord1) * QY(-coord2) * QX(-APA) * QY(-yoff) - term2 = QZ(s * xoff) * QX(YapPA) * QY(V3ref) * QZ(-V2ref) - return term1 * term2 + # RLH: Suggest deletion in favor of __str__, which has the advantage + # that it is called on print. + def display(self): + """Print the values""" + return "[%f, %f, %f]" % (self.x, self.y, self.z) + # Recommend deletion -- better to use a single interface that takes + # two vectors. + def dot(self, V2): + """returns dot product between two vectors. -def cvt_body2inertial_Q_to_c1c2pa_tuple(Q): - """Creates a angle tuple from Q, assuming body frame to inertial Q and - 321 rotation sequence. + Parameters + ---------- + V2: Vector + The vector to dot. - Parameters - ---------- - Q: Quaternion - The quaternion. + Returns + ------- + Vector + The resultant vector. + """ + return self.x * V2.x + self.y * V2.y + self.z * V2.z - Returns - ------- - coord1 : float - The first coordinate. - coord2 : float - The second coordinate. - pa : float - The poosition angle. - """ - # Conversion from Euler symmetric parameters to matrix elements and - # matrix elements to rotation angles is given in Isaac's papers - r11 = Q.q1 * Q.q1 - Q.q2 * Q.q2 - Q.q3 * Q.q3 + Q.q4 * Q.q4 - r21 = 2. * (Q.q1 * Q.q2 + Q.q3 * Q.q4) - r31 = 2. * (Q.q1 * Q.q3 - Q.q2 * Q.q4) - r32 = 2. * (Q.q2 * Q.q3 + Q.q1 * Q.q4) - r33 = -Q.q1 * Q.q1 - Q.q2 * Q.q2 + Q.q3 * Q.q3 + Q.q4 * Q.q4 - coord1 = atan2(r21, r11) - if coord1 < 0.: - coord1 += PI2 - coord2 = math2.asin2(r31) # use "safe" version of sine - pa = atan2(-r32, r33) - if pa < 0.: - pa += PI2 - return coord1, coord2, pa + def length(self): + """Returns magnitude of the vector """ + return(sqrt(self.x * self.x + self.y * self.y + self.z * self.z)) + def normalize(self): + """Returns copy of the normalized vector """ + mag = self.length() + return (Vector(self.x / mag, self.y / mag, self.z / mag)) -def cvt_v2v3_using_body2inertial_Q_to_c1c2pa_tuple(Q, v2, v3): - """Given Q and v2, v3 gives pos on sky and V3 PA. + # RLH: What do these add? We're creating methods just to access + # individual attributes. + def rx(self): + """The magnitude of x""" + return self.x - Parameters - ---------- - Q: Quaternion - The quaternion. - v2: float - The V2 position. - v3: float - The V3 position. + def ry(self): + """The magnitude of y""" + return self.y - Returns - ------- - tuple - The coordinates and position angle - """ - Vp_body = Vector(0., 0., 0.) - Vp_body.set_xyz_from_angs(v2, v3) - Vp_eci_pt = Q.cnvrt(Vp_body) - coord1 = atan2(Vp_eci_pt.y, Vp_eci_pt.x) - if coord1 < 0.: - coord1 += PI2 - coord2 = asin(unit_limit(Vp_eci_pt.z)) + def rz(self): + """The magnitude of z""" + return self.z - V3_body = Vector(0., 0., 1.) - V3_eci_pt = Q.cnvrt(V3_body) - NP_eci = Vector(0., 0., 1.) - V_left = cross(NP_eci, Vp_eci_pt) - if V_left.length() > 0.: - V_left = V_left / V_left.length() - NP_in_plane = cross(Vp_eci_pt, V_left) - x = dot(V3_eci_pt, NP_in_plane) - y = dot(V3_eci_pt, V_left) - pa = atan2(y, x) - if pa < 0.: - pa += PI2 + def set_eq(self, x=None, y=None, z=None): + """Assigns new value to vector. - return coord1, coord2, pa + Arguments are now optional to permit this to be used with 2D vectors + or to modify any subset of coordinates. + + Parameters + ---------- + x: float + The x coordinate. + y: float + The y coordinate. + z: float + The z coordinate. + """ + if x is not None: + self.x = x + if y is not None: + self.y = y + if z is not None: + self.z = z + # RLH: Not necessary if CelestialVector is used. + def set_xyz(self, ra, dec): + """Creates a unit vector from spherical coordinates -def cvt_c1c2_using_body2inertial_Q_to_v2v3pa_tuple(Q, coord1, coord2): - """Given Q and a position, returns v2, v3, V3PA tuple + Parameters + ---------- + ra: float + The right ascension. + dec: float + The declination. + """ + self.x = math2.cosd(dec) * math2.cosd(ra) + self.y = math2.cosd(dec) * math2.sind(ra) + self.z = math2.sind(dec) - Parameters - ---------- - Q: Quaternion - The quaternion. - coord1: float - The first coordinate. - coord2: float - The second coordinate - Returns - ------- - coord1 : float - The first coordinate. - coord2 : float - The second coordinate. - pa : float - The poosition angle. - """ - Vp_eci = Vector(1., 0., 0.) - Vp_eci.set_xyz_from_angs(coord1, coord2) - Vp_body_pt = Q.inv_cnvrt(Vp_eci) - v2 = atan2(Vp_body_pt.y, Vp_body_pt.x) - v3 = asin(unit_limit(Vp_body_pt.z)) - V3_body = Vector(0., 0., 1.) - V3_eci_pt = Q.cnvrt(V3_body) - NP_eci = Vector(0., 0., 1.) - V_left = cross(NP_eci, Vp_eci) - if V_left.length() > 0.: - V_left = V_left / V_left.length() - NP_in_plane = cross(Vp_eci, V_left) - x = dot(V3_eci_pt, NP_in_plane) - y = dot(V3_eci_pt, V_left) - pa = atan2(y, x) - if pa < 0.: - pa += PI2 - return v2, v3, pa +class CelestialVector (Vector): + "Class to encapsulate a unit vector on the celestial sphere." + def __init__(self, ra=0.0, dec=0.0, frame='eq', degrees=True): + """Constructor for a celestial vector. -class Quaternion: - """This representation is used by Wertz and Markley""" + There are two spherical coordinates, a longitudinal coordinate (called + right ascension), and a latitudinal coordinate (called declination). + The RA is defined as the counterclockwise angle from a reference + direction on the equatorial plane; it ranges from 0-360 degrees. + The DEC is the angle between the vector and the equatorial plane; + it ranges from -90 to 90 degrees. Angles are specified in degrees but + represented internally as radians. - def __init__(self, V, q4): - """Quaternion constructor. + The frame attribute indicates the coordinate frame of the vector, + which may be 'eq' (equatorial, default), 'ec' (ecliptic), or 'gal' + (galactic). In equatorial coordinates, the equatorial plane is the + celestial equator (extension of the Earth's equator) and the reference + axis is the vernal equinox. In ecliptic coordiantes, the equatorial + plane is the ecliptic (the Earth's orbital plane) and the reference + axis is usually defined relative to the Sun. In galactic coordinates, + the equatorial plane is the plane of the Galaxy. + + The degrees attribute should be True if the RA, DEC inputs are in + degrees. Otherwise radians is assumed. + + The coordinates "ra" and "dec" may be used in all three systems. + Other names for coordinates in different frames may be defined for + clarity. + + A CelestialVector is also an ordinary unit vector, with Cartesian + coordinates defined relative to the equatorial plane. Parameters ---------- - V: Vector - The vector to construct the quaternion with. - q4: Vector - The fourth vector. + ra: float + The right ascension. + dec: float + The declination. + frame: str + The frame to use. + degrees: bool + Use degrees. """ - self.q1 = V.x - self.q2 = V.y - self.q3 = V.z - self.q4 = q4 + if (degrees): + ra = math2.D2R * ra + dec = math2.D2R * dec - def __str__(self): - """Returns a string representation of the quaternion.""" - text = (self.q1, self.q2, self.q3, self.q4) - return 'Quaternion: q1: %.3f, q2: %.3f, q3: %.3f, q4: %.3f' % text + self.ra = ra + self.dec = dec + self.frame = frame - def length(self): - """Returns length of the Q """ - a = self.q1 * self.q1 - b = self.q2 * self.q2 - c = self.q3 * self.q3 - d = self.q4 * self.q4 - return sqrt(a + b + c + d) + # Initialize standard vector with translated Cartesian coordinates + x = cos(ra) * cos(dec) + y = sin(ra) * cos(dec) + z = sin(dec) + Vector.__init__(self, x=x, y=y, z=z) - def normalize(self): - """Returns a copy of the Q normalized """ - scale = self.length() - return Quaternion( - Vector( - self.q1 / scale, - self.q2 / scale, - self.q3 / scale), - self.q4 / scale) + def __str__(self, verbose=True): + """Returns a string representation of the vector. Displays angles + in degrees. - def conjugate(self): - """Returns a copy of the conjugated Q """ - return Quaternion(Vector(-self.q1, -self.q2, -self.q3), self.q4) + Parameters + ---------- + verbose: bool + Print some information. + """ + a = (math2.R2D * self.ra, math2.R2D * self.dec, self.frame) + celest_info = 'CelestialVector: RA: %.3fD, DEC: %.3fD, frame: %s' % a + + if (verbose): + clss = super(CelestialVector, self).__str__() + celest_info = celest_info + '\n' + clss + return celest_info + + def position_angle(self, v): + """Returns the position angle of v at the self vector, in radians. + + v is an arbitrary vector that should be a CelestialVector object. + The position angle is the angle between the North vector on the + plane orthogonal to the self vector and the projection of v onto + that plane, defined counterclockwise. + See "V3-axis Position Angle", John Isaacs, May 2003 for + further discussion. + + Parameters + ---------- + v: Vector + The vector to measure against. + + Returns + ------- + pa : float + The position angle between the two vectors. + """ + y_coord = cos(v.dec) * sin(v.ra - self.ra) + b = cos(v.dec) * sin(self.dec) * cos(v.ra - self.ra) + x_coord = sin(v.dec) * cos(self.dec) - b + pa = atan2(y_coord, x_coord) + + if (pa < 0): + pa += (2 * pi) # PA has range 0-360 degrees + + return(pa) + + def rotate_about_axis(self, angle, axis): + """This rotates a vector about an axis by the specified angle + by using a rotation matrix. + A new vector is returned. + + Axis must be 'x', 'y', or 'z'. + The x-rotation rotates the y-axis toward the z-axis. + The y-rotation rotates the z-axis toward the x-axis. + The z-rotation rotates the x-axis toward the y-axis. + + Parameters + ---------- + angle: float + The angle of rotation. + axis: str + The axis to rotate about, ['x', 'y', 'z']. + + Returns + ------- + result : Vector + The rotated vector. + """ + if (axis == 'x'): + rot_matrix = Matrix([[1, 0, 0], [0, cos(angle), -sin(angle)], + [0, sin(angle), cos(angle)]]) + + elif (axis == 'y'): + rot_matrix = Matrix([[cos(angle), 0, sin(angle)], [0, 1, 0], + [-sin(angle), 0, cos(angle)]]) + + elif (axis == 'z'): + rot_matrix = Matrix([[cos(angle), -sin(angle), 0], + [sin(angle), cos(angle), 0], [0, 0, 1]]) + + else: + print('Error') + return + + new_matrix = rot_matrix * self.create_matrix() + new_vector = new_matrix.column(0) + result = CelestialVector() # initialize with Cartesian coordiantes + result.update_cartesian(x=new_vector[0], y=new_vector[1], + z=new_vector[2]) + return result + + def rotate_about_eigenaxis(self, angle, eigenaxis): + """Rotates a vector about arbitrary eigenaxis. + + eigenaxis = Vector object (axis about which to rotate). + angle = angle to rotate by in radians. + Rotation is counterclockwise looking outward from origin along + eigenaxis. Function uses rotation matrix from Rodrigues formula. - def __mul__(self, rs): - """Defines Q*Q for quaternion multiplication. + Note: This function is more general than rotate_about_axis above and + could be used in its place. However, rotate_about_axis is faster and + clearer when the rotation axis is one of the Cartesian axes. Parameters ---------- - rs: Quaternion - The quaternion to multiply. + angle: float + The angle of rotation. + eigenaxis: Vector + The eigenaxis to rotate about. Returns ------- - Q : Quaternion - The multiplied Quaternion. + result : Vector + The rotated vector. """ - Q = Quaternion(Vector(0., 0., 0.), 0.) - # Q.V = rs.V*self.q4 + self.V*rs.q4 + cross(self.V, rs.V) - Q.q1 = rs.q1 * self.q4 + self.q1 * rs.q4 + \ - (self.q2 * rs.q3 - self.q3 * rs.q2) - Q.q2 = rs.q2 * self.q4 + self.q2 * rs.q4 + \ - (self.q3 * rs.q1 - self.q1 * rs.q3) - Q.q3 = rs.q3 * self.q4 + self.q3 * rs.q4 + \ - (self.q1 * rs.q2 - self.q2 * rs.q1) - Q.q4 = self.q4 * rs.q4 - \ - (self.q1 * rs.q1 + self.q2 * rs.q2 + self.q3 * rs.q3) - return Q + cos_ang = cos(angle) # Used repeatedly below + sin_ang = sin(angle) - def cnvrt(self, V): - """Rotates a vector from the starting frame to the ending frame - defined by the Q. + # Fill out the Rodrigues rotation matrix + R11 = cos_ang + eigenaxis.x**2 * (1 - cos_ang) + R12 = eigenaxis.x * eigenaxis.y * (1 - cos_ang) - eigenaxis.z * sin_ang + R13 = eigenaxis.x * eigenaxis.z * (1 - cos_ang) + eigenaxis.y * sin_ang + R21 = eigenaxis.x * eigenaxis.y * (1 - cos_ang) + eigenaxis.z * sin_ang + R22 = cos_ang + eigenaxis.y**2 * (1 - cos_ang) + R23 = eigenaxis.y * eigenaxis.z * (1 - cos_ang) - eigenaxis.x * sin_ang + R31 = eigenaxis.x * eigenaxis.z * (1 - cos_ang) - eigenaxis.y * sin_ang + R32 = eigenaxis.y * eigenaxis.z * (1 - cos_ang) + eigenaxis.x * sin_ang + R33 = cos_ang + eigenaxis.z**2 * (1 - cos_ang) + + r1, r2, r3 = [R11, R12, R13], [R21, R22, R23], [R31, R32, R33] + rot_matrix = Matrix([r1, r2, r3]) + new_matrix = rot_matrix * self.create_matrix() + new_vector = new_matrix.column(0) + result = CelestialVector() # initialize with Cartesian coordinates + result.update_cartesian(x=new_vector[0], y=new_vector[1], + z=new_vector[2]) + return(result) + + def rotate_by_posang(self, pa): + """Returns the vector that results from rotating the self vector + counterclockwise from the North projection onto the plane + orthogonal to that vector by the specified position angle + (in radians). See "V3-axis Position Angle", John Isaacs, May 2003 for + further discussion. Parameters ---------- - V: Vector - The vector to rotate. + pa: float + The position angle. Returns ------- - Vector - The rotated Vector. + result : Vector + The rotated vector. """ - QV = Qmake_a_point(V) - QV = self * QV * self.conjugate() - return Vector(QV.q1, QV.q2, QV.q3) + x_coord = -cos(self.ra) * sin(self.dec) * \ + cos(pa) - sin(self.ra) * sin(pa) + y_coord = -sin(self.ra) * sin(self.dec) * \ + cos(pa) + cos(self.ra) * sin(pa) + z_coord = cos(self.dec) * cos(pa) + result = CelestialVector() + result.update_cartesian(x_coord, y_coord, z_coord) + return(result) - def inv_cnvrt(self, V): - """Rotates a vector from the ending frame to the starting frame - defined by the Q. + def rotate_using_quaternion(self, angle, eigenaxis): + """Rotates a vector about arbitrary eigenaxis using quaternion. + + This is an alternative formulation for rotate_about_eigenaxis. + Interface is the same as rotate_about_eigenaxis. Parameters ---------- - V: Vector - The vector to invert. + angle: float + The angle of rotation. + eigenaxis: Vector + The eigenaxis to rotate about. Returns ------- Vector - The inverted Vector. + The rotated vector. """ - QV = Qmake_a_point(V) - QV = self.conjugate() * QV * self - return Vector(QV.q1, QV.q2, QV.q3) + q = Quaternion(eigenaxis, 0.0) - def set_values(self, V, angle): - """Sets quaterion values using a direction vector and a rotation of - the coordinate frame about it. + # Need to negate here because set_values performs a negative rotation + # quaternion now represents the rotation + q.set_values(eigenaxis, -angle) + return(make_celestial_vector(q.cnvrt(self))) - Parameters - ---------- - V: Vector - The direction Vector. - angle: float - The angle of rotation. - """ - S = sin(-angle / 2.) - self.q1 = V.x * S - self.q2 = V.y * S - self.q3 = V.z * S - self.q4 = cos(angle / 2.) + def set_eq(self, ra, dec, degrees=False): + """Modifies a celestial vector with a new RA and DEC. - def set_as_QX(self, angle): - """Sets quaterion in place like QX function. + degrees = True if units are degrees. Default is radians. Parameters ---------- - angle: float - The angle of rotation + ra: float + The right ascension. + dec: float + The declination. + degrees: bool + Use degrees. """ - self.q1 = sin(-angle / 2.) - self.q2 = 0. - self.q3 = 0. - self.q4 = cos(angle / 2.) + if (degrees): + ra = math2.D2R * ra + dec = math2.D2R * dec - def set_as_QY(self, angle): - """Sets quaterion in place like QY function + self.ra = ra + self.dec = dec - Parameters - ---------- - angle: float - The angle of rotation - """ - self.q1 = 0. - self.q2 = sin(-angle / 2.) - self.q3 = 0. - self.q4 = cos(angle / 2.) + # Update Cartesian coordinates as well. + x = cos(ra) * cos(dec) + y = sin(ra) * cos(dec) + z = sin(dec) + super(CelestialVector, self).set_eq(x, y, z) - def set_as_QZ(self, angle): - """Sets quaterion in place like QZ function. + def transform_frame(self, new_frame): + """Transforms coordinates between celestial and ecliptic frames + and returns result as a new CelestialVector. + If new coordinate frame is the same as the old, a copy of the vector + is returned. Parameters ---------- - angle: float - The angle of rotation. - """ - self.q1 = 0. - self.q2 = 0. - self.q3 = sin(-angle / 2.) - self.q4 = cos(angle / 2.) - - def set_as_mult(self, QQ1, QQ2): - """Sets self as QQ1*QQ2 in place for quaternion multiplication. + new_frame: str + Convert to new frame. - Parameters - ---------- - QQ1: Quaternion - The first quaternion. - QQ2: Quaternion - The second quaternion. + Returns + ------- + result : Vector + The transformed vector. """ - a = QQ1.q2 * QQ2.q3 - QQ1.q3 * QQ2.q2 - b = QQ1.q3 * QQ2.q1 - QQ1.q1 * QQ2.q3 - c = QQ1.q1 * QQ2.q2 - QQ1.q2 * QQ2.q1 - d = QQ1.q1 * QQ2.q1 + QQ1.q2 * QQ2.q2 + QQ1.q3 * QQ2.q3 - self.q1 = QQ2.q1 * QQ1.q4 + QQ1.q1 * QQ2.q4 + a - self.q2 = QQ2.q2 * QQ1.q4 + QQ1.q2 * QQ2.q4 + b - self.q3 = QQ2.q3 * QQ1.q4 + QQ1.q3 * QQ2.q4 + c - self.q4 = QQ1.q4 * QQ2.q4 - d - - def set_as_point(self, V): - """Set V as a point. + result = None + gal_ec = new_frame == 'gal' and self.frame == 'ec' + ec_gal = new_frame == 'ec' and self.frame == 'gal' - Parameters - ---------- - V: Vector - The vector to set as a point. - """ - self.q1 = V.x - self.q2 = V.y - self.q3 = V.z - self.q4 = 0. + # Equatorial to ecliptic: rotate z-axis toward y-axis. + if ((new_frame == 'ec') and (self.frame == 'eq')): + result = self.rotate_about_axis(-math2.OBLIQUITY, 'x') - def set_equal(self, Q): - """Assigns values from other Q to this one. + # Ecliptic to equatorial: rotate y-axis toward z-axis. + elif ((new_frame == 'eq') and (self.frame == 'ec')): + result = self.rotate_about_axis(math2.OBLIQUITY, 'x') - Parameters - ---------- - Q: Quaternion - The quaternion value to set. - """ - self.q1 = Q.q1 - self.q2 = Q.q2 - self.q3 = Q.q3 - self.q4 = Q.q4 + elif ((new_frame == 'gal') and (self.frame == 'eq')): + # Use formula from Wayne Kinzel's book, adjusted for + # J2000 coordinates. + a = cos(self.dec) * cos(NGP.longitude) * \ + cos(self.ra - NGP.latitude) + b = math2.asin2(a + sin(self.dec) * sin(NGP.longitude)) + arg1 = sin(self.dec) - sin(b) * sin(NGP.longitude) + arg2 = cos(self.dec) * sin(self.ra - NGP.latitude) * \ + cos(NGP.longitude) - def set_as_conjugate(self): - """Assigns conjugate values in place. """ - self.q1 *= -1. - self.q2 *= -1. - self.q3 *= -1. + lng = atan2(arg1, arg2) + NGP.anode + result = CelestialVector(lng, b, degrees=False) -class NumericList(list): - """List class that supports multiplication. Only valid for numbers.""" + elif ((new_frame == 'eq') and (self.frame == 'gal')): + lng = self.ra # use l, b notation here for clarity + b = self.dec + term1 = cos(b) * cos(NGP.longitude) * sin(lng - NGP.anode) + dec = math2.asin2(term1 + sin(b) * sin(NGP.longitude)) + arg1 = cos(b) * cos(lng - NGP.anode) + sinterm = sin(NGP.longitude) * sin(lng - NGP.anode) + arg2 = sin(b) * cos(NGP.longitude) - cos(b) * sinterm + ra = atan2(arg1, arg2) + NGP.latitude - def __mul__(L1, L2): - """Take the dot product of two numeric lists. - Not using Vector for this because it is limited to three dimensions. - Lists must have the same number of elements + result = CelestialVector(ra, dec, degrees=False) - Parameters - ---------- - L1: sequence - The first list. - L2: sequence - The second list. + elif gal_ec or ec_gal: + print("""Error: Direct conversion between ecliptic and\ + galactic coordinates not supported yet""") - Returns - ------- - float - The sum of the lists. - """ - return(sum(map(lambda x, y: x * y, L1, L2))) + elif new_frame != self.frame: + print("Error: unrecognized coordinate frame.") + # If there was an error, return a copy of the initial vector. + if result is None: + result = CelestialVector(self.ra, self.dec, self.frame, False) -class Matrix(list): - """Class to encapsulate matrix data and methods. + else: + result.frame = new_frame # record new frame - A matrix is simply a list of lists that correspond to rows of the matrix. - This is just intended to handle simple multiplication and vector rotations. - For anything more advanced or computationally intensive, Python library - routines should be used.""" + return (result) - def __init__(self, rows): - """Constructor for a matrix. + def update_cartesian(self, x=None, y=None, z=None): + """Modifies a celestial vector by specifying new Cartesian coordinates. - This accepts a list of rows. - It is assumed the rows are all of the same length. + Any subset of the Cartesian coordinates may be specifed. Parameters ---------- - rows: sequence - The rows of the matrix. + x: float + The extent in x. + y: float + The extent in y. + z: float + The extent in z. """ - for row in rows: - self.append(NumericList(row)) # copy list - def __str__(self): - """Returns a string representation of the matrix.""" - return_str = 'Matrix:' + if x is not None: + self.x = x + if y is not None: + self.y = y + if z is not None: + self.z = z - for row_index in range(len(self)): - row_str = 'Row %d: ' % (row_index + 1) - row = self[row_index] + self.ra = atan2(self.y, self.x) # RA is arctan of y/x + if (self.ra < 0): # Make sure RA is positive + self.ra += 2 * pi - for col_index in range(len(row)): - row_str = row_str + '%6.3f ' % (row[col_index]) + self.dec = math2.asin2(self.z) # DEC is arcsin of z - return_str = return_str + '\n' + row_str - return(return_str) +class Attitude(CelestialVector): + "Defines an Observatory attitude by adding a position angle.""" - def element(self, row_index, col_index): - """Returns an element of the matrix indexed by row and column. + def __init__(self, ra=0.0, dec=0.0, pa=0.0, frame='eq', degrees=True): + """Constructor for an Attitude. - Indices begin with 0. + pa = position_angle in degrees(default) or radians if degrees=False + is specified. Other arguments are the same as with CelestialVector Parameters ---------- - row_index: int - The row index. - col_index: int - The column index. - - Returns - ------- - float - The matrix value. + ra: float + The right ascension. + dec: float + The declination. + pa: float + The position angle. + frame: str + The frame to use. + degrees: bool + Use degrees. """ - return ((self[row_index])[col_index]) - - def row(self, row_index): - """Returns a specified row of the matrix. + super(Attitude, self).__init__(ra=ra, dec=dec, frame=frame, + degrees=degrees) - Parameters - ---------- - row_index: int - The row index. + if (degrees): # convert into radians + pa = math2.D2R * pa - Returns - ------- - list - The row values. - """ + self.pa = pa - return(self[row_index]) + def __str__(self, verbose=True): + """Returns a string representation of the attitude. - def column(self, col_index): - """Returns a specified column of the matrix as a numeric list. + verbose (optional) = flag indicating whether detailed Vector + information should be included. Parameters ---------- - col_index: int - The column index. + verbose: bool + Print information. Returns ------- - list - The column values. + att_info : str + A string representation of the attitute. """ - return(NumericList([row[col_index] for row in self])) - - def num_rows(self): - """Returns the number of rows in the matrix.""" - - return(len(self)) - - def num_cols(self): - """Returns the number of columns in the matrix.""" - - return (len(self[0])) # assumes all rows of equal length + att_info = 'Attitude: PA: %.3fD' % (math2.R2D * self.pa) + att_info = att_info + '\n' + super(Attitude, self).__str__(verbose) + return att_info - def get_cols(self): - """Returns list of all columns in a matrix.""" - rng = range(0, self.num_cols()) - return ([self.column(col_index) for col_index in rng]) - def __mul__(m1, m2): - """Multiplies two Matrix objects and returns the resulting matrix. +class GalacticPole: + """Represents coordinates of galactic pole.""" - Number of rows in m1 must equal the number of columns in m2. + def __init__(self, latitude, longitude, ascending_node): + """Initializes the coordinates of the galactic pole. Parameters ---------- - m1: Matrix - The first matrix. - m2: Matrix - The second matrix. - - Returns - ------- - Matrix - The resultant matrix. + latitude: float + Latitude of pole, in degrees. + longitude: float + Longitude of pole, in degrees. + ascending_node: float + Ascending node of pole, in degrees. """ - result_rows = [] - - # Iterate over the rows in m1. The first column of row i is formed by - # multiplying the ith row of m1 by the first column of m2. The second - # column is formed by muliplying the ith row of m1 by the second - # column of m2, etc. - for row in m1: - new_row = [] - - for col in m2.get_cols(): - new_row.append(row * col) - - result_rows.append(new_row) - - return (Matrix(result_rows)) + # Arguments specified in degrees, but values represented in radians. + self.latitude = radians(latitude) + self.longitude = radians(longitude) + self.anode = radians(ascending_node) + def __str__(self): + """Returns string representation of the galactic pole.""" + text = (degrees(self.latitude), degrees(self.longitude), + degrees(self.anode)) + # Convert attributes back into degrees for readability. + return """GalacticPole: latitude: %.3fD, longitude: %.3fD,\ + anode: %.3fD""" % text -class Vector: - "Class to encapsulate vector data and operations." - def __init__(self, x=0.0, y=0.0, z=0.0): - """Constructor for a three-dimensional vector. +# supports transformation to galactic coordinates +NGP = GalacticPole(192.859508, 27.128336, 32.932) - Note that two-dimensional vectors can be constructed by omitting one - of the coordinates, which will default to 0. - Parameters - ---------- - x: float - The x coordinate. - y: float - The y coordinate. - z: float - The z coordinate. - """ - self.x = x # Cartesian x coordinate - self.y = y # Cartesian y coordinate - self.z = z # Cartesian z coordinate +class Matrix(list): + """Class to encapsulate matrix data and methods. - def __str__(self): - """Returns a string representation of the vector.""" - return('Vector: x: %.3f, y: %.3f, z: %.3f' % (self.x, self.y, self.z)) + A matrix is simply a list of lists that correspond to rows of the matrix. + This is just intended to handle simple multiplication and vector rotations. + For anything more advanced or computationally intensive, Python library + routines should be used.""" - def set_eq(self, x=None, y=None, z=None): - """Assigns new value to vector. + def __init__(self, rows): + """Constructor for a matrix. - Arguments are now optional to permit this to be used with 2D vectors - or to modify any subset of coordinates. + This accepts a list of rows. + It is assumed the rows are all of the same length. Parameters ---------- - x: float - The x coordinate. - y: float - The y coordinate. - z: float - The z coordinate. + rows: sequence + The rows of the matrix. """ - if x is not None: - self.x = x - if y is not None: - self.y = y - if z is not None: - self.z = z - - def length(self): - """Returns magnitude of the vector """ - return(sqrt(self.x * self.x + self.y * self.y + self.z * self.z)) - - def normalize(self): - """Returns copy of the normalized vector """ - mag = self.length() - return (Vector(self.x / mag, self.y / mag, self.z / mag)) + for row in rows: + self.append(NumericList(row)) # copy list - def __mul__(self, rs): - """Implements Vector * scalar. Can then use '*' syntax in multiplying - a vector by a scalar rs + def __mul__(m1, m2): + """Multiplies two Matrix objects and returns the resulting matrix. + + Number of rows in m1 must equal the number of columns in m2. Parameters ---------- - rs: float - The scalar to multiply. + m1: Matrix + The first matrix. + m2: Matrix + The second matrix. Returns ------- - Vector - The resultant vector. + Matrix + The resultant matrix. """ - x = self.x * rs - y = self.y * rs - z = self.z * rs - return (Vector(x, y, z)) + result_rows = [] - def __rmul__(self, ls): - """Implements float * Vector. + # Iterate over the rows in m1. The first column of row i is formed by + # multiplying the ith row of m1 by the first column of m2. The second + # column is formed by muliplying the ith row of m1 by the second + # column of m2, etc. + for row in m1: + new_row = [] - Parameters - ---------- - ls: float - The scalar to multiply. + for col in m2.get_cols(): + new_row.append(row * col) - Returns - ------- - Vector - The resultant vector. - """ - x = self.x * ls - y = self.y * ls - z = self.z * ls - return (Vector(x, y, z)) + result_rows.append(new_row) - def __add__(self, rs): - """Implements Vector + Vector + return (Matrix(result_rows)) - Parameters - ---------- - rs: float - The scalar to add. + def __str__(self): + """Returns a string representation of the matrix.""" + return_str = 'Matrix:' - Returns - ------- - Vector - The resultant vector. - """ - x = self.x + rs.x - y = self.y + rs.y - z = self.z + rs.z - return (Vector(x, y, z)) + for row_index in range(len(self)): + row_str = 'Row %d: ' % (row_index + 1) + row = self[row_index] - def __sub__(self, rs): - """Implements Vector - Vector. + for col_index in range(len(row)): + row_str = row_str + '%6.3f ' % (row[col_index]) - Parameters - ---------- - rs: float - The scalar to subtract. + return_str = return_str + '\n' + row_str - Returns - ------- - Vector - The resultant vector. - """ - x = self.x - rs.x - y = self.y - rs.y - z = self.z - rs.z - return (Vector(x, y, z)) + return(return_str) - def __truediv__(self, rs): - """Implements Vector / float. + def column(self, col_index): + """Returns a specified column of the matrix as a numeric list. Parameters ---------- - rs: float - The scalar to divide. + col_index: int + The column index. Returns ------- - Vector - The resultant vector. + list + The column values. """ - x = self.x / rs - y = self.y / rs - z = self.z / rs - return (Vector(x, y, z)) + return(NumericList([row[col_index] for row in self])) - def __imul__(self, rs): - """Implements Vector *= float + def element(self, row_index, col_index): + """Returns an element of the matrix indexed by row and column. + + Indices begin with 0. Parameters ---------- - rs: float - The scalar to multiply. + row_index: int + The row index. + col_index: int + The column index. Returns ------- - Vector - The resultant vector. + float + The matrix value. """ - self.x *= rs - self.y *= rs - self.z *= rs - return (self) + return ((self[row_index])[col_index]) - def __iadd__(self, rs): - """Implements Vector += vector. + def get_cols(self): + """Returns list of all columns in a matrix.""" + rng = range(0, self.num_cols()) + return ([self.column(col_index) for col_index in rng]) - Parameters - ---------- - rs: float - The scalar to add. + def num_cols(self): + """Returns the number of columns in the matrix.""" - Returns - ------- - Vector - The resultant vector. - """ - self.x += rs.x - self.y += rs.y - self.z += rs.z - return (self) + return (len(self[0])) # assumes all rows of equal length - def __isub__(self, rs): - """Implements Vector -= vector. + def num_rows(self): + """Returns the number of rows in the matrix.""" + + return(len(self)) + + def row(self, row_index): + """Returns a specified row of the matrix. Parameters ---------- - rs: float - The scalar to subtract. + row_index: int + The row index. Returns ------- - Vector - The resultant vector. + list + The row values. """ - self.x -= rs.x - self.y -= rs.y - self.z -= rs.z - return (self) - def __idiv__(self, rs): - """Implements Vector /= float. + return(self[row_index]) + + +class NumericList(list): + """List class that supports multiplication. Only valid for numbers.""" + + def __mul__(L1, L2): + """Take the dot product of two numeric lists. + Not using Vector for this because it is limited to three dimensions. + Lists must have the same number of elements Parameters ---------- - rs: float - The scalar to divide. + L1: sequence + The first list. + L2: sequence + The second list. Returns ------- - Vector - The resultant vector. + float + The sum of the lists. """ - self.x /= rs - self.y /= rs - self.z /= rs - return (self) + return(sum(map(lambda x, y: x * y, L1, L2))) - def create_matrix(self): - """Converts a Vector into a single-column matrix.""" - column = [self.x, self.y, self.z] - return(Matrix([[element] for element in column])) # singleton list +class Quaternion: + """This representation is used by Wertz and Markley""" - # Recommend deletion -- better to use a single interface that takes - # two vectors. - def dot(self, V2): - """returns dot product between two vectors. + def __init__(self, V, q4): + """Quaternion constructor. Parameters ---------- - V2: Vector - The vector to dot. + V: Vector + The vector to construct the quaternion with. + q4: Vector + The fourth vector. + """ + self.q1 = V.x + self.q2 = V.y + self.q3 = V.z + self.q4 = q4 + + def __mul__(self, rs): + """Defines Q*Q for quaternion multiplication. + + Parameters + ---------- + rs: Quaternion + The quaternion to multiply. Returns ------- - Vector - The resultant vector. + Q : Quaternion + The multiplied Quaternion. """ - return self.x * V2.x + self.y * V2.y + self.z * V2.z + Q = Quaternion(Vector(0., 0., 0.), 0.) + # Q.V = rs.V*self.q4 + self.V*rs.q4 + cross(self.V, rs.V) + Q.q1 = rs.q1 * self.q4 + self.q1 * rs.q4 + \ + (self.q2 * rs.q3 - self.q3 * rs.q2) + Q.q2 = rs.q2 * self.q4 + self.q2 * rs.q4 + \ + (self.q3 * rs.q1 - self.q1 * rs.q3) + Q.q3 = rs.q3 * self.q4 + self.q3 * rs.q4 + \ + (self.q1 * rs.q2 - self.q2 * rs.q1) + Q.q4 = self.q4 * rs.q4 - \ + (self.q1 * rs.q1 + self.q2 * rs.q2 + self.q3 * rs.q3) + return Q - # Recommend deletion in favor of non-method version. - def cross(self, V1, V2): - """returns cross product of two vectors + def __str__(self): + """Returns a string representation of the quaternion.""" + text = (self.q1, self.q2, self.q3, self.q4) + return 'Quaternion: q1: %.3f, q2: %.3f, q3: %.3f, q4: %.3f' % text + + def conjugate(self): + """Returns a copy of the conjugated Q """ + return Quaternion(Vector(-self.q1, -self.q2, -self.q3), self.q4) + + def cnvrt(self, V): + """Rotates a vector from the starting frame to the ending frame + defined by the Q. Parameters ---------- - V1: Vector - The vector to cross. - V2: Vector - The vector to cross. + V: Vector + The vector to rotate. Returns ------- Vector - The resultant vector. + The rotated Vector. """ - x = self.y * V2.z - V1.z * V2.y - y = self.z * V2.x - V1.x * V2.z - z = self.x * V2.y - V1.y * V2.x - return Vector(x, y, z) + QV = Qmake_a_point(V) + QV = self * QV * self.conjugate() + return Vector(QV.q1, QV.q2, QV.q3) - # Replace by separation - RLH - def angle(self, V2): - """Returns angle between the two vectors in degrees. + def inv_cnvrt(self, V): + """Rotates a vector from the ending frame to the starting frame + defined by the Q. Parameters ---------- - V2: Vector - The vector to measure. + V: Vector + The vector to invert. Returns ------- - float - The angle between the two vectors. + Vector + The inverted Vector. """ - R1 = self.length() - R2 = V2.length() - adot = dot(self, V2) - adot = adot / R1 / R2 - adot = min(1., adot) - adot = max(-1., adot) - return math2.acosd(adot) - - # RLH: What do these add? We're creating methods just to access - # individual attributes. - def rx(self): - """The magnitude of x""" - return self.x + QV = Qmake_a_point(V) + QV = self.conjugate() * QV * self + return Vector(QV.q1, QV.q2, QV.q3) - def ry(self): - """The magnitude of y""" - return self.y + def normalize(self): + """Returns a copy of the Q normalized """ + scale = self.length() + return Quaternion( + Vector( + self.q1 / scale, + self.q2 / scale, + self.q3 / scale), + self.q4 / scale) - def rz(self): - """The magnitude of z""" - return self.z + def length(self): + """Returns length of the Q """ + a = self.q1 * self.q1 + b = self.q2 * self.q2 + c = self.q3 * self.q3 + d = self.q4 * self.q4 + return sqrt(a + b + c + d) - # RLH: Suggest deletion in favor of __str__, which has the advantage - # that it is called on print. - def display(self): - """Print the values""" - return "[%f, %f, %f]" % (self.x, self.y, self.z) + def set_as_conjugate(self): + """Assigns conjugate values in place. """ + self.q1 *= -1. + self.q2 *= -1. + self.q3 *= -1. - # RLH: Not necessary if CelestialVector is used. - def set_xyz(self, ra, dec): - """Creates a unit vector from spherical coordinates + def set_as_mult(self, QQ1, QQ2): + """Sets self as QQ1*QQ2 in place for quaternion multiplication. Parameters ---------- - ra: float - The right ascension. - dec: float - The declination. + QQ1: Quaternion + The first quaternion. + QQ2: Quaternion + The second quaternion. """ - self.x = math2.cosd(dec) * math2.cosd(ra) - self.y = math2.cosd(dec) * math2.sind(ra) - self.z = math2.sind(dec) - - -class CelestialVector (Vector): - "Class to encapsulate a unit vector on the celestial sphere." - - def __init__(self, ra=0.0, dec=0.0, frame='eq', degrees=True): - """Constructor for a celestial vector. - - There are two spherical coordinates, a longitudinal coordinate (called - right ascension), and a latitudinal coordinate (called declination). - The RA is defined as the counterclockwise angle from a reference - direction on the equatorial plane; it ranges from 0-360 degrees. - The DEC is the angle between the vector and the equatorial plane; - it ranges from -90 to 90 degrees. Angles are specified in degrees but - represented internally as radians. - - The frame attribute indicates the coordinate frame of the vector, - which may be 'eq' (equatorial, default), 'ec' (ecliptic), or 'gal' - (galactic). In equatorial coordinates, the equatorial plane is the - celestial equator (extension of the Earth's equator) and the reference - axis is the vernal equinox. In ecliptic coordiantes, the equatorial - plane is the ecliptic (the Earth's orbital plane) and the reference - axis is usually defined relative to the Sun. In galactic coordinates, - the equatorial plane is the plane of the Galaxy. + a = QQ1.q2 * QQ2.q3 - QQ1.q3 * QQ2.q2 + b = QQ1.q3 * QQ2.q1 - QQ1.q1 * QQ2.q3 + c = QQ1.q1 * QQ2.q2 - QQ1.q2 * QQ2.q1 + d = QQ1.q1 * QQ2.q1 + QQ1.q2 * QQ2.q2 + QQ1.q3 * QQ2.q3 + self.q1 = QQ2.q1 * QQ1.q4 + QQ1.q1 * QQ2.q4 + a + self.q2 = QQ2.q2 * QQ1.q4 + QQ1.q2 * QQ2.q4 + b + self.q3 = QQ2.q3 * QQ1.q4 + QQ1.q3 * QQ2.q4 + c + self.q4 = QQ1.q4 * QQ2.q4 - d - The degrees attribute should be True if the RA, DEC inputs are in - degrees. Otherwise radians is assumed. + def set_as_point(self, V): + """Set V as a point. - The coordinates "ra" and "dec" may be used in all three systems. - Other names for coordinates in different frames may be defined for - clarity. + Parameters + ---------- + V: Vector + The vector to set as a point. + """ + self.q1 = V.x + self.q2 = V.y + self.q3 = V.z + self.q4 = 0. - A CelestialVector is also an ordinary unit vector, with Cartesian - coordinates defined relative to the equatorial plane. + def set_as_QX(self, angle): + """Sets quaterion in place like QX function. Parameters ---------- - ra: float - The right ascension. - dec: float - The declination. - frame: str - The frame to use. - degrees: bool - Use degrees. + angle: float + The angle of rotation """ - if (degrees): - ra = math2.D2R * ra - dec = math2.D2R * dec + self.q1 = sin(-angle / 2.) + self.q2 = 0. + self.q3 = 0. + self.q4 = cos(angle / 2.) - self.ra = ra - self.dec = dec - self.frame = frame + def set_as_QY(self, angle): + """Sets quaterion in place like QY function - # Initialize standard vector with translated Cartesian coordinates - x = cos(ra) * cos(dec) - y = sin(ra) * cos(dec) - z = sin(dec) - Vector.__init__(self, x=x, y=y, z=z) + Parameters + ---------- + angle: float + The angle of rotation + """ + self.q1 = 0. + self.q2 = sin(-angle / 2.) + self.q3 = 0. + self.q4 = cos(angle / 2.) - def __str__(self, verbose=True): - """Returns a string representation of the vector. Displays angles - in degrees. + def set_as_QZ(self, angle): + """Sets quaterion in place like QZ function. Parameters ---------- - verbose: bool - Print some information. + angle: float + The angle of rotation. """ - a = (math2.R2D * self.ra, math2.R2D * self.dec, self.frame) - celest_info = 'CelestialVector: RA: %.3fD, DEC: %.3fD, frame: %s' % a + self.q1 = 0. + self.q2 = 0. + self.q3 = sin(-angle / 2.) + self.q4 = cos(angle / 2.) - if (verbose): - clss = super(CelestialVector, self).__str__() - celest_info = celest_info + '\n' + clss - return celest_info + def set_equal(self, Q): + """Assigns values from other Q to this one. - def set_eq(self, ra, dec, degrees=False): - """Modifies a celestial vector with a new RA and DEC. + Parameters + ---------- + Q: Quaternion + The quaternion value to set. + """ + self.q1 = Q.q1 + self.q2 = Q.q2 + self.q3 = Q.q3 + self.q4 = Q.q4 - degrees = True if units are degrees. Default is radians. + def set_values(self, V, angle): + """Sets quaterion values using a direction vector and a rotation of + the coordinate frame about it. Parameters ---------- - ra: float - The right ascension. - dec: float - The declination. - degrees: bool - Use degrees. + V: Vector + The direction Vector. + angle: float + The angle of rotation. """ - if (degrees): - ra = math2.D2R * ra - dec = math2.D2R * dec - - self.ra = ra - self.dec = dec + S = sin(-angle / 2.) + self.q1 = V.x * S + self.q2 = V.y * S + self.q3 = V.z * S + self.q4 = cos(angle / 2.) - # Update Cartesian coordinates as well. - x = cos(ra) * cos(dec) - y = sin(ra) * cos(dec) - z = sin(dec) - super(CelestialVector, self).set_eq(x, y, z) - def update_cartesian(self, x=None, y=None, z=None): - """Modifies a celestial vector by specifying new Cartesian coordinates. +# RLH: Recommend replacement by separation. +def angle(V1, V2): + """returns angle between two vectors in degrees, non class member. - Any subset of the Cartesian coordinates may be specifed. + Parameters + ---------- + V1: Vector + The first vector. + V2: Vector + The second vector. - Parameters - ---------- - x: float - The extent in x. - y: float - The extent in y. - z: float - The extent in z. - """ + Returns + ------- + float + The angle between the vectors. + """ + R1 = V1.length() + R2 = V2.length() + adot = dot(V1, V2) + adot = adot / R1 / R2 + adot = min(1., adot) + adot = max(-1., adot) + return math2.acosd(adot) - if x is not None: - self.x = x - if y is not None: - self.y = y - if z is not None: - self.z = z - self.ra = atan2(self.y, self.x) # RA is arctan of y/x - if (self.ra < 0): # Make sure RA is positive - self.ra += 2 * pi +def cross(v1, v2): + """Returns cross product between two vectors, non class member. - self.dec = math2.asin2(self.z) # DEC is arcsin of z + Parameters + ---------- + v1: Vector + The first vector. + v2: Vector + The second vector. - def rotate_about_axis(self, angle, axis): - """This rotates a vector about an axis by the specified angle - by using a rotation matrix. - A new vector is returned. + Returns + ------- + float + The cross product of the vectors. + """ + x = v1.y * v2.z - v1.z * v2.y + y = v1.z * v2.x - v1.x * v2.z + z = v1.x * v2.y - v1.y * v2.x + return Vector(x, y, z) - Axis must be 'x', 'y', or 'z'. - The x-rotation rotates the y-axis toward the z-axis. - The y-rotation rotates the z-axis toward the x-axis. - The z-rotation rotates the x-axis toward the y-axis. - Parameters - ---------- - angle: float - The angle of rotation. - axis: str - The axis to rotate about, ['x', 'y', 'z']. +def cvt_body2inertial_Q_to_c1c2pa_tuple(Q): + """Creates a angle tuple from Q, assuming body frame to inertial Q and + 321 rotation sequence. - Returns - ------- - result : Vector - The rotated vector. - """ - if (axis == 'x'): - rot_matrix = Matrix([[1, 0, 0], [0, cos(angle), -sin(angle)], - [0, sin(angle), cos(angle)]]) + Parameters + ---------- + Q: Quaternion + The quaternion. - elif (axis == 'y'): - rot_matrix = Matrix([[cos(angle), 0, sin(angle)], [0, 1, 0], - [-sin(angle), 0, cos(angle)]]) + Returns + ------- + coord1 : float + The first coordinate. + coord2 : float + The second coordinate. + pa : float + The poosition angle. + """ + # Conversion from Euler symmetric parameters to matrix elements and + # matrix elements to rotation angles is given in Isaac's papers + r11 = Q.q1 * Q.q1 - Q.q2 * Q.q2 - Q.q3 * Q.q3 + Q.q4 * Q.q4 + r21 = 2. * (Q.q1 * Q.q2 + Q.q3 * Q.q4) + r31 = 2. * (Q.q1 * Q.q3 - Q.q2 * Q.q4) + r32 = 2. * (Q.q2 * Q.q3 + Q.q1 * Q.q4) + r33 = -Q.q1 * Q.q1 - Q.q2 * Q.q2 + Q.q3 * Q.q3 + Q.q4 * Q.q4 + coord1 = atan2(r21, r11) + if coord1 < 0.: + coord1 += PI2 + coord2 = math2.asin2(r31) # use "safe" version of sine + pa = atan2(-r32, r33) + if pa < 0.: + pa += PI2 + return coord1, coord2, pa - elif (axis == 'z'): - rot_matrix = Matrix([[cos(angle), -sin(angle), 0], - [sin(angle), cos(angle), 0], [0, 0, 1]]) - else: - print('Error') - return +def cvt_c1c2_using_body2inertial_Q_to_v2v3pa_tuple(Q, coord1, coord2): + """Given Q and a position, returns v2, v3, V3PA tuple - new_matrix = rot_matrix * self.create_matrix() - new_vector = new_matrix.column(0) - result = CelestialVector() # initialize with Cartesian coordiantes - result.update_cartesian(x=new_vector[0], y=new_vector[1], - z=new_vector[2]) - return result + Parameters + ---------- + Q: Quaternion + The quaternion. + coord1: float + The first coordinate. + coord2: float + The second coordinate - def rotate_about_eigenaxis(self, angle, eigenaxis): - """Rotates a vector about arbitrary eigenaxis. + Returns + ------- + coord1 : float + The first coordinate. + coord2 : float + The second coordinate. + pa : float + The poosition angle. + """ + Vp_eci = Vector(1., 0., 0.) + Vp_eci.set_xyz_from_angs(coord1, coord2) + Vp_body_pt = Q.inv_cnvrt(Vp_eci) + v2 = atan2(Vp_body_pt.y, Vp_body_pt.x) + v3 = asin(unit_limit(Vp_body_pt.z)) + V3_body = Vector(0., 0., 1.) + V3_eci_pt = Q.cnvrt(V3_body) + NP_eci = Vector(0., 0., 1.) + V_left = cross(NP_eci, Vp_eci) + if V_left.length() > 0.: + V_left = V_left / V_left.length() + NP_in_plane = cross(Vp_eci, V_left) + x = dot(V3_eci_pt, NP_in_plane) + y = dot(V3_eci_pt, V_left) + pa = atan2(y, x) + if pa < 0.: + pa += PI2 + return v2, v3, pa - eigenaxis = Vector object (axis about which to rotate). - angle = angle to rotate by in radians. - Rotation is counterclockwise looking outward from origin along - eigenaxis. Function uses rotation matrix from Rodrigues formula. - Note: This function is more general than rotate_about_axis above and - could be used in its place. However, rotate_about_axis is faster and - clearer when the rotation axis is one of the Cartesian axes. +def cvt_pt_Q_to_V(Q): + """Converts a pure (pointing) Q to a unit position Vector - Parameters - ---------- - angle: float - The angle of rotation. - eigenaxis: Vector - The eigenaxis to rotate about. + Parameters + ---------- + Q: Quaternion + The quaternion to convert to a Vector. - Returns - ------- - result : Vector - The rotated vector. - """ - cos_ang = cos(angle) # Used repeatedly below - sin_ang = sin(angle) + Returns + ------- + Vector + The point as a vector. + """ + return Vector(Q.q1, Q.q2, Q.q3) - # Fill out the Rodrigues rotation matrix - R11 = cos_ang + eigenaxis.x**2 * (1 - cos_ang) - R12 = eigenaxis.x * eigenaxis.y * (1 - cos_ang) - eigenaxis.z * sin_ang - R13 = eigenaxis.x * eigenaxis.z * (1 - cos_ang) + eigenaxis.y * sin_ang - R21 = eigenaxis.x * eigenaxis.y * (1 - cos_ang) + eigenaxis.z * sin_ang - R22 = cos_ang + eigenaxis.y**2 * (1 - cos_ang) - R23 = eigenaxis.y * eigenaxis.z * (1 - cos_ang) - eigenaxis.x * sin_ang - R31 = eigenaxis.x * eigenaxis.z * (1 - cos_ang) - eigenaxis.y * sin_ang - R32 = eigenaxis.y * eigenaxis.z * (1 - cos_ang) + eigenaxis.x * sin_ang - R33 = cos_ang + eigenaxis.z**2 * (1 - cos_ang) - r1, r2, r3 = [R11, R12, R13], [R21, R22, R23], [R31, R32, R33] - rot_matrix = Matrix([r1, r2, r3]) - new_matrix = rot_matrix * self.create_matrix() - new_vector = new_matrix.column(0) - result = CelestialVector() # initialize with Cartesian coordinates - result.update_cartesian(x=new_vector[0], y=new_vector[1], - z=new_vector[2]) - return(result) +def cvt_v2v3_using_body2inertial_Q_to_c1c2pa_tuple(Q, v2, v3): + """Given Q and v2, v3 gives pos on sky and V3 PA. - def rotate_using_quaternion(self, angle, eigenaxis): - """Rotates a vector about arbitrary eigenaxis using quaternion. + Parameters + ---------- + Q: Quaternion + The quaternion. + v2: float + The V2 position. + v3: float + The V3 position. - This is an alternative formulation for rotate_about_eigenaxis. - Interface is the same as rotate_about_eigenaxis. + Returns + ------- + tuple + The coordinates and position angle + """ + Vp_body = Vector(0., 0., 0.) + Vp_body.set_xyz_from_angs(v2, v3) + Vp_eci_pt = Q.cnvrt(Vp_body) + coord1 = atan2(Vp_eci_pt.y, Vp_eci_pt.x) + if coord1 < 0.: + coord1 += PI2 + coord2 = asin(unit_limit(Vp_eci_pt.z)) - Parameters - ---------- - angle: float - The angle of rotation. - eigenaxis: Vector - The eigenaxis to rotate about. + V3_body = Vector(0., 0., 1.) + V3_eci_pt = Q.cnvrt(V3_body) + NP_eci = Vector(0., 0., 1.) + V_left = cross(NP_eci, Vp_eci_pt) + if V_left.length() > 0.: + V_left = V_left / V_left.length() + NP_in_plane = cross(Vp_eci_pt, V_left) + x = dot(V3_eci_pt, NP_in_plane) + y = dot(V3_eci_pt, V_left) + pa = atan2(y, x) + if pa < 0.: + pa += PI2 - Returns - ------- - Vector - The rotated vector. - """ - q = Quaternion(eigenaxis, 0.0) + return coord1, coord2, pa - # Need to negate here because set_values performs a negative rotation - # quaternion now represents the rotation - q.set_values(eigenaxis, -angle) - return(make_celestial_vector(q.cnvrt(self))) - def transform_frame(self, new_frame): - """Transforms coordinates between celestial and ecliptic frames - and returns result as a new CelestialVector. - If new coordinate frame is the same as the old, a copy of the vector - is returned. +def dec_separation(v1, v2): + """Returns difference in declination between two CelestialVectors. - Parameters - ---------- - new_frame: str - Convert to new frame. + Parameters + ---------- + v1: Vector + The first vector. + v2: Vector + The second vector. - Returns - ------- - result : Vector - The transformed vector. - """ - result = None - gal_ec = new_frame == 'gal' and self.frame == 'ec' - ec_gal = new_frame == 'ec' and self.frame == 'gal' + Returns + ------- + float + The separation between Dec values + """ + return(v1.dec - v2.dec) # simply take the difference in declination - # Equatorial to ecliptic: rotate z-axis toward y-axis. - if ((new_frame == 'ec') and (self.frame == 'eq')): - result = self.rotate_about_axis(-math2.OBLIQUITY, 'x') - # Ecliptic to equatorial: rotate y-axis toward z-axis. - elif ((new_frame == 'eq') and (self.frame == 'ec')): - result = self.rotate_about_axis(math2.OBLIQUITY, 'x') +# Functions that operate on vectors but are not methods. +def dot(v1, v2): + """returns dot product between two vectors, non class member. - elif ((new_frame == 'gal') and (self.frame == 'eq')): - # Use formula from Wayne Kinzel's book, adjusted for - # J2000 coordinates. - a = cos(self.dec) * cos(NGP.longitude) * \ - cos(self.ra - NGP.latitude) - b = math2.asin2(a + sin(self.dec) * sin(NGP.longitude)) - arg1 = sin(self.dec) - sin(b) * sin(NGP.longitude) - arg2 = cos(self.dec) * sin(self.ra - NGP.latitude) * \ - cos(NGP.longitude) + Parameters + ---------- + v1: Vector + The first vector. + v2: Vector + The second vector. - lng = atan2(arg1, arg2) + NGP.anode + Returns + ------- + float + The dot product of the vectors. + """ + return(v1.x * v2.x + v1.y * v2.y + v1.z * v2.z) - result = CelestialVector(lng, b, degrees=False) - elif ((new_frame == 'eq') and (self.frame == 'gal')): - lng = self.ra # use l, b notation here for clarity - b = self.dec - term1 = cos(b) * cos(NGP.longitude) * sin(lng - NGP.anode) - dec = math2.asin2(term1 + sin(b) * sin(NGP.longitude)) - arg1 = cos(b) * cos(lng - NGP.anode) - sinterm = sin(NGP.longitude) * sin(lng - NGP.anode) - arg2 = sin(b) * cos(NGP.longitude) - cos(b) * sinterm - ra = atan2(arg1, arg2) + NGP.latitude +def make_celestial_vector(v): + """Takes a Vector object and creates an equivalent CelestialVector. - result = CelestialVector(ra, dec, degrees=False) + Input vector v must be a unit vector. - elif gal_ec or ec_gal: - print("""Error: Direct conversion between ecliptic and\ - galactic coordinates not supported yet""") + Parameters + ---------- + v: Vector + The vector to convert. - elif new_frame != self.frame: - print("Error: unrecognized coordinate frame.") + Returns + ------- + result : Vector + The updated vector. + """ + result = CelestialVector() + result.update_cartesian(v.x, v.y, v.z) + return(result) - # If there was an error, return a copy of the initial vector. - if result is None: - result = CelestialVector(self.ra, self.dec, self.frame, False) - else: - result.frame = new_frame # record new frame +def pos_V_to_ra_dec(V): + """Returns tuple of spherical angles from unit direction Vector. - return (result) + Parameters + ---------- + V: Vector + The vector to analyze. - def rotate_by_posang(self, pa): - """Returns the vector that results from rotating the self vector - counterclockwise from the North projection onto the plane - orthogonal to that vector by the specified position angle - (in radians). See "V3-axis Position Angle", John Isaacs, May 2003 for - further discussion. + Returns + ------- + ra : float + The ra of the vector. + dec : float + The dec of the vector. + """ + ra = math2.atan2d(V.y, V.x) + V.z = min(1., V.z) + V.z = max(-1., V.z) + dec = math2.asind(V.z) + if ra < 0.: + ra += 360. + return(ra, dec) - Parameters - ---------- - pa: float - The position angle. - Returns - ------- - result : Vector - The rotated vector. - """ - x_coord = -cos(self.ra) * sin(self.dec) * \ - cos(pa) - sin(self.ra) * sin(pa) - y_coord = -sin(self.ra) * sin(self.dec) * \ - cos(pa) + cos(self.ra) * sin(pa) - z_coord = cos(self.dec) * cos(pa) - result = CelestialVector() - result.update_cartesian(x_coord, y_coord, z_coord) - return(result) +def projection(v, axis): + """Returns projection of vector v on plane normal to axis. - def position_angle(self, v): - """Returns the position angle of v at the self vector, in radians. + First take cross-product of v and the axis and normalize it. + Then cross the axis with the result and return a CelestialVector. + See http://www.euclideanspace.com/maths/geometry/elements/plane/ + lineOnPlane/index.htm. - v is an arbitrary vector that should be a CelestialVector object. - The position angle is the angle between the North vector on the - plane orthogonal to the self vector and the projection of v onto - that plane, defined counterclockwise. - See "V3-axis Position Angle", John Isaacs, May 2003 for - further discussion. + Parameters + ---------- + v: Vector + The vector to convert. + axis: str + The axis to project onto. - Parameters - ---------- - v: Vector - The vector to measure against. + Returns + ------- + Vector + The updated vector. + """ + return(make_celestial_vector(cross(axis, (cross(v, axis)).normalize()))) - Returns - ------- - pa : float - The position angle between the two vectors. - """ - y_coord = cos(v.dec) * sin(v.ra - self.ra) - b = cos(v.dec) * sin(self.dec) * cos(v.ra - self.ra) - x_coord = sin(v.dec) * cos(self.dec) - b - pa = atan2(y_coord, x_coord) - if (pa < 0): - pa += (2 * pi) # PA has range 0-360 degrees +def Qmake_a_point(V): + """Creates a pure Q, i.e. defines a pointing not a rotation - return(pa) + Parameters + ---------- + V: Vector + The vector. + Returns + ------- + Quaternion + The point as a quaternion. + """ + return Quaternion(V, 0.) -class Attitude(CelestialVector): - "Defines an Observatory attitude by adding a position angle.""" - def __init__(self, ra=0.0, dec=0.0, pa=0.0, frame='eq', degrees=True): - """Constructor for an Attitude. +def QX(angle): + """Creates rotation quaternion about X axis, rotates a vector about + this axis. - pa = position_angle in degrees(default) or radians if degrees=False - is specified. Other arguments are the same as with CelestialVector + Parameters + ---------- + angle: float + The angle to rotate by. - Parameters - ---------- - ra: float - The right ascension. - dec: float - The declination. - pa: float - The position angle. - frame: str - The frame to use. - degrees: bool - Use degrees. - """ - super(Attitude, self).__init__(ra=ra, dec=dec, frame=frame, - degrees=degrees) + Result + ------ + Quarternion + The rotated quaternion. + """ + return Quaternion(Vector(sin(angle / 2.), 0., 0.), cos(angle / 2.)) - if (degrees): # convert into radians - pa = math2.D2R * pa - self.pa = pa +def QY(angle): + """Creates rotation quaternion about Y axis, rotates a vector about + this axis. - def __str__(self, verbose=True): - """Returns a string representation of the attitude. + Parameters + ---------- + angle: float + The angle to rotate by. - verbose (optional) = flag indicating whether detailed Vector - information should be included. + Result + ------ + Quarternion + The rotated quaternion. + """ + return Quaternion(Vector(0., sin(angle / 2.), 0.), cos(angle / 2.)) + + +def QZ(angle): + """Creates rotation quaternion about Z axis, rotates a vector about + this axis - Parameters - ---------- - verbose: bool - Print information. + Parameters + ---------- + angle: float + The angle to rotate by. - Returns - ------- - att_info : str - A string representation of the attitute. - """ - att_info = 'Attitude: PA: %.3fD' % (math2.R2D * self.pa) - att_info = att_info + '\n' + super(Attitude, self).__str__(verbose) - return att_info + Result + ------ + Quarternion + The rotated quaternion. + """ + return Quaternion(Vector(0., 0., sin(angle / 2.)), cos(angle / 2.)) -# Functions that operate on vectors but are not methods. -def dot(v1, v2): - """returns dot product between two vectors, non class member. +def Qmake_aperture2inertial(coord1, coord2, APA, xoff, yoff, s, YapPA, + V3ref, V2ref): + """Creates a rotation Q, going from the target in aperture frame to body. Parameters ---------- - v1: Vector - The first vector. - v2: Vector - The second vector. + coord1: float + The first coordinate. + coord2: float + The second coordinate. + APA: float + The apature position. + xoff: float + The x offset. + yoff: float + The y offset. + s: float + The multiplicative factor. + V2ref: float + The V2 position. + V3ref: float + The V3 position. Returns ------- - float - The dot product of the vectors. + Quaternion + The rotation quaternion. """ - return(v1.x * v2.x + v1.y * v2.y + v1.z * v2.z) + term1 = QZ(coord1) * QY(-coord2) * QX(-APA) * QY(-yoff) + term2 = QZ(s * xoff) * QX(YapPA) * QY(V3ref) * QZ(-V2ref) + return term1 * term2 -def cross(v1, v2): - """Returns cross product between two vectors, non class member. +# The following functions are dependent upon the spacecraft definitions +# and perhaps should be moved to that module +def Qmake_body2inertial(coord1, coord2, V3pa): + """Creates a rotation Q, going from the body frame to inertial. Parameters ---------- - v1: Vector - The first vector. - v2: Vector - The second vector. + coord1: float + The first coordinate. + coord2: float + The second coordinate. + V3pa: float + The V3 position. Returns ------- - float - The cross product of the vectors. + Quaternion + The rotation quaternion """ - x = v1.y * v2.z - v1.z * v2.y - y = v1.z * v2.x - v1.x * v2.z - z = v1.x * v2.y - v1.y * v2.x - return Vector(x, y, z) - + return QZ(coord1) * QY(-coord2) * QX(-V3pa) -def separation(v1, v2, norm=False): - """Returns angle between two unit vectors in radians. - The angle between two normalized vectors is the arc-cosine of the dot - product. Unless the norm attribute is set to True, it is assumed the - vectors are already normalized (for performance). +def Qmake_v2v3_2body(v2, v3): + """Creates a rotation Q, going from v2 and v3 in the body frame to + inertial. Parameters ---------- - v1: Vector - The first vector. - v2: Vector - The second vector. - norm: bool - Normalize the vectors. + v2: float + The V2 position. + V3: float + The V3 position. Returns ------- - separation : float - The separation of the vectors. + Quaternion + The rotation quaternion. """ - if (norm): - v1 = v1.normalize() - v2 = v2.normalize() + return QY(v3) * QZ(-v2) - separation = math2.acos2(dot(v1, v2)) - # For very small angles, cos and acos behave poorly as the cosine of a - # very small angle is interpreted as 1.0. Therefore, recompute using - # the cross product if the result is less than 1 degree. - if (separation < math2.D2R): - vcross = cross(v1, v2) - separation = math2.asin2(vcross.length()) +def Qmake_v2v3_2inertial(coord1, coord2, V3pa, v2, v3): + """Creates a rotation Q, going from v2 and v3 in the body frame to + inertial - return(separation) + Parameters + ---------- + coord1: float + The first coordinate. + coord2: float + The second coordinate. + V3pa: float + The V3 position. + v2: float + The V2 position. + v3: float + The V3 position. + + Returns + ------- + Quaternion + The rotation quaternion. + """ + return QZ(coord1) * QY(-coord2) * QX(-V3pa) * QY(v3) * QZ(-v2) def ra_delta(v1, v2): @@ -1617,8 +1690,12 @@ def ra_separation(v1, v2): return(delta_ra * cos(dec)) -def dec_separation(v1, v2): - """Returns difference in declination between two CelestialVectors. +def separation(v1, v2, norm=False): + """Returns angle between two unit vectors in radians. + + The angle between two normalized vectors is the arc-cosine of the dot + product. Unless the norm attribute is set to True, it is assumed the + vectors are already normalized (for performance). Parameters ---------- @@ -1626,105 +1703,28 @@ def dec_separation(v1, v2): The first vector. v2: Vector The second vector. + norm: bool + Normalize the vectors. Returns ------- - float - The separation between Dec values - """ - return(v1.dec - v2.dec) # simply take the difference in declination - - -def make_celestial_vector(v): - """Takes a Vector object and creates an equivalent CelestialVector. - - Input vector v must be a unit vector. - - Parameters - ---------- - v: Vector - The vector to convert. - - Returns - ------- - result : Vector - The updated vector. - """ - result = CelestialVector() - result.update_cartesian(v.x, v.y, v.z) - return(result) - - -def projection(v, axis): - """Returns projection of vector v on plane normal to axis. - - First take cross-product of v and the axis and normalize it. - Then cross the axis with the result and return a CelestialVector. - See http://www.euclideanspace.com/maths/geometry/elements/plane/ - lineOnPlane/index.htm. - - Parameters - ---------- - v: Vector - The vector to convert. - axis: str - The axis to project onto. - - Returns - ------- - Vector - The updated vector. - """ - return(make_celestial_vector(cross(axis, (cross(v, axis)).normalize()))) - - -def pos_V_to_ra_dec(V): - """Returns tuple of spherical angles from unit direction Vector. - - Parameters - ---------- - V: Vector - The vector to analyze. - - Returns - ------- - ra : float - The ra of the vector. - dec : float - The dec of the vector. + separation : float + The separation of the vectors. """ - ra = math2.atan2d(V.y, V.x) - V.z = min(1., V.z) - V.z = max(-1., V.z) - dec = math2.asind(V.z) - if ra < 0.: - ra += 360. - return(ra, dec) - + if (norm): + v1 = v1.normalize() + v2 = v2.normalize() -# RLH: Recommend replacement by separation. -def angle(V1, V2): - """returns angle between two vectors in degrees, non class member. + separation = math2.acos2(dot(v1, v2)) - Parameters - ---------- - V1: Vector - The first vector. - V2: Vector - The second vector. + # For very small angles, cos and acos behave poorly as the cosine of a + # very small angle is interpreted as 1.0. Therefore, recompute using + # the cross product if the result is less than 1 degree. + if (separation < math2.D2R): + vcross = cross(v1, v2) + separation = math2.asin2(vcross.length()) - Returns - ------- - float - The angle between the vectors. - """ - R1 = V1.length() - R2 = V2.length() - adot = dot(V1, V2) - adot = adot / R1 / R2 - adot = min(1., adot) - adot = max(-1., adot) - return math2.acosd(adot) + return(separation) def vel_ab(U, Vel): diff --git a/exoctk/contam_visibility/time_extensionsx.py b/exoctk/contam_visibility/time_extensionsx.py index 7c093589..aad41200 100755 --- a/exoctk/contam_visibility/time_extensionsx.py +++ b/exoctk/contam_visibility/time_extensionsx.py @@ -3,117 +3,176 @@ Dates are represented as modified Julian dates (mjd). An mjd gives the number of days since midnight on November 17, 1858. """ -import string + from math import ceil, floor +import string # Constant for converting Julian dates to modified Julian dates MJD_BASELINE = 2400000.5 -def is_leap_year(year): - """Returns True if the year is a leap year, False otherwise. +class Interval(object): + """Class to represent a simple temporal interval. + """ - Parameters - ---------- - year: int - The year to check. + def __init__(self, start, end): + """Constructor for an interval. - Returns - ------- - bool - Is the year a leap year? - """ - return (((year % 4 == 0) and ((year % 100 > 0) or (year % 400 == 0)))) + Parameters + ---------- + start: float + The start time. + end: float + The end time. + """ + self.start = start + self.end = end + def __str__(self): + """Returns a string representation of the interval.""" + return('Interval: start: %s, end: %s' % (display_date(self.start), + display_date(self.end))) -def days_in_year(year): - """Returns the number of days in a year. + def start_time(self): + """Returns the start of the interval.""" + return(self.start) - Parameters - ---------- - year: int - The year to search. + def end_time(self): + """Returns the end of the interval.""" + return(self.end) - Returns - ------- - days : int - The number of days that year. - """ - days = 365 + def duration(self): + """Returns the duration of an interval in fractional days.""" + return(self.end_time() - self.start_time()) - if (is_leap_year(year)): - days += 1 + def temporal_relationship(self, time): + """Returns the temporal relationship between an interval and an + absolute time. - return(days) + Returns 'before' if the interval ends at or before the time, + 'after' if the interval begins at or after the time, + 'includes' if the time occurs during the interval. + Parameters + ---------- + time: float + The time. -def leap_years(year1, year2): - """Returns the number of leap years between year1 and year2, - non-inclusive. + Returns + ------- + rel : str + The temporal relationship. + """ + if (self.end_time() <= time): + rel = 'before' + elif (self.start_time() >= time): + rel = 'after' + else: + rel = 'includes' - year1 and year2 must be integers, with year2 > year1 + return(rel) - Parameters - ---------- - year1: int - The start year. - year2: int - The end year. - Returns - ------- - int - The number of leap years between year1 and year2. +class FlexibleInterval(Interval): + """Class to represent an interval with flexibility on when it can + start and end. """ - # Find next years after year1 that are divisible by 4, 100, and 400 - next_div4 = int(4 * ceil(year1 / 4.0)) - next_div100 = int(100 * ceil(year1 / 100.0)) - next_div400 = int(400 * ceil(year1 / 400.0)) + def __init__(self, est, lst, let): + """Constructor for a FlexibileInterval. - # Now compute number of years between year1 and year2 that are - # evenly divisible by 4, 100, 400 - div4_years = int(ceil((year2 - next_div4) / 4.0)) - div100_years = int(ceil((year2 - next_div100) / 100.0)) - div400_years = int(ceil((year2 - next_div400) / 400.0)) + Parameters + ---------- + est: float + Earliest start time (mjd). + lst: float + Latest start time (mjd). + let: float + Latest end time (mjd). + """ + self.est = est + self.lst = lst + self.let = let - # Leap years are years divisible by 4, except for years - # divisible by 100 that are not divisible by 400 - return(div4_years - (div100_years - div400_years)) + def __str__(self): + """Returns a string representation of the FlexibleInterval.""" + txt = (display_date(self.est), display_date(self.lst), + display_date(self.let)) + return('FlexibleInterval: EST: %s, LST: %s, LET: %s' % txt) + + def start_time(self): + """Returns the start of the FlexibleInterval.""" + return(self.est) -def integer_days(time): - """Takes a time in fractional days and returns integer component. + def end_time(self): + """Returns the end of the FlexibleInterval.""" + + return(self.let) + + def flexibility(self): + """Returns the flexibility of the FlexibleInterval, in + fractional days.""" + + return(self.lst - self.est) + + def maximum_duration(self): + """Returns the maximum duration of the FlexibleInterval, in + fractional days.""" + + return(self.let - self.lst) + + +def compute_mjd(year, day_of_year, hour, minute, second): + """Computes a modified Julian date from a date specified as a year, + day of year, hour, minute, and second. + Arguments should be integers. Parameters ---------- - time: float - The float time. + year: int + The year. + day_of_year: int + The day. + hour: int + The hour. + minute: int + The minute. + second: int + The second. Returns ------- - int - The integer time. + float + The modified julian day. """ - # If time is negative, integer days is a larger negative number - return(int(floor(time))) + fractional_days = (hour * 3600 + minute * 60 + second) / 86400.0 + mjd_years = year - 1859 + num_leaps = leap_years(1858, year) # number of leap years since 1858 + # Add 45 days from Nov. 17 to end of 1858 + return((365 * mjd_years) + num_leaps + 45 + (day_of_year - 1) + fractional_days) -def seconds_into_day(time): - """Takes a time in fractional days and returns number of seconds since - the start of the current day. + +def days_in_year(year): + """Returns the number of days in a year. Parameters ---------- - time: float - The time as a float. + year: int + The year to search. Returns ------- - int - The day's duration in seconds. + days : int + The number of days that year. """ - return(int(round(86400.0 * (time % 1)))) + days = 365 + + if (is_leap_year(year)): + days += 1 + + return(days) def days_to_seconds(days): @@ -133,37 +192,35 @@ def days_to_seconds(days): return(int(round(86400 * days))) -def seconds_to_days(seconds): - """Takes a time in integer seconds and converts it into fractional - days. +def display_date(mjd): + """Returns a string representation of the date represented by a + modified Julian date. Parameters ---------- - seconds: int - The number of seconds. + mjd: float + The modified julian day. Returns ------- - float - The number of days as a float. + str + The MJD as a string. """ - return(seconds / 86400.0) - - -def round_to_second(time): - """Rounds a time in days to the nearest second. + # adjust to number of days since Dec. 31, 1857 + int_days = int(floor(321.0 + mjd)) + # seconds_in_day = seconds_into_day(mjd) + fractional_day = mjd % 1 - Parameters - ---------- - time: int - The number of days as a float. + # First compute year and day without allowing for leap years, then adjust + year = 1858 + int_days / 365 + day_of_year = int_days % 365 - leap_years(1858, year) + # handle case where leap year adjustment has made day negative + while (day_of_year < 1): + year -= 1 + day_of_year = day_of_year + days_in_year(year) - Returns - ------- - float - The number of seconds in as many days. - """ - return(round(time * 86400) / 86400.0) + year_string = '%s:' % (year) + return(year_string + display_time(day_of_year + fractional_day)) def display_time(time, force_hours=False): @@ -221,121 +278,88 @@ def display_time(time, force_hours=False): return(neg_string + day_string + hour_string + min_string + sec_string) -def time_from_string(time_string): - """Takes a string of the form ddd:hh:mm:ss and converts it to fractional - days. All subfields above seconds are optional and may be omitted if the - subfield and all higher-order ones are zero. +def integer_days(time): + """Takes a time in fractional days and returns integer component. Parameters ---------- - time_string: str - The time as a string. + time: float + The float time. Returns ------- - float - The fractional days. + int + The integer time. """ - # extract fields - fields = (string.split(time_string, ':')) - seconds = int(fields[-1]) - num_fields = len(fields) - # default to zero if not provided - minutes = hours = days = 0 - - if (num_fields > 1): - minutes = int(fields[-2]) - if (num_fields > 2): - hours = int(fields[-3]) - if (num_fields > 3): - days = int(fields[-4]) - - total_seconds = seconds + 60 * minutes + 3600 * hours + 86400 * days - return(seconds_to_days(total_seconds)) + # If time is negative, integer days is a larger negative number + return(int(floor(time))) -def display_date(mjd): - """Returns a string representation of the date represented by a - modified Julian date. +def is_leap_year(year): + """Returns True if the year is a leap year, False otherwise. Parameters ---------- - mjd: float - The modified julian day. + year: int + The year to check. Returns ------- - str - The MJD as a string. + bool + Is the year a leap year? """ - # adjust to number of days since Dec. 31, 1857 - int_days = int(floor(321.0 + mjd)) - # seconds_in_day = seconds_into_day(mjd) - fractional_day = mjd % 1 - - # First compute year and day without allowing for leap years, then adjust - year = 1858 + int_days / 365 - day_of_year = int_days % 365 - leap_years(1858, year) - # handle case where leap year adjustment has made day negative - while (day_of_year < 1): - year -= 1 - day_of_year = day_of_year + days_in_year(year) - - year_string = '%s:' % (year) - return(year_string + display_time(day_of_year + fractional_day)) + return (((year % 4 == 0) and ((year % 100 > 0) or (year % 400 == 0)))) -def compute_mjd(year, day_of_year, hour, minute, second): - """Computes a modified Julian date from a date specified as a year, - day of year, hour, minute, and second. - Arguments should be integers. +def jd_to_mjd(jd): + """Converts a Julian date to a modified Julian date. Parameters ---------- - year: int - The year. - day_of_year: int - The day. - hour: int - The hour. - minute: int - The minute. - second: int - The second. + jd: float + The true Julian day. Returns ------- float - The modified julian day. + The modified Julian day. """ - fractional_days = (hour * 3600 + minute * 60 + second) / 86400.0 - mjd_years = year - 1859 - num_leaps = leap_years(1858, year) # number of leap years since 1858 + return (jd - MJD_BASELINE) - # Add 45 days from Nov. 17 to end of 1858 - return((365 * mjd_years) + num_leaps + 45 + (day_of_year - 1) + fractional_days) +def leap_years(year1, year2): + """Returns the number of leap years between year1 and year2, + non-inclusive. -def mjd_from_string(time_string): - """Takes a string of the form yyyy.ddd:hh:mm:ss and returns an mjd. + year1 and year2 must be integers, with year2 > year1 Parameters ---------- - time_string: str - The MJD as a string. + year1: int + The start year. + year2: int + The end year. Returns ------- - float - The modified julian day. + int + The number of leap years between year1 and year2. """ - years = int(time_string[0:4]) - days = int(time_string[5:8]) - hours = int(time_string[9:11]) - minutes = int(time_string[12:14]) - seconds = int(time_string[15:17]) - return(compute_mjd(years, days, hours, minutes, seconds)) + # Find next years after year1 that are divisible by 4, 100, and 400 + next_div4 = int(4 * ceil(year1 / 4.0)) + next_div100 = int(100 * ceil(year1 / 100.0)) + next_div400 = int(400 * ceil(year1 / 400.0)) + + # Now compute number of years between year1 and year2 that are + # evenly divisible by 4, 100, 400 + div4_years = int(ceil((year2 - next_div4) / 4.0)) + div100_years = int(ceil((year2 - next_div100) / 100.0)) + div400_years = int(ceil((year2 - next_div400) / 400.0)) + + # Leap years are years divisible by 4, except for years + # divisible by 100 that are not divisible by 400 + return(div4_years - (div100_years - div400_years)) def mjd_to_jd(mjd): @@ -354,129 +378,106 @@ def mjd_to_jd(mjd): return(MJD_BASELINE + mjd) -def jd_to_mjd(jd): - """Converts a Julian date to a modified Julian date. +def mjd_from_string(time_string): + """Takes a string of the form yyyy.ddd:hh:mm:ss and returns an mjd. Parameters ---------- - jd: float - The true Julian day. + time_string: str + The MJD as a string. Returns ------- float - The modified Julian day. - """ - return (jd - MJD_BASELINE) - - -class Interval(object): - """Class to represent a simple temporal interval. + The modified julian day. """ + years = int(time_string[0:4]) + days = int(time_string[5:8]) + hours = int(time_string[9:11]) + minutes = int(time_string[12:14]) + seconds = int(time_string[15:17]) - def __init__(self, start, end): - """Constructor for an interval. - - Parameters - ---------- - start: float - The start time. - end: float - The end time. - """ - self.start = start - self.end = end - - def __str__(self): - """Returns a string representation of the interval.""" - return('Interval: start: %s, end: %s' % (display_date(self.start), - display_date(self.end))) - - def start_time(self): - """Returns the start of the interval.""" - return(self.start) - - def end_time(self): - """Returns the end of the interval.""" - return(self.end) + return(compute_mjd(years, days, hours, minutes, seconds)) - def duration(self): - """Returns the duration of an interval in fractional days.""" - return(self.end_time() - self.start_time()) - def temporal_relationship(self, time): - """Returns the temporal relationship between an interval and an - absolute time. +def round_to_second(time): + """Rounds a time in days to the nearest second. - Returns 'before' if the interval ends at or before the time, - 'after' if the interval begins at or after the time, - 'includes' if the time occurs during the interval. + Parameters + ---------- + time: int + The number of days as a float. - Parameters - ---------- - time: float - The time. + Returns + ------- + float + The number of seconds in as many days. + """ + return(round(time * 86400) / 86400.0) - Returns - ------- - rel : str - The temporal relationship. - """ - if (self.end_time() <= time): - rel = 'before' - elif (self.start_time() >= time): - rel = 'after' - else: - rel = 'includes' - return(rel) +def seconds_into_day(time): + """Takes a time in fractional days and returns number of seconds since + the start of the current day. + Parameters + ---------- + time: float + The time as a float. -class FlexibleInterval(Interval): - """Class to represent an interval with flexibility on when it can - start and end. + Returns + ------- + int + The day's duration in seconds. """ + return(int(round(86400.0 * (time % 1)))) - def __init__(self, est, lst, let): - """Constructor for a FlexibileInterval. - - Parameters - ---------- - est: float - Earliest start time (mjd). - lst: float - Latest start time (mjd). - let: float - Latest end time (mjd). - """ - self.est = est - self.lst = lst - self.let = let - def __str__(self): - """Returns a string representation of the FlexibleInterval.""" - txt = (display_date(self.est), display_date(self.lst), - display_date(self.let)) - return('FlexibleInterval: EST: %s, LST: %s, LET: %s' % txt) +def seconds_to_days(seconds): + """Takes a time in integer seconds and converts it into fractional + days. - def start_time(self): - """Returns the start of the FlexibleInterval.""" + Parameters + ---------- + seconds: int + The number of seconds. - return(self.est) + Returns + ------- + float + The number of days as a float. + """ + return(seconds / 86400.0) - def end_time(self): - """Returns the end of the FlexibleInterval.""" - return(self.let) +def time_from_string(time_string): + """Takes a string of the form ddd:hh:mm:ss and converts it to fractional + days. All subfields above seconds are optional and may be omitted if the + subfield and all higher-order ones are zero. - def flexibility(self): - """Returns the flexibility of the FlexibleInterval, in - fractional days.""" + Parameters + ---------- + time_string: str + The time as a string. - return(self.lst - self.est) + Returns + ------- + float + The fractional days. + """ + # extract fields + fields = (string.split(time_string, ':')) + seconds = int(fields[-1]) + num_fields = len(fields) + # default to zero if not provided + minutes = hours = days = 0 - def maximum_duration(self): - """Returns the maximum duration of the FlexibleInterval, in - fractional days.""" + if (num_fields > 1): + minutes = int(fields[-2]) + if (num_fields > 2): + hours = int(fields[-3]) + if (num_fields > 3): + days = int(fields[-4]) - return(self.let - self.lst) + total_seconds = seconds + 60 * minutes + 3600 * hours + 86400 * days + return(seconds_to_days(total_seconds)) diff --git a/exoctk/contam_visibility/visibilityPA.py b/exoctk/contam_visibility/visibilityPA.py index 39c22f0e..3bbf00e7 100755 --- a/exoctk/contam_visibility/visibilityPA.py +++ b/exoctk/contam_visibility/visibilityPA.py @@ -15,9 +15,8 @@ from astropy.table import Table from astropy.time import Time -from bokeh.plotting import figure, ColumnDataSource from bokeh.models import HoverTool, ranges -from bokeh.models.widgets import Panel, Tabs +from bokeh.plotting import figure, ColumnDataSource import matplotlib.dates as mdates import numpy as np @@ -342,8 +341,7 @@ def using_gtvt( # Making the output table # Creating new lists w/o the NaN values - v3minnan, v3maxnan, paNomnan, paMinnan, paMaxnan, gdnan, mjds = \ - [], [], [], [], [], [], [] + v3minnan, v3maxnan, paNomnan, paMinnan, paMaxnan, gdnan = [], [], [], [], [], [] for vmin, vmax, pnom, pmin, pmax, date in zip( v3min, v3max, paNom, paMin, paMax, gd): From 45fd9d28a6083f5debe1ee57a2d8b42c27479f0a Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Thu, 3 Jun 2021 10:31:56 -0400 Subject: [PATCH 03/20] PEP8 fixes for exoctk_app modules --- exoctk/exoctk_app/app_exoctk.py | 1013 +++++++++++++------------- exoctk/exoctk_app/form_validation.py | 114 +-- 2 files changed, 553 insertions(+), 574 deletions(-) diff --git a/exoctk/exoctk_app/app_exoctk.py b/exoctk/exoctk_app/app_exoctk.py index c4f8ac80..0d3ed283 100644 --- a/exoctk/exoctk_app/app_exoctk.py +++ b/exoctk/exoctk_app/app_exoctk.py @@ -1,21 +1,21 @@ from functools import wraps -import os -import json import io +import json +import os from pkg_resources import resource_filename from astropy.coordinates import SkyCoord import astropy.table as at from astropy.time import Time import astropy.units as u -from bokeh.resources import INLINE from bokeh.embed import components +from bokeh.resources import INLINE import flask -from flask import Flask, Response, send_from_directory -from flask import request, send_file, make_response, render_template +from flask import Flask, make_response, render_template, Response, request, send_file import form_validation as fv import numpy as np +from exoctk import log_exoctk from exoctk.contam_visibility import visibilityPA as vpa from exoctk.contam_visibility import field_simulator as fs from exoctk.contam_visibility import contamination_figure as cf @@ -23,15 +23,10 @@ from exoctk.forward_models.forward_models import fortney_grid, generic_grid from exoctk.groups_integrations.groups_integrations import perform_calculation from exoctk.limb_darkening import limb_darkening_fit as lf -from exoctk.utils import filter_table, get_env_variables, get_target_data, get_canonical_name from exoctk.modelgrid import ModelGrid from exoctk.phase_constraint_overlap.phase_constraint_overlap import phase_overlap_constraint, calculate_pre_duration -from exoctk import log_exoctk from exoctk.throughputs import Throughput - -from matplotlib.backends.backend_pdf import PdfPages - -from svo_filters import svo +from exoctk.utils import filter_table, get_env_variables, get_target_data, get_canonical_name # FLASK SET UP app_exoctk = Flask(__name__) @@ -40,7 +35,6 @@ app_exoctk.config['CACHE_TYPE'] = 'null' app_exoctk.config['SECRET_KEY'] = 'Thisisasecret!' - # Load the database to log all form submissions if get_env_variables()['exoctklog_dir'] is None: dbpath = ':memory:' @@ -54,208 +48,307 @@ DB = None -# Redirect to the index -@app_exoctk.route('/') -@app_exoctk.route('/index') -def index(): - """Returns the rendered index page - +def _param_fort_validation(args): + """Validates the input parameters for the forward models + Returns ------- - ``flask.render_template`` obj - The rendered template for the index page. - + input_args : dict + Dictionary with the input parameters for the forward models. """ - return render_template('index.html') + temp = args.get('ptemp', 1000) + chem = args.get('pchem', 'noTiO') + cloud = args.get('cloud', '0') + pmass = args.get('pmass', '1.5') + m_unit = args.get('m_unit', 'M_jup') + reference_radius = args.get('refrad', 1) + r_unit = args.get('r_unit', 'R_jup') + rstar = args.get('rstar', 1) + rstar_unit = args.get('rstar_unit', 'R_sun') + + input_args = {'temp': temp, 'chem': chem, 'cloud': cloud, 'pmass': pmass, + 'm_unit': m_unit, 'reference_radius': reference_radius, + 'r_unit': r_unit, 'rstar': rstar, 'rstar_unit': rstar_unit} + + return input_args + + +@app_exoctk.route('/atmospheric_retrievals') +def atmospheric_retrievals(): + """A landing page for the atmospheric_retrievals tools""" + + return render_template('atmospheric_retrievals.html') + + +def authenticate(): + """Sends a 401 response that enables basic auth""" + + return Response('Could not verify your access level for that URL.\n' + 'You have to login with proper credentials', 401, + {'WWW-Authenticate': 'Basic realm="Login Required"'}) + + +def check_auth(username, password): + """This function is called to check if a username password combination is + valid + + Parameters + ---------- + username: str + The username + password: str + The password + """ + + return username == 'admin' and password == 'secret' + + +@app_exoctk.route('/contam_visibility', methods=['GET', 'POST']) +def contam_visibility(): + """The contamination and visibility form page -@app_exoctk.route('/limb_darkening', methods=['GET', 'POST']) -def limb_darkening(): - """Returns the rendered limb darkening form page. - Returns ------- ``flask.render_template`` obj - The rendered template for the limb-darkening page. - + The rendered template for the contamination and visibility page. """ + # Load default form - form = fv.LimbDarkeningForm() + form = fv.ContamVisForm() + form.calculate_contam_submit.disabled = False + + if request.method == 'GET': + + # http://0.0.0.0:5000/contam_visibility?ra=24.354208334287005&dec=-45.677930555343636&target=WASP-18%20b + target_name = request.args.get('target') + form.targname.data = target_name + + ra = request.args.get('ra') + form.ra.data = ra + + dec = request.args.get('dec') + form.dec.data = dec + + return render_template('contam_visibility.html', form=form) # Reload page with stellar data from ExoMAST if form.resolve_submit.data: if form.targname.data.strip() != '': + # Resolve the target in exoMAST try: - - # Resolve the target in exoMAST form.targname.data = get_canonical_name(form.targname.data) - data, target_url = get_target_data(form.targname.data) + data, url = get_target_data(form.targname.data) - # Update the form data - form.feh.data = data.get('Fe/H') - form.teff.data = data.get('Teff') - form.logg.data = data.get('stellar_gravity') - form.target_url.data = str(target_url) + # Update the coordinates + ra_deg = data.get('RA') + dec_deg = data.get('DEC') + + # Set the form values + form.ra.data = ra_deg + form.dec.data = dec_deg + form.target_url.data = url except Exception: form.target_url.data = '' form.targname.errors = ["Sorry, could not resolve '{}' in exoMAST.".format(form.targname.data)] # Send it back to the main page - return render_template('limb_darkening.html', form=form) + return render_template('contam_visibility.html', form=form) - # Reload page with appropriate filter data - if form.filter_submit.data: + # Reload page with appropriate mode data + if form.mode_submit.data: - kwargs = {} - if form.bandpass.data == 'tophat': - kwargs['n_bins'] = 1 - kwargs['pixels_per_bin'] = 100 - kwargs['wave_min'] = 1 * u.um - kwargs['wave_max'] = 2 * u.um + # Update the button + if ('NIRCam' in form.inst.data) or (form.inst.data == 'MIRI') or (form.inst.data == 'NIRSpec'): + form.calculate_contam_submit.disabled = True + else: + form.calculate_contam_submit.disabled = False - # Get the filter - bandpass = Throughput(form.bandpass.data, **kwargs) + # Send it back to the main page + return render_template('contam_visibility.html', form=form) - # Update the form data - form.wave_min.data = bandpass.wave_min.value - form.wave_max.data = bandpass.wave_max.value + if form.validate_on_submit() and (form.calculate_submit.data or form.calculate_contam_submit.data): - # Send it back to the main page - return render_template('limb_darkening.html', form=form) + try: - # Update validation values after a model grid is selected - if form.modelgrid_submit.data: + # Log the form inputs + log_exoctk.log_form_input(request.form, 'contam_visibility', DB) - # Load the modelgrid - mg = ModelGrid(form.modeldir.data, resolution=500) - teff_rng = mg.Teff_vals.min(), mg.Teff_vals.max() - logg_rng = mg.logg_vals.min(), mg.logg_vals.max() - feh_rng = mg.FeH_vals.min(), mg.FeH_vals.max() + # Make plot + title = form.targname.data or ', '.join([str(form.ra.data), str(form.dec.data)]) + pG, pB, dates, vis_plot, table, badPAs = vpa.using_gtvt(str(form.ra.data), + str(form.dec.data), + form.inst.data.split(' ')[0], + targetName=str(title)) - # Update the validation parameters by setting validator attributes - setattr(form.teff.validators[1], 'min', float(teff_rng[0])) - setattr(form.teff.validators[1], 'max', float(teff_rng[1])) - setattr(form.teff.validators[1], 'message', 'Effective temperature must be between {} and {}'.format(*teff_rng)) - setattr(form.logg.validators[1], 'min', float(logg_rng[0])) - setattr(form.logg.validators[1], 'max', float(logg_rng[1])) - setattr(form.logg.validators[1], 'message', 'Surface gravity must be between {} and {}'.format(*logg_rng)) - setattr(form.feh.validators[1], 'min', float(feh_rng[0])) - setattr(form.feh.validators[1], 'max', float(feh_rng[1])) - setattr(form.feh.validators[1], 'message', 'Metallicity must be between {} and {}'.format(*feh_rng)) + # Make output table + fh = io.StringIO() + table.write(fh, format='csv', delimiter=',') + visib_table = fh.getvalue() - # Send it back to the main page - return render_template('limb_darkening.html', form=form) + # Get scripts + vis_js = INLINE.render_js() + vis_css = INLINE.render_css() + vis_script, vis_div = components(vis_plot) - # Validate form and submit for results - if form.validate_on_submit() and form.calculate_submit.data: + # Contamination plot too + if form.calculate_contam_submit.data: - # Form inputs for logging - form_input = dict(request.form) + # First convert ra and dec to HH:MM:SS + ra_deg, dec_deg = float(form.ra.data), float(form.dec.data) + sc = SkyCoord(ra_deg, dec_deg, unit='deg') + ra_dec = sc.to_string('hmsdms') + ra_hms, dec_dms = ra_dec.split(' ')[0], ra_dec.split(' ')[1] - # Get the stellar parameters - star_params = [float(form.teff.data), float(form.logg.data), float(form.feh.data)] + # Make field simulation + contam_cube = fs.fieldSim(ra_hms, dec_dms, form.inst.data, binComp=form.companion.data) + contam_plot = cf.contam(contam_cube, form.inst.data, targetName=str(title), paRange=[int(form.pa_min.data), int(form.pa_max.data)], badPAs=badPAs, fig='bokeh') - # Load the model grid - model_grid = ModelGrid(form.modeldir.data, resolution=500) - form.modeldir.data = [j for i, j in form.modeldir.choices if i == form.modeldir.data][0] + # Get scripts + contam_js = INLINE.render_js() + contam_css = INLINE.render_css() + contam_script, contam_div = components(contam_plot) - # Grism details - kwargs = {'n_bins': form.n_bins.data, 'wave_min': form.wave_min.data * u.um, 'wave_max': form.wave_max.data * u.um} + else: - # Make filter object and plot - bandpass = Throughput(form.bandpass.data, **kwargs) - bk_plot = bandpass.plot(draw=False) - bk_plot.plot_width = 580 - bk_plot.plot_height = 280 - js_resources = INLINE.render_js() - css_resources = INLINE.render_css() - filt_script, filt_plot = components(bk_plot) + contam_script = contam_div = contam_js = contam_css = '' - # Trim the grid to nearby grid points to speed up calculation - # full_rng = [model_grid.Teff_vals, model_grid.logg_vals, model_grid.FeH_vals] - # trim_rng = find_closest(full_rng, star_params, n=1, values=True) + return render_template('contam_visibility_results.html', + form=form, vis_plot=vis_div, + vis_table=visib_table, + vis_script=vis_script, vis_js=vis_js, + vis_css=vis_css, contam_plot=contam_div, + contam_script=contam_script, + contam_js=contam_js, + contam_css=contam_css) - # Calculate the coefficients for each profile - ld = lf.LDC(model_grid) - for prof in form.profiles.data: - ld.calculate(*star_params, prof, mu_min=float(form.mu_min.data), bandpass=bandpass) + except Exception as e: + err = 'The following error occurred: ' + str(e) + return render_template('groups_integrations_error.html', err=err) - # Draw tabbed figure - final = ld.plot_tabs() + return render_template('contam_visibility.html', form=form) - # Get HTML - script, div = components(final) - # Store the tables as a string - keep_cols = ['Teff', 'logg', 'FeH', 'profile', 'filter', 'wave_min', 'wave_eff', 'wave_max', 'c1', 'e1', 'c2', 'e2', 'c3', 'e3', 'c4', 'e4'] - print_table = ld.results[[col for col in keep_cols if col in ld.results.colnames]] - file_as_string = '\n'.join(print_table.pformat(max_lines=-1, max_width=-1)) +@app_exoctk.route('/download', methods=['POST']) +def exoctk_savefile(): + """Save results to file - # Make a table for each profile with a row for each wavelength bin - profile_tables = [] - for profile in form.profiles.data: + Returns + ------- + ``flask.make_response`` obj + Returns response including results in txt form. + """ - # Make LaTeX for polynomials - latex = lf.ld_profile(profile, latex=True) - poly = '\({}\)'.format(latex).replace('*', '\cdot').replace('\e', 'e') + file_as_string = eval(request.form['file_as_string']) - # Make the table into LaTeX - table = filter_table(ld.results, profile=profile) - co_cols = [c for c in ld.results.colnames if (c.startswith('c') or c.startswith('e')) and len(c) == 2 and not np.all([np.isnan(i) for i in table[c]])] - table = table[['wave_eff', 'wave_min', 'wave_max'] + co_cols] - table.rename_column('wave_eff', '\(\lambda_\mbox{eff}\hspace{5px}(\mu m)\)') - table.rename_column('wave_min', '\(\lambda_\mbox{min}\hspace{5px}(\mu m)\)') - table.rename_column('wave_max', '\(\lambda_\mbox{max}\hspace{5px}(\mu m)\)') + response = make_response(file_as_string) + response.headers["Content-type"] = 'text; charset=utf-8' + response.headers["Content-Disposition"] = "attachment; filename=ExoCTK_results.txt" - # Add the results to the lists - html_table = '\n'.join(table.pformat(max_width=-1, max_lines=-1, html=True)).replace(' 0: + log_exoctk.log_form_input(args, 'fortney', DB) + + return html + + +@app_exoctk.route('/fortney_download') +def fortney_download(): + """Download the fortney grid data""" + + fortney_data = os.path.join(get_env_variables()['fortgrid_dir'], 'fortney_grid.db') + return send_file(fortney_data, attachment_filename='fortney_grid.db', as_attachment=True) + + +@app_exoctk.route('/generic', methods=['GET', 'POST']) +def generic(): + """ + Pull up Generic Grid plot the results and download -@app_exoctk.route('/limb_darkening_error', methods=['GET', 'POST']) -def limb_darkening_error(): - """The limb darkening error page - Returns ------- - ``flask.render_template`` obj - The rendered template for the limb-darkening error page. - + html: ``flask.render_template`` obj + The rendered template for the generic grid page. """ - return render_template('limb_darkening_error.html') + # Grab the inputs arguments from the URL + args = dict(flask.request.args) + fig, fh, closest_match, error_message = generic_grid(args) + + # Write table string + table_string = fh.getvalue() + + # Web-ify bokeh plot + js_resources = INLINE.render_js() + css_resources = INLINE.render_css() + + script, div = components(fig) + + html = flask.render_template('generic.html', + inputs=args, + closest_match=closest_match, + error_message=error_message, + table_string=table_string, + plot_script=script, + plot_div=div, + js_resources=js_resources, + css_resources=css_resources, + ) + + # Log the form inputs + if len(args) > 0: + log_exoctk.log_form_input(args, 'generic', DB) + + return html @app_exoctk.route('/groups_integrations', methods=['GET', 'POST']) def groups_integrations(): """The groups and integrations calculator form page - + Returns ------- ``flask.render_template`` obj - The rendered template for the Groups & Integrations calculator page. + The rendered template for the Groups & Integrations calculator page. """ # Print out pandeia sat values @@ -419,427 +512,202 @@ def groups_integrations(): return render_template('groups_integrations.html', form=form, sat_data=sat_data) -@app_exoctk.route('/contam_visibility', methods=['GET', 'POST']) -def contam_visibility(): - """The contamination and visibility form page - +# Redirect to the index +@app_exoctk.route('/') +@app_exoctk.route('/index') +def index(): + """Returns the rendered index page + Returns ------- ``flask.render_template`` obj - The rendered template for the contamination and visibility page. - + The rendered template for the index page. """ - # Load default form - form = fv.ContamVisForm() - form.calculate_contam_submit.disabled = False + return render_template('index.html') - if request.method == 'GET': - # http://0.0.0.0:5000/contam_visibility?ra=24.354208334287005&dec=-45.677930555343636&target=WASP-18%20b - target_name = request.args.get('target') - form.targname.data = target_name +@app_exoctk.route('/lightcurve_fitting') +def lightcurve_fitting(): + """A landing page for the lightcurve_fitting tool""" - ra = request.args.get('ra') - form.ra.data = ra + return render_template('lightcurve_fitting.html') - dec = request.args.get('dec') - form.dec.data = dec - return render_template('contam_visibility.html', form=form) +@app_exoctk.route('/limb_darkening', methods=['GET', 'POST']) +def limb_darkening(): + """Returns the rendered limb darkening form page. + + Returns + ------- + ``flask.render_template`` obj + The rendered template for the limb-darkening page. + """ + # Load default form + form = fv.LimbDarkeningForm() # Reload page with stellar data from ExoMAST if form.resolve_submit.data: if form.targname.data.strip() != '': - # Resolve the target in exoMAST try: - form.targname.data = get_canonical_name(form.targname.data) - data, url = get_target_data(form.targname.data) - # Update the coordinates - ra_deg = data.get('RA') - dec_deg = data.get('DEC') + # Resolve the target in exoMAST + form.targname.data = get_canonical_name(form.targname.data) + data, target_url = get_target_data(form.targname.data) - # Set the form values - form.ra.data = ra_deg - form.dec.data = dec_deg - form.target_url.data = url + # Update the form data + form.feh.data = data.get('Fe/H') + form.teff.data = data.get('Teff') + form.logg.data = data.get('stellar_gravity') + form.target_url.data = str(target_url) except Exception: form.target_url.data = '' form.targname.errors = ["Sorry, could not resolve '{}' in exoMAST.".format(form.targname.data)] # Send it back to the main page - return render_template('contam_visibility.html', form=form) - - # Reload page with appropriate mode data - if form.mode_submit.data: - - # Update the button - if ('NIRCam' in form.inst.data) or (form.inst.data == 'MIRI') or (form.inst.data == 'NIRSpec'): - form.calculate_contam_submit.disabled = True - else: - form.calculate_contam_submit.disabled = False - - # Send it back to the main page - return render_template('contam_visibility.html', form=form) - - if form.validate_on_submit() and (form.calculate_submit.data or form.calculate_contam_submit.data): - - try: - - # Log the form inputs - log_exoctk.log_form_input(request.form, 'contam_visibility', DB) - - # Make plot - title = form.targname.data or ', '.join([str(form.ra.data), str(form.dec.data)]) - pG, pB, dates, vis_plot, table, badPAs = vpa.using_gtvt(str(form.ra.data), - str(form.dec.data), - form.inst.data.split(' ')[0], - targetName=str(title)) - - # Make output table - fh = io.StringIO() - table.write(fh, format='csv', delimiter=',') - visib_table = fh.getvalue() - - # Get scripts - vis_js = INLINE.render_js() - vis_css = INLINE.render_css() - vis_script, vis_div = components(vis_plot) - - # Contamination plot too - if form.calculate_contam_submit.data: - - # First convert ra and dec to HH:MM:SS - ra_deg, dec_deg = float(form.ra.data), float(form.dec.data) - sc = SkyCoord(ra_deg, dec_deg, unit='deg') - ra_dec = sc.to_string('hmsdms') - ra_hms, dec_dms = ra_dec.split(' ')[0], ra_dec.split(' ')[1] - - # Make field simulation - contam_cube = fs.fieldSim(ra_hms, dec_dms, form.inst.data, binComp=form.companion.data) - contam_plot = cf.contam(contam_cube, form.inst.data, targetName=str(title), paRange=[int(form.pa_min.data), int(form.pa_max.data)], badPAs=badPAs, fig='bokeh') - - # Get scripts - contam_js = INLINE.render_js() - contam_css = INLINE.render_css() - contam_script, contam_div = components(contam_plot) - - else: - - contam_script = contam_div = contam_js = contam_css = '' - - return render_template('contam_visibility_results.html', - form=form, vis_plot=vis_div, - vis_table=visib_table, - vis_script=vis_script, vis_js=vis_js, - vis_css=vis_css, contam_plot=contam_div, - contam_script=contam_script, - contam_js=contam_js, - contam_css=contam_css) - - except Exception as e: - err = 'The following error occurred: ' + str(e) - return render_template('groups_integrations_error.html', err=err) - - return render_template('contam_visibility.html', form=form) - - -@app_exoctk.route('/visib_result', methods=['POST']) -def save_visib_result(): - """Save the results of the Visibility Only calculation - - Returns - ------- - ``flask.Response`` obj - flask.Response object with the results of the visibility only calculation. - - """ - - visib_table = flask.request.form['data_file'] - targname = flask.request.form['targetname'] - targname = targname.replace(' ', '_') # no spaces - instname = flask.request.form['instrumentname'] - - return flask.Response(visib_table, mimetype="text/dat", - headers={"Content-disposition": "attachment; filename={}_{}_visibility.csv".format(targname, instname)}) - -@app_exoctk.route('/contam_verify', methods=['GET', 'POST']) -def save_contam_pdf(): - """Save the results of the Contamination Science FOV - - Returns - ------- - ``flask.render_template`` obj - The rendered template (and attachment) for the Contamination FOV. - """ - - RA, DEC = '19:50:50.2400', '+48:04:51.00' - contam_pdf = contamVerify(RA, DEC, 'NIRISS', [1,2], binComp=[], PDF='', web=True) - - filename = contam_pdf.split('/')[-1] - pdf_obj = PdfPages(contam_pdf) - - return render_template(contam_pdf, filename, as_attachment=True)#, mimetype="application/pdf", as_attachment=True) - #return flask.Response(pdf_obj, mimetype="application/pdf", - # headers={"Content-disposition": "attachment; filename={}_{}_contam.pdf".format(targname, instname)}) - - -@app_exoctk.route('/download', methods=['POST']) -def exoctk_savefile(): - """Save results to file - - Returns - ------- - ``flask.make_response`` obj - Returns response including results in txt form. - - """ - - file_as_string = eval(request.form['file_as_string']) - - response = make_response(file_as_string) - response.headers["Content-type"] = 'text; charset=utf-8' - response.headers["Content-Disposition"] = "attachment; filename=ExoCTK_results.txt" - return response - - -def _param_fort_validation(args): - """Validates the input parameters for the forward models - - Returns - ------- - input_args : dict - Dictionary with the input parameters for the forward models. - """ - - temp = args.get('ptemp', 1000) - chem = args.get('pchem', 'noTiO') - cloud = args.get('cloud', '0') - pmass = args.get('pmass', '1.5') - m_unit = args.get('m_unit', 'M_jup') - reference_radius = args.get('refrad', 1) - r_unit = args.get('r_unit', 'R_jup') - rstar = args.get('rstar', 1) - rstar_unit = args.get('rstar_unit', 'R_sun') - - input_args = {'temp': temp, 'chem': chem, 'cloud': cloud, 'pmass': pmass, - 'm_unit': m_unit, 'reference_radius': reference_radius, - 'r_unit': r_unit, 'rstar': rstar, 'rstar_unit': rstar_unit} - - return input_args - - -@app_exoctk.route('/fortney', methods=['GET', 'POST']) -def fortney(): - """ - Pull up Forntey Grid plot the results and download - - Returns - ------- - html : ``flask.render_template`` obj - The rendered template for the Fortney results. - - """ - - # Grab the inputs arguments from the URL - args = flask.request.args - - input_args = _param_fort_validation(args) - fig, fh, temp_out = fortney_grid(input_args) - - table_string = fh.getvalue() - - js_resources = INLINE.render_js() - css_resources = INLINE.render_css() - - script, div = components(fig) - - html = flask.render_template('fortney.html', - plot_script=script, - plot_div=div, - js_resources=js_resources, - css_resources=css_resources, - temp=temp_out, - table_string=table_string - ) - - # Log the form inputs - if len(args) > 0: - log_exoctk.log_form_input(args, 'fortney', DB) - - return html - - -@app_exoctk.route('/generic', methods=['GET', 'POST']) -def generic(): - """ - Pull up Generic Grid plot the results and download - - Returns - ------- - html: ``flask.render_template`` obj - The rendered template for the generic grid page. - - """ - - # Grab the inputs arguments from the URL - args = dict(flask.request.args) - fig, fh, closest_match, error_message = generic_grid(args) - - # Write table string - table_string = fh.getvalue() - - # Web-ify bokeh plot - js_resources = INLINE.render_js() - css_resources = INLINE.render_css() - - script, div = components(fig) - - html = flask.render_template('generic.html', - inputs=args, - closest_match=closest_match, - error_message=error_message, - table_string=table_string, - plot_script=script, - plot_div=div, - js_resources=js_resources, - css_resources=css_resources, - ) - - # Log the form inputs - if len(args) > 0: - log_exoctk.log_form_input(args, 'generic', DB) - - return html - - -@app_exoctk.route('/fortney_result', methods=['POST']) -def save_fortney_result(): - """Save the results of the Fortney grid""" - - table_string = flask.request.form['data_file'] - return flask.Response(table_string, mimetype="text/dat", headers={"Content-disposition": "attachment; filename=fortney.dat"}) - - -@app_exoctk.route('/generic_result', methods=['POST']) -def save_generic_result(): - """Save the results of the generic grid""" - - table_string = flask.request.form['data_file'] - return flask.Response(table_string, mimetype="text/dat", headers={"Content-disposition": "attachment; filename=generic.dat"}) - - -@app_exoctk.route('/groups_integrations_download') -def groups_integrations_download(): - """Download the groups and integrations calculator data""" + return render_template('limb_darkening.html', form=form) - return send_file(resource_filename('exoctk', 'data/groups_integrations/groups_integrations_input_data.json'), mimetype="text/json", attachment_filename='groups_integrations_input_data.json', as_attachment=True) + # Reload page with appropriate filter data + if form.filter_submit.data: + kwargs = {} + if form.bandpass.data == 'tophat': + kwargs['n_bins'] = 1 + kwargs['pixels_per_bin'] = 100 + kwargs['wave_min'] = 1 * u.um + kwargs['wave_max'] = 2 * u.um -@app_exoctk.route('/fortney_download') -def fortney_download(): - """Download the fortney grid data""" + # Get the filter + bandpass = Throughput(form.bandpass.data, **kwargs) - fortney_data = os.path.join(get_env_variables()['fortgrid_dir'], 'fortney_grid.db') - return send_file(fortney_data, attachment_filename='fortney_grid.db', as_attachment=True) + # Update the form data + form.wave_min.data = bandpass.wave_min.value + form.wave_max.data = bandpass.wave_max.value + # Send it back to the main page + return render_template('limb_darkening.html', form=form) -def check_auth(username, password): - """This function is called to check if a username password combination is - valid + # Update validation values after a model grid is selected + if form.modelgrid_submit.data: - Parameters - ---------- - username: str - The username - password: str - The password - """ + # Load the modelgrid + mg = ModelGrid(form.modeldir.data, resolution=500) + teff_rng = mg.Teff_vals.min(), mg.Teff_vals.max() + logg_rng = mg.logg_vals.min(), mg.logg_vals.max() + feh_rng = mg.FeH_vals.min(), mg.FeH_vals.max() - return username == 'admin' and password == 'secret' + # Update the validation parameters by setting validator attributes + setattr(form.teff.validators[1], 'min', float(teff_rng[0])) + setattr(form.teff.validators[1], 'max', float(teff_rng[1])) + setattr(form.teff.validators[1], 'message', 'Effective temperature must be between {} and {}'.format(*teff_rng)) + setattr(form.logg.validators[1], 'min', float(logg_rng[0])) + setattr(form.logg.validators[1], 'max', float(logg_rng[1])) + setattr(form.logg.validators[1], 'message', 'Surface gravity must be between {} and {}'.format(*logg_rng)) + setattr(form.feh.validators[1], 'min', float(feh_rng[0])) + setattr(form.feh.validators[1], 'max', float(feh_rng[1])) + setattr(form.feh.validators[1], 'message', 'Metallicity must be between {} and {}'.format(*feh_rng)) + # Send it back to the main page + return render_template('limb_darkening.html', form=form) -def authenticate(): - """Sends a 401 response that enables basic auth""" + # Validate form and submit for results + if form.validate_on_submit() and form.calculate_submit.data: - return Response('Could not verify your access level for that URL.\n' - 'You have to login with proper credentials', 401, - {'WWW-Authenticate': 'Basic realm="Login Required"'}) + # Form inputs for logging + form_input = dict(request.form) + # Get the stellar parameters + star_params = [float(form.teff.data), float(form.logg.data), float(form.feh.data)] -def requires_auth(page): - """Requires authentication for a page before loading + # Load the model grid + model_grid = ModelGrid(form.modeldir.data, resolution=500) + form.modeldir.data = [j for i, j in form.modeldir.choices if i == form.modeldir.data][0] - Parameters - ---------- - page: function - The function that sets a route + # Grism details + kwargs = {'n_bins': form.n_bins.data, 'wave_min': form.wave_min.data * u.um, 'wave_max': form.wave_max.data * u.um} - Returns - ------- - function - The decorated route - """ + # Make filter object and plot + bandpass = Throughput(form.bandpass.data, **kwargs) + bk_plot = bandpass.plot(draw=False) + bk_plot.plot_width = 580 + bk_plot.plot_height = 280 + js_resources = INLINE.render_js() + css_resources = INLINE.render_css() + filt_script, filt_plot = components(bk_plot) - @wraps(page) - def decorated(*args, **kwargs): - auth = request.authorization - if not auth or not check_auth(auth.username, auth.password): - return authenticate() - return page(*args, **kwargs) - return decorated + # Trim the grid to nearby grid points to speed up calculation + # full_rng = [model_grid.Teff_vals, model_grid.logg_vals, model_grid.FeH_vals] + # trim_rng = find_closest(full_rng, star_params, n=1, values=True) + # Calculate the coefficients for each profile + ld = lf.LDC(model_grid) + for prof in form.profiles.data: + ld.calculate(*star_params, prof, mu_min=float(form.mu_min.data), bandpass=bandpass) -@app_exoctk.route('/admin') -@requires_auth -def secret_page(): - """Shhhhh! This is a secret page of admin stuff - - Returns - ------- - ``flask.render_template`` obj - The rendered template for the admin page. + # Draw tabbed figure + final = ld.plot_tabs() - """ + # Get HTML + script, div = components(final) - tables = [i[0] for i in DB.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()] + # Store the tables as a string + keep_cols = ['Teff', 'logg', 'FeH', 'profile', 'filter', 'wave_min', 'wave_eff', 'wave_max', 'c1', 'e1', 'c2', 'e2', 'c3', 'e3', 'c4', 'e4'] + print_table = ld.results[[col for col in keep_cols if col in ld.results.colnames]] + file_as_string = '\n'.join(print_table.pformat(max_lines=-1, max_width=-1)) - log_tables = [] - for table in tables: + # Make a table for each profile with a row for each wavelength bin + profile_tables = [] + for profile in form.profiles.data: - try: - data = log_exoctk.view_log(DB, table) + # Make LaTeX for polynomials + latex = lf.ld_profile(profile, latex=True) + poly = '\({}\)'.format(latex).replace('*', '\cdot').replace('\e', 'e') + + # Make the table into LaTeX + table = filter_table(ld.results, profile=profile) + co_cols = [c for c in ld.results.colnames if (c.startswith('c') or c.startswith('e')) and len(c) == 2 and not np.all([np.isnan(i) for i in table[c]])] + table = table[['wave_eff', 'wave_min', 'wave_max'] + co_cols] + table.rename_column('wave_eff', '\(\lambda_\mbox{eff}\hspace{5px}(\mu m)\)') + table.rename_column('wave_min', '\(\lambda_\mbox{min}\hspace{5px}(\mu m)\)') + table.rename_column('wave_max', '\(\lambda_\mbox{max}\hspace{5px}(\mu m)\)') # Add the results to the lists - html_table = '\n'.join(data.pformat(max_width=500, html=True)).replace(' 1.) or (form.eccentricity.data < 0.): - form.eccentricity.data = None - if np.abs(form.omega.data)>360.: - form.omega.data = None - if (form.inclination.data < 0) or (form.inclination.data>90.): - form.inclination.data = None - """ + # Send it back to the main page return render_template('phase_constraint.html', form=form) +def requires_auth(page): + """Requires authentication for a page before loading + + Parameters + ---------- + page: function + The function that sets a route + + Returns + ------- + function + The decorated route + """ + + @wraps(page) + def decorated(*args, **kwargs): + auth = request.authorization + if not auth or not check_auth(auth.username, auth.password): + return authenticate() + return page(*args, **kwargs) + + return decorated + + +@app_exoctk.route('/contam_verify', methods=['GET', 'POST']) +def save_contam_pdf(): + """Save the results of the Contamination Science FOV + + Returns + ------- + ``flask.render_template`` obj + The rendered template (and attachment) for the Contamination FOV. + """ + + RA, DEC = '19:50:50.2400', '+48:04:51.00' + contam_pdf = contamVerify(RA, DEC, 'NIRISS', [1, 2], binComp=[], PDF='', web=True) + filename = contam_pdf.split('/')[-1] + + return render_template(contam_pdf, filename, as_attachment=True) + + +@app_exoctk.route('/fortney_result', methods=['POST']) +def save_fortney_result(): + """Save the results of the Fortney grid""" + + table_string = flask.request.form['data_file'] + return flask.Response(table_string, mimetype="text/dat", headers={"Content-disposition": "attachment; filename=fortney.dat"}) + + +@app_exoctk.route('/generic_result', methods=['POST']) +def save_generic_result(): + """Save the results of the generic grid""" + + table_string = flask.request.form['data_file'] + return flask.Response(table_string, mimetype="text/dat", headers={"Content-disposition": "attachment; filename=generic.dat"}) + + +@app_exoctk.route('/groups_integrations_download') +def groups_integrations_download(): + """Download the groups and integrations calculator data""" + + return send_file(resource_filename('exoctk', 'data/groups_integrations/groups_integrations_input_data.json'), mimetype="text/json", attachment_filename='groups_integrations_input_data.json', as_attachment=True) + + +@app_exoctk.route('/visib_result', methods=['POST']) +def save_visib_result(): + """Save the results of the Visibility Only calculation + + Returns + ------- + ``flask.Response`` obj + flask.Response object with the results of the visibility only calculation. + """ + + visib_table = flask.request.form['data_file'] + targname = flask.request.form['targetname'] + targname = targname.replace(' ', '_') # no spaces + instname = flask.request.form['instrumentname'] + + return flask.Response(visib_table, mimetype="text/dat", + headers={"Content-disposition": "attachment; filename={}_{}_visibility.csv".format(targname, instname)}) + + +@app_exoctk.route('/admin') +@requires_auth +def secret_page(): + """Shhhhh! This is a secret page of admin stuff + + Returns + ------- + ``flask.render_template`` obj + The rendered template for the admin page. + """ + + tables = [i[0] for i in DB.execute("SELECT name FROM sqlite_master WHERE type='table'").fetchall()] + + log_tables = [] + for table in tables: + + try: + data = log_exoctk.view_log(DB, table) + + # Add the results to the lists + html_table = '\n'.join(data.pformat(max_width=500, html=True)).replace(' Date: Thu, 3 Jun 2021 11:10:17 -0400 Subject: [PATCH 04/20] Fixed docstring length to comply to PEP257 --- exoctk/contam_visibility/astro_funcx.py | 8 +- .../contam_visibility/contamination_figure.py | 20 +-- exoctk/contam_visibility/ephemeris_old2x.py | 10 +- .../contam_visibility/f_visibilityPeriods.py | 22 +-- exoctk/contam_visibility/field_simulator.py | 35 ++-- exoctk/contam_visibility/math_extensionsx.py | 63 +++---- exoctk/contam_visibility/miniTools.py | 39 +++-- exoctk/contam_visibility/quaternionx.py | 155 ++++++++++-------- exoctk/contam_visibility/time_extensionsx.py | 10 +- exoctk/contam_visibility/visibilityPA.py | 15 +- exoctk/exoctk_app/app_exoctk.py | 13 +- 11 files changed, 213 insertions(+), 177 deletions(-) diff --git a/exoctk/contam_visibility/astro_funcx.py b/exoctk/contam_visibility/astro_funcx.py index 366a34d8..e838835a 100755 --- a/exoctk/contam_visibility/astro_funcx.py +++ b/exoctk/contam_visibility/astro_funcx.py @@ -15,8 +15,8 @@ def delta_pa_no_roll(pos1_c1, pos1_c2, pos2_c1, pos2_c2): - """Calculates the change in position angle between two positions with no - roll about V1 + """Calculates the change in position angle between two positions + with no roll about V1 Parameters ---------- @@ -66,8 +66,8 @@ def dist(obj1_c1, obj1_c2, obj2_c1, obj2_c2): def JWST_same_ori(tgt0_c1, tgt0_c2, p0, tgt_c1, tgt_c2): - """Calculates normal orientation of second target, given first target's - orientation is normal. This is in Ecliptic coordinates! + """Calculates normal orientation of second target, given first + target's orientation is normal. This is in Ecliptic coordinates! Parameters ---------- diff --git a/exoctk/contam_visibility/contamination_figure.py b/exoctk/contam_visibility/contamination_figure.py index d213acb7..56df13a5 100755 --- a/exoctk/contam_visibility/contamination_figure.py +++ b/exoctk/contam_visibility/contamination_figure.py @@ -242,8 +242,8 @@ def contam(cube, instrument, targetName='noName', paRange=[0, 360], def miriContam(cube, paRange=[0, 360]): - """ Generates the contamination figure that will be plotted on the website - for MIRI LRS. + """ Generates the contamination figure that will be plotted on the + website for MIRI LRS. """ # Get data from FITS file if isinstance(cube, str): @@ -296,20 +296,20 @@ def miriContam(cube, paRange=[0, 360]): def nircamContam(cube, instrument, paRange=[0, 360]): - """ Generates the contamination figure that will be plotted on the website - for NIRCam Grism Time Series mode. + """ Generates the contamination figure that will be plotted on the + website for NIRCam Grism Time Series mode. PARAMETERS ---------- cube : arr or str - A 3D array of the simulated field at every Aperture Position Angle (APA). - The shape of the cube is (361, subY, subX). + A 3D array of the simulated field at every Aperture Position + Angle (APA). The shape of the cube is (361, subY, subX). or The name of an HDU .fits file sthat has the cube. instrument : str - The name of the instrument + what filter is being used. For NIRCam the - options are: 'NIRCam F322W2', 'NIRCam F444W' + The name of the instrument + what filter is being used. For + NIRCam the options are: 'NIRCam F322W2', 'NIRCam F444W' RETURNS ------- @@ -361,8 +361,8 @@ def nircamContam(cube, instrument, paRange=[0, 360]): def nirissContam(cube, paRange=[0, 360]): - """ Generates the contamination figure that will be plotted on the website - for NIRISS SOSS. + """ Generates the contamination figure that will be plotted on the + website for NIRISS SOSS. """ # Get data from FITS file if isinstance(cube, str): diff --git a/exoctk/contam_visibility/ephemeris_old2x.py b/exoctk/contam_visibility/ephemeris_old2x.py index cb84de17..36574b53 100755 --- a/exoctk/contam_visibility/ephemeris_old2x.py +++ b/exoctk/contam_visibility/ephemeris_old2x.py @@ -27,7 +27,8 @@ class Ephemeris: 12/05/2008 Switched to spline fit. 07/29/2010 Added OP window calc 08/03/2010 Got rid of degrees trig functions - Removed in_FOR, is_valid_att etc. as those are S/C dependent + Removed in_FOR, is_valid_att etc. as those are S/C + dependent """ def __init__(self, ephem_file, cnvrt=False): @@ -330,9 +331,10 @@ def OP_window(self, adate, ngc_1, ngc_2, pa, mdelta, pdelta): return (OP_min, OP_max) def pos(self, adate): - """Computes the position of the telescope at a given date using the - grid of positions of the ephemeris as a starting point and - applying a linear interpolation between the ephemeris grid points + """Computes the position of the telescope at a given date using + the grid of positions of the ephemeris as a starting point and + applying a linear interpolation between the ephemeris grid + points Parameters ---------- diff --git a/exoctk/contam_visibility/f_visibilityPeriods.py b/exoctk/contam_visibility/f_visibilityPeriods.py index e6337f89..ef038525 100755 --- a/exoctk/contam_visibility/f_visibilityPeriods.py +++ b/exoctk/contam_visibility/f_visibilityPeriods.py @@ -1,16 +1,16 @@ """ -Series of functions to compute the visibility periods for a given (RA,DEC) -with in some cases the possibility to select a PA value. +Series of functions to compute the visibility periods for a given +(RA, DEC) with in some cases the possibility to select a PA value. Functions derived from the code of Wayne Kinzel provided by Jeff Valenti Extract from the e-mail of Wayne Kinzel: -As before, the code is not officially tested, nor is it an official STScI -product. Users should be warned that the apparent position of the Sun changes -~+/-0.2 degrees epending upon where JWST is in its orbit. -So do not rely strongly on these results if the target is within ~0.2 degrees -of |ecliptic latitude| 45 degrees or 85 degrees. -For example if a target is at 84.9 degrees latitude and the tool says it is -CVZ, it may not be with the operational orbit. +As before, the code is not officially tested, nor is it an official +STScI product. Users should be warned that the apparent position of the +Sun changes ~+/-0.2 degrees epending upon where JWST is in its orbit. +So do not rely strongly on these results if the target is within ~0.2 +degrees of |ecliptic latitude| 45 degrees or 85 degrees. +For example if a target is at 84.9 degrees latitude and the tool says it +is CVZ, it may not be with the operational orbit. """ import math @@ -21,8 +21,8 @@ def f_computeDurationOfVisibilityPeriodWithPA(ephemeris, mjdmin, mjdmax, ra, dec, pa, mjdc): - """Computes the duration of a specific visibility period associated to a - given (RA,DEC), a given PA and given date + """Computes the duration of a specific visibility period associated + to a given (RA,DEC), a given PA and given date flag = 0 visibility period fully in the search interval flag = -1 start of the visibility period truncated by diff --git a/exoctk/contam_visibility/field_simulator.py b/exoctk/contam_visibility/field_simulator.py index 5256baeb..edd6c218 100755 --- a/exoctk/contam_visibility/field_simulator.py +++ b/exoctk/contam_visibility/field_simulator.py @@ -17,9 +17,9 @@ def fieldSim(ra, dec, instrument, binComp='', testing=False): - """ Wraps ``sossFieldSim``, ``gtsFieldSim``, and ``lrsFieldSim`` together. - Produces a field simulation for a target using any instrument (NIRISS, - NIRCam, or MIRI). + """ Wraps ``sossFieldSim``, ``gtsFieldSim``, and ``lrsFieldSim`` + together. Produces a field simulation for a target using any + instrument (NIRISS, NIRCam, or MIRI). Parameters ---------- @@ -34,17 +34,18 @@ def fieldSim(ra, dec, instrument, binComp='', testing=False): binComp : sequence The parameters of a binary companion. testing : bool - Shoud be ``True`` if running fieldSim for testing / troubleshooting - purposes. This will generate a matplotlib figure showing the target - FOV. The neighboring stars in this FOV will be included in the - contamination calculation (contamFig.py). + Shoud be ``True`` if running fieldSim for testing / + troubleshooting purposes. This will generate a matplotlib figure + showing the target FOV. The neighboring stars in this FOV will + be included in the contamination calculation (contamFig.py). Returns ------- simuCube : np.ndarray - The simulated data cube. Index 0 and 1 (axis=0) show the trace of - the target for orders 1 and 2 (respectively). Index 2-362 show the trace - of the target at every position angle (PA) of the instrument. + The simulated data cube. Index 0 and 1 (axis=0) show the trace + of the target for orders 1 and 2 (respectively). Index 2-362 + show the trace of the target at every position angle (PA) of the + instrument. plt.plot() : matplotlib object A plot. Only if `testing` parameter is set to True. """ @@ -83,9 +84,10 @@ def gtsFieldSim(ra, dec, filter, binComp=''): Returns ------- simuCube : np.ndarray - The simulated data cube. Index 0 and 1 (axis=0) show the trace of - the target for orders 1 and 2 (respectively). Index 2-362 show the trace - of the target at every position angle (PA) of the instrument. + The simulated data cube. Index 0 and 1 (axis=0) show the trace + of the target for orders 1 and 2 (respectively). Index 2-362 + show the trace of the target at every position angle (PA) of the + instrument. """ # Instantiate a pySIAF object siaf = pysiaf.Siaf('NIRCam') @@ -326,9 +328,10 @@ def lrsFieldSim(ra, dec, binComp=''): Returns ------- simuCube : np.ndarray - The simulated data cube. Index 0 and 1 (axis=0) show the trace of - the target for orders 1 and 2 (respectively). Index 2-362 show the trace - of the target at every position angle (PA) of the instrument. + The simulated data cube. Index 0 and 1 (axis=0) show the trace + of the target for orders 1 and 2 (respectively). Index 2-362 + show the trace of the target at every position angle (PA) of the + instrument. """ # INSTRUMENT PARAMETERS # Instantiate a pySIAF object diff --git a/exoctk/contam_visibility/math_extensionsx.py b/exoctk/contam_visibility/math_extensionsx.py index d8732cec..ce1b4a63 100755 --- a/exoctk/contam_visibility/math_extensionsx.py +++ b/exoctk/contam_visibility/math_extensionsx.py @@ -32,8 +32,8 @@ def __str__(self): return(result) def normalize(self, total=None): - """Takes a histogram and returns a new histogram that normalizes all - its values. + """Takes a histogram and returns a new histogram that normalizes + all its values. Parameters ---------- @@ -62,7 +62,8 @@ def num_items(self): return(sum([bin.count for bin in self.bins])) def retrieve_count(self, bin_index): - """Returns the number of items stored in a given bin of the histogram. + """Returns the number of items stored in a given bin of the + histogram. Parameters ---------- @@ -102,7 +103,8 @@ def __init__(self, coefficients): Parameters ---------- coefficients: sequence - The list of coefficients, starting with order 0 and increasing. + The list of coefficients, starting with order 0 and + increasing. """ self.coefficients = coefficients @@ -168,13 +170,15 @@ def area(self): return(self.length * self.width) def motion_tolerant_area(self, motion_length, motion_angle): - """Returns the area within a rectangle that can tolerate a motion in - a known direction while remaining within the rectangle. + """Returns the area within a rectangle that can tolerate a + motion in a known direction while remaining within the + rectangle. Parameters ---------- motion_length: float - Distance of motion (same units as rectangle length and width). + Distance of motion (same units as rectangle length and + width). motion_angle: float Angle in radians between the direction of motion and long direction of rectangle. @@ -418,8 +422,8 @@ def store_items(self, value, count=1): class LinearEquation(Polynomial): """Subclass of Polynomial for linear equations. - This implementation is three times faster, so Polynomial should be reserved - for higher orders.""" + This implementation is three times faster, so Polynomial should be + reserved for higher orders.""" def __init__(self, coeff0, coeff1): """Constructor for a linear equation to provide a more 'natural' @@ -465,8 +469,8 @@ def __init__(self, mean, max_boundary): Mean parameter for the Poisson distribution. max_boundary: float The largest parameter for which the probability is to - be computed. All values larger than max_boundary will be lumped - into the highest bin. + be computed. All values larger than max_boundary will be + lumped into the highest bin. """ self.mean = mean self.bins = [] @@ -489,13 +493,14 @@ def __str__(self): return(poisson_info + generic_info) def cumulative_probability(self, value): - """Returns the probability that a random variable will have a value no - greater than the one specified. + """Returns the probability that a random variable will have a + value no greater than the one specified. Parameters ---------- value: float - The value between 0 and the max_boundary of the distribution. + The value between 0 and the max_boundary of the + distribution. Returns ------- @@ -520,8 +525,8 @@ def generate_distribution(self): self.bins[-1].count = 1.0 - cum def probability(self, k): - """Computes the probability that the Poisson distribution takes on - the value k. + """Computes the probability that the Poisson distribution takes + on the value k. Value must be a nonnegative integer. @@ -689,8 +694,8 @@ def __str__(self): return('Square: side = % .2f' % (self.side)) def inner_area(self, excluded_width): - """Returns the area of the square after removing a strip of specified - width along each edge. + """Returns the area of the square after removing a strip of + specified width along each edge. Parameters ---------- @@ -741,8 +746,8 @@ def compute_rms(self): def compute_statistics(self, min_value=None, max_value=None, max_bins=None): - """Computes statistics for a StatisticalList object; must contain at - least one element. + """Computes statistics for a StatisticalList object; must + contain at least one element. Parameters ---------- @@ -829,8 +834,8 @@ def compute_variance(self): def acos2(val): - """Safe version of acos that handles invalid arguments in the same way as - asin2 + """Safe version of acos that handles invalid arguments in the same + way as asin2 Parameters ---------- @@ -848,8 +853,8 @@ def acos2(val): def asin2(val): """Safe version of asin that handles invalid arguments. - Arguments greater than 1 are truncated to 1; arguments less than -1 are - set to -1. + Arguments greater than 1 are truncated to 1; arguments less than -1 + are set to -1. Parameters ---------- @@ -915,8 +920,8 @@ def avg2(num1, num2): def combine_histograms(histograms): - """Takes a list of histograms and returns a new Histogram object that sums - the values in each bin. + """Takes a list of histograms and returns a new Histogram object + that sums the values in each bin. All histograms in the list must be identical except for the count. @@ -1100,7 +1105,8 @@ def sind(x): def stdev(numbers): - """Standard deviation of a list of numbers that represent sample values + """Standard deviation of a list of numbers that represent sample + values Parameters ---------- @@ -1136,7 +1142,8 @@ def variance(numbers): def average_histograms(histograms): - """Takes a list of histogram objects and simply averages all the bin values. + """Takes a list of histogram objects and simply averages all the bin + values. All histograms in the list must be identical except for the count. diff --git a/exoctk/contam_visibility/miniTools.py b/exoctk/contam_visibility/miniTools.py index 7aaadc8b..1eb9afaa 100644 --- a/exoctk/contam_visibility/miniTools.py +++ b/exoctk/contam_visibility/miniTools.py @@ -1,12 +1,13 @@ -""" The contamVerify mini tool will be a companion to ExoCTK's Contamination -Overlap tool, as it will visualize the Contaminaton Bokeh plots on the website. +""" The contamVerify mini tool will be a companion to ExoCTK's +Contamination Overlap tool, as it will visualize the Contaminaton Bokeh +plots on the website. Functions are: plotTemps - Plots the temperatures of stars according to color. traceLengths - Fine-tunes the trace lengths in the plot. -contamVerify - The main mini tool. Outputs a .pdf file with one or more figures - showing the FOV in the science frame according to the input - Aperture Position Angle(s) it is fed. +contamVerify - The main mini tool. Outputs a .pdf file with one or more + figures showing the FOV in the science frame according to + the input Aperture Position Angle(s) it is fed. Author(s) --------- @@ -41,7 +42,8 @@ def contamVerify(RA, DEC, INSTRUMENT, APAlist, binComp=[], PDF='', web=False): """ Generates a PDF file of figures displaying a simulation - of the science image for any given observation using the parameters provided. + of the science image for any given observation using the parameters + provided. Parameter(s) ------------ @@ -54,7 +56,8 @@ def contamVerify(RA, DEC, INSTRUMENT, APAlist, binComp=[], PDF='', web=False): The software currently supports: 'MIRI', 'NIRISS', 'NIRCam F322W2', 'NIRCam F444W' APAlist : list - A list of Aperture Position Angle(s). Element(s) must be in integers. + A list of Aperture Position Angle(s). Element(s) must be in + integers. Example 1: [1, 25, 181, 205] Example 2: @@ -65,23 +68,24 @@ def contamVerify(RA, DEC, INSTRUMENT, APAlist, binComp=[], PDF='', web=False): [RA (arcseconds), DEC (arcseconds), J mag, H mag, K mag] [string, string, integer, integer, integer] PDF : string - The path to where the PDF file will be saved. If left blank, the PDF - file will be saved in your current working directory. + The path to where the PDF file will be saved. If left blank, the + PDF file will be saved in your current working directory. Example: 'path/to/my/file.pdf' web : boolean - Makes it easier to integrate it onto the website. Leave this as false, - unless you're running this in app_exoctk.py + Makes it easier to integrate it onto the website. Leave this as + false, unless you're running this in app_exoctk.py Returns ------- A .PDF file containing a simulation of the FOV of your target in the - science coordinate system. Some things to consider when reading the figures: + science coordinate system. Some things to consider when reading the + figures: 1. The target is circled in red 2. Stellar temperatures of all sources are plotted by color - 3. The gray region oulined in blue represents the aperture for the given - instrument. + 3. The gray region oulined in blue represents the aperture for the + given instrument. 4. The blue square represents the readout region, or the "origin" """ @@ -284,8 +288,8 @@ def contamVerify(RA, DEC, INSTRUMENT, APAlist, binComp=[], PDF='', web=False): def plotTemps(TEMPS, allRA, allDEC): - """ The stars' colors in the plot will be a function of effective stellar - temperatures when plotting with this function. """ + """ The stars' colors in the plot will be a function of effective + stellar temperatures when plotting with this function.""" # Getting the color palette colors = cm.get_cmap('viridis', len(TEMPS)) @@ -321,7 +325,8 @@ def plotTemps(TEMPS, allRA, allDEC): def traceLength(inst): - """ For fine-tuning the trace lengths in the contamVerify output figures """ + """ For fine-tuning the trace lengths in the contamVerify output + figures""" # Getting example trace to calculate rough estimate of trace lengths if 'NIRCam' in inst: diff --git a/exoctk/contam_visibility/quaternionx.py b/exoctk/contam_visibility/quaternionx.py index b61a05ec..1cd6a9a2 100755 --- a/exoctk/contam_visibility/quaternionx.py +++ b/exoctk/contam_visibility/quaternionx.py @@ -2,9 +2,8 @@ # quaternion module """Version 4 September 9, 2010 WMK Flipped sign of the angle in the QX, QY, QZ, QJX, QJY, QJZ, set_values, -set_as_QX, ... functions to be consistent with the corrected multiplication. -Also updated -the doc strings. +set_as_QX, ... functions to be consistent with the corrected +multiplication. Also updated the doc strings. Version 3 September 8, 2010 RLH Backed out change to cnvrt in version 2. @@ -18,12 +17,14 @@ 4/9/2010 WMK Redefined the __init__ inputs Changed from a Vector internal representation to 3 scalers -Fixed an error in cvt_att_Q_to_angles, was assuming an att2inertial Quaternion! +Fixed an error in cvt_att_Q_to_angles, was assuming an att2inertial +Quaternion! Streamlined some of the functions Version 1.0 August 3, 2010 Got rid of degrees trig functions. -Combined this and rotationsx.py module to avoid circular imports and made it +Combined this and rotationsx.py module to avoid circular imports and +made it PEP Compliant Joe Filippazzo - 2018/06/26 """ @@ -115,8 +116,8 @@ def __imul__(self, rs): def __init__(self, x=0.0, y=0.0, z=0.0): """Constructor for a three-dimensional vector. - Note that two-dimensional vectors can be constructed by omitting one - of the coordinates, which will default to 0. + Note that two-dimensional vectors can be constructed by omitting + one of the coordinates, which will default to 0. Parameters ---------- @@ -150,8 +151,8 @@ def __isub__(self, rs): return (self) def __mul__(self, rs): - """Implements Vector * scalar. Can then use '*' syntax in multiplying - a vector by a scalar rs + """Implements Vector * scalar. Can then use '*' syntax in + multiplying a vector by a scalar rs Parameters ---------- @@ -324,8 +325,8 @@ def rz(self): def set_eq(self, x=None, y=None, z=None): """Assigns new value to vector. - Arguments are now optional to permit this to be used with 2D vectors - or to modify any subset of coordinates. + Arguments are now optional to permit this to be used with 2D + vectors or to modify any subset of coordinates. Parameters ---------- @@ -365,32 +366,33 @@ class CelestialVector (Vector): def __init__(self, ra=0.0, dec=0.0, frame='eq', degrees=True): """Constructor for a celestial vector. - There are two spherical coordinates, a longitudinal coordinate (called - right ascension), and a latitudinal coordinate (called declination). - The RA is defined as the counterclockwise angle from a reference - direction on the equatorial plane; it ranges from 0-360 degrees. - The DEC is the angle between the vector and the equatorial plane; - it ranges from -90 to 90 degrees. Angles are specified in degrees but - represented internally as radians. - - The frame attribute indicates the coordinate frame of the vector, - which may be 'eq' (equatorial, default), 'ec' (ecliptic), or 'gal' - (galactic). In equatorial coordinates, the equatorial plane is the - celestial equator (extension of the Earth's equator) and the reference - axis is the vernal equinox. In ecliptic coordiantes, the equatorial - plane is the ecliptic (the Earth's orbital plane) and the reference - axis is usually defined relative to the Sun. In galactic coordinates, - the equatorial plane is the plane of the Galaxy. - - The degrees attribute should be True if the RA, DEC inputs are in - degrees. Otherwise radians is assumed. + There are two spherical coordinates, a longitudinal coordinate + (called right ascension), and a latitudinal coordinate (called + declination). The RA is defined as the counterclockwise angle + from a reference direction on the equatorial plane; it ranges + from 0-360 degrees. The DEC is the angle between the vector and + the equatorial plane; it ranges from -90 to 90 degrees. Angles + are specified in degrees but represented internally as radians. + + The frame attribute indicates the coordinate frame of the + vector, which may be 'eq' (equatorial, default), 'ec' + (ecliptic), or 'gal' (galactic). In equatorial coordinates, the + equatorial plane is the celestial equator (extension of the + Earth's equator) and the reference axis is the vernal equinox. + In ecliptic coordiantes, the equatorial plane is the ecliptic + (the Earth's orbital plane) and the reference axis is usually + defined relative to the Sun. In galactic coordinates, the + equatorial plane is the plane of the Galaxy. + + The degrees attribute should be True if the RA, DEC inputs are + in degrees. Otherwise radians is assumed. The coordinates "ra" and "dec" may be used in all three systems. - Other names for coordinates in different frames may be defined for - clarity. + Other names for coordinates in different frames may be defined + for clarity. - A CelestialVector is also an ordinary unit vector, with Cartesian - coordinates defined relative to the equatorial plane. + A CelestialVector is also an ordinary unit vector, with + Cartesian coordinates defined relative to the equatorial plane. Parameters ---------- @@ -418,8 +420,8 @@ def __init__(self, ra=0.0, dec=0.0, frame='eq', degrees=True): Vector.__init__(self, x=x, y=y, z=z) def __str__(self, verbose=True): - """Returns a string representation of the vector. Displays angles - in degrees. + """Returns a string representation of the vector. Displays + angles in degrees. Parameters ---------- @@ -435,12 +437,13 @@ def __str__(self, verbose=True): return celest_info def position_angle(self, v): - """Returns the position angle of v at the self vector, in radians. + """Returns the position angle of v at the self vector, in + radians. - v is an arbitrary vector that should be a CelestialVector object. - The position angle is the angle between the North vector on the - plane orthogonal to the self vector and the projection of v onto - that plane, defined counterclockwise. + v is an arbitrary vector that should be a CelestialVector + object. The position angle is the angle between the North vector + on the plane orthogonal to the self vector and the projection of + v onto that plane, defined counterclockwise. See "V3-axis Position Angle", John Isaacs, May 2003 for further discussion. @@ -517,9 +520,10 @@ def rotate_about_eigenaxis(self, angle, eigenaxis): Rotation is counterclockwise looking outward from origin along eigenaxis. Function uses rotation matrix from Rodrigues formula. - Note: This function is more general than rotate_about_axis above and - could be used in its place. However, rotate_about_axis is faster and - clearer when the rotation axis is one of the Cartesian axes. + Note: This function is more general than rotate_about_axis above + and could be used in its place. However, rotate_about_axis is + faster and clearer when the rotation axis is one of the + Cartesian axes. Parameters ---------- @@ -560,8 +564,8 @@ def rotate_by_posang(self, pa): """Returns the vector that results from rotating the self vector counterclockwise from the North projection onto the plane orthogonal to that vector by the specified position angle - (in radians). See "V3-axis Position Angle", John Isaacs, May 2003 for - further discussion. + (in radians). See "V3-axis Position Angle", John Isaacs, May + 2003 for further discussion. Parameters ---------- @@ -637,8 +641,8 @@ def set_eq(self, ra, dec, degrees=False): def transform_frame(self, new_frame): """Transforms coordinates between celestial and ecliptic frames and returns result as a new CelestialVector. - If new coordinate frame is the same as the old, a copy of the vector - is returned. + If new coordinate frame is the same as the old, a copy of the + vector is returned. Parameters ---------- @@ -705,7 +709,8 @@ def transform_frame(self, new_frame): return (result) def update_cartesian(self, x=None, y=None, z=None): - """Modifies a celestial vector by specifying new Cartesian coordinates. + """Modifies a celestial vector by specifying new Cartesian + coordinates. Any subset of the Cartesian coordinates may be specifed. @@ -739,8 +744,9 @@ class Attitude(CelestialVector): def __init__(self, ra=0.0, dec=0.0, pa=0.0, frame='eq', degrees=True): """Constructor for an Attitude. - pa = position_angle in degrees(default) or radians if degrees=False - is specified. Other arguments are the same as with CelestialVector + pa = position_angle in degrees(default) or radians if + degrees=False is specified. Other arguments are the same as with + CelestialVector Parameters ---------- @@ -820,10 +826,10 @@ def __str__(self): class Matrix(list): """Class to encapsulate matrix data and methods. - A matrix is simply a list of lists that correspond to rows of the matrix. - This is just intended to handle simple multiplication and vector rotations. - For anything more advanced or computationally intensive, Python library - routines should be used.""" + A matrix is simply a list of lists that correspond to rows of the + matrix. This is just intended to handle simple multiplication and + vector rotations. For anything more advanced or computationally + intensive, Python library routines should be used.""" def __init__(self, rows): """Constructor for a matrix. @@ -840,7 +846,8 @@ def __init__(self, rows): self.append(NumericList(row)) # copy list def __mul__(m1, m2): - """Multiplies two Matrix objects and returns the resulting matrix. + """Multiplies two Matrix objects and returns the resulting + matrix. Number of rows in m1 must equal the number of columns in m2. @@ -954,12 +961,13 @@ def row(self, row_index): class NumericList(list): - """List class that supports multiplication. Only valid for numbers.""" + """List class that supports multiplication. Only valid for + numbers.""" def __mul__(L1, L2): """Take the dot product of two numeric lists. - Not using Vector for this because it is limited to three dimensions. - Lists must have the same number of elements + Not using Vector for this because it is limited to three + dimensions. Lists must have the same number of elements Parameters ---------- @@ -1173,8 +1181,8 @@ def set_equal(self, Q): self.q4 = Q.q4 def set_values(self, V, angle): - """Sets quaterion values using a direction vector and a rotation of - the coordinate frame about it. + """Sets quaterion values using a direction vector and a rotation + of the coordinate frame about it. Parameters ---------- @@ -1237,8 +1245,8 @@ def cross(v1, v2): def cvt_body2inertial_Q_to_c1c2pa_tuple(Q): - """Creates a angle tuple from Q, assuming body frame to inertial Q and - 321 rotation sequence. + """Creates a angle tuple from Q, assuming body frame to inertial Q + and 321 rotation sequence. Parameters ---------- @@ -1542,7 +1550,8 @@ def QZ(angle): def Qmake_aperture2inertial(coord1, coord2, APA, xoff, yoff, s, YapPA, V3ref, V2ref): - """Creates a rotation Q, going from the target in aperture frame to body. + """Creates a rotation Q, going from the target in aperture frame to + body. Parameters ---------- @@ -1640,7 +1649,8 @@ def Qmake_v2v3_2inertial(coord1, coord2, V3pa, v2, v3): def ra_delta(v1, v2): - """Returns difference in right ascension between two CelestialVectors. + """Returns difference in right ascension between two + CelestialVectors. Parameters ---------- @@ -1668,8 +1678,9 @@ def ra_delta(v1, v2): def ra_separation(v1, v2): - """Returns separation in right ascension between two CelestialVectors. - This is accurate only if the difference in declination is small. + """Returns separation in right ascension between two + CelestialVectors. This is accurate only if the difference in + declination is small. |sep| = DELTA-RA cos DEC @@ -1693,9 +1704,9 @@ def ra_separation(v1, v2): def separation(v1, v2, norm=False): """Returns angle between two unit vectors in radians. - The angle between two normalized vectors is the arc-cosine of the dot - product. Unless the norm attribute is set to True, it is assumed the - vectors are already normalized (for performance). + The angle between two normalized vectors is the arc-cosine of the + dot product. Unless the norm attribute is set to True, it is assumed + the vectors are already normalized (for performance). Parameters ---------- @@ -1728,8 +1739,8 @@ def separation(v1, v2, norm=False): def vel_ab(U, Vel): - """Takes a unit vector and a velocity vector(km/s) and returns a unit - vector modidifed by the velocity abberation. + """Takes a unit vector and a velocity vector(km/s) and returns a + unit vector modidifed by the velocity abberation. Parameters ---------- diff --git a/exoctk/contam_visibility/time_extensionsx.py b/exoctk/contam_visibility/time_extensionsx.py index aad41200..c488e3b8 100755 --- a/exoctk/contam_visibility/time_extensionsx.py +++ b/exoctk/contam_visibility/time_extensionsx.py @@ -417,8 +417,8 @@ def round_to_second(time): def seconds_into_day(time): - """Takes a time in fractional days and returns number of seconds since - the start of the current day. + """Takes a time in fractional days and returns number of seconds + since the start of the current day. Parameters ---------- @@ -451,9 +451,9 @@ def seconds_to_days(seconds): def time_from_string(time_string): - """Takes a string of the form ddd:hh:mm:ss and converts it to fractional - days. All subfields above seconds are optional and may be omitted if the - subfield and all higher-order ones are zero. + """Takes a string of the form ddd:hh:mm:ss and converts it to + fractional days. All subfields above seconds are optional and may be + omitted if the subfield and all higher-order ones are zero. Parameters ---------- diff --git a/exoctk/contam_visibility/visibilityPA.py b/exoctk/contam_visibility/visibilityPA.py index 3bbf00e7..7dbb48ff 100755 --- a/exoctk/contam_visibility/visibilityPA.py +++ b/exoctk/contam_visibility/visibilityPA.py @@ -34,9 +34,11 @@ def checkVisPA(ra, dec, targetName=None, ephFileName=None, fig=None): Parameters ---------- ra: str - The RA of the target in hh:mm:ss.s or dd:mm:ss.s or representing a float + The RA of the target in hh:mm:ss.s or dd:mm:ss.s or representing + a float dec: str - The Dec of the target in hh:mm:ss.s or dd:mm:ss.s or representing a float + The Dec of the target in hh:mm:ss.s or dd:mm:ss.s or + representing a float targetName: str The target name ephFileName: str @@ -230,16 +232,19 @@ def using_gtvt( Parameters ---------- ra : str - The RA of the target (in degrees) hh:mm:ss.s or dd:mm:ss.s or representing a float + The RA of the target (in degrees) hh:mm:ss.s or dd:mm:ss.s or + representing a float dec : str - The Dec of the target (in degrees) hh:mm:ss.s or dd:mm:ss.s or representing a float + The Dec of the target (in degrees) hh:mm:ss.s or dd:mm:ss.s or + representing a float instrument : str Name of the instrument. Can either be (case-sensitive): 'NIRISS', 'NIRCam', 'MIRI', 'FGS', or 'NIRSpec' ephFileName : str The filename of the ephemeris file. output : str - Switches on plotting with Bokeh. Parameter value must be 'bokeh'. + Switches on plotting with Bokeh. Parameter value must be + 'bokeh'. Returns ------- diff --git a/exoctk/exoctk_app/app_exoctk.py b/exoctk/exoctk_app/app_exoctk.py index 0d3ed283..1937c3a0 100644 --- a/exoctk/exoctk_app/app_exoctk.py +++ b/exoctk/exoctk_app/app_exoctk.py @@ -90,8 +90,8 @@ def authenticate(): def check_auth(username, password): - """This function is called to check if a username password combination is - valid + """This function is called to check if a username password + combination is valid Parameters ---------- @@ -348,7 +348,8 @@ def groups_integrations(): Returns ------- ``flask.render_template`` obj - The rendered template for the Groups & Integrations calculator page. + The rendered template for the Groups & Integrations calculator + page. """ # Print out pandeia sat values @@ -823,7 +824,8 @@ def save_contam_pdf(): Returns ------- ``flask.render_template`` obj - The rendered template (and attachment) for the Contamination FOV. + The rendered template (and attachment) for the Contamination + FOV. """ RA, DEC = '19:50:50.2400', '+48:04:51.00' @@ -863,7 +865,8 @@ def save_visib_result(): Returns ------- ``flask.Response`` obj - flask.Response object with the results of the visibility only calculation. + flask.Response object with the results of the visibility only + calculation. """ visib_table = flask.request.form['data_file'] From bad789eefc2e5c015b7e6abfb73feb79258d102d Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Thu, 3 Jun 2021 11:20:52 -0400 Subject: [PATCH 05/20] PEP8 fixes for forward models --- exoctk/forward_models/forward_models.py | 65 ++++++++++--------------- 1 file changed, 25 insertions(+), 40 deletions(-) diff --git a/exoctk/forward_models/forward_models.py b/exoctk/forward_models/forward_models.py index 0e865def..3cd55f8b 100644 --- a/exoctk/forward_models/forward_models.py +++ b/exoctk/forward_models/forward_models.py @@ -1,7 +1,8 @@ -"""Module to hold our forward models that until now have been floating in the -web application. +"""Module to hold our forward models that until now have been floating +in the web application. -Right now this includes database interaction and model rescaling software. +Right now this includes database interaction and model rescaling +software. Authors @@ -13,30 +14,24 @@ Use --- -This is meant to be run through the Flask application, or can be run manually -with a provided list or arguments. +This is meant to be run through the Flask application, or can be run +manually with a provided list or arguments. """ -## -- IMPORTS -import os import io +import os import astropy.constants as constants import astropy.table as at import astropy.units as u -from bokeh.resources import INLINE -from bokeh.embed import components -from bokeh.models import Range1d -from bokeh.models.widgets import Panel, Tabs from bokeh.plotting import figure, output_file, save import h5py import numpy as np import pandas as pd from sqlalchemy import create_engine -from exoctk.utils import get_env_variables +from exoctk import utils -## -- FUNCTIONS def fortney_grid(args, write_plot=False, write_table=False): """ @@ -72,10 +67,9 @@ def fortney_grid(args, write_plot=False, write_table=False): utils.check_for_data('fortney') # Check for Fortney Grid database - print(os.path.join(get_env_variables()['exoctk_data'], 'fortney/fortney_models.db')) + print(os.path.join(utils.get_env_variables()['exoctk_data'], 'fortney/fortney_models.db')) try: - db = create_engine('sqlite:///' + - os.path.join(get_env_variables()['exoctk_data'], 'fortney/fortney_models.db')) + db = create_engine('sqlite:///' + os.path.join(utils.get_env_variables()['exoctk_data'], 'fortney/fortney_models.db')) header = pd.read_sql_table('header', db) except: raise Exception('Fortney Grid File Path is incorrect, or not initialized') @@ -114,9 +108,7 @@ def fortney_grid(args, write_plot=False, write_table=False): fort_grav = 25.0 * u.m / u.s**2 - df = header.loc[(header.gravity == fort_grav) & (header.temp == temp) & - (header.noTiO == noTiO) & (header.ray == ray) & - (header.flat == flat)] + df = header.loc[(header.gravity == fort_grav) & (header.temp == temp) & (header.noTiO == noTiO) & (header.ray == ray) & (header.flat == flat)] wave_planet = np.array(pd.read_sql_table(df['name'].values[0], db)['wavelength'])[::-1] r_lambda = np.array(pd.read_sql_table(df['name'].values[0], db)['radius']) * u.km @@ -209,7 +201,7 @@ def generic_grid(input_args, write_plot=False, write_table=False): utils.check_for_data('generic') # Find path to the database. - database_path = os.path.join(get_env_variables()['exoctk_data'], 'generic/generic_grid_db.hdf5') + database_path = os.path.join(utils.get_env_variables()['exoctk_data'], 'generic/generic_grid_db.hdf5') # Build rescaled model solution, inputs, closest_match, error_message = rescale_generic_grid(input_args, database_path) @@ -277,7 +269,7 @@ def rescale_generic_grid(input_args, database_path): # Parameter validation # Set up some nasty tuples first scaling_space = [('r_star', [0.05, 10000]), - ('r_planet', [0.0, 10000]), + ('r_planet', [0.0, 10000]), ('gravity', [.5, 50]), ('temperature', [400, 2600])] @@ -310,7 +302,7 @@ def rescale_generic_grid(input_args, database_path): ('metallicity', ['+0.0', '+1.0', '+1.7', '+2.0', '+2.3']), ('c_o', ['0.35', '0.56', '0.70', '1.00']), ('haze', ['0001', '0010', '0150', '1100']), - ('cloud', ['0.00', '0.06', '0.20','1.00'])] + ('cloud', ['0.00', '0.06', '0.20', '1.00'])] model_key = '' for tup in model_space: @@ -323,13 +315,12 @@ def rescale_generic_grid(input_args, database_path): break model_key = model_key[:-1] - # Define constants - boltzmann = 1.380658E-16 # gm*cm^2/s^2 * Kelvin - permitivity = 1.6726E-24 * 2.3 #g cgs Hydrogen + Helium Atmosphere + boltzmann = 1.380658E-16 # gm*cm^2/s^2 * Kelvin + permitivity = 1.6726E-24 * 2.3 # g cgs Hydrogen + Helium Atmosphere optical_depth = 0.56 - r_sun = 69580000000 # cm - r_jupiter = 6991100000 # cm + r_sun = 69580000000 # cm + r_jupiter = 6991100000 # cm closest_match = {'model_key': model_key, 'model_gravity': model_grav, 'model_temperature': model_temp} @@ -339,7 +330,7 @@ def rescale_generic_grid(input_args, database_path): model_wv = f['/wavelength'][...][:-1] model_spectra = f['/spectra/{}'.format(model_key)][...][:-1] - radius_ratio = np.sqrt(model_spectra) * inputs['r_planet']/inputs['r_star'] + radius_ratio = np.sqrt(model_spectra) * inputs['r_planet'] / inputs['r_star'] r_star = inputs['r_star'] * r_sun r_planet = inputs['r_planet'] * r_jupiter model_grav = model_grav * 1e2 @@ -348,34 +339,28 @@ def rescale_generic_grid(input_args, database_path): # Start with baseline based on model parameters scale_height = (boltzmann * model_temp) / (permitivity * model_grav) r_planet_base = np.sqrt(radius_ratio) * r_sun - altitude = r_planet_base - (np.sqrt(radius_ratio[2000])*r_sun) - opacity = optical_depth * np.sqrt((boltzmann * model_temp * permitivity * model_grav) / \ - (2 * np.pi * r_planet_base)) * \ - np.exp(altitude / scale_height) + altitude = r_planet_base - (np.sqrt(radius_ratio[2000]) * r_sun) + opacity = optical_depth * np.sqrt((boltzmann * model_temp * permitivity * model_grav) / (2 * np.pi * r_planet_base)) * np.exp(altitude / scale_height) # Now rescale from baseline solution = {} solution['scale_height'] = (boltzmann * inputs['temperature']) / (permitivity * inputs['gravity']) - solution['altitude'] = solution['scale_height'] * \ - np.log10(opacity/optical_depth * \ - np.sqrt((2 * np.pi * r_planet) / \ - (boltzmann * inputs['temperature'] * inputs['gravity']))) + solution['altitude'] = solution['scale_height'] * np.log10(opacity / optical_depth * np.sqrt((2 * np.pi * r_planet) / (boltzmann * inputs['temperature'] * inputs['gravity']))) solution['radius'] = solution['altitude'] + r_planet # Sort data sort = np.argsort(model_wv) solution['wv'] = model_wv[sort] solution['radius'] = solution['radius'][sort] - solution['spectra'] = (solution['radius']/r_star)**2 + solution['spectra'] = (solution['radius'] / r_star)**2 - except (KeyError, ValueError) as e: + except (KeyError, ValueError): error_message = 'One of the parameters to make up the model was missing or out of range.' model_key = 'rainout_0400_50_+0.0_0.70_0010_1.00' solution = {} with h5py.File(database_path) as f: solution['wv'] = f['/wavelength'][...][:-1] solution['spectra'] = f['/spectra/{}'.format(model_key)][...][:-1] - closest_match = {'model_key': model_key, 'model_temperature': 400, - 'model_gravity': 50} + closest_match = {'model_key': model_key, 'model_temperature': 400, 'model_gravity': 50} inputs = input_args return solution, inputs, closest_match, error_message From b534ad9b392730e1244f4a6990ef1646ea997790 Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Thu, 3 Jun 2021 11:22:17 -0400 Subject: [PATCH 06/20] PEP8 fixes for groups and integrations --- exoctk/groups_integrations/groups_integrations.py | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/exoctk/groups_integrations/groups_integrations.py b/exoctk/groups_integrations/groups_integrations.py index ff8e6cf7..e200ffed 100644 --- a/exoctk/groups_integrations/groups_integrations.py +++ b/exoctk/groups_integrations/groups_integrations.py @@ -90,7 +90,8 @@ def calc_exposure_time(num_integrations, ramp_time): def calc_frame_time(num_columns, num_rows, num_amps, instrument): - """Calculates the frame time for a given instrument/readmode/subarray. + """Calculates the frame time for a given + instrument/readmode/subarray. Parameters ---------- @@ -218,7 +219,8 @@ def calc_observation_efficiency(exposure_time, duration_time): def calc_ramp_time(integration_time, num_reset_frames, frame_time): - """Calculates the ramp time -- or the integration time plus overhead for resets. + """Calculates the ramp time -- or the integration time plus overhead + for resets. Parameters ---------- From a67d89348a90356607c65865b01138cac065584a Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Thu, 3 Jun 2021 11:27:24 -0400 Subject: [PATCH 07/20] PEP8 fixes for helpers.py --- exoctk/helpers.py | 91 +++++++++++++++++++++++------------------------ 1 file changed, 45 insertions(+), 46 deletions(-) diff --git a/exoctk/helpers.py b/exoctk/helpers.py index 703b8408..28e6eaad 100755 --- a/exoctk/helpers.py +++ b/exoctk/helpers.py @@ -25,10 +25,7 @@ - ``numpy`` """ -from glob import glob -import os from pkg_resources import resource_filename -from shutil import copyfile import astropy.constants as ac from astropy.io import fits, ascii @@ -36,35 +33,14 @@ import numpy as np -def external_files(): - """A snippet to propagate the external files directory to the - to the submodules - - - Returns - ------- - ext_files : str - The external files directory +def convert_ATLAS9(filepath, destination='', template=resource_filename('exoctk', 'data/core/ModelGrid_tmp.fits')): """ - try: - from ConfigParser import ConfigParser - except ImportError: - from configparser import ConfigParser + Split ATLAS9 FITS files into separate files containing one Teff, + log(g), and Fe/H - conf = ConfigParser() - conf.read(['../setup.cfg']) - metadata = dict(conf.items('metadata')) - ext_files = metadata.get('external_files') + ACES models are in [erg/s/cm2/cm] whereas ATLAS9 models are in + [erg/cm2/s/hz/ster] - return ext_files - - -def convert_ATLAS9(filepath, destination='', template=resource_filename('exoctk', 'data/core/ModelGrid_tmp.fits')): - """ - Split ATLAS9 FITS files into separate files containing one Teff, log(g), and Fe/H - - ACES models are in [erg/s/cm2/cm] whereas ATLAS9 models are in [erg/cm2/s/hz/ster] - Parameters ---------- filepath : str @@ -80,12 +56,12 @@ def convert_ATLAS9(filepath, destination='', template=resource_filename('exoctk' # Get the indexes of each log(g) chunk start = [] - for idx,l in enumerate(L): + for idx, l in enumerate(L): if l.startswith('TEFF'): start.append(idx) # Break up into chunks - for n,idx in enumerate(start): + for n, idx in enumerate(start): try: @@ -95,21 +71,21 @@ def convert_ATLAS9(filepath, destination='', template=resource_filename('exoctk' logg = float(h[3][:3]) vturb = float(h[8]) xlen = float(h[11]) - feh = float(h[6].replace('[','').replace(']','')) + feh = float(h[6].replace('[', '').replace(']', '')) # Parse the data try: - end = start[n+1] + end = start[n + 1] except: end = -1 - data = L[idx+3:end-4] + data = L[idx + 3:end - 4] # Fix column spacing - for n,l in enumerate(data): - data[n] = l[:19]+' '+' '.join([l[idx:idx+6] for idx in np.arange(19,len(l),6)]) + for n, l in enumerate(data): + data[n] = l[:19] + ' ' + ' '.join([l[idx:idx + 6] for idx in np.arange(19, len(l), 6)]) # Get cols and data - cols = ['wl']+L[idx+2].strip().split() + cols = ['wl'] + L[idx + 2].strip().split() data = ascii.read(data, names=cols) # Put intensity array for increasing mu values in a cube @@ -120,28 +96,28 @@ def convert_ATLAS9(filepath, destination='', template=resource_filename('exoctk' data_cube[-1] *= 1E5 # Apply units - data_cube = data_cube*q.erg/q.cm**2/q.s/q.steradian/q.Hz + data_cube = data_cube * q.erg / q.cm ** 2 / q.s / q.steradian / q.Hz # mu values - mu = list(map(float,cols[1:]))[::-1] + mu = list(map(float, cols[1:]))[::-1] # Get the wavelength and convert from nm to A - wave = np.array(data['wl'])*q.nm.to(q.AA) + wave = np.array(data['wl']) * q.nm.to(q.AA) # Convert the flux from [erg/cm2/s/hz/ster] to [erg*m/cm**2/Hz/s**2/sr] # by multiplying by c/lambda**2 - data_cube = data_cube*ac.c/(wave**2) + data_cube = data_cube * ac.c / (wave ** 2) # Convert [m/sr] to [cm-1] - data_cube = data_cube*q.steradian/q.cm**2 + data_cube = data_cube * q.steradian / q.cm ** 2 # Convert to [erg/s/cm2/cm] - data_cube = data_cube.to(q.erg/q.s/q.cm**3)*1E16 + data_cube = data_cube.to(q.erg / q.s / q.cm ** 3) * 1E16 # Copy the old HDU list - logg_txt = str(abs(int(logg*10.))).zfill(2) - feh_txt = '{}{}'.format('m' if feh<0 else 'p', str(abs(int(feh*10.))).zfill(2)) - new_file = destination+'ATLAS9_{}_{}_{}.fits'.format(teff,logg_txt,feh_txt) + logg_txt = str(abs(int(logg * 10.))).zfill(2) + feh_txt = '{}{}'.format('m' if feh < 0 else 'p', str(abs(int(feh * 10.))).zfill(2)) + new_file = destination + 'ATLAS9_{}_{}_{}.fits'.format(teff, logg_txt, feh_txt) HDU = fits.open(template) # Write the new data @@ -178,3 +154,26 @@ def convert_ATLAS9(filepath, destination='', template=resource_filename('exoctk' except: pass + + +def external_files(): + """A snippet to propagate the external files directory to the + to the submodules + + + Returns + ------- + ext_files : str + The external files directory + """ + try: + from ConfigParser import ConfigParser + except ImportError: + from configparser import ConfigParser + + conf = ConfigParser() + conf.read(['../setup.cfg']) + metadata = dict(conf.items('metadata')) + ext_files = metadata.get('external_files') + + return ext_files From b7d2adcfb28bbec43c0cef94f3b817bbd87099ce Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Thu, 3 Jun 2021 12:47:03 -0400 Subject: [PATCH 08/20] PEP8 fixes for lightcurve fitting modules --- exoctk/lightcurve_fitting/fitters.py | 11 +++-- exoctk/lightcurve_fitting/lightcurve.py | 59 ++++++++++++------------ exoctk/lightcurve_fitting/models.py | 1 + exoctk/lightcurve_fitting/parameters.py | 4 +- exoctk/lightcurve_fitting/simulations.py | 7 +-- 5 files changed, 43 insertions(+), 39 deletions(-) diff --git a/exoctk/lightcurve_fitting/fitters.py b/exoctk/lightcurve_fitting/fitters.py index abd768be..7d809b01 100644 --- a/exoctk/lightcurve_fitting/fitters.py +++ b/exoctk/lightcurve_fitting/fitters.py @@ -3,10 +3,12 @@ Author: Joe Filippazzo Email: jfilippazzo@stsci.edu """ -import numpy as np -import lmfit + import copy +import lmfit +import numpy as np + from .parameters import Parameters @@ -36,7 +38,7 @@ def lmfitter(time, data, model, uncertainty=None, verbose=True, **kwargs): # Initialize lmfit Params object initialParams = lmfit.Parameters() - #TODO: Do something so that duplicate param names can all be handled (e.g. two Polynomail models with c0). Perhaps append something to the parameter name like c0_1 and c0_2?) + # TODO: Do something so that duplicate param names can all be handled (e.g. two Polynomail models with c0). Perhaps append something to the parameter name like c0_1 and c0_2?) # Concatenate the lists of parameters all_params = [i for j in [model.components[n].parameters.list @@ -71,8 +73,7 @@ def lmfitter(time, data, model, uncertainty=None, verbose=True, **kwargs): uncertainty = np.ones(len(data)) # Fit light curve model to the simulated data - result = lcmodel.fit(data, weights=1/uncertainty, params=initialParams, - **indep_vars, **kwargs) + result = lcmodel.fit(data, weights=1 / uncertainty, params=initialParams, **indep_vars, **kwargs) if verbose: print(result.fit_report()) diff --git a/exoctk/lightcurve_fitting/lightcurve.py b/exoctk/lightcurve_fitting/lightcurve.py index 246ef9c9..b6f5b75a 100644 --- a/exoctk/lightcurve_fitting/lightcurve.py +++ b/exoctk/lightcurve_fitting/lightcurve.py @@ -3,41 +3,16 @@ Author: Joe Filippazzo Email: jfilippazzo@stsci.edu """ + +from bokeh.plotting import figure, show import numpy as np import pandas as pd -from bokeh.plotting import figure, show -from .models import Model, CompositeModel from .fitters import lmfitter +from .models import Model, CompositeModel from ..utils import COLORS -class LightCurveFitter: - def __init__(self, time, flux, model): - """Fit the model to the flux cube - - Parameters - ---------- - time: - 1D or 2D time axes - flux: - 2D flux - """ - self.flux = np.ones(100) - self.time = np.arange(100) - self.results = pd.DataFrame(names=('fit_number', 'wavelength', 'P', - 'Tc', 'a/Rs', 'b', 'd', 'ldcs', - 'e', 'w', 'model_name', 'chi2')) - - def run(self): - """Run the model fits""" - pass - - # Method to return sliced results table - def master_slicer(self, value, param_name='wavelength'): - return self.results.iloc[self.results[param_name] == value] - - class LightCurve(Model): def __init__(self, time, flux, unc=None, parameters=None, units='MJD', name='My Light Curve'): """ @@ -74,7 +49,7 @@ def __init__(self, time, flux, unc=None, parameters=None, units='MJD', name='My self.unc = unc else: - self.unc = np.array([np.nan]*len(self.time)) + self.unc = np.array([np.nan] * len(self.time)) # Set the time and flux axes self.time = time @@ -155,3 +130,29 @@ def plot(self, fits=True, draw=True): def reset(self): """Reset the results""" self.results = [] + + +class LightCurveFitter: + def __init__(self, time, flux, model): + """Fit the model to the flux cube + + Parameters + ---------- + time: + 1D or 2D time axes + flux: + 2D flux + """ + self.flux = np.ones(100) + self.time = np.arange(100) + self.results = pd.DataFrame(names=('fit_number', 'wavelength', 'P', + 'Tc', 'a/Rs', 'b', 'd', 'ldcs', + 'e', 'w', 'model_name', 'chi2')) + + def run(self): + """Run the model fits""" + pass + + # Method to return sliced results table + def master_slicer(self, value, param_name='wavelength'): + return self.results.iloc[self.results[param_name] == value] diff --git a/exoctk/lightcurve_fitting/models.py b/exoctk/lightcurve_fitting/models.py index 2aff3efa..750c4cfa 100644 --- a/exoctk/lightcurve_fitting/models.py +++ b/exoctk/lightcurve_fitting/models.py @@ -4,6 +4,7 @@ Author: Joe Filippazzo Email: jfilippazzo@stsci.edu """ + import copy import inspect import os diff --git a/exoctk/lightcurve_fitting/parameters.py b/exoctk/lightcurve_fitting/parameters.py index 4654e510..919d7a4c 100644 --- a/exoctk/lightcurve_fitting/parameters.py +++ b/exoctk/lightcurve_fitting/parameters.py @@ -3,6 +3,7 @@ Author: Joe Filippazzo Email: jfilippazzo@stsci.edu """ + import os import json @@ -76,8 +77,7 @@ def values(self): class Parameters: - """A class to hold the Parameter instances - """ + """A class to hold the Parameter instances""" def __init__(self, param_file=None, **kwargs): """Initialize the parameter object diff --git a/exoctk/lightcurve_fitting/simulations.py b/exoctk/lightcurve_fitting/simulations.py index b81a3b39..ef05b15c 100644 --- a/exoctk/lightcurve_fitting/simulations.py +++ b/exoctk/lightcurve_fitting/simulations.py @@ -3,12 +3,13 @@ Author: Joe Filippazzo Email: jfilippazzo@stsci.edu """ -import numpy as np -from bokeh.plotting import figure, show + try: import batman except ImportError: print("Could not import batman. Functionality may be limited.") +from bokeh.plotting import figure, show +import numpy as np from .. import utils @@ -76,7 +77,7 @@ def simulate_lightcurve(target, snr=1000., npts=1000, nbins=10, radius=None, ldc # Add noise ideal_flux = np.asarray(flux) - flux = np.random.normal(loc=ideal_flux, scale=ideal_flux/snr) + flux = np.random.normal(loc=ideal_flux, scale=ideal_flux / snr) unc = flux - ideal_flux # Plot it From 9a8b113c81fbaaab190ae124de920af9abeceec2 Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Thu, 3 Jun 2021 12:57:27 -0400 Subject: [PATCH 09/20] PEP8 fixes for limb darkening module --- exoctk/limb_darkening/limb_darkening_fit.py | 106 ++++++++++---------- 1 file changed, 53 insertions(+), 53 deletions(-) diff --git a/exoctk/limb_darkening/limb_darkening_fit.py b/exoctk/limb_darkening/limb_darkening_fit.py index 18efeb59..2f6236e9 100755 --- a/exoctk/limb_darkening/limb_darkening_fit.py +++ b/exoctk/limb_darkening/limb_darkening_fit.py @@ -1,29 +1,29 @@ #!/usr/bin/python # -*- coding: latin-1 -*- """ -A module to calculate limb darkening coefficients from a grid of model spectra +A module to calculate limb darkening coefficients from a grid of model +spectra """ -import copy + import inspect import os import warnings from astropy.io import ascii as ii import astropy.table as at -import astropy.units as q from astropy.utils.exceptions import AstropyWarning +import bokeh.plotting as bkp +from bokeh.models import Range1d +from bokeh.models.widgets import Panel, Tabs import matplotlib import matplotlib.pyplot as plt from matplotlib import rc import numpy as np from scipy.optimize import curve_fit from svo_filters import svo -import bokeh.plotting as bkp -from bokeh.models import Range1d -from bokeh.models.widgets import Panel, Tabs -from .. import utils from .. import modelgrid +from .. import utils rc('font', **{'family': 'sans-serif', 'sans-serif': ['Helvetica'], 'size': 16}) rc('text', usetex=True) @@ -134,8 +134,8 @@ def __init__(self, model_grid='ACES'): Parameters ---------- model_grid: exoctk.modelgrid.ModelGrid - The grid of synthetic spectra from which the coefficients will - be calculated + The grid of synthetic spectra from which the coefficients + will be calculated """ # Try ACES or ATLAS if model_grid == 'ACES': @@ -198,8 +198,8 @@ def calculate(self, Teff, logg, FeH, profile, mu_min=0.05, ld_min=0.01, bandpass=None, name=None, color=None, **kwargs): """ Calculates the limb darkening coefficients for a given synthetic - spectrum. If the model grid does not contain a spectrum of the given - parameters, the grid is interpolated to those parameters. + spectrum. If the model grid does not contain a spectrum of the + given parameters, the grid is interpolated to those parameters. Reference for limb-darkening laws: http://www.astro.ex.ac.uk/people/sing/David_Sing/Limb_Darkening.html @@ -367,48 +367,6 @@ def calculate(self, Teff, logg, FeH, profile, mu_min=0.05, ld_min=0.01, except ValueError: print("Could not calculate coefficients at {}".format(wave_eff)) - def plot_tabs(self, show=False, **kwargs): - """Plot the LDCs in a tabbed figure - - Parameters - ---------- - fig: matplotlib.pyplot.figure, bokeh.plotting.figure (optional) - An existing figure to plot on - show: bool - Show the figure - """ - # Change names to reflect ld profile - old_names = self.results['name'] - for n, row in enumerate(self.results): - self.results[n]['name'] = row['profile'] - - # Draw a figure for each wavelength bin - tabs = [] - for wav in np.unique(self.results['wave_eff']): - - # Plot it - TOOLS = 'box_zoom, box_select, crosshair, reset, hover' - fig = bkp.figure(tools=TOOLS, x_range=Range1d(0, 1), y_range=Range1d(0, 1), plot_width=800, plot_height=400) - self.plot(wave_eff=wav, fig=fig) - - # Plot formatting - fig.legend.location = 'bottom_right' - fig.xaxis.axis_label = 'mu' - fig.yaxis.axis_label = 'Intensity' - - tabs.append(Panel(child=fig, title=str(wav))) - - # Make the final tabbed figure - final = Tabs(tabs=tabs) - - # Put the names back - self.results['name'] = old_names - - if show: - bkp.show(final) - else: - return final - def plot(self, fig=None, show=False, **kwargs): """Plot the LDCs @@ -496,6 +454,48 @@ def plot(self, fig=None, show=False, **kwargs): else: return fig + def plot_tabs(self, show=False, **kwargs): + """Plot the LDCs in a tabbed figure + + Parameters + ---------- + fig: matplotlib.pyplot.figure, bokeh.plotting.figure (optional) + An existing figure to plot on + show: bool + Show the figure + """ + # Change names to reflect ld profile + old_names = self.results['name'] + for n, row in enumerate(self.results): + self.results[n]['name'] = row['profile'] + + # Draw a figure for each wavelength bin + tabs = [] + for wav in np.unique(self.results['wave_eff']): + + # Plot it + TOOLS = 'box_zoom, box_select, crosshair, reset, hover' + fig = bkp.figure(tools=TOOLS, x_range=Range1d(0, 1), y_range=Range1d(0, 1), plot_width=800, plot_height=400) + self.plot(wave_eff=wav, fig=fig) + + # Plot formatting + fig.legend.location = 'bottom_right' + fig.xaxis.axis_label = 'mu' + fig.yaxis.axis_label = 'Intensity' + + tabs.append(Panel(child=fig, title=str(wav))) + + # Make the final tabbed figure + final = Tabs(tabs=tabs) + + # Put the names back + self.results['name'] = old_names + + if show: + bkp.show(final) + else: + return final + def save(self, filepath): """ Save the LDC results to file From cf8f1322b76cded9a7ce079db19ebaeb87339bed Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Mon, 7 Jun 2021 11:10:45 -0400 Subject: [PATCH 10/20] PEP8 fixes --- exoctk/log_exoctk.py | 12 +-- exoctk/modelgrid.py | 177 ++++++++++++++++++++++--------------------- 2 files changed, 97 insertions(+), 92 deletions(-) diff --git a/exoctk/log_exoctk.py b/exoctk/log_exoctk.py index 2b4bbbea..d93a67eb 100644 --- a/exoctk/log_exoctk.py +++ b/exoctk/log_exoctk.py @@ -26,8 +26,8 @@ - ``sqlite3`` """ -import os import datetime +import os import astropy.table as at import numpy as np @@ -162,6 +162,11 @@ def log_form_input(form_dict, table, database): print(e) +def scrub(table_name): + """Snippet to prevent SQL injection attcks! PEW PEW PEW!""" + return ''.join(chr for chr in table_name if chr.isalnum()) + + def view_log(database, table, limit=50): """Visually inspect the job log. @@ -201,8 +206,3 @@ def view_log(database, table, limit=50): table.add_row(row) return table - - -def scrub(table_name): - """Snippet to prevent SQL injection attcks! PEW PEW PEW!""" - return ''.join(chr for chr in table_name if chr.isalnum()) diff --git a/exoctk/modelgrid.py b/exoctk/modelgrid.py index 1af6f614..5ec5f2d2 100644 --- a/exoctk/modelgrid.py +++ b/exoctk/modelgrid.py @@ -3,6 +3,7 @@ """ A module for creating and managing grids of model spectra """ + from functools import partial from glob import glob import multiprocessing @@ -13,9 +14,9 @@ import warnings from astropy.io import fits -from astropy.utils.exceptions import AstropyWarning import astropy.table as at import astropy.units as q +from astropy.utils.exceptions import AstropyWarning import h5py import numpy as np from scipy.interpolate import RegularGridInterpolator @@ -37,7 +38,8 @@ class ModelGrid(object): Attributes ---------- path: str - The path to the directory of FITS files used to create the ModelGrid + The path to the directory of FITS files used to create the + ModelGrid refs: list, str The references for the data contained in the ModelGrid teff_rng: tuple @@ -231,6 +233,89 @@ def __init__(self, model_directory, bibcode='2013A & A...553A...6H', if kwargs: self.customize(**kwargs) + def customize(self, Teff_rng=(2300, 8000), logg_rng=(0, 6), + FeH_rng=(-2, 1), wave_rng=(0 * q.um, 40 * q.um), n_bins=''): + """ + Trims the model grid by the given ranges in effective + temperature, surface gravity, and metallicity. Also sets the + wavelength range and number of bins for retrieved model spectra. + + Parameters + ---------- + Teff_rng: array-like + The lower and upper inclusive bounds for the effective + temperature (K) + logg_rng: array-like + The lower and upper inclusive bounds for the logarithm of + the surface gravity (dex) + FeH_rng: array-like + The lower and upper inclusive bounds for the logarithm of + the ratio of the metallicity and solar metallicity (dex) + wave_rng: array-like + The lower and upper inclusive bounds for the wavelength + (microns) + n_bins: int + The number of bins for the wavelength axis + + """ + # Make a copy of the grid + grid = self.data.copy() + self.wave_rng = wave_rng + self.n_bins = n_bins or self.n_bins + + # Filter grid by given parameters + self.data = grid[[(grid['Teff'] >= Teff_rng[0]) & + (grid['Teff'] <= Teff_rng[1]) & + (grid['logg'] >= logg_rng[0]) & + (grid['logg'] <= logg_rng[1]) & + (grid['FeH'] >= FeH_rng[0]) & + (grid['FeH'] <= FeH_rng[1])]] + + # Print a summary of the returned grid + print('{}/{}'.format(len(self.data), len(grid)), + 'spectra in parameter range', + 'Teff: ', Teff_rng, ', logg: ', logg_rng, + ', FeH: ', FeH_rng, ', wavelength: ', wave_rng) + + # Do nothing if he cut leaves the grid empty + if len(self.data) == 0: + self.data = grid + print('The given param ranges would leave 0 models in the grid.') + print('The model grid has not been updated. Please try again.') + return + + # Update the wavelength and flux attributes + if isinstance(self.wavelength, np.ndarray): + w = self.wavelength + W_idx, = np.where((w >= wave_rng[0]) & (w <= wave_rng[1])) + T_idx, = np.where((self.Teff_vals >= Teff_rng[0]) & + (self.Teff_vals <= Teff_rng[1])) + G_idx, = np.where((self.logg_vals >= logg_rng[0]) & + (self.logg_vals <= logg_rng[1])) + M_idx, = np.where((self.FeH_vals >= FeH_rng[0]) & + (self.FeH_vals <= FeH_rng[1])) + + # Trim arrays + self.wavelength = w[W_idx] + self.flux = self.flux[T_idx[0]: T_idx[-1] + 1, + G_idx[0]: G_idx[-1] + 1, + M_idx[0]: M_idx[-1] + 1, + :, W_idx[0]: W_idx[-1] + 1] + self.mu = self.mu[T_idx[0]: T_idx[-1] + 1, + G_idx[0]: G_idx[-1] + 1, + M_idx[0]: M_idx[-1] + 1] + + # Update the parameter attributes + self.Teff_vals = np.unique(self.data['Teff']) + self.logg_vals = np.unique(self.data['logg']) + self.FeH_vals = np.unique(self.data['FeH']) + + # Reload the flux array with the new grid parameters + self.load_flux(reset=True) + + # Clear the grid copy from memory + del grid + def export(self, filepath, **kwargs): """Export the model with the given parameters to a FITS file at the given filepath @@ -535,88 +620,6 @@ def load_flux(self, reset=False): else: print('Data already loaded.') - def customize(self, Teff_rng=(2300, 8000), logg_rng=(0, 6), - FeH_rng=(-2, 1), wave_rng=(0 * q.um, 40 * q.um), n_bins=''): - """ - Trims the model grid by the given ranges in effective temperature, - surface gravity, and metallicity. Also sets the wavelength range - and number of bins for retrieved model spectra. - - Parameters - ---------- - Teff_rng: array-like - The lower and upper inclusive bounds for the effective - temperature (K) - logg_rng: array-like - The lower and upper inclusive bounds for the logarithm of the - surface gravity (dex) - FeH_rng: array-like - The lower and upper inclusive bounds for the logarithm of the - ratio of the metallicity and solar metallicity (dex) - wave_rng: array-like - The lower and upper inclusive bounds for the wavelength (microns) - n_bins: int - The number of bins for the wavelength axis - - """ - # Make a copy of the grid - grid = self.data.copy() - self.wave_rng = wave_rng - self.n_bins = n_bins or self.n_bins - - # Filter grid by given parameters - self.data = grid[[(grid['Teff'] >= Teff_rng[0]) & - (grid['Teff'] <= Teff_rng[1]) & - (grid['logg'] >= logg_rng[0]) & - (grid['logg'] <= logg_rng[1]) & - (grid['FeH'] >= FeH_rng[0]) & - (grid['FeH'] <= FeH_rng[1])]] - - # Print a summary of the returned grid - print('{}/{}'.format(len(self.data), len(grid)), - 'spectra in parameter range', - 'Teff: ', Teff_rng, ', logg: ', logg_rng, - ', FeH: ', FeH_rng, ', wavelength: ', wave_rng) - - # Do nothing if he cut leaves the grid empty - if len(self.data) == 0: - self.data = grid - print('The given param ranges would leave 0 models in the grid.') - print('The model grid has not been updated. Please try again.') - return - - # Update the wavelength and flux attributes - if isinstance(self.wavelength, np.ndarray): - w = self.wavelength - W_idx, = np.where((w >= wave_rng[0]) & (w <= wave_rng[1])) - T_idx, = np.where((self.Teff_vals >= Teff_rng[0]) & - (self.Teff_vals <= Teff_rng[1])) - G_idx, = np.where((self.logg_vals >= logg_rng[0]) & - (self.logg_vals <= logg_rng[1])) - M_idx, = np.where((self.FeH_vals >= FeH_rng[0]) & - (self.FeH_vals <= FeH_rng[1])) - - # Trim arrays - self.wavelength = w[W_idx] - self.flux = self.flux[T_idx[0]: T_idx[-1] + 1, - G_idx[0]: G_idx[-1] + 1, - M_idx[0]: M_idx[-1] + 1, - :, W_idx[0]: W_idx[-1] + 1] - self.mu = self.mu[T_idx[0]: T_idx[-1] + 1, - G_idx[0]: G_idx[-1] + 1, - M_idx[0]: M_idx[-1] + 1] - - # Update the parameter attributes - self.Teff_vals = np.unique(self.data['Teff']) - self.logg_vals = np.unique(self.data['logg']) - self.FeH_vals = np.unique(self.data['FeH']) - - # Reload the flux array with the new grid parameters - self.load_flux(reset=True) - - # Clear the grid copy from memory - del grid - def info(self): """ Print a table of info about the current ModelGrid @@ -663,7 +666,8 @@ def set_units(self, wave_units=q.um): class ACES(ModelGrid): - """A convenience function to load the ACES model grid from the EXOCTK_DATA directory""" + """A convenience function to load the ACES model grid from the + EXOCTK_DATA directory""" def __init__(self, **kwargs): """Initialize the ModelGrid object with the ACES models""" # Get the ACES model directory from the EXOCTK_DATA directory @@ -674,7 +678,8 @@ def __init__(self, **kwargs): class ATLAS9(ModelGrid): - """A convenience function to load the ATLAS9 model grid from the EXOCTK_DATA directory""" + """A convenience function to load the ATLAS9 model grid from the + EXOCTK_DATA directory""" def __init__(self, **kwargs): """Initialize the ModelGrid object with the ACES models""" # Get the ACES model directory from the EXOCTK_DATA directory From f4613ddffbcc02b63c0fade3120c408ec2d5ada3 Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Mon, 7 Jun 2021 11:33:34 -0400 Subject: [PATCH 11/20] PEP8 fixes for phase constraint --- .../phase_constraint_overlap.py | 671 ++++++++++-------- 1 file changed, 365 insertions(+), 306 deletions(-) diff --git a/exoctk/phase_constraint_overlap/phase_constraint_overlap.py b/exoctk/phase_constraint_overlap/phase_constraint_overlap.py index aef78ba9..7ba38d92 100644 --- a/exoctk/phase_constraint_overlap/phase_constraint_overlap.py +++ b/exoctk/phase_constraint_overlap/phase_constraint_overlap.py @@ -1,6 +1,7 @@ #! /usr/bin/env python -"""Phase contraint overlap tool. This tool calculates the minimum and maximum phase of -the primary or secondary transit (by default, primary) based on parameters provided by the user. +"""Phase contraint overlap tool. This tool calculates the minimum and +maximum phase of the primary or secondary transit (by default, primary) +based on parameters provided by the user. Authors: Catherine Martlin, 2018 @@ -9,7 +10,7 @@ Usage: calculate_constraint [--t0=] [--period=

] [--pre_duration=] [--transit_duration=] [--window_size=] [--secondary] [--eccentricity=] [--omega=] [--inclination=] [--winn_approx] [--get_secondary_time] - + Arguments: Name of target Options: @@ -28,53 +29,55 @@ --get_secondary_time If active, calculation also returns time-of-secondary eclipse. Needs t0 as input. """ -import math -import os - -import argparse +from astropy.time import Time from docopt import docopt import numpy as np -import requests -import urllib from scipy import optimize -from astropy.time import Time from exoctk.utils import get_target_data def calculate_phase(period, pre_duration, window_size, t0=None, ecc=None, omega=None, inc=None, secondary=False, winn_approx=False, get_secondary_time=False): - ''' Function to calculate the min and max phase. - - Parameters - ---------- - period : float - The period of the transit in days. - pre_duration : float - The duration of observations *before* transit/eclipse mid-time in hours. - window_size : float - The window size of transit in hours. Default is 1 hour. - t0 : float - The time of (primary) transit center (only needed if get_secondary_time is True). - ecc : float - The eccentricity of the orbit (only needed for secondary eclipses). - omega : float - The argument of periastron passage, in degrees (only needed for secondary eclipses). - inc : float - The inclination of the orbit, in degrees (only needed for secondary eclipses). - secondary : boolean - If True, calculation will be done for secondary eclipses. - winn_approx : boolean - If True, secondary eclipse calculation will use the Winn (2010) approximation to estimate time - of secondary eclipse --- (only valid for not very eccentric and inclined orbits). - get_secondary_time : boolean - If True, return time of secondary eclipse along with the phase constraints. - - Returns - ------- - minphase : float - The minimum phase constraint. - maxphase : float - The maximum phase constraint. ''' + """Function to calculate the min and max phase. + + Parameters + ---------- + period : float + The period of the transit in days. + pre_duration : float + The duration of observations *before* transit/eclipse mid-time + in hours. + window_size : float + The window size of transit in hours. Default is 1 hour. + t0 : float + The time of (primary) transit center (only needed if + get_secondary_time is True). + ecc : float + The eccentricity of the orbit (only needed for secondary + eclipses). + omega : float + The argument of periastron passage, in degrees (only needed for + secondary eclipses). + inc : float + The inclination of the orbit, in degrees (only needed for + secondary eclipses). + secondary : boolean + If True, calculation will be done for secondary eclipses. + winn_approx : boolean + If True, secondary eclipse calculation will use the Winn (2010) + approximation to estimate time of secondary eclipse --- (only + valid for not very eccentric and inclined orbits). + get_secondary_time : boolean + If True, return time of secondary eclipse along with the phase + constraints. + + Returns + ------- + minphase : float + The minimum phase constraint. + maxphase : float + The maximum phase constraint. + """ if t0 is None: if get_secondary_time: @@ -82,21 +85,21 @@ def calculate_phase(period, pre_duration, window_size, t0=None, ecc=None, omega= t0 = 1. if not secondary: - minphase = 1.0 - ((pre_duration + window_size)/24./period) - maxphase = 1.0 - ((pre_duration)/24./period) + minphase = 1.0 - ((pre_duration + window_size) / 24. / period) + maxphase = 1.0 - ((pre_duration) / 24. / period) else: - deg_to_rad = (np.pi/180.) + deg_to_rad = (np.pi / 180.) # Calculate time of secondary eclipse: - tsec = calculate_tsec(period, ecc, omega*deg_to_rad, inc*deg_to_rad, t0=t0, winn_approximation=winn_approx) + tsec = calculate_tsec(period, ecc, omega * deg_to_rad, inc * deg_to_rad, t0=t0, winn_approximation=winn_approx) # Calculate difference in phase-space between primary and secondary eclipse (note calculate_tsec ensures tsec is # *the next* secondary eclipse after t0): - phase_diff = (tsec - t0)/period - # Estimate minphase and maxphase centered around this phase (thinking here is that, e.g., if phase_diff is 0.3 + phase_diff = (tsec - t0) / period + # Estimate minphase and maxphase centered around this phase (thinking here is that, e.g., if phase_diff is 0.3 # then eclipse happens at 0.3 after 1 (being the latter by definition the time of primary eclipse --- i.e., transit). # Because phase runs from 0 to 1, this implies eclipse happens at phase 0.3): - minphase = phase_diff - ((pre_duration + window_size)/24./period) - maxphase = phase_diff - ((pre_duration)/24./period) - # Wrap the phases around 0 and 1 in case limits blow in the previous calculation (unlikely, but user might be doing + minphase = phase_diff - ((pre_duration + window_size) / 24. / period) + maxphase = phase_diff - ((pre_duration) / 24. / period) + # Wrap the phases around 0 and 1 in case limits blow in the previous calculation (unlikely, but user might be doing # something crazy or orbit could be extremely weird such that this can reasonably happen in the future). Note this # assumes -1 < minphase,maxphase < 2: if minphase < 0: @@ -107,30 +110,149 @@ def calculate_phase(period, pre_duration, window_size, t0=None, ecc=None, omega= return minphase, maxphase, tsec return minphase, maxphase + def calculate_pre_duration(transitDur): - ''' Function to calculate the pre-transit hours to be spent on target as recommended by the - Tdwell equation: + """Function to calculate the pre-transit hours to be spent on target + as recommended by the Tdwell equation: - 0.75 + Max(1hr,T14/2) (before transit) + T14 + Max(1hr, T14/2) (after transit) + 1hr (timing window) + 0.75 + Max(1hr,T14/2) (before transit) + T14 + Max(1hr, T14/2) + (after transit) + 1hr (timing window) - The output is, thus, 0.75 + Max(1hr,T14/2) (before transit) + T14/2. + The output is, thus, 0.75 + Max(1hr,T14/2) (before transit) + + T14/2. - Parameters - ---------- - transitDur : float - The duration of the transit/eclipse in hours. + Parameters + ---------- + transitDur : float + The duration of the transit/eclipse in hours. - Returns - ------- - pretransit_duration : float - The duration of the observation prior to transit/eclipse mid-time in hours. ''' + Returns + ------- + pretransit_duration : float + The duration of the observation prior to transit/eclipse + mid-time in hours. + """ - pretransit_duration = 0.75 + np.max([1., transitDur/2.]) + transitDur/2. + pretransit_duration = 0.75 + np.max([1., transitDur / 2.]) + transitDur / 2. return pretransit_duration -def drsky_2prime(x, ecc, omega, inc): - ''' Second derivative of function drsky. This is the second derivative with respect to f of the drsky function. + +def calculate_tsec(period, ecc, omega, inc, t0=None, tperi=None, winn_approximation=False): + """Function to calculate the time of secondary eclipse. + + This uses Halley's method (Newton-Raphson, but using second + derivatives) to first find the true anomaly (f) at which + secondary eclipse occurs, then uses this to get the eccentric + anomaly (E) at secondary eclipse, which gives the mean anomaly + (M) at secondary eclipse using Kepler's equation. This finally + leads to the time of secondary eclipse using the definition of + the mean anomaly (M = n*(t - tau) --- here tau is the time of + pericenter passage, n = 2*pi/period the mean motion). + + Time inputs can be either the time of periastron passage + directly or the time of transit center. If the latter, the true + anomaly for primary transit will be calculated using Halley's + method as well, and this will be used to get the time of + periastron passage. + + Parameters + ---------- + period : float + The period of the transit in days. + ecc : float + Eccentricity of the orbit + omega : float + Argument of periastron passage (in radians) + inc : string + Inclination of the orbit (in radians) + + t0 : float + The transit time in BJD or HJD (will be used to get time of + periastron passage). + + tperi : float + The time of periastron passage in BJD or HJD (needed if t0 is + not supplied). + + winn_approximation : boolean + If True, the approximation in Winn (2010) is used --- (only + valid for not very eccentric and inclined orbits). + + Returns + ------- + tsec : float + The time of secondary eclipse + """ + + # Check user is not playing trick on us: + if period < 0: + raise Exception('Period cannot be a negative number.') + if ecc < 0 or ecc > 1: + raise Exception('Eccentricity (e) is out of bounds (0 < e < 1).') + + # Use true anomaly approximation given in Winn (2010) as starting point: + f_occ_0 = (-0.5 * np.pi) - omega + if not winn_approximation: + f_occ = optimize.newton(drsky, f_occ_0, fprime=drsky_prime, fprime2=drsky_2prime, args=(ecc, omega, inc,)) + else: + f_occ = f_occ_0 + # Define the mean motion, n: + n = 2. * np.pi / period + + # If time of transit center is given, use it to calculate the time of periastron passage. If no time of periastron + # or time-of-transit center given, raise error: + if tperi is None: + # For this, find true anomaly during transit. Use Winn (2010) as starting point: + f_tra_0 = (np.pi / 2.) - omega + if not winn_approximation: + f_tra = optimize.newton(drsky, f_tra_0, fprime=drsky_prime, fprime2=drsky_2prime, args=(ecc, omega, inc,)) + else: + f_tra = f_tra_0 + # Get eccentric anomaly during transit: + E = getE(f_tra, ecc) + + # Get mean anomaly during transit: + M = getM(E, ecc) + + # Get time of periastron passage from mean anomaly definition: + tperi = t0 - (M / n) + + elif (tperi is None) and (t0 is None): + raise ValueError('The time of periastron passage or time-of-transit center has to be supplied for the calculation to work.') + + # Get eccentric anomaly: + E = getE(f_occ, ecc) + + # Get mean anomaly during secondary eclipse: + M = getM(E, ecc) + + # Get the time of secondary eclipse using the definition of the mean anomaly: + tsec = (M / n) + tperi + + # Note returned time-of-secondary eclipse is the closest to the time of periastron passage and/or time-of-transit center. Check that + # the returned tsec is the *next* tsec to the time of periastron or t0 (i.e., the closest *future* tsec): + if t0 is not None: + tref = t0 + else: + tref = tperi + if tref > tsec: + while True: + tsec += period + if tsec > tref: + break + return tsec + + +def drsky(x, ecc, omega, inc): + """Function whose roots we wish to find to obtain time of secondary + (and primary) eclipse(s) + + When one takes the derivative of equation (5) in Winn + (2010; https://arxiv.org/abs/1001.2010v5), and equates that to zero + (to find the minimum/maximum of said function), one gets to an + equation of the form g(x) = 0. This function (drsky) is g(x), where + x is the true anomaly. Parameters ---------- @@ -141,26 +263,26 @@ def drsky_2prime(x, ecc, omega, inc): omega : float Argument of periastron passage (in radians) inc : float - Inclination of the orbit (in radians) - + Inclination of the orbit (in radians) + Returns ------- - drsky_2prime : float - Function evaluated at x, ecc, omega, inc''' + drsky : float + Function evaluated at x, ecc, omega, inc + """ - sq_sini = np.sin(inc)**2 - sin_o_p_f = np.sin(x+omega) - cos_o_p_f = np.cos(x+omega) - ecosf = ecc*np.cos(x) - esinf = ecc*np.sin(x) + sq_sini = np.sin(inc) ** 2 + sin_o_p_f = np.sin(x + omega) + cos_o_p_f = np.cos(x + omega) - f1 = esinf - esinf*sq_sini*(sin_o_p_f**2) - f2 = -sq_sini*(ecosf + 4.)*(sin_o_p_f*cos_o_p_f) + f1 = sin_o_p_f * cos_o_p_f * sq_sini * (1. + ecc * np.cos(x)) + f2 = ecc * np.sin(x) * (1. - sin_o_p_f ** 2 * sq_sini) + return f1 - f2 - return f1+f2 -def drsky_prime(x, ecc, omega, inc): - ''' Derivative of function drsky. This is the first derivative with respect to f of the drsky function. +def drsky_2prime(x, ecc, omega, inc): + """Second derivative of function drsky. This is the second + derivative with respect to f of the drsky function. Parameters ---------- @@ -171,31 +293,29 @@ def drsky_prime(x, ecc, omega, inc): omega : float Argument of periastron passage (in radians) inc : float - Inclination of the orbit (in radians) - + Inclination of the orbit (in radians) + Returns ------- - drsky_prime : float - Function evaluated at x, ecc, omega, inc''' + drsky_2prime : float + Function evaluated at x, ecc, omega, inc + """ - sq_sini = np.sin(inc)**2 - sin_o_p_f = np.sin(x+omega) - cos_o_p_f = np.cos(x+omega) - ecosf = ecc*np.cos(x) - esinf = ecc*np.sin(x) + sq_sini = np.sin(inc) ** 2 + sin_o_p_f = np.sin(x + omega) + cos_o_p_f = np.cos(x + omega) + ecosf = ecc * np.cos(x) + esinf = ecc * np.sin(x) - f1 = (cos_o_p_f**2 - sin_o_p_f**2)*(sq_sini)*(1. + ecosf) - f2 = -ecosf*(1 - (sin_o_p_f**2)*(sq_sini)) - f3 = esinf*sin_o_p_f*cos_o_p_f*sq_sini - - return f1+f2+f3 + f1 = esinf - esinf * sq_sini * (sin_o_p_f ** 2) + f2 = -sq_sini * (ecosf + 4.) * (sin_o_p_f * cos_o_p_f) -def drsky(x, ecc, omega, inc): - ''' Function whose roots we wish to find to obtain time of secondary (and primary) eclipse(s) + return f1 + f2 - When one takes the derivative of equation (5) in Winn (2010; https://arxiv.org/abs/1001.2010v5), and equates that to zero (to find the - minimum/maximum of said function), one gets to an equation of the form g(x) = 0. This function (drsky) is g(x), where x is the true - anomaly. + +def drsky_prime(x, ecc, omega, inc): + """Derivative of function drsky. This is the first derivative with + respect to f of the drsky function. Parameters ---------- @@ -206,27 +326,36 @@ def drsky(x, ecc, omega, inc): omega : float Argument of periastron passage (in radians) inc : float - Inclination of the orbit (in radians) - + Inclination of the orbit (in radians) + Returns ------- - drsky : float - Function evaluated at x, ecc, omega, inc ''' + drsky_prime : float + Function evaluated at x, ecc, omega, inc + """ - sq_sini = np.sin(inc)**2 - sin_o_p_f = np.sin(x+omega) - cos_o_p_f = np.cos(x+omega) + sq_sini = np.sin(inc) ** 2 + sin_o_p_f = np.sin(x + omega) + cos_o_p_f = np.cos(x + omega) + ecosf = ecc * np.cos(x) + esinf = ecc * np.sin(x) + + f1 = (cos_o_p_f ** 2 - sin_o_p_f ** 2) * (sq_sini) * (1. + ecosf) + f2 = -ecosf * (1 - (sin_o_p_f ** 2) * (sq_sini)) + f3 = esinf * sin_o_p_f * cos_o_p_f * sq_sini + + return f1 + f2 + f3 - f1 = sin_o_p_f*cos_o_p_f*sq_sini*(1. + ecc*np.cos(x)) - f2 = ecc*np.sin(x)*(1. - sin_o_p_f**2 * sq_sini) - return f1 - f2 -def getE(f,ecc): - """ Function that returns the eccentric anomaly +def getE(f, ecc): + """Function that returns the eccentric anomaly - Note normally this is defined in terms of cosines (see, e.g., Section 2.4 in Murray and Dermott), but numerically - this is troublesome because the arccosine doesn't handle negative numbers by definition (equation 2.43). That's why - the arctan version is better as signs are preserved (derivation is also in the same section, equation 2.46). + Note normally this is defined in terms of cosines (see, e.g., + Section 2.4 in Murray and Dermott), but numerically + this is troublesome because the arccosine doesn't handle negative + numbers by definition (equation 2.43). That's why the arctan version + is better as signs are preserved (derivation is also in the same + section, equation 2.46). Parameters ---------- @@ -239,199 +368,127 @@ def getE(f,ecc): Returns ------- E : float - Eccentric anomaly """ + Eccentric anomaly + """ - return 2. * np.arctan(np.sqrt((1.-ecc)/(1.+ecc))*np.tan(f/2.)) + return 2. * np.arctan(np.sqrt((1. - ecc) / (1. + ecc)) * np.tan(f / 2.)) -def getM(E, ecc): - """ Function that returns the mean anomaly using Kepler's equation + +def getLTT(a, c, ecc, omega, inc, f): + """Function that calculates the Light Travel Time (LTT) for eclipses + and transit + + This function returns the light travel time of an eclipse or transit + given the orbital parameters, the semi-major axis of the orbit and + the speed of light. Consistent units must be used for the latter two + parameters. The returned time is in the units of time given by the + speed of light input parameter. Parameters ---------- - E : float - Eccentric anomaly - - ecc: float - Eccentricity + a : float + Semi-major axis of the orbit. Can be in any unit, as long as it + is consistent with c. + c : float + Speed of light. Can be in any unit, as long as it is consistent + with a. The time-unit for the speed of light. will define the + returned time-unit for the light-travel time. + ecc : float + Eccentricity of the orbit. + omega : float + Argument of periastron passage (in radians). + inc : string + Inclination of the orbit (in radians) + f : float + True anomaly at the time of transit and/or eclipse (in radians). Returns ------- - M : float - Mean anomaly """ + ltt : float + The light travel time, defined here as the time it takes for a + photon to go from the planet at transit/eclipse to the plane in + the sky where the star is located. + """ - return E - ecc*np.sin(E) - -def getLTT(a, c, ecc, omega, inc, f): - ''' Function that calculates the Light Travel Time (LTT) for eclipses and transit - - This function returns the light travel time of an eclipse or transit given the orbital parameters, - the semi-major axis of the orbit and the speed of light. Consistent units must be used for the latter - two parameters. The returned time is in the units of time given by the speed of light input parameter. - - Parameters - ---------- - a : float - Semi-major axis of the orbit. Can be in any unit, as long as it is consistent with c. - c : float - Speed of light. Can be in any unit, as long as it is consistent with a. The time-unit for the speed of light - will define the returned time-unit for the light-travel time. - ecc : float - Eccentricity of the orbit. - omega : float - Argument of periastron passage (in radians). - inc : string - Inclination of the orbit (in radians) - f : float - True anomaly at the time of transit and/or eclipse (in radians). - - Returns - ------- - ltt : float - The light travel time, defined here as the time it takes for a photon to go from the planet at transit/eclipse to the plane in the sky where the star is located. - - ''' # Distance from star to the planet in stellar reference system: - r = a*( 1. - ecc**2)/(1. + ecc*np.cos(f)) + r = a * (1. - ecc ** 2) / (1. + ecc * np.cos(f)) # Projected distance from planet to the plane the star is on the sky ('light travel distance'): - ltd = r * np.sin(omega + f)*np.sin(inc) + ltd = r * np.sin(omega + f) * np.sin(inc) # Return distance divided by c to get LTT: - return ltd/c - -def calculate_tsec(period, ecc, omega, inc, t0 = None, tperi = None, winn_approximation = False): - ''' Function to calculate the time of secondary eclipse. - - This uses Halley's method (Newton-Raphson, but using second derivatives) to first find the true anomaly (f) at which secondary eclipse occurs, - then uses this to get the eccentric anomaly (E) at secondary eclipse, which gives the mean anomaly (M) at secondary - eclipse using Kepler's equation. This finally leads to the time of secondary eclipse using the definition of the mean - anomaly (M = n*(t - tau) --- here tau is the time of pericenter passage, n = 2*pi/period the mean motion). - - Time inputs can be either the time of periastron passage directly or the time of transit center. If the latter, the - true anomaly for primary transit will be calculated using Halley's method as well, and this will be used to get the - time of periastron passage. - - Parameters - ---------- - period : float - The period of the transit in days. - ecc : float - Eccentricity of the orbit - omega : float - Argument of periastron passage (in radians) - inc : string - Inclination of the orbit (in radians) - - t0 : float - The transit time in BJD or HJD (will be used to get time of periastron passage). - - tperi : float - The time of periastron passage in BJD or HJD (needed if t0 is not supplied). - - winn_approximation : boolean - If True, the approximation in Winn (2010) is used --- (only valid for not very eccentric and inclined orbits). - - Returns - ------- - tsec : float - The time of secondary eclipse ''' - # Check user is not playing trick on us: - if period<0: - raise Exception('Period cannot be a negative number.') - if ecc<0 or ecc>1: - raise Exception('Eccentricity (e) is out of bounds (0 < e < 1).') + return ltd / c - # Use true anomaly approximation given in Winn (2010) as starting point: - f_occ_0 = (-0.5*np.pi) - omega - if not winn_approximation: - f_occ = optimize.newton(drsky, f_occ_0, fprime = drsky_prime, fprime2 = drsky_2prime, args = (ecc, omega, inc,)) - else: - f_occ = f_occ_0 - # Define the mean motion, n: - n = 2.*np.pi/period - # If time of transit center is given, use it to calculate the time of periastron passage. If no time of periastron - # or time-of-transit center given, raise error: - if tperi is None: - # For this, find true anomaly during transit. Use Winn (2010) as starting point: - f_tra_0 = (np.pi/2.) - omega - if not winn_approximation: - f_tra = optimize.newton(drsky, f_tra_0, fprime = drsky_prime, fprime2 = drsky_2prime, args = (ecc, omega, inc,)) - else: - f_tra = f_tra_0 - # Get eccentric anomaly during transit: - E = getE(f_tra, ecc) +def getM(E, ecc): + """Function that returns the mean anomaly using Kepler's equation - # Get mean anomaly during transit: - M = getM(E, ecc) + Parameters + ---------- + E : float + Eccentric anomaly - # Get time of periastron passage from mean anomaly definition: - tperi = t0 - (M/n) + ecc: float + Eccentricity - elif (tperi is None) and (t0 is None): - raise ValueError('The time of periastron passage or time-of-transit center has to be supplied for the calculation to work.') + Returns + ------- + M : float + Mean anomaly + """ - # Get eccentric anomaly: - E = getE(f_occ, ecc) + return E - ecc * np.sin(E) - # Get mean anomaly during secondary eclipse: - M = getM(E, ecc) - # Get the time of secondary eclipse using the definition of the mean anomaly: - tsec = (M/n) + tperi +def phase_overlap_constraint(target_name, period=None, t0=None, pretransit_duration=None, transit_dur=None, window_size=None, secondary=False, ecc=None, omega=None, inc=None, winn_approx=False, get_secondary_time=False): + """The main function to calculate the phase overlap constraints. - # Note returned time-of-secondary eclipse is the closest to the time of periastron passage and/or time-of-transit center. Check that - # the returned tsec is the *next* tsec to the time of periastron or t0 (i.e., the closest *future* tsec): - if t0 is not None: - tref = t0 - else: - tref = tperi - if tref > tsec: - while True: - tsec += period - if tsec > tref: - break - return tsec + We will update to allow a user to just plug in the target_name and + get the other variables. -def phase_overlap_constraint(target_name, period=None, t0=None, pretransit_duration=None, transit_dur=None, window_size=None, secondary = False, ecc = None, omega = None, inc = None, winn_approx = False, get_secondary_time = False): - ''' The main function to calculate the phase overlap constraints. - We will update to allow a user to just plug in the target_name - and get the other variables. - - Parameters - ---------- - target_name : string - The name of the target transiting planet. - period : float - The period of the transit in days. - t0 : float - The transit mid-time in BJD or HJD (only useful if time-of-secondary eclipse wants to be returned). - pretransit_duration : float - The duration of the observations *before* transit/eclipse in hours. - transit_dur : float - The duration of the transit/eclipse in hours. - window_size : float - The window size of transit in hours. Default is 1 hour. - secondary : boolean - If True, phase constraint will be the one for the secondary eclipse. Default is primary (i.e., transits). - ecc : float - Eccentricity of the orbit. Needed only if secondary is True. - omega : float - Argument of periastron of the orbit in degrees. Needed only if secondary is True. - inc : float - Inclination of the orbit in degrees. Needed only if secondary is true. - winn_approx : boolean - If True, instead of running the whole Kepler equation calculation, time of secondary eclipse is calculated using eq. (6) in Winn (2010; https://arxiv.org/abs/1001.2010v5) - get_secondary_time : boolean - If True, this function also returns the time-of-mid secondary eclipse. - - Returns - ------- - minphase : float - The minimum phase constraint. - maxphase : float - The maximum phase constraint. - tsec : float - (optional) If get_secondary_time is True, the time of secondary eclipse in the same units as input t0. - ''' + Parameters + ---------- + target_name : string + The name of the target transiting planet. + period : float + The period of the transit in days. + t0 : float + The transit mid-time in BJD or HJD (only useful if + time-of-secondary eclipse wants to be returned). + pretransit_duration : float + The duration of the observations *before* transit/eclipse in + hours. + transit_dur : float + The duration of the transit/eclipse in hours. + window_size : float + The window size of transit in hours. Default is 1 hour. + secondary : boolean + If True, phase constraint will be the one for the secondary + eclipse. Default is primary (i.e., transits). + ecc : float + Eccentricity of the orbit. Needed only if secondary is True. + omega : float + Argument of periastron of the orbit in degrees. Needed only if + secondary is True. + inc : float + Inclination of the orbit in degrees. Needed only if secondary is + true. + winn_approx : boolean + If True, instead of running the whole Kepler equation + calculation, time of secondary eclipse is calculated using eq. + (6) in Winn (2010; https://arxiv.org/abs/1001.2010v5) + get_secondary_time : boolean + If True, this function also returns the time-of-mid secondary + eclipse. + + Returns + ------- + minphase : float + The minimum phase constraint. + maxphase : float + The maximum phase constraint. + tsec : float + (optional) If get_secondary_time is True, the time of secondary + eclipse in the same units as input t0. + """ gotdata = False # If secondary eclipse, check eccentricity, omega and inclination are given. If not, get data from exoMAST: @@ -466,33 +523,35 @@ def phase_overlap_constraint(target_name, period=None, t0=None, pretransit_durat if period is None: period = data[0]['orbital_period'] t0 = Time(data[0]['transit_time'], format='mjd') - print('Retrieved period is {}. Retrieved t0 is {}.'.format(period,t0)) + print('Retrieved period is {}. Retrieved t0 is {}.'.format(period, t0)) # If transit/eclipse duration not supplied by the user, extract it from the get_target_data function: if transit_dur is None and pretransit_duration is None: # Transit duration from get_target_data comes in days: - transit_dur = data[0]['transit_duration']*24. + transit_dur = data[0]['transit_duration'] * 24. if secondary: - # Factor from equation (16) in Winn (2010). This is, of course, an approximation. - # TODO: Implement equation (13) in the same paper. Needs optimization plus numerical integration. - factor = np.sqrt(1. - ecc**2)/(1. - ecc*np.sin(omega*(np.pi/180.))) - transit_dur = transit_dur*factor + # Factor from equation (16) in Winn (2010). This is, of course, an approximation. + # TODO: Implement equation (13) in the same paper. Needs optimization plus numerical integration. + factor = np.sqrt(1. - ecc ** 2) / (1. - ecc * np.sin(omega * (np.pi / 180.))) + transit_dur = transit_dur * factor if pretransit_duration is None: pretransit_duration = calculate_pre_duration(transit_dur) - print('Retrieved transit/eclipse duration is: {} hrs; implied pre mid-transit/eclipse on-target time: {} hrs.'.format(transit_dur,pretransit_duration)) - print('Performing calculations with Period: {}, t0: {}, ecc: {}, omega: {} degs, inc: {} degs.'.format(period,t0,ecc,omega,inc)) + print('Retrieved transit/eclipse duration is: {} hrs; implied pre mid-transit/eclipse on-target time: {} hrs.'.format(transit_dur, pretransit_duration)) + print('Performing calculations with Period: {}, t0: {}, ecc: {}, omega: {} degs, inc: {} degs.'.format(period, t0, ecc, omega, inc)) if get_secondary_time: - minphase, maxphase, tsec = calculate_phase(period, pretransit_duration, window_size, t0 = t0, ecc = ecc, omega = omega, inc = inc, secondary = secondary, - winn_approx = winn_approx, get_secondary_time = get_secondary_time) - print('MINIMUM PHASE: {}, MAXIMUM PHASE: {}, TSEC: {}'.format(minphase, maxphase,tsec)) + minphase, maxphase, tsec = calculate_phase(period, pretransit_duration, window_size, t0=t0, ecc=ecc, omega=omega, inc=inc, secondary=secondary, + winn_approx=winn_approx, get_secondary_time=get_secondary_time) + print('MINIMUM PHASE: {}, MAXIMUM PHASE: {}, TSEC: {}'.format(minphase, maxphase, tsec)) return minphase, maxphase, tsec else: - minphase, maxphase = calculate_phase(period, pretransit_duration, window_size, t0 = t0, ecc = ecc, omega = omega, inc = inc, secondary = secondary, - winn_approx = winn_approx, get_secondary_time = get_secondary_time) + minphase, maxphase = calculate_phase(period, pretransit_duration, window_size, t0=t0, ecc=ecc, omega=omega, inc=inc, secondary=secondary, + winn_approx=winn_approx, get_secondary_time=get_secondary_time) print('MINIMUM PHASE: {}, MAXIMUM PHASE: {}'.format(minphase, maxphase)) return minphase, maxphase + # Need to make entry point for this! if __name__ == '__main__': + args = docopt(__doc__, version='0.1') # First, save the secondary flag (which is a boolean): @@ -502,16 +561,16 @@ def phase_overlap_constraint(target_name, period=None, t0=None, pretransit_durat # And the get_secondary_time flag: get_secondary_time = args['--get_secondary_time'] # Convert all entries from strs to floats (this will convert the --secondary arg above, but that's OK): - for k,v in args.items(): + for k, v in args.items(): try: args[k] = float(v) except (ValueError, TypeError): # Handles None and char strings. continue - - phase_overlap_constraint(args[''], period = args['--period'], - t0 = args['--t0'], pretransit_duration = args['--pre_duration'], - transit_dur = args['--transit_duration'], - window_size = args['--window_size'], secondary = secondary, - ecc = args['--eccentricity'], omega = args['--omega'], - inc = args['--inclination'], winn_approx = winn_approx, get_secondary_time = get_secondary_time) + + phase_overlap_constraint(args[''], period=args['--period'], + t0=args['--t0'], pretransit_duration=args['--pre_duration'], + transit_dur=args['--transit_duration'], + window_size=args['--window_size'], secondary=secondary, + ecc=args['--eccentricity'], omega=args['--omega'], + inc=args['--inclination'], winn_approx=winn_approx, get_secondary_time=get_secondary_time) From 6abafeded1d298ebaf00f4b401ed6806f94177ec Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Mon, 7 Jun 2021 11:40:27 -0400 Subject: [PATCH 12/20] PEP8 fixes for tests --- exoctk/tests/test_lightcurve_fitting.py | 34 +++------- exoctk/tests/test_limb_darkening.py | 85 ++++++++++++------------- exoctk/tests/test_modelgrid.py | 2 - exoctk/tests/test_visibilityPA.py | 2 + 4 files changed, 53 insertions(+), 70 deletions(-) diff --git a/exoctk/tests/test_lightcurve_fitting.py b/exoctk/tests/test_lightcurve_fitting.py index 57421b6f..ed9fbc48 100755 --- a/exoctk/tests/test_lightcurve_fitting.py +++ b/exoctk/tests/test_lightcurve_fitting.py @@ -21,7 +21,7 @@ import numpy as np -from ..lightcurve_fitting import lightcurve, models, parameters, simulations +from ..lightcurve_fitting import lightcurve, models, parameters class TestLightcurve(unittest.TestCase): @@ -47,6 +47,14 @@ def test_lightcurve(self): class TestModels(unittest.TestCase): """Tests for the models.py module""" + + def test_compositemodel(self): + """Tests for the CompositeModel class""" + model1 = models.Model() + model2 = models.Model() + self.comp_model = model1*model2 + self.comp_model.name = 'composite' + def setUp(self): """Setup for the tests""" # Set time to use for evaluations @@ -65,13 +73,6 @@ def test_model(self): self.assertEqual(self.model.units, 'MJD') self.assertRaises(TypeError, setattr, self.model.units, 'foobar') - def test_compositemodel(self): - """Tests for the CompositeModel class""" - model1 = models.Model() - model2 = models.Model() - self.comp_model = model1*model2 - self.comp_model.name = 'composite' - def test_polynomialmodel(self): """Tests for the PolynomialModel class""" # Create the model @@ -141,20 +142,3 @@ def test_parameters(self): # Test the auto attribute assignment self.assertEqual(self.params.param1.values, ('param1', 123.456, 'free')) self.assertEqual(self.params.param2.values, ('param2', 234.567, 'free', 200, 300)) - - -# class TestSimulations(unittest.TestCase): -# """Test the simulations.py module""" -# def setUp(self): -# """Setup for the tests""" -# pass -# -# def test_simulation(self): -# """Test the simulations can be made properly""" -# # Test to pass -# npts = 1234 -# time, flux, unc, params = simulations.simulate_lightcurve('WASP-19b', 0.1, npts=npts, plot=True) -# self.assertEqual(len(time), npts) -# -# # Test to fail -# self.assertRaises(ValueError, simulations.simulate_lightcurve, 'foobar', 0.1) diff --git a/exoctk/tests/test_limb_darkening.py b/exoctk/tests/test_limb_darkening.py index 52baae59..27b270df 100755 --- a/exoctk/tests/test_limb_darkening.py +++ b/exoctk/tests/test_limb_darkening.py @@ -17,7 +17,6 @@ pytest -s test_limb_darkening.py """ -import os from pkg_resources import resource_filename from svo_filters import Filter @@ -30,48 +29,6 @@ MODELGRID = mg.ModelGrid(resource_filename('exoctk', 'data/core/modelgrid/')) -def test_ldc_object(): - """Test to see that an LDC object can be loaded""" - print('Testing LDC object creation...') - - ld_session = ldf.LDC(MODELGRID) - - assert isinstance(ld_session, ldf.LDC) - - -def test_ldc_plot(): - """Test that the LDC plots work""" - print('Testing LDC object plotting...') - - ld_session = ldf.LDC(MODELGRID) - - # Run the calculations - ld_session.calculate(Teff=4000, logg=4.5, FeH=0, profile='quadratic') - - # Regular plot - fig = ld_session.plot() - assert str(type(fig)) == "" - - # Tabbed plot - fig = ld_session.plot_tabs() - assert str(type(fig)) in ["", ""] - - -def test_ldc_calculation_no_filter(): - """Test to see if a calculation can be performed with no filter and - that they are appended to the results table""" - print('Testing LDC calculation with no filter bandpass...') - - # Make the session - ld_session = ldf.LDC(MODELGRID) - - # Run the calculations - ld_session.calculate(Teff=4000, logg=4.5, FeH=0, profile='quadratic') - ld_session.calculate(Teff=4000, logg=4.5, FeH=0, profile='4-parameter') - - assert len(ld_session.results) == 2 - - def test_ldc_calculation_filter(): """Test to see if a calculation can be performed with a filter and that they are appended to the results table""" @@ -134,3 +91,45 @@ def test_ldc_calculation_interpolation(): ld_session.calculate(Teff=4023, logg=4.1, FeH=-0.1, profile='quadratic') assert len(ld_session.results) == 7 + + +def test_ldc_calculation_no_filter(): + """Test to see if a calculation can be performed with no filter and + that they are appended to the results table""" + print('Testing LDC calculation with no filter bandpass...') + + # Make the session + ld_session = ldf.LDC(MODELGRID) + + # Run the calculations + ld_session.calculate(Teff=4000, logg=4.5, FeH=0, profile='quadratic') + ld_session.calculate(Teff=4000, logg=4.5, FeH=0, profile='4-parameter') + + assert len(ld_session.results) == 2 + + +def test_ldc_object(): + """Test to see that an LDC object can be loaded""" + print('Testing LDC object creation...') + + ld_session = ldf.LDC(MODELGRID) + + assert isinstance(ld_session, ldf.LDC) + + +def test_ldc_plot(): + """Test that the LDC plots work""" + print('Testing LDC object plotting...') + + ld_session = ldf.LDC(MODELGRID) + + # Run the calculations + ld_session.calculate(Teff=4000, logg=4.5, FeH=0, profile='quadratic') + + # Regular plot + fig = ld_session.plot() + assert str(type(fig)) == "" + + # Tabbed plot + fig = ld_session.plot_tabs() + assert str(type(fig)) in ["", ""] diff --git a/exoctk/tests/test_modelgrid.py b/exoctk/tests/test_modelgrid.py index ec8cffd4..f973b23f 100755 --- a/exoctk/tests/test_modelgrid.py +++ b/exoctk/tests/test_modelgrid.py @@ -17,7 +17,6 @@ pytest -s modelgrid.py """ -import os from pkg_resources import resource_filename import numpy as np @@ -61,4 +60,3 @@ def test_model_getter_off_grid(): model = mgrid.get(4023, 4.1, -0.1) assert isinstance(model.get('flux'), np.ndarray) - diff --git a/exoctk/tests/test_visibilityPA.py b/exoctk/tests/test_visibilityPA.py index d5bb72d0..3dc08830 100644 --- a/exoctk/tests/test_visibilityPA.py +++ b/exoctk/tests/test_visibilityPA.py @@ -7,6 +7,8 @@ @pytest.mark.skipif(sys.version_info > (3, 9), reason='jwst_gtvt does not currently support python>=3.9.') def test_using_gtvt(): + """Tests the ``using_gtvt`` function""" + instrument = 'NIRISS' # this ra/dec has bad PAs From bdbed1e79df27cff6faae97fa1f9011339e5101e Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Mon, 7 Jun 2021 11:47:01 -0400 Subject: [PATCH 13/20] PEP8 fixes --- exoctk/references.py | 10 +- exoctk/setup_package.py | 2 - exoctk/throughputs.py | 10 +- exoctk/utils.py | 640 ++++++++++++++++++++-------------------- 4 files changed, 336 insertions(+), 326 deletions(-) diff --git a/exoctk/references.py b/exoctk/references.py index c5d72589..04518907 100755 --- a/exoctk/references.py +++ b/exoctk/references.py @@ -3,6 +3,7 @@ """ A module for managing references in ExoCTK """ + import os import pkg_resources @@ -17,7 +18,8 @@ class References(object): Attributes ---------- bibfile: str - The path to the bibtex file from which the references will be read + The path to the bibtex file from which the references will be + read refs: list The list of bibcodes saved during the user session database: bibtexparser.bibdatabase.BibDatabase object @@ -34,7 +36,8 @@ def __init__(self, bibfile=''): Parameters ---------- bibfile: str - The path to the bibtex file from which the references will be read + The path to the bibtex file from which the references will be + read """ bibfile = bibfile or \ @@ -79,7 +82,8 @@ def remove(self, bibcode): Parameters ---------- bibcode: str - The unique compact identifier for the reference to be removed + The unique compact identifier for the reference to be + removed """ # Check that the bibcode is in the bibtex file diff --git a/exoctk/setup_package.py b/exoctk/setup_package.py index f2ebf4dc..9ead6130 100644 --- a/exoctk/setup_package.py +++ b/exoctk/setup_package.py @@ -1,4 +1,2 @@ -from distutils.extension import Extension - def get_package_data(): return {'exoctk': ['data/*', 'data/images/*', 'data/core/*', 'data/core/modelgrid/*', 'data/contam_visibility/*', 'exoctk_app/static/images/*', 'exoctk_app/static/css/*', 'exoctk_app/static/js/*', 'exoctk_app/static/js/vendor/*']} diff --git a/exoctk/throughputs.py b/exoctk/throughputs.py index a4b9c69e..8dea1458 100644 --- a/exoctk/throughputs.py +++ b/exoctk/throughputs.py @@ -3,11 +3,11 @@ """ A module for creating and managing grids of model spectra """ + from glob import glob import json import os from pkg_resources import resource_filename -import warnings import numpy as np from svo_filters.svo import Filter @@ -24,7 +24,8 @@ class Throughput(Filter): def __init__(self, name, **kwargs): """ - Initialize the Throughput object as a child class of svo_filters.svo.Filter + Initialize the Throughput object as a child class of + svo_filters.svo.Filter Parameters ---------- @@ -81,7 +82,8 @@ def get_pce(instrument='niriss', mode='soss', filter='clear', disperser='gr700xd def generate_JWST_throughputs(path=None, data_dir=None): """ - Function to generate .txt filte of all JWST filter and grism throughputs + Function to generate .txt filte of all JWST filter and grism + throughputs """ # Check if environment variable exists path = path or os.environ.get('pandeia_refdata') @@ -147,4 +149,4 @@ def generate_JWST_throughputs(path=None, data_dir=None): print("{} file created!".format(datafile_path)) except KeyError: - print("Could not produce throughput for {}".format(conf)) \ No newline at end of file + print("Could not produce throughput for {}".format(conf)) diff --git a/exoctk/utils.py b/exoctk/utils.py index ef29412c..575c98b3 100755 --- a/exoctk/utils.py +++ b/exoctk/utils.py @@ -46,8 +46,7 @@ 'https://data.science.stsci.edu/redirect/JWST/ExoCTK/compressed/fortney.tar.gz', 'https://data.science.stsci.edu/redirect/JWST/ExoCTK/compressed/groups_integrations.tar.gz', 'https://data.science.stsci.edu/redirect/JWST/ExoCTK/compressed/groups_integrations.tar.gz', - 'https://data.science.stsci.edu/redirect/JWST/ExoCTK/compressed/exoctk_contam.tar.gz'] - } + 'https://data.science.stsci.edu/redirect/JWST/ExoCTK/compressed/exoctk_contam.tar.gz']} # If the variable is blank or doesn't exist HOME_DIR = os.path.expanduser('~') @@ -81,8 +80,54 @@ MODELGRID_DIR = os.path.join(EXOCTK_DATA, 'modelgrid/') +def build_target_url(target_name): + """Build restful api url based on target name. + + Parameters + ---------- + target_name : string + The name of the target transit. + + Returns + ------- + target_url : string + """ + + # Encode the target name string. + encode_target_name = urllib.parse.quote(target_name, encoding='utf-8') + target_url = "https://exo.mast.stsci.edu/api/v0.1/exoplanets/{}/properties/".format(encode_target_name) + + return target_url + + +def calc_zoom(R_f, arr): + """ + Calculate the zoom factor required to make the given + array into the given resolution + + Parameters + ---------- + R_f: int + The desired final resolution of the wavelength array + arr: array-like + The array to zoom + """ + + # Get initial resolution + lam = arr[-1] - arr[0] + d_lam_i = np.nanmean(np.diff(arr)) + # R_i = lam/d_lam_i + + # Calculate zoom + d_lam_f = lam / R_f + z = d_lam_i / d_lam_f + + return z + + def check_for_data(tool): - """Checks to see if the necessary data has been downloaded for the given tool + """Checks to see if the necessary data has been downloaded for the + given tool Parameters ---------- @@ -102,6 +147,45 @@ def check_for_data(tool): raise IOError("This tool requires the '{0}' data. Try downloading with exoctk.utils.download_exoctk_data('{0}')".format(tool)) +def color_gen(colormap='viridis', key=None, n=10): + """Color generator for Bokeh plots + + Parameters + ---------- + colormap: str, sequence + The name of the color map + + Returns + ------- + generator + A generator for the color palette + """ + if colormap in dir(bpal): + palette = getattr(bpal, colormap) + + if isinstance(palette, dict): + if key is None: + key = list(palette.keys())[0] + palette = palette[key] + + elif callable(palette): + palette = palette(n) + + else: + raise TypeError("pallette must be a bokeh palette name or a sequence of color hex values.") + + elif isinstance(colormap, (list, tuple)): + palette = colormap + + else: + raise TypeError("pallette must be a bokeh palette name or a sequence of color hex values.") + + yield from itertools.cycle(palette) + + +COLORS = color_gen('Category10') + + def download_exoctk_data(tool='all', exoctk_data_dir=EXOCTK_DATA): """Retrieves the ``exoctk_data`` materials from Box, downloads them to the user's local machine, uncompresses the files, and arranges @@ -115,6 +199,7 @@ def download_exoctk_data(tool='all', exoctk_data_dir=EXOCTK_DATA): The path to where the ExoCTK data package will be downloaded. The default setting is the user's $HOME directory. """ + # Validate tool if tool not in DATA_URLS: raise ValueError("'{}' not a supported tool. Try {}".format(tool, list(DATA_URLS.keys()))) @@ -164,290 +249,22 @@ def download_exoctk_data(tool='all', exoctk_data_dir=EXOCTK_DATA): except FileExistsError: pass modelgrid_files = glob.glob(os.path.join(exoctk_data_dir, 'modelgrid.*', '*')) - for src in modelgrid_files: - if 'ATLAS9' in src: - dst = os.path.join(exoctk_data_dir, 'modelgrid', 'ATLAS9') - elif 'ACES_' in src: - dst = os.path.join(exoctk_data_dir, 'modelgrid', 'ACES') - try: - shutil.move(src, dst) - except shutil.Error: - print('Unable to organize modelgrid/ directory') - - for dir in ['modelgrid.ATLAS9', 'modelgrid.ACES_1', 'modelgrid.ACES_2']: - path = os.path.join(exoctk_data_dir, dir) - if os.path.exists(path): - shutil.rmtree(path) - - print('Completed!') - - -def color_gen(colormap='viridis', key=None, n=10): - """Color generator for Bokeh plots - - Parameters - ---------- - colormap: str, sequence - The name of the color map - - Returns - ------- - generator - A generator for the color palette - """ - if colormap in dir(bpal): - palette = getattr(bpal, colormap) - - if isinstance(palette, dict): - if key is None: - key = list(palette.keys())[0] - palette = palette[key] - - elif callable(palette): - palette = palette(n) - - else: - raise TypeError("pallette must be a bokeh palette name or a sequence of color hex values.") - - elif isinstance(colormap, (list, tuple)): - palette = colormap - - else: - raise TypeError("pallette must be a bokeh palette name or a sequence of color hex values.") - - yield from itertools.cycle(palette) - - -COLORS = color_gen('Category10') - - -def interp_flux(mu, flux, params, values): - """ - Interpolate a cube of synthetic spectra for a - given index of mu - - Parameters - ---------- - mu: int - The index of the (Teff, logg, FeH, *mu*, wavelength) - data cube to interpolate - flux: np.ndarray - The 5D data array - params: list - A list of each free parameter range - values: list - A list of each free parameter values - - Returns - ------- - tu - The array of new flux values - """ - # Iterate over each wavelength (-1 index of flux array) - shp = flux.shape[-1] - flx = np.zeros(shp) - generators = [] - for lam in range(shp): - interp_f = RegularGridInterpolator(params, flux[:, :, :, mu, lam]) - f, = interp_f(values) - - flx[lam] = f - generators.append(interp_f) - - return flx, generators - - -def calc_zoom(R_f, arr): - """ - Calculate the zoom factor required to make the given - array into the given resolution - - Parameters - ---------- - R_f: int - The desired final resolution of the wavelength array - arr: array-like - The array to zoom - """ - # Get initial resolution - lam = arr[-1] - arr[0] - d_lam_i = np.nanmean(np.diff(arr)) - # R_i = lam/d_lam_i - - # Calculate zoom - d_lam_f = lam / R_f - z = d_lam_i / d_lam_f - - return z - - -def rebin_spec(spec, wavnew, oversamp=100, plot=False): - """ - Rebin a spectrum to a new wavelength array while preserving - the total flux - - Parameters - ---------- - spec: array-like - The wavelength and flux to be binned - wavenew: array-like - The new wavelength array - - Returns - ------- - np.ndarray - The rebinned flux - - """ - wave, flux = spec - nlam = len(wave) - x0 = np.arange(nlam, dtype=float) - x0int = np.arange((nlam - 1.) * oversamp + 1., dtype=float) / oversamp - w0int = np.interp(x0int, x0, wave) - spec0int = np.interp(w0int, wave, flux) / oversamp - - # Set up the bin edges for down-binning - maxdiffw1 = np.diff(wavnew).max() - w1bins = np.concatenate(([wavnew[0] - maxdiffw1], .5 * (wavnew[1::] + wavnew[0: -1]), [wavnew[-1] + maxdiffw1])) - - # Bin down the interpolated spectrum: - w1bins = np.sort(w1bins) - nbins = len(w1bins) - 1 - specnew = np.zeros(nbins) - inds2 = [[w0int.searchsorted(w1bins[ii], side='left'), w0int.searchsorted(w1bins[ii + 1], side='left')] for ii in range(nbins)] - - for ii in range(nbins): - specnew[ii] = np.sum(spec0int[inds2[ii][0]: inds2[ii][1]]) - - return specnew - - -def writeFITS(filename, extensions, headers=()): - """ - Write some data to a new FITS file - - Parameters - ---------- - filename: str - The filename of the output FITS file - extensions: dict - The extension name and associated data to include - in the file - headers: array-like - The (keyword, value, comment) groups for the PRIMARY - header extension - - """ - # Write the arrays to a FITS file - prihdu = fits.PrimaryHDU() - prihdu.name = 'PRIMARY' - hdulist = fits.HDUList([prihdu]) - - # Write the header to the PRIMARY HDU - hdulist['PRIMARY'].header.extend(headers, end=True) - - # Write the data to the HDU - for k, v in extensions.items(): - hdulist.append(fits.ImageHDU(data=v, name=k)) - - # Write the file - hdulist.writeto(filename, clobber=True) - hdulist.close() - - # Insert END card to prevent header error - # hdulist[0].header.tofile(filename, endcard=True, clobber=True) - - -def smooth(x, window_len=10, window='hanning'): - """smooth the data using a window with requested size. - - This method is based on the convolution of a scaled window with the signal. - The signal is prepared by introducing reflected copies of the signal - (with the window size) in both ends so that transient parts are minimized - in the begining and end part of the output signal. - - Parameters - ---------- - x: sequence - The input signal - window_len: int - The dimension of the smoothing window - window: str - The type of window from 'flat', 'hanning', 'hamming', 'bartlett', - 'blackman'. 'flat' window will produce a moving average smoothing. - - Retruns - ------- - np.ndarray - The smoothed signal - - Example - ------- - t = linspace(-2, 2, 0.1) - x = sin(t)+randn(len(t))*0.1 - y = smooth(x) - """ - if x.ndim != 1: - raise ValueError("smooth only accepts 1 dimension arrays.") - - if x.size < window_len: - raise ValueError("Input vector needs to be bigger than window size.") - - if window_len < 3: - return x - - if window not in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']: - raise ValueError("Window is one of 'flat', 'hanning', 'hamming',\ - 'bartlett', 'blackman'") - - s = np.r_[2 * np.median(x[0: window_len / 5]) - x[window_len: 1: -1], x, 2 * np.median(x[-window_len / 5:]) - x[-1: -window_len: -1]] - - if window == 'flat': - w = np.ones(window_len, 'd') - else: - w = eval('np.' + window + '(window_len)') - - y = np.convolve(w / w.sum(), s, mode='same') - - return y[window_len - 1: -window_len + 1] - - -def medfilt(x, window_len): - """ - Apply a length-k median filter to a 1D array x. - Boundaries are extended by repeating endpoints. - - Parameters - ---------- - x: np.array - The 1D array to smooth - window_len: int - The size of the smoothing window + for src in modelgrid_files: + if 'ATLAS9' in src: + dst = os.path.join(exoctk_data_dir, 'modelgrid', 'ATLAS9') + elif 'ACES_' in src: + dst = os.path.join(exoctk_data_dir, 'modelgrid', 'ACES') + try: + shutil.move(src, dst) + except shutil.Error: + print('Unable to organize modelgrid/ directory') - Returns - ------- - np.ndarray - The smoothed 1D array - """ - # assert x.ndim == 1, "Input must be one-dimensional." - if window_len % 2 == 0: - s1 = "Median filter length (" - s2 = ") must be odd. Adding 1." - print(s1 + str(window_len) + s2) - window_len += 1 - window_len = int(window_len) - k2 = int((window_len - 1) // 2) - s = np.r_[2 * np.median(x[0: int(window_len / 5)]) - x[window_len: 1: -1], x, 2 * np.median(x[int(-window_len / 5):]) - x[-1: -window_len: -1]] - y = np.zeros((len(s), window_len), dtype=s.dtype) + for dir in ['modelgrid.ATLAS9', 'modelgrid.ACES_1', 'modelgrid.ACES_2']: + path = os.path.join(exoctk_data_dir, dir) + if os.path.exists(path): + shutil.rmtree(path) - y[:, k2] = s - for i in range(k2): - j = k2 - i - y[j:, i] = s[:-j] - y[: j, i] = s[0] - y[: -j, -(i + 1)] = s[j:] - y[-j:, -(i + 1)] = s[-1] - return np.median(y[window_len - 1: -window_len + 1], axis=1) + print('Completed!') def filter_table(table, **kwargs): @@ -470,6 +287,7 @@ def filter_table(table, **kwargs): astropy.table.Table, pandas.DataFrame The filtered table """ + for param, value in kwargs.items(): # Check it is a valid column @@ -555,8 +373,7 @@ def filter_table(table, **kwargs): def find_closest(axes, points, n=1, values=False): - """ - Find the n-neighboring elements of a given value in an array + """Find the n-neighboring elements of a given value in an array Parameters ---------- @@ -571,6 +388,7 @@ def find_closest(axes, points, n=1, values=False): np.ndarray The n-values to the left and right of 'points' in 'axes' """ + results = [] if not isinstance(axes, list): axes = [axes] @@ -596,37 +414,18 @@ def find_closest(axes, points, n=1, values=False): return results -def build_target_url(target_name): - '''Build restful api url based on target name. - - Parameters - ---------- - target_name : string - The name of the target transit. - - Returns - ------- - target_url : string - ''' - # Encode the target name string. - encode_target_name = urllib.parse.quote(target_name, encoding='utf-8') - target_url = "https://exo.mast.stsci.edu/api/v0.1/exoplanets/{}/properties/".format(encode_target_name) - - return target_url - - def get_canonical_name(target_name): - '''Get ExoMAST prefered name for exoplanet. + """Get ExoMAST prefered name for exoplanet. - Parameters - ---------- - target_name : string - The name of the target transit. + Parameters + ---------- + target_name : string + The name of the target transit. - Returns - ------- - canonical_name : string - ''' + Returns + ------- + canonical_name : string + """ target_url = "https://exo.mast.stsci.edu/api/v0.1/exoplanets/identifiers/" @@ -686,8 +485,7 @@ def get_env_variables(): def get_target_data(target_name): - """ - Send request to exomast restful api for target information. + """Send request to exomast restful api for target information. Parameters ---------- @@ -732,3 +530,211 @@ def get_target_data(target_name): url = 'https://exo.mast.stsci.edu/exomast_planet.html?planet={}'.format(re.sub(r'\W+', '', canonical_name)) return target_data, url + + +def interp_flux(mu, flux, params, values): + """ + Interpolate a cube of synthetic spectra for a + given index of mu + + Parameters + ---------- + mu: int + The index of the (Teff, logg, FeH, *mu*, wavelength) + data cube to interpolate + flux: np.ndarray + The 5D data array + params: list + A list of each free parameter range + values: list + A list of each free parameter values + + Returns + ------- + tu + The array of new flux values + """ + + # Iterate over each wavelength (-1 index of flux array) + shp = flux.shape[-1] + flx = np.zeros(shp) + generators = [] + for lam in range(shp): + interp_f = RegularGridInterpolator(params, flux[:, :, :, mu, lam]) + f, = interp_f(values) + + flx[lam] = f + generators.append(interp_f) + + return flx, generators + + +def medfilt(x, window_len): + """Apply a length-k median filter to a 1D array x. + Boundaries are extended by repeating endpoints. + + Parameters + ---------- + x: np.array + The 1D array to smooth + window_len: int + The size of the smoothing window + + Returns + ------- + np.ndarray + The smoothed 1D array + """ + + # assert x.ndim == 1, "Input must be one-dimensional." + if window_len % 2 == 0: + s1 = "Median filter length (" + s2 = ") must be odd. Adding 1." + print(s1 + str(window_len) + s2) + window_len += 1 + window_len = int(window_len) + k2 = int((window_len - 1) // 2) + s = np.r_[2 * np.median(x[0: int(window_len / 5)]) - x[window_len: 1: -1], x, 2 * np.median(x[int(-window_len / 5):]) - x[-1: -window_len: -1]] + y = np.zeros((len(s), window_len), dtype=s.dtype) + + y[:, k2] = s + for i in range(k2): + j = k2 - i + y[j:, i] = s[:-j] + y[: j, i] = s[0] + y[: -j, -(i + 1)] = s[j:] + y[-j:, -(i + 1)] = s[-1] + return np.median(y[window_len - 1: -window_len + 1], axis=1) + + +def rebin_spec(spec, wavnew, oversamp=100, plot=False): + """ + Rebin a spectrum to a new wavelength array while preserving + the total flux + + Parameters + ---------- + spec: array-like + The wavelength and flux to be binned + wavenew: array-like + The new wavelength array + + Returns + ------- + np.ndarray + The rebinned flux + """ + + wave, flux = spec + nlam = len(wave) + x0 = np.arange(nlam, dtype=float) + x0int = np.arange((nlam - 1.) * oversamp + 1., dtype=float) / oversamp + w0int = np.interp(x0int, x0, wave) + spec0int = np.interp(w0int, wave, flux) / oversamp + + # Set up the bin edges for down-binning + maxdiffw1 = np.diff(wavnew).max() + w1bins = np.concatenate(([wavnew[0] - maxdiffw1], .5 * (wavnew[1::] + wavnew[0: -1]), [wavnew[-1] + maxdiffw1])) + + # Bin down the interpolated spectrum: + w1bins = np.sort(w1bins) + nbins = len(w1bins) - 1 + specnew = np.zeros(nbins) + inds2 = [[w0int.searchsorted(w1bins[ii], side='left'), w0int.searchsorted(w1bins[ii + 1], side='left')] for ii in range(nbins)] + + for ii in range(nbins): + specnew[ii] = np.sum(spec0int[inds2[ii][0]: inds2[ii][1]]) + + return specnew + + +def smooth(x, window_len=10, window='hanning'): + """smooth the data using a window with requested size. + + This method is based on the convolution of a scaled window with the + signal. The signal is prepared by introducing reflected copies of + the signal (with the window size) in both ends so that transient + parts are minimized in the begining and end part of the output + signal. + + Parameters + ---------- + x: sequence + The input signal + window_len: int + The dimension of the smoothing window + window: str + The type of window from 'flat', 'hanning', 'hamming', + 'bartlett', 'blackman'. 'flat' window will produce a moving + average smoothing. + + Retruns + ------- + np.ndarray + The smoothed signal + + Example + ------- + t = linspace(-2, 2, 0.1) + x = sin(t)+randn(len(t))*0.1 + y = smooth(x) + """ + if x.ndim != 1: + raise ValueError("smooth only accepts 1 dimension arrays.") + + if x.size < window_len: + raise ValueError("Input vector needs to be bigger than window size.") + + if window_len < 3: + return x + + if window not in ['flat', 'hanning', 'hamming', 'bartlett', 'blackman']: + raise ValueError("Window is one of 'flat', 'hanning', 'hamming',\ + 'bartlett', 'blackman'") + + s = np.r_[2 * np.median(x[0: window_len / 5]) - x[window_len: 1: -1], x, 2 * np.median(x[-window_len / 5:]) - x[-1: -window_len: -1]] + + if window == 'flat': + w = np.ones(window_len, 'd') + else: + w = eval('np.' + window + '(window_len)') + + y = np.convolve(w / w.sum(), s, mode='same') + + return y[window_len - 1: -window_len + 1] + + +def writeFITS(filename, extensions, headers=()): + """ + Write some data to a new FITS file + + Parameters + ---------- + filename: str + The filename of the output FITS file + extensions: dict + The extension name and associated data to include + in the file + headers: array-like + The (keyword, value, comment) groups for the PRIMARY + header extension + + """ + # Write the arrays to a FITS file + prihdu = fits.PrimaryHDU() + prihdu.name = 'PRIMARY' + hdulist = fits.HDUList([prihdu]) + + # Write the header to the PRIMARY HDU + hdulist['PRIMARY'].header.extend(headers, end=True) + + # Write the data to the HDU + for k, v in extensions.items(): + hdulist.append(fits.ImageHDU(data=v, name=k)) + + # Write the file + hdulist.writeto(filename, clobber=True) + hdulist.close() + + # Insert END card to prevent header error + # hdulist[0].header.tofile(filename, endcard=True, clobber=True) From a11fa3c159fcddec290a672fabeb471633aafbac Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Mon, 7 Jun 2021 12:03:01 -0400 Subject: [PATCH 14/20] Fixed some lingering @pep8speaks issues --- exoctk/tests/test_lightcurve_fitting.py | 4 ++-- exoctk/throughputs.py | 9 ++++----- 2 files changed, 6 insertions(+), 7 deletions(-) diff --git a/exoctk/tests/test_lightcurve_fitting.py b/exoctk/tests/test_lightcurve_fitting.py index ed9fbc48..09aa70a3 100755 --- a/exoctk/tests/test_lightcurve_fitting.py +++ b/exoctk/tests/test_lightcurve_fitting.py @@ -39,7 +39,7 @@ def test_lightcurve(self): # Test that parameters can be assigned lin1 = models.PolynomialModel(c1=0.0005, c0=0.997, name='linear 1') lin2 = models.PolynomialModel(c1=0.001, c0=0.92, name='linear 2') - comp_model = lin1*lin2 + comp_model = lin1 * lin2 # Test the fitting routine self.lc.fit(comp_model) @@ -52,7 +52,7 @@ def test_compositemodel(self): """Tests for the CompositeModel class""" model1 = models.Model() model2 = models.Model() - self.comp_model = model1*model2 + self.comp_model = model1 * model2 self.comp_model.name = 'composite' def setUp(self): diff --git a/exoctk/throughputs.py b/exoctk/throughputs.py index 81776a47..5fc515fa 100644 --- a/exoctk/throughputs.py +++ b/exoctk/throughputs.py @@ -81,10 +81,9 @@ def get_pce(instrument='niriss', mode='soss', filter='clear', disperser='gr700xd def generate_JWST_throughputs(path=None, data_dir=None): - """ - Function to generate .txt filte of all JWST filter and grism - throughputs - """ + """Function to generate .txt filte of all JWST filter and grism + throughputs""" + # Check if environment variable exists path = path or os.environ.get('pandeia_refdata') data_dir = data_dir or resource_filename('exoctk', 'data/throughputs/') @@ -149,7 +148,7 @@ def generate_JWST_throughputs(path=None, data_dir=None): print("{} file created!".format(datafile_path)) except KeyError: - print("Could not produce throughput for {}".format(conf) + print("Could not produce throughput for {}".format(conf)) # Do NIRSpec separately because pandeia doesn't show them correctly nirspec_combos = ['G140M/F070LP', 'G140M/F100LP', 'G235M/F170LP', 'G395M/F290LP', 'G140H/F070LP', 'G140H/F100LP', 'G235H/F170LP', 'G395H/F290LP', 'PRISM/CLEAR'] From 4a0a0514d34891f998cc7ca8e79afc05e2774ec1 Mon Sep 17 00:00:00 2001 From: Matthew Bourque Date: Mon, 7 Jun 2021 12:09:43 -0400 Subject: [PATCH 15/20] Fixed issue with MultiCheckBoxField --- exoctk/exoctk_app/form_validation.py | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/exoctk/exoctk_app/form_validation.py b/exoctk/exoctk_app/form_validation.py index 9cf49c2a..1b56fb3d 100644 --- a/exoctk/exoctk_app/form_validation.py +++ b/exoctk/exoctk_app/form_validation.py @@ -3,7 +3,7 @@ from flask_wtf import FlaskForm import numpy as np from svo_filters import svo -from wtforms import StringField, SubmitField, DecimalField, MultiCheckboxField, RadioField, SelectField, SelectMultipleField, IntegerField, FloatField +from wtforms import StringField, SubmitField, DecimalField, RadioField, SelectField, SelectMultipleField, IntegerField, FloatField from wtforms.validators import InputRequired, NumberRange from wtforms.widgets import ListWidget, CheckboxInput @@ -21,6 +21,12 @@ class BaseForm(FlaskForm): resolve_submit = SubmitField('Resolve Target') +class MultiCheckboxField(SelectMultipleField): + """Makes a list of checkbox inputs""" + widget = ListWidget(prefix_label=False) + option_widget = CheckboxInput() + + class ContamVisForm(BaseForm): """Form validation for the contamination_visibility tool""" # Form submits @@ -131,12 +137,6 @@ class LimbDarkeningForm(BaseForm): modelgrid_submit = SubmitField('Model Grid Selected') -class MultiCheckboxField(SelectMultipleField): - """Makes a list of checkbox inputs""" - widget = ListWidget(prefix_label=False) - option_widget = CheckboxInput() - - class PhaseConstraint(BaseForm): """Form validation for the phase-constraint tool""" From 19ec3889d0f5fda5957b9799859bcb5155fdafc2 Mon Sep 17 00:00:00 2001 From: nespinoza Date: Fri, 9 Jul 2021 10:54:48 -0400 Subject: [PATCH 16/20] Fixed phase-constraint bug that didnt set eccentricity to nan when not found in target resolver. --- exoctk/exoctk_app/app_exoctk.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exoctk/exoctk_app/app_exoctk.py b/exoctk/exoctk_app/app_exoctk.py index 1937c3a0..9566d864 100644 --- a/exoctk/exoctk_app/app_exoctk.py +++ b/exoctk/exoctk_app/app_exoctk.py @@ -750,7 +750,7 @@ def phase_constraint(transit_type='primary'): form.eccentricity.data = data.get('eccentricity') if form.eccentricity.data is None: - form.omega.data = np.nan + form.eccentricity.data = np.nan form.target_url.data = str(target_url) From 3cd096075bf2cff2c25959badbabd39a131298a1 Mon Sep 17 00:00:00 2001 From: nespinoza Date: Fri, 9 Jul 2021 10:58:03 -0400 Subject: [PATCH 17/20] Bumped ExoCTK version to 1.2.2 --- setup.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/setup.py b/setup.py index b77ab0d8..8995873f 100755 --- a/setup.py +++ b/setup.py @@ -49,7 +49,7 @@ setup( name='exoctk', - version='1.2.1', + version='1.2.2', description='Observation reduction and planning tools for exoplanet science', packages=find_packages( ".", From 1f6adf10ee7cfc2932968576f81b415532801d87 Mon Sep 17 00:00:00 2001 From: nespinoza Date: Fri, 9 Jul 2021 11:04:39 -0400 Subject: [PATCH 18/20] Updated DOI and info on citation. --- README.rst | 50 +++++++++++++++++++++++++++----------------------- 1 file changed, 27 insertions(+), 23 deletions(-) diff --git a/README.rst b/README.rst index 03cd8e24..de08f73c 100644 --- a/README.rst +++ b/README.rst @@ -138,33 +138,37 @@ If you use ExoCTK for work/research presented in a publication (whether directly :: - This research made use of the open source Python package exoctk, the Exoplanet Characterization Toolkit (Espinoza et al, 2021). + This research made use of the open source Python package exoctk, the Exoplanet Characterization Toolkit (Bourque et al, 2021). -where (Espinoza et al, 2021) is a citation of the Zenodo record, e.g.: +where (Bourque et al, 2021) is a citation of the Zenodo record, e.g.: :: - @software{nestor_espinoza_2021_4556063, - author = {Néstor Espinoza and - Matthew Bourque and - Joseph Filippazzo and - Michael Fox and - Jules Fowler and - Teagan King and - Catherine Martlin and - Jennifer Medina and - Mees Fix and - Kevin Stevenson and - Jeff Valenti}, - title = {The Exoplanet Characterization Toolkit (ExoCTK)}, - month = feb, - year = 2021, - publisher = {Zenodo}, - version = {1.0.0}, - doi = {10.5281/zenodo.4556063}, - url = {https://doi.org/10.5281/zenodo.4556063} - } - + @software{matthew_bourque_2021_4556063, + author = {Matthew Bourque and + Néstor Espinoza and + Joseph Filippazzo and + Mees Fix and + Teagan King and + Catherine Martlin and + Jennifer Medina and + Natasha Batalha and + Michael Fox and + Jules Fowler and + Jonathan Fraine and + Matthew Hill and + Nikole Lewis and + Kevin Stevenson and + Jeff Valenti and + Hannah Wakeford}, + title = {The Exoplanet Characterization Toolkit (ExoCTK)}, + month = feb, + year = 2021, + publisher = {Zenodo}, + version = {1.0.0}, + doi = {10.5281/zenodo.4556063}, + url = {https://doi.org/10.5281/zenodo.4556063} + } Want to stay up-to-date with our releases and updates? ------------------------------------------------------ From 2ffdd77234fc10a3019109adaf3dc04aa27d8969 Mon Sep 17 00:00:00 2001 From: nespinoza Date: Fri, 9 Jul 2021 11:05:23 -0400 Subject: [PATCH 19/20] updated changelog --- CHANGELOG.md | 10 ++++++++++ 1 file changed, 10 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8691d693..d90b33c2 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,16 @@ This file tracks all major changes in each `exoctk` release. The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/), and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html). +## [1.2.2] - 2021-07-09 + +### Added + +- Sweep to update code to match PEP8 standards. +- Extra authors on citation information to match current working DOI. + +### Fixed + +- Phase-constraint bug that didn't change `eccentricity` to `nan` when not found by the target resolver. ## [1.2.1] - 2021-06-09 From afd65734243975a2befa57f3edd8bae44513bb53 Mon Sep 17 00:00:00 2001 From: nespinoza Date: Fri, 9 Jul 2021 11:08:13 -0400 Subject: [PATCH 20/20] Bumped web-app version to match 1.2.2 --- exoctk/exoctk_app/templates/base.html | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/exoctk/exoctk_app/templates/base.html b/exoctk/exoctk_app/templates/base.html index aa544972..fd05bb60 100644 --- a/exoctk/exoctk_app/templates/base.html +++ b/exoctk/exoctk_app/templates/base.html @@ -146,7 +146,7 @@

Running
- exoctk v1.2.1{{version|safe}} + exoctk v1.2.2{{version|safe}}

Admin Area