Simulatore Irraggiamento
This commit is contained in:
679
pvsim/sun.py
Normal file
679
pvsim/sun.py
Normal file
@@ -0,0 +1,679 @@
|
||||
```python
|
||||
"""
|
||||
sun.py
|
||||
|
||||
Modello della posizione del sole per il simulatore fotovoltaico.
|
||||
|
||||
Utilizza pvlib per calcolare:
|
||||
|
||||
- elevazione solare;
|
||||
- azimuth solare;
|
||||
- zenith solare;
|
||||
- sunrise;
|
||||
- sunset;
|
||||
- durata del giorno;
|
||||
- angolo di incidenza sui pannelli;
|
||||
- fattore geometrico di incidenza.
|
||||
|
||||
Il modulo non simula ancora nuvole, pioggia o irraggiamento.
|
||||
Queste funzionalità saranno gestite successivamente da weather.py.
|
||||
|
||||
Flusso:
|
||||
|
||||
Timestamp
|
||||
|
|
||||
v
|
||||
SunModel
|
||||
|
|
||||
+-- Solar Position
|
||||
|
|
||||
+-- Sunrise / Sunset
|
||||
|
|
||||
+-- Daylight
|
||||
|
|
||||
+-- Panel Geometry
|
||||
|
|
||||
v
|
||||
Weather Model
|
||||
|
|
||||
v
|
||||
PV Panel
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass
|
||||
from datetime import datetime
|
||||
from typing import Dict, Optional
|
||||
|
||||
import numpy as np
|
||||
import pandas as pd
|
||||
import pvlib
|
||||
|
||||
|
||||
@dataclass
|
||||
class SunModel:
|
||||
"""
|
||||
Modello della posizione solare.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
latitude:
|
||||
Latitudine del sito [gradi].
|
||||
|
||||
longitude:
|
||||
Longitudine del sito [gradi].
|
||||
|
||||
timezone:
|
||||
Timezone del sito, ad esempio "Europe/Rome".
|
||||
|
||||
altitude:
|
||||
Altitudine del sito sul livello del mare [m].
|
||||
"""
|
||||
|
||||
latitude: float
|
||||
|
||||
longitude: float
|
||||
|
||||
timezone: str = "UTC"
|
||||
|
||||
altitude: float = 0.0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Creazione localizzazione
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_location(
|
||||
self
|
||||
) -> pvlib.location.Location:
|
||||
"""
|
||||
Restituisce un oggetto pvlib Location.
|
||||
"""
|
||||
|
||||
return pvlib.location.Location(
|
||||
|
||||
latitude=self.latitude,
|
||||
|
||||
longitude=self.longitude,
|
||||
|
||||
tz=self.timezone,
|
||||
|
||||
altitude=self.altitude
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Preparazione timestamp
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def _prepare_timestamp(
|
||||
self,
|
||||
timestamp: datetime
|
||||
) -> pd.DatetimeIndex:
|
||||
"""
|
||||
Converte un datetime in DatetimeIndex timezone-aware.
|
||||
|
||||
Se il datetime non contiene timezone viene interpretato
|
||||
nella timezone configurata per l'impianto.
|
||||
"""
|
||||
|
||||
ts = pd.Timestamp(
|
||||
timestamp
|
||||
)
|
||||
|
||||
if ts.tzinfo is None:
|
||||
|
||||
ts = ts.tz_localize(
|
||||
self.timezone
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
ts = ts.tz_convert(
|
||||
self.timezone
|
||||
)
|
||||
|
||||
return pd.DatetimeIndex(
|
||||
[ts]
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Posizione del sole
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def solar_position(
|
||||
self,
|
||||
timestamp: datetime
|
||||
) -> Dict[str, float]:
|
||||
"""
|
||||
Calcola la posizione del sole per un determinato istante.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict
|
||||
Dizionario contenente:
|
||||
|
||||
- apparent_zenith
|
||||
- zenith
|
||||
- apparent_elevation
|
||||
- elevation
|
||||
- azimuth
|
||||
- equation_of_time
|
||||
"""
|
||||
|
||||
times = (
|
||||
self._prepare_timestamp(
|
||||
timestamp
|
||||
)
|
||||
)
|
||||
|
||||
location = (
|
||||
self.get_location()
|
||||
)
|
||||
|
||||
position = (
|
||||
location.get_solarposition(
|
||||
times
|
||||
)
|
||||
)
|
||||
|
||||
row = position.iloc[0]
|
||||
|
||||
return {
|
||||
|
||||
"apparent_zenith_deg":
|
||||
float(
|
||||
row[
|
||||
"apparent_zenith"
|
||||
]
|
||||
),
|
||||
|
||||
"zenith_deg":
|
||||
float(
|
||||
row[
|
||||
"zenith"
|
||||
]
|
||||
),
|
||||
|
||||
"apparent_elevation_deg":
|
||||
float(
|
||||
row[
|
||||
"apparent_elevation"
|
||||
]
|
||||
),
|
||||
|
||||
"elevation_deg":
|
||||
float(
|
||||
row[
|
||||
"elevation"
|
||||
]
|
||||
),
|
||||
|
||||
"azimuth_deg":
|
||||
float(
|
||||
row[
|
||||
"azimuth"
|
||||
]
|
||||
),
|
||||
|
||||
"equation_of_time_minutes":
|
||||
float(
|
||||
row[
|
||||
"equation_of_time"
|
||||
]
|
||||
)
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Posizione solare per serie temporale
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def solar_position_series(
|
||||
self,
|
||||
timestamps: pd.DatetimeIndex
|
||||
) -> pd.DataFrame:
|
||||
"""
|
||||
Calcola la posizione solare per una serie temporale completa.
|
||||
|
||||
Utile per simulazioni annuali dove è inefficiente calcolare
|
||||
la posizione del sole timestamp per timestamp.
|
||||
"""
|
||||
|
||||
if not isinstance(
|
||||
timestamps,
|
||||
pd.DatetimeIndex
|
||||
):
|
||||
|
||||
timestamps = pd.DatetimeIndex(
|
||||
timestamps
|
||||
)
|
||||
|
||||
if timestamps.tz is None:
|
||||
|
||||
timestamps = timestamps.tz_localize(
|
||||
self.timezone
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
timestamps = timestamps.tz_convert(
|
||||
self.timezone
|
||||
)
|
||||
|
||||
location = (
|
||||
self.get_location()
|
||||
)
|
||||
|
||||
return location.get_solarposition(
|
||||
timestamps
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Giorno / notte
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def is_daylight(
|
||||
self,
|
||||
timestamp: datetime
|
||||
) -> bool:
|
||||
"""
|
||||
Determina se il sole è sopra l'orizzonte.
|
||||
|
||||
Viene utilizzata un'elevazione solare di 0 gradi
|
||||
come soglia geometrica.
|
||||
"""
|
||||
|
||||
position = (
|
||||
self.solar_position(
|
||||
timestamp
|
||||
)
|
||||
)
|
||||
|
||||
return (
|
||||
position[
|
||||
"apparent_elevation_deg"
|
||||
]
|
||||
> 0.0
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def is_sun_above_horizon(
|
||||
self,
|
||||
timestamp: datetime
|
||||
) -> bool:
|
||||
"""
|
||||
Alias esplicito per verificare se il sole è sopra l'orizzonte.
|
||||
"""
|
||||
|
||||
return self.is_daylight(
|
||||
timestamp
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Alba e tramonto
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def sunrise_sunset(
|
||||
self,
|
||||
date: datetime
|
||||
) -> Dict[str, Optional[datetime]]:
|
||||
"""
|
||||
Calcola alba e tramonto per una determinata data.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict
|
||||
Contiene:
|
||||
|
||||
sunrise
|
||||
sunset
|
||||
"""
|
||||
|
||||
location = (
|
||||
self.get_location()
|
||||
)
|
||||
|
||||
day = pd.Timestamp(
|
||||
date
|
||||
)
|
||||
|
||||
if day.tzinfo is None:
|
||||
|
||||
day = day.tz_localize(
|
||||
self.timezone
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
day = day.tz_convert(
|
||||
self.timezone
|
||||
)
|
||||
|
||||
times = pd.date_range(
|
||||
|
||||
start=day.normalize(),
|
||||
|
||||
end=(
|
||||
day.normalize()
|
||||
+ pd.Timedelta(
|
||||
days=1
|
||||
)
|
||||
),
|
||||
|
||||
freq="1min",
|
||||
|
||||
inclusive="left"
|
||||
)
|
||||
|
||||
solar_events = (
|
||||
pvlib.solarposition.sun_rise_set_transit_spa(
|
||||
|
||||
times,
|
||||
|
||||
latitude=
|
||||
self.latitude,
|
||||
|
||||
longitude=
|
||||
self.longitude,
|
||||
|
||||
altitude=
|
||||
self.altitude
|
||||
)
|
||||
)
|
||||
|
||||
# Cerchiamo l'evento relativo al giorno richiesto
|
||||
sunrise = solar_events[
|
||||
"sunrise"
|
||||
].iloc[0]
|
||||
|
||||
sunset = solar_events[
|
||||
"sunset"
|
||||
].iloc[0]
|
||||
|
||||
return {
|
||||
|
||||
"sunrise":
|
||||
sunrise.to_pydatetime()
|
||||
if pd.notna(sunrise)
|
||||
else None,
|
||||
|
||||
"sunset":
|
||||
sunset.to_pydatetime()
|
||||
if pd.notna(sunset)
|
||||
else None
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Durata del giorno
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def daylight_duration_hours(
|
||||
self,
|
||||
date: datetime
|
||||
) -> float:
|
||||
"""
|
||||
Calcola la durata del giorno in ore.
|
||||
"""
|
||||
|
||||
events = (
|
||||
self.sunrise_sunset(
|
||||
date
|
||||
)
|
||||
)
|
||||
|
||||
sunrise = events[
|
||||
"sunrise"
|
||||
]
|
||||
|
||||
sunset = events[
|
||||
"sunset"
|
||||
]
|
||||
|
||||
if (
|
||||
sunrise is None
|
||||
or sunset is None
|
||||
):
|
||||
|
||||
return 0.0
|
||||
|
||||
duration = (
|
||||
sunset - sunrise
|
||||
)
|
||||
|
||||
return (
|
||||
duration.total_seconds()
|
||||
/ 3600.0
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Angolo di incidenza
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def angle_of_incidence(
|
||||
self,
|
||||
timestamp: datetime,
|
||||
surface_tilt_deg: float,
|
||||
surface_azimuth_deg: float
|
||||
) -> float:
|
||||
"""
|
||||
Calcola l'angolo di incidenza della radiazione solare
|
||||
sulla superficie del pannello.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
surface_tilt_deg:
|
||||
Inclinazione del pannello rispetto all'orizzontale.
|
||||
|
||||
surface_azimuth_deg:
|
||||
Azimuth della superficie.
|
||||
|
||||
Convenzione pvlib:
|
||||
180° = Sud
|
||||
90° = Est
|
||||
270° = Ovest
|
||||
0° = Nord
|
||||
|
||||
Returns
|
||||
-------
|
||||
float
|
||||
Angolo di incidenza [gradi].
|
||||
"""
|
||||
|
||||
position = (
|
||||
self.solar_position(
|
||||
timestamp
|
||||
)
|
||||
)
|
||||
|
||||
aoi = (
|
||||
pvlib.irradiance.aoi(
|
||||
|
||||
surface_tilt=
|
||||
surface_tilt_deg,
|
||||
|
||||
surface_azimuth=
|
||||
surface_azimuth_deg,
|
||||
|
||||
solar_zenith=
|
||||
position[
|
||||
"apparent_zenith_deg"
|
||||
],
|
||||
|
||||
solar_azimuth=
|
||||
position[
|
||||
"azimuth_deg"
|
||||
]
|
||||
)
|
||||
)
|
||||
|
||||
return float(
|
||||
aoi
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Fattore geometrico
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def incidence_factor(
|
||||
self,
|
||||
timestamp: datetime,
|
||||
surface_tilt_deg: float,
|
||||
surface_azimuth_deg: float
|
||||
) -> float:
|
||||
"""
|
||||
Calcola un fattore geometrico semplificato
|
||||
basato sull'angolo di incidenza.
|
||||
|
||||
1.0 = incidenza ideale
|
||||
0.0 = sole dietro il pannello / nessuna produzione
|
||||
|
||||
Il fattore è calcolato come cos(AOI).
|
||||
"""
|
||||
|
||||
aoi = (
|
||||
self.angle_of_incidence(
|
||||
|
||||
timestamp=
|
||||
|
||||
timestamp,
|
||||
|
||||
surface_tilt_deg=
|
||||
|
||||
surface_tilt_deg,
|
||||
|
||||
surface_azimuth_deg=
|
||||
|
||||
surface_azimuth_deg
|
||||
)
|
||||
)
|
||||
|
||||
if aoi >= 90.0:
|
||||
|
||||
return 0.0
|
||||
|
||||
factor = np.cos(
|
||||
np.radians(
|
||||
aoi
|
||||
)
|
||||
)
|
||||
|
||||
return float(
|
||||
max(
|
||||
factor,
|
||||
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 principali informazioni solari
|
||||
per un pannello o una superficie fotovoltaica.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict
|
||||
Condizioni solari complete.
|
||||
"""
|
||||
|
||||
position = (
|
||||
self.solar_position(
|
||||
timestamp
|
||||
)
|
||||
)
|
||||
|
||||
daylight = (
|
||||
position[
|
||||
"apparent_elevation_deg"
|
||||
]
|
||||
> 0.0
|
||||
)
|
||||
|
||||
if daylight:
|
||||
|
||||
aoi = (
|
||||
self.angle_of_incidence(
|
||||
|
||||
timestamp,
|
||||
|
||||
surface_tilt_deg,
|
||||
|
||||
surface_azimuth_deg
|
||||
)
|
||||
)
|
||||
|
||||
incidence = (
|
||||
self.incidence_factor(
|
||||
|
||||
timestamp,
|
||||
|
||||
surface_tilt_deg,
|
||||
|
||||
surface_azimuth_deg
|
||||
)
|
||||
)
|
||||
|
||||
else:
|
||||
|
||||
aoi = 90.0
|
||||
|
||||
incidence = 0.0
|
||||
|
||||
return {
|
||||
|
||||
"timestamp":
|
||||
timestamp,
|
||||
|
||||
"solar_elevation_deg":
|
||||
position[
|
||||
"apparent_elevation_deg"
|
||||
],
|
||||
|
||||
"solar_azimuth_deg":
|
||||
position[
|
||||
"azimuth_deg"
|
||||
],
|
||||
|
||||
"solar_zenith_deg":
|
||||
position[
|
||||
"apparent_zenith_deg"
|
||||
],
|
||||
|
||||
"angle_of_incidence_deg":
|
||||
aoi,
|
||||
|
||||
"incidence_factor":
|
||||
incidence,
|
||||
|
||||
"daylight":
|
||||
daylight
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Rappresentazione
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def __repr__(
|
||||
self
|
||||
) -> str:
|
||||
"""
|
||||
Rappresentazione leggibile del modello solare.
|
||||
"""
|
||||
|
||||
return (
|
||||
|
||||
f"SunModel("
|
||||
f"latitude={self.latitude}, "
|
||||
f"longitude={self.longitude}, "
|
||||
f"timezone='{self.timezone}', "
|
||||
f"altitude={self.altitude}m)"
|
||||
)
|
||||
```
|
||||
Reference in New Issue
Block a user