From 06ab37f46a4e2546ab1f023c13099468cef958b4 Mon Sep 17 00:00:00 2001 From: Giovanni Date: Sun, 19 Jul 2026 15:08:15 +0000 Subject: [PATCH] Simulazione del modello metereologico --- pvsim/weather.py | 921 +++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 921 insertions(+) create mode 100644 pvsim/weather.py diff --git a/pvsim/weather.py b/pvsim/weather.py new file mode 100644 index 0000000..c8893c8 --- /dev/null +++ b/pvsim/weather.py @@ -0,0 +1,921 @@ +```python +""" +weather.py + +Modello meteorologico per il simulatore fotovoltaico. + +Responsabilità: + +- generazione dell'irraggiamento clear-sky; +- generazione di GHI, DNI e DHI; +- simulazione nuvolosità; +- simulazione pioggia; +- simulazione temperatura ambiente; +- ciclicità giornaliera; +- ciclicità stagionale; +- variabilità casuale; +- calcolo dell'irraggiamento sul piano del pannello (POA); +- produzione di un output meteorologico coerente con SunModel. + +Flusso: + + Timestamp + | + v + SunModel + | + +-- Solar Position + | + v + Clear Sky Model + | + +-- DNI + +-- GHI + +-- DHI + | + v + Cloud Model + | + +-- Cloud Factor + | + v + Rain Model + | + v + POA Irradiance + | + v + PVPanel +""" + +from __future__ import annotations + +from dataclasses import dataclass +from datetime import datetime +from typing import Dict, Optional + +import math +import random + +import pandas as pd +import pvlib + +from .sun import SunModel + + +@dataclass +class WeatherModel: + """ + Modello meteorologico del campo fotovoltaico. + + Parameters + ---------- + sun: + Istanza di SunModel utilizzata per la posizione solare. + + clear_sky_model: + Modello clear-sky utilizzato da pvlib. + Valori tipici: "ineichen", "haurwitz", "simplified_solis". + + mean_annual_temperature_C: + Temperatura media annuale del sito [°C]. + + daily_temperature_variation_C: + Ampiezza della variazione giornaliera della temperatura [°C]. + + seasonal_temperature_variation_C: + Ampiezza della variazione stagionale [°C]. + + cloud_enabled: + Abilita la simulazione delle nuvole. + + mean_cloud_factor: + Fattore medio di trasmissione delle nuvole. + + cloud_variability: + Variabilità casuale delle nuvole. + + rain_enabled: + Abilita la simulazione della pioggia. + + rain_probability_daily: + Probabilità giornaliera di pioggia. + + random_seed: + Seed opzionale per rendere la simulazione riproducibile. + """ + + sun: SunModel + + clear_sky_model: str = "ineichen" + + mean_annual_temperature_C: float = 15.0 + + daily_temperature_variation_C: float = 10.0 + + seasonal_temperature_variation_C: float = 12.0 + + cloud_enabled: bool = True + + mean_cloud_factor: float = 0.85 + + cloud_variability: float = 0.15 + + rain_enabled: bool = True + + rain_probability_daily: float = 0.08 + + random_seed: Optional[int] = None + + # Stato interno della pioggia + _rain_active: bool = False + + _rain_factor: float = 1.0 + + # ------------------------------------------------------------------ + # Inizializzazione + # ------------------------------------------------------------------ + + def __post_init__(self) -> None: + """ + Inizializza il generatore casuale. + """ + + if self.random_seed is not None: + + random.seed( + self.random_seed + ) + + # ------------------------------------------------------------------ + # Clear Sky + # ------------------------------------------------------------------ + + def clear_sky( + self, + timestamp: datetime + ) -> Dict[str, float]: + """ + Calcola l'irraggiamento clear-sky tramite pvlib. + + Returns + ------- + Dict + Contiene: + + ghi_Wm2 + dni_Wm2 + dhi_Wm2 + """ + + times = pd.DatetimeIndex( + [ + pd.Timestamp( + timestamp + ) + ] + ) + + if times.tz is None: + + times = times.tz_localize( + self.sun.timezone + ) + + location = ( + self.sun.get_location() + ) + + clear_sky = ( + location.get_clearsky( + + times, + + model= + self.clear_sky_model + ) + ) + + row = clear_sky.iloc[0] + + return { + + "ghi_Wm2": + max( + float( + row["ghi"] + ), + 0.0 + ), + + "dni_Wm2": + max( + float( + row["dni"] + ), + 0.0 + ), + + "dhi_Wm2": + max( + float( + row["dhi"] + ), + 0.0 + ) + } + + # ------------------------------------------------------------------ + # Ciclo stagionale + # ------------------------------------------------------------------ + + def seasonal_temperature( + self, + timestamp: datetime + ) -> float: + """ + Calcola la componente stagionale della temperatura. + + La temperatura massima viene raggiunta indicativamente + durante l'estate e la minima durante l'inverno. + + Formula: + + T_season = + amplitude * + sin(2π * (day_of_year - phase) / 365) + + La fase è scelta per avere il massimo intorno a luglio. + """ + + day_of_year = ( + timestamp.timetuple() + .tm_yday + ) + + phase = 172 + + seasonal = ( + + self.seasonal_temperature_variation_C + + * math.sin( + + 2.0 + * math.pi + * ( + day_of_year + - phase + ) + / 365.25 + ) + ) + + return seasonal + + # ------------------------------------------------------------------ + # Ciclo giornaliero temperatura + # ------------------------------------------------------------------ + + def daily_temperature( + self, + timestamp: datetime + ) -> float: + """ + Calcola la componente giornaliera della temperatura. + + La temperatura minima viene raggiunta intorno all'alba. + La temperatura massima nel primo pomeriggio. + """ + + hour = ( + timestamp.hour + + timestamp.minute / 60.0 + ) + + # Massimo intorno alle 15:00 + phase = 15.0 + + daily = ( + + self.daily_temperature_variation_C + + * math.sin( + + 2.0 + * math.pi + * ( + hour + - phase + + 6.0 + ) + / 24.0 + ) + ) + + return daily + + # ------------------------------------------------------------------ + # Temperatura ambiente + # ------------------------------------------------------------------ + + def ambient_temperature( + self, + timestamp: datetime + ) -> float: + """ + Genera la temperatura ambiente. + + Combina: + + - media annuale; + - variazione stagionale; + - variazione giornaliera; + - rumore casuale. + """ + + seasonal = ( + self.seasonal_temperature( + timestamp + ) + ) + + daily = ( + self.daily_temperature( + timestamp + ) + ) + + noise = random.gauss( + 0.0, + 1.5 + ) + + temperature = ( + + self.mean_annual_temperature_C + + + seasonal + + + daily + + + noise + ) + + return temperature + + # ------------------------------------------------------------------ + # Nuvole + # ------------------------------------------------------------------ + + def cloud_factor( + self, + timestamp: datetime + ) -> float: + """ + Genera un fattore di attenuazione dovuto alle nuvole. + + 1.0: + cielo sereno. + + 0.0: + oscuramento completo. + + Il fattore viene applicato all'irraggiamento clear-sky. + """ + + if not self.cloud_enabled: + + return 1.0 + + if not self.sun.is_daylight( + timestamp + ): + + return 0.0 + + base = ( + self.mean_cloud_factor + ) + + variation = random.gauss( + + 0.0, + + self.cloud_variability + ) + + factor = ( + base + + variation + ) + + return min( + max( + factor, + 0.0 + ), + 1.0 + ) + + # ------------------------------------------------------------------ + # Pioggia + # ------------------------------------------------------------------ + + def update_rain_state( + self, + timestamp: datetime + ) -> None: + """ + Aggiorna lo stato della pioggia. + + La probabilità di inizio pioggia viene valutata + una volta al giorno circa. + + La pioggia può ridurre significativamente + l'irraggiamento disponibile. + """ + + if not self.rain_enabled: + + self._rain_active = False + + self._rain_factor = 1.0 + + return + + # Valutazione approssimata all'inizio della giornata + if ( + timestamp.hour == 0 + and timestamp.minute == 0 + ): + + rain_event = ( + + random.random() + + < self.rain_probability_daily + ) + + self._rain_active = ( + rain_event + ) + + if self._rain_active: + + self._rain_factor = random.uniform( + + 0.15, + + 0.60 + ) + + else: + + self._rain_factor = 1.0 + + # ------------------------------------------------------------------ + + def rain_factor( + self, + timestamp: datetime + ) -> float: + """ + Restituisce il fattore di attenuazione dovuto alla pioggia. + """ + + self.update_rain_state( + timestamp + ) + + if not self._rain_active: + + return 1.0 + + # La pioggia è tipicamente associata + # a maggiore copertura nuvolosa. + return self._rain_factor + + # ------------------------------------------------------------------ + # Irraggiamento globale + # ------------------------------------------------------------------ + + def irradiance( + self, + timestamp: datetime + ) -> Dict[str, float]: + """ + Calcola l'irraggiamento meteorologico. + + Processo: + + Clear Sky + ↓ + Cloud Factor + ↓ + Rain Factor + ↓ + Effective Irradiance + + Returns + ------- + Dict + GHI, DNI, DHI e irraggiamento effettivo. + """ + + clear = ( + self.clear_sky( + timestamp + ) + ) + + cloud = ( + self.cloud_factor( + timestamp + ) + ) + + rain = ( + self.rain_factor( + timestamp + ) + ) + + weather_factor = ( + + cloud + + * rain + ) + + ghi = ( + + clear[ + "ghi_Wm2" + ] + + * weather_factor + ) + + dni = ( + + clear[ + "dni_Wm2" + ] + + * weather_factor + ) + + dhi = ( + + clear[ + "dhi_Wm2" + ] + + * weather_factor + ) + + return { + + "ghi_Wm2": + max( + ghi, + 0.0 + ), + + "dni_Wm2": + max( + dni, + 0.0 + ), + + "dhi_Wm2": + max( + dhi, + 0.0 + ), + + "clear_sky_ghi_Wm2": + clear[ + "ghi_Wm2" + ], + + "clear_sky_dni_Wm2": + clear[ + "dni_Wm2" + ], + + "clear_sky_dhi_Wm2": + clear[ + "dhi_Wm2" + ], + + "cloud_factor": + cloud, + + "rain_factor": + rain, + + "rain_active": + self._rain_active + } + + # ------------------------------------------------------------------ + # Irraggiamento sul piano del pannello + # ------------------------------------------------------------------ + + def plane_of_array_irradiance( + self, + timestamp: datetime, + surface_tilt_deg: float, + surface_azimuth_deg: float + ) -> Dict[str, float]: + """ + Calcola l'irraggiamento sul piano del pannello (POA). + + Vengono utilizzati: + + - DNI; + - DHI; + - GHI; + - posizione del sole; + - inclinazione del pannello; + - azimuth del pannello. + + Returns + ------- + Dict + POA totale e componenti diretta, diffusa e riflessa. + """ + + irradiance = ( + self.irradiance( + timestamp + ) + ) + + solar = ( + self.sun.solar_position( + timestamp + ) + ) + + poa = ( + pvlib.irradiance.get_total_irradiance( + + surface_tilt= + surface_tilt_deg, + + surface_azimuth= + surface_azimuth_deg, + + solar_zenith= + solar[ + "apparent_zenith_deg" + ], + + solar_azimuth= + solar[ + "azimuth_deg" + ], + + dni= + irradiance[ + "dni_Wm2" + ], + + ghi= + irradiance[ + "ghi_Wm2" + ], + + dhi= + irradiance[ + "dhi_Wm2" + ] + ) + ) + + return { + + "poa_global_Wm2": + max( + float( + poa[ + "poa_global" + ] + ), + 0.0 + ), + + "poa_direct_Wm2": + max( + float( + poa[ + "poa_direct" + ] + ), + 0.0 + ), + + "poa_diffuse_Wm2": + max( + float( + poa[ + "poa_diffuse" + ] + ), + 0.0 + ), + + "poa_sky_diffuse_Wm2": + max( + float( + poa[ + "poa_sky_diffuse" + ] + ), + 0.0 + ), + + "poa_ground_diffuse_Wm2": + max( + float( + poa[ + "poa_ground_diffuse" + ] + ), + 0.0 + ) + } + + # ------------------------------------------------------------------ + # Output completo + # ------------------------------------------------------------------ + + def get_conditions( + self, + timestamp: datetime, + surface_tilt_deg: float = 30.0, + surface_azimuth_deg: float = 180.0 + ) -> Dict[str, float]: + """ + Restituisce tutte le condizioni ambientali e solari + necessarie per simulare un pannello. + """ + + solar = ( + self.sun.get_conditions( + + timestamp= + + timestamp, + + surface_tilt_deg= + + surface_tilt_deg, + + surface_azimuth_deg= + + surface_azimuth_deg + ) + ) + + weather = ( + self.irradiance( + timestamp + ) + ) + + poa = ( + self.plane_of_array_irradiance( + + timestamp= + + timestamp, + + surface_tilt_deg= + + surface_tilt_deg, + + surface_azimuth_deg= + + surface_azimuth_deg + ) + ) + + temperature = ( + self.ambient_temperature( + timestamp + ) + ) + + return { + + "timestamp": + timestamp, + + "ambient_temperature_C": + temperature, + + "solar_elevation_deg": + solar[ + "solar_elevation_deg" + ], + + "solar_azimuth_deg": + solar[ + "solar_azimuth_deg" + ], + + "solar_zenith_deg": + solar[ + "solar_zenith_deg" + ], + + "angle_of_incidence_deg": + solar[ + "angle_of_incidence_deg" + ], + + "incidence_factor": + solar[ + "incidence_factor" + ], + + "daylight": + solar[ + "daylight" + ], + + "ghi_Wm2": + weather[ + "ghi_Wm2" + ], + + "dni_Wm2": + weather[ + "dni_Wm2" + ], + + "dhi_Wm2": + weather[ + "dhi_Wm2" + ], + + "poa_global_Wm2": + poa[ + "poa_global_Wm2" + ], + + "poa_direct_Wm2": + poa[ + "poa_direct_Wm2" + ], + + "poa_diffuse_Wm2": + poa[ + "poa_diffuse_Wm2" + ], + + "cloud_factor": + weather[ + "cloud_factor" + ], + + "rain_factor": + weather[ + "rain_factor" + ], + + "rain_active": + weather[ + "rain_active" + ] + } + + # ------------------------------------------------------------------ + # Rappresentazione + # ------------------------------------------------------------------ + + def __repr__( + self + ) -> str: + """ + Rappresentazione leggibile del modello meteorologico. + """ + + return ( + + f"WeatherModel(" + f"location=" + f"{self.sun.latitude}," + f"{self.sun.longitude}, " + f"clear_sky=" + f"'{self.clear_sky_model}', " + f"clouds=" + f"{self.cloud_enabled}, " + f"rain=" + f"{self.rain_enabled})" + ) +```