{"action":"created","comment":{"url":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/comments/101081909","pull_request_review_id":21800870,"id":101081909,"diff_hunk":"@@ -0,0 +1,642 @@\n+import math\n+import numpy as np\n+from scipy.interpolate import interp1d\n+from astropy import units as u\n+from astropy import constants as c\n+from astropy.table import Table\n+from astropy.convolution import discretize_model\n+from astropy.modeling.functional_models import Moffat2D\n+from matplotlib import pyplot as plt\n+import matplotlib as mpl\n+mpl.rc('text', usetex=False)\n+mpl.rcParams['figure.figsize'] = 8, 6\n+import os\n+\n+\n+def ensure_unit(arg, unit):\n+    if not isinstance(arg, u.Quantity):\n+        arg = arg * unit\n+    return arg.to(unit)\n+\n+\n+class Optic:\n+\n+    def __init__(self, aperture, focal_length, throughput_filename, central_obstruction=0 * u.mm):\n+        \"\"\"\n+        Class representing imager optics (e.g. Canon lens, RASA telescope),\n+        incorporates basic attributes such as aperture diameter, focal length\n+        and central obstruction.\n+        \"\"\"\n+        self.aperture = ensure_unit(aperture, u.mm)\n+        self.central_obstruction = ensure_unit(central_obstruction, u.mm)\n+\n+        self.aperture_area = np.pi * (self.aperture**2 - self.central_obstruction**2).to(u.m**2) / 4\n+\n+        self.focal_length = ensure_unit(focal_length, u.mm)\n+\n+        tau_data = Table.read(os.path.join(os.getenv('POCS'), throughput_filename))\n+\n+        if not tau_data['Wavelength'].unit:\n+            tau_data['Wavelength'].unit = u.nm\n+        self.wavelengths = tau_data['Wavelength'].quantity.to(u.nm)\n+\n+        if not tau_data['Throughput'].unit:\n+            tau_data['Throughput'].unit = u.dimensionless_unscaled\n+        self.throughput = tau_data['Throughput'].quantity.to(u.dimensionless_unscaled)\n+\n+\n+class Camera:\n+\n+    def __init__(self, bit_depth, full_well, gain, readout_time, pixel_size, resolution, read_noise, dark_current,\n+                 QE_filename, minimum_exposure):\n+        \"\"\"\n+        Class representing a camera, incorporated basic properties such\n+        as bit depth, full well, gain, readout time, pixel size, resolution, read noise and dark current.\n+        \"\"\"\n+        self.bit_depth = int(bit_depth)\n+        self.full_well = ensure_unit(full_well, u.electron)\n+        self.gain = ensure_unit(gain, u.electron / u.adu)\n+        self.readout_time = ensure_unit(readout_time, u.second)\n+        self.pixel_size = ensure_unit(pixel_size, u.micron / u.pixel)\n+        self.resolution = ensure_unit(resolution, u.pixel)\n+        self.read_noise = ensure_unit(read_noise, u.electron / u.pixel)\n+        self.dark_current = ensure_unit(dark_current, u.electron / (u.second * u.pixel))\n+        self.minimum_exposure = ensure_unit(minimum_exposure, u.second)\n+\n+        QE_data = Table.read(os.path.join(os.getenv('POCS'), QE_filename))\n+\n+        if not QE_data['Wavelength'].unit:\n+            QE_data['Wavelength'].unit = u.nm\n+        self.wavelengths = QE_data['Wavelength'].quantity.to(u.nm)\n+\n+        if not QE_data['QE'].unit:\n+            QE_data['QE'].unit = u.electron / u.photon\n+        self.QE = QE_data['QE'].quantity.to(u.electron / u.photon)\n+\n+\n+class Filter:\n+\n+    def __init__(self, transmission_filename, sky_mu):\n+        \"\"\"\n+        Class representing a bandpass filter, which incorporates sky surface brightness for\n+        the filter band.\n+        \"\"\"\n+        transmission_data = Table.read(os.path.join(os.getenv('POCS'), transmission_filename))\n+\n+        if not transmission_data['Wavelength'].unit:\n+            transmission_data['Wavelength'].unit = u.nm\n+        self.wavelengths = transmission_data['Wavelength'].quantity.to(u.nm)\n+\n+        if not transmission_data['Transmission'].unit:\n+            transmission_data['Transmission'].unit = u.dimensionless_unscaled\n+        self.transmission = transmission_data['Transmission'].quantity.to(u.dimensionless_unscaled)\n+\n+        self.sky_mu = ensure_unit(sky_mu, u.ABmag)\n+\n+\n+class Imager:\n+\n+    def __init__(self, optic, camera, band, num_imagers=1, num_computer=5, PSF=None):\n+        if not isinstance(optic, Optic):\n+            raise ValueError(\"optic must be an instance of the Optic class\")\n+        if not isinstance(camera, Camera):\n+            raise ValueError(\"camera must be an instance of the Camera class\")\n+        if not isinstance(band, Filter):\n+            raise ValueError(\"band must be an instance of the Filter class\")\n+        if PSF and not isinstance(PSF, Moffat_PSF):\n+            raise ValueError(\"PSF must be an instance of the Moffat PSF class\")\n+\n+        self.optic = optic\n+        self.camera = camera\n+        self.band = band\n+        self.PSF = PSF\n+        self.num_imagers = int(num_imagers)\n+        self.num_computer = int(num_computer)\n+\n+        # Calculate pixel scale, area\n+        self.pixel_scale = (self.camera.pixel_size / self.optic.focal_length)\n+        self.pixel_scale = self.pixel_scale.to(u.arcsecond / u.pixel,\n+                                               equivalencies=u.equivalencies.dimensionless_angles())\n+        self.pixel_area = self.pixel_scale**2 * u.pixel\n+        self.PSF.pixel_scale = self.pixel_scale\n+\n+        # Calculate field of view.\n+        self.field_of_view = (self.camera.resolution * self.pixel_scale)\n+        self.field_of_view = self.field_of_view.to(u.degree, equivalencies=u.dimensionless_angles())\n+\n+        # Calculate n_pix value\n+        self.n_pix = self.PSF.n_pix()\n+\n+        # Calculate peak value\n+        self.peak = self.PSF.peak()\n+\n+        # Calculate end to end efficiencies, etc.\n+        self._efficiencies()\n+\n+        # Calculate sky count rate for later use\n+        self.sky_rate = self.SB_to_rate(self.band.sky_mu)\n+\n+    def SB_signal_noise(self, signal_SB, total_exp_time, sub_exp_time=300 * u.second, binning=1, N=1, saturation_SB=None):\n+\n+        # Signal count rates\n+        signal_rate = self.SB_to_rate(signal_SB)  # e/pixel/second\n+\n+        # Number of sub-exposures\n+        total_exp_time = ensure_unit(total_exp_time, u.second)\n+        sub_exp_time = ensure_unit(sub_exp_time, u.second)\n+\n+        number_subs = int(math.ceil(total_exp_time / sub_exp_time))\n+\n+        if (total_exp_time != number_subs * sub_exp_time):\n+            total_exp_time = number_subs * sub_exp_time\n+            print('Rounding up total exposure time to next integer multiple of sub-exposure time:', total_exp_time)\n+\n+        # Noise sources\n+        signal = (signal_rate * total_exp_time).to(u.electron / u.pixel)\n+        sky_counts = (self.sky_rate * total_exp_time).to(u.electron / u.pixel)\n+        dark_counts = (self.camera.dark_current * total_exp_time).to(u.electron / u.pixel)\n+        total_read_noise = ((number_subs)**0.5 * self.camera.read_noise).to(u.electron / u.pixel)\n+\n+        noise = (signal.value + sky_counts.value + dark_counts.value + total_read_noise.value**2)**0.5\n+        noise = noise * u.electron / u.pixel\n+\n+        signal = np.where(signal_SB > saturation_SB, signal, 0.0 * u.electron / u.pixel) * N * binning\n+        noise = np.where(signal_SB > saturation_SB, noise, 0.0 * u.electron / u.pixel) * (N * binning)**0.5\n+        return signal, noise\n+\n+    def SB_snr(self, signal_SB, total_exp_time, sub_exp_time=300 * u.second, binning=1, N=1, saturation_SB=None):\n+        signal = self.SB_signal_noise(signal_SB, total_exp_time, sub_exp_time, binning, N, saturation_SB)[0]\n+        noise = self.SB_signal_noise(signal_SB, total_exp_time, sub_exp_time, binning, N, saturation_SB)[1]\n+        snr = np.where(noise != 0.0, signal / noise, 0.0)\n+        return snr  # returns the signal-to-noise ratio\n+\n+    def SB_etc(self, signal_SB, snr_target, sub_exp_time=300 * u.second, binning=1, N=1):\n+\n+        # Science count rates\n+        signal_rate = self.SB_to_rate(signal_SB)\n+\n+        # Convert target SNR per array combined, binned pixel to SNR per unbinned pixel\n+        snr_target = snr_target / (N * binning)**0.5\n+        snr_target = ensure_unit(snr_target, u.dimensionless_unscaled)\n+\n+        sub_exp_time = ensure_unit(sub_exp_time, u.second)\n+\n+        # If required total exposure time is much greater than the length of a sub-exposure then\n+        # all noise sources (including read noise) are proportional to t^0.5 and we can use a\n+        # simplified expression to estimate total exposure time.\n+        noise_squared_rate = (signal_rate.value + self.sky_rate.value + self.camera.dark_current.value +\n+                              self.camera.read_noise.value**2 / sub_exp_time.value)\n+        noise_squared_rate = noise_squared_rate * u.electron**2 / (u.pixel**2 * u.second)\n+        total_exp_time = (snr_target**2 * noise_squared_rate / signal_rate**2).to(u.second)\n+\n+        # The simplified expression underestimates read noise due to fractional number of sub-exposure,\n+        # the effect will be neglible unless the total exposure time is very short but we can fix it anyway...\n+        # First round up to the next integer number of sub-exposures:\n+        number_subs = int(math.ceil(total_exp_time / sub_exp_time))\n+        # If the SNR has dropped below the target value as a result of the extra read noise add another sub\n+        # Note: calling snr() here is horribly inefficient as it recalculates a bunch of stuff but I don't care.\n+        while self.SB_snr(signal_SB, number_subs * sub_exp_time, sub_exp_time, binning, N) < snr_target:\n+            print(\"Adding a sub-exposure to overcome read noise!\")\n+            number_subs = number_subs + 1\n+\n+        return number_subs * sub_exp_time, number_subs\n+\n+    def SB_limit(self, total_exp_time, snr_target, snr_type='per pixel', sub_exp_time=600 * u.second, binning=1, N=1,\n+                 enable_read_noise=True, enable_sky_noise=True, enable_dark_noise=True):\n+        snr_target = snr_target / (N * binning)**0.5\n+        # Convert target SNR per array combined, binned pixel to SNR per unbinned pixel\n+        if snr_type == 'per pixel':\n+            pass\n+        elif snr_type == 'per arcseconds squared':\n+            snr_target = snr_target * self.pixel_scale / (u.arcsecond / u.pixel)\n+        else:\n+            raise ValueError('invalid snr target type {}'.format(snr_calculation))\n+\n+        snr_target = ensure_unit(snr_target, u.dimensionless_unscaled)\n+\n+        # Number of sub-exposures\n+        total_exp_time = ensure_unit(total_exp_time, u.second)\n+        sub_exp_time = ensure_unit(sub_exp_time, u.second)\n+\n+        number_subs = np.ceil(total_exp_time / sub_exp_time).astype(int)\n+\n+        total_exp_time = number_subs * sub_exp_time\n+\n+        # Noise sources\n+        sky_counts = self.sky_rate * total_exp_time if enable_sky_noise else 0.0 * u.electron / u.pixel\n+        dark_counts = self.camera.dark_current * total_exp_time if enable_dark_noise else 0.0 * u.electron / u.pixel\n+        total_read_noise = np.sqrt(number_subs) * \\\n+            self.camera.read_noise if enable_read_noise else 0.0 * u.electron / u.pixel\n+\n+        noise_squared = (sky_counts.value + dark_counts.value + total_read_noise.value**2) * u.electron**2 / u.pixel**2\n+\n+        # Calculate science count rate for target signal to noise ratio\n+        a = (total_exp_time**2).value\n+        b = -((snr_target)**2 * total_exp_time).value\n+        c = -((snr_target)**2 * noise_squared).value\n+\n+        signal_rate = (-b + np.sqrt(b**2 - 4 * a * c)) / (2 * a) * u.electron / (u.pixel * u.second)\n+\n+        return self.rate_to_SB(signal_rate)\n+\n+    def ABmag_to_rate(self, mag):\n+        mag = ensure_unit(mag, u.ABmag)\n+\n+        f_nu = mag.to(u.W / (u.m**2 * u.Hz), equivalencies=u.equivalencies.spectral_density(self.pivot_wave))\n+        rate = f_nu * self.optic.aperture_area * self._iminus1 * u.photon / c.h\n+\n+        return rate.to(u.electron / u.second)\n+\n+    def rate_to_ABmag(self, rate):\n+        ensure_unit(rate, u.electron / u.second)\n+\n+        f_nu = rate * c.h / (self.optic.aperture_area * self._iminus1 * u.photon)\n+        return f_nu.to(u.ABmag, equivalencies=u.equivalencies.spectral_density(self.pivot_wave))\n+\n+    def SB_to_rate(self, mag):\n+        SB_rate = self.ABmag_to_rate(mag) * self.pixel_area / (u.arcsecond**2)\n+        return SB_rate.to(u.electron / (u.second * u.pixel))\n+\n+    def rate_to_SB(self, SB_rate):\n+        ensure_unit(SB_rate, u.electron / (u.second * u.pixel))\n+\n+        rate = SB_rate * u.arcsecond**2 / self.pixel_area\n+        return self.rate_to_ABmag(rate)\n+\n+    def ABmag_to_flux(self, mag):\n+        mag = ensure_unit(mag, u.ABmag)\n+\n+        f_nu = mag.to(u.W / (u.m**2 * u.Hz), equivalencies=u.equivalencies.spectral_density(self.pivot_wave))\n+        flux = f_nu * c.c * self._iminus2 * u.photon / u.electron\n+\n+        return flux.to(u.W / (u.m**2))\n+\n+    def maximum_time(self, total_elapsed_time, sub_exp_time):\n+        '''Now we create a function that takes in a sub exposure time and total elapsed time and gives out total\n+        exposure time (maximum time). '''\n+        total_elapsed_time = ensure_unit(total_elapsed_time, u.second)\n+        sub_exp_time = ensure_unit(sub_exp_time, u.second)\n+        num_of_subs = total_elapsed_time / (sub_exp_time + self.camera.readout_time *\n+                                            min(self.num_imagers, self.num_computer))\n+        total_exposure_time = num_of_subs * sub_exp_time\n+        return total_exposure_time\n+\n+    def pointsource_signal_noise(self, signal_mag, total_exp_time, sub_exp_time=300 * u.second, N=1):\n+        # ensuring that the units of point source signal magnitude is AB mag\n+        signal_mag = ensure_unit(signal_mag, u.ABmag)\n+        # calculating the saturation magnitude limit\n+        saturation_mag = self.pointsource_saturation(total_exp_time)\n+        # converting the signal magnitude to signal rate in electron/pixel/second\n+        signal_rate = self.ABmag_to_rate(signal_mag) / (self.n_pix * u.pixel)\n+        # converting the saturation magnitude to saturation rate in electron/pixel/second\n+        saturation_rate = self.ABmag_to_rate(saturation_mag) / (self.n_pix * u.pixel)\n+        saturation_SB = self.rate_to_SB(saturation_rate)  # converting the saturation rate to surface brightness\n+        signal_SB = self.rate_to_SB(signal_rate)  # converting the signal rate to surface brightness\n+        binning = self.n_pix  # scaling the binning by the number of pixels covered by the point source\n+        return self.SB_signal_noise(signal_SB, total_exp_time, sub_exp_time, binning, N, saturation_SB)\n+\n+    def pointsource_snr(self, signal_mag, total_exp_time, sub_exp_time=300 * u.second, N=1):\n+        # ensuring that the units of point source signal magnitude is AB mag\n+        signal_mag = ensure_unit(signal_mag, u.ABmag)\n+        # calculating the saturation magnitude limit\n+        saturation_mag = self.pointsource_saturation(total_exp_time)\n+        # converting the signal magnitude to signal rate in electron/pixel/second\n+        signal_rate = self.ABmag_to_rate(signal_mag) / (self.n_pix * u.pixel)\n+        # converting the saturation magnitude to saturation rate in electron/pixel/second\n+        saturation_rate = self.ABmag_to_rate(saturation_mag) / (self.n_pix * u.pixel)\n+        saturation_SB = self.rate_to_SB(saturation_rate)  # converting the saturation rate to surface brightness\n+        signal_SB = self.rate_to_SB(signal_rate)  # converting the signal rate to surface brightness\n+        binning = self.n_pix  # scaling the binning by the number of pixels covered by the point source\n+        return self.SB_snr(signal_SB, total_exp_time, sub_exp_time, binning, N, saturation_SB)\n+\n+    def pointsource_etc(self, signal_mag, snr_target, sub_exp_time=300 * u.second, N=1):\n+        signal_mag = ensure_unit(signal_mag, u.ABmag)\n+        signal_rate = self.ABmag_to_rate(signal_mag) / (self.n_pix * u.pixel)\n+        signal_SB = self.rate_to_SB(signal_rate)\n+        binning = self.n_pix\n+        return self.SB_etc(signal_SB.value, snr_target, sub_exp_time, binning, N)\n+\n+    def pointsource_limit(self, total_time, snr_target, sub_exp_time=600 * u.second,\n+                          N=1, enable_read_noise=True, enable_sky_noise=True, enable_dark_noise=True):\n+        snr_type = 'per pixel'\n+        binning = self.n_pix\n+        signal_SB = self.SB_limit(total_time, snr_target, snr_type, sub_exp_time, binning, N, enable_read_noise,\n+                                  enable_sky_noise, enable_dark_noise)  # Calculating the surface brightness limit\n+        # Calculating the signal rate associated with the limit\n+        signal_rate = self.SB_to_rate(signal_SB) * (self.n_pix * u.pixel)\n+        return self.rate_to_ABmag(signal_rate)  # Converting the signal rate to point source brightness limit\n+\n+    # Calculating the saturation magnitude limit for point objects\n+    def pointsource_saturation(self, exp_time):\n+        # calculation of the maximum signal limit (in electrons) before saturation\n+        digital_limit = ((2 ** self.camera.bit_depth - 1) - 1500) * u.adu * self.camera.gain\n+        exp_time = ensure_unit(exp_time, u.second)\n+        max_rate = min(self.camera.full_well, digital_limit) / (exp_time *\n+                                                                u.pixel)  # Max total count rate, electrons/s/pixel\n+        max_flux_fraction = self.peak / u.pixel  # Max proportion of point source flux per pixel\n+        max_signal_rate = (max_rate - self.sky_rate - self.camera.dark_current) / \\\n+            max_flux_fraction  # Max count rate from point source, electron/s\n+\n+        return self.rate_to_ABmag(max_signal_rate)  # Converting the rate into saturation level in ABmag\n+\n+    def exp_at_saturation(self, minimum_mag):\n+        ''' This function takes in a minimum magnitude value for a point source and returns the minimum sub exposure time for\n+        which a point source of this magnitude does not saturate '''\n+        digital_limit = ((2 ** self.camera.bit_depth - 1) - 1500) * u.adu * self.camera.gain\n+        minimum_mag = ensure_unit(minimum_mag, u.ABmag)\n+        max_signal_rate = self.ABmag_to_rate(minimum_mag)\n+        max_flux_fraction = self.peak / u.pixel\n+        max_rate = (max_signal_rate * max_flux_fraction) + self.sky_rate + self.camera.dark_current\n+        sub_exp_time = (min(self.camera.full_well, digital_limit)) / (max_rate * u.pixel)\n+        sub_exp_time = ensure_unit(sub_exp_time, u.second)\n+        return sub_exp_time\n+\n+    def _efficiencies(self):\n+        # Fine wavelength grid spanning range of filter transmission profile\n+        waves = np.arange(self.band.wavelengths.value.min(), self.band.wavelengths.value.max(), 1) * u.nm\n+\n+        # Interpolate throughput, filter transmission and QE to new grid\n+        tau = interp1d(self.optic.wavelengths, self.optic.throughput, kind='linear', fill_value='extrapolate')\n+        ft = interp1d(self.band.wavelengths, self.band.transmission, kind='linear', fill_value='extrapolate')\n+        qe = interp1d(self.camera.wavelengths, self.camera.QE, kind='linear', fill_value='extrapolate')\n+\n+        # End-to-end efficiency. Need to put units back after interpolation\n+        effs = tau(waves) * ft(waves) * qe(waves) * u.electron / u.photon\n+\n+        # Band averaged efficiency, effective wavelengths, bandwidth (STSci definition), flux_integral\n+        i0 = np.trapz(effs, x=waves)\n+        i1 = np.trapz(effs * waves, x=waves)\n+        self._iminus1 = np.trapz(effs / waves, x=waves)  # This one is useful later\n+        self._iminus2 = np.trapz(effs / waves**2, x=waves)\n+\n+        self.wavelengths = waves\n+        self.efficiencies = effs\n+        self.efficiency = i0 / (waves[-1] - waves[0])\n+        self.mean_wave = i1 / i0\n+        self.pivot_wave = (i1 / self._iminus1)**0.5\n+        self.bandwidth = i0 / effs.max()\n+\n+    def _gamma0(self):\n+        \"\"\"\n+        Calculates 'gamma0', the number of photons/second/pixel at the top of atmosphere\n+        that corresponds to 0 AB mag/arcsec^2 for a given band, aperture & pixel scale.\n+        \"\"\"\n+        # Spectral flux density corresponding to 0 ABmag, pseudo-SI units\n+        sfd_0 = (0 * u.ABmag).to(u.W / (u.m**2 * u.um),\n+                                 equivalencies=u.equivalencies.spectral_density(self.pivot_wave))\n+        # Change to surface brightness (0 ABmag/arcsec^2)\n+        sfd_sb_0 = sfd_0 / u.arcsecond**2\n+        # Average photon energy\n+        energy = c.h * c.c / (self.pivot_wave * u.photon)\n+        # Divide by photon energy & multiply by aperture area, pixel area and bandwidth to get photons/s/pixel\n+        photon_flux = (sfd_sb_0 / energy) * self.optic.aperture_area * self.pixel_area * self.bandwidth\n+\n+        self.gamma0 = photon_flux.to(u.photon / (u.s * u.pixel))\n+\n+\n+class Moffat_PSF(Moffat2D):\n+\n+    def __init__(self, FWHM, alpha=2.5, pixel_scale=None):\n+        \"\"\"\n+        Class representing a 2D Moffat profile point spread function.\n+\n+        Used to calculate pixelated version of the PSF and associated parameters useful for\n+        point source signal to noise and saturation limit calculations.\n+\n+        Args:\n+            FWHM (u.arcseconds): Full Width at Half-Maximum of the PSF\n+            alpha (optional): shape parameter, must be > 1, default 2.5\n+\n+        Smaller values of the alpha parameter correspond to 'wingier' profiles.\n+        A value of 4.765 would give the best fit to pure Kolmogorov atmospheric turbulence.\n+        When instrumental effects are added a lower value is appropriate.\n+        IRAF uses a default of 2.5.\n+        \"\"\"\n+        if alpha <= 1.0:\n+            raise ValueError('alpha must be greater than 1!')\n+        super(Moffat_PSF, self).__init__(alpha=alpha)\n+\n+        self.FWHM = FWHM\n+\n+        if pixel_scale:\n+            self.pixel_scale = pixel_scale\n+\n+    @property\n+    def FWHM(self):\n+        return self._FWHM\n+\n+    @FWHM.setter\n+    def FWHM(self, FWHM):\n+        self._FWHM = ensure_unit(FWHM, u.arcsecond)\n+        # If a pixel scale has been set should update model parameters when FWHM changes.\n+        if self.pixel_scale:\n+            self._update_model()\n+\n+    @property\n+    def pixel_scale(self):\n+        try:\n+            return self._pixel_scale\n+        except AttributeError:\n+            return None\n+\n+    @pixel_scale.setter\n+    def pixel_scale(self, pixel_scale):\n+        self._pixel_scale = ensure_unit(pixel_scale, (u.arcsecond / u.pixel))\n+        # When pixel scale is set/changed need to update model parameters:\n+        self._update_model()\n+\n+    def pixellated(self, pixel_scale=None, n_pix=21, offsets=(0.0, 0.0)):\n+        \"\"\"\n+        Calculates a pixellated version of the PSF for a given pixel scale\n+        \"\"\"\n+        if pixel_scale:\n+            self.pixel_scale = pixel_scale\n+        else:\n+            # Should make sure _update_model() gets called anyway, in case\n+            # alpha has been changed.\n+            self._update_model()\n+\n+        # Update PSF centre coordinates\n+        self.x_0 = offsets[0]\n+        self.y_0 = offsets[1]\n+\n+        xrange = (-(n_pix - 1) / 2, (n_pix + 1) / 2)\n+        yrange = (-(n_pix - 1) / 2, (n_pix + 1) / 2)\n+\n+        return discretize_model(self, xrange, yrange, mode='oversample', factor=10)\n+\n+    def peak(self, pixel_scale=None):\n+        \"\"\"\n+        Calculate the peak pixel value (as a fraction of total counts) for a PSF centred\n+        on a pixel. This is useful for calculating saturation limits for point sources.\n+        \"\"\"\n+        # Odd number of pixels (1) so offsets = (0, 0) is centred on a pixel\n+        centred_psf = self.pixellated(pixel_scale, 1, offsets=(0, 0))\n+        return centred_psf[0, 0]\n+\n+    def n_pix(self, pixel_scale=None, n_pix=20):\n+        \"\"\"\n+        Calculate the effective number of pixels for PSF fitting photometry with this\n+        PSF, in the worse case where the PSF is centred on the corner of a pixel.\n+        \"\"\"\n+        # Want a even number of pixels.\n+        n_pix = n_pix + n_pix % 2\n+        # Even number of pixels so offsets = (0, 0) is centred on pixel corners\n+        corner_psf = self.pixellated(pixel_scale, n_pix, offsets=(0, 0))\n+        return 1 / ((corner_psf**2).sum())\n+\n+    def _update_model(self):\n+        # Convert FWHM from arcsecond to pixel units\n+        self._FWHM_pix = self.FWHM / self.pixel_scale\n+        # Convert to FWHM to Moffat profile width parameter in pixel units\n+        gamma = self._FWHM_pix / (2 * np.sqrt(2**(1 / self.alpha) - 1))\n+        # Calculate amplitude required for normalised PSF\n+        amplitude = (self.alpha - 1) / (np.pi * gamma**2)\n+        # Update model parameters\n+        self.gamma = gamma.to(u.pixel).value\n+        self.amplitude = amplitude.to(u.pixel**-2).value\n+\n+\n+class ImagerArray:\n+    ''' ImagerArray class takes in a set of imagers, and calculates a set of sub exposure times that each of the imager spans\n+    through'''\n+\n+    def __init__(self, imagers):\n+        for imager in imagers.values():\n+            if not isinstance(imager, Imager):\n+                raise ValueError(\"{} not an instance of the  Imager class\".format(imager))\n+        self.imagers = imagers\n+        self.num_imagers = 0  # initializing\n+        for imager in imagers.values():\n+            self.num_imagers += imager.num_imagers\n+\n+        self.minimum_exposure = max([imager.camera.minimum_exposure for imager in self.imagers.values()])\n+\n+    def HDR_mode(self, minimum_magnitude, num_longexp=1, factor=2, maximum_exptime=300 * u.second, name='canon_sbig_g', generate_plots=False, maximum_magnitude=None):\n+        exptime_array = self.exposure_time_array(minimum_magnitude, num_longexp, factor, maximum_exptime, name,\n+                                                 maximum_magnitude)\n+        saturation_limits_array = self.saturation_limits(exptime_array, minimum_magnitude, num_longexp, factor,\n+                                                         maximum_exptime, name, maximum_magnitude)\n+        total_time_calc = self.total_time_calculation(exptime_array, minimum_magnitude, num_longexp, factor, maximum_exptime,\n+                                                      name, maximum_magnitude)\n+        snr_plot_data = self.snr_plot(exptime_array, minimum_magnitude, num_longexp, factor, maximum_exptime, name,\n+                                      generate_plots, maximum_magnitude)\n+        return exptime_array, saturation_limits_array, total_time_calc, snr_plot_data\n+\n+    def exposure_time_array(self, minimum_magnitude, num_longexp=1, factor=2, maximum_exptime=300 * u.second, name='canon_sbig_g', maximum_magnitude=None):\n+        explist = []  # setting a list up\n+        a = []  # initializing a list\n+        '''Calculating the number of exposure times such that minimum exposure time is a factor^integer multiple\n+        of the highest exposure time set to be 300 seconds'''\n+        minimum_exptime = min([imager.exp_at_saturation(minimum_magnitude) for imager in self.imagers.values()])\n+\n+        num_exp_array = math.ceil(math.log(maximum_exptime / minimum_exptime, factor))\n+\n+        # Calculating the corresponding shortest exposure time\n+        shortest_exposure = (maximum_exptime / (factor ** num_exp_array))\n+\n+        # Ensuring the shortest exposure time is not lower than the minimum exposure time of the camera(s)\n+        while shortest_exposure < self.minimum_exposure:\n+            shortest_exposure = shortest_exposure * factor\n+\n+        # Creating a list of exposure times from the lowest exposure time to the\n+        # one directly below the maximum exposure time\n+        for i in range(0, num_exp_array):\n+            explist.append(shortest_exposure * (factor ** i))\n+            explist[i] = int(np.round(explist[i].to(u.second).value / 0.01)) * \\\n+                (0.01 * u.second)  # rounding to 2 dec places\n+\n+        maxmag_longexp = - len(explist)\n+        if maximum_magnitude is not None:\n+            snr = 0  # initializing\n+            net_signal = 0\n+            net_noise = 0\n+            N = self.imagers[name].num_imagers\n+            for exp_time in explist:\n+                if snr < 5:\n+                    net_signal = net_signal + self.imagers[name].pointsource_signal_noise(maximum_magnitude, exp_time,\n+                                                                                          exp_time, N)[0]\n+                    net_noise_sqd = net_noise ** 2\n+                    net_noise = np.sqrt(net_noise_sqd + self.imagers[name].pointsource_signal_noise\n+                                        (maximum_magnitude, exp_time, exp_time, N)[1] ** 2)\n+                    snr = net_signal / net_noise\n+                    maxmag_longexp = maxmag_longexp + 1\n+                    if maxmag_longexp >= 0:\n+                        explist.append(maximum_exptime)\n+\n+            if maxmag_longexp >= 0:\n+                del explist[-1]\n+\n+        else:\n+            for i in range(0, num_longexp):\n+                explist.append(maximum_exptime)\n+\n+        return explist\n+\n+    def total_time_calculation(self, explist, minimum_magnitude, num_longexp=1, factor=2, maximum_exptime=300 * u.second,\n+                               name='canon_sbig_g', maximum_magnitude=None):\n+        '''Given a list of exposure times that each camera spans through, total_time_calculation calculates the\n+        total exposure time and total elapsed time (includes the readout time between successive sub exposures'''\n+        total_exp_time = sum(explist)\n+        total_elapsed_time = total_exp_time + len(explist) * min(self.imagers[name].num_computer,\n+                                                                 self.imagers[name].num_imagers\n+                                                                 ) * self.imagers[name].camera.readout_time\n+\n+        return total_exp_time, total_elapsed_time\n+\n+    def saturation_limits(self, explist, minimum_magnitude, num_longexp=1, factor=2, maximum_exptime=300 * u.second,\n+                          name='canon_sbig_g', maximum_magnitude=None):\n+        saturation = []\n+        for i in range(0, len(explist)):","path":"pocs/utils/signal_to_noise.py","position":590,"original_position":590,"commit_id":"7cb1ab25512ea51060dbea5156eab323f3269b1c","original_commit_id":"7cb1ab25512ea51060dbea5156eab323f3269b1c","user":{"login":"AnthonyHorton","id":5875088,"avatar_url":"https://avatars.githubusercontent.com/u/5875088?v=3","gravatar_id":"","url":"https://api.github.com/users/AnthonyHorton","html_url":"https://github.com/AnthonyHorton","followers_url":"https://api.github.com/users/AnthonyHorton/followers","following_url":"https://api.github.com/users/AnthonyHorton/following{/other_user}","gists_url":"https://api.github.com/users/AnthonyHorton/gists{/gist_id}","starred_url":"https://api.github.com/users/AnthonyHorton/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AnthonyHorton/subscriptions","organizations_url":"https://api.github.com/users/AnthonyHorton/orgs","repos_url":"https://api.github.com/users/AnthonyHorton/repos","events_url":"https://api.github.com/users/AnthonyHorton/events{/privacy}","received_events_url":"https://api.github.com/users/AnthonyHorton/received_events","type":"User","site_admin":false},"body":"If you make `explist` a `Quantity` as I suggested above then you shouldn't need the loop, calling `self.imagers[name].point_source_saturation(explist)` should return a `Quantity` containing the list of saturation limits. If it doesn't work then `point_source_saturation` needs fixing.","created_at":"2017-02-14T16:42:58Z","updated_at":"2017-02-14T16:42:58Z","html_url":"https://github.com/AstroHuntsman/POCS/pull/2#discussion_r101081909","pull_request_url":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/2","_links":{"self":{"href":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/comments/101081909"},"html":{"href":"https://github.com/AstroHuntsman/POCS/pull/2#discussion_r101081909"},"pull_request":{"href":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/2"}}},"pull_request":{"url":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/2","id":105815670,"html_url":"https://github.com/AstroHuntsman/POCS/pull/2","diff_url":"https://github.com/AstroHuntsman/POCS/pull/2.diff","patch_url":"https://github.com/AstroHuntsman/POCS/pull/2.patch","issue_url":"https://api.github.com/repos/AstroHuntsman/POCS/issues/2","number":2,"state":"open","locked":false,"title":"Hdr","user":{"login":"Ankitshrestha96","id":23667463,"avatar_url":"https://avatars.githubusercontent.com/u/23667463?v=3","gravatar_id":"","url":"https://api.github.com/users/Ankitshrestha96","html_url":"https://github.com/Ankitshrestha96","followers_url":"https://api.github.com/users/Ankitshrestha96/followers","following_url":"https://api.github.com/users/Ankitshrestha96/following{/other_user}","gists_url":"https://api.github.com/users/Ankitshrestha96/gists{/gist_id}","starred_url":"https://api.github.com/users/Ankitshrestha96/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Ankitshrestha96/subscriptions","organizations_url":"https://api.github.com/users/Ankitshrestha96/orgs","repos_url":"https://api.github.com/users/Ankitshrestha96/repos","events_url":"https://api.github.com/users/Ankitshrestha96/events{/privacy}","received_events_url":"https://api.github.com/users/Ankitshrestha96/received_events","type":"User","site_admin":false},"body":"Merging from develop to HDR for Anthony to review","created_at":"2017-02-13T01:05:59Z","updated_at":"2017-02-14T16:42:58Z","closed_at":null,"merged_at":null,"merge_commit_sha":"efe11df4eb4532edf9ff98c9b78a543ec80ebfd7","assignee":{"login":"Ankitshrestha96","id":23667463,"avatar_url":"https://avatars.githubusercontent.com/u/23667463?v=3","gravatar_id":"","url":"https://api.github.com/users/Ankitshrestha96","html_url":"https://github.com/Ankitshrestha96","followers_url":"https://api.github.com/users/Ankitshrestha96/followers","following_url":"https://api.github.com/users/Ankitshrestha96/following{/other_user}","gists_url":"https://api.github.com/users/Ankitshrestha96/gists{/gist_id}","starred_url":"https://api.github.com/users/Ankitshrestha96/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Ankitshrestha96/subscriptions","organizations_url":"https://api.github.com/users/Ankitshrestha96/orgs","repos_url":"https://api.github.com/users/Ankitshrestha96/repos","events_url":"https://api.github.com/users/Ankitshrestha96/events{/privacy}","received_events_url":"https://api.github.com/users/Ankitshrestha96/received_events","type":"User","site_admin":false},"assignees":[{"login":"Ankitshrestha96","id":23667463,"avatar_url":"https://avatars.githubusercontent.com/u/23667463?v=3","gravatar_id":"","url":"https://api.github.com/users/Ankitshrestha96","html_url":"https://github.com/Ankitshrestha96","followers_url":"https://api.github.com/users/Ankitshrestha96/followers","following_url":"https://api.github.com/users/Ankitshrestha96/following{/other_user}","gists_url":"https://api.github.com/users/Ankitshrestha96/gists{/gist_id}","starred_url":"https://api.github.com/users/Ankitshrestha96/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/Ankitshrestha96/subscriptions","organizations_url":"https://api.github.com/users/Ankitshrestha96/orgs","repos_url":"https://api.github.com/users/Ankitshrestha96/repos","events_url":"https://api.github.com/users/Ankitshrestha96/events{/privacy}","received_events_url":"https://api.github.com/users/Ankitshrestha96/received_events","type":"User","site_admin":false},{"login":"TaylahB","id":24306346,"avatar_url":"https://avatars.githubusercontent.com/u/24306346?v=3","gravatar_id":"","url":"https://api.github.com/users/TaylahB","html_url":"https://github.com/TaylahB","followers_url":"https://api.github.com/users/TaylahB/followers","following_url":"https://api.github.com/users/TaylahB/following{/other_user}","gists_url":"https://api.github.com/users/TaylahB/gists{/gist_id}","starred_url":"https://api.github.com/users/TaylahB/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/TaylahB/subscriptions","organizations_url":"https://api.github.com/users/TaylahB/orgs","repos_url":"https://api.github.com/users/TaylahB/repos","events_url":"https://api.github.com/users/TaylahB/events{/privacy}","received_events_url":"https://api.github.com/users/TaylahB/received_events","type":"User","site_admin":false}],"milestone":null,"commits_url":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/2/commits","review_comments_url":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/2/comments","review_comment_url":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/comments{/number}","comments_url":"https://api.github.com/repos/AstroHuntsman/POCS/issues/2/comments","statuses_url":"https://api.github.com/repos/AstroHuntsman/POCS/statuses/7cb1ab25512ea51060dbea5156eab323f3269b1c","head":{"label":"AstroHuntsman:HDR","ref":"HDR","sha":"7cb1ab25512ea51060dbea5156eab323f3269b1c","user":{"login":"AstroHuntsman","id":25214893,"avatar_url":"https://avatars.githubusercontent.com/u/25214893?v=3","gravatar_id":"","url":"https://api.github.com/users/AstroHuntsman","html_url":"https://github.com/AstroHuntsman","followers_url":"https://api.github.com/users/AstroHuntsman/followers","following_url":"https://api.github.com/users/AstroHuntsman/following{/other_user}","gists_url":"https://api.github.com/users/AstroHuntsman/gists{/gist_id}","starred_url":"https://api.github.com/users/AstroHuntsman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AstroHuntsman/subscriptions","organizations_url":"https://api.github.com/users/AstroHuntsman/orgs","repos_url":"https://api.github.com/users/AstroHuntsman/repos","events_url":"https://api.github.com/users/AstroHuntsman/events{/privacy}","received_events_url":"https://api.github.com/users/AstroHuntsman/received_events","type":"Organization","site_admin":false},"repo":{"id":69636835,"name":"POCS","full_name":"AstroHuntsman/POCS","owner":{"login":"AstroHuntsman","id":25214893,"avatar_url":"https://avatars.githubusercontent.com/u/25214893?v=3","gravatar_id":"","url":"https://api.github.com/users/AstroHuntsman","html_url":"https://github.com/AstroHuntsman","followers_url":"https://api.github.com/users/AstroHuntsman/followers","following_url":"https://api.github.com/users/AstroHuntsman/following{/other_user}","gists_url":"https://api.github.com/users/AstroHuntsman/gists{/gist_id}","starred_url":"https://api.github.com/users/AstroHuntsman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AstroHuntsman/subscriptions","organizations_url":"https://api.github.com/users/AstroHuntsman/orgs","repos_url":"https://api.github.com/users/AstroHuntsman/repos","events_url":"https://api.github.com/users/AstroHuntsman/events{/privacy}","received_events_url":"https://api.github.com/users/AstroHuntsman/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/AstroHuntsman/POCS","description":"PANOPTES Observatory Control System","fork":true,"url":"https://api.github.com/repos/AstroHuntsman/POCS","forks_url":"https://api.github.com/repos/AstroHuntsman/POCS/forks","keys_url":"https://api.github.com/repos/AstroHuntsman/POCS/keys{/key_id}","collaborators_url":"https://api.github.com/repos/AstroHuntsman/POCS/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/AstroHuntsman/POCS/teams","hooks_url":"https://api.github.com/repos/AstroHuntsman/POCS/hooks","issue_events_url":"https://api.github.com/repos/AstroHuntsman/POCS/issues/events{/number}","events_url":"https://api.github.com/repos/AstroHuntsman/POCS/events","assignees_url":"https://api.github.com/repos/AstroHuntsman/POCS/assignees{/user}","branches_url":"https://api.github.com/repos/AstroHuntsman/POCS/branches{/branch}","tags_url":"https://api.github.com/repos/AstroHuntsman/POCS/tags","blobs_url":"https://api.github.com/repos/AstroHuntsman/POCS/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/AstroHuntsman/POCS/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/AstroHuntsman/POCS/git/refs{/sha}","trees_url":"https://api.github.com/repos/AstroHuntsman/POCS/git/trees{/sha}","statuses_url":"https://api.github.com/repos/AstroHuntsman/POCS/statuses/{sha}","languages_url":"https://api.github.com/repos/AstroHuntsman/POCS/languages","stargazers_url":"https://api.github.com/repos/AstroHuntsman/POCS/stargazers","contributors_url":"https://api.github.com/repos/AstroHuntsman/POCS/contributors","subscribers_url":"https://api.github.com/repos/AstroHuntsman/POCS/subscribers","subscription_url":"https://api.github.com/repos/AstroHuntsman/POCS/subscription","commits_url":"https://api.github.com/repos/AstroHuntsman/POCS/commits{/sha}","git_commits_url":"https://api.github.com/repos/AstroHuntsman/POCS/git/commits{/sha}","comments_url":"https://api.github.com/repos/AstroHuntsman/POCS/comments{/number}","issue_comment_url":"https://api.github.com/repos/AstroHuntsman/POCS/issues/comments{/number}","contents_url":"https://api.github.com/repos/AstroHuntsman/POCS/contents/{+path}","compare_url":"https://api.github.com/repos/AstroHuntsman/POCS/compare/{base}...{head}","merges_url":"https://api.github.com/repos/AstroHuntsman/POCS/merges","archive_url":"https://api.github.com/repos/AstroHuntsman/POCS/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/AstroHuntsman/POCS/downloads","issues_url":"https://api.github.com/repos/AstroHuntsman/POCS/issues{/number}","pulls_url":"https://api.github.com/repos/AstroHuntsman/POCS/pulls{/number}","milestones_url":"https://api.github.com/repos/AstroHuntsman/POCS/milestones{/number}","notifications_url":"https://api.github.com/repos/AstroHuntsman/POCS/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/AstroHuntsman/POCS/labels{/name}","releases_url":"https://api.github.com/repos/AstroHuntsman/POCS/releases{/id}","deployments_url":"https://api.github.com/repos/AstroHuntsman/POCS/deployments","created_at":"2016-09-30T05:33:44Z","updated_at":"2017-02-14T05:11:23Z","pushed_at":"2017-02-14T10:35:39Z","git_url":"git://github.com/AstroHuntsman/POCS.git","ssh_url":"git@github.com:AstroHuntsman/POCS.git","clone_url":"https://github.com/AstroHuntsman/POCS.git","svn_url":"https://github.com/AstroHuntsman/POCS","homepage":"http://projectpanoptes.org","size":102016,"stargazers_count":0,"watchers_count":0,"language":"Jupyter Notebook","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"develop"}},"base":{"label":"AstroHuntsman:develop","ref":"develop","sha":"987fdcbcf16223580fc0938173534d283cf98321","user":{"login":"AstroHuntsman","id":25214893,"avatar_url":"https://avatars.githubusercontent.com/u/25214893?v=3","gravatar_id":"","url":"https://api.github.com/users/AstroHuntsman","html_url":"https://github.com/AstroHuntsman","followers_url":"https://api.github.com/users/AstroHuntsman/followers","following_url":"https://api.github.com/users/AstroHuntsman/following{/other_user}","gists_url":"https://api.github.com/users/AstroHuntsman/gists{/gist_id}","starred_url":"https://api.github.com/users/AstroHuntsman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AstroHuntsman/subscriptions","organizations_url":"https://api.github.com/users/AstroHuntsman/orgs","repos_url":"https://api.github.com/users/AstroHuntsman/repos","events_url":"https://api.github.com/users/AstroHuntsman/events{/privacy}","received_events_url":"https://api.github.com/users/AstroHuntsman/received_events","type":"Organization","site_admin":false},"repo":{"id":69636835,"name":"POCS","full_name":"AstroHuntsman/POCS","owner":{"login":"AstroHuntsman","id":25214893,"avatar_url":"https://avatars.githubusercontent.com/u/25214893?v=3","gravatar_id":"","url":"https://api.github.com/users/AstroHuntsman","html_url":"https://github.com/AstroHuntsman","followers_url":"https://api.github.com/users/AstroHuntsman/followers","following_url":"https://api.github.com/users/AstroHuntsman/following{/other_user}","gists_url":"https://api.github.com/users/AstroHuntsman/gists{/gist_id}","starred_url":"https://api.github.com/users/AstroHuntsman/starred{/owner}{/repo}","subscriptions_url":"https://api.github.com/users/AstroHuntsman/subscriptions","organizations_url":"https://api.github.com/users/AstroHuntsman/orgs","repos_url":"https://api.github.com/users/AstroHuntsman/repos","events_url":"https://api.github.com/users/AstroHuntsman/events{/privacy}","received_events_url":"https://api.github.com/users/AstroHuntsman/received_events","type":"Organization","site_admin":false},"private":false,"html_url":"https://github.com/AstroHuntsman/POCS","description":"PANOPTES Observatory Control System","fork":true,"url":"https://api.github.com/repos/AstroHuntsman/POCS","forks_url":"https://api.github.com/repos/AstroHuntsman/POCS/forks","keys_url":"https://api.github.com/repos/AstroHuntsman/POCS/keys{/key_id}","collaborators_url":"https://api.github.com/repos/AstroHuntsman/POCS/collaborators{/collaborator}","teams_url":"https://api.github.com/repos/AstroHuntsman/POCS/teams","hooks_url":"https://api.github.com/repos/AstroHuntsman/POCS/hooks","issue_events_url":"https://api.github.com/repos/AstroHuntsman/POCS/issues/events{/number}","events_url":"https://api.github.com/repos/AstroHuntsman/POCS/events","assignees_url":"https://api.github.com/repos/AstroHuntsman/POCS/assignees{/user}","branches_url":"https://api.github.com/repos/AstroHuntsman/POCS/branches{/branch}","tags_url":"https://api.github.com/repos/AstroHuntsman/POCS/tags","blobs_url":"https://api.github.com/repos/AstroHuntsman/POCS/git/blobs{/sha}","git_tags_url":"https://api.github.com/repos/AstroHuntsman/POCS/git/tags{/sha}","git_refs_url":"https://api.github.com/repos/AstroHuntsman/POCS/git/refs{/sha}","trees_url":"https://api.github.com/repos/AstroHuntsman/POCS/git/trees{/sha}","statuses_url":"https://api.github.com/repos/AstroHuntsman/POCS/statuses/{sha}","languages_url":"https://api.github.com/repos/AstroHuntsman/POCS/languages","stargazers_url":"https://api.github.com/repos/AstroHuntsman/POCS/stargazers","contributors_url":"https://api.github.com/repos/AstroHuntsman/POCS/contributors","subscribers_url":"https://api.github.com/repos/AstroHuntsman/POCS/subscribers","subscription_url":"https://api.github.com/repos/AstroHuntsman/POCS/subscription","commits_url":"https://api.github.com/repos/AstroHuntsman/POCS/commits{/sha}","git_commits_url":"https://api.github.com/repos/AstroHuntsman/POCS/git/commits{/sha}","comments_url":"https://api.github.com/repos/AstroHuntsman/POCS/comments{/number}","issue_comment_url":"https://api.github.com/repos/AstroHuntsman/POCS/issues/comments{/number}","contents_url":"https://api.github.com/repos/AstroHuntsman/POCS/contents/{+path}","compare_url":"https://api.github.com/repos/AstroHuntsman/POCS/compare/{base}...{head}","merges_url":"https://api.github.com/repos/AstroHuntsman/POCS/merges","archive_url":"https://api.github.com/repos/AstroHuntsman/POCS/{archive_format}{/ref}","downloads_url":"https://api.github.com/repos/AstroHuntsman/POCS/downloads","issues_url":"https://api.github.com/repos/AstroHuntsman/POCS/issues{/number}","pulls_url":"https://api.github.com/repos/AstroHuntsman/POCS/pulls{/number}","milestones_url":"https://api.github.com/repos/AstroHuntsman/POCS/milestones{/number}","notifications_url":"https://api.github.com/repos/AstroHuntsman/POCS/notifications{?since,all,participating}","labels_url":"https://api.github.com/repos/AstroHuntsman/POCS/labels{/name}","releases_url":"https://api.github.com/repos/AstroHuntsman/POCS/releases{/id}","deployments_url":"https://api.github.com/repos/AstroHuntsman/POCS/deployments","created_at":"2016-09-30T05:33:44Z","updated_at":"2017-02-14T05:11:23Z","pushed_at":"2017-02-14T10:35:39Z","git_url":"git://github.com/AstroHuntsman/POCS.git","ssh_url":"git@github.com:AstroHuntsman/POCS.git","clone_url":"https://github.com/AstroHuntsman/POCS.git","svn_url":"https://github.com/AstroHuntsman/POCS","homepage":"http://projectpanoptes.org","size":102016,"stargazers_count":0,"watchers_count":0,"language":"Jupyter Notebook","has_issues":true,"has_downloads":true,"has_wiki":true,"has_pages":false,"forks_count":0,"mirror_url":null,"open_issues_count":1,"forks":0,"open_issues":1,"watchers":0,"default_branch":"develop"}},"_links":{"self":{"href":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/2"},"html":{"href":"https://github.com/AstroHuntsman/POCS/pull/2"},"issue":{"href":"https://api.github.com/repos/AstroHuntsman/POCS/issues/2"},"comments":{"href":"https://api.github.com/repos/AstroHuntsman/POCS/issues/2/comments"},"review_comments":{"href":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/2/comments"},"review_comment":{"href":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/comments{/number}"},"commits":{"href":"https://api.github.com/repos/AstroHuntsman/POCS/pulls/2/commits"},"statuses":{"href":"https://api.github.com/repos/AstroHuntsman/POCS/statuses/7cb1ab25512ea51060dbea5156eab323f3269b1c"}}}}
{"actor":{"display_login":"AnthonyHorton"}}