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.

Files changed (41) hide show
  1. openenergyid/__init__.py +1 -1
  2. openenergyid/abstractsim/__init__.py +5 -0
  3. openenergyid/abstractsim/abstract.py +102 -0
  4. openenergyid/baseload/__init__.py +1 -1
  5. openenergyid/capacity/__init__.py +1 -1
  6. openenergyid/capacity/main.py +1 -0
  7. openenergyid/capacity/models.py +2 -0
  8. openenergyid/const.py +11 -0
  9. openenergyid/dyntar/main.py +10 -9
  10. openenergyid/dyntar/models.py +3 -2
  11. openenergyid/elia/__init__.py +4 -0
  12. openenergyid/elia/api.py +91 -0
  13. openenergyid/elia/const.py +18 -0
  14. openenergyid/energysharing/data_formatting.py +9 -1
  15. openenergyid/energysharing/main.py +13 -2
  16. openenergyid/energysharing/models.py +3 -2
  17. openenergyid/models.py +10 -4
  18. openenergyid/mvlr/__init__.py +1 -1
  19. openenergyid/mvlr/main.py +1 -1
  20. openenergyid/mvlr/models.py +2 -3
  21. openenergyid/pvsim/__init__.py +8 -0
  22. openenergyid/pvsim/abstract.py +60 -0
  23. openenergyid/pvsim/elia/__init__.py +3 -0
  24. openenergyid/pvsim/elia/main.py +89 -0
  25. openenergyid/pvsim/main.py +49 -0
  26. openenergyid/pvsim/pvlib/__init__.py +11 -0
  27. openenergyid/pvsim/pvlib/main.py +115 -0
  28. openenergyid/pvsim/pvlib/models.py +235 -0
  29. openenergyid/pvsim/pvlib/quickscan.py +99 -0
  30. openenergyid/pvsim/pvlib/weather.py +91 -0
  31. openenergyid/sim/__init__.py +5 -0
  32. openenergyid/sim/main.py +67 -0
  33. openenergyid/simeval/__init__.py +6 -0
  34. openenergyid/simeval/main.py +148 -0
  35. openenergyid/simeval/models.py +162 -0
  36. {openenergyid-0.1.29.dist-info → openenergyid-0.1.30.dist-info}/METADATA +3 -1
  37. openenergyid-0.1.30.dist-info/RECORD +50 -0
  38. openenergyid-0.1.29.dist-info/RECORD +0 -30
  39. {openenergyid-0.1.29.dist-info → openenergyid-0.1.30.dist-info}/WHEEL +0 -0
  40. {openenergyid-0.1.29.dist-info → openenergyid-0.1.30.dist-info}/licenses/LICENSE +0 -0
  41. {openenergyid-0.1.29.dist-info → openenergyid-0.1.30.dist-info}/top_level.txt +0 -0
openenergyid/__init__.py CHANGED
@@ -1,6 +1,6 @@
1
1
  """Open Energy ID Python SDK."""
2
2
 
3
- __version__ = "0.1.28"
3
+ __version__ = "0.1.30"
4
4
 
5
5
  from .enums import Granularity
6
6
  from .models import TimeDataFrame, TimeSeries
@@ -0,0 +1,5 @@
1
+ """Module with abstract definitions that ALL SIMULATIONS should adhere to."""
2
+
3
+ from .abstract import SimulationInputAbstract, SimulationSummary, Simulator
4
+
5
+ __all__ = ["SimulationInputAbstract", "SimulationSummary", "Simulator"]
@@ -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"]
@@ -2,6 +2,7 @@
2
2
 
3
3
  import datetime as dt
4
4
  import typing
5
+
5
6
  import pandas as pd
6
7
  import pandera.typing as pdt
7
8
 
@@ -1,7 +1,9 @@
1
1
  """Model for Capacity Analysis."""
2
2
 
3
3
  import datetime as dt
4
+
4
5
  from pydantic import BaseModel, ConfigDict, Field
6
+
5
7
  from openenergyid.models import TimeSeries
6
8
 
7
9
 
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"
@@ -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
- RLP_WEIGHTED_PRICE_DELIVERED,
25
- SPP_WEIGHTED_PRICE_EXPORTED,
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
 
@@ -1,11 +1,12 @@
1
1
  """Models for dynamic tariff analysis."""
2
2
 
3
3
  from typing import Literal
4
- from pydantic import Field, conlist, confloat, BaseModel
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",
@@ -0,0 +1,4 @@
1
+ from .api import get_dataset, parse_response
2
+ from .const import Region
3
+
4
+ __all__ = ["get_dataset", "parse_response", "Region"]
@@ -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
- from .const import GROSS_INJECTION, GROSS_OFFTAKE, KEY, NET_INJECTION, NET_OFFTAKE, SHARED_ENERGY
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
- from .data_formatting import create_multi_index_input_frame
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
  """
@@ -3,10 +3,10 @@
3
3
  from .main import find_best_mvlr
4
4
  from .models import (
5
5
  IndependentVariableInput,
6
+ IndependentVariableResult,
6
7
  MultiVariableRegressionInput,
7
8
  MultiVariableRegressionResult,
8
9
  ValidationParameters,
9
- IndependentVariableResult,
10
10
  )
11
11
 
12
12
  __all__ = [
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
 
@@ -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
- from pydantic import BaseModel, Field, ConfigDict
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,8 @@
1
+ from .main import (
2
+ PVSimulationInput,
3
+ PVSimulationSummary,
4
+ apply_simulation,
5
+ get_simulator,
6
+ )
7
+
8
+ __all__ = ["PVSimulationInput", "get_simulator", "apply_simulation", "PVSimulationSummary"]
@@ -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,3 @@
1
+ from .main import EliaPVSimulationInput, EliaPVSimulator
2
+
3
+ __all__ = ["EliaPVSimulationInput", "EliaPVSimulator"]
@@ -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
+ )