Skip to content

Commit 53794fb

Browse files
Merge pull request #11 from openclimatefix/add-gsp
add gsp model
2 parents 0a82b9a + 9fce8af commit 53794fb

File tree

14 files changed

+358
-52
lines changed

14 files changed

+358
-52
lines changed

diagram.png

-243 Bytes
Loading

nowcasting_datamodel/fake.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -9,12 +9,12 @@
99
ForecastSQL,
1010
ForecastValueSQL,
1111
InputDataLastUpdatedSQL,
12-
LocationSQL,
1312
MLModelSQL,
1413
PVSystemSQL,
1514
national_gb_label,
1615
)
17-
from nowcasting_datamodel.read import get_location, get_model
16+
from nowcasting_datamodel.models.gsp import LocationSQL
17+
from nowcasting_datamodel.read.read import get_location, get_model
1818

1919

2020
def make_fake_location(gsp_id: int) -> LocationSQL:
Lines changed: 48 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,48 @@
1+
"""empty message
2+
3+
Revision ID: d2f031f24593
4+
Revises: 14e1747b9710
5+
Create Date: 2022-03-21 15:56:09.757720
6+
7+
"""
8+
import sqlalchemy as sa
9+
from alembic import op
10+
11+
# revision identifiers, used by Alembic.
12+
revision = "d2f031f24593"
13+
down_revision = "14e1747b9710"
14+
branch_labels = None
15+
depends_on = None
16+
17+
18+
def upgrade(): # noqa 103
19+
# ### commands auto generated by Alembic - please adjust! ###
20+
op.create_table(
21+
"gsp_yield",
22+
sa.Column("created_utc", sa.DateTime(timezone=True), nullable=True),
23+
sa.Column("id", sa.Integer(), nullable=False),
24+
sa.Column("datetime_utc", sa.DateTime(), nullable=True),
25+
sa.Column("solar_generation_kw", sa.String(), nullable=True),
26+
sa.Column("regime", sa.String(), nullable=True),
27+
sa.Column("location_id", sa.Integer(), nullable=True),
28+
sa.ForeignKeyConstraint(
29+
["location_id"],
30+
["location.id"],
31+
),
32+
sa.PrimaryKeyConstraint("id"),
33+
)
34+
op.create_index(op.f("ix_gsp_yield_datetime_utc"), "gsp_yield", ["datetime_utc"], unique=False)
35+
op.create_index(
36+
"ix_gsp_yield_datetime_utc_desc", "gsp_yield", [sa.text("datetime_utc DESC")], unique=False
37+
)
38+
op.create_index(op.f("ix_gsp_yield_location_id"), "gsp_yield", ["location_id"], unique=False)
39+
# ### end Alembic commands ###
40+
41+
42+
def downgrade(): # noqa 103
43+
# ### commands auto generated by Alembic - please adjust! ###
44+
op.drop_index(op.f("ix_gsp_yield_location_id"), table_name="gsp_yield")
45+
op.drop_index("ix_gsp_yield_datetime_utc_desc", table_name="gsp_yield")
46+
op.drop_index(op.f("ix_gsp_yield_datetime_utc"), table_name="gsp_yield")
47+
op.drop_table("gsp_yield")
48+
# ### end Alembic commands ###

