-
Notifications
You must be signed in to change notification settings - Fork 40
Weather Data Sync-out #148
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Large diffs are not rendered by default.
Large diffs are not rendered by default.
Large diffs are not rendered by default.
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,12 @@ | ||
| { | ||
| "name": "Mountain View Moffett Field Naval Air Station", | ||
| "city": "Sunnyvale", | ||
| "country": "US", | ||
| "state": "California", | ||
| "locality": "Mountain View", | ||
| "postal": "94043", | ||
| "lat": "37 24 35.000", | ||
| "lng": "-122 02 56.000", | ||
| "timezone": "America/Los_Angeles", | ||
| "elevation": 11 | ||
| } |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -2,6 +2,7 @@ | |
|
|
||
| import abc | ||
| import math | ||
| import os | ||
| from typing import Final, Mapping, Optional, Sequence, Tuple | ||
|
|
||
| import gin | ||
|
|
@@ -19,6 +20,15 @@ | |
| _MAX_RADIANS: Final[float] = 3.0 * math.pi / 2.0 | ||
| _EPOCH: Final[pd.Timestamp] = pd.Timestamp('1970-01-01', tz='UTC') | ||
|
|
||
| WEATHER_CSV_FILEPATH: Final[str] = os.path.join( | ||
| os.path.dirname(__file__), | ||
| '..', | ||
| 'configs', | ||
| 'resources', | ||
| 'sb1', | ||
| 'local_weather_moffett_field_20230701_20231122.csv', | ||
| ) | ||
|
|
||
|
|
||
| @gin.configurable | ||
| class BaseWeatherController(metaclass=abc.ABCMeta): | ||
|
|
@@ -28,6 +38,8 @@ class BaseWeatherController(metaclass=abc.ABCMeta): | |
| def get_current_temp(self, timestamp: pd.Timestamp) -> float: | ||
| """Gets outside temp at specified timestamp.""" | ||
|
|
||
| # SHOULD THIS BASE CLASS IMPLEMENT get_air_convection_coefficient AS WELL? | ||
|
|
||
|
|
||
| @gin.configurable | ||
| class WeatherController(BaseWeatherController): | ||
|
|
@@ -149,55 +161,138 @@ def get_outside_air_temp(observation_response): | |
|
|
||
|
|
||
| @gin.configurable | ||
| class ReplayWeatherController: | ||
| class ReplayWeatherController(BaseWeatherController): | ||
| """Weather controller that interplolates real weather from past observations. | ||
|
|
||
| Attributes: | ||
| local_weather_path: Path to local weather file. | ||
| local_weather_path: Path to local weather CSV file. | ||
| weather_df: Pandas dataframe of historical weather data. | ||
| convection_coefficient: Air convection coefficient (W/m2/K). | ||
| humidity_column: Column name of the humidity in the weather CSV file. | ||
| """ | ||
|
|
||
| def __init__( | ||
| self, | ||
| local_weather_path: str, | ||
| local_weather_path: str = WEATHER_CSV_FILEPATH, | ||
| convection_coefficient: float = 12.0, | ||
| humidity_column: str = 'Humidity', | ||
| ): | ||
| self._weather_data = pd.read_csv(local_weather_path) | ||
| self._weather_data['Time'] = [ | ||
| pd.Timestamp(t, tz='UTC') for t in self._weather_data['Time'] | ||
| ] | ||
| self._weather_data.index = [ | ||
| (t - _EPOCH).total_seconds() for t in self._weather_data['Time'] | ||
| ] | ||
| self.local_weather_path = local_weather_path | ||
| self.weather_df = self.read_weather_csv(self.local_weather_path) | ||
| self.convection_coefficient = convection_coefficient | ||
| self.humidity_column = humidity_column | ||
|
|
||
| def get_current_temp(self, timestamp: pd.Timestamp) -> float: | ||
| """Returns current temperature in K. | ||
| @property | ||
| def csv_filepath(self) -> str: | ||
| """Alias for the local weather CSV file path.""" | ||
| return self.local_weather_path | ||
|
|
||
| def read_weather_csv(self, csv_filepath: str) -> pd.DataFrame: | ||
| """Loads time series weather data from the specified CSV file. | ||
|
|
||
| The CSV file is expected to have at least the following columns: | ||
|
|
||
| + `Time`: the time, as a string, in the format: `%Y%m%d-%H%M` | ||
| (e.g. `20230701-0000`). Assumed to be in UTC. | ||
| + `TempF`: the temperature in Fahrenheit at the specified time. | ||
| + `Humidity`: the relative humidity in percent at the specified time | ||
| (0 to 100). | ||
|
|
||
| Coerces the times to UTC. Updates the index to be seconds since epoch. | ||
|
|
||
| Args: | ||
| csv_filepath: Path to local weather CSV file. | ||
|
|
||
| Returns: | ||
| Pandas dataframe of weather data. | ||
| """ | ||
| df = pd.read_csv(csv_filepath) | ||
| df = df.drop(columns=['Unnamed: 0'], errors='ignore') | ||
|
|
||
| df['Time'] = pd.to_datetime(df['Time'], utc=True) | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Consider cleaning the data here. See #144 |
||
|
|
||
| df.index = (df['Time'] - _EPOCH).dt.total_seconds() | ||
| df.index.name = 'SecondsSinceEpoch' | ||
|
|
||
| return df | ||
|
|
||
| @property | ||
| def min_time(self) -> pd.Timestamp: | ||
| """Earliest timestamp in the weather data.""" | ||
| return min(self.weather_df['Time']) | ||
|
|
||
| @property | ||
| def max_time(self) -> pd.Timestamp: | ||
| """Latest timestamp in the weather data.""" | ||
| return max(self.weather_df['Time']) | ||
|
|
||
| @property | ||
| def times_in_seconds(self) -> pd.Index: | ||
| """Returns the timestamps of the weather data, as seconds since epoch.""" | ||
| return self.weather_df.index | ||
|
|
||
| @property | ||
| def temps_f(self) -> pd.Series: | ||
| """Returns the temperatures in Fahrenheit of the weather data.""" | ||
| return self.weather_df['TempF'] | ||
|
|
||
| @property | ||
| def humidities(self) -> pd.Series: | ||
| """Returns the humidities of the weather data.""" | ||
| return self.weather_df[self.humidity_column] | ||
|
|
||
| def _get_interpolated_value( | ||
| self, timestamp: pd.Timestamp, values: pd.Series | ||
|
Collaborator
Author
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. We are currently passing the list of values to be interpolated, but this requires a separate property for each column of values (e.g. If possible, let's implement a more flexible interface where we can just pass the column name (instead of the values) into the |
||
| ) -> float: | ||
| """Helper to get interpolated value from a given series. | ||
|
|
||
| The timestamp need not exactly appear in the weather data, but should be | ||
| within the range of the data. | ||
| If there is no exact match, linear interpolation is used to estimate the | ||
| temperature between the nearest timestamps. | ||
|
|
||
| Args: | ||
| timestamp: Pandas timestamp to get temperature for interpolation. | ||
| timestamp: Pandas timestamp to get temperature for interpolation. If the | ||
| timestamp is timezone aware, it will be converted to UTC. If the | ||
| timestamp is timezone naive, it will be localized to UTC. This allows | ||
| for accurate comparisons against the min and max timestamps, as well as | ||
| the epoch, which are always timezone aware (in UTC). | ||
| values: Pandas series to interpolate from. | ||
|
|
||
| Returns: | ||
| The interpolated value from the series at the given timestamp. | ||
| """ | ||
| timestamp = timestamp.tz_convert('UTC') | ||
| min_time = min(self._weather_data['Time']) | ||
| if timestamp < min_time: | ||
| # convert timestamp to UTC to enable proper comparisons: | ||
| if timestamp.tzname() is not None: | ||
| # timestamp is timezone aware, unable to localize, so convert to UTC: | ||
| timestamp = timestamp.tz_convert('UTC') | ||
| else: | ||
| # timestamp is timezone naive, unable to convert, so localize to UTC: | ||
| timestamp = timestamp.tz_localize('UTC') | ||
|
|
||
| if timestamp < self.min_time: | ||
| raise ValueError( | ||
| f'Attempting to get weather data at {timestamp}, before the latest' | ||
| f' timestamp {min_time}.' | ||
| f'Timestamp not in range. Timestamp {timestamp} is before the' | ||
| f' earliest timestamp {self.min_time}.' | ||
| ) | ||
| max_time = max(self._weather_data['Time']) | ||
| if timestamp > max_time: | ||
|
|
||
| if timestamp > self.max_time: | ||
| raise ValueError( | ||
| f'Attempting to get weather data at {timestamp}, after the latest' | ||
| f' timestamp {max_time}.' | ||
| f'Timestamp not in range. Timestamp {timestamp} is after the' | ||
| f' latest timestamp {self.max_time}.' | ||
| ) | ||
|
|
||
| times = np.array(self._weather_data.index) | ||
| target_timestamp = (timestamp - _EPOCH).total_seconds() | ||
| temps = self._weather_data['TempF'] | ||
| temp_f = np.interp(target_timestamp, times, temps) | ||
| return utils.fahrenheit_to_kelvin(temp_f) | ||
| time_in_seconds = (timestamp - _EPOCH).total_seconds() | ||
| return np.interp(time_in_seconds, self.times_in_seconds, values) | ||
|
|
||
| def get_current_temp(self, timestamp: pd.Timestamp) -> float: | ||
| """For a given timestamp, returns the current temperature in Kelvin.""" | ||
| return utils.fahrenheit_to_kelvin( | ||
| self._get_interpolated_value(timestamp, self.temps_f) | ||
| ) | ||
|
|
||
| def get_current_humidity(self, timestamp: pd.Timestamp) -> float: | ||
| """For a given timestamp, returns the current humidity level in percent.""" | ||
| return self._get_interpolated_value(timestamp, self.humidities) | ||
|
|
||
| # pylint: disable=unused-argument | ||
| def get_air_convection_coefficient(self, timestamp: pd.Timestamp) -> float: | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Contains data up until 8/25. We need to update the data to include the full year.
We have a pipeline for updating this data, so this issue should be handled internally by Google staff.