Repository: ExoCTK/exoctk (language: python)

Issue:
Phase constraint hotfix + ready for development updates
Hi folks,

 Here comes a (hot) fix to a bug found by Nikolay Nikolov, in which the phase-constraint wouldn't work if eccentricity is not set manually to a number (or when not found by the target resolver). The problem was very simple: when the parameter is not found, it is populated by `None` by default in the web app forms. In theory we had solved this over a year ago, but apparently in the code I was double setting `omega` (and not `eccentricity`) as `np.nan` (which the web app form likes more than `None` values). I have now fixed that.

 In addition, I took the liberty of updating all the info in the `setup.py`, `CHANGELOG.md` and the web-app to match the next version release with this bug fixed (and Matthew's sweep of PEP8 on the code). I also took the liberty of updating the `Readme.rst` to match the DOI @bourque set up for us with the full list of co-authors to date of the package.

 This should be a relatively straightforward merge to `develop`!

N.


Discussion / hints:

Fix patch:
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
 
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?
 ------------------------------------------------------
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..e838835a 100755
--- a/exoctk/contam_visibility/astro_funcx.py
+++ b/exoctk/contam_visibility/astro_funcx.py
@@ -14,39 +14,9 @@
 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
+    """Calculates the change in position angle between two positions
+    with no roll about V1
 
     Parameters
     ----------
@@ -96,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
     ----------
@@ -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..56df13a5 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..36574b53 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
@@ -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):
@@ -83,168 +84,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 +225,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 +330,106 @@ 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..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
 
@@ -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..edd6c218 100755
--- a/exoctk/contam_visibility/field_simulator.py
+++ b/exoctk/contam_visibility/field_simulator.py
@@ -3,210 +3,67 @@
 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
 
 
@@ -227,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')
@@ -244,7 +102,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
@@ -256,8 +113,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
@@ -275,12 +131,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)))
@@ -290,17 +145,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
@@ -334,8 +183,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)
 
@@ -343,9 +191,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)))
 
@@ -377,8 +225,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):
 
@@ -388,8 +235,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(
@@ -408,8 +254,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',
@@ -483,12 +328,12 @@ 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######################
-    ########################################################################
+    # INSTRUMENT PARAMETERS
     # Instantiate a pySIAF object
     siaf = pysiaf.Siaf('MIRI')
     aper = siaf.apertures['MIRIM_SLITLESSPRISM']
@@ -498,7 +343,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
@@ -510,8 +354,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
@@ -529,12 +372,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)))
@@ -544,17 +386,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
@@ -588,8 +424,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)
 
@@ -597,9 +432,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)))
 
@@ -630,8 +465,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):
 
@@ -641,8 +475,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)
 
@@ -657,8 +490,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',
@@ -719,58 +551,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..ce1b4a63 100755
--- a/exoctk/contam_visibility/math_extensionsx.py
+++ b/exoctk/contam_visibility/math_extensionsx.py
@@ -14,379 +14,293 @@
 OBLIQUITY = 23.43929 * D2R  # Obliquity of Earth's orbit, in radians
 
 
-def sind(x):
-    """Return the sin in degrees.
+# Some classes need to be defined first so they are available to other classes
+class Histogram(object):
+    """Class to represent a histogram."""
 
-    Parameters
-    ----------
-    x: float
-        The evaluand.
+    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
 
-    Returns
-    -------
-    float
-        The sin of x in degrees.
-    """
-    return sin(radians(x))
+        for bin in self.bins:
+            result = result + '%s\n' % (bin.__str__())
 
+        return(result)
 
-def cosd(x):
-    """Return the cos in degrees
+    def normalize(self, total=None):
+        """Takes a histogram and returns a new histogram that normalizes
+        all its values.
 
-    Parameters
-    ----------
-    x: float
-        The evaluand.
+        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
-    -------
-    float
-        The cos of x in degrees.
-    """
-    return cos(radians(x))
+        Returns
+        -------
+        new_histogram : Histogram
+            The new histogram.
+        """
+        if (total is None):
+            total = self.num_items()
 
+        new_histogram = deepcopy(self)   # make a deep copy
 
-def atan2d(x):
-    """Return the arctan in degrees
+        for bin in new_histogram.bins:
+            bin.count = (float(bin.count)) / total
 
-    Parameters
-    ----------
-    x: float
-        The evaluand.
+        return(new_histogram)
 
-    Returns
-    -------
-    float
-        The arctan of x in degrees.
-    """
-    return atan2(radians(x))
+    def num_items(self):
+        """Returns the total number of items stored in the histogram
+        """
+        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.
 
-def really_less_than(x, y):
-    """Safe less-than function that returns true if and only if x is
-    "significantly" less than y.
+        Parameters
+        ----------
+        bin_index: int
+            The index to use (starts with 1).
 
-    Parameters
-    ----------
-    x: float
-        The first number.
-    y: float
-        The second number.
+        Returns
+        -------
+        int
+            The number of items in the bin.
+        """
+        return(self.bins[bin_index - 1].count)
 
-    Returns
-    -------
-    bool
-        True if x is less, else False.
-    """
-    return(x < y - EPSILON)
 
+class HistogramBin(object):
+    """Class to represent a bin within a histogram."""
 
-def really_greater_than(x, y):
-    """Safe greater-than function that returns true if and only if x is
-    "significantly" greater than y
+    def store_items(self, num_items=1):
+        """Stores a given number of items in the bin.
 
-    Parameters
-    ----------
-    x: float
-        The first number.
-    y: float
-        The second number.
+        Parameters
+        ----------
+        num_items: int
+            Number of items to store (default 1).
+        """
+        self.count += num_items
 
-    Returns
-    -------
-    bool
-        True if x is greater, else False.
-    """
-    return(x > y + EPSILON)
 
+class Polynomial(object):
+    """Class to represent a polynomial."""
 
-def asin2(val):
-    """Safe version of asin that handles invalid arguments.
+    def __init__(self, coefficients):
+        """Constructor for a polynomial.
+        Coefficients = a list of coefficients, starting with order 0 and
+        increasing.
 
-    Arguments greater than 1 are truncated to 1; arguments less than -1 are
-    set to -1.
+        Parameters
+        ----------
+        coefficients: sequence
+            The list of coefficients, starting with order 0 and
+            increasing.
+        """
+        self.coefficients = coefficients
 
-    Parameters
-    ----------
-    val: float
-        The evaluand.
+    def __str__(self):
+        """Returns a string representation of a polynomial."""
+        order = len(self.coefficients) - 1
+        return_string = 'Polynomial: order % d' % (order)
 
-    Returns
-    -------
-    float
-        The arcsin of the value.
-    """
-    return(asin(max(-1.0, min(1.0, val))))
+        for index in range(order + 1):  # print list of coefficients
+            c = index, self.coefficients[index]
+            return_string = return_string + '\nCoefficient % d = % .3f' % c
 
+        return(return_string)
 
-def acos2(val):
-    """Safe version of acos that handles invalid arguments in the same way as
-    asin2
+    def apply(self, value):
+        """Returns the result of applying a polynomial to an input value
 
-    Parameters
-    ----------
-    val: float
-        The evaluand.
+        Parameters
+        ----------
+        value: float
+            The evaluand.
 
-    Returns
-    -------
-    float
-        The arccos of the value.
-    """
-    return(acos(max(-1.0, min(1.0, val))))
+        Returns
+        -------
+        float
+            The result of the evaluated equation.
+        """
+        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
 
-def avg(l):
-    """Returns the average of a list of numbers
+        return (result)
 
-    Parameters
-    ----------
-    l: sequence
-        The list of numbers.
 
-    Returns
-    -------
-    float
-        The average.
-    """
-    return(sum(l) / float(len(l)))
+class Rectangle(object):
+    """Class to represent a rectangle."""
 
+    def __init__(self, length, width):
+        """Initialize a rectangle with a specified length and width.
 
-def avg2(num1, num2):
-    """Returns the average of two numbers
+        Parameters
+        ----------
+        length: float
+            The length of the rectangle.
+        width: float
+            The width of the rectangle.
+        """
+        self.length = length
+        self.width = width
 
-    Parameters
-    ----------
-    num1: float
-        The first number.
-    num2: float
-        The second number.
+    def __str__(self):
+        """Inspector method for the rectangle.
+        """
+        dims = (self.length, self.width)
+        return('Rectangle: length = % .2f, width = % .2f' % dims)
 
-    Returns
-    -------
-    float
-        The average.
-    """
-    return((num1 + num2) / 2.0)
+    def area(self):
+        """Returns the area of the rectangle.
+        """
+        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.
 
-def output_as_percentage(num, fractional_digits=1):
-    """Output a percentage neatly.
+        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.
 
-    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
+        -------
+        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))
 
-    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)
-
-
-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,176 +340,16 @@ def ismatch(self, value):
         return(value == self.bin_value)
 
 
-class RangeBin(HistogramBin):
-    """Class to represent a bin with a range."""
+class DiscreteHistogram(Histogram):
+    """Class to represent a histogram with discrete values."""
 
-    def __init__(self, min_value=None, max_value=None, lower_inclusive=False,
-                 upper_inclusive=True):
-        """Constructor for a range bin.
+    def __init__(self, values):
+        """Initializes a histogram with discrete values.
 
         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."""
-
-    def __init__(self, values):
-        """Initializes a histogram with discrete values.
-
-        Parameters
-        ----------
-        values: sequence
-            List of the discrete values.
+        values: sequence
+            List of the discrete values.
         """
         self.bins = []
 
@@ -603,11 +357,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 +379,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 +420,94 @@ 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.
+        self.coefficients = [coeff0, coeff1]
 
-        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
+    def apply(self, value):
+        """Applies a linear equation to an input value.
 
-        self.bins.append(RangeBin(min_value=lower_lim))   # Add highest bin
+        This is intended to be faster than the more general method with
+        Polynomial.
 
-        # 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
+        ----------
+        value: float
+            The evaluand.
 
-    def retrieve_boundaries(self):
-        """Returns the list of boundaries of a continuous histogram.
+        Returns
+        -------
+        float
+            The result of the evaluated equation.
         """
-        # Return upper limits of all bins except the last.
-        return([bin.max_value for bin in self.bins[:-1]])
+        return(value * self.coefficients[1] + self.coefficients[0])
 
-    def store_items(self, value, count=1):
-        """Stores a value in the continuous histogram.
+
+class PoissonDistribution(DiscreteHistogram):
+    """Class to represent a Poisson distribution."""
+
+    def __init__(self, mean, max_boundary):
+        """Constructor function for the Poisson distribution.
 
         Parameters
         ----------
-        value: float
-            The value to store.
-        count: int
-            Number of items with that value to store (default 1).
+        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.
         """
-        bin_index = 0
-        found = False
+        self.mean = mean
+        self.bins = []
 
-        # 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]
+        # Create discrete bins for values from 0 to max_boundary, inclusive.
+        for value in range(max_boundary + 1):
+            self.bins.append(DiscreteBin(value))
 
-            if (not (bin.istoo_high(value))):
-                found = True
-                bin.store_items(count)
+        # Use a RangeBin object for the highest bin.
+        self.bins.append(RangeBin(min_value=max_boundary))
 
-            bin_index += 1
+        # Now generate the distribution.
+        self.generate_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])
-
-    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)
-
-
-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 +524,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,21 +569,147 @@ 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.
+    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 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 __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 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 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
+
+    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 between 0 and the max_boundary of the distribution.
+            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 probability.
+            The area.
         """
-        return(sum([self.bins[i].count for i in range(value + 1)]))
+        # 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):
@@ -937,16 +728,15 @@ def __init__(self, data=None):
             for i in range(len(data)):
                 self.append(data[i])
 
-    def compute_variance(self):
-        """Computes the variance of a statistical list.
-        """
-        mean = self.mean
-
-        # 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 __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.
@@ -956,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
         ----------
@@ -1031,129 +821,347 @@ 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..1eb9afaa 100644
--- a/exoctk/contam_visibility/miniTools.py
+++ b/exoctk/contam_visibility/miniTools.py
@@ -1,30 +1,30 @@
-""" 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)
 ---------
 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,73 +40,10 @@
 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.
