590 lines
8.8 KiB
Python
590 lines
8.8 KiB
Python
"""
|
|
pvsim/utils.py
|
|
|
|
Funzioni di utilità condivise dal progetto PV Simulator.
|
|
|
|
Contiene funzioni per:
|
|
|
|
- gestione timestamp;
|
|
- generazione intervalli temporali;
|
|
- conversione oggetti -> dizionari;
|
|
- conversione oggetti -> DataFrame;
|
|
- aggregazione temporale;
|
|
- gestione directory;
|
|
- conversione sicura dei valori numerici;
|
|
- calcolo di energia;
|
|
- serializzazione JSON.
|
|
|
|
Questo modulo non contiene logica specifica del modello fotovoltaico.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import json
|
|
|
|
from dataclasses import asdict, is_dataclass
|
|
|
|
from datetime import datetime, timedelta
|
|
|
|
from pathlib import Path
|
|
|
|
from typing import Any, Dict, Iterable, List, Optional
|
|
|
|
import pandas as pd
|
|
|
|
|
|
# ======================================================================
|
|
# TIME
|
|
# ======================================================================
|
|
|
|
|
|
def generate_time_range(
|
|
start: datetime,
|
|
end: datetime,
|
|
timestep_minutes: int,
|
|
) -> Iterable[datetime]:
|
|
"""
|
|
Genera una sequenza temporale uniforme.
|
|
|
|
Parameters
|
|
----------
|
|
start:
|
|
Timestamp iniziale.
|
|
|
|
end:
|
|
Timestamp finale, escluso.
|
|
|
|
timestep_minutes:
|
|
Durata del timestep in minuti.
|
|
|
|
Yields
|
|
------
|
|
datetime
|
|
"""
|
|
|
|
if end <= start:
|
|
|
|
raise ValueError(
|
|
"end deve essere maggiore di start"
|
|
)
|
|
|
|
if timestep_minutes <= 0:
|
|
|
|
raise ValueError(
|
|
"timestep_minutes deve essere > 0"
|
|
)
|
|
|
|
current = start
|
|
|
|
delta = timedelta(
|
|
|
|
minutes=timestep_minutes
|
|
)
|
|
|
|
while current < end:
|
|
|
|
yield current
|
|
|
|
current += delta
|
|
|
|
|
|
# ======================================================================
|
|
|
|
|
|
def datetime_to_string(
|
|
value: datetime,
|
|
) -> str:
|
|
"""
|
|
Converte datetime in stringa ISO.
|
|
"""
|
|
|
|
return value.isoformat()
|
|
|
|
|
|
# ======================================================================
|
|
|
|
|
|
def ensure_datetime(
|
|
value: Any,
|
|
) -> datetime:
|
|
"""
|
|
Converte un valore in datetime.
|
|
"""
|
|
|
|
if isinstance(
|
|
value,
|
|
datetime,
|
|
):
|
|
|
|
return value
|
|
|
|
return pd.to_datetime(
|
|
|
|
value
|
|
).to_pydatetime()
|
|
|
|
|
|
# ======================================================================
|
|
# OBJECT CONVERSION
|
|
# ======================================================================
|
|
|
|
|
|
def object_to_dict(
|
|
obj: Any,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Converte un oggetto in dizionario.
|
|
|
|
Supporta:
|
|
|
|
- dataclass;
|
|
- oggetti con to_dict();
|
|
- oggetti con __dict__;
|
|
- dizionari.
|
|
"""
|
|
|
|
if isinstance(
|
|
obj,
|
|
dict,
|
|
):
|
|
|
|
return dict(
|
|
obj
|
|
)
|
|
|
|
if hasattr(
|
|
obj,
|
|
"to_dict",
|
|
):
|
|
|
|
return obj.to_dict()
|
|
|
|
if is_dataclass(
|
|
obj
|
|
):
|
|
|
|
return asdict(
|
|
obj
|
|
)
|
|
|
|
if hasattr(
|
|
obj,
|
|
"__dict__",
|
|
):
|
|
|
|
return dict(
|
|
obj.__dict__
|
|
)
|
|
|
|
raise TypeError(
|
|
f"Tipo non supportato: "
|
|
f"{type(obj)}"
|
|
)
|
|
|
|
|
|
# ======================================================================
|
|
|
|
|
|
def objects_to_dataframe(
|
|
objects: Iterable[Any],
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Converte una sequenza di oggetti in DataFrame.
|
|
"""
|
|
|
|
records = [
|
|
|
|
object_to_dict(
|
|
obj
|
|
)
|
|
|
|
for obj in objects
|
|
]
|
|
|
|
if not records:
|
|
|
|
return pd.DataFrame()
|
|
|
|
return pd.DataFrame(
|
|
records
|
|
)
|
|
|
|
|
|
# ======================================================================
|
|
# DATAFRAME
|
|
# ======================================================================
|
|
|
|
|
|
def normalize_dataframe(
|
|
dataframe: pd.DataFrame,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Normalizza un DataFrame.
|
|
|
|
Operazioni:
|
|
|
|
- copia del DataFrame;
|
|
- conversione timestamp;
|
|
- ordinamento temporale;
|
|
- reset dell'indice.
|
|
"""
|
|
|
|
if dataframe.empty:
|
|
|
|
return dataframe.copy()
|
|
|
|
dataframe = dataframe.copy()
|
|
|
|
if "timestamp" in dataframe.columns:
|
|
|
|
dataframe[
|
|
"timestamp"
|
|
] = pd.to_datetime(
|
|
|
|
dataframe[
|
|
"timestamp"
|
|
]
|
|
)
|
|
|
|
dataframe = dataframe.sort_values(
|
|
|
|
"timestamp"
|
|
)
|
|
|
|
return dataframe.reset_index(
|
|
|
|
drop=True
|
|
)
|
|
|
|
|
|
# ======================================================================
|
|
|
|
|
|
def aggregate_dataframe(
|
|
dataframe: pd.DataFrame,
|
|
frequency: str = "1D",
|
|
numeric_only: bool = True,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Aggrega un DataFrame temporalmente.
|
|
|
|
Esempio:
|
|
|
|
daily = aggregate_dataframe(
|
|
df,
|
|
"1D"
|
|
)
|
|
|
|
Frequenze comuni:
|
|
|
|
5min
|
|
15min
|
|
1H
|
|
1D
|
|
1W
|
|
1ME
|
|
"""
|
|
|
|
if dataframe.empty:
|
|
|
|
return dataframe.copy()
|
|
|
|
if "timestamp" not in dataframe.columns:
|
|
|
|
raise ValueError(
|
|
"Il DataFrame deve contenere "
|
|
"la colonna timestamp"
|
|
)
|
|
|
|
df = normalize_dataframe(
|
|
|
|
dataframe
|
|
)
|
|
|
|
df = df.set_index(
|
|
|
|
"timestamp"
|
|
)
|
|
|
|
result = (
|
|
|
|
df.resample(
|
|
|
|
frequency
|
|
).mean(
|
|
|
|
numeric_only=numeric_only
|
|
)
|
|
)
|
|
|
|
return result.reset_index()
|
|
|
|
|
|
# ======================================================================
|
|
|
|
|
|
def ensure_directory(
|
|
path: str | Path,
|
|
) -> Path:
|
|
"""
|
|
Crea una directory se non esiste.
|
|
"""
|
|
|
|
path = Path(
|
|
path
|
|
)
|
|
|
|
path.mkdir(
|
|
|
|
parents=True,
|
|
|
|
exist_ok=True
|
|
)
|
|
|
|
return path
|
|
|
|
|
|
# ======================================================================
|
|
# NUMBERS
|
|
# ======================================================================
|
|
|
|
|
|
def safe_float(
|
|
value: Any,
|
|
default: float = 0.0,
|
|
) -> float:
|
|
"""
|
|
Converte un valore in float in modo sicuro.
|
|
"""
|
|
|
|
try:
|
|
|
|
if value is None:
|
|
|
|
return default
|
|
|
|
return float(
|
|
value
|
|
)
|
|
|
|
except (
|
|
TypeError,
|
|
ValueError,
|
|
):
|
|
|
|
return default
|
|
|
|
|
|
# ======================================================================
|
|
|
|
|
|
def safe_divide(
|
|
numerator: float,
|
|
denominator: float,
|
|
default: float = 0.0,
|
|
) -> float:
|
|
"""
|
|
Divisione sicura.
|
|
"""
|
|
|
|
if denominator == 0:
|
|
|
|
return default
|
|
|
|
return numerator / denominator
|
|
|
|
|
|
# ======================================================================
|
|
|
|
|
|
def power_to_energy(
|
|
power_W: float,
|
|
timestep_minutes: float,
|
|
) -> float:
|
|
"""
|
|
Converte potenza istantanea in energia.
|
|
|
|
W * h = Wh
|
|
"""
|
|
|
|
timestep_hours = (
|
|
|
|
timestep_minutes
|
|
|
|
/ 60.0
|
|
)
|
|
|
|
return (
|
|
|
|
power_W
|
|
|
|
* timestep_hours
|
|
)
|
|
|
|
|
|
# ======================================================================
|
|
# JSON
|
|
# ======================================================================
|
|
|
|
|
|
def make_json_serializable(
|
|
value: Any,
|
|
) -> Any:
|
|
"""
|
|
Converte ricorsivamente oggetti Python
|
|
in strutture compatibili con JSON.
|
|
"""
|
|
|
|
if isinstance(
|
|
value,
|
|
datetime,
|
|
):
|
|
|
|
return value.isoformat()
|
|
|
|
if is_dataclass(
|
|
value
|
|
):
|
|
|
|
return make_json_serializable(
|
|
|
|
asdict(
|
|
value
|
|
)
|
|
)
|
|
|
|
if isinstance(
|
|
value,
|
|
dict,
|
|
):
|
|
|
|
return {
|
|
|
|
str(key):
|
|
|
|
make_json_serializable(
|
|
item
|
|
)
|
|
|
|
for key, item
|
|
in value.items()
|
|
}
|
|
|
|
if isinstance(
|
|
value,
|
|
(
|
|
list,
|
|
tuple,
|
|
set,
|
|
),
|
|
):
|
|
|
|
return [
|
|
|
|
make_json_serializable(
|
|
item
|
|
)
|
|
|
|
for item in value
|
|
]
|
|
|
|
if hasattr(
|
|
value,
|
|
"item",
|
|
):
|
|
|
|
try:
|
|
|
|
return value.item()
|
|
|
|
except (
|
|
ValueError,
|
|
TypeError,
|
|
):
|
|
|
|
pass
|
|
|
|
return value
|
|
|
|
|
|
# ======================================================================
|
|
|
|
|
|
def save_json(
|
|
data: Any,
|
|
path: str | Path,
|
|
indent: int = 2,
|
|
) -> Path:
|
|
"""
|
|
Salva dati in JSON.
|
|
"""
|
|
|
|
path = Path(
|
|
path
|
|
)
|
|
|
|
ensure_directory(
|
|
|
|
path.parent
|
|
)
|
|
|
|
serializable = (
|
|
|
|
make_json_serializable(
|
|
data
|
|
)
|
|
)
|
|
|
|
with path.open(
|
|
|
|
"w",
|
|
|
|
encoding="utf-8",
|
|
) as file:
|
|
|
|
json.dump(
|
|
|
|
serializable,
|
|
|
|
file,
|
|
|
|
indent=indent,
|
|
|
|
ensure_ascii=False,
|
|
)
|
|
|
|
return path
|
|
|
|
|
|
# ======================================================================
|
|
|
|
|
|
__all__ = [
|
|
|
|
"generate_time_range",
|
|
|
|
"datetime_to_string",
|
|
|
|
"ensure_datetime",
|
|
|
|
"object_to_dict",
|
|
|
|
"objects_to_dataframe",
|
|
|
|
"normalize_dataframe",
|
|
|
|
"aggregate_dataframe",
|
|
|
|
"ensure_directory",
|
|
|
|
"safe_float",
|
|
|
|
"safe_divide",
|
|
|
|
"power_to_energy",
|
|
|
|
"make_json_serializable",
|
|
|
|
"save_json",
|
|
]
|