Ejemplo n.º 1
0
def get_transit_observability(site,
                              ra,
                              dec,
                              name,
                              t_mid_0,
                              period,
                              duration,
                              n_transits=100,
                              obs_start_time=Time(
                                  dt.datetime.today().isoformat()),
                              min_local_time=dt.time(16, 0),
                              max_local_time=dt.time(9, 0),
                              min_altitude=20 * u.deg,
                              oot_duration=30 * u.minute,
                              minokmoonsep=30 * u.deg):
    """
    note: barycentric corrections not yet implemented. (could do this myself!)
    -> 16 minutes of imprecision is baked into this observability calculator!

    args:

        site (astroplan.observer.Observer)

        ra, dec (units u.deg), e.g.:
            ra=101.28715533*u.deg, dec=16.71611586*u.deg,
        or can also accept
            ra="17 56 35.51", dec="-29 32 21.5"

        name (str), e.g., "Sirius"

        t_mid_0 (float): in BJD_TDB, preferably (but see note above).

        period (astropy quantity, units time)

        duration (astropy quantity, units time)

        n_transits (int): number of transits forward extrapolated to

        obs_start_time (astropy.Time object): when to start calculation from

        min_local_time, max_local_time: earliest time when you think observing
        is OK. E.g., 16:00 local and 09:00 local are earliest and latest. Note
        this constraint is a bit silly, since the astroplan "AtNightConstraint"
        is imposed automatically. As implemented, these are ignored.

        min_altitude (astropy quantity, units deg): 20 degrees is the more
        relevant constraint.

        oot_duration (astropy quantity, units time): with which to brack
        transit observations, to get an OOT baseline.
    """

    if (isinstance(ra, u.quantity.Quantity)
            and isinstance(dec, u.quantity.Quantity)):
        target_coord = SkyCoord(ra=ra, dec=dec)
    elif (isinstance(ra, str) and isinstance(dec, str)):
        target_coord = SkyCoord(ra=ra, dec=dec, unit=(u.hourangle, u.deg))
    else:
        raise NotImplementedError

    target = FixedTarget(coord=target_coord, name=name)

    primary_eclipse_time = Time(t_mid_0, format='jd')

    system = EclipsingSystem(primary_eclipse_time=primary_eclipse_time,
                             orbital_period=period,
                             duration=duration,
                             name=name)

    midtransit_times = system.next_primary_eclipse_time(obs_start_time,
                                                        n_eclipses=n_transits)

    # for the time being, omit any local time constraints.
    constraints = [
        AtNightConstraint.twilight_civil(),
        AltitudeConstraint(min=min_altitude),
        MoonSeparationConstraint(min=minokmoonsep)
    ]
    #constraints = [AtNightConstraint.twilight_civil(),
    #               AltitudeConstraint(min=min_altitude),
    #               LocalTimeConstraint(min=min_local_time, max=max_local_time)]

    # observable just at midtime (bottom)
    b = is_event_observable(constraints, site, target, times=midtransit_times)

    # observable full transits (ingress, bottom, egress)
    ing_egr = system.next_primary_ingress_egress_time(obs_start_time,
                                                      n_eclipses=n_transits)

    ibe = is_event_observable(constraints,
                              site,
                              target,
                              times_ingress_egress=ing_egr)

    # get moon separation over each transit. take minimum moon sep at
    # ing/tmid/egr as the moon separation.
    moon_tmid = get_moon(midtransit_times, location=site.location)
    moon_separation_tmid = moon_tmid.separation(target_coord)

    moon_ing = get_moon(ing_egr[:, 0], location=site.location)
    moon_separation_ing = moon_ing.separation(target_coord)

    moon_egr = get_moon(ing_egr[:, 1], location=site.location)
    moon_separation_egr = moon_egr.separation(target_coord)

    moon_separation = np.round(
        np.array(
            [moon_separation_tmid, moon_separation_ing,
             moon_separation_egr]).min(axis=0), 0).astype(int)

    moon_illumination = np.round(
        100 * moon.moon_illumination(midtransit_times), 0).astype(int)

    # completely observable transits (OOT, ingress, bottom, egress, OOT)
    oot_ing_egr = np.concatenate(
        (np.array(ing_egr[:, 0] - oot_duration)[:, None],
         np.array(ing_egr[:, 1] + oot_duration)[:, None]),
        axis=1)

    oibeo = is_event_observable(constraints,
                                site,
                                target,
                                times_ingress_egress=oot_ing_egr)

    ing_tmid_egr = np.concatenate(
        (np.array(ing_egr[:, 0])[:, None], np.array(midtransit_times)[:, None],
         np.array(ing_egr[:, 1])[:, None]),
        axis=1)

    return ibe, oibeo, ing_tmid_egr, moon_separation, moon_illumination
