openenergyid 0.1.29__py3-none-any.whl → 0.1.30__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Potentially problematic release.
This version of openenergyid might be problematic. Click here for more details.
- openenergyid/__init__.py +1 -1
- openenergyid/abstractsim/__init__.py +5 -0
- openenergyid/abstractsim/abstract.py +102 -0
- openenergyid/baseload/__init__.py +1 -1
- openenergyid/capacity/__init__.py +1 -1
- openenergyid/capacity/main.py +1 -0
- openenergyid/capacity/models.py +2 -0
- openenergyid/const.py +11 -0
- openenergyid/dyntar/main.py +10 -9
- openenergyid/dyntar/models.py +3 -2
- openenergyid/elia/__init__.py +4 -0
- openenergyid/elia/api.py +91 -0
- openenergyid/elia/const.py +18 -0
- openenergyid/energysharing/data_formatting.py +9 -1
- openenergyid/energysharing/main.py +13 -2
- openenergyid/energysharing/models.py +3 -2
- openenergyid/models.py +10 -4
- openenergyid/mvlr/__init__.py +1 -1
- openenergyid/mvlr/main.py +1 -1
- openenergyid/mvlr/models.py +2 -3
- openenergyid/pvsim/__init__.py +8 -0
- openenergyid/pvsim/abstract.py +60 -0
- openenergyid/pvsim/elia/__init__.py +3 -0
- openenergyid/pvsim/elia/main.py +89 -0
- openenergyid/pvsim/main.py +49 -0
- openenergyid/pvsim/pvlib/__init__.py +11 -0
- openenergyid/pvsim/pvlib/main.py +115 -0
- openenergyid/pvsim/pvlib/models.py +235 -0
- openenergyid/pvsim/pvlib/quickscan.py +99 -0
- openenergyid/pvsim/pvlib/weather.py +91 -0
- openenergyid/sim/__init__.py +5 -0
- openenergyid/sim/main.py +67 -0
- openenergyid/simeval/__init__.py +6 -0
- openenergyid/simeval/main.py +148 -0
- openenergyid/simeval/models.py +162 -0
- {openenergyid-0.1.29.dist-info → openenergyid-0.1.30.dist-info}/METADATA +3 -1
- openenergyid-0.1.30.dist-info/RECORD +50 -0
- openenergyid-0.1.29.dist-info/RECORD +0 -30
- {openenergyid-0.1.29.dist-info → openenergyid-0.1.30.dist-info}/WHEEL +0 -0
- {openenergyid-0.1.29.dist-info → openenergyid-0.1.30.dist-info}/licenses/LICENSE +0 -0
- {openenergyid-0.1.29.dist-info → openenergyid-0.1.30.dist-info}/top_level.txt +0 -0
openenergyid/__init__.py
CHANGED
|
@@ -0,0 +1,102 @@
|
|
|
1
|
+
from abc import ABC, abstractmethod
|
|
2
|
+
from typing import Annotated, Self
|
|
3
|
+
|
|
4
|
+
import pandas as pd
|
|
5
|
+
from aiohttp import ClientSession
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from ..simeval.models import ComparisonPayload, EvalPayload, EvaluationOutput
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class SimulationInputAbstract(BaseModel):
|
|
12
|
+
"""Abstract input parameters for any Simulation"""
|
|
13
|
+
|
|
14
|
+
type: str
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class SimulationSummary(BaseModel):
|
|
18
|
+
"""Summary of a simulation including ex-ante, simulation results, ex-post, and comparisons."""
|
|
19
|
+
|
|
20
|
+
ex_ante: Annotated[EvalPayload, Field(description="Ex-ante evaluation results.")]
|
|
21
|
+
simulation_result: Annotated[EvalPayload, Field(description="Simulation results.")]
|
|
22
|
+
ex_post: Annotated[EvalPayload, Field(description="Ex-post evaluation results.")]
|
|
23
|
+
comparison: Annotated[
|
|
24
|
+
ComparisonPayload, Field(description="Comparison between ex-ante and ex-post results.")
|
|
25
|
+
]
|
|
26
|
+
|
|
27
|
+
@classmethod
|
|
28
|
+
def from_simulation(
|
|
29
|
+
cls,
|
|
30
|
+
ex_ante: dict[str, pd.DataFrame | pd.Series],
|
|
31
|
+
simulation_result: dict[str, pd.DataFrame | pd.Series],
|
|
32
|
+
ex_post: dict[str, pd.DataFrame | pd.Series],
|
|
33
|
+
comparison: dict[str, dict[str, pd.DataFrame | pd.Series]],
|
|
34
|
+
) -> Self:
|
|
35
|
+
"""Create a SimulationSummary from simulation data."""
|
|
36
|
+
ea = {
|
|
37
|
+
k: EvaluationOutput.from_pandas(v) if isinstance(v, pd.DataFrame) else v.to_dict()
|
|
38
|
+
for k, v in ex_ante.items()
|
|
39
|
+
}
|
|
40
|
+
sr = {
|
|
41
|
+
k: EvaluationOutput.from_pandas(v) if isinstance(v, pd.DataFrame) else v.to_dict()
|
|
42
|
+
for k, v in simulation_result.items()
|
|
43
|
+
}
|
|
44
|
+
ep = {
|
|
45
|
+
k: EvaluationOutput.from_pandas(v) if isinstance(v, pd.DataFrame) else v.to_dict()
|
|
46
|
+
for k, v in ex_post.items()
|
|
47
|
+
}
|
|
48
|
+
c = {
|
|
49
|
+
k: {
|
|
50
|
+
kk: EvaluationOutput.from_pandas(vv)
|
|
51
|
+
if isinstance(vv, pd.DataFrame)
|
|
52
|
+
else vv.to_dict()
|
|
53
|
+
for kk, vv in v.items()
|
|
54
|
+
}
|
|
55
|
+
for k, v in comparison.items()
|
|
56
|
+
}
|
|
57
|
+
return cls(
|
|
58
|
+
ex_ante=ea, # type: ignore
|
|
59
|
+
simulation_result=sr, # type: ignore
|
|
60
|
+
ex_post=ep, # type: ignore
|
|
61
|
+
comparison=c, # type: ignore
|
|
62
|
+
)
|
|
63
|
+
|
|
64
|
+
|
|
65
|
+
class Simulator(ABC):
|
|
66
|
+
"""
|
|
67
|
+
An abstract base class simulators.
|
|
68
|
+
"""
|
|
69
|
+
|
|
70
|
+
@property
|
|
71
|
+
@abstractmethod
|
|
72
|
+
def simulation_results(self):
|
|
73
|
+
"""The results of the simulation."""
|
|
74
|
+
raise NotImplementedError()
|
|
75
|
+
|
|
76
|
+
@abstractmethod
|
|
77
|
+
def simulate(self, **kwargs):
|
|
78
|
+
"""
|
|
79
|
+
Run the simulation and return the results.
|
|
80
|
+
"""
|
|
81
|
+
raise NotImplementedError()
|
|
82
|
+
|
|
83
|
+
@abstractmethod
|
|
84
|
+
def result_as_frame(self) -> pd.DataFrame:
|
|
85
|
+
"""
|
|
86
|
+
Convert the simulation results to a DataFrame.
|
|
87
|
+
"""
|
|
88
|
+
raise NotImplementedError()
|
|
89
|
+
|
|
90
|
+
@classmethod
|
|
91
|
+
def from_pydantic(cls, input_: SimulationInputAbstract) -> Self:
|
|
92
|
+
"""
|
|
93
|
+
Create an instance of the simulator from Pydantic input data.
|
|
94
|
+
"""
|
|
95
|
+
return cls(**input_.model_dump())
|
|
96
|
+
|
|
97
|
+
@abstractmethod
|
|
98
|
+
async def load_resources(self, session: ClientSession) -> None:
|
|
99
|
+
"""
|
|
100
|
+
Asynchronously load any required resources using the provided session.
|
|
101
|
+
"""
|
|
102
|
+
raise NotImplementedError()
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
"""Baseload analysis package for power consumption data."""
|
|
2
2
|
|
|
3
|
-
from .models import PowerReadingSchema, PowerSeriesSchema, BaseloadResultSchema
|
|
4
3
|
from .analysis import BaseloadAnalyzer
|
|
5
4
|
from .exceptions import InsufficientDataError, InvalidDataError
|
|
5
|
+
from .models import BaseloadResultSchema, PowerReadingSchema, PowerSeriesSchema
|
|
6
6
|
|
|
7
7
|
__version__ = "0.1.0"
|
|
8
8
|
__all__ = [
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
"""Power Offtake peak analysis module."""
|
|
2
2
|
|
|
3
|
-
from .models import CapacityInput, CapacityOutput, PeakDetail
|
|
4
3
|
from .main import CapacityAnalysis
|
|
4
|
+
from .models import CapacityInput, CapacityOutput, PeakDetail
|
|
5
5
|
|
|
6
6
|
__all__ = ["CapacityInput", "CapacityAnalysis", "CapacityOutput", "PeakDetail"]
|
openenergyid/capacity/main.py
CHANGED
openenergyid/capacity/models.py
CHANGED
openenergyid/const.py
CHANGED
|
@@ -7,6 +7,8 @@ from typing import Literal
|
|
|
7
7
|
ELECTRICITY_DELIVERED: Literal["electricity_delivered"] = "electricity_delivered"
|
|
8
8
|
ELECTRICITY_EXPORTED: Literal["electricity_exported"] = "electricity_exported"
|
|
9
9
|
ELECTRICITY_PRODUCED: Literal["electricity_produced"] = "electricity_produced"
|
|
10
|
+
ELECTRICITY_CONSUMED: Literal["electricity_consumed"] = "electricity_consumed"
|
|
11
|
+
ELECTRICITY_SELF_CONSUMED: Literal["electricity_self_consumed"] = "electricity_self_consumed"
|
|
10
12
|
|
|
11
13
|
PRICE_DAY_AHEAD: Literal["price_day_ahead"] = "price_day_ahead"
|
|
12
14
|
PRICE_IMBALANCE_UPWARD: Literal["price_imbalance_upward"] = "price_imbalance_upward"
|
|
@@ -16,3 +18,12 @@ PRICE_ELECTRICITY_EXPORTED: Literal["price_electricity_exported"] = "price_elect
|
|
|
16
18
|
|
|
17
19
|
RLP: Literal["RLP"] = "RLP"
|
|
18
20
|
SPP: Literal["SPP"] = "SPP"
|
|
21
|
+
|
|
22
|
+
COST_ELECTRICITY_DELIVERED: Literal["cost_electricity_delivered"] = "cost_electricity_delivered"
|
|
23
|
+
EARNINGS_ELECTRICITY_EXPORTED: Literal["earnings_electricity_exported"] = (
|
|
24
|
+
"earnings_electricity_exported"
|
|
25
|
+
)
|
|
26
|
+
COST_ELECTRICITY_NET: Literal["cost_electricity_net"] = "cost_electricity_net"
|
|
27
|
+
|
|
28
|
+
RATIO_SELF_CONSUMPTION: Literal["ratio_self_consumption"] = "ratio_self_consumption"
|
|
29
|
+
RATIO_SELF_SUFFICIENCY: Literal["ratio_self_sufficiency"] = "ratio_self_sufficiency"
|
openenergyid/dyntar/main.py
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
1
|
"""Main module of the DynTar package."""
|
|
2
2
|
|
|
3
3
|
from typing import cast
|
|
4
|
+
|
|
4
5
|
import pandas as pd
|
|
5
6
|
|
|
6
7
|
from openenergyid.const import (
|
|
@@ -13,22 +14,22 @@ from openenergyid.const import (
|
|
|
13
14
|
)
|
|
14
15
|
|
|
15
16
|
from .const import (
|
|
16
|
-
ELECTRICITY_DELIVERED_SMR3,
|
|
17
|
-
ELECTRICITY_EXPORTED_SMR3,
|
|
18
|
-
ELECTRICITY_DELIVERED_SMR2,
|
|
19
|
-
ELECTRICITY_EXPORTED_SMR2,
|
|
20
17
|
COST_ELECTRICITY_DELIVERED_SMR2,
|
|
21
|
-
COST_ELECTRICITY_EXPORTED_SMR2,
|
|
22
18
|
COST_ELECTRICITY_DELIVERED_SMR3,
|
|
19
|
+
COST_ELECTRICITY_EXPORTED_SMR2,
|
|
23
20
|
COST_ELECTRICITY_EXPORTED_SMR3,
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
ELECTRICITY_DELIVERED_SMR2,
|
|
22
|
+
ELECTRICITY_DELIVERED_SMR3,
|
|
23
|
+
ELECTRICITY_EXPORTED_SMR2,
|
|
24
|
+
ELECTRICITY_EXPORTED_SMR3,
|
|
26
25
|
HEATMAP_DELIVERED,
|
|
27
|
-
HEATMAP_EXPORTED,
|
|
28
|
-
HEATMAP_TOTAL,
|
|
29
26
|
HEATMAP_DELIVERED_DESCRIPTION,
|
|
27
|
+
HEATMAP_EXPORTED,
|
|
30
28
|
HEATMAP_EXPORTED_DESCRIPTION,
|
|
29
|
+
HEATMAP_TOTAL,
|
|
31
30
|
HEATMAP_TOTAL_DESCRIPTION,
|
|
31
|
+
RLP_WEIGHTED_PRICE_DELIVERED,
|
|
32
|
+
SPP_WEIGHTED_PRICE_EXPORTED,
|
|
32
33
|
Register,
|
|
33
34
|
)
|
|
34
35
|
|
openenergyid/dyntar/models.py
CHANGED
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
"""Models for dynamic tariff analysis."""
|
|
2
2
|
|
|
3
3
|
from typing import Literal
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, confloat, conlist
|
|
5
6
|
|
|
6
7
|
from openenergyid.models import TimeDataFrame
|
|
7
|
-
from .const import Register
|
|
8
8
|
|
|
9
|
+
from .const import Register
|
|
9
10
|
|
|
10
11
|
RequiredColumns = Literal[
|
|
11
12
|
"electricity_delivered",
|
openenergyid/elia/api.py
ADDED
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the functions to interact with the Elia API.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
|
|
7
|
+
import aiohttp
|
|
8
|
+
import pandas as pd
|
|
9
|
+
|
|
10
|
+
from .const import Region
|
|
11
|
+
|
|
12
|
+
DATE_FORMAT = "%Y-%m-%d"
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
async def get_dataset(
|
|
16
|
+
dataset: str,
|
|
17
|
+
start: dt.date,
|
|
18
|
+
end: dt.date,
|
|
19
|
+
region: Region,
|
|
20
|
+
select: set[str],
|
|
21
|
+
session: aiohttp.ClientSession,
|
|
22
|
+
timezone: str = "Europe/Brussels",
|
|
23
|
+
) -> dict:
|
|
24
|
+
"""
|
|
25
|
+
Fetches a dataset from the Elia open data API within a specified date range and region.
|
|
26
|
+
|
|
27
|
+
Args:
|
|
28
|
+
dataset (str): The name of the dataset to fetch.
|
|
29
|
+
start (dt.date): The start date for the data range.
|
|
30
|
+
end (dt.date): The end date for the data range.
|
|
31
|
+
region (Region): The region for which to fetch the data.
|
|
32
|
+
select (set[str]): A set of fields to select in the dataset.
|
|
33
|
+
session (aiohttp.ClientSession): The aiohttp session to use for making the request.
|
|
34
|
+
timezone (str, optional): The timezone to use for the data. Defaults to "Europe/Brussels".
|
|
35
|
+
|
|
36
|
+
Returns:
|
|
37
|
+
dict: The dataset fetched from the API.
|
|
38
|
+
|
|
39
|
+
Raises:
|
|
40
|
+
aiohttp.ClientError: If there is an error making the request.
|
|
41
|
+
"""
|
|
42
|
+
url = f"https://opendata.elia.be/api/explore/v2.1/catalog/datasets/{dataset}/exports/json"
|
|
43
|
+
|
|
44
|
+
if "datetime" not in select:
|
|
45
|
+
select.add("datetime")
|
|
46
|
+
select_str = ",".join(select)
|
|
47
|
+
|
|
48
|
+
params = {
|
|
49
|
+
"where": (
|
|
50
|
+
f"datetime IN [date'{start.strftime(DATE_FORMAT)}'..date'{end.strftime(DATE_FORMAT)}'] "
|
|
51
|
+
f"AND region='{region.value}'"
|
|
52
|
+
),
|
|
53
|
+
"timezone": timezone,
|
|
54
|
+
"select": select_str,
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async with session.get(url, params=params) as response:
|
|
58
|
+
data = await response.json()
|
|
59
|
+
|
|
60
|
+
return data
|
|
61
|
+
|
|
62
|
+
|
|
63
|
+
def parse_response(
|
|
64
|
+
data: dict, index: str, columns: list[str], timezone: str = "Europe/Brussels"
|
|
65
|
+
) -> pd.DataFrame:
|
|
66
|
+
"""
|
|
67
|
+
Parses a response dictionary into a pandas DataFrame.
|
|
68
|
+
|
|
69
|
+
Args:
|
|
70
|
+
data (dict): The input data where each key is a column name
|
|
71
|
+
and each value is a list of column values.
|
|
72
|
+
index (str): The key in the data dictionary to be used as the index for the DataFrame.
|
|
73
|
+
columns (list[str]): The list of column names for the DataFrame.
|
|
74
|
+
timezone (str, optional): The timezone to convert the DataFrame index to.
|
|
75
|
+
Defaults to "Europe/Brussels".
|
|
76
|
+
|
|
77
|
+
Returns:
|
|
78
|
+
pd.DataFrame: A pandas DataFrame with the specified columns and index,
|
|
79
|
+
converted to the specified timezone.
|
|
80
|
+
"""
|
|
81
|
+
df = pd.DataFrame(
|
|
82
|
+
data,
|
|
83
|
+
index=pd.to_datetime([x[index] for x in data], utc=True),
|
|
84
|
+
columns=columns,
|
|
85
|
+
)
|
|
86
|
+
df.index = pd.DatetimeIndex(df.index)
|
|
87
|
+
df = df.tz_convert(timezone)
|
|
88
|
+
df.sort_index(inplace=True)
|
|
89
|
+
df.dropna(inplace=True)
|
|
90
|
+
|
|
91
|
+
return df
|
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
from enum import Enum
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class Region(Enum):
|
|
5
|
+
Belgium = "Belgium"
|
|
6
|
+
Brussels = "Brussels"
|
|
7
|
+
Flanders = "Flanders"
|
|
8
|
+
Wallonia = "Wallonia"
|
|
9
|
+
Antwerp = "Antwerp"
|
|
10
|
+
East_Flanders = "East-Flanders"
|
|
11
|
+
Flemish_Brabant = "Flemish-Brabant"
|
|
12
|
+
Limburg = "Limburg"
|
|
13
|
+
West_Flanders = "West-Flanders"
|
|
14
|
+
Hainaut = "Hainaut"
|
|
15
|
+
Liège = "Liège"
|
|
16
|
+
Luxembourg = "Luxembourg"
|
|
17
|
+
Namur = "Namur"
|
|
18
|
+
Walloon_Brabant = "Walloon-Brabant"
|
|
@@ -1,7 +1,15 @@
|
|
|
1
1
|
"""Functions to create multi-indexed DataFrames for input and output data for energy sharing."""
|
|
2
2
|
|
|
3
3
|
import pandas as pd
|
|
4
|
-
|
|
4
|
+
|
|
5
|
+
from .const import (
|
|
6
|
+
GROSS_INJECTION,
|
|
7
|
+
GROSS_OFFTAKE,
|
|
8
|
+
KEY,
|
|
9
|
+
NET_INJECTION,
|
|
10
|
+
NET_OFFTAKE,
|
|
11
|
+
SHARED_ENERGY,
|
|
12
|
+
)
|
|
5
13
|
|
|
6
14
|
|
|
7
15
|
def create_multi_index_input_frame(
|
|
@@ -1,9 +1,20 @@
|
|
|
1
1
|
"""Main Calcuation Module for Energy Sharing."""
|
|
2
2
|
|
|
3
3
|
import pandas as pd
|
|
4
|
+
|
|
5
|
+
from .const import (
|
|
6
|
+
GROSS_INJECTION,
|
|
7
|
+
GROSS_OFFTAKE,
|
|
8
|
+
KEY,
|
|
9
|
+
NET_INJECTION,
|
|
10
|
+
NET_OFFTAKE,
|
|
11
|
+
SHARED_ENERGY,
|
|
12
|
+
)
|
|
13
|
+
from .data_formatting import (
|
|
14
|
+
create_multi_index_output_frame,
|
|
15
|
+
result_to_input_for_reiteration,
|
|
16
|
+
)
|
|
4
17
|
from .models import CalculationMethod
|
|
5
|
-
from .const import GROSS_INJECTION, GROSS_OFFTAKE, KEY, NET_INJECTION, NET_OFFTAKE, SHARED_ENERGY
|
|
6
|
-
from .data_formatting import create_multi_index_output_frame, result_to_input_for_reiteration
|
|
7
18
|
|
|
8
19
|
|
|
9
20
|
def _calculate(df: pd.DataFrame, method: CalculationMethod) -> pd.DataFrame:
|
|
@@ -3,12 +3,13 @@
|
|
|
3
3
|
from enum import Enum
|
|
4
4
|
from typing import Annotated, Any
|
|
5
5
|
|
|
6
|
-
from pydantic import BaseModel, Field, confloat
|
|
7
6
|
import pandas as pd
|
|
7
|
+
from pydantic import BaseModel, Field, confloat
|
|
8
8
|
|
|
9
9
|
from openenergyid import TimeDataFrame
|
|
10
|
-
|
|
10
|
+
|
|
11
11
|
from .const import NET_INJECTION, NET_OFFTAKE, SHARED_ENERGY
|
|
12
|
+
from .data_formatting import create_multi_index_input_frame
|
|
12
13
|
|
|
13
14
|
|
|
14
15
|
class CalculationMethod(Enum):
|
openenergyid/models.py
CHANGED
|
@@ -1,13 +1,11 @@
|
|
|
1
1
|
"""Data models for the Open Energy ID."""
|
|
2
2
|
|
|
3
3
|
import datetime as dt
|
|
4
|
-
from typing import overload
|
|
5
|
-
|
|
6
|
-
from typing import Self
|
|
4
|
+
from typing import Self, overload
|
|
7
5
|
|
|
8
6
|
import pandas as pd
|
|
9
|
-
from pydantic import BaseModel
|
|
10
7
|
import polars as pl
|
|
8
|
+
from pydantic import BaseModel
|
|
11
9
|
|
|
12
10
|
|
|
13
11
|
class TimeSeriesBase(BaseModel):
|
|
@@ -62,6 +60,14 @@ class TimeSeriesBase(BaseModel):
|
|
|
62
60
|
return cls.model_validate_json(file.read(), **kwargs)
|
|
63
61
|
raise ValueError("Either string or path must be provided.")
|
|
64
62
|
|
|
63
|
+
def first_timestamp(self) -> dt.datetime:
|
|
64
|
+
"""Get the first timestamp in the index."""
|
|
65
|
+
return min(self.index)
|
|
66
|
+
|
|
67
|
+
def last_timestamp(self) -> dt.datetime:
|
|
68
|
+
"""Get the last timestamp in the index."""
|
|
69
|
+
return max(self.index)
|
|
70
|
+
|
|
65
71
|
|
|
66
72
|
class TimeSeries(TimeSeriesBase):
|
|
67
73
|
"""
|
openenergyid/mvlr/__init__.py
CHANGED
openenergyid/mvlr/main.py
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
"""Main module for the MultiVariableLinearRegression class."""
|
|
2
2
|
|
|
3
|
-
from .models import MultiVariableRegressionInput, MultiVariableRegressionResult
|
|
4
3
|
from .helpers import resample_input_data
|
|
4
|
+
from .models import MultiVariableRegressionInput, MultiVariableRegressionResult
|
|
5
5
|
from .mvlr import MultiVariableLinearRegression
|
|
6
6
|
|
|
7
7
|
|
openenergyid/mvlr/models.py
CHANGED
|
@@ -1,17 +1,16 @@
|
|
|
1
1
|
"""Models for multivariable linear regression."""
|
|
2
2
|
|
|
3
3
|
from typing import Any
|
|
4
|
-
import pandas as pd
|
|
5
4
|
|
|
6
|
-
|
|
5
|
+
import pandas as pd
|
|
7
6
|
import statsmodels.formula.api as fm
|
|
7
|
+
from pydantic import BaseModel, ConfigDict, Field
|
|
8
8
|
|
|
9
9
|
from openenergyid.enums import Granularity
|
|
10
10
|
from openenergyid.models import TimeDataFrame
|
|
11
11
|
|
|
12
12
|
from .mvlr import MultiVariableLinearRegression
|
|
13
13
|
|
|
14
|
-
|
|
15
14
|
COLUMN_TEMPERATUREEQUIVALENT = "temperatureEquivalent"
|
|
16
15
|
|
|
17
16
|
|
|
@@ -0,0 +1,60 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the abstract base class for PVSimulator.
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import datetime as dt
|
|
6
|
+
from abc import ABC
|
|
7
|
+
from typing import cast
|
|
8
|
+
|
|
9
|
+
import pandas as pd
|
|
10
|
+
from pydantic import Field
|
|
11
|
+
|
|
12
|
+
from openenergyid.models import TimeSeries
|
|
13
|
+
|
|
14
|
+
from ..abstractsim import SimulationInputAbstract, Simulator
|
|
15
|
+
from ..const import ELECTRICITY_PRODUCED
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
class PVSimulationInputAbstract(SimulationInputAbstract):
|
|
19
|
+
"""
|
|
20
|
+
Input parameters for the PV simulation.
|
|
21
|
+
"""
|
|
22
|
+
|
|
23
|
+
start: dt.date
|
|
24
|
+
end: dt.date
|
|
25
|
+
result_resolution: str = Field(
|
|
26
|
+
"15min",
|
|
27
|
+
description="Resolution of the simulation results",
|
|
28
|
+
examples=["15min", "1h", "D", "MS"],
|
|
29
|
+
)
|
|
30
|
+
|
|
31
|
+
|
|
32
|
+
class PVSimulator(Simulator, ABC):
|
|
33
|
+
"""
|
|
34
|
+
An abstract base class for PV simulators.
|
|
35
|
+
"""
|
|
36
|
+
|
|
37
|
+
def __init__(self, result_resolution: str = "15min", **kwargs) -> None:
|
|
38
|
+
self._simulation_results: pd.Series | None = None
|
|
39
|
+
self.result_resolution = result_resolution
|
|
40
|
+
|
|
41
|
+
@property
|
|
42
|
+
def simulation_results(self) -> pd.Series:
|
|
43
|
+
"""The results of the simulation."""
|
|
44
|
+
if self._simulation_results is None:
|
|
45
|
+
results = self.simulate()
|
|
46
|
+
self._simulation_results = cast(pd.Series, results)
|
|
47
|
+
return self._simulation_results
|
|
48
|
+
|
|
49
|
+
def result_to_timeseries(self):
|
|
50
|
+
"""
|
|
51
|
+
Convert the simulation results to a TimeSeries object.
|
|
52
|
+
"""
|
|
53
|
+
result = self.simulation_results.resample(self.result_resolution).sum()
|
|
54
|
+
return TimeSeries.from_pandas(result)
|
|
55
|
+
|
|
56
|
+
def result_as_frame(self) -> pd.DataFrame:
|
|
57
|
+
"""
|
|
58
|
+
Convert the simulation results to a DataFrame.
|
|
59
|
+
"""
|
|
60
|
+
return self.simulation_results.rename(ELECTRICITY_PRODUCED).to_frame()
|
|
@@ -0,0 +1,89 @@
|
|
|
1
|
+
"""
|
|
2
|
+
This module contains the LoadFactorPVSimulator class which
|
|
3
|
+
simulates the power output of a PV system based on load factors.
|
|
4
|
+
"""
|
|
5
|
+
|
|
6
|
+
import datetime as dt
|
|
7
|
+
from typing import Literal
|
|
8
|
+
|
|
9
|
+
import aiohttp
|
|
10
|
+
import pandas as pd
|
|
11
|
+
from pydantic import Field
|
|
12
|
+
|
|
13
|
+
from openenergyid import elia
|
|
14
|
+
from openenergyid.pvsim.abstract import PVSimulationInputAbstract, PVSimulator
|
|
15
|
+
|
|
16
|
+
|
|
17
|
+
class EliaPVSimulationInput(PVSimulationInputAbstract):
|
|
18
|
+
"""Input parameters for the Elia PV simulation."""
|
|
19
|
+
|
|
20
|
+
type: Literal["eliapvsimulation"] = Field("eliapvsimulation", frozen=True) # tag
|
|
21
|
+
region: elia.Region
|
|
22
|
+
panel_power: float = Field(..., gt=0, description="Installed panel power in W")
|
|
23
|
+
inverter_power: float = Field(..., gt=0, description="Installed inverter power in W")
|
|
24
|
+
|
|
25
|
+
|
|
26
|
+
class EliaPVSimulator(PVSimulator):
|
|
27
|
+
"""
|
|
28
|
+
A PV simulator that simulates the power output of a PV system based on load factors.
|
|
29
|
+
"""
|
|
30
|
+
|
|
31
|
+
def __init__(
|
|
32
|
+
self,
|
|
33
|
+
start: dt.date,
|
|
34
|
+
end: dt.date,
|
|
35
|
+
panel_power: float,
|
|
36
|
+
inverter_power: float,
|
|
37
|
+
region: elia.Region,
|
|
38
|
+
load_factors: pd.Series | None = None,
|
|
39
|
+
**kwargs,
|
|
40
|
+
):
|
|
41
|
+
self.start = start
|
|
42
|
+
self.end = end
|
|
43
|
+
self.panel_power = panel_power / 1000 # convert to kW
|
|
44
|
+
self.inverter_power = inverter_power / 1000 # convert to kW
|
|
45
|
+
self.region = region
|
|
46
|
+
self.load_factors = load_factors if load_factors is not None else pd.Series(dtype=float)
|
|
47
|
+
|
|
48
|
+
super().__init__(**kwargs)
|
|
49
|
+
|
|
50
|
+
def simulate(self, **kwargs) -> pd.Series:
|
|
51
|
+
"""Run the simulation."""
|
|
52
|
+
result = self.load_factors * self.panel_power * 0.01
|
|
53
|
+
result.clip(upper=self.inverter_power, inplace=True)
|
|
54
|
+
result = result * 0.25 # To Energy
|
|
55
|
+
result.name = None
|
|
56
|
+
return result
|
|
57
|
+
|
|
58
|
+
@staticmethod
|
|
59
|
+
async def download_load_factors(
|
|
60
|
+
start: dt.date,
|
|
61
|
+
end: dt.date,
|
|
62
|
+
region: elia.Region,
|
|
63
|
+
session: aiohttp.ClientSession,
|
|
64
|
+
) -> pd.Series:
|
|
65
|
+
"""Download load factors from the API."""
|
|
66
|
+
pv_load_factor_json = await elia.get_dataset(
|
|
67
|
+
dataset="ods032",
|
|
68
|
+
start=start,
|
|
69
|
+
end=end,
|
|
70
|
+
region=region,
|
|
71
|
+
select={"loadfactor"},
|
|
72
|
+
session=session,
|
|
73
|
+
)
|
|
74
|
+
pv_load_factors = elia.parse_response(
|
|
75
|
+
data=pv_load_factor_json, index="datetime", columns=["loadfactor"]
|
|
76
|
+
)["loadfactor"]
|
|
77
|
+
pv_load_factors = pv_load_factors.truncate(
|
|
78
|
+
after=pd.Timestamp(end, tz="Europe/Brussels") - pd.Timedelta(minutes=15)
|
|
79
|
+
)
|
|
80
|
+
return pv_load_factors
|
|
81
|
+
|
|
82
|
+
async def load_resources(self, session: aiohttp.ClientSession) -> None:
|
|
83
|
+
"""Load resources required for the simulation."""
|
|
84
|
+
self.load_factors = await self.download_load_factors(
|
|
85
|
+
start=self.start,
|
|
86
|
+
end=self.end,
|
|
87
|
+
region=self.region,
|
|
88
|
+
session=session,
|
|
89
|
+
)
|