""" 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})" ) ```