def get_event_observability(
    eventclass,
    site, ra, dec, name, t_mid_0, period, duration, n_transits=100,
    obs_start_time=Time(dt.datetime.today().isoformat()),
    min_altitude = None,
    oot_duration = 30*u.minute,
    minokmoonsep = 30*u.deg,
    max_airmass = None,
    twilight_limit = 'nautical'):
    """
    note: barycentric corrections not yet implemented. (could do this myself!)
    -> 16 minutes of imprecision is baked into this observability calculator!

    args:

        eventclass: e.g., "OIBE". Function does NOT return longer events.

        site (astroplan.observer.Observer)

        ra, dec (units u.deg), e.g.:
            ra=101.28715533*u.deg, dec=16.71611586*u.deg,
        or can also accept
            ra="17 56 35.51", dec="-29 32 21.5"

        name (str), e.g., "Sirius"

        t_mid_0 (float): in BJD_TDB, preferably (but see note above).

        period (astropy quantity, units time)

        duration (astropy quantity, units time)

        n_transits (int): number of transits forward extrapolated to

        obs_start_time (astropy.Time object): when to start calculation from

        min_altitude (astropy quantity, units deg): 20 degrees is the more
        relevant constraint.

        max_airmass: e.g., 2.5. One of max_airmass or min_altitude is required.

        oot_duration (astropy quantity, units time): with which to brack
        transit observations, to get an OOT baseline.

        twilight_limit: 'astronomical', 'nautical', 'civil' for -18, -12, -6
        deg.
    """
    if eventclass not in [
        'OIBEO', 'OIBE', 'IBEO', 'IBE', 'BEO', 'OIB', 'OI', 'EO'
    ]:
        raise AssertionError

    if (isinstance(ra, u.quantity.Quantity) and
        isinstance(dec, u.quantity.Quantity)
    ):
        target_coord = SkyCoord(ra=ra, dec=dec)
    elif (isinstance(ra, str) and
          isinstance(dec, str)
    ):
        target_coord = SkyCoord(ra=ra, dec=dec, unit=(u.hourangle, u.deg))
    else:
        raise NotImplementedError

    if (
        not isinstance(max_airmass, float)
        or isinstance(min_altitude, u.quantity.Quantity)
    ):
        raise NotImplementedError

    target = FixedTarget(coord=target_coord, name=name)

    primary_eclipse_time = Time(t_mid_0, format='jd')

    system = EclipsingSystem(primary_eclipse_time=primary_eclipse_time,
                             orbital_period=period, duration=duration,
                             name=name)

    midtransit_times = system.next_primary_eclipse_time(
        obs_start_time, n_eclipses=n_transits)

    # for the time being, omit any local time constraints.
    if twilight_limit == 'astronomical':
        twilight_constraint = AtNightConstraint.twilight_astronomical()
    elif twilight_limit == 'nautical':
        twilight_constraint = AtNightConstraint.twilight_nautical()
    else:
        raise NotImplementedError('civil twilight is janky.')

    constraints = [twilight_constraint,
                   AltitudeConstraint(min=min_altitude),
                   AirmassConstraint(max=max_airmass),
                   MoonSeparationConstraint(min=minokmoonsep)]

    # tabulate ingress and egress times.
    ing_egr = system.next_primary_ingress_egress_time(
        obs_start_time, n_eclipses=n_transits
    )

    oibeo_window = np.concatenate(
        (np.array(ing_egr[:,0] - oot_duration)[:,None],
         np.array(ing_egr[:,1] + oot_duration)[:,None]),
        axis=1)
    oibe_window = np.concatenate(
        (np.array(ing_egr[:,0] - oot_duration)[:,None],
         np.array(ing_egr[:,1])[:,None]),
        axis=1)
    ibeo_window = np.concatenate(
        (np.array(ing_egr[:,0])[:,None],
         np.array(ing_egr[:,1] + oot_duration)[:,None]),
        axis=1)
    oib_window = np.concatenate(
        (np.array(ing_egr[:,0] - oot_duration)[:,None],
         np.array(midtransit_times)[:,None]),
        axis=1)
    beo_window = np.concatenate(
        (np.array(midtransit_times)[:,None],
         np.array(ing_egr[:,1] + oot_duration)[:,None]),
        axis=1)
    ibe_window = ing_egr
    oi_window = np.concatenate(
        (np.array(ing_egr[:,0] - oot_duration)[:,None],
        np.array(ing_egr[:,0])[:,None]),
        axis=1)
    eo_window = np.concatenate(
        (np.array(ing_egr[:,1])[:,None],
        np.array(ing_egr[:,1] + oot_duration)[:,None]),
        axis=1)

    keys = ['oibeo','oibe','ibeo','oib','beo','ibe','oi','eo']
    windows = [oibeo_window, oibe_window, ibeo_window,
               oib_window, beo_window, ibe_window, oi_window, eo_window]
    is_obs_dict = {}
    for key, window in zip(keys, windows):
        is_obs_dict[key] = np.array(
            is_event_observable(constraints, site, target,
                                times_ingress_egress=window)
        ).flatten()

    is_obs_df = pd.DataFrame(is_obs_dict)

    is_obs_df['ing'] = ing_egr[:,0]
    is_obs_df['egr'] = ing_egr[:,1]
    is_obs_df['isoing'] = Time(ing_egr[:,0], format='iso')
    is_obs_df['isoegr'] = Time(ing_egr[:,1], format='iso')

    # this function returns the observable events that are LONGEST. e.g.,
    # during an OIBEO transit you COULD observe just OIB, but why would you?

    if eventclass == 'OIBEO':
        event_ind = np.array(is_obs_df[eventclass.lower()])[None,:]
    elif eventclass in ['IBEO', 'OIBE']:
        event_ind = np.array(
            is_obs_df[eventclass.lower()] & ~is_obs_df['oibeo']
        )[None,:]
    elif eventclass in ['IBE', 'OIB', 'BEO']:
        event_ind = np.array(
            is_obs_df[eventclass.lower()]
            & ~is_obs_df['oibeo']
            & ~is_obs_df['oibe']
            & ~is_obs_df['ibeo']
        )[None,:]
    elif eventclass in ['OI', 'EO']:
        event_ind = np.array(
            is_obs_df[eventclass.lower()]
            & ~is_obs_df['oibeo']
            & ~is_obs_df['oibe']
            & ~is_obs_df['ibeo']
            & ~is_obs_df['oib']
            & ~is_obs_df['ibe']
            & ~is_obs_df['beo']
        )[None,:]

    # get moon separation over each transit. take minimum moon sep at
    # ing/tmid/egr as the moon separation.
    moon_tmid = get_moon(midtransit_times, location=site.location)
    moon_separation_tmid = moon_tmid.separation(target_coord)

    moon_ing = get_moon(ing_egr[:,0], location=site.location)
    moon_separation_ing = moon_ing.separation(target_coord)

    moon_egr = get_moon(ing_egr[:,1], location=site.location)
    moon_separation_egr = moon_egr.separation(target_coord)

    moon_separation = np.round(np.array(
        [moon_separation_tmid, moon_separation_ing,
         moon_separation_egr]).min(axis=0),0).astype(int)

    moon_illumination = np.round(
        100*moon.moon_illumination(midtransit_times),0).astype(int)

    # completely observable transits (OOT, ingress, bottom, egress, OOT)
    oibeo = is_event_observable(constraints, site, target,
                                times_ingress_egress=oibeo_window)

    ing_tmid_egr = np.concatenate(
        (np.array(ing_egr[:,0])[:,None],
         np.array(midtransit_times)[:,None],
         np.array(ing_egr[:,1])[:,None]),
        axis=1)

    target_window = np.array(windows)[
        int(np.argwhere(np.array(keys)==eventclass.lower())), :, :
    ]

    return (
        event_ind, oibeo, ing_tmid_egr, target_window,
        moon_separation, moon_illumination
    )