+    of the science image for any given observation using the parameters
+    provided.
 
     Parameter(s)
     ------------
@@ -119,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:
@@ -130,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"
 
     """
@@ -194,11 +133,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 +179,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 +276,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 +285,67 @@ 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..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
 """
@@ -37,679 +38,816 @@
 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.
+        rs: float
+            The scalar to add.
+
+        Returns
+        -------
+        Vector
+            The resultant vector.
         """
-        # Arguments specified in degrees, but values represented in radians.
-        self.latitude = radians(latitude)
-        self.longitude = radians(longitude)
-        self.anode = radians(ascending_node)
+        x = self.x + rs.x
+        y = self.y + rs.y
+        z = self.z + rs.z
+        return (Vector(x, y, z))
 
-    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
+    def __iadd__(self, rs):
+        """Implements Vector += vector.
 
+        Parameters
+        ----------
+        rs: float
+            The scalar to add.
 
-# supports transformation to galactic coordinates
-NGP = GalacticPole(192.859508, 27.128336, 32.932)
+        Returns
+        -------
+        Vector
+            The resultant vector.
+        """
+        self.x += rs.x
+        self.y += rs.y
+        self.z += rs.z
+        return (self)
 
+    def __idiv__(self, rs):
+        """Implements Vector /= float.
 
-def QX(angle):
-    """Creates rotation quaternion about X axis, rotates a vector about
-    this axis.
+        Parameters
+        ----------
+        rs: float
+            The scalar to divide.
 
-    Parameters
-    ----------
-    angle: float
-        The angle to rotate by.
+        Returns
+        -------
+        Vector
+            The resultant vector.
+        """
+        self.x /= rs
+        self.y /= rs
+        self.z /= rs
+        return (self)
 
-    Result
-    ------
-    Quarternion
-        The rotated quaternion.
-    """
-    return Quaternion(Vector(sin(angle / 2.), 0., 0.), cos(angle / 2.))
+    def __imul__(self, rs):
+        """Implements Vector *= float
 
+        Parameters
+        ----------
+        rs: float
+            The scalar to multiply.
 
-def QY(angle):
-    """Creates rotation quaternion about Y axis, rotates a vector about
-    this axis.
+        Returns
+        -------
+        Vector
+            The resultant vector.
+        """
+        self.x *= rs
+        self.y *= rs
+        self.z *= rs
+        return (self)
 
-    Parameters
-    ----------
-    angle: float
-        The angle to rotate by.
+    def __init__(self, x=0.0, y=0.0, z=0.0):
+        """Constructor for a three-dimensional vector.
 
-    Result
-    ------
-    Quarternion
-        The rotated quaternion.
-    """
-    return Quaternion(Vector(0., sin(angle / 2.), 0.), cos(angle / 2.))
+        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
 
-def QZ(angle):
-    """Creates rotation quaternion about Z axis, rotates a vector about
-    this axis
+    def __isub__(self, rs):
+        """Implements Vector -= vector.
 
-    Parameters
-    ----------
-    angle: float
-        The angle to rotate by.
+        Parameters
+        ----------
+        rs: float
+            The scalar to subtract.
 
-    Result
-    ------
-    Quarternion
-        The rotated quaternion.
-    """
-    return Quaternion(Vector(0., 0., sin(angle / 2.)), cos(angle / 2.))
+        Returns
+        -------
+        Vector
+            The resultant vector.
+        """
+        self.x -= rs.x
+        self.y -= rs.y
+        self.z -= rs.z
+        return (self)
 
+    def __mul__(self, rs):
+        """Implements Vector * scalar.  Can then use '*' syntax in
+        multiplying a vector by a scalar rs
 
-def Qmake_a_point(V):
-    """Creates a pure Q, i.e. defines a pointing not a rotation
+        Parameters
+        ----------
+        rs: float
+            The scalar to multiply.
 
-    Parameters
-    ----------
-    V: Vector
-        The vector.
+        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 point as a quaternion.
-    """
-    return Quaternion(V, 0.)
+    def __rmul__(self, ls):
+        """Implements float * Vector.
 
+        Parameters
+        ----------
+        ls: float
+            The scalar to multiply.
 
-def cvt_pt_Q_to_V(Q):
-    """Converts a pure (pointing) Q to a unit position Vector
+        Returns
+        -------
+        Vector
+            The resultant vector.
+        """
+        x = self.x * ls
+        y = self.y * ls
+        z = self.z * ls
+        return (Vector(x, y, z))
 
