921 lines
19 KiB
Python
921 lines
19 KiB
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})"
|
|
)
|
|
```
|