Ejemplo n.º 3
0
    def get_schedulable_blocks(self) -> list:
        """Returns list of schedulable blocks.

        Returns:
            List of schedulable blocks
        """

        # get requests
        r = requests.get(urljoin(self._url,
                                 '/api/requestgroups/schedulable_requests/'),
                         headers=self._header,
                         proxies=self._proxies)
        if r.status_code != 200:
            raise ValueError('Could not fetch list of schedulable requests.')
        schedulable = r.json()

        # get proposal priorities
        r = requests.get(urljoin(self._url, '/api/proposals/'),
                         headers=self._header,
                         proxies=self._proxies)
        if r.status_code != 200:
            raise ValueError('Could not fetch list of proposals.')
        tac_priorities = {
            p['id']: p['tac_priority']
            for p in r.json()['results']
        }

        # loop all request groups
        blocks = []
        for group in schedulable:
            # get base priority, which is tac_priority * ipp_value
            proposal = group['proposal']
            if proposal not in tac_priorities:
                log.error('Could not find proposal "%s".', proposal)
                continue
            base_priority = group['ipp_value'] * tac_priorities[proposal]

            # loop all requests in group
            for req in group['requests']:
                # still pending?
                if req['state'] != 'PENDING':
                    continue

                # duration
                duration = req['duration'] * u.second

                # time constraints
                time_constraints = [
                    TimeConstraint(Time(wnd['start']), Time(wnd['end']))
                    for wnd in req['windows']
                ]

                # loop configs
                for cfg in req['configurations']:
                    # get instrument and check, whether we schedule it
                    instrument = cfg['instrument_type']
                    if instrument.lower(
                    ) != self._portal_instrument_type.lower():
                        continue

                    # target
                    t = cfg['target']
                    target = SkyCoord(t['ra'] * u.deg,
                                      t['dec'] * u.deg,
                                      frame=t['type'].lower())

                    # constraints
                    c = cfg['constraints']
                    constraints = [
                        AirmassConstraint(max=c['max_airmass'],
                                          boolean_constraint=False),
                        MoonSeparationConstraint(min=c['min_lunar_distance'] *
                                                 u.deg)
                    ]

                    # priority is base_priority times duration in minutes
                    priority = base_priority * duration.value / 60.

                    # create block
                    block = ObservingBlock(
                        FixedTarget(target, name=req["id"]),
                        duration,
                        priority,
                        constraints=[*constraints, *time_constraints],
                        configuration={'request': req})
                    blocks.append(block)

        # return blocks
        return blocks