-    Parameters
-    ----------
-    Q: Quaternion
-        The quaternion to convert to a Vector.
+    def __str__(self):
+        """Returns a string representation of the vector."""
+        return('Vector: x: %.3f, y: %.3f, z: %.3f' % (self.x, self.y, self.z))
 
-    Returns
-    -------
-    Vector
-        The point as a vector.
-    """
-    return Vector(Q.q1, Q.q2, Q.q3)
+    def __sub__(self, rs):
+        """Implements Vector - Vector.
 
+        Parameters
+        ----------
+        rs: float
+            The scalar to subtract.
 
-# 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.
+        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))
 
-    Parameters
-    ----------
-    coord1: float
-        The first coordinate.
-    coord2: float
-        The second coordinate.
-    V3pa: float
-        The V3 position.
+    def __truediv__(self, rs):
+        """Implements Vector / float.
 
-    Returns
-    -------
-    Quaternion
-        The rotation quaternion
-    """
-    return QZ(coord1) * QY(-coord2) * QX(-V3pa)
+        Parameters
+        ----------
+        rs: float
+            The scalar to divide.
 
+        Returns
+        -------
+        Vector
+            The resultant vector.
+        """
+        x = self.x / rs
+        y = self.y / rs
+        z = self.z / rs
+        return (Vector(x, y, z))
 
-def Qmake_v2v3_2body(v2, v3):
-    """Creates a rotation Q, going from v2 and v3 in the body frame to
-    inertial.
+    # Replace by separation - RLH
+    def angle(self, V2):
+        """Returns angle between the two vectors in degrees.
 
-    Parameters
-    ----------
-    v2: float
-        The V2 position.
-    V3: float
-        The V3 position.
+        Parameters
+        ----------
+        V2: Vector
+            The vector to measure.
 
-    Returns
-    -------
-    Quaternion
-        The rotation quaternion.
-    """
-    return QY(v3) * QZ(-v2)
+        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)
 
+    def create_matrix(self):
+        """Converts a Vector into a single-column matrix."""
 
-def Qmake_v2v3_2inertial(coord1, coord2, V3pa, v2, v3):
-    """Creates a rotation Q, going from v2 and v3 in the body frame to
-    inertial
+        column = [self.x, self.y, self.z]
+        return(Matrix([[element] for element in column]))  # singleton list
 
-    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.
+    # Recommend deletion in favor of non-method version.
+    def cross(self, V1, V2):
+        """returns cross product of two vectors
 
-    Returns
-    -------
-    Quaternion
-        The rotation quaternion.
-    """
-    return QZ(coord1) * QY(-coord2) * QX(-V3pa) * QY(v3) * QZ(-v2)
+        Parameters
+        ----------
+        V1: Vector
+            The vector to cross.
+        V2: Vector
+            The vector to cross.
 
+        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)
 
-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.
+    # 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)
 
-    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.
+    # Recommend deletion -- better to use a single interface that takes
+    # two vectors.
+    def dot(self, V2):
+        """returns dot product between two vectors.
 
-    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
+        Parameters
+        ----------
+        V2: Vector
+            The vector to dot.
 
+        Returns
+        -------
+        Vector
+            The resultant vector.
+        """
+        return self.x * V2.x + self.y * V2.y + self.z * V2.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.
+    def length(self):
+        """Returns magnitude of the vector """
+        return(sqrt(self.x * self.x + self.y * self.y + self.z * self.z))
 
-    Parameters
-    ----------
-    Q: Quaternion
-        The quaternion.
+    def normalize(self):
+        """Returns copy of the normalized vector """
+        mag = self.length()
+        return (Vector(self.x / mag, self.y / mag, self.z / mag))
 
-    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
+    # RLH: What do these add?  We're creating methods just to access
+    # individual attributes.
+    def rx(self):
+        """The magnitude of x"""
+        return self.x
 
+    def ry(self):
+        """The magnitude of y"""
+        return self.y
 
-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 rz(self):
+        """The magnitude of z"""
+        return self.z
 
-    Parameters
-    ----------
-    Q: Quaternion
-        The quaternion.
-    v2: float
-        The V2 position.
-    v3: float
-        The V3 position.
+    def set_eq(self, x=None, y=None, z=None):
+        """Assigns new value to vector.
 
-    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))
+        Arguments are now optional to permit this to be used with 2D
+        vectors or to modify any subset of coordinates.
 
-    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
+        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
 
-    return coord1, coord2, pa
+    # RLH: Not necessary if CelestialVector is used.
+    def set_xyz(self, ra, dec):
+        """Creates a unit vector from spherical coordinates
 
+        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)
 
-def cvt_c1c2_using_body2inertial_Q_to_v2v3pa_tuple(Q, coord1, coord2):
-    """Given Q and a position, returns v2, v3, V3PA tuple
 
-    Parameters
-    ----------
-    Q: Quaternion
-        The quaternion.
-    coord1: float
-        The first coordinate.
-    coord2: float
-        The second coordinate
+class CelestialVector (Vector):
+    "Class to encapsulate a unit vector on the celestial sphere."
 
-    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
+    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.
+
+        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
+        ----------
+        ra: float
+            The right ascension.
+        dec: float
+            The declination.
+        frame: str
+            The frame to use.
+        degrees: bool
+            Use degrees.
+        """
+        if (degrees):
+            ra = math2.D2R * ra
+            dec = math2.D2R * dec
 
+        self.ra = ra
+        self.dec = dec
+        self.frame = frame
 
-class Quaternion:
-    """This representation is used by Wertz and Markley"""
+        # 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 __init__(self, V, q4):
-        """Quaternion constructor.
+    def __str__(self, verbose=True):
+        """Returns a string representation of the vector.  Displays
+        angles in degrees.
 
         Parameters
         ----------
-        V: Vector
-            The vector to construct the quaternion with.
-        q4: Vector
-            The fourth vector.
+        verbose: bool
+            Print some information.
         """
-        self.q1 = V.x
-        self.q2 = V.y
-        self.q3 = V.z
-        self.q4 = q4
-
-    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 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)
+        a = (math2.R2D * self.ra, math2.R2D * self.dec, self.frame)
+        celest_info = 'CelestialVector: RA: %.3fD, DEC: %.3fD, frame: %s' % a
 
-    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)
+        if (verbose):
+            clss = super(CelestialVector, self).__str__()
+            celest_info = celest_info + '\n' + clss
+            return celest_info
 
-    def conjugate(self):
-        """Returns a copy of the conjugated Q """
-        return Quaternion(Vector(-self.q1, -self.q2, -self.q3), self.q4)
+    def position_angle(self, v):
+        """Returns the position angle of v at the self vector, in
+        radians.
 
-    def __mul__(self, rs):
-        """Defines Q*Q for quaternion multiplication.
+        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
         ----------
-        rs: Quaternion
-            The quaternion to multiply.
+        v: Vector
+            The vector to measure against.
 
         Returns
         -------
-        Q : Quaternion
-            The multiplied Quaternion.
+        pa : float
+            The position angle between the two vectors.
         """
-        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
+        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)
 
-    def cnvrt(self, V):
-        """Rotates a vector from the starting frame to the ending frame
-        defined by the Q.
+        if (pa < 0):
+            pa += (2 * pi)  # PA has range 0-360 degrees
 
-        Parameters
-        ----------
-        V: Vector
-            The vector to rotate.
+        return(pa)
 
-        Returns
-        -------
-        Vector
-            The rotated Vector.
-        """
-        QV = Qmake_a_point(V)
-        QV = self * QV * self.conjugate()
-        return Vector(QV.q1, QV.q2, QV.q3)
+    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.
 
-    def inv_cnvrt(self, V):
-        """Rotates a vector from the ending frame to the starting frame
-        defined by the Q.
+        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
         ----------
-        V: Vector
-            The vector to invert.
+        angle: float
+            The angle of rotation.
+        axis: str
+            The axis to rotate about, ['x', 'y', 'z'].
 
         Returns
         -------
