class MarketType(db.Model): """Describing market types for our purposes. TODO: Add useful attributes like frequency (e.g. 1H) and the meaning of units (e.g. Mwh). """ name = db.Column(db.String(80), primary_key=True) display_name = db.Column(db.String(80), default="", unique=True) daily_seasonality = db.Column(db.Boolean(), nullable=False, default=False) weekly_seasonality = db.Column(db.Boolean(), nullable=False, default=False) yearly_seasonality = db.Column(db.Boolean(), nullable=False, default=False) def __init__(self, **kwargs): super(MarketType, self).__init__(**kwargs) self.name = self.name.replace(" ", "_").lower() if "display_name" not in kwargs: self.display_name = humanize(self.name) @property def preconditions(self) -> Dict[str, bool]: """Assumptions about the time series data set, such as normality and stationarity For now, this is usable input for Prophet (see init), but it might evolve or go away.""" return dict( daily_seasonality=self.daily_seasonality, weekly_seasonality=self.weekly_seasonality, yearly_seasonality=self.yearly_seasonality, ) def __repr__(self): return "<MarketType %r>" % self.name
class DataSource(db.Model, tb.BeliefSourceDBMixin): """Each data source is a data-providing entity.""" __tablename__ = "data_source" # The type of data source (e.g. user, forecasting script or scheduling script) type = db.Column(db.String(80), default="") # The id of the source (can link e.g. to fm_user table) user_id = db.Column( db.Integer, db.ForeignKey("fm_user.id"), nullable=True, unique=True ) user = db.relationship("User", backref=db.backref("data_source", lazy=True)) def __init__( self, name: Optional[str] = None, type: Optional[str] = None, user: Optional[User] = None, **kwargs, ): if user is not None: name = user.username type = "user" self.user_id = user.id self.type = type tb.BeliefSourceDBMixin.__init__(self, name=name) db.Model.__init__(self, **kwargs) @property def label(self): """ Human-readable label (preferably not starting with a capital letter so it can be used in a sentence). """ if self.type == "user": return f"data entered by user {self.user.username}" # todo: give users a display name elif self.type == "forecasting script": return f"forecast by {self.name}" # todo: give DataSource an optional db column to persist versioned models separately to the name of the data source? elif self.type == "scheduling script": return f"schedule by {self.name}" elif self.type == "crawling script": return f"data retrieved from {self.name}" elif self.type == "demo script": return f"demo data entered by {self.name}" else: return f"data from {self.name}" def __repr__(self): return "<Data source %r (%s)>" % (self.id, self.label)
class WeatherSensorType(db.Model): """ " TODO: Add useful attributes like ...? """ name = db.Column(db.String(80), primary_key=True) display_name = db.Column(db.String(80), default="", unique=True) daily_seasonality = True weekly_seasonality = False yearly_seasonality = True def __init__(self, **kwargs): super(WeatherSensorType, self).__init__(**kwargs) self.name = self.name.replace(" ", "_").lower() if "display_name" not in kwargs: self.display_name = humanize(self.name) def __repr__(self): return "<WeatherSensorType %r>" % self.name
class Market(db.Model, tb.SensorDBMixin): """Each market is a pricing service.""" name = db.Column(db.String(80), unique=True) display_name = db.Column(db.String(80), default="", unique=True) market_type_name = db.Column( db.String(80), db.ForeignKey("market_type.name"), nullable=False ) def __init__(self, **kwargs): # Set default knowledge horizon function for an economic sensor if "knowledge_horizon_fnc" not in kwargs: kwargs["knowledge_horizon_fnc"] = knowledge_horizons.ex_ante.__name__ if "knowledge_horizon_par" not in kwargs: kwargs["knowledge_horizon_par"] = { knowledge_horizons.ex_ante.__code__.co_varnames[1]: "PT0H" } super(Market, self).__init__(**kwargs) self.name = self.name.replace(" ", "_").lower() if "display_name" not in kwargs: self.display_name = humanize(self.name) @property def price_unit(self) -> str: """Return the 'unit' property of the generic asset, just with a more insightful name.""" return self.unit market_type = db.relationship( "MarketType", backref=db.backref("markets", lazy=True) ) def __repr__(self): return "<Market %s:%r (%r) res.: %s>" % ( self.id, self.name, self.market_type_name, self.event_resolution, ) def to_dict(self) -> Dict[str, str]: return dict(name=self.name, market_type=self.market_type.name)
class LatestTaskRun(db.Model): """ " Log the (latest) running of a task. This is intended to be used for live monitoring. For a full analysis, there are log files. """ name = db.Column(db.String(80), primary_key=True) datetime = db.Column(db.DateTime(timezone=True), default=datetime.utcnow().replace(tzinfo=pytz.utc)) status = db.Column(db.Boolean, default=True) def __repr__(self): return "<TaskRun [%s] at %s (status: %s)>" % ( self.name, self.datetime, { True: "ok", False: "err" }[self.status], )
class Price(TimedValue, db.Model): """ All prices are stored in one slim table. TODO: datetime objects take up most of the space (12 bytes each)). One way out is to normalise them out to a table. """ market_id = db.Column( db.Integer(), db.ForeignKey("market.id"), primary_key=True, index=True ) market = db.relationship("Market", backref=db.backref("prices", lazy=True)) @classmethod def make_query(cls, **kwargs) -> Query: """Construct the database query.""" return super().make_query(asset_class=Market, **kwargs) def __init__(self, **kwargs): super(Price, self).__init__(**kwargs)
class Weather(TimedValue, db.Model): """ All weather measurements are stored in one slim table. TODO: datetime objects take up most of the space (12 bytes each)). One way out is to normalise them out to a table. """ sensor_id = db.Column(db.Integer(), db.ForeignKey("weather_sensor.id"), primary_key=True, index=True) sensor = db.relationship("WeatherSensor", backref=db.backref("weather", lazy=True)) @classmethod def make_query(cls, **kwargs) -> Query: """Construct the database query.""" return super().make_query(asset_class=WeatherSensor, **kwargs) def __init__(self, **kwargs): super(Weather, self).__init__(**kwargs)
class WeatherSensor(db.Model, tb.SensorDBMixin): """A weather sensor has a location on Earth and measures weather values of a certain weather sensor type, such as temperature, wind speed and radiation.""" name = db.Column(db.String(80), unique=True) display_name = db.Column(db.String(80), default="", unique=False) weather_sensor_type_name = db.Column( db.String(80), db.ForeignKey("weather_sensor_type.name"), nullable=False) # latitude is the North/South coordinate latitude = db.Column(db.Float, nullable=False) # longitude is the East/West coordinate longitude = db.Column(db.Float, nullable=False) # only one sensor of any type is needed at one location __table_args__ = (UniqueConstraint( "weather_sensor_type_name", "latitude", "longitude", name="weather_sensor_type_name_latitude_longitude_key", ), ) def __init__(self, **kwargs): super(WeatherSensor, self).__init__(**kwargs) self.name = self.name.replace(" ", "_").lower() @property def weather_unit(self) -> float: """Return the 'unit' property of the generic asset, just with a more insightful name.""" return self.unit @property def location(self) -> Tuple[float, float]: return self.latitude, self.longitude @hybrid_property def cos_rad_lat(self): return math.cos(math.radians(self.latitude)) @hybrid_property def sin_rad_lat(self): return math.sin(math.radians(self.latitude)) @hybrid_property def rad_lng(self): return math.radians(self.longitude) @hybrid_method def great_circle_distance(self, **kwargs): """Query great circle distance (in km). Can be called with an object that has latitude and longitude properties, for example: great_circle_distance(object=asset) Can also be called with latitude and longitude parameters, for example: great_circle_distance(latitude=32, longitude=54) great_circle_distance(lat=32, lng=54) """ r = 6371 # Radius of Earth in kilometers other_latitude, other_longitude = parse_lat_lng(kwargs) if other_latitude is None or other_longitude is None: return None other_cos_rad_lat = math.cos(math.radians(other_latitude)) other_sin_rad_lat = math.sin(math.radians(other_latitude)) other_rad_lng = math.radians(other_longitude) return (math.acos(self.cos_rad_lat * other_cos_rad_lat * math.cos(self.rad_lng - other_rad_lng) + self.sin_rad_lat * other_sin_rad_lat) * r) @great_circle_distance.expression def great_circle_distance(self, **kwargs): """Query great circle distance (unclear if in km or in miles). Can be called with an object that has latitude and longitude properties, for example: great_circle_distance(object=asset) Can also be called with latitude and longitude parameters, for example: great_circle_distance(latitude=32, longitude=54) great_circle_distance(lat=32, lng=54) """ other_latitude, other_longitude = parse_lat_lng(kwargs) if other_latitude is None or other_longitude is None: return None return func.earth_distance( func.ll_to_earth(self.latitude, self.longitude), func.ll_to_earth(other_latitude, other_longitude), ) sensor_type = db.relationship("WeatherSensorType", backref=db.backref("sensors", lazy=True)) def __repr__(self): return "<WeatherSensor %s:%r (%r), res.:%s>" % ( self.id, self.name, self.weather_sensor_type_name, self.event_resolution, ) def to_dict(self) -> Dict[str, str]: return dict(name=self.name, sensor_type=self.weather_sensor_type_name)