Ejemplo n.º 4
0
def get_obs_data(target, observers, current_time, alt_limit=30):
    """Compile infomation about the target's visibility from the given observers."""
    all_data = {}
    if target is None:
        return all_data

    for observer in observers:
        data = {}
        data['observer'] = observer
        data['current_time'] = current_time

        # Get midnight and astronomical twilight times
        midnight = observer.midnight(current_time, which='next')
        sun_set = observer.twilight_evening_astronomical(midnight,
                                                         which='previous')
        sun_rise = observer.twilight_morning_astronomical(midnight,
                                                          which='next')
        dark_time = Time([sun_set, sun_rise])
        data['midnight'] = midnight
        data['sun_set'] = sun_set
        data['sun_rise'] = sun_rise

        # Apply a constraint on altitude
        min_alt = alt_limit * u.deg
        alt_constraint = AltitudeConstraint(min=min_alt, max=None)
        alt_observable = is_observable(alt_constraint,
                                       observer,
                                       target,
                                       time_range=dark_time)[0]
        data['alt_constraint'] = alt_constraint
        data['alt_observable'] = alt_observable

        # Get target rise and set times
        if alt_observable:
            with warnings.catch_warnings():
                warnings.simplefilter('ignore')
                target_rise = observer.target_rise_time(midnight,
                                                        target,
                                                        which='nearest',
                                                        horizon=min_alt)
                target_set = observer.target_set_time(target_rise,
                                                      target,
                                                      which='next',
                                                      horizon=min_alt)

            # Get observation times
            observation_start = target_rise
            observation_end = target_set
            if target_rise.jd < 0 or target_set.jd < 0:
                # target is always above the horizon, so visible all night
                observation_start = sun_set
                observation_end = sun_rise
            if target_rise < sun_set:
                # target is already up when the sun sets
                observation_start = sun_set
            if target_set > sun_rise:
                # target sets after the sun rises
                observation_end = sun_rise

            data['target_rise'] = target_rise
            data['target_set'] = target_set
            data['observation_start'] = observation_start
            data['observation_end'] = observation_end
        else:
            data['target_rise'] = None
            data['target_set'] = None
            data['observation_start'] = None
            data['observation_end'] = None

        # Apply a constraint on distance from the Moon
        min_moon = 5 * u.deg
        moon_constraint = MoonSeparationConstraint(min=min_moon, max=None)
        moon_observable = is_observable(moon_constraint,
                                        observer,
                                        target,
                                        time_range=dark_time)[0]
        data['moon_constraint'] = moon_constraint
        data['moon_observable'] = moon_observable

        all_data[observer.name] = data

    return all_data