-        Vector
-            The inverted Vector.
+        result : Vector
+            The rotated vector.
         """
-        QV = Qmake_a_point(V)
-        QV = self.conjugate() * QV * self
-        return Vector(QV.q1, QV.q2, QV.q3)
+        if (axis == 'x'):
+            rot_matrix = Matrix([[1, 0, 0], [0, cos(angle), -sin(angle)],
+                                 [0, sin(angle), cos(angle)]])
 
-    def set_values(self, V, angle):
-        """Sets quaterion values using a direction vector and a rotation of
-        the coordinate frame about it.
+        elif (axis == 'y'):
+            rot_matrix = Matrix([[cos(angle), 0, sin(angle)], [0, 1, 0],
+                                 [-sin(angle), 0, cos(angle)]])
 
-        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.)
+        elif (axis == 'z'):
+            rot_matrix = Matrix([[cos(angle), -sin(angle), 0],
+                                 [sin(angle), cos(angle), 0], [0, 0, 1]])
 
-    def set_as_QX(self, angle):
-        """Sets quaterion in place like QX function.
+        else:
+            print('Error')
+            return
 
-        Parameters
-        ----------
-        angle: float
-            The angle of rotation
-        """
-        self.q1 = sin(-angle / 2.)
-        self.q2 = 0.
-        self.q3 = 0.
-        self.q4 = cos(angle / 2.)
+        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 set_as_QY(self, angle):
-        """Sets quaterion in place like QY function
+    def rotate_about_eigenaxis(self, angle, eigenaxis):
+        """Rotates a vector about arbitrary eigenaxis.
 
-        Parameters
-        ----------
-        angle: float
-            The angle of rotation
-        """
-        self.q1 = 0.
-        self.q2 = sin(-angle / 2.)
-        self.q3 = 0.
-        self.q4 = cos(angle / 2.)
+        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 set_as_QZ(self, angle):
-        """Sets quaterion in place like QZ function.
+        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
         ----------
         angle: float
             The angle of rotation.
+        eigenaxis: Vector
+            The eigenaxis to rotate about.
+
+        Returns
+        -------
+        result : Vector
+            The rotated vector.
         """
-        self.q1 = 0.
-        self.q2 = 0.
-        self.q3 = sin(-angle / 2.)
-        self.q4 = cos(angle / 2.)
+        cos_ang = cos(angle)    # Used repeatedly below
+        sin_ang = sin(angle)
 
-    def set_as_mult(self, QQ1, QQ2):
-        """Sets self as QQ1*QQ2 in place for quaternion multiplication.
+        # 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
         ----------
-        QQ1: Quaternion
-            The first quaternion.
-        QQ2: Quaternion
-            The second quaternion.
+        pa: float
+            The position angle.
+
+        Returns
+        -------
+        result : Vector
+            The rotated 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
+        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 set_as_point(self, V):
-        """Set V as a point.
+    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 set as a point.
+        angle: float
+            The angle of rotation.
+        eigenaxis: Vector
+            The eigenaxis to rotate about.
+
+        Returns
+        -------
+        Vector
+            The rotated vector.
         """
-        self.q1 = V.x
-        self.q2 = V.y
-        self.q3 = V.z
-        self.q4 = 0.
+        q = Quaternion(eigenaxis, 0.0)
 
-    def set_equal(self, Q):
-        """Assigns values from other Q to this one.
+        # 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 set_eq(self, ra, dec, degrees=False):
+        """Modifies a celestial vector with a new RA and DEC.
+
+        degrees = True if units are degrees.  Default is radians.
 
         Parameters
         ----------
-        Q: Quaternion
-            The quaternion value to set.
+        ra: float
+            The right ascension.
+        dec: float
+            The declination.
+        degrees: bool
+            Use degrees.
         """
-        self.q1 = Q.q1
-        self.q2 = Q.q2
-        self.q3 = Q.q3
-        self.q4 = Q.q4
-
-    def set_as_conjugate(self):
-        """Assigns conjugate values in place. """
-        self.q1 *= -1.
-        self.q2 *= -1.
-        self.q3 *= -1.
+        if (degrees):
+            ra = math2.D2R * ra
+            dec = math2.D2R * dec
 
+            self.ra = ra
+            self.dec = dec
 
-class NumericList(list):
-    """List class that supports multiplication.  Only valid for numbers."""
+        # 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 __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
+    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
         ----------
-        L1: sequence
-            The first list.
-        L2: sequence
-            The second list.
+        new_frame: str
+            Convert to new frame.
 
         Returns
         -------
-        float
-            The sum of the lists.
+        result : Vector
+            The transformed vector.
         """
-        return(sum(map(lambda x, y: x * y, L1, L2)))
+        result = None
+        gal_ec = new_frame == 'gal' and self.frame == 'ec'
+        ec_gal = new_frame == 'ec' and self.frame == 'gal'
+
+        # 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')
 
-class Matrix(list):
-    """Class to encapsulate matrix data and methods.
+        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)
 
-    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."""
+            lng = atan2(arg1, arg2) + NGP.anode
 
-    def __init__(self, rows):
-        """Constructor for a matrix.
+            result = CelestialVector(lng, b, degrees=False)
 
-        This accepts a list of rows.
-        It is assumed the rows are all of the same length.
+        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
+
+            result = CelestialVector(ra, dec, degrees=False)
+
+        elif gal_ec or ec_gal:
+            print("""Error: Direct conversion between ecliptic and\
+                     galactic coordinates not supported yet""")
+
+        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)
+
+        else:
+            result.frame = new_frame  # record new frame
+
+        return (result)
+
+    def update_cartesian(self, x=None, y=None, z=None):
+        """Modifies a celestial vector by specifying new Cartesian
+        coordinates.
+
+        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])
+        super(Attitude, self).__init__(ra=ra, dec=dec, frame=frame,
+                                       degrees=degrees)
 
-    def row(self, row_index):
-        """Returns a specified row of the matrix.
+        if (degrees):   # convert into radians
+            pa = math2.D2R * pa
+
+        self.pa = pa
+
+    def __str__(self, verbose=True):
+        """Returns a string representation of the attitude.
+
+        verbose (optional) = flag indicating whether detailed Vector
+        information should be included.
 
         Parameters
         ----------
-        row_index: int
-            The row index.
+        verbose: bool
+            Print information.
 
         Returns
         -------
-        list
-            The row values.
+        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
 
-        return(self[row_index])
 
-    def column(self, col_index):
-        """Returns a specified column of the matrix as a numeric list.
+class GalacticPole:
+    """Represents coordinates of galactic pole."""
+
+    def __init__(self, latitude, longitude, ascending_node):
+        """Initializes the coordinates of the galactic pole.
 
         Parameters
         ----------
-        col_index: int
-            The column index.
-
-        Returns
-        -------
-        list
-            The column values.
+        latitude: float
+            Latitude of pole, in degrees.
+        longitude: float
+            Longitude of pole, in degrees.
+        ascending_node: float
+            Ascending node of pole, in degrees.
         """
-        return(NumericList([row[col_index] for row in self]))
+        # 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
 
-    def num_rows(self):
-        """Returns the number of rows in the matrix."""
 
-        return(len(self))
+# supports transformation to galactic coordinates
+NGP = GalacticPole(192.859508, 27.128336, 32.932)
 
-    def num_cols(self):
-        """Returns the number of columns in the matrix."""
 
-        return (len(self[0]))  # assumes all rows of equal length
+class Matrix(list):
+    """Class to encapsulate matrix data and methods.
 
-    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])
+    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.
+
+        This accepts a list of rows.
+        It is assumed the rows are all of the same length.
+
+        Parameters
+        ----------
+        rows: sequence
+            The rows of the matrix.
+        """
+        for row in 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.
 
@@ -741,833 +879,778 @@ def __mul__(m1, m2):
 
         return (Matrix(result_rows))
 
-
-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.
-
-        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
-
     def __str__(self):
-        """Returns a string representation of the vector."""
-        return('Vector: x: %.3f, y: %.3f, z: %.3f' % (self.x, self.y, self.z))
-
-    def set_eq(self, x=None, y=None, z=None):
-        """Assigns new value to vector.
+        """Returns a string representation of the matrix."""
+        return_str = 'Matrix:'
 
-        Arguments are now optional to permit this to be used with 2D vectors
-        or to modify any subset of coordinates.
+        for row_index in range(len(self)):
+            row_str = 'Row %d: ' % (row_index + 1)
+            row = self[row_index]
 
-        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
+            for col_index in range(len(row)):
+                row_str = row_str + '%6.3f  ' % (row[col_index])
 
