Skip to content
49 changes: 48 additions & 1 deletion astroplan/constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,8 @@
"LocalTimeConstraint", "PrimaryEclipseConstraint",
"SecondaryEclipseConstraint", "Constraint", "TimeConstraint",
"observability_table", "months_observable", "max_best_rescale",
"min_best_rescale", "PhaseConstraint", "is_event_observable"]
"min_best_rescale", "PhaseConstraint", "is_event_observable",
"MeridianSeparationConstraint"]

_current_year = time.localtime().tm_year # needed for backward compatibility
_current_year_time_range = Time( # needed for backward compatibility
Expand Down Expand Up @@ -924,6 +925,52 @@ def compute_constraint(self, times, observer=None, targets=None):
return mask


class MeridianSeparationConstraint(Constraint):
"""
Constraint on angular separation from the meridian.
"""
def __init__(self, min=None, max=None, boolean_constraint=True):
"""
Parameters
----------
min : `~astropy.units.Quantity` or `None`, optional
Minimum acceptable angular distance from the meridian.
`None` indicates no lower limit.

max : `~astropy.units.Quantity` or `None`, optional
Maximum acceptable angular distance from the meridian.
`None` indicates no upper limit.

boolean_constraint : bool

Examples
--------
Constrain observations to targets that are between 3 and 35 degrees
away from the meridian.
>>> import astropy.units as u
>>> constraint = MeridianSeparationConstraint(min=3*u.deg, max=35*u.deg)

This can be useful for observations using German-Equatorial Mounts, to avoid
flipping the side of the pier during exposures.
"""
self.min = min if min is not None else 0*u.deg
self.max = max if max is not None else 180*u.deg
self.boolean_constraint = boolean_constraint

def compute_constraint(self, times, observer, targets):
lst = observer.local_sidereal_time(times)
meridian = SkyCoord(ra=lst, dec=targets.dec)

meridian_separation = meridian.separation(targets)

if self.boolean_constraint:
mask = (self.min < meridian_separation) & (meridian_separation < self.max)
return mask
else:
rescale = min_best_rescale(meridian_separation, self.min, self.max, less_than_min=0)
return rescale


def is_always_observable(constraints, observer, targets, times=None,
time_range=None, time_grid_resolution=0.5*u.hour):
"""
Expand Down
18 changes: 14 additions & 4 deletions astroplan/tests/test_constraints.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,7 @@
TimeConstraint, LocalTimeConstraint, months_observable,
max_best_rescale, min_best_rescale, PhaseConstraint,
PrimaryEclipseConstraint, SecondaryEclipseConstraint,
is_event_observable)
is_event_observable, MeridianSeparationConstraint)
from astroplan.exceptions import MissingConstraintWarning
from astroplan.observer import Observer
from astroplan.periodic import EclipsingSystem
Expand Down Expand Up @@ -202,9 +202,18 @@ def test_sun_separation():
assert np.all(is_constraint_met == [False, True, True])


@pytest.mark.remote_data
# astropy.coordinates.errors.NonRotationTransformationWarning
@pytest.mark.filterwarnings("ignore")
def test_meridian_separation():
time_range = Time(["2024-10-08 21:00", "2024-10-08 23:00"])
target = FixedTarget(coord=SkyCoord(ra=19.75*u.hour, dec=-22.50*u.deg), name="name")

# Pico dos Dias Observatory (Brazil)
opd = Observer(location=EarthLocation(lat=-22.53, lon=-45.58, height=1864))
constraint = MeridianSeparationConstraint(min=3*u.deg, max=35*u.deg)

results = constraint(opd, target, times=time_grid_from_range(time_range))
assert np.all(results == [True, False, True, True, True])


def test_moon_separation():
time = Time('2003-04-05 06:07:08')
apo = Observer.at_site("APO")
Expand Down Expand Up @@ -429,6 +438,7 @@ def test_rescale_minmax():
AtNightConstraint(),
SunSeparationConstraint(min=90*u.deg),
MoonSeparationConstraint(min=20*u.deg),
MeridianSeparationConstraint(min=3*u.deg),
LocalTimeConstraint(min=dt.time(23, 50), max=dt.time(4, 8)),
TimeConstraint(*Time(["2015-08-28 03:30", "2015-09-05 10:30"]))
]
Expand Down
Loading