Ejemplo n.º 5
0
    async def get_schedulable_blocks(self) -> List[ObservingBlock]:
        """Returns list of schedulable blocks.

        Returns:
            List of schedulable blocks
        """

        # check
        if self._portal_instrument_type is None:
            raise ValueError("No instrument type for portal set.")

        # get data
        schedulable = await self._portal_get(
            urljoin(self._url, "/api/requestgroups/schedulable_requests/"))

        # get proposal priorities
        data = await self._portal_get(urljoin(self._url, "/api/proposals/"))
        tac_priorities = {p["id"]: p["tac_priority"] for p in data["results"]}

        # loop all request groups
        blocks = []
        for group in schedulable:
            # get base priority, which is tac_priority * ipp_value
            proposal = group["proposal"]
            if proposal not in tac_priorities:
                log.error('Could not find proposal "%s".', proposal)
                continue
            base_priority = group["ipp_value"] * tac_priorities[proposal]

            # loop all requests in group
            for req in group["requests"]:
                # still pending?
                if req["state"] != "PENDING":
                    continue

                # duration
                duration = req["duration"] * u.second

                # time constraints
                time_constraints = [
                    TimeConstraint(Time(wnd["start"]), Time(wnd["end"]))
                    for wnd in req["windows"]
                ]

                # loop configs
                for cfg in req["configurations"]:
                    # get instrument and check, whether we schedule it
                    instrument = cfg["instrument_type"]
                    if instrument.lower(
                    ) != self._portal_instrument_type.lower():
                        continue

                    # target
                    t = cfg["target"]
                    target = SkyCoord(t["ra"] * u.deg,
                                      t["dec"] * u.deg,
                                      frame=t["type"].lower())

                    # constraints
                    c = cfg["constraints"]
                    constraints = []
                    if "max_airmass" in c and c["max_airmass"] is not None:
                        constraints.append(
                            AirmassConstraint(max=c["max_airmass"],
                                              boolean_constraint=False))
                    if "min_lunar_distance" in c and c[
                            "min_lunar_distance"] is not None:
                        constraints.append(
                            MoonSeparationConstraint(
                                min=c["min_lunar_distance"] * u.deg))
                    if "max_lunar_phase" in c and c[
                            "max_lunar_phase"] is not None:
                        constraints.append(
                            MoonIlluminationConstraint(
                                max=c["max_lunar_phase"]))
                        # if max lunar phase <= 0.4 (which would be DARK), we also enforce the sun to be <-18 degrees
                        if c["max_lunar_phase"] <= 0.4:
                            constraints.append(
                                AtNightConstraint.twilight_astronomical())

                    # priority is base_priority times duration in minutes
                    # priority = base_priority * duration.value / 60.
                    priority = base_priority

                    # create block
                    block = ObservingBlock(
                        FixedTarget(target, name=req["id"]),
                        duration,
                        priority,
                        constraints=[*constraints, *time_constraints],
                        configuration={"request": req},
                    )
                    blocks.append(block)

        # return blocks
        return blocks
