Simulazione del modello metereologico
This commit is contained in:
921
pvsim/weather.py
Normal file
921
pvsim/weather.py
Normal file
@@ -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})"
|
||||
)
|
||||
```
|
||||
Reference in New Issue
Block a user