nowcasting_datamodel/models/__init__.py

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -20,5 +20,6 @@
2020
The primary keys could be 'gsp_id' and 'target_datetime_utc'.
2121
"""
2222

23+
from .gsp import * # noqa F403
2324
from .models import * # noqa F403
2425
from .pv import * # noqa F403

nowcasting_datamodel/models/gsp.py

Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
""" Pydantic and Sqlalchemy models for the database
2+
3+
2. Location objects, where the forecast is for
4+
8. GSP yield for storing GSP yield data
5+
6+
"""
7+
import logging
8+
from datetime import datetime
9+
from typing import Optional
10+
11+
from pydantic import Field, validator
12+
from sqlalchemy import Column, DateTime, ForeignKey, Index, Integer, String
13+
from sqlalchemy.orm import relationship
14+
15+
from nowcasting_datamodel.models.base import Base_Forecast
16+
from nowcasting_datamodel.models.utils import CreatedMixin, EnhancedBaseModel
17+
from nowcasting_datamodel.utils import datetime_must_have_timezone
18+
19+
logger = logging.getLogger(__name__)
20+
21+
########
22+
# 2. Location
23+
########
24+
25+
26+
class LocationSQL(Base_Forecast):
27+
"""Location that the forecast is for"""
28+
29+
__tablename__ = "location"
30+
31+
id = Column(Integer, primary_key=True)
32+
label = Column(String)
33+
gsp_id = Column(Integer)
34+
gsp_name = Column(String, nullable=True)
35+
gsp_group = Column(String, nullable=True)
36+
region_name = Column(String, nullable=True)
37+
38+
forecast = relationship("ForecastSQL", back_populates="location")
39+
gsp_yield = relationship("GSPYieldSQL", back_populates="location")
40+
41+
42+
class Location(EnhancedBaseModel):
43+
"""Location that the forecast is for"""
44+
45+
label: str = Field(..., description="")
46+
gsp_id: Optional[int] = Field(None, description="The Grid Supply Point (GSP) id", index=True)
47+
gsp_name: Optional[str] = Field(None, description="The GSP name")
48+
gsp_group: Optional[str] = Field(None, description="The GSP group name")
49+
region_name: Optional[str] = Field(None, description="The GSP region name")
50+
51+
rm_mode = True
52+
53+
def to_orm(self) -> LocationSQL:
54+
"""Change model to LocationSQL"""
55+
return LocationSQL(
56+
label=self.label,
57+
gsp_id=self.gsp_id,
58+
gsp_name=self.gsp_name,
59+
gsp_group=self.gsp_group,
60+
region_name=self.region_name,
61+
)
62+
63+
64+
class GSPYieldSQL(Base_Forecast, CreatedMixin):
65+
"""GSP Yield data"""
66+
67+
__tablename__ = "gsp_yield"
68+
69+
id = Column(Integer, primary_key=True)
70+
datetime_utc = Column(DateTime, index=True)
71+
solar_generation_kw = Column(String)
72+
regime = Column(String, nullable=True)
73+
74+
# many (forecasts) to one (location)
75+
location = relationship("LocationSQL", back_populates="gsp_yield")
76+
location_id = Column(Integer, ForeignKey("location.id"), index=True)
77+
78+
Index("ix_gsp_yield_datetime_utc_desc", datetime_utc.desc())
79+
80+
81+
class GSPYield(EnhancedBaseModel):
82+
"""GSP Yield data"""
83+
84+
datetime_utc: datetime = Field(..., description="The timestamp of the gsp yield")
85+
solar_generation_kw: float = Field(..., description="The amount of solar generation")
86+
regime: str = Field(
87+
"in-day", description="When the GSP data is pulled, can be 'in-day' or 'day-after'"
88+
)
89+
90+
_normalize_target_time = validator("datetime_utc", allow_reuse=True)(
91+
datetime_must_have_timezone
92+
)
93+
94+
gsp: Optional[Location] = Field(
95+
None,
96+
description="The GSP associated with this model",
97+
)
98+
99+
@validator("solar_generation_kw")
100+
def validate_solar_generation_kw(cls, v):
101+
"""Validate the solar_generation_kw field"""
102+
if v < 0:
103+
logger.debug(f"Changing solar_generation_kw ({v}) to 0")
104+
v = 0
105+
return v
106+
107+
@validator("regime")
108+
def validate_regime(cls, v):
109+
"""Validate the solar_generation_kw field"""
110+
if v not in ["day-after", "in-da"]:
111+
message = f"Regime ({v}) not in 'day-after', 'in-da'"
112+
logger.debug(message)
113+
raise Exception(message)
114+
return v
115+
116+
def to_orm(self) -> GSPYieldSQL:
117+
"""Change model to GSPYieldSQL"""
118+
return GSPYieldSQL(
119+
datetime_utc=self.datetime_utc,
120+
solar_generation_kw=self.solar_generation_kw,
121+
regime=self.regime,
122+
)

nowcasting_datamodel/models/models.py

Lines changed: 1 addition & 36 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,6 @@
22
33
The following class are made
44
1. Reusable classes
5-
2. Location objects, where the forecast is for
65
3. Model object, what forecast model is used
76
4. ForecastValue objects, specific values of a forecast and time
87
5. Input data status, shows when the data was collected
@@ -18,6 +17,7 @@
1817
from sqlalchemy.orm import relationship
1918

2019
from nowcasting_datamodel.models.base import Base_Forecast
20+
from nowcasting_datamodel.models.gsp import Location
2121
from nowcasting_datamodel.models.utils import CreatedMixin, EnhancedBaseModel
2222
from nowcasting_datamodel.utils import datetime_must_have_timezone
2323

@@ -28,41 +28,6 @@
2828
########
2929
# 2. Location
3030
########
31-
class LocationSQL(Base_Forecast):
32-
"""Location that the forecast is for"""
33-
34-
__tablename__ = "location"
35-
36-
id = Column(Integer, primary_key=True)
37-
label = Column(String)
38-
gsp_id = Column(Integer)
39-
gsp_name = Column(String, nullable=True)
40-
gsp_group = Column(String, nullable=True)
41-
region_name = Column(String, nullable=True)
42-
43-
forecast = relationship("ForecastSQL", back_populates="location")
44-
45-
46-
class Location(EnhancedBaseModel):
47-
"""Location that the forecast is for"""
48-
49-
label: str = Field(..., description="")
50-
gsp_id: Optional[int] = Field(None, description="The Grid Supply Point (GSP) id", index=True)
51-
gsp_name: Optional[str] = Field(None, description="The GSP name")
52-
gsp_group: Optional[str] = Field(None, description="The GSP group name")
53-
region_name: Optional[str] = Field(None, description="The GSP region name")
54-
55-
rm_mode = True
56-
57-
def to_orm(self) -> LocationSQL:
58-
"""Change model to LocationSQL"""
59-
return LocationSQL(
60-
label=self.label,
61-
gsp_id=self.gsp_id,
62-
gsp_name=self.gsp_name,
63-
gsp_group=self.gsp_group,
64-
region_name=self.region_name,
65-
)
6631

6732

6833
########
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
""" init for read functions """
File renamed without changes.
Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
""" Read pv functions """
2+
from typing import List, Union
3+
4+
from sqlalchemy import desc
5+
from sqlalchemy.orm import Session
6+
7+
from nowcasting_datamodel.models import GSPYieldSQL, LocationSQL
8+
9+
10+
def get_latest_gsp_yield(
11+
session: Session, gsps: List[LocationSQL], append_to_gsps: bool = False, regime: str = "in-day"
12+
) -> Union[List[GSPYieldSQL], List[LocationSQL]]:
13+
"""
14+
Get the last gsp yield data
15+
16+
:param session: database sessions
17+
:param gsps: list of gsps
18+
:param append_to_gsps: append gsp yield to pv systems, or return pv systems.
19+
If appended the yield is access by 'pv_system.last_gsp_yield'
20+
:param regime: What regime the data is in, either 'in-day' or 'day-after'
21+
:return: either list of gsp yields, or pv systems
22+
"""
23+
24+
gsp_ids = [gsp.id for gsp in gsps]
25+
26+
# start main query
27+
query = session.query(GSPYieldSQL)
28+
query = query.join(LocationSQL)
29+
query = query.where(
30+
LocationSQL.id == GSPYieldSQL.location_id,
31+
)
32+
33+
# filter on regime
34+
query = query.where(GSPYieldSQL.regime == regime)
35+
36+
# only select on results per pv system
37+
query = query.distinct(LocationSQL.id)
38+
39+
# select only th epv systems we want
40+
query = query.where(LocationSQL.id.in_(gsp_ids))
41+
42+
# order by 'created_utc' desc, so we get the latest one
43+
query = query.order_by(
44+
LocationSQL.id, desc(GSPYieldSQL.datetime_utc), desc(GSPYieldSQL.created_utc)
45+
)
46+
47+
# get all results
48+
gsp_yields: List[GSPYieldSQL] = query.all()
49+
50+
if not append_to_gsps:
51+
return gsp_yields
52+
else:
53+
# get list of pvsystems with last pv yields
54+
gsp_systems_with_gsp_yields = []
55+
for gsp_yield in gsp_yields:
56+
gsp = gsp_yield.location
57+
gsp.last_gsp_yield = gsp_yield
58+
59+
gsp_systems_with_gsp_yields.append(gsp)
60+
61+
# add pv systems that dont have any pv yields
62+
gsp_systems_with_gsp_yields_ids = [gsp.id for gsp in gsp_systems_with_gsp_yields]
63+
64+
gsp_systems_with_no_gsp_yields = []
65+
for gsp in gsps:
66+
if gsp.id not in gsp_systems_with_gsp_yields_ids:
67+
gsp.last_gsp_yield = None
68+
69+
gsp_systems_with_no_gsp_yields.append(gsp)
70+
71+
all_gsp_systems = gsp_systems_with_gsp_yields_ids + gsp_systems_with_no_gsp_yields
72+
73+
return all_gsp_systems
File renamed without changes.

0 commit comments

Comments
 (0)