-    def length(self):
-        """Returns magnitude of the vector """
-        return(sqrt(self.x * self.x + self.y * self.y + self.z * self.z))
+            return_str = return_str + '\n' + row_str
 
-    def normalize(self):
-        """Returns copy of the normalized vector """
-        mag = self.length()
-        return (Vector(self.x / mag, self.y / mag, self.z / mag))
+        return(return_str)
 
-    def __mul__(self, rs):
-        """Implements Vector * scalar.  Can then use '*' syntax in multiplying
-        a vector by a scalar rs
+    def column(self, col_index):
+        """Returns a specified column of the matrix as a numeric list.
 
         Parameters
         ----------
-        rs: float
-            The scalar to multiply.
+        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 __rmul__(self, ls):
-        """Implements float * Vector.
+    def element(self, row_index, col_index):
+        """Returns an element of the matrix indexed by row and column.
+
+        Indices begin with 0.
 
         Parameters
         ----------
-        ls: 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.
         """
-        x = self.x * ls
-        y = self.y * ls
-        z = self.z * ls
-        return (Vector(x, y, z))
-
-    def __add__(self, rs):
-        """Implements Vector + Vector
+        return ((self[row_index])[col_index])
 
-        Parameters
-        ----------
-        rs: float
-            The scalar to add.
+    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])
 
-        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 num_cols(self):
+        """Returns the number of columns in the matrix."""
 
-    def __sub__(self, rs):
-        """Implements Vector - Vector.
+        return (len(self[0]))  # assumes all rows of equal length
 
-        Parameters
-        ----------
-        rs: float
-            The scalar to subtract.
+    def num_rows(self):
+        """Returns the number of rows in the 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))
+        return(len(self))
 
-    def __truediv__(self, rs):
-        """Implements Vector / float.
+    def row(self, row_index):
+        """Returns a specified row of the matrix.
 
         Parameters
         ----------
-        rs: float
-            The scalar to divide.
+        row_index: int
+            The row index.
 
         Returns
         -------
-        Vector
-            The resultant vector.
+        list
+            The row values.
         """
-        x = self.x / rs
-        y = self.y / rs
-        z = self.z / rs
-        return (Vector(x, y, z))
 
-    def __imul__(self, rs):
-        """Implements Vector *= float
+        return(self[row_index])
 
-        Parameters
-        ----------
-        rs: float
-            The scalar to multiply.
 
-        Returns
-        -------
-        Vector
-            The resultant vector.
-        """
-        self.x *= rs
-        self.y *= rs
-        self.z *= rs
-        return (self)
+class NumericList(list):
+    """List class that supports multiplication.  Only valid for
+    numbers."""
 
-    def __iadd__(self, rs):
-        """Implements Vector += vector.
+    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 add.
+        L1: sequence
+            The first list.
+        L2: sequence
+            The second list.
 
         Returns
         -------
-        Vector
-            The resultant vector.
+        float
+            The sum of the lists.
         """
-        self.x += rs.x
-        self.y += rs.y
-        self.z += rs.z
-        return (self)
-
-    def __isub__(self, rs):
-        """Implements Vector -= vector.
+        return(sum(map(lambda x, y: x * y, L1, L2)))
 
-        Parameters
-        ----------
-        rs: float
-            The scalar to subtract.
 
-        Returns
-        -------
-        Vector
-            The resultant vector.
-        """
-        self.x -= rs.x
-        self.y -= rs.y
-        self.z -= rs.z
-        return (self)
+class Quaternion:
+    """This representation is used by Wertz and Markley"""
 
-    def __idiv__(self, rs):
-        """Implements Vector /= float.
+    def __init__(self, V, q4):
+        """Quaternion constructor.
 
         Parameters
         ----------
-        rs: float
-            The scalar to divide.
-
-        Returns
-        -------
-        Vector
-            The resultant vector.
-        """
-        self.x /= rs
-        self.y /= rs
-        self.z /= rs
-        return (self)
-
-    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
+        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
 
-    # Recommend deletion -- better to use a single interface that takes
-    # two vectors.
-    def dot(self, V2):
-        """returns dot product between two vectors.
+    def __mul__(self, rs):
+        """Defines Q*Q for quaternion multiplication.
 
         Parameters
         ----------
-        V2: Vector
-            The vector to dot.
+        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.
-
-        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 = 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
 
-        A CelestialVector is also an ordinary unit vector, with Cartesian
-        coordinates defined relative to the equatorial plane.
+    def set_as_point(self, V):
+        """Set V as a point.
 
         Parameters
         ----------
-        ra: float
-            The right ascension.
-        dec: float
-            The declination.
-        frame: str
-            The frame to use.
-        degrees: bool
-            Use degrees.
+        V: Vector
+            The vector to set as a point.
         """
-        if (degrees):
-            ra = math2.D2R * ra
-            dec = math2.D2R * dec
-
-        self.ra = ra
-        self.dec = dec
-        self.frame = frame
-
-        # 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)
+        self.q1 = V.x
+        self.q2 = V.y
+        self.q3 = V.z
+        self.q4 = 0.
 
-    def __str__(self, verbose=True):
-        """Returns a string representation of the vector.  Displays angles
-        in degrees.
+    def set_as_QX(self, angle):
+        """Sets quaterion in place like QX 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 = sin(-angle / 2.)
+        self.q2 = 0.
+        self.q3 = 0.
+        self.q4 = cos(angle / 2.)
 
-        if (verbose):
-            clss = super(CelestialVector, self).__str__()
-            celest_info = celest_info + '\n' + clss
-            return celest_info
+    def set_as_QY(self, angle):
+        """Sets quaterion in place like QY function
 
-    def set_eq(self, ra, dec, degrees=False):
-        """Modifies a celestial vector with a new RA and DEC.
+        Parameters
+        ----------
+        angle: float
+            The angle of rotation
+        """
+        self.q1 = 0.
+        self.q2 = sin(-angle / 2.)
+        self.q3 = 0.
+        self.q4 = cos(angle / 2.)
 
-        degrees = True if units are degrees.  Default is radians.
+    def set_as_QZ(self, angle):
+        """Sets quaterion in place like QZ function.
 
         Parameters
         ----------
-        ra: float
-            The right ascension.
-        dec: float
-            The declination.
-        degrees: bool
-            Use degrees.
+        angle: float
+            The angle of rotation.
         """
-        if (degrees):
-            ra = math2.D2R * ra
-            dec = math2.D2R * dec
-
-            self.ra = ra
-            self.dec = dec
+        self.q1 = 0.
+        self.q2 = 0.
+        self.q3 = sin(-angle / 2.)
+        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_equal(self, Q):
+        """Assigns values from other Q to this one.
 
-    def update_cartesian(self, x=None, y=None, z=None):
-        """Modifies a celestial vector by specifying new Cartesian coordinates.
+        Parameters
+        ----------
+        Q: Quaternion
+            The quaternion value to set.
+        """
+        self.q1 = Q.q1
+        self.q2 = Q.q2
+        self.q3 = Q.q3
+        self.q4 = Q.q4
 
-        Any subset of the Cartesian coordinates may be specifed.
+    def set_values(self, V, angle):
+        """Sets quaterion values using a direction vector and a rotation
+        of the coordinate frame about it.
 
         Parameters
         ----------
-        x: float
-            The extent in x.
-        y: float
-            The extent in y.
-        z: float
-            The extent in z.
+        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.)
 
-        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
+# RLH: Recommend replacement by separation.
+def angle(V1, V2):
+    """returns angle between two vectors in degrees, 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 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)
 
-        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 cross(v1, v2):
+    """Returns cross product between two vectors, non class member.
 
-        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
+    ----------
+    v1: Vector
+        The first vector.
+    v2: Vector
+        The second vector.
 
-        elif (axis == 'y'):
-            rot_matrix = Matrix([[cos(angle), 0, sin(angle)], [0, 1, 0],
-                                 [-sin(angle), 0, cos(angle)]])
+    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)
 
-        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_body2inertial_Q_to_c1c2pa_tuple(Q):
+    """Creates a angle tuple from Q, assuming body frame to inertial Q
+    and 321 rotation sequence.
 
-        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.
 
-    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.
+    """
+    # 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
 
-        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_c1c2_using_body2inertial_Q_to_v2v3pa_tuple(Q, coord1, coord2):
+    """Given Q and a position, returns v2, v3, V3PA tuple
 
