From 091133c0faa8b4733a215c7128ef1819eb8f4d7c Mon Sep 17 00:00:00 2001 From: kvyh Date: Mon, 22 Aug 2016 19:40:07 -0700 Subject: [PATCH 1/4] created a weighted scorer and started writing tests --- astroplan/constraints.py | 49 ++++++++++++++++----- astroplan/scheduling.py | 70 ++++++++++++++++++++++++++++++ astroplan/tests/test_scheduling.py | 27 ++++++++++++ 3 files changed, 136 insertions(+), 10 deletions(-) diff --git a/astroplan/constraints.py b/astroplan/constraints.py index 670750c3..33a3d6b2 100644 --- a/astroplan/constraints.py +++ b/astroplan/constraints.py @@ -529,7 +529,7 @@ class MoonIlluminationConstraint(Constraint): Constraint is also satisfied if the Moon has set. """ - def __init__(self, min=None, max=None, ephemeris=None): + def __init__(self, min=None, max=None, ephemeris=None, boolean_constraint=True): """ Parameters ---------- @@ -543,10 +543,15 @@ def __init__(self, min=None, max=None, ephemeris=None): Ephemeris to use. If not given, use the one set with `~astropy.coordinates.solar_system_ephemeris` (which is set to 'builtin' by default). + boolean_constraint : bool + If True, the constraint is treated as a boolean (True for within the + limits and False for outside). If False, the constraint returns a + float on [0, 1], where 0 is the min altitude and 1 is the max. """ self.min = min self.max = max self.ephemeris = ephemeris + self.boolean_constraint = boolean_constraint @classmethod def dark(cls, min=None, max=0.25, **kwargs): @@ -607,16 +612,40 @@ def compute_constraint(self, times, observer, targets): moon_up_mask = moon_alt >= 0 illumination = cached_moon['illum'] - if self.min is None and self.max is not None: - mask = (self.max >= illumination) | moon_down_mask - elif self.max is None and self.min is not None: - mask = (self.min <= illumination) & moon_up_mask - elif self.min is not None and self.max is not None: - mask = ((self.min <= illumination) & - (illumination <= self.max)) & moon_up_mask + if self.boolean_constraint: + if self.min is None and self.max is not None: + mask = (self.max >= illumination) | moon_down_mask + elif self.max is None and self.min is not None: + mask = (self.min <= illumination) & moon_up_mask + elif self.min is not None and self.max is not None: + mask = ((self.min <= illumination) & + (illumination <= self.max)) & moon_up_mask + else: + raise ValueError("No max and/or min specified in " + "MoonSeparationConstraint.") else: - raise ValueError("No max and/or min specified in " - "MoonSeparationConstraint.") + if self.min is None and self.max is not None: + moon_down = np.where(moon_down_mask == 1) + mask = min_best_rescale(illumination, 0, self.max, 0) + mask[moon_down] = 1 + elif self.max is None and self.min is not None: + moon_down = np.where(moon_down_mask == 1) + mask = min_best_rescale(illumination, self.min, 1, 0) + if self.min == 0: + mask[moon_down] = 1 + else: + mask[moon_down] = 0 + elif self.min is not None and self.max is not None: + moon_down = np.where(moon_down_mask == 1) + mask = min_best_rescale(illumination, self.min, + self.max, 0) + if self.min == 0: + mask[moon_down] = 1 + else: + mask[moon_down] = 0 + else: + raise ValueError("No max and/or min specified in " + "MoonSeparationConstraint.") if targets is not None: mask = np.tile(mask, len(targets)) diff --git a/astroplan/scheduling.py b/astroplan/scheduling.py index 697f694f..420fd015 100644 --- a/astroplan/scheduling.py +++ b/astroplan/scheduling.py @@ -147,6 +147,76 @@ def create_score_array(self, time_resolution=1*u.minute): score_array *= constraint(self.observer, targets, times) return score_array + def weighted_score_array(self, time_resolution=1*u.minute): + """ + For each block, this returns a score for each time using the + formula (score*weight+score*weight+...)/(weight+weight+..) + """ + # note: with this method, a non-boolean constraint with weight + # zero has the exact same effect if it was boolean + start = self.schedule.start_time + end = self.schedule.end_time + times = time_grid_from_range((start, end), time_resolution) + score_array = np.zeros((len(self.blocks), len(times))) + zeros = np.ones((len(self.blocks), len(times)), dtype=int) + local_constraints = [] + weights = [] + for i, block in enumerate(self.blocks): + weights.append(0) + local_constraints.append([]) + # schedulers put global constraints into ._all_constraints + # so we can use .constraints for the local constraints + if block.constraints: + for constraint in block.constraints: + local_constraints[i].append(constraint.__class__.__name__) + applied_score = constraint(self.observer, [block.target], + times=times)[0] + if constraint.boolean_constraint: + # add to the mask designating where the score is zero + zeros[i] &= applied_score + # TODO: make a default weight=1 and merge these + elif constraint.weight: + zeros[i][np.where(applied_score == 0)] = 0 + weight = constraint.weight + weights[i] += weight + score_array[i] += applied_score * weight + else: + zeros[i][np.where(applied_score == 0)] = 0 + weights[i] += 1 + score_array[i] += applied_score + targets = [block.target for block in self.blocks] + for constraint in self.global_constraints: + skip_global = [] + for i, block in enumerate(local_constraints): + if constraint.__class__.__name__ in block: + skip_global.append(i) + global_score = constraint(self.observer, targets, times) + if constraint.boolean_constraint: + zeros &= 1-global_score + elif constraint.weight: + weight = constraint.weight + for i, score in enumerate(global_score): + if i not in skip_global: + weights[i] += weight + score_array[i] += score*weight + zeros[i][np.where(score == 0)] = 0 + else: + for i, score in enumerate(global_score): + if i not in skip_global: + weights[i] += 1 + score_array[i] += score + zeros[i][np.where(score == 0)] = 0 + + for i, scores in enumerate(score_array): + if weights[i]: + scores *= 1/weights[i] + else: + # if no weight, then nothing was added to the score_array + # just use the zeros (squaring 0 and 1 gives 0 and 1) + scores += zeros[i] + score_array *= zeros + return score_array + @classmethod def from_start_end(cls, blocks, observer, start_time, end_time, global_constraints=[]): diff --git a/astroplan/tests/test_scheduling.py b/astroplan/tests/test_scheduling.py index 4e02ec40..2fb5c630 100644 --- a/astroplan/tests/test_scheduling.py +++ b/astroplan/tests/test_scheduling.py @@ -265,3 +265,30 @@ def test_scorer(): scores = scorer.create_score_array(time_resolution=20 * u.minute) # the ``global_constraint``: constraint2 should have applied to the blocks assert np.array_equal(c2, scores) + + +def test_weighted_scorer(): + times = time_grid_from_range(Time(['2016-02-06 00:00', '2016-02-06 08:00']), + time_resolution=20 * u.minute) + constraint = AirmassConstraint(max=2, boolean_constraint=False) + constraint.weight = .8 + c = constraint(apo, [vega, rigel], times) + block = ObservingBlock(vega, 1 * u.hour, 0, constraints=[constraint]) + block2 = ObservingBlock(rigel, 1 * u.hour, 0, constraints=[constraint]) + scorer = Scorer.from_start_end([block, block2], apo, Time('2016-02-06 00:00'), + Time('2016-02-06 08:00')) + scores = scorer.weighted_score_array(time_resolution=20 * u.minute) + # due to float multiplication and division c and scores are not exactly equal + assert np.array_equal(np.round(c, 10), np.round(scores, 10)) + ''' + constraint2 = MoonIlluminationConstraint(max=.6, boolean_constraint=False) + constraint2.weight = .7 + c2 = constraint2(apo, [vega, rigel], times) + block = ObservingBlock(vega, 1 * u.hour, 0, constraints=[constraint, constraint2]) + block2 = ObservingBlock(rigel, 1 * u.hour, 0, constraints=[constraint]) + scorer = Scorer.from_start_end([block, block2], apo, Time('2016-02-06 00:00'), + Time('2016-02-06 08:00')) + scores = scorer.weighted_score_array(time_resolution=20 * u.minute) + assert all(scores[0] - (c[0] * .8 + c2[0] * .7)/1.5 + np.array_equal(np.round(scores[0], 10), np.round((c[0] * .8 + c2[0] * .7)/1.5, 10)) + assert np.array_equal(np.round(scores[1], 10), np.round(c[1], 10))''' From 741d63a81805f34d88fd09a9cbcf0bc797d32880 Mon Sep 17 00:00:00 2001 From: kvyh Date: Tue, 4 Oct 2016 08:57:55 -0700 Subject: [PATCH 2/4] removed unnecessary np.where calls --- astroplan/scheduling.py | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/astroplan/scheduling.py b/astroplan/scheduling.py index 420fd015..a826cd88 100644 --- a/astroplan/scheduling.py +++ b/astroplan/scheduling.py @@ -157,7 +157,9 @@ def weighted_score_array(self, time_resolution=1*u.minute): start = self.schedule.start_time end = self.schedule.end_time times = time_grid_from_range((start, end), time_resolution) + # create an array to hold all of the scores score_array = np.zeros((len(self.blocks), len(times))) + # create an array to record where any of the constraints are zero zeros = np.ones((len(self.blocks), len(times)), dtype=int) local_constraints = [] weights = [] @@ -176,12 +178,12 @@ def weighted_score_array(self, time_resolution=1*u.minute): zeros[i] &= applied_score # TODO: make a default weight=1 and merge these elif constraint.weight: - zeros[i][np.where(applied_score == 0)] = 0 + zeros[i][(applied_score == 0)] = 0 weight = constraint.weight weights[i] += weight score_array[i] += applied_score * weight else: - zeros[i][np.where(applied_score == 0)] = 0 + zeros[i][(applied_score == 0)] = 0 weights[i] += 1 score_array[i] += applied_score targets = [block.target for block in self.blocks] @@ -199,13 +201,13 @@ def weighted_score_array(self, time_resolution=1*u.minute): if i not in skip_global: weights[i] += weight score_array[i] += score*weight - zeros[i][np.where(score == 0)] = 0 + zeros[i][(score == 0)] = 0 else: for i, score in enumerate(global_score): if i not in skip_global: weights[i] += 1 score_array[i] += score - zeros[i][np.where(score == 0)] = 0 + zeros[i][(score == 0)] = 0 for i, scores in enumerate(score_array): if weights[i]: From 4be33d1546330cf2ae377c620f1ecf4415cdabe8 Mon Sep 17 00:00:00 2001 From: kvyh Date: Mon, 10 Oct 2016 11:33:33 -0700 Subject: [PATCH 3/4] rename zeros to constraint_zeros --- astroplan/scheduling.py | 19 ++++++++++--------- astroplan/tests/test_scheduling.py | 6 +++--- 2 files changed, 13 insertions(+), 12 deletions(-) diff --git a/astroplan/scheduling.py b/astroplan/scheduling.py index a826cd88..6bc2471d 100644 --- a/astroplan/scheduling.py +++ b/astroplan/scheduling.py @@ -160,7 +160,7 @@ def weighted_score_array(self, time_resolution=1*u.minute): # create an array to hold all of the scores score_array = np.zeros((len(self.blocks), len(times))) # create an array to record where any of the constraints are zero - zeros = np.ones((len(self.blocks), len(times)), dtype=int) + constraint_zeros = np.ones((len(self.blocks), len(times)), dtype=int) local_constraints = [] weights = [] for i, block in enumerate(self.blocks): @@ -175,15 +175,16 @@ def weighted_score_array(self, time_resolution=1*u.minute): times=times)[0] if constraint.boolean_constraint: # add to the mask designating where the score is zero - zeros[i] &= applied_score + # if either is 0, constraint_zeros becomes 0 + constraint_zeros[i] &= applied_score # TODO: make a default weight=1 and merge these elif constraint.weight: - zeros[i][(applied_score == 0)] = 0 + constraint_zeros[i][(applied_score == 0)] = 0 weight = constraint.weight weights[i] += weight score_array[i] += applied_score * weight else: - zeros[i][(applied_score == 0)] = 0 + constraint_zeros[i][(applied_score == 0)] = 0 weights[i] += 1 score_array[i] += applied_score targets = [block.target for block in self.blocks] @@ -194,20 +195,20 @@ def weighted_score_array(self, time_resolution=1*u.minute): skip_global.append(i) global_score = constraint(self.observer, targets, times) if constraint.boolean_constraint: - zeros &= 1-global_score + constraint_zeros &= 1 - global_score elif constraint.weight: weight = constraint.weight for i, score in enumerate(global_score): if i not in skip_global: weights[i] += weight score_array[i] += score*weight - zeros[i][(score == 0)] = 0 + constraint_zeros[i][(score == 0)] = 0 else: for i, score in enumerate(global_score): if i not in skip_global: weights[i] += 1 score_array[i] += score - zeros[i][(score == 0)] = 0 + constraint_zeros[i][(score == 0)] = 0 for i, scores in enumerate(score_array): if weights[i]: @@ -215,8 +216,8 @@ def weighted_score_array(self, time_resolution=1*u.minute): else: # if no weight, then nothing was added to the score_array # just use the zeros (squaring 0 and 1 gives 0 and 1) - scores += zeros[i] - score_array *= zeros + scores += constraint_zeros[i] + score_array *= constraint_zeros return score_array @classmethod diff --git a/astroplan/tests/test_scheduling.py b/astroplan/tests/test_scheduling.py index 2fb5c630..a825efe7 100644 --- a/astroplan/tests/test_scheduling.py +++ b/astroplan/tests/test_scheduling.py @@ -280,7 +280,7 @@ def test_weighted_scorer(): scores = scorer.weighted_score_array(time_resolution=20 * u.minute) # due to float multiplication and division c and scores are not exactly equal assert np.array_equal(np.round(c, 10), np.round(scores, 10)) - ''' + constraint2 = MoonIlluminationConstraint(max=.6, boolean_constraint=False) constraint2.weight = .7 c2 = constraint2(apo, [vega, rigel], times) @@ -289,6 +289,6 @@ def test_weighted_scorer(): scorer = Scorer.from_start_end([block, block2], apo, Time('2016-02-06 00:00'), Time('2016-02-06 08:00')) scores = scorer.weighted_score_array(time_resolution=20 * u.minute) - assert all(scores[0] - (c[0] * .8 + c2[0] * .7)/1.5 + assert all(scores[0] - (c[0] * .8 + c2[0] * .7)/1.5) np.array_equal(np.round(scores[0], 10), np.round((c[0] * .8 + c2[0] * .7)/1.5, 10)) - assert np.array_equal(np.round(scores[1], 10), np.round(c[1], 10))''' + assert np.array_equal(np.round(scores[1], 10), np.round(c[1], 10)) From 084cef5028af8e8247b9c7073d0f8728dbe91b30 Mon Sep 17 00:00:00 2001 From: kvyh Date: Mon, 10 Oct 2016 12:13:47 -0700 Subject: [PATCH 4/4] added a few comments and fixed global constraint_zeros --- astroplan/scheduling.py | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/astroplan/scheduling.py b/astroplan/scheduling.py index 6bc2471d..d58ecdb6 100644 --- a/astroplan/scheduling.py +++ b/astroplan/scheduling.py @@ -195,7 +195,9 @@ def weighted_score_array(self, time_resolution=1*u.minute): skip_global.append(i) global_score = constraint(self.observer, targets, times) if constraint.boolean_constraint: - constraint_zeros &= 1 - global_score + # This should apply to every block (if the global fails, then + # the local should have failed anyway) + constraint_zeros &= global_score elif constraint.weight: weight = constraint.weight for i, score in enumerate(global_score): @@ -212,11 +214,13 @@ def weighted_score_array(self, time_resolution=1*u.minute): for i, scores in enumerate(score_array): if weights[i]: - scores *= 1/weights[i] + scores *= 1/float(weights[i]) else: # if no weight, then nothing was added to the score_array # just use the zeros (squaring 0 and 1 gives 0 and 1) scores += constraint_zeros[i] + # considering the else above, score_array would be constraint_zeros^2 + # which is fine since all of its values should be 0 or 1 score_array *= constraint_zeros return score_array