Files
PV_Simulator/pvsim/sun.py
2026-07-19 15:05:38 +00:00

680 lines
14 KiB
Python

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