-        Parameters
-        ----------
-        angle: float
-            The angle of rotation.
-        eigenaxis: Vector
-            The eigenaxis to rotate about.
+    Parameters
+    ----------
+    Q: Quaternion
+        The quaternion.
+    coord1: float
+        The first coordinate.
+    coord2: float
+        The second coordinate
 
-        Returns
-        -------
-        result : Vector
-            The rotated vector.
-        """
-        cos_ang = cos(angle)    # Used repeatedly below
-        sin_ang = sin(angle)
+    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
 
-        # 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_pt_Q_to_V(Q):
+    """Converts a pure (pointing) Q to a unit position Vector
 
-    def rotate_using_quaternion(self, angle, eigenaxis):
-        """Rotates a vector about arbitrary eigenaxis using quaternion.
+    Parameters
+    ----------
+    Q: Quaternion
+        The quaternion to convert to a Vector.
+
+    Returns
+    -------
+    Vector
+        The point as a vector.
+    """
+    return Vector(Q.q1, Q.q2, Q.q3)
+
+
+def cvt_v2v3_using_body2inertial_Q_to_c1c2pa_tuple(Q, v2, v3):
+    """Given Q and v2, v3 gives pos on sky and V3 PA.
+
+    Parameters
+    ----------
+    Q: Quaternion
+        The quaternion.
+    v2: float
+        The V2 position.
+    v3: float
+        The V3 position.
+
+    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))
+
+    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
 
-        This is an alternative formulation for rotate_about_eigenaxis.
-        Interface is the same as rotate_about_eigenaxis.
+    return coord1, coord2, pa
 
-        Parameters
-        ----------
-        angle: float
-            The angle of rotation.
-        eigenaxis: Vector
-            The eigenaxis to rotate about.
 
-        Returns
-        -------
-        Vector
-            The rotated vector.
-        """
-        q = Quaternion(eigenaxis, 0.0)
+def dec_separation(v1, v2):
+    """Returns difference in declination between two CelestialVectors.
 
-        # 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
+    ----------
+    v1: Vector
+        The first vector.
+    v2: Vector
+        The second vector.
 
-    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.
+    Returns
+    -------
+    float
+        The separation between Dec values
+    """
+    return(v1.dec - v2.dec)    # simply take the difference in declination
 
-        Parameters
-        ----------
-        new_frame: str
-            Convert to new frame.
 
-        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'
+# Functions that operate on vectors but are not methods.
+def dot(v1, v2):
+    """returns dot product between two vectors, non class member.
 
-        # 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')
+    Parameters
+    ----------
+    v1: Vector
+        The first vector.
+    v2: Vector
+        The second vector.
 
-        # 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')
+    Returns
+    -------
+    float
+        The dot product of the vectors.
+    """
+    return(v1.x * v2.x + v1.y * v2.y + v1.z * v2.z)
 
-        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)
 
-            lng = atan2(arg1, arg2) + NGP.anode
+def make_celestial_vector(v):
+    """Takes a Vector object and creates an equivalent CelestialVector.
 
-            result = CelestialVector(lng, b, degrees=False)
+    Input vector v must be a unit vector.
 
-        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
+    Parameters
+    ----------
+    v: Vector
+        The vector to convert.
 
-            result = CelestialVector(ra, dec, degrees=False)
+    Returns
+    -------
+    result : Vector
+        The updated vector.
+    """
+    result = CelestialVector()
+    result.update_cartesian(v.x, v.y, v.z)
+    return(result)
 
-        elif gal_ec or ec_gal:
-            print("""Error: Direct conversion between ecliptic and\
-                     galactic coordinates not supported yet""")
 
-        elif new_frame != self.frame:
-            print("Error: unrecognized coordinate frame.")
+def pos_V_to_ra_dec(V):
+    """Returns tuple of spherical angles from unit direction Vector.
 
-        # 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)
+    Parameters
+    ----------
+    V: Vector
+        The vector to analyze.
 
-        else:
-            result.frame = new_frame  # record new frame
+    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)
 
-        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.
+def projection(v, axis):
+    """Returns projection of vector v on plane normal to axis.
 
-        Parameters
-        ----------
-        pa: float
-            The position angle.
+    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.
 
-        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)
+    Parameters
+    ----------
+    v: Vector
+        The vector to convert.
+    axis: str
+        The axis to project onto.
 
-    def position_angle(self, v):
-        """Returns the position angle of v at the self vector, in radians.
+    Returns
+    -------
+    Vector
+        The updated vector.
+    """
+    return(make_celestial_vector(cross(axis, (cross(v, axis)).normalize())))
 
-        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.
+def Qmake_a_point(V):
+    """Creates a pure Q, i.e. defines a pointing not a rotation
 
-        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)
+    Parameters
+    ----------
+    V: Vector
+        The vector.
 
-        if (pa < 0):
-            pa += (2 * pi)  # PA has range 0-360 degrees
+    Returns
+    -------
+    Quaternion
+        The point as a quaternion.
+    """
+    return Quaternion(V, 0.)
 
-        return(pa)
 
+def QX(angle):
+    """Creates rotation quaternion about X axis, rotates a vector about
+    this axis.
 
-class Attitude(CelestialVector):
-    "Defines an Observatory attitude by adding a position angle."""
+    Parameters
+    ----------
+    angle: float
+        The angle to rotate by.
 
-    def __init__(self, ra=0.0, dec=0.0, pa=0.0, frame='eq', degrees=True):
-        """Constructor for an Attitude.
+    Result
+    ------
+    Quarternion
+        The rotated quaternion.
+    """
+    return Quaternion(Vector(sin(angle / 2.), 0., 0.), cos(angle / 2.))
 
-        pa = position_angle in degrees(default) or radians if degrees=False
-        is specified. Other arguments are the same as with CelestialVector
 
-        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)
+def QY(angle):
+    """Creates rotation quaternion about Y axis, rotates a vector about
+    this axis.
 
-        if (degrees):   # convert into radians
-            pa = math2.D2R * pa
+    Parameters
+    ----------
+    angle: float
+        The angle to rotate by.
 
-        self.pa = pa
+    Result
+    ------
+    Quarternion
+        The rotated quaternion.
+    """
+    return Quaternion(Vector(0., sin(angle / 2.), 0.), cos(angle / 2.))
 
-    def __str__(self, verbose=True):
-        """Returns a string representation of the attitude.
 
-        verbose (optional) = flag indicating whether detailed Vector
-        information should be included.
+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):
-    """Returns difference in right ascension between two CelestialVectors.
+    """Returns difference in right ascension between two
+    CelestialVectors.
 
     Parameters
     ----------
@@ -1595,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
 
@@ -1617,8 +1701,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,110 +1714,33 @@ 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):
-    """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 7c093589..c488e3b8 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..7dbb48ff 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
 
@@ -35,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
@@ -231,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
     -------
@@ -342,8 +346,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):
diff --git a/exoctk/exoctk_app/app_exoctk.py b/exoctk/exoctk_app/app_exoctk.py
index c4f8ac80..9566d864 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,308 @@
     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('<table', '<table id="myTable" class="table table-striped table-hover"')
+    return response
 
-            # Add the table title
-            header = '<br></br><strong>{}</strong><br><p>\(I(\mu)/I(\mu=1)\) = {}</p>'.format(profile, poly)
-            html_table = header + html_table
 
-            profile_tables.append(html_table)
+@app_exoctk.route('/fortney', methods=['GET', 'POST'])
+def fortney():
+    """
+    Pull up Forntey Grid plot the results and download
 
-            # Add the profile to the form inputs
-            form_input[profile] = 'true'
+    Returns
+    -------
+    html : ``flask.render_template`` obj
+        The rendered template for the Fortney results.
+    """
 
-        # Log the successful form inputs
-        log_exoctk.log_form_input(form_input, 'limb_darkening', DB)
+    # Grab the inputs arguments from the URL
+    args = flask.request.args
 
-        return render_template('limb_darkening_results.html', form=form,
-                               table=profile_tables, script=script, plot=div,
-                               file_as_string=repr(file_as_string),
-                               filt_plot=filt_plot, filt_script=filt_script,
-                               js=js_resources, css=css_resources)
+    input_args = _param_fort_validation(args)
+    fig, fh, temp_out = fortney_grid(input_args)
 
