diff --git a/.gitignore b/.gitignore index cedeadf..f6db003 100644 --- a/.gitignore +++ b/.gitignore @@ -1,5 +1,6 @@ # Byte-compiled / optimized / DLL files __pycache__/ +*.DS_Store *.py[cod] *$py.class @@ -160,7 +161,4 @@ cython_debug/ #.idea/ # Repo specific files (testing) -test/test_files/empty_config.yaml -test/test_files/test_config.yaml -test/test_files/test_saving/survey_2023_05_19_12_57_23_4222/run_config.yaml -test/test_files/test_saving/survey_2023_05_19_12_57_23_4698/survey_results.json +test/test_files/* diff --git a/docs/source/cummulative_survey.rst b/docs/source/cummulative_survey.rst new file mode 100644 index 0000000..08a4ccb --- /dev/null +++ b/docs/source/cummulative_survey.rst @@ -0,0 +1,10 @@ +Cummulative Surveys +======= +.. autoclass:: telescope_positioning_simulation.Survey.CummulativeSurvey + :members: + +.. autoclass:: telescope_positioning_simulation.Survey.UniformSurvey + :members: + +.. autoclass:: telescope_positioning_simulation.Survey.LowVisiblitySurvey + :members: \ No newline at end of file diff --git a/telescope_positioning_simulation/Survey/__init__.py b/telescope_positioning_simulation/Survey/__init__.py index 37e9f7d..5c50307 100644 --- a/telescope_positioning_simulation/Survey/__init__.py +++ b/telescope_positioning_simulation/Survey/__init__.py @@ -2,3 +2,7 @@ from telescope_positioning_simulation.Survey.observation_variables import ( ObservationVariables, ) +from telescope_positioning_simulation.Survey.cummulative_survey import ( + UniformSurvey, + LowVisiblitySurvey, +) diff --git a/telescope_positioning_simulation/Survey/cummulative_survey.py b/telescope_positioning_simulation/Survey/cummulative_survey.py new file mode 100644 index 0000000..726445b --- /dev/null +++ b/telescope_positioning_simulation/Survey/cummulative_survey.py @@ -0,0 +1,217 @@ +from telescope_positioning_simulation.Survey.survey import Survey +import numpy as np +import pandas as pd +import json + + +class CummulativeSurvey(Survey): + def __init__( + self, observatory_config: dict, survey_config: dict, *args, **kwargs + ) -> None: + """ + Abstract class for a survey that evaluated across multiple steps + + Args: + observatory_config (dict): _description_ + survey_config (dict): _description_ + """ + super().__init__(observatory_config, survey_config) + self.all_steps = pd.DataFrame() + + def reset(self): + """Return the survey to its initial condition, wipe out all the original steps""" + self.all_steps = pd.DataFrame() + return super().reset() + + def cummulative_reward(self): + """Reward that uses the 'all_steps' class parameter.""" + raise NotImplemented + + def step(self, action: dict): + """ + Move the observator forward with one action and add the reward and stop condition to the returned observation. + Reward is defined with 'cummulative_reward' + + Args: + action (dict): Dictionary containing "time" (array in units Mean Julian Date) "location"(dict with ra, decl, in degrees as arrays) (optional), "band" (str of the represention of the optical filter) (optional) + + Returns: + Tuple : observation (dict, containing survey_config["variables"], vality, Time (in mjd)), reward (array), stop (array), log (dictionary) + """ + observation, reward, stop, log = super().step(action) + observation_pd = {key: observation[key].ravel() for key in observation.keys()} + + observation_pd = pd.DataFrame(observation_pd) + observation_pd["action"] = str( + {"location": action["location"], "band": self.observator.band} + ) + observation_pd["band"] = self.observator.band + observation_pd["location"] = str(action["location"]) + observation_pd["reward"] = reward + + self.all_steps = self.all_steps.append(observation_pd) + + reward = self.cummulative_reward() + return observation, reward, stop, log + + +class UniformSurvey(CummulativeSurvey): + """ + A child survey that instead of evaluating the schedule at every step, evaluates it for a full schedule. + This survey requires a threshold is reached for each observation before it starts recording any sort of reward. + + Args: + obseravtory_config (dict): Setup parameters for Survey.ObservationVariables, the telescope configuration, as read by IO.ReadConfig + survey_config (dict): Parameters for the survey, including the stopping conditions, the validity conditions, the variables to collect, as read by IO.ReadConfig + threshold (float): Threshold the survey must pass to have its quality counted towards the total reward + uniform (str): ["site", "quality"] - If measuring the uniformity of the number of times each site has been visited, or the uniformity of the quality of observations + """ + + def __init__( + self, + observatory_config: dict, + survey_config: dict, + threshold: float = 1.0, + uniform: str = "site", + ) -> None: + super().__init__(observatory_config, survey_config) + + self.threshold = threshold + reward_function = { + "site": self.site_reward, + "quality": self.quality_reward, + } + assert uniform in reward_function + + self.reward_function = reward_function[uniform] + + def site_reward(self): + """ + Calculate the reward for all sites, assuming it is required for a threshold on the number of times each site is visited. + Follows the equation: + + $$R_{S_{n}} = \frac{1}{||T||* Var(\\{||(s_i)||: s_i \\in S_n\\})}* \\Sigma^{S_{n}}_{i=0} \begin{cases} \\Sigma^{t_{n}}_{j=0} \tau_{eff}(s_{i,j}) & ||s_i|| \\geq N \\ 0 & ||s_i|| < N \\ \\end{cases}$$ + + Returns: + float: reward based on if number of sites a site was visited reached a threshold + """ + counts = self.all_steps["action"].value_counts() + reward_scale = 1 / (len(self.all_steps) * np.var(counts)) + + current_steps = self.all_steps.copy() + # Replace reward if it doesn't make it pass the threshold + current_steps.loc[ + self.all_steps["action"].isin(counts.index[counts < self.threshold]), + "reward", + ] = 0 + reward_sum = ( + current_steps.groupby(["mjd", "action", "band"])["reward"].sum().sum() + ) + + return reward_scale * reward_sum + + def quality_reward(self): + """Cummulitive reward based on if a per site reward threshold is reached. + Defined as: + + $$R_{S_{n}} = \frac{1}{||T||* Var(\\{\tau_{eff}(s_i): s_i \\in S_n)\\}}* \\Sigma^{t_{n}}_{i=0} \begin{cases} \tau_{eff}(s_i) & T_{eff}(s_i) \\geq \theta \\ 0 & \tau_{eff}(s_i) < \theta \\ \\end{cases}$$ + + Returns: + float: reward based on if a threshold in reward per site is reached. + """ + reward_scale = 1 / (len(self.all_steps)) * np.var(self.all_steps["reward"]) + + current_steps = self.all_steps.copy() + # Replace reward if it doesn't make it pass the threshold + current_steps.loc[current_steps["reward"] < self.threshold, "reward"] = 0 + reward_sum = current_steps["reward"].sum() + return reward_scale * reward_sum + + def cummulative_reward(self, *args, **kwargs): + """ + Quality thresholded or visit thresholded reward + + Returns: + float: all sites reward + """ + if len(self.all_steps) != 0: + reward = self.reward_function() + reward = reward if not (pd.isnull(reward) or reward == -np.inf) else 0 + + return reward + else: + return 0 + + +class LowVisiblitySurvey(CummulativeSurvey): + def __init__( + self, + observatory_config: dict, + survey_config: dict, + required_sites: list = [], + other_site_weight: float = 0.6, + time_tolerance: float = 0.01388, + ) -> None: + """ + Survey that evaluates how many times a "required site" has been visited, and defines the reward of a site based on this. + + Args: + obseravtory_config (dict): Setup parameters for Survey.ObservationVariables, the telescope configuration, as read by IO.ReadConfig + survey_config (dict): Parameters for the survey, including the stopping conditions, the validity conditions, the variables to collect, as read by IO.ReadConfig + required_sites (list, optional): Sites that must be visited to achieve reward. Same format as an action. Defaults to []. + other_site_weight (float, optional): Weighting factor applied to reward gained from visiting sites not in the "required_sites" list. Defaults to 0.6. + time_tolerance (float, optional): Tolerance given to a required time. Defaults to 0.01388 (seconds, 20 minutes). + """ + super().__init__(observatory_config, survey_config) + + self.required_sites = required_sites + self.time_tolerance = time_tolerance + self.weight = other_site_weight + + def sites_hit(self): + """Count the number of times a required site was visited. Does not follow a thresholding rule. + + Returns: + int: times the requires sites were visited. + """ + + # TODO do this with sets and arrays instead of a loop + hit_counter = 0 + for site in self.required_sites: + + subset = self.all_steps.copy() + if "time" in site.keys(): + subset = subset[ + (subset["mjd"] < site["time"][0] + self.time_tolerance) + & (subset["mjd"] > site["time"][0] - self.time_tolerance) + ] + + if "band" in site.keys(): + subset = subset[subset["band"] == site["band"]] + + subset = subset[subset["location"] == str(site["location"])] + hit_counter += len(subset) + + return hit_counter + + def cummulative_reward(self): + """ + Reward for all visited sites as defined as + $$ R_{s_{n}} = \frac{1}{||T||} (||\\{s_i: s_i \\in S_{interest}\\}|| + \\lambda \\Sigma^{t_{n}}_{t=0} T_{eff_t}(s_t)) $$ + + Returns: + float: reward calculted as a result of all current sites visited in the schedule + """ + if len(self.all_steps) != 0: + + reward_scale = 1 / len(self.all_steps) + weighted_term = self.weight * self.all_steps["reward"].sum() + number_of_interest_hit = self.sites_hit() + + reward = reward_scale * (weighted_term + number_of_interest_hit) + reward = reward if not (pd.isnull(reward) or reward == -np.inf) else 0 + + return reward + + else: + return 0.0 diff --git a/telescope_positioning_simulation/Survey/survey.py b/telescope_positioning_simulation/Survey/survey.py index 9e1c899..8512b82 100644 --- a/telescope_positioning_simulation/Survey/survey.py +++ b/telescope_positioning_simulation/Survey/survey.py @@ -139,7 +139,9 @@ def step(self, action: dict): Returns: Tuple : observation (dict, containing survey_config["variables"], vality, Time (in mjd)), reward (array), stop (array), log (dictionary) """ - print(action) + if "time" not in action: + action["time"] = np.array(self.time) + self.observator.update(**action) self.time = self.observator.time.mjd.mean() observation = self._observation_calculation() @@ -164,6 +166,12 @@ def _observation_calculation(self): return observation def __call__(self): + """ + Run the survey with the initial location until the stopping condition is met, return the completed survey + + Returns: + dict: Evaluated survey in the form of time:{"variable_name":[variable_value]} + """ stop = False results = {} while not stop: diff --git a/test/test_cummaltive_survey.py b/test/test_cummaltive_survey.py new file mode 100644 index 0000000..3aa31d6 --- /dev/null +++ b/test/test_cummaltive_survey.py @@ -0,0 +1,189 @@ +import pytest + +from telescope_positioning_simulation.Survey import UniformSurvey, LowVisiblitySurvey +from telescope_positioning_simulation.IO import ReadConfig + +action = {"location": {"ra": [0], "decl": [0]}, "band": "g"} +action_2 = {"location": {"ra": [1], "decl": [1]}, "band": "g"} + + +def test_uniform_site(): + obs_config = ReadConfig(survey=False)() + obs_config["location"] = {"ra": [0], "decl": [0]} + obs_config["start_time"] = 59946 + uniform_survey = UniformSurvey( + observatory_config=obs_config, survey_config=ReadConfig(survey=True)() + ) + assert len(uniform_survey.all_steps) == 0 + assert uniform_survey.cummulative_reward() == 0 + + uniform_survey.step(action) + assert uniform_survey.cummulative_reward() == 0 + assert len(uniform_survey.all_steps) == 1 + + uniform_survey.step(action_2) + assert len(uniform_survey.all_steps) == 2 + assert uniform_survey.cummulative_reward() == 0 + + +def test_uniform_quality(): + + obs_config = ReadConfig(survey=False)() + obs_config["location"] = {"ra": [0], "decl": [0]} + obs_config["start_time"] = 59946 + + uniform_survey = UniformSurvey( + observatory_config=obs_config, + survey_config=ReadConfig(survey=True)(), + uniform="quality", + ) + assert len(uniform_survey.all_steps) == 0 + assert uniform_survey.cummulative_reward() == 0 + + uniform_survey.step(action) + assert uniform_survey.cummulative_reward() == 0 + assert len(uniform_survey.all_steps) == 1 + + uniform_survey.step(action_2) + assert len(uniform_survey.all_steps) == 2 + assert uniform_survey.cummulative_reward() == 0 + + +# Todo - schedule that passes the conditions in the defaults + + +def test_lowvis_hit_required_sites(): + required_sites = [ + {"location": {"ra": [ra], "decl": [decl]}} for ra, decl in zip([0, 10], [0, 10]) + ] + expected_reward = 1 + obs_config = ReadConfig(survey=False)() + obs_config["location"] = {"ra": [0], "decl": [0]} + survey_config = ReadConfig(survey=True)() + survey_config["invalid_penality"] = 0 + + survey = LowVisiblitySurvey( + observatory_config=obs_config, + survey_config=survey_config, + required_sites=required_sites, + other_site_weight=0, + ) + actions = [ + {"location": {"ra": [0], "decl": [0]}, "band": "g"}, + {"location": {"ra": [10], "decl": [10]}, "band": "g"}, + ] + for action in actions: + survey.step(action) + + print(survey.all_steps) + assert survey.cummulative_reward() >= expected_reward + + +def test_lowvis_hit_required_sites_correct_time(): + required_sites = [ + {"time": [time], "location": {"ra": [ra], "decl": [decl]}} + for ra, decl, time in zip([0, 10], [0, 10], [59946.08, 59946.1]) + ] + + expected_reward = 1 + obs_config = ReadConfig(survey=False)() + obs_config["location"] = {"ra": [0], "decl": [0]} + survey_config = ReadConfig(survey=True)() + survey_config["invalid_penality"] = 0 + + survey = LowVisiblitySurvey( + observatory_config=obs_config, + survey_config=survey_config, + required_sites=required_sites, + other_site_weight=0, + ) + for action in required_sites: + survey.step(action) + + assert survey.cummulative_reward() >= expected_reward + + +def test_lowvis_hit_required_sites_incorrect_time(): + required_sites = [ + {"time": [time], "location": {"ra": [ra], "decl": [decl]}} + for ra, decl, time in zip([0, 10], [0, 10], [59946.08, 59946.1]) + ] + + expected_reward = 1 + obs_config = ReadConfig(survey=False)() + obs_config["location"] = {"ra": [0], "decl": [0]} + survey_config = ReadConfig(survey=True)() + survey_config["invalid_penality"] = 0 + + survey = LowVisiblitySurvey( + observatory_config=obs_config, + survey_config=survey_config, + required_sites=required_sites, + other_site_weight=0, + ) + actions = [ + {"time": [time], "location": {"ra": [ra], "decl": [decl]}} + for ra, decl, time in zip([0, 10], [0, 10], [59948, 59948.1]) + ] + for action in actions: + survey.step(action) + + assert survey.cummulative_reward() < expected_reward + + +def test_lowvis_hit_required_sites_incorrect_band(): + required_sites = [ + {"band": band, "location": {"ra": [ra], "decl": [decl]}} + for ra, decl, band in zip([0, 10], [0, 10], ["g", "g"]) + ] + + expected_reward = 1 + obs_config = ReadConfig(survey=False)() + obs_config["location"] = {"ra": [0], "decl": [0]} + survey_config = ReadConfig(survey=True)() + survey_config["invalid_penality"] = 0 + + survey = LowVisiblitySurvey( + observatory_config=obs_config, + survey_config=survey_config, + required_sites=required_sites, + other_site_weight=0, + ) + actions = [ + {"band": band, "location": {"ra": [ra], "decl": [decl]}} + for ra, decl, band in zip([0, 10], [0, 10], ["b", "v"]) + ] + + for action in actions: + survey.step(action) + + assert survey.cummulative_reward() < expected_reward + + +def test_lowvis_incorrect_sites(): + required_sites = [ + {"location": {"ra": [ra], "decl": [decl]}} + for ra, decl in zip([15, 20], [10, 20]) + ] + + expected_reward = 1 + obs_config = ReadConfig(survey=False)() + obs_config["location"] = {"ra": [0], "decl": [0]} + + survey_config = ReadConfig(survey=True)() + survey_config["invalid_penality"] = 0 + + survey = LowVisiblitySurvey( + observatory_config=obs_config, + survey_config=survey_config, + required_sites=required_sites, + other_site_weight=0, + ) + actions = [ + {"location": {"ra": [ra], "decl": [decl]}} for ra, decl in zip([0, 10], [0, 10]) + ] + + for action in actions: + survey.step(action) + + assert survey.cummulative_reward() < expected_reward