openenergyid 0.1.31__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.
- openenergyid/__init__.py +8 -0
- openenergyid/abstractsim/__init__.py +5 -0
- openenergyid/abstractsim/abstract.py +102 -0
- openenergyid/baseload/__init__.py +15 -0
- openenergyid/baseload/analysis.py +190 -0
- openenergyid/baseload/exceptions.py +9 -0
- openenergyid/baseload/models.py +32 -0
- openenergyid/capacity/__init__.py +6 -0
- openenergyid/capacity/main.py +103 -0
- openenergyid/capacity/models.py +32 -0
- openenergyid/const.py +29 -0
- openenergyid/dyntar/__init__.py +20 -0
- openenergyid/dyntar/const.py +31 -0
- openenergyid/dyntar/main.py +313 -0
- openenergyid/dyntar/models.py +101 -0
- openenergyid/elia/__init__.py +4 -0
- openenergyid/elia/api.py +91 -0
- openenergyid/elia/const.py +18 -0
- openenergyid/energysharing/__init__.py +12 -0
- openenergyid/energysharing/const.py +8 -0
- openenergyid/energysharing/data_formatting.py +77 -0
- openenergyid/energysharing/main.py +122 -0
- openenergyid/energysharing/models.py +80 -0
- openenergyid/enums.py +16 -0
- openenergyid/models.py +174 -0
- openenergyid/mvlr/__init__.py +19 -0
- openenergyid/mvlr/helpers.py +30 -0
- openenergyid/mvlr/main.py +34 -0
- openenergyid/mvlr/models.py +227 -0
- openenergyid/mvlr/mvlr.py +450 -0
- 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.31.dist-info/METADATA +32 -0
- openenergyid-0.1.31.dist-info/RECORD +50 -0
- openenergyid-0.1.31.dist-info/WHEEL +5 -0
- openenergyid-0.1.31.dist-info/licenses/LICENSE +21 -0
- openenergyid-0.1.31.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Weather data utilities for PV simulation.
|
|
3
|
+
|
|
4
|
+
Provides functions to retrieve and process weather data for PV simulations,
|
|
5
|
+
including timezone-aware handling and leap year adjustments.
|
|
6
|
+
"""
|
|
7
|
+
|
|
8
|
+
import datetime as dt
|
|
9
|
+
import typing
|
|
10
|
+
|
|
11
|
+
import pandas as pd
|
|
12
|
+
import pvlib
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
def get_utc_offset_on_1_jan(timezone: str) -> int:
|
|
16
|
+
"""
|
|
17
|
+
Get the UTC offset in hours for the given timezone on January 1st.
|
|
18
|
+
|
|
19
|
+
Args:
|
|
20
|
+
timezone: The timezone string (e.g., "Europe/Amsterdam").
|
|
21
|
+
|
|
22
|
+
Returns:
|
|
23
|
+
The UTC offset in hours as an integer.
|
|
24
|
+
|
|
25
|
+
Raises:
|
|
26
|
+
ValueError: If the UTC offset cannot be determined.
|
|
27
|
+
"""
|
|
28
|
+
jan_first = pd.Timestamp("2020-01-01 00:00:00", tz=timezone)
|
|
29
|
+
utc_offset = jan_first.utcoffset()
|
|
30
|
+
if utc_offset is None:
|
|
31
|
+
raise ValueError(f"Could not determine UTC offset for timezone {timezone}")
|
|
32
|
+
return int(utc_offset.total_seconds() / 3600)
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def get_weather(
|
|
36
|
+
latitude: float, longitude: float, start: dt.date, end: dt.date, tz: str
|
|
37
|
+
) -> pd.DataFrame:
|
|
38
|
+
"""
|
|
39
|
+
Retrieve and process weather data for the specified location and date range.
|
|
40
|
+
|
|
41
|
+
Downloads a "normal year" TMY dataset from PVGIS, aligns it to the requested
|
|
42
|
+
date range and timezone, and handles leap year adjustments.
|
|
43
|
+
|
|
44
|
+
Args:
|
|
45
|
+
latitude: Latitude of the location.
|
|
46
|
+
longitude: Longitude of the location.
|
|
47
|
+
start: Start date (inclusive).
|
|
48
|
+
end: End date (exclusive).
|
|
49
|
+
tz: Timezone string.
|
|
50
|
+
|
|
51
|
+
Returns:
|
|
52
|
+
A pandas DataFrame indexed by timestamp with weather data.
|
|
53
|
+
"""
|
|
54
|
+
# Get a "normal year" from pvgis, it will be indexed in 1990
|
|
55
|
+
utc_offset = get_utc_offset_on_1_jan(tz)
|
|
56
|
+
weather = pvlib.iotools.get_pvgis_tmy(
|
|
57
|
+
latitude=latitude, longitude=longitude, roll_utc_offset=utc_offset
|
|
58
|
+
)[0]
|
|
59
|
+
weather = typing.cast(pd.DataFrame, weather)
|
|
60
|
+
weather = weather.tz_convert(tz)
|
|
61
|
+
weather.index.name = None
|
|
62
|
+
|
|
63
|
+
# Check if 29 februari is included in the weather data
|
|
64
|
+
weather_index = typing.cast(pd.DatetimeIndex, weather.index)
|
|
65
|
+
leap_included = "02-29" in weather_index.strftime("%m-%d").unique()
|
|
66
|
+
|
|
67
|
+
# Construct our desired index
|
|
68
|
+
new_index = pd.date_range(start, end, freq="15min", tz=tz)
|
|
69
|
+
temp_df = pd.DataFrame(index=new_index)
|
|
70
|
+
# Add a key that doesn't contain year
|
|
71
|
+
temp_df["tkey"] = new_index.tz_convert("UTC").strftime("%m-%dT%H:%M:%S%z")
|
|
72
|
+
temp_df["timestamp"] = new_index
|
|
73
|
+
if not leap_included:
|
|
74
|
+
# Replace all tkey's starting with "02-29" with "02-28"
|
|
75
|
+
temp_df.loc[temp_df.tkey.str.startswith("02-29"), "tkey"] = temp_df.loc[
|
|
76
|
+
temp_df.tkey.str.startswith("02-29"), "tkey"
|
|
77
|
+
].str.replace("02-29", "02-28")
|
|
78
|
+
|
|
79
|
+
# Add the key to the weather frame and join
|
|
80
|
+
weather["tkey"] = weather_index.tz_convert("UTC").strftime("%m-%dT%H:%M:%S%z")
|
|
81
|
+
df = (
|
|
82
|
+
pd.merge(temp_df, weather, on="tkey", how="left")
|
|
83
|
+
.set_index("timestamp")
|
|
84
|
+
.drop(columns=["tkey"])
|
|
85
|
+
)
|
|
86
|
+
|
|
87
|
+
# Interpolate and drop last value
|
|
88
|
+
df = df.interpolate(method="time")
|
|
89
|
+
df = df.iloc[:-1]
|
|
90
|
+
|
|
91
|
+
return df
|
openenergyid/sim/main.py
ADDED
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
"""Generic Simulation Analysis Module."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Union
|
|
4
|
+
|
|
5
|
+
import aiohttp
|
|
6
|
+
from pydantic import BaseModel, Field
|
|
7
|
+
|
|
8
|
+
from ..abstractsim import SimulationSummary, Simulator
|
|
9
|
+
from ..pvsim import PVSimulationInput, apply_simulation
|
|
10
|
+
from ..pvsim import get_simulator as get_pv_simulator
|
|
11
|
+
from ..simeval import EvaluationInput, compare_results, evaluate
|
|
12
|
+
from ..simeval.models import Frequency
|
|
13
|
+
|
|
14
|
+
# Here we define all types of simulations
|
|
15
|
+
SimulationInput = Annotated[Union[PVSimulationInput], Field(discriminator="type")]
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def get_simulator(input_: SimulationInput) -> Simulator:
|
|
19
|
+
"""Get an instance of the simulator based on the input data."""
|
|
20
|
+
# Only PV simulators for now
|
|
21
|
+
return get_pv_simulator(input_)
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
class ExAnteData(EvaluationInput):
|
|
25
|
+
"""Ex-ante data for simulation analysis."""
|
|
26
|
+
|
|
27
|
+
|
|
28
|
+
class FullSimulationInput(BaseModel):
|
|
29
|
+
"""Full input for running a simulation analysis."""
|
|
30
|
+
|
|
31
|
+
ex_ante_data: ExAnteData
|
|
32
|
+
simulation_parameters: SimulationInput
|
|
33
|
+
timezone: str = "Europe/Brussels"
|
|
34
|
+
return_frequencies: list[Frequency] | None = Field(
|
|
35
|
+
default=None,
|
|
36
|
+
examples=["MS", "W-MON"],
|
|
37
|
+
description="Optional list of frequencies that should be included in the analysis. Be default, only `total` is included, but you can add more here. Uses the Pandas freqstr.",
|
|
38
|
+
)
|
|
39
|
+
|
|
40
|
+
|
|
41
|
+
async def run_simulation(
|
|
42
|
+
input_: FullSimulationInput, session: aiohttp.ClientSession
|
|
43
|
+
) -> SimulationSummary:
|
|
44
|
+
"""Run the full simulation analysis workflow."""
|
|
45
|
+
df = input_.ex_ante_data.to_pandas(timezone=input_.timezone)
|
|
46
|
+
|
|
47
|
+
ex_ante_eval = evaluate(df, return_frequencies=input_.return_frequencies)
|
|
48
|
+
|
|
49
|
+
simulator: Simulator = get_simulator(input_.simulation_parameters)
|
|
50
|
+
await simulator.load_resources(session=session)
|
|
51
|
+
|
|
52
|
+
sim_eval = evaluate(simulator.result_as_frame(), return_frequencies=input_.return_frequencies)
|
|
53
|
+
|
|
54
|
+
df_post = apply_simulation(df, simulator.simulation_results)
|
|
55
|
+
|
|
56
|
+
post_eval = evaluate(df_post, return_frequencies=input_.return_frequencies)
|
|
57
|
+
|
|
58
|
+
comparison = compare_results(ex_ante_eval, post_eval)
|
|
59
|
+
|
|
60
|
+
summary = SimulationSummary.from_simulation(
|
|
61
|
+
ex_ante=ex_ante_eval,
|
|
62
|
+
simulation_result=sim_eval,
|
|
63
|
+
ex_post=post_eval,
|
|
64
|
+
comparison=comparison,
|
|
65
|
+
)
|
|
66
|
+
|
|
67
|
+
return summary
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
"""Module for evaluating energy simulation data."""
|
|
2
|
+
|
|
3
|
+
import typing
|
|
4
|
+
|
|
5
|
+
import numpy as np
|
|
6
|
+
import pandas as pd
|
|
7
|
+
|
|
8
|
+
from .. import const
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
def evaluate(
|
|
12
|
+
data: pd.DataFrame, return_frequencies: list[str] | None = None
|
|
13
|
+
) -> dict[str, pd.DataFrame | pd.Series]:
|
|
14
|
+
"""Evaluate the data and return resampled results.
|
|
15
|
+
|
|
16
|
+
Args:
|
|
17
|
+
data: A pandas DataFrame containing time series data with columns:
|
|
18
|
+
- electricity_delivered
|
|
19
|
+
- electricity_exported
|
|
20
|
+
- electricity_produced
|
|
21
|
+
return_frequencies: List of pandas offset aliases for resampling frequencies.
|
|
22
|
+
Defaults to ['MS'] (Month Start) if None.
|
|
23
|
+
|
|
24
|
+
Returns:
|
|
25
|
+
A dictionary with keys as frequencies and values as resampled DataFrames.
|
|
26
|
+
"""
|
|
27
|
+
evaluator = Evaluator(data=data, return_frequencies=return_frequencies)
|
|
28
|
+
return evaluator.evaluate()
|
|
29
|
+
|
|
30
|
+
|
|
31
|
+
class Evaluator:
|
|
32
|
+
"""Evaluator for basic energy system evaluation."""
|
|
33
|
+
|
|
34
|
+
def __init__(self, data: pd.DataFrame, return_frequencies: list[str] | None = None):
|
|
35
|
+
"""Initialize the evaluator with data and return frequencies."""
|
|
36
|
+
self.data = data.copy()
|
|
37
|
+
if return_frequencies is None:
|
|
38
|
+
self.return_frequencies = []
|
|
39
|
+
else:
|
|
40
|
+
self.return_frequencies = return_frequencies
|
|
41
|
+
|
|
42
|
+
def evaluate(self) -> dict[str, pd.DataFrame | pd.Series]:
|
|
43
|
+
"""Evaluate the data and return resampled results."""
|
|
44
|
+
if const.ELECTRICITY_DELIVERED not in self.data.columns:
|
|
45
|
+
self.data[const.ELECTRICITY_DELIVERED] = float("NaN")
|
|
46
|
+
if const.ELECTRICITY_EXPORTED not in self.data.columns:
|
|
47
|
+
self.data[const.ELECTRICITY_EXPORTED] = float("NaN")
|
|
48
|
+
if const.ELECTRICITY_PRODUCED not in self.data.columns:
|
|
49
|
+
self.data[const.ELECTRICITY_PRODUCED] = float("NaN")
|
|
50
|
+
if const.PRICE_ELECTRICITY_DELIVERED not in self.data.columns:
|
|
51
|
+
self.data[const.PRICE_ELECTRICITY_DELIVERED] = float("NaN")
|
|
52
|
+
if const.PRICE_ELECTRICITY_EXPORTED not in self.data.columns:
|
|
53
|
+
self.data[const.PRICE_ELECTRICITY_EXPORTED] = float("NaN")
|
|
54
|
+
|
|
55
|
+
# Add electricy_consumed
|
|
56
|
+
self.data[const.ELECTRICITY_CONSUMED] = (
|
|
57
|
+
self.data[const.ELECTRICITY_DELIVERED]
|
|
58
|
+
- self.data[const.ELECTRICITY_EXPORTED].fillna(0.0)
|
|
59
|
+
+ self.data[const.ELECTRICITY_PRODUCED].fillna(0.0)
|
|
60
|
+
).clip(lower=0)
|
|
61
|
+
|
|
62
|
+
# Add electricy_self_consumed
|
|
63
|
+
self.data[const.ELECTRICITY_SELF_CONSUMED] = (
|
|
64
|
+
self.data[const.ELECTRICITY_PRODUCED] - self.data[const.ELECTRICITY_EXPORTED]
|
|
65
|
+
).clip(lower=0)
|
|
66
|
+
|
|
67
|
+
# Add costs
|
|
68
|
+
self.data[const.COST_ELECTRICITY_DELIVERED] = (
|
|
69
|
+
self.data[const.ELECTRICITY_DELIVERED] * self.data[const.PRICE_ELECTRICITY_DELIVERED]
|
|
70
|
+
)
|
|
71
|
+
self.data[const.EARNINGS_ELECTRICITY_EXPORTED] = (
|
|
72
|
+
self.data[const.ELECTRICITY_EXPORTED] * self.data[const.PRICE_ELECTRICITY_EXPORTED]
|
|
73
|
+
)
|
|
74
|
+
self.data[const.COST_ELECTRICITY_NET] = (
|
|
75
|
+
self.data[const.COST_ELECTRICITY_DELIVERED]
|
|
76
|
+
- self.data[const.EARNINGS_ELECTRICITY_EXPORTED]
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
# Calculate sums
|
|
80
|
+
results: dict[str, pd.DataFrame | pd.Series] = {}
|
|
81
|
+
results["total"] = (
|
|
82
|
+
self.data[
|
|
83
|
+
[
|
|
84
|
+
const.ELECTRICITY_DELIVERED,
|
|
85
|
+
const.ELECTRICITY_EXPORTED,
|
|
86
|
+
const.ELECTRICITY_PRODUCED,
|
|
87
|
+
const.ELECTRICITY_CONSUMED,
|
|
88
|
+
const.ELECTRICITY_SELF_CONSUMED,
|
|
89
|
+
const.COST_ELECTRICITY_DELIVERED,
|
|
90
|
+
const.EARNINGS_ELECTRICITY_EXPORTED,
|
|
91
|
+
const.COST_ELECTRICITY_NET,
|
|
92
|
+
]
|
|
93
|
+
]
|
|
94
|
+
.dropna(axis=1, how="all")
|
|
95
|
+
.sum()
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
for freq in self.return_frequencies:
|
|
99
|
+
resampled = (
|
|
100
|
+
self.data[
|
|
101
|
+
[
|
|
102
|
+
const.ELECTRICITY_DELIVERED,
|
|
103
|
+
const.ELECTRICITY_EXPORTED,
|
|
104
|
+
const.ELECTRICITY_PRODUCED,
|
|
105
|
+
const.ELECTRICITY_CONSUMED,
|
|
106
|
+
const.ELECTRICITY_SELF_CONSUMED,
|
|
107
|
+
const.COST_ELECTRICITY_DELIVERED,
|
|
108
|
+
const.EARNINGS_ELECTRICITY_EXPORTED,
|
|
109
|
+
const.COST_ELECTRICITY_NET,
|
|
110
|
+
]
|
|
111
|
+
]
|
|
112
|
+
.dropna(axis=1, how="all")
|
|
113
|
+
.resample(freq)
|
|
114
|
+
.sum()
|
|
115
|
+
)
|
|
116
|
+
results[freq] = resampled
|
|
117
|
+
|
|
118
|
+
# Add ratios
|
|
119
|
+
for _, frame in results.items():
|
|
120
|
+
if const.ELECTRICITY_SELF_CONSUMED in frame:
|
|
121
|
+
frame[const.RATIO_SELF_CONSUMPTION] = np.divide(
|
|
122
|
+
frame[const.ELECTRICITY_SELF_CONSUMED], frame[const.ELECTRICITY_PRODUCED]
|
|
123
|
+
)
|
|
124
|
+
frame[const.RATIO_SELF_SUFFICIENCY] = np.divide(
|
|
125
|
+
frame[const.ELECTRICITY_SELF_CONSUMED],
|
|
126
|
+
frame[const.ELECTRICITY_CONSUMED],
|
|
127
|
+
)
|
|
128
|
+
return results
|
|
129
|
+
|
|
130
|
+
|
|
131
|
+
def compare_results(
|
|
132
|
+
res_1: dict[str, pd.DataFrame | pd.Series], res_2: dict[str, pd.DataFrame | pd.Series]
|
|
133
|
+
) -> dict[str, dict[str, pd.Series | pd.DataFrame]]:
|
|
134
|
+
"""Compare two evaluation results and return the differences."""
|
|
135
|
+
results = {}
|
|
136
|
+
for key in res_1.keys():
|
|
137
|
+
if key in res_2:
|
|
138
|
+
df_1 = res_1[key]
|
|
139
|
+
df_2 = res_2[key]
|
|
140
|
+
if isinstance(df_1, pd.Series) and isinstance(df_2, pd.Series):
|
|
141
|
+
df_1 = df_1.to_frame().T
|
|
142
|
+
df_2 = df_2.to_frame().T
|
|
143
|
+
df_1, df_2 = typing.cast(pd.DataFrame, df_1), typing.cast(pd.DataFrame, df_2)
|
|
144
|
+
diff = df_2 - df_1
|
|
145
|
+
results[key] = {}
|
|
146
|
+
results[key]["diff"] = diff.dropna(how="all", axis=1).squeeze(axis=0)
|
|
147
|
+
results[key]["ratio_diff"] = (diff / df_1).dropna(how="all", axis=1).squeeze(axis=0)
|
|
148
|
+
return results
|
|
@@ -0,0 +1,162 @@
|
|
|
1
|
+
"""Models for basic energy system evaluation."""
|
|
2
|
+
|
|
3
|
+
from typing import Annotated, Literal, Union
|
|
4
|
+
|
|
5
|
+
from pydantic import BaseModel, Field, RootModel, StringConstraints, confloat, conlist
|
|
6
|
+
|
|
7
|
+
from ..models import TimeDataFrame
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class EvaluationInput(TimeDataFrame):
|
|
11
|
+
"""Input frame for basic energy system evaluation."""
|
|
12
|
+
|
|
13
|
+
columns: list[
|
|
14
|
+
Literal[
|
|
15
|
+
"electricity_delivered",
|
|
16
|
+
"electricity_exported",
|
|
17
|
+
"electricity_produced",
|
|
18
|
+
"price_electricity_delivered",
|
|
19
|
+
"price_electricity_exported",
|
|
20
|
+
]
|
|
21
|
+
] = Field(min_length=1, max_length=5)
|
|
22
|
+
data: list[conlist(item_type=confloat(allow_inf_nan=True), min_length=1, max_length=5)] # type: ignore
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class EvaluationOutput(TimeDataFrame):
|
|
26
|
+
"""Output frame for basic energy system evaluation."""
|
|
27
|
+
|
|
28
|
+
columns: list[
|
|
29
|
+
Literal[
|
|
30
|
+
"electricity_delivered",
|
|
31
|
+
"electricity_exported",
|
|
32
|
+
"electricity_produced",
|
|
33
|
+
"electricity_consumed",
|
|
34
|
+
"electricity_self_consumed",
|
|
35
|
+
"cost_electricity_delivered",
|
|
36
|
+
"earnings_electricity_exported",
|
|
37
|
+
"cost_electricity_net",
|
|
38
|
+
"ratio_self_consumption",
|
|
39
|
+
"ratio_self_sufficiency",
|
|
40
|
+
]
|
|
41
|
+
] = Field(min_length=1, max_length=10)
|
|
42
|
+
data: list[conlist(item_type=confloat(allow_inf_nan=True), min_length=1, max_length=10)] # type: ignore
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
# ---------- Reusable bits ----------
|
|
46
|
+
|
|
47
|
+
Frequency = Annotated[
|
|
48
|
+
str,
|
|
49
|
+
StringConstraints(pattern=r"^(\d+min|H|D|W(?:-[A-Z]{3})?|MS|M|Q|QS|A|AS)$"),
|
|
50
|
+
Field(
|
|
51
|
+
title="Frequency key",
|
|
52
|
+
description=(
|
|
53
|
+
"Pandas-style frequency string (freqstr). "
|
|
54
|
+
"Typical examples: '15min', 'H', 'D', 'MS', 'W-MON'."
|
|
55
|
+
),
|
|
56
|
+
examples=["15min", "H", "MS", "W-MON"],
|
|
57
|
+
),
|
|
58
|
+
]
|
|
59
|
+
|
|
60
|
+
Metric = Annotated[
|
|
61
|
+
str,
|
|
62
|
+
Field(
|
|
63
|
+
description="Metric identifier.",
|
|
64
|
+
examples=[
|
|
65
|
+
"electricity_delivered",
|
|
66
|
+
"electricity_exported",
|
|
67
|
+
"electricity_produced",
|
|
68
|
+
"electricity_consumed",
|
|
69
|
+
"electricity_self_consumed",
|
|
70
|
+
"cost_electricity_delivered",
|
|
71
|
+
"earnings_electricity_exported",
|
|
72
|
+
"cost_electricity_net",
|
|
73
|
+
"ratio_self_consumption",
|
|
74
|
+
"ratio_self_sufficiency",
|
|
75
|
+
],
|
|
76
|
+
),
|
|
77
|
+
]
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
class MetricSummary(RootModel[dict[Metric, float]]):
|
|
81
|
+
"""Total/aggregate values per metric (e.g. { 'electricity_delivered': 123.4 })."""
|
|
82
|
+
|
|
83
|
+
root: dict[Metric, float]
|
|
84
|
+
|
|
85
|
+
|
|
86
|
+
# ---------- Eval payloads ----------
|
|
87
|
+
# We model “either { 'total': MetricSummary } OR { '<freq>': EvaluationOutput, ... }”
|
|
88
|
+
# as two schemas and present them via oneOf.
|
|
89
|
+
|
|
90
|
+
|
|
91
|
+
class EvalTotals(BaseModel):
|
|
92
|
+
"""Totals per metric (no time series)."""
|
|
93
|
+
|
|
94
|
+
total: MetricSummary = Field(
|
|
95
|
+
..., description="Aggregate totals per metric over the full evaluation window."
|
|
96
|
+
)
|
|
97
|
+
|
|
98
|
+
|
|
99
|
+
class EvalByFrequency(RootModel[dict[Frequency, EvaluationOutput]]):
|
|
100
|
+
"""Per-frequency time series results."""
|
|
101
|
+
|
|
102
|
+
root: dict[Frequency, EvaluationOutput]
|
|
103
|
+
|
|
104
|
+
|
|
105
|
+
EvalPayload = Annotated[
|
|
106
|
+
Union[EvalTotals, EvalByFrequency],
|
|
107
|
+
Field(description="Either totals-only, or per-frequency time series results."),
|
|
108
|
+
]
|
|
109
|
+
|
|
110
|
+
# ---------- Comparison payloads ----------
|
|
111
|
+
# Shape:
|
|
112
|
+
# EITHER { "total": { "diff": MetricSummary } } or { "total": { "ratio_diff": MetricSummary } }
|
|
113
|
+
# OR { "<freq>": { "diff": EvaluationOutput } } or { "<freq>": { "ratio_diff": EvaluationOutput } }
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
class DiffTotal(BaseModel):
|
|
117
|
+
"""Totals-level absolute differences."""
|
|
118
|
+
|
|
119
|
+
diff: MetricSummary = Field(..., description="Absolute differences of totals.")
|
|
120
|
+
|
|
121
|
+
|
|
122
|
+
class RatioDiffTotal(BaseModel):
|
|
123
|
+
"""Totals-level relative (ratio) differences."""
|
|
124
|
+
|
|
125
|
+
ratio_diff: MetricSummary = Field(..., description="Relative (ratio) differences of totals.")
|
|
126
|
+
|
|
127
|
+
|
|
128
|
+
class DiffTS(BaseModel):
|
|
129
|
+
"""Time series absolute differences at a given frequency."""
|
|
130
|
+
|
|
131
|
+
diff: EvaluationOutput = Field(
|
|
132
|
+
..., description="Absolute differences as a time series at this frequency."
|
|
133
|
+
)
|
|
134
|
+
|
|
135
|
+
|
|
136
|
+
class RatioDiffTS(BaseModel):
|
|
137
|
+
"""Time series relative (ratio) differences at a given frequency."""
|
|
138
|
+
|
|
139
|
+
ratio_diff: EvaluationOutput = Field(
|
|
140
|
+
..., description="Relative (ratio) differences as a time series at this frequency."
|
|
141
|
+
)
|
|
142
|
+
|
|
143
|
+
|
|
144
|
+
# Enforce exclusivity if you decide to combine diff/ratio into one model; here we keep them separate for clean docs.
|
|
145
|
+
|
|
146
|
+
|
|
147
|
+
class ComparisonTotals(BaseModel):
|
|
148
|
+
"""Totals comparison; pick exactly one of diff or ratio_diff by choosing the model."""
|
|
149
|
+
|
|
150
|
+
total: DiffTotal | RatioDiffTotal = Field(..., description="Totals-level comparison.")
|
|
151
|
+
|
|
152
|
+
|
|
153
|
+
class ComparisonByFrequency(RootModel[dict[Frequency, Union[DiffTS, RatioDiffTS]]]):
|
|
154
|
+
"""Per-frequency comparison; for each freq, pick diff or ratio_diff."""
|
|
155
|
+
|
|
156
|
+
root: dict[Frequency, DiffTS | RatioDiffTS]
|
|
157
|
+
|
|
158
|
+
|
|
159
|
+
ComparisonPayload = Annotated[
|
|
160
|
+
Union[ComparisonTotals, ComparisonByFrequency],
|
|
161
|
+
Field(description="Comparison results at totals level or per-frequency."),
|
|
162
|
+
]
|
|
@@ -0,0 +1,32 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: openenergyid
|
|
3
|
+
Version: 0.1.31
|
|
4
|
+
Summary: Open Source Python library for energy analytics and simulations
|
|
5
|
+
Author-email: Jan Pecinovsky <jan@energieid.be>, Max Helskens <max@energieid.be>, Oscar Swyns <oscar@energieid.be>
|
|
6
|
+
License-Expression: MIT
|
|
7
|
+
Keywords: energy,analytics,simulation
|
|
8
|
+
Classifier: Development Status :: 3 - Alpha
|
|
9
|
+
Classifier: Intended Audience :: Developers
|
|
10
|
+
Classifier: Intended Audience :: Science/Research
|
|
11
|
+
Classifier: Natural Language :: English
|
|
12
|
+
Classifier: Operating System :: OS Independent
|
|
13
|
+
Classifier: Programming Language :: Python :: 3
|
|
14
|
+
Classifier: Topic :: Scientific/Engineering
|
|
15
|
+
Classifier: Topic :: Scientific/Engineering :: Mathematics
|
|
16
|
+
Classifier: Topic :: Scientific/Engineering :: Physics
|
|
17
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
18
|
+
Requires-Python: >=3.11
|
|
19
|
+
Description-Content-Type: text/markdown
|
|
20
|
+
License-File: LICENSE
|
|
21
|
+
Requires-Dist: aiohttp>=3.12.15
|
|
22
|
+
Requires-Dist: pandera[polars]>=0.22.1
|
|
23
|
+
Requires-Dist: pvlib>=0.13.0
|
|
24
|
+
Requires-Dist: pydantic>=2.8.2
|
|
25
|
+
Requires-Dist: statsmodels>=0.14.2
|
|
26
|
+
Dynamic: license-file
|
|
27
|
+
|
|
28
|
+
# OpenEnergyID
|
|
29
|
+
|
|
30
|
+
Open Source Python library for energy data analytics and simulations
|
|
31
|
+
|
|
32
|
+
[*more info for developers*](DEVELOPERS.md)
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
openenergyid/__init__.py,sha256=W7FsfPYrI4DcjTcIucigfC7QR_cSuAcx2ocomFDAYzc,193
|
|
2
|
+
openenergyid/const.py,sha256=2cRYP2rF8YtW4vP1nnsyn-JBgmTEfVO1tS6tqJvC6_M,1458
|
|
3
|
+
openenergyid/enums.py,sha256=jdw4CB1gkisx0re_SesrTEyh_T-UxYp6uieE7iYlHdA,357
|
|
4
|
+
openenergyid/models.py,sha256=G3xXbdNewcXSBr-vyvg4pE6zdaI44OrJIWQxgKjwvkg,6313
|
|
5
|
+
openenergyid/abstractsim/__init__.py,sha256=cl-Eh5y_ZOmacZ0X2YW4TeooqR-oD68Fju3UsH9Yziw,228
|
|
6
|
+
openenergyid/abstractsim/abstract.py,sha256=dhUNoaHiKH-ug2mcHUxDINbKMKh7VWMiAnEhPAUAFiE,3252
|
|
7
|
+
openenergyid/baseload/__init__.py,sha256=nEN4IX5m1_X11paXt11G-G9_iJ-ypwccc9NyQhkwtAQ,437
|
|
8
|
+
openenergyid/baseload/analysis.py,sha256=B7hlTE0EVJeRXLYaBjLfqoV2yrBJ-3h7jdIu0JXLGQg,8410
|
|
9
|
+
openenergyid/baseload/exceptions.py,sha256=uPPQlFmOikp3wuwdVxj3Mx-45TzPkLF86rKMFjT5qB4,250
|
|
10
|
+
openenergyid/baseload/models.py,sha256=wnqZWIenguLWcI8-YKIDf6LuQhtSrNi1m0P7QeEKgMI,996
|
|
11
|
+
openenergyid/capacity/__init__.py,sha256=TVWtOBEA2h_Fy17TRYUBmxXUgd00-CJcbObjFkvRPwQ,221
|
|
12
|
+
openenergyid/capacity/main.py,sha256=vANuP4PVPWdfSnCZdEVvJD01RQHRzNr1sD1pkQeu29s,3675
|
|
13
|
+
openenergyid/capacity/models.py,sha256=aFarIPXzaaKqfEzYDVXG44uDaPTapqNsxKHFpysc9N8,834
|
|
14
|
+
openenergyid/dyntar/__init__.py,sha256=lUrk7ktS7yAqiafRHFoBE0RvFSI9mzDoO37diwLHuBg,495
|
|
15
|
+
openenergyid/dyntar/const.py,sha256=eJJV9VfpHlS9vWV47DWQkS3ICIXWhDmG4cU-ofbZJ3Q,1100
|
|
16
|
+
openenergyid/dyntar/main.py,sha256=6vdJ_jBdoDg5ec0vevMjdBS-p3DDCe3vlvie_Y4unIE,10639
|
|
17
|
+
openenergyid/dyntar/models.py,sha256=jqLY_sGG19eYQX2YG0btxdUNhMOYu6mkguBqDKxJCwI,3099
|
|
18
|
+
openenergyid/elia/__init__.py,sha256=shcLGBpzMm6r8teov7YwGD424f-um58jCiVM2yeHycQ,126
|
|
19
|
+
openenergyid/elia/api.py,sha256=Azs7D7Jr4zlHerD7Ueo4bs2P6ZwJ-0JfeNuvFEFfm3I,2772
|
|
20
|
+
openenergyid/elia/const.py,sha256=b0dQHVSHR4O9sztN7C07vijTGoAURERthP8pKn-k8gc,442
|
|
21
|
+
openenergyid/energysharing/__init__.py,sha256=A4JfrUYf-hBCzhUm0qL1GGlNMvpO8OwXJo80dJxFIvw,274
|
|
22
|
+
openenergyid/energysharing/const.py,sha256=X2zEPtTlsmZ66w6RmLS_h8NmdzObAEi5N6-0yrLN5V4,219
|
|
23
|
+
openenergyid/energysharing/data_formatting.py,sha256=47OAKDmsxgrChrU0jqCCMf1MTXK0qUU9HV2znevTg2o,2613
|
|
24
|
+
openenergyid/energysharing/main.py,sha256=nLXwhA6omGBIjNJzcOlqTmYWF3uuBqqsAO9dsUKf4xo,4492
|
|
25
|
+
openenergyid/energysharing/models.py,sha256=1GPigtXffO1EsZhFkHG0OYwGXK71I9xLC1fD5rHxKwQ,2527
|
|
26
|
+
openenergyid/mvlr/__init__.py,sha256=QNLdUqzE3lg4hkMc5Z47ft7DFydQnw-BF1LaLpLLcaY,471
|
|
27
|
+
openenergyid/mvlr/helpers.py,sha256=Uzbfrj3IpH26wA206KOl0hNucKE-n9guJNC_EROBVKA,983
|
|
28
|
+
openenergyid/mvlr/main.py,sha256=X7H1lzKx3JYOQkHR0R3HLqHvCGa8PuI2CafvYquhEZI,1491
|
|
29
|
+
openenergyid/mvlr/models.py,sha256=3Y_y3A5FmJxDEYT4XGpXiuUTMS8p0umPMeXNFRufGAY,8580
|
|
30
|
+
openenergyid/mvlr/mvlr.py,sha256=zcVWsKDfcS2jdfa-45QPu3YCnhL4p14FHuWpQLLwIL4,18626
|
|
31
|
+
openenergyid/pvsim/__init__.py,sha256=gVNEFsEGEMz9SqhrUCH91GWEgYGCWH6hwbztO6I-OfM,204
|
|
32
|
+
openenergyid/pvsim/abstract.py,sha256=IjYndlmvi9VTnUQ0vCDOLc5wB9d1jWnbq-e5zq0Br88,1688
|
|
33
|
+
openenergyid/pvsim/main.py,sha256=1Pcdkgg3NgSPtESqhqaJdlw8hoBTiwV8XjNtIzWq2go,1796
|
|
34
|
+
openenergyid/pvsim/elia/__init__.py,sha256=XVynbFXNWjDhNDuyJJoe6CYKq5dk46IW21sFqzfrPTE,113
|
|
35
|
+
openenergyid/pvsim/elia/main.py,sha256=hqA1Kbsk3yUgR_Oe_iGzsSnaqSvpjm971G6zljeteac,2900
|
|
36
|
+
openenergyid/pvsim/pvlib/__init__.py,sha256=07PCnTLDi9yPUCXiI2Fa25KXtw2KdS65ICkgZZnjzj4,244
|
|
37
|
+
openenergyid/pvsim/pvlib/main.py,sha256=ZpjYyK60vr45KWld5gZVIS3xi_9Ze_Ggka_2zzIhgAc,3145
|
|
38
|
+
openenergyid/pvsim/pvlib/models.py,sha256=a3xomqRu7WwKQLTTZ57EsCPGfHQsgFL-ZdwOSG_tjXE,7421
|
|
39
|
+
openenergyid/pvsim/pvlib/quickscan.py,sha256=7DoUiUzPd4vZKQxL_Uo6bCz_THJ-G1ul9Ni-eS5N6RM,3874
|
|
40
|
+
openenergyid/pvsim/pvlib/weather.py,sha256=D2xbkTYg3mtt7GDoz0uu_ML2U6ElsQHjbvWs28q-5tM,3022
|
|
41
|
+
openenergyid/sim/__init__.py,sha256=gKslrJwVB80Tm1s27OJyNKiouq0CcAt-vvPXhsu2sec,198
|
|
42
|
+
openenergyid/sim/main.py,sha256=93owbBfC2XXXnmN8Vq7EqcwV-5sI_oYgbonyHs9NPZE,2261
|
|
43
|
+
openenergyid/simeval/__init__.py,sha256=hk-q7enbBpxi3g2D9LMYmw7R1ikdte8mYIvPLVg0aBs,252
|
|
44
|
+
openenergyid/simeval/main.py,sha256=5pucS9mGKKeQmM5P-cyYdw4FQtyBXfil6MU4ppWTpGQ,5895
|
|
45
|
+
openenergyid/simeval/models.py,sha256=xUUr2NEXEUGzo6Jq2X-aDEWyVjINouPZhpHxxmYucNo,4964
|
|
46
|
+
openenergyid-0.1.31.dist-info/licenses/LICENSE,sha256=NgRdcNHwyXVCXZ8sJwoTp0DCowThJ9LWWl4xhbV1IUY,1074
|
|
47
|
+
openenergyid-0.1.31.dist-info/METADATA,sha256=F_nGe1ktTCiWcKb30IMeQxprRbD_X9j_Gz6NWMRPy4c,1216
|
|
48
|
+
openenergyid-0.1.31.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
49
|
+
openenergyid-0.1.31.dist-info/top_level.txt,sha256=vf3DmJCXgD_lJjSPFktonUJJT6vEg6KIPCKMPfCWCFI,13
|
|
50
|
+
openenergyid-0.1.31.dist-info/RECORD,,
|
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2023 EnergieID cvba-so
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
openenergyid
|