840 lines
15 KiB
Python
840 lines
15 KiB
Python
"""
|
|
pvsim/statistics.py
|
|
|
|
Modulo per il calcolo delle statistiche e dei KPI
|
|
della simulazione fotovoltaica.
|
|
|
|
Livelli supportati:
|
|
|
|
Plant
|
|
|
|
|
+-- Inverter
|
|
|
|
|
+-- Combiner
|
|
|
|
|
+-- Panel
|
|
|
|
KPI principali:
|
|
|
|
- energia prodotta;
|
|
- potenza media;
|
|
- potenza massima;
|
|
- potenza minima;
|
|
- efficienza;
|
|
- performance ratio;
|
|
- availability;
|
|
- fault count;
|
|
- fault duration;
|
|
- clipping;
|
|
- yield specifico.
|
|
|
|
Il modulo riceve un SimulationResult e non modifica
|
|
i dati della simulazione.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
from dataclasses import dataclass
|
|
|
|
from typing import Any, Dict, Optional
|
|
|
|
import pandas as pd
|
|
|
|
from .simulator import SimulationResult
|
|
|
|
from .utils import (
|
|
normalize_dataframe,
|
|
safe_divide,
|
|
)
|
|
|
|
|
|
# ======================================================================
|
|
# KPI RESULT
|
|
# ======================================================================
|
|
|
|
|
|
@dataclass
|
|
class KPIResult:
|
|
"""
|
|
Contenitore dei KPI principali.
|
|
"""
|
|
|
|
energy_Wh: float = 0.0
|
|
|
|
energy_kWh: float = 0.0
|
|
|
|
peak_power_W: float = 0.0
|
|
|
|
average_power_W: float = 0.0
|
|
|
|
minimum_power_W: float = 0.0
|
|
|
|
nominal_power_W: float = 0.0
|
|
|
|
efficiency: float = 0.0
|
|
|
|
performance_ratio: float = 0.0
|
|
|
|
availability: float = 0.0
|
|
|
|
fault_count: int = 0
|
|
|
|
clipping_loss_Wh: float = 0.0
|
|
|
|
specific_yield_kWh_kWp: float = 0.0
|
|
|
|
def to_dict(
|
|
self,
|
|
) -> Dict[str, Any]:
|
|
|
|
return {
|
|
|
|
"energy_Wh":
|
|
self.energy_Wh,
|
|
|
|
"energy_kWh":
|
|
self.energy_kWh,
|
|
|
|
"peak_power_W":
|
|
self.peak_power_W,
|
|
|
|
"average_power_W":
|
|
self.average_power_W,
|
|
|
|
"minimum_power_W":
|
|
self.minimum_power_W,
|
|
|
|
"nominal_power_W":
|
|
self.nominal_power_W,
|
|
|
|
"efficiency":
|
|
self.efficiency,
|
|
|
|
"performance_ratio":
|
|
self.performance_ratio,
|
|
|
|
"availability":
|
|
self.availability,
|
|
|
|
"fault_count":
|
|
self.fault_count,
|
|
|
|
"clipping_loss_Wh":
|
|
self.clipping_loss_Wh,
|
|
|
|
"specific_yield_kWh_kWp":
|
|
self.specific_yield_kWh_kWp,
|
|
}
|
|
|
|
|
|
# ======================================================================
|
|
# STATISTICS
|
|
# ======================================================================
|
|
|
|
|
|
class SimulationStatistics:
|
|
"""
|
|
Calcola statistiche e KPI su SimulationResult.
|
|
"""
|
|
|
|
def __init__(
|
|
self,
|
|
result: SimulationResult,
|
|
) -> None:
|
|
|
|
self.result = result
|
|
|
|
# ==================================================================
|
|
# DATAFRAME
|
|
# ==================================================================
|
|
|
|
def plant_dataframe(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
|
|
return normalize_dataframe(
|
|
|
|
self.result.plant_dataframe()
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
def inverter_dataframe(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
|
|
return normalize_dataframe(
|
|
|
|
self.result.inverter_dataframe()
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
def combiner_dataframe(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
|
|
return normalize_dataframe(
|
|
|
|
self.result.combiner_dataframe()
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
def panel_dataframe(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
|
|
return normalize_dataframe(
|
|
|
|
self.result.panel_dataframe()
|
|
)
|
|
|
|
# ------------------------------------------------------------------
|
|
|
|
def faults_dataframe(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
|
|
return normalize_dataframe(
|
|
|
|
self.result.faults_dataframe()
|
|
)
|
|
|
|
# ==================================================================
|
|
# PLANT KPI
|
|
# ==================================================================
|
|
|
|
def plant_kpi(
|
|
self,
|
|
) -> KPIResult:
|
|
"""
|
|
Calcola i KPI dell'intero impianto.
|
|
"""
|
|
|
|
df = self.plant_dataframe()
|
|
|
|
if df.empty:
|
|
|
|
return KPIResult()
|
|
|
|
# --------------------------------------------------------------
|
|
# Energia
|
|
# --------------------------------------------------------------
|
|
|
|
if "energy_Wh" in df.columns:
|
|
|
|
energy_Wh = (
|
|
|
|
df[
|
|
"energy_Wh"
|
|
].sum()
|
|
)
|
|
|
|
else:
|
|
|
|
energy_Wh = 0.0
|
|
|
|
# --------------------------------------------------------------
|
|
# Potenza
|
|
# --------------------------------------------------------------
|
|
|
|
power_column = (
|
|
|
|
"effective_ac_power_W"
|
|
|
|
if "effective_ac_power_W"
|
|
in df.columns
|
|
|
|
else "ac_power_W"
|
|
)
|
|
|
|
power = df[
|
|
power_column
|
|
]
|
|
|
|
peak_power = (
|
|
|
|
power.max()
|
|
)
|
|
|
|
average_power = (
|
|
|
|
power.mean()
|
|
)
|
|
|
|
minimum_power = (
|
|
|
|
power.min()
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Nominale
|
|
# --------------------------------------------------------------
|
|
|
|
nominal_power = 0.0
|
|
|
|
if "nominal_power_W" in df.columns:
|
|
|
|
nominal_power = (
|
|
|
|
df[
|
|
"nominal_power_W"
|
|
].max()
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Efficienza
|
|
# --------------------------------------------------------------
|
|
|
|
efficiency = 0.0
|
|
|
|
if (
|
|
|
|
"efficiency"
|
|
|
|
in df.columns
|
|
):
|
|
|
|
efficiency = (
|
|
|
|
df[
|
|
"efficiency"
|
|
].mean()
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Performance Ratio
|
|
# --------------------------------------------------------------
|
|
|
|
performance_ratio = 0.0
|
|
|
|
if (
|
|
|
|
"performance_ratio"
|
|
|
|
in df.columns
|
|
):
|
|
|
|
performance_ratio = (
|
|
|
|
df[
|
|
"performance_ratio"
|
|
].mean()
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Availability
|
|
# --------------------------------------------------------------
|
|
|
|
availability = 0.0
|
|
|
|
if (
|
|
|
|
"availability"
|
|
|
|
in df.columns
|
|
):
|
|
|
|
availability = (
|
|
|
|
df[
|
|
"availability"
|
|
].mean()
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Faults
|
|
# --------------------------------------------------------------
|
|
|
|
faults = self.faults_dataframe()
|
|
|
|
fault_count = len(
|
|
|
|
faults
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Clipping
|
|
# --------------------------------------------------------------
|
|
|
|
clipping_loss_Wh = 0.0
|
|
|
|
if (
|
|
|
|
"clipping_loss_W"
|
|
|
|
in df.columns
|
|
):
|
|
|
|
clipping_loss_Wh = (
|
|
|
|
df[
|
|
"clipping_loss_W"
|
|
].sum()
|
|
)
|
|
|
|
# --------------------------------------------------------------
|
|
# Specific Yield
|
|
# --------------------------------------------------------------
|
|
|
|
energy_kWh = (
|
|
|
|
energy_Wh
|
|
|
|
/ 1000.0
|
|
)
|
|
|
|
nominal_kWp = (
|
|
|
|
nominal_power
|
|
|
|
/ 1000.0
|
|
)
|
|
|
|
specific_yield = (
|
|
|
|
safe_divide(
|
|
|
|
energy_kWh,
|
|
|
|
nominal_kWp
|
|
)
|
|
)
|
|
|
|
return KPIResult(
|
|
|
|
energy_Wh=
|
|
|
|
energy_Wh,
|
|
|
|
energy_kWh=
|
|
|
|
energy_kWh,
|
|
|
|
peak_power_W=
|
|
|
|
peak_power,
|
|
|
|
average_power_W=
|
|
|
|
average_power,
|
|
|
|
minimum_power_W=
|
|
|
|
minimum_power,
|
|
|
|
nominal_power_W=
|
|
|
|
nominal_power,
|
|
|
|
efficiency=
|
|
|
|
efficiency,
|
|
|
|
performance_ratio=
|
|
|
|
performance_ratio,
|
|
|
|
availability=
|
|
|
|
availability,
|
|
|
|
fault_count=
|
|
|
|
fault_count,
|
|
|
|
clipping_loss_Wh=
|
|
|
|
clipping_loss_Wh,
|
|
|
|
specific_yield_kWh_kWp=
|
|
|
|
specific_yield,
|
|
)
|
|
|
|
# ==================================================================
|
|
# GENERIC GROUP KPI
|
|
# ==================================================================
|
|
|
|
def _group_kpi(
|
|
self,
|
|
dataframe: pd.DataFrame,
|
|
group_column: str,
|
|
power_column: str,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Calcola KPI aggregati per componente.
|
|
"""
|
|
|
|
if dataframe.empty:
|
|
|
|
return pd.DataFrame()
|
|
|
|
if group_column not in dataframe.columns:
|
|
|
|
return pd.DataFrame()
|
|
|
|
if power_column not in dataframe.columns:
|
|
|
|
return pd.DataFrame()
|
|
|
|
grouped = (
|
|
|
|
dataframe
|
|
|
|
.groupby(
|
|
|
|
group_column
|
|
)
|
|
|
|
.agg(
|
|
|
|
power_mean_W=(
|
|
|
|
power_column,
|
|
|
|
"mean"
|
|
),
|
|
|
|
power_peak_W=(
|
|
|
|
power_column,
|
|
|
|
"max"
|
|
),
|
|
|
|
power_min_W=(
|
|
|
|
power_column,
|
|
|
|
"min"
|
|
),
|
|
|
|
records=(
|
|
|
|
power_column,
|
|
|
|
"count"
|
|
),
|
|
)
|
|
|
|
.reset_index()
|
|
)
|
|
|
|
return grouped
|
|
|
|
# ==================================================================
|
|
# PANEL KPI
|
|
# ==================================================================
|
|
|
|
def panel_kpi(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
KPI per ogni pannello.
|
|
"""
|
|
|
|
df = self.panel_dataframe()
|
|
|
|
return self._group_kpi(
|
|
|
|
df,
|
|
|
|
"panel_id",
|
|
|
|
"dc_power_W",
|
|
)
|
|
|
|
# ==================================================================
|
|
# COMBINER KPI
|
|
# ==================================================================
|
|
|
|
def combiner_kpi(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
KPI per ogni Combiner Box.
|
|
"""
|
|
|
|
df = self.combiner_dataframe()
|
|
|
|
return self._group_kpi(
|
|
|
|
df,
|
|
|
|
"combiner_id",
|
|
|
|
"dc_power_W",
|
|
)
|
|
|
|
# ==================================================================
|
|
# INVERTER KPI
|
|
# ==================================================================
|
|
|
|
def inverter_kpi(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
KPI per ogni inverter.
|
|
"""
|
|
|
|
df = self.inverter_dataframe()
|
|
|
|
return self._group_kpi(
|
|
|
|
df,
|
|
|
|
"inverter_id",
|
|
|
|
"ac_power_W",
|
|
)
|
|
|
|
# ==================================================================
|
|
# DAILY
|
|
# ==================================================================
|
|
|
|
def daily_statistics(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Calcola statistiche giornaliere.
|
|
"""
|
|
|
|
df = self.plant_dataframe()
|
|
|
|
if df.empty:
|
|
|
|
return pd.DataFrame()
|
|
|
|
df = df.copy()
|
|
|
|
df[
|
|
"date"
|
|
] = df[
|
|
"timestamp"
|
|
].dt.date
|
|
|
|
aggregation = {
|
|
|
|
"ac_power_W":
|
|
[
|
|
"mean",
|
|
"max",
|
|
],
|
|
|
|
"energy_Wh":
|
|
"sum",
|
|
}
|
|
|
|
valid_columns = {
|
|
|
|
key: value
|
|
|
|
for key, value
|
|
in aggregation.items()
|
|
|
|
if key in df.columns
|
|
}
|
|
|
|
if not valid_columns:
|
|
|
|
return pd.DataFrame()
|
|
|
|
return (
|
|
|
|
df
|
|
|
|
.groupby(
|
|
|
|
"date"
|
|
)
|
|
|
|
.agg(
|
|
|
|
valid_columns
|
|
)
|
|
|
|
.reset_index()
|
|
)
|
|
|
|
# ==================================================================
|
|
# MONTHLY
|
|
# ==================================================================
|
|
|
|
def monthly_statistics(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Calcola statistiche mensili.
|
|
"""
|
|
|
|
df = self.plant_dataframe()
|
|
|
|
if df.empty:
|
|
|
|
return pd.DataFrame()
|
|
|
|
df = df.set_index(
|
|
|
|
"timestamp"
|
|
)
|
|
|
|
result = (
|
|
|
|
df.resample(
|
|
|
|
"ME"
|
|
)
|
|
.agg(
|
|
|
|
{
|
|
|
|
"ac_power_W":
|
|
|
|
[
|
|
"mean",
|
|
"max",
|
|
],
|
|
|
|
"energy_Wh":
|
|
|
|
"sum",
|
|
}
|
|
)
|
|
)
|
|
|
|
return result.reset_index()
|
|
|
|
# ==================================================================
|
|
# FAULT SUMMARY
|
|
# ==================================================================
|
|
|
|
def fault_summary(
|
|
self,
|
|
) -> pd.DataFrame:
|
|
"""
|
|
Aggrega i fault per livello e componente.
|
|
"""
|
|
|
|
df = self.faults_dataframe()
|
|
|
|
if df.empty:
|
|
|
|
return pd.DataFrame()
|
|
|
|
columns = [
|
|
|
|
"component_level",
|
|
|
|
"component_id",
|
|
|
|
"fault_type",
|
|
]
|
|
|
|
columns = [
|
|
|
|
column
|
|
|
|
for column
|
|
in columns
|
|
|
|
if column in df.columns
|
|
]
|
|
|
|
if not columns:
|
|
|
|
return pd.DataFrame()
|
|
|
|
return (
|
|
|
|
df
|
|
|
|
.groupby(
|
|
|
|
columns
|
|
)
|
|
|
|
.size()
|
|
|
|
.reset_index(
|
|
|
|
name="fault_count"
|
|
)
|
|
)
|
|
|
|
# ==================================================================
|
|
# FULL REPORT
|
|
# ==================================================================
|
|
|
|
def report(
|
|
self,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Genera un report completo.
|
|
"""
|
|
|
|
plant = self.plant_kpi()
|
|
|
|
return {
|
|
|
|
"plant": plant.to_dict(),
|
|
|
|
"panel_kpi":
|
|
|
|
self.panel_kpi(),
|
|
|
|
"combiner_kpi":
|
|
|
|
self.combiner_kpi(),
|
|
|
|
"inverter_kpi":
|
|
|
|
self.inverter_kpi(),
|
|
|
|
"daily":
|
|
|
|
self.daily_statistics(),
|
|
|
|
"monthly":
|
|
|
|
self.monthly_statistics(),
|
|
|
|
"faults":
|
|
|
|
self.fault_summary(),
|
|
}
|
|
|
|
|
|
# ======================================================================
|
|
# HELPER
|
|
# ======================================================================
|
|
|
|
|
|
def calculate_statistics(
|
|
result: SimulationResult,
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Helper function.
|
|
"""
|
|
|
|
statistics = SimulationStatistics(
|
|
|
|
result
|
|
)
|
|
|
|
return statistics.report()
|
|
|
|
|
|
# ======================================================================
|
|
|
|
|
|
__all__ = [
|
|
|
|
"KPIResult",
|
|
|
|
"SimulationStatistics",
|
|
|
|
"calculate_statistics",
|
|
]
|