diff --git a/pvsim/panel.py b/pvsim/panel.py new file mode 100644 index 0000000..bbf611d --- /dev/null +++ b/pvsim/panel.py @@ -0,0 +1,243 @@ +```python +""" +panel.py + +Modello del singolo pannello fotovoltaico. + +Ogni pannello riceve dal simulatore: + +- Irradianza [W/m²] +- Temperatura ambiente [°C] +- Timestamp + +e restituisce le principali grandezze elettriche. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime +from typing import Dict +import random +import math + + +@dataclass +class PVPanel: + + panel_id: str + + nominal_power: float = 450.0 # W + area: float = 2.1 # m² + + vmp: float = 41.5 + imp: float = 10.85 + + voc: float = 49.8 + isc: float = 11.35 + + temp_coeff: float = -0.0035 # -0.35 %/°C + + nominal_temp: float = 25.0 + + annual_degradation: float = 0.005 + + sensor_noise: float = 0.01 + + # stato pannello + enabled: bool = True + soiling_factor: float = 1.0 + degradation: float = 1.0 + + total_energy_Wh: float = 0.0 + + # ------------------------------------------------------------------ + + def module_temperature( + self, + ambient_temperature: float, + irradiance: float + ) -> float: + """ + Stima della temperatura del modulo. + + Formula semplificata: + Tmodule = Tamb + irradiance * 0.03 + """ + + return ambient_temperature + irradiance * 0.03 + + # ------------------------------------------------------------------ + + def degradation_factor( + self, + years: float + ) -> float: + + return (1.0 - self.annual_degradation) ** years + + # ------------------------------------------------------------------ + + def compute_power( + self, + irradiance: float, + ambient_temperature: float, + years_from_start: float + ) -> float: + + if not self.enabled: + return 0.0 + + module_temp = self.module_temperature( + ambient_temperature, + irradiance + ) + + temp_factor = ( + 1 + + self.temp_coeff * + (module_temp - self.nominal_temp) + ) + + degr = self.degradation_factor(years_from_start) + + power = ( + self.nominal_power * + (irradiance / 1000.0) * + temp_factor * + degr * + self.soiling_factor + ) + + power = max(power, 0.0) + + noise = random.gauss(1.0, self.sensor_noise) + + return power * noise + + # ------------------------------------------------------------------ + + def compute_voltage( + self, + power: float + ) -> float: + + if power <= 0: + return 0.0 + + voltage = self.vmp * ( + 0.98 + 0.04 * random.random() + ) + + return voltage + + # ------------------------------------------------------------------ + + def compute_current( + self, + power: float, + voltage: float + ) -> float: + + if voltage <= 0: + return 0.0 + + return power / voltage + + # ------------------------------------------------------------------ + + def update( + self, + timestamp: datetime, + irradiance: float, + ambient_temperature: float, + years_from_start: float, + timestep_minutes: float + ) -> Dict: + + power = self.compute_power( + irradiance, + ambient_temperature, + years_from_start + ) + + voltage = self.compute_voltage(power) + + current = self.compute_current( + power, + voltage + ) + + module_temp = self.module_temperature( + ambient_temperature, + irradiance + ) + + energy = power * timestep_minutes / 60.0 + + self.total_energy_Wh += energy + + return { + + "timestamp": timestamp, + + "panel_id": self.panel_id, + + "irradiance_Wm2": irradiance, + + "ambient_temperature_C": ambient_temperature, + + "module_temperature_C": module_temp, + + "voltage_V": voltage, + + "current_A": current, + + "power_W": power, + + "energy_Wh": energy, + + "total_energy_Wh": self.total_energy_Wh, + + "enabled": self.enabled, + + "soiling_factor": self.soiling_factor, + + "degradation_factor": + self.degradation_factor(years_from_start) + + } + + # ------------------------------------------------------------------ + + def fail(self): + + self.enabled = False + + # ------------------------------------------------------------------ + + def repair(self): + + self.enabled = True + + # ------------------------------------------------------------------ + + def soil(self, loss=0.9): + + self.soiling_factor = loss + + # ------------------------------------------------------------------ + + def clean(self): + + self.soiling_factor = 1.0 + + # ------------------------------------------------------------------ + + def __repr__(self): + + return ( + f"PVPanel(" + f"{self.panel_id}, " + f"{self.nominal_power}W)" + ) \ No newline at end of file