diff --git a/pvsim/models.py b/pvsim/models.py new file mode 100644 index 0000000..2cdde22 --- /dev/null +++ b/pvsim/models.py @@ -0,0 +1,1127 @@ +```python +""" +pvsim/models.py + +Modelli dati standardizzati per la telemetria del simulatore +di campo fotovoltaico. + +Gerarchia: + + PVPlant + | + +-- Inverter + | + +-- CombinerBox + | + +-- PVPanel + +I modelli definiti in questo modulo rappresentano i dati prodotti +dalla simulazione a ogni timestep. + +Struttura: + + PanelData + | + v + CombinerData + | + v + InverterData + | + v + PlantData + +Sono inoltre definiti: + + WeatherData + FaultData + SimulationStep + +Obiettivo: + + Separare la logica di simulazione dalla rappresentazione + e serializzazione dei dati. + +Questo permette in futuro di salvare gli stessi modelli in: + + - CSV + - Parquet + - SQLite + - PostgreSQL + - TimescaleDB + - MQTT + - API REST +""" + +from __future__ import annotations + +from dataclasses import asdict, dataclass, field +from datetime import datetime +from typing import Any, Dict, List, Optional + + +# ====================================================================== +# WEATHER DATA +# ====================================================================== + + +@dataclass +class WeatherData: + """ + Condizioni meteorologiche e solari del timestep. + + Tutti i valori sono riferiti allo stesso timestamp. + + Parameters + ---------- + timestamp: + Istante della misura. + + ambient_temperature_C: + Temperatura ambiente [°C]. + + solar_elevation_deg: + Elevazione solare [°]. + + solar_azimuth_deg: + Azimuth solare [°]. + + solar_zenith_deg: + Angolo zenitale solare [°]. + + angle_of_incidence_deg: + Angolo di incidenza sul piano del pannello [°]. + + ghi_Wm2: + Global Horizontal Irradiance [W/m²]. + + dni_Wm2: + Direct Normal Irradiance [W/m²]. + + dhi_Wm2: + Diffuse Horizontal Irradiance [W/m²]. + + poa_global_Wm2: + Irraggiamento globale sul piano del pannello [W/m²]. + + poa_direct_Wm2: + Componente diretta POA [W/m²]. + + poa_diffuse_Wm2: + Componente diffusa POA [W/m²]. + + cloud_factor: + Fattore di attenuazione delle nuvole [0..1]. + + rain_factor: + Fattore di attenuazione della pioggia [0..1]. + + rain_active: + Indica se è attiva la pioggia. + + daylight: + Indica se il sole è sopra l'orizzonte. + """ + + timestamp: datetime + + ambient_temperature_C: float + + solar_elevation_deg: float = 0.0 + + solar_azimuth_deg: float = 0.0 + + solar_zenith_deg: float = 180.0 + + angle_of_incidence_deg: float = 90.0 + + ghi_Wm2: float = 0.0 + + dni_Wm2: float = 0.0 + + dhi_Wm2: float = 0.0 + + poa_global_Wm2: float = 0.0 + + poa_direct_Wm2: float = 0.0 + + poa_diffuse_Wm2: float = 0.0 + + cloud_factor: float = 1.0 + + rain_factor: float = 1.0 + + rain_active: bool = False + + daylight: bool = False + + def to_dict( + self + ) -> Dict[str, Any]: + """ + Converte il modello in un dizionario. + """ + + return asdict( + self + ) + + +# ====================================================================== +# PANEL DATA +# ====================================================================== + + +@dataclass +class PanelData: + """ + Telemetria di un singolo pannello fotovoltaico. + + Questo è il livello più basso della gerarchia. + + Ogni record rappresenta lo stato di un pannello + in un determinato timestep. + + Parameters + ---------- + timestamp: + Istante della misura. + + panel_id: + Identificativo univoco del pannello. + + combiner_id: + Combiner Box a cui appartiene il pannello. + + inverter_id: + Inverter a cui appartiene il pannello. + + dc_power_W: + Potenza DC prodotta [W]. + + dc_voltage_V: + Tensione DC [V]. + + dc_current_A: + Corrente DC [A]. + + nominal_power_W: + Potenza nominale del pannello [W]. + + irradiance_Wm2: + Irraggiamento incidente [W/m²]. + + panel_temperature_C: + Temperatura stimata del pannello [°C]. + + efficiency: + Efficienza istantanea [0..1]. + + performance_ratio: + Rapporto tra produzione effettiva e produzione attesa. + + fault_factor: + Fattore di riduzione dovuto ai fault [0..1]. + + enabled: + Indica se il pannello è operativo. + + fault_active: + Indica se il pannello presenta almeno un fault. + + status: + Stato testuale del pannello. + """ + + timestamp: datetime + + panel_id: str + + combiner_id: str + + inverter_id: str + + dc_power_W: float = 0.0 + + dc_voltage_V: float = 0.0 + + dc_current_A: float = 0.0 + + nominal_power_W: float = 0.0 + + irradiance_Wm2: float = 0.0 + + panel_temperature_C: float = 25.0 + + efficiency: float = 0.0 + + performance_ratio: float = 0.0 + + fault_factor: float = 1.0 + + enabled: bool = True + + fault_active: bool = False + + status: str = "normal" + + def to_dict( + self + ) -> Dict[str, Any]: + """ + Converte il modello in dizionario. + """ + + return asdict( + self + ) + + +# ====================================================================== +# COMBINER DATA +# ====================================================================== + + +@dataclass +class CombinerData: + """ + Telemetria di una Combiner Box. + + La Combiner Box aggrega la produzione DC + di più stringhe o gruppi di pannelli. + + Parameters + ---------- + timestamp: + Istante della misura. + + combiner_id: + Identificativo della Combiner Box. + + inverter_id: + Inverter associato. + + dc_power_W: + Potenza DC totale [W]. + + dc_voltage_V: + Tensione DC [V]. + + dc_current_A: + Corrente DC totale [A]. + + nominal_power_W: + Potenza nominale aggregata [W]. + + panel_count: + Numero totale di pannelli collegati. + + active_panel_count: + Numero di pannelli operativi. + + fault_factor: + Fattore di riduzione dovuto ai fault. + + fault_active: + Indica se è presente almeno un'anomalia. + + availability: + Disponibilità del combiner [0..1]. + + status: + Stato del combiner. + """ + + timestamp: datetime + + combiner_id: str + + inverter_id: str + + dc_power_W: float = 0.0 + + dc_voltage_V: float = 0.0 + + dc_current_A: float = 0.0 + + nominal_power_W: float = 0.0 + + panel_count: int = 0 + + active_panel_count: int = 0 + + fault_factor: float = 1.0 + + fault_active: bool = False + + availability: float = 1.0 + + status: str = "normal" + + def to_dict( + self + ) -> Dict[str, Any]: + """ + Converte il modello in dizionario. + """ + + return asdict( + self + ) + + +# ====================================================================== +# INVERTER DATA +# ====================================================================== + + +@dataclass +class InverterData: + """ + Telemetria di un inverter. + + L'inverter riceve la produzione DC aggregata + dalle Combiner Box e la converte in AC. + + Parameters + ---------- + timestamp: + Istante della misura. + + inverter_id: + Identificativo dell'inverter. + + dc_power_W: + Potenza DC in ingresso [W]. + + ac_power_W: + Potenza AC in uscita [W]. + + dc_voltage_V: + Tensione DC [V]. + + dc_current_A: + Corrente DC [A]. + + ac_voltage_V: + Tensione AC [V]. + + ac_current_A: + Corrente AC [A]. + + efficiency: + Efficienza di conversione [0..1]. + + nominal_power_W: + Potenza nominale dell'inverter [W]. + + clipping_loss_W: + Perdita dovuta al clipping [W]. + + temperature_C: + Temperatura dell'inverter [°C]. + + fault_factor: + Fattore di riduzione dovuto ai fault. + + combiner_count: + Numero di Combiner Box collegate. + + active_combiner_count: + Numero di Combiner Box operative. + + fault_active: + Indica la presenza di un fault. + + availability: + Disponibilità dell'inverter [0..1]. + + status: + Stato operativo. + """ + + timestamp: datetime + + inverter_id: str + + dc_power_W: float = 0.0 + + ac_power_W: float = 0.0 + + dc_voltage_V: float = 0.0 + + dc_current_A: float = 0.0 + + ac_voltage_V: float = 0.0 + + ac_current_A: float = 0.0 + + efficiency: float = 0.0 + + nominal_power_W: float = 0.0 + + clipping_loss_W: float = 0.0 + + temperature_C: float = 25.0 + + fault_factor: float = 1.0 + + combiner_count: int = 0 + + active_combiner_count: int = 0 + + fault_active: bool = False + + availability: float = 1.0 + + status: str = "normal" + + def to_dict( + self + ) -> Dict[str, Any]: + """ + Converte il modello in dizionario. + """ + + return asdict( + self + ) + + +# ====================================================================== +# PLANT DATA +# ====================================================================== + + +@dataclass +class PlantData: + """ + Telemetria aggregata dell'intero campo fotovoltaico. + + Parameters + ---------- + timestamp: + Istante della misura. + + plant_id: + Identificativo dell'impianto. + + dc_power_W: + Potenza DC totale [W]. + + ac_power_W: + Potenza AC totale [W]. + + effective_ac_power_W: + Potenza AC dopo l'applicazione dei fault [W]. + + nominal_power_W: + Potenza nominale installata [W]. + + energy_Wh: + Energia prodotta nel timestep [Wh]. + + cumulative_energy_Wh: + Energia cumulativa [Wh]. + + inverter_count: + Numero di inverter. + + active_inverter_count: + Numero di inverter operativi. + + combiner_count: + Numero di Combiner Box. + + active_combiner_count: + Numero di Combiner Box operative. + + panel_count: + Numero totale di pannelli. + + active_panel_count: + Numero di pannelli operativi. + + efficiency: + Efficienza globale dell'impianto. + + performance_ratio: + Performance Ratio globale. + + availability: + Disponibilità globale. + + fault_factor: + Fattore globale di riduzione. + + fault_active: + Presenza di anomalie. + + status: + Stato dell'impianto. + """ + + timestamp: datetime + + plant_id: str + + dc_power_W: float = 0.0 + + ac_power_W: float = 0.0 + + effective_ac_power_W: float = 0.0 + + nominal_power_W: float = 0.0 + + energy_Wh: float = 0.0 + + cumulative_energy_Wh: float = 0.0 + + inverter_count: int = 0 + + active_inverter_count: int = 0 + + combiner_count: int = 0 + + active_combiner_count: int = 0 + + panel_count: int = 0 + + active_panel_count: int = 0 + + efficiency: float = 0.0 + + performance_ratio: float = 0.0 + + availability: float = 1.0 + + fault_factor: float = 1.0 + + fault_active: bool = False + + status: str = "normal" + + def to_dict( + self + ) -> Dict[str, Any]: + """ + Converte il modello in dizionario. + """ + + return asdict( + self + ) + + +# ====================================================================== +# FAULT DATA +# ====================================================================== + + +@dataclass +class FaultData: + """ + Rappresentazione serializzabile di un fault. + + Questo modello è separato da FaultEvent perché + FaultEvent rappresenta la logica interna del FaultManager, + mentre FaultData rappresenta il dato destinato + allo storage o alla telemetria. + + Parameters + ---------- + timestamp: + Timestamp del rilevamento. + + fault_id: + ID del fault. + + fault_type: + Tipo di fault. + + component_level: + Livello gerarchico. + + component_id: + ID del componente. + + severity: + Severità [0..1]. + + reduction_factor: + Fattore di produzione residua [0..1]. + + description: + Descrizione. + """ + + timestamp: datetime + + fault_id: str + + fault_type: str + + component_level: str + + component_id: str + + severity: float = 0.0 + + reduction_factor: float = 1.0 + + description: str = "" + + def to_dict( + self + ) -> Dict[str, Any]: + """ + Converte il modello in dizionario. + """ + + return asdict( + self + ) + + +# ====================================================================== +# SIMULATION STEP +# ====================================================================== + + +@dataclass +class SimulationStep: + """ + Contenitore di tutti i dati prodotti durante + un singolo timestep. + + Questo modello rappresenta una fotografia completa + dell'impianto in un determinato istante. + + Gerarchia: + + SimulationStep + | + +-- WeatherData + | + +-- PanelData[] + | + +-- CombinerData[] + | + +-- InverterData[] + | + +-- PlantData + | + +-- FaultData[] + """ + + timestamp: datetime + + weather: Optional[ + WeatherData + ] = None + + panels: List[ + PanelData + ] = field( + default_factory=list + ) + + combiners: List[ + CombinerData + ] = field( + default_factory=list + ) + + inverters: List[ + InverterData + ] = field( + default_factory=list + ) + + plant: Optional[ + PlantData + ] = None + + faults: List[ + FaultData + ] = field( + default_factory=list + ) + + # ------------------------------------------------------------------ + # Conversione dizionario + # ------------------------------------------------------------------ + + def to_dict( + self + ) -> Dict[str, Any]: + """ + Converte l'intero timestep in un dizionario. + """ + + return { + + "timestamp": + self.timestamp, + + "weather": + ( + self.weather.to_dict() + if self.weather + else None + ), + + "panels": + [ + panel.to_dict() + + for panel + in self.panels + ], + + "combiners": + [ + combiner.to_dict() + + for combiner + in self.combiners + ], + + "inverters": + [ + inverter.to_dict() + + for inverter + in self.inverters + ], + + "plant": + ( + self.plant.to_dict() + if self.plant + else None + ), + + "faults": + [ + fault.to_dict() + + for fault + in self.faults + ] + } + + +# ====================================================================== +# UTILITY FUNCTIONS +# ====================================================================== + + +def flatten_simulation_step( + step: SimulationStep +) -> Dict[str, List[Dict[str, Any]]]: + """ + Trasforma un SimulationStep in quattro liste + facilmente convertibili in DataFrame. + + Returns + ------- + Dict + Dizionario con: + + weather + panel + combiner + inverter + plant + faults + """ + + return { + + "weather": + + ( + [ + step.weather.to_dict() + ] + + if step.weather is not None + + else [] + ), + + "panel": + + [ + panel.to_dict() + + for panel + in step.panels + ], + + "combiner": + + [ + combiner.to_dict() + + for combiner + in step.combiners + ], + + "inverter": + + [ + inverter.to_dict() + + for inverter + in step.inverters + ], + + "plant": + + ( + [ + step.plant.to_dict() + ] + + if step.plant is not None + + else [] + ), + + "faults": + + [ + fault.to_dict() + + for fault + in step.faults + ] + } + + +# ====================================================================== +# FACTORY FUNCTIONS +# ====================================================================== + + +def create_panel_data( + timestamp: datetime, + panel_id: str, + combiner_id: str, + inverter_id: str, + dc_power_W: float, + nominal_power_W: float, + irradiance_Wm2: float, + panel_temperature_C: float, + fault_factor: float = 1.0, + enabled: bool = True +) -> PanelData: + """ + Factory function per creare un record PanelData. + + La funzione calcola automaticamente: + + - corrente; + - efficienza; + - performance ratio; + - stato. + """ + + # -------------------------------------------------------------- + # Tensione semplificata + # -------------------------------------------------------------- + + dc_voltage_V = 40.0 + + # -------------------------------------------------------------- + # Corrente + # -------------------------------------------------------------- + + if dc_voltage_V > 0: + + dc_current_A = ( + + dc_power_W + + / dc_voltage_V + ) + + else: + + dc_current_A = 0.0 + + # -------------------------------------------------------------- + # Efficienza + # -------------------------------------------------------------- + + if irradiance_Wm2 > 0: + + efficiency = ( + + dc_power_W + + / ( + + irradiance_Wm2 + * 2.0 + ) + ) + + else: + + efficiency = 0.0 + + # -------------------------------------------------------------- + # Performance Ratio + # -------------------------------------------------------------- + + if nominal_power_W > 0: + + performance_ratio = ( + + dc_power_W + + / nominal_power_W + ) + + else: + + performance_ratio = 0.0 + + # -------------------------------------------------------------- + # Stato + # -------------------------------------------------------------- + + if not enabled: + + status = "disabled" + + elif fault_factor < 1.0: + + status = "fault" + + elif dc_power_W <= 0: + + status = "idle" + + else: + + status = "normal" + + return PanelData( + + timestamp= + timestamp, + + panel_id= + panel_id, + + combiner_id= + combiner_id, + + inverter_id= + inverter_id, + + dc_power_W= + dc_power_W, + + dc_voltage_V= + dc_voltage_V, + + dc_current_A= + dc_current_A, + + nominal_power_W= + nominal_power_W, + + irradiance_Wm2= + irradiance_Wm2, + + panel_temperature_C= + panel_temperature_C, + + efficiency= + efficiency, + + performance_ratio= + performance_ratio, + + fault_factor= + fault_factor, + + enabled= + enabled, + + fault_active= + fault_factor < 1.0, + + status= + status + ) + + +# ====================================================================== +# DATAFRAME HELPERS +# ====================================================================== + + +def records_to_dataframe( + records: List[Any] +): + """ + Converte una lista di modelli dati in DataFrame pandas. + + Esempio: + + df = records_to_dataframe( + simulation_result.panel + ) + """ + + import pandas as pd + + if not records: + + return pd.DataFrame() + + return pd.DataFrame( + + [ + record.to_dict() + + for record + in records + ] + ) + + +# ====================================================================== +# EXPORT +# ====================================================================== + + +__all__ = [ + + "WeatherData", + + "PanelData", + + "CombinerData", + + "InverterData", + + "PlantData", + + "FaultData", + + "SimulationStep", + + "flatten_simulation_step", + + "create_panel_data", + + "records_to_dataframe" +] +```