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 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 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 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)