-    return render_template('limb_darkening.html', form=form)
+    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('/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 +513,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('<table', '<table id="myTable" class="table table-striped table-hover"')
+            html_table = '\n'.join(table.pformat(max_width=-1, max_lines=-1, html=True)).replace('<table', '<table id="myTable" class="table table-striped table-hover"')
 
-        except Exception:
-            html_table = '<p>No data to display</p>'
+            # Add the table title
+            header = '<br></br><strong>{}</strong><br><p>\(I(\mu)/I(\mu=1)\) = {}</p>'.format(profile, poly)
+            html_table = header + html_table
 
-        # Add the table title
-        header = '<h3>{}</h3>'.format(table)
-        html_table = header + html_table
+            profile_tables.append(html_table)
 
-        log_tables.append(html_table)
+            # Add the profile to the form inputs
+            form_input[profile] = 'true'
 
-    return render_template('admin_page.html', tables=log_tables)
+        # Log the successful form inputs
+        log_exoctk.log_form_input(form_input, 'limb_darkening', DB)
 
+        return render_template('limb_darkening_results.html', form=form,
+                               table=profile_tables, script=script, plot=div,
+                               file_as_string=repr(file_as_string),
+                               filt_plot=filt_plot, filt_script=filt_script,
+                               js=js_resources, css=css_resources)
 
-@app_exoctk.route('/lightcurve_fitting')
-def lightcurve_fitting():
-    """A landing page for the lightcurve_fitting tool"""
+    return render_template('limb_darkening.html', form=form)
 
-    return render_template('lightcurve_fitting.html')
 
+@app_exoctk.route('/limb_darkening_error', methods=['GET', 'POST'])
+def limb_darkening_error():
+    """The limb darkening error page
 
-@app_exoctk.route('/atmospheric_retrievals')
-def atmospheric_retrievals():
-    """A landing page for the atmospheric_retrievals tools"""
+    Returns
+    -------
+    ``flask.render_template`` obj
+        The rendered template for the limb-darkening error page.
+    """
 
-    return render_template('atmospheric_retrievals.html')
+    return render_template('limb_darkening_error.html')
 
 
 @app_exoctk.route('/phase_constraint', methods=['GET', 'POST'])
@@ -851,6 +720,7 @@ def phase_constraint(transit_type='primary'):
     ``flask.render_template`` obj
         The rendered template for the phase-constraint calculator.
     """
+
     # Load default form
     form = fv.PhaseConstraint()
 
@@ -880,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)
 
@@ -918,19 +788,131 @@ def phase_constraint(transit_type='primary'):
             minphase, maxphase = np.nan, np.nan
         form.minimum_phase.data = minphase
         form.maximum_phase.data = maxphase
-    """
-    if (form.eccentricity.data > 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('<table', '<table id="myTable" class="table table-striped table-hover"')
+
+        except Exception:
+            html_table = '<p>No data to display</p>'
+
+        # Add the table title
+        header = '<h3>{}</h3>'.format(table)
+        html_table = header + html_table
+
+        log_tables.append(html_table)
+
+    return render_template('admin_page.html', tables=log_tables)
+
+
 if __name__ == '__main__':
-    # os.chmod('/internal/data1/app_data/.astropy/cache/', 777)
+
     port = int(os.environ.get('PORT', 5000))
     app_exoctk.run(host='0.0.0.0', port=port)
diff --git a/exoctk/exoctk_app/form_validation.py b/exoctk/exoctk_app/form_validation.py
index 0b3f5868..1b56fb3d 100644
--- a/exoctk/exoctk_app/form_validation.py
+++ b/exoctk/exoctk_app/form_validation.py
@@ -1,20 +1,14 @@
-import numpy as np
 import os
 
 from flask_wtf import FlaskForm
+import numpy as np
+from svo_filters import svo
 from wtforms import StringField, SubmitField, DecimalField, RadioField, SelectField, SelectMultipleField, IntegerField, FloatField
-from wtforms.validators import InputRequired, Length, NumberRange, AnyOf, ValidationError
+from wtforms.validators import InputRequired, NumberRange
 from wtforms.widgets import ListWidget, CheckboxInput
 
 from exoctk.modelgrid import ModelGrid
 from exoctk.utils import get_env_variables, FILTERS_LIST, PROFILES
-from svo_filters import svo
-
-
-class MultiCheckboxField(SelectMultipleField):
-    """Makes a list of checkbox inputs"""
-    widget = ListWidget(prefix_label=False)
-    option_widget = CheckboxInput()
 
 
 class BaseForm(FlaskForm):
@@ -27,6 +21,28 @@ 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
+    calculate_submit = SubmitField('Calculate Visibility')
+    calculate_contam_submit = SubmitField('Calculate Visibility and Contamination')
+    mode_submit = SubmitField('Mode Selected')
+
+    # Form inputs
+    ra = DecimalField('ra', validators=[NumberRange(min=0, max=360, message='RA must be between 0 and 360 degrees')])
+    dec = DecimalField('dec', validators=[NumberRange(min=-90, max=90, message='Declinaton must be between -90 and 90 degrees')])
+    inst = SelectField('inst', choices=[('NIRISS', 'NIRISS - SOSS'), ('NIRCam F322W2', 'NIRCam - Grism Time Series (F322W2)'), ('NIRCam F444W', 'NIRCam - Grism Time Series (F444W)'), ('MIRI', 'MIRI - LRS'), ('NIRSpec', 'NIRSpec (Visibility Only)')])
+    companion = StringField('companion', default='')
+    pa_min = DecimalField('pa_min', default=0, validators=[NumberRange(min=0, max=360, message='Minimum PA must be between 0 and 360 degrees')])
+    pa_max = DecimalField('pa_max', default=360, validators=[NumberRange(min=0, max=360, message='Maximum PA must be between 0 and 360 degrees')])
+
+
 class FortneyModelForm(BaseForm):
     """Form validation for the forward model tools"""
     # Parameters
@@ -44,40 +60,6 @@ class FortneyModelForm(BaseForm):
     calculate_submit = SubmitField('Calculate Forward Model')
 
 
-class LimbDarkeningForm(BaseForm):
-    """Form validation for the limb_darkening tool"""
-    # Model grid
-    modelgrid_dir = get_env_variables()['modelgrid_dir']
-    default_modelgrid = os.path.join(modelgrid_dir, 'ATLAS9/')
-    mg = ModelGrid(default_modelgrid, 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()
-    modeldir = RadioField('modeldir', default=default_modelgrid, choices=[(os.path.join(modelgrid_dir, 'ATLAS9/'), 'Kurucz ATLAS9'), (os.path.join(modelgrid_dir, 'ACES/'), 'Phoenix ACES')], validators=[InputRequired('A model grid is required!')])
-
-    # Stellar parameters
-    teff = DecimalField('teff', default=3500, validators=[InputRequired('An effective temperature is required!'), NumberRange(min=float(teff_rng[0]), max=float(teff_rng[1]), message='Effective temperature must be between {} and {} for this model grid'.format(*teff_rng))])
-    logg = DecimalField('logg', default=4.5, validators=[InputRequired('A surface gravity is required!'), NumberRange(min=float(logg_rng[0]), max=float(logg_rng[1]), message='Surface gravity must be between {} and {} for this model grid'.format(*logg_rng))])
-    feh = DecimalField('feh', default=0.0, validators=[InputRequired('A surface gravity is required!'), NumberRange(min=float(feh_rng[0]), max=float(feh_rng[1]), message='Metallicity must be between {} and {} for this model grid'.format(*feh_rng))])
-    mu_min = DecimalField('mu_min', default=0.1, validators=[InputRequired('A minimum mu value is required!'), NumberRange(min=0.0, max=1.0, message='Minimum mu must be between 0 and 1')])
-
-    # LD profile
-    profiles = MultiCheckboxField('profiles', choices=[(x, x) for x in PROFILES], validators=[InputRequired('At least one profile is required!')])
-
-    # Bandpass
-    default_filter = 'Kepler.K'
-    defilt = svo.Filter(default_filter)
-    bandpass = SelectField('bandpass', default=default_filter, choices=[('tophat', 'Top Hat')] + [(filt, filt) for filt in FILTERS_LIST], validators=[InputRequired('A filter is required!')])
-    wave_min = DecimalField('wave_min', default=defilt.wave_min.value, validators=[NumberRange(min=0, max=30, message='Minimum wavelength must be between 0 and 30 microns!')])
-    wave_max = DecimalField('wave_max', default=defilt.wave_max.value, validators=[NumberRange(min=0, max=30, message='Maximum wavelength must be between 0 and 30 microns!')])
-    n_bins = IntegerField('n_bins', default=1)
-
-    # Form submits
-    calculate_submit = SubmitField('Calculate Coefficients')
-    filter_submit = SubmitField('Filter Selected')
-    modelgrid_submit = SubmitField('Model Grid Selected')
-
-
 class GroupsIntsForm(BaseForm):
     """Form validation for the groups_integrations tool"""
     # Form submits
@@ -121,20 +103,38 @@ class GroupsIntsForm(BaseForm):
     sat_max = DecimalField('sat_max', default=0.95, validators=[InputRequired('A saturation level is required!'), NumberRange(min=0.0, message='Saturation level must be positive.')])
 
 
-class ContamVisForm(BaseForm):
-    """Form validation for the contamination_visibility tool"""
-    # Form submits
-    calculate_submit = SubmitField('Calculate Visibility')
-    calculate_contam_submit = SubmitField('Calculate Visibility and Contamination')
-    mode_submit = SubmitField('Mode Selected')
+class LimbDarkeningForm(BaseForm):
+    """Form validation for the limb_darkening tool"""
+    # Model grid
+    modelgrid_dir = get_env_variables()['modelgrid_dir']
+    default_modelgrid = os.path.join(modelgrid_dir, 'ATLAS9/')
+    mg = ModelGrid(default_modelgrid, 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()
+    modeldir = RadioField('modeldir', default=default_modelgrid, choices=[(os.path.join(modelgrid_dir, 'ATLAS9/'), 'Kurucz ATLAS9'), (os.path.join(modelgrid_dir, 'ACES/'), 'Phoenix ACES')], validators=[InputRequired('A model grid is required!')])
 
-    # Form inputs
-    ra = DecimalField('ra', validators=[NumberRange(min=0, max=360, message='RA must be between 0 and 360 degrees')])
-    dec = DecimalField('dec', validators=[NumberRange(min=-90, max=90, message='Declinaton must be between -90 and 90 degrees')])
-    inst = SelectField('inst', choices=[('NIRISS', 'NIRISS - SOSS'), ('NIRCam F322W2', 'NIRCam - Grism Time Series (F322W2)'), ('NIRCam F444W', 'NIRCam - Grism Time Series (F444W)'), ('MIRI', 'MIRI - LRS'), ('NIRSpec', 'NIRSpec (Visibility Only)')])
-    companion = StringField('companion', default='')
-    pa_min = DecimalField('pa_min', default=0, validators=[NumberRange(min=0, max=360, message='Minimum PA must be between 0 and 360 degrees')])
-    pa_max = DecimalField('pa_max', default=360, validators=[NumberRange(min=0, max=360, message='Maximum PA must be between 0 and 360 degrees')])
+    # Stellar parameters
+    teff = DecimalField('teff', default=3500, validators=[InputRequired('An effective temperature is required!'), NumberRange(min=float(teff_rng[0]), max=float(teff_rng[1]), message='Effective temperature must be between {} and {} for this model grid'.format(*teff_rng))])
+    logg = DecimalField('logg', default=4.5, validators=[InputRequired('A surface gravity is required!'), NumberRange(min=float(logg_rng[0]), max=float(logg_rng[1]), message='Surface gravity must be between {} and {} for this model grid'.format(*logg_rng))])
+    feh = DecimalField('feh', default=0.0, validators=[InputRequired('A surface gravity is required!'), NumberRange(min=float(feh_rng[0]), max=float(feh_rng[1]), message='Metallicity must be between {} and {} for this model grid'.format(*feh_rng))])
+    mu_min = DecimalField('mu_min', default=0.1, validators=[InputRequired('A minimum mu value is required!'), NumberRange(min=0.0, max=1.0, message='Minimum mu must be between 0 and 1')])
+
+    # LD profile
+    profiles = MultiCheckboxField('profiles', choices=[(x, x) for x in PROFILES], validators=[InputRequired('At least one profile is required!')])
+
+    # Bandpass
+    default_filter = 'Kepler.K'
+    defilt = svo.Filter(default_filter)
+    bandpass = SelectField('bandpass', default=default_filter, choices=[('tophat', 'Top Hat')] + [(filt, filt) for filt in FILTERS_LIST], validators=[InputRequired('A filter is required!')])
+    wave_min = DecimalField('wave_min', default=defilt.wave_min.value, validators=[NumberRange(min=0, max=30, message='Minimum wavelength must be between 0 and 30 microns!')])
+    wave_max = DecimalField('wave_max', default=defilt.wave_max.value, validators=[NumberRange(min=0, max=30, message='Maximum wavelength must be between 0 and 30 microns!')])
+    n_bins = IntegerField('n_bins', default=1)
+
+    # Form submits
+    calculate_submit = SubmitField('Calculate Coefficients')
+    filter_submit = SubmitField('Filter Selected')
+    modelgrid_submit = SubmitField('Model Grid Selected')
 
 
 class PhaseConstraint(BaseForm):
@@ -142,7 +142,7 @@ class PhaseConstraint(BaseForm):
 
     calculate_submit = SubmitField('Calculate Phase Constraint')
 
-    orbital_period = FloatField('orbital_period', validators=[InputRequired('Orbital period is a required field')]) 
+    orbital_period = FloatField('orbital_period', validators=[InputRequired('Orbital period is a required field')])
     eccentricity = FloatField('eccentricity', default=np.nan)
     transit_type = SelectField('transit_type', choices=[('primary', 'primary'), ('secondary', 'secondary')])
     omega = FloatField('omega', default=np.nan)
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 @@
                         <div style="float:right; width:120px;text-align:center;">
                             <p>
                                 Running<br>
-                                <a href='https://github.com/ExoCTK/exoctk/releases/tag/v1.2.1'>exoctk v1.2.1</a>{{version|safe}}
+                                <a href='https://github.com/ExoCTK/exoctk/releases/tag/v1.2.2'>exoctk v1.2.2</a>{{version|safe}}
                             </p>
                             <p style='font-size:0.8em'>
                                 <a href='admin' style='color:#aaaaaa;'>Admin Area</a>
diff --git a/exoctk/forward_models/forward_models.py b/exoctk/forward_models/forward_models.py
index 260835a7..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, check_for_data
+from exoctk import utils
 
-## -- FUNCTIONS
 
 def fortney_grid(args, write_plot=False, write_table=False):
     """
@@ -69,13 +64,12 @@ def fortney_grid(args, write_plot=False, write_table=False):
     temp_out : list of str of int
         The list of temperatures in the model grid.
     """
-    check_for_data('fortney')
+    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
@@ -206,10 +198,10 @@ def generic_grid(input_args, write_plot=False, write_table=False):
     error_message : str
         An error message, or lack therof.
     """
-    check_for_data('generic')
+    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
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
     ----------
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
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
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
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
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 <target_name> [--t0=<t0>] [--period=<p>] [--pre_duration=<pre_duration>] [--transit_duration=<trans_dur>] [--window_size=<win_size>] [--secondary] [--eccentricity=<ecc>] [--omega=<omega>] [--inclination=<inc>] [--winn_approx] [--get_secondary_time]
-  
+
 Arguments:
   <target_name>                     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['<target_name>'], 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['<target_name>'], 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)
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 36b3cfb8..5fc515fa 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
         ----------
@@ -80,9 +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/')
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)
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(
         ".",


Test patch:
diff --git a/exoctk/tests/test_lightcurve_fitting.py b/exoctk/tests/test_lightcurve_fitting.py
index 57421b6f..09aa70a3 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):
@@ -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)
@@ -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)) == "<class 'bokeh.plotting.figure.Figure'>"
-
-    # Tabbed plot
-    fig = ld_session.plot_tabs()
-    assert str(type(fig)) in ["<class 'bokeh.models.widgets.panels.Tabs'>", "<class 'bokeh.models.layouts.Tabs'>"]
-
-
-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)) == "<class 'bokeh.plotting.figure.Figure'>"
+
+    # Tabbed plot
+    fig = ld_session.plot_tabs()
+    assert str(type(fig)) in ["<class 'bokeh.models.widgets.panels.Tabs'>", "<class 'bokeh.models.layouts.Tabs'>"]
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