Ejemplo n.º 6
0
def run_nights(observers, nameList, args):
    # Define range of times to observe between
    startDate = datetime.datetime.strptime(args.startDate,"%Y-%m-%d")
    beginTimeFirstNight = datetime.datetime(startDate.year,startDate.month,startDate.day,hour=16)
    endTimeFirstNight = beginTimeFirstNight + datetime.timedelta(hours=16)
    t_datetimes_nights_list = []
    for iDay in range(args.nNights):
        beginTime = beginTimeFirstNight + datetime.timedelta(days=iDay)
        endTime = endTimeFirstNight + datetime.timedelta(days=iDay)
        currTime = beginTime
        t_datetime = []
        while currTime <= endTime:
            t_datetime.append(currTime)
            currTime += datetime.timedelta(hours=1)
        t_datetimes_nights_list.append(t_datetime)

    targets = [FixedTarget(coord=lookuptarget(name),name=name) for name in nameList]
    targetLabelList, ylabelsize = makeTargetLabels(nameList,args)

    constraints = [
        AltitudeConstraint(min=args.minAlt*u.deg),
        AtNightConstraint.twilight_astronomical(),
        MoonSeparationConstraint(min=args.minMoonSep*u.deg),
        MoonIlluminationConstraint(max=args.maxMoonIllum),
    ]

    outfn = args.outFileNameBase+"_nightly.pdf"
    with PdfPages(outfn) as pdf:
        for observer in observers:
            fig, axes = mpl.subplots(
                figsize=(8.5,11),
                ncols=args.nNights,
                sharex="col",
                gridspec_kw={
                    "top":0.92,
                    "bottom":0.03,
                    "left":0.13,
                    "right":0.98,
                    "hspace":0,
                    "wspace":0
                },
                tight_layout=False,constrained_layout=False
            )

            observability_grids = []
            for iNight in range(args.nNights):
                t_datetime = t_datetimes_nights_list[iNight]
                time_grid = [Time(observer.timezone.localize(t)) for t in t_datetime]
                
                observability_grid = numpy.zeros((len(targets),len(time_grid)-1))
                for i in range(len(time_grid)-1):
                    tmp = is_always_observable(constraints, observer, targets, times=[time_grid[i],time_grid[i+1]])
                    observability_grid[:, i] = tmp
                observability_grids.append(observability_grid)

            observable_targets = targets
            observable_target_labels = targetLabelList
            ever_observability_grids = observability_grids
            if args.onlyEverObservable:
                target_is_observable = numpy.zeros(len(targets))
                for observability_grid in observability_grids:
                    for iTime in range(observability_grid.shape[1]):
                        target_is_observable += observability_grid[:,iTime]
                target_is_observable = target_is_observable > 0. # change to boolean numpy array
                observable_targets = [x for x, o in zip(targets,target_is_observable) if o]
                observable_target_labels = [x for x, o in zip(targetLabelList,target_is_observable) if o]
                ever_observability_grids = []
                for observability_grid in observability_grids:
                    ever_observability_grid = observability_grid[target_is_observable,:]
                    ever_observability_grids.append(ever_observability_grid)

            for iNight in range(args.nNights):
                ax = axes[iNight]
                t_datetime = t_datetimes_nights_list[iNight]
                extent = [0, len(t_datetime)-1, -0.5, len(observable_targets)-0.5]
                ax.imshow(ever_observability_grids[iNight], extent=extent, origin="lower", aspect="auto", cmap=mpl.get_cmap("Greens"))
                ax.xaxis.tick_top()
                ax.xaxis.set_label_position("top")
                ax.invert_yaxis()

                if iNight == 0:
                    ax.set_yticks(range(0,len(observable_targets)))
                    ax.set_yticklabels(observable_target_labels, fontsize=ylabelsize)
                else:
                    ax.set_yticks([])

                ax.set_xticks(range(0,len(t_datetime)-1,4))
                ax.set_xticks(range(0,len(t_datetime)),minor=True)
                ax.set_xticklabels([t_datetime[i].strftime("%Hh") for i in range(0,len(t_datetime)-1,4)])

                ax.set_xlabel(t_datetime[0].strftime("%a %b %d"))

                ax.set_yticks(numpy.arange(extent[2], extent[3]), minor=True)

                ax.grid(axis="x",which="minor",color="0.7",ls="-", linewidth=0.5)
                ax.grid(axis="x",which="major",color="0.7",ls="-", linewidth=1)
                ax.grid(axis="y",which="minor",color="0.7",ls="-", linewidth=0.5)

                ax.tick_params(axis='y', which='minor', left=False, right=False)
                ax.tick_params(axis='x', which='minor', bottom=False, top=False)
        
            fig.suptitle(f"Observability at {observer.name} in {startDate.year}")
            fig.text(1.0,0.0,"Constraints: Astronomical Twilight, Altitude $\geq {:.0f}^\circ$, Moon Seperation $\geq {:.0f}^\circ$, Moon Illumination $\leq {:.2f}$".format(args.minAlt,args.minMoonSep,args.maxMoonIllum),ha="right",va="bottom")
            pdf.savefig(fig)
        print(f"Writing out file: {outfn}")