diff --git a/pvsim/inverter.py b/pvsim/inverter.py new file mode 100644 index 0000000..b9fa01e --- /dev/null +++ b/pvsim/inverter.py @@ -0,0 +1,768 @@ +```python +""" +inverter.py + +Modello di un inverter fotovoltaico. + +Gerarchia: + + PV Plant + | + +-- Inverter + | + +-- Combiner Box + | | + | +-- Panels + | + +-- Combiner Box + | + +-- Panels + +Responsabilità principali: + +- aggregare la produzione DC delle Combiner Box; +- convertire DC -> AC; +- applicare il rendimento dell'inverter; +- simulare il clipping della potenza; +- simulare il consumo notturno; +- simulare tensione e corrente AC; +- simulare la frequenza di rete; +- simulare guasti; +- accumulare energia prodotta. +""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from datetime import datetime +from typing import Dict, List +import random + +from .combiner import CombinerBox + + +@dataclass +class Inverter: + """ + Rappresenta un inverter fotovoltaico. + + Parameters + ---------- + inverter_id: + Identificativo univoco dell'inverter. + + nominal_power_kW: + Potenza nominale AC dell'inverter [kW]. + + combiners: + Lista delle Combiner Box collegate all'inverter. + + nominal_efficiency: + Rendimento nominale dell'inverter. + + night_consumption_W: + Consumo interno dell'inverter durante la notte [W]. + + ac_voltage_V: + Tensione nominale AC [V]. + + grid_frequency_Hz: + Frequenza nominale della rete [Hz]. + + sensor_noise: + Rumore relativo delle misure. + + enabled: + Stato operativo dell'inverter. + """ + + inverter_id: str + + nominal_power_kW: float = 100.0 + + combiners: List[CombinerBox] = field( + default_factory=list + ) + + nominal_efficiency: float = 0.985 + + night_consumption_W: float = 50.0 + + ac_voltage_V: float = 400.0 + + grid_frequency_Hz: float = 50.0 + + sensor_noise: float = 0.005 + + enabled: bool = True + + internal_temperature_C: float = 25.0 + + total_energy_Wh: float = 0.0 + + total_dc_energy_Wh: float = 0.0 + + total_ac_energy_Wh: float = 0.0 + + # ------------------------------------------------------------------ + # Proprietà + # ------------------------------------------------------------------ + + @property + def nominal_power_W(self) -> float: + """ + Potenza nominale AC dell'inverter in Watt. + """ + + return self.nominal_power_kW * 1000.0 + + # ------------------------------------------------------------------ + # Gestione Combiner + # ------------------------------------------------------------------ + + def add_combiner( + self, + combiner: CombinerBox + ) -> None: + """ + Aggiunge una Combiner Box all'inverter. + """ + + self.combiners.append(combiner) + + # ------------------------------------------------------------------ + + def remove_combiner( + self, + combiner_id: str + ) -> None: + """ + Rimuove una Combiner Box utilizzando il suo ID. + """ + + self.combiners = [ + combiner + for combiner in self.combiners + if combiner.combiner_id != combiner_id + ] + + # ------------------------------------------------------------------ + + def get_combiner_count(self) -> int: + """ + Restituisce il numero di Combiner Box collegate. + """ + + return len(self.combiners) + + # ------------------------------------------------------------------ + + def get_active_combiner_count(self) -> int: + """ + Restituisce il numero di Combiner Box attualmente operative. + """ + + return sum( + 1 + for combiner in self.combiners + if combiner.enabled + ) + + # ------------------------------------------------------------------ + # Calcolo potenza DC + # ------------------------------------------------------------------ + + def calculate_dc_power( + self, + combiner_data: List[Dict] + ) -> float: + """ + Somma la potenza DC proveniente dalle Combiner Box. + """ + + return sum( + data["power_W"] + for data in combiner_data + ) + + # ------------------------------------------------------------------ + # Rendimento + # ------------------------------------------------------------------ + + def calculate_efficiency( + self, + dc_power_W: float + ) -> float: + """ + Calcola il rendimento dell'inverter. + + Il rendimento varia leggermente in funzione del carico. + + A carico molto basso il rendimento è inferiore. + Intorno al carico nominale raggiunge il rendimento nominale. + """ + + if dc_power_W <= 0: + return 0.0 + + load_ratio = ( + dc_power_W + / self.nominal_power_W + ) + + # Rendimento ridotto a bassissimo carico + if load_ratio < 0.05: + + efficiency = ( + self.nominal_efficiency + * 0.90 + ) + + elif load_ratio < 0.20: + + efficiency = ( + self.nominal_efficiency + * 0.96 + ) + + else: + + efficiency = ( + self.nominal_efficiency + ) + + # Piccola variazione casuale + efficiency *= random.gauss( + 1.0, + 0.002 + ) + + return min( + max(efficiency, 0.0), + 1.0 + ) + + # ------------------------------------------------------------------ + # Conversione DC -> AC + # ------------------------------------------------------------------ + + def convert_dc_to_ac( + self, + dc_power_W: float + ) -> Dict: + """ + Converte la potenza DC in potenza AC. + + Include: + + - rendimento; + - clipping; + - rumore della misura. + """ + + if not self.enabled: + + return { + "ac_power_W": 0.0, + "efficiency": 0.0, + "clipping_W": 0.0 + } + + if dc_power_W <= 0: + + return { + "ac_power_W": 0.0, + "efficiency": 0.0, + "clipping_W": 0.0 + } + + efficiency = ( + self.calculate_efficiency( + dc_power_W + ) + ) + + theoretical_ac_power = ( + dc_power_W + * efficiency + ) + + # Clipping alla potenza nominale + ac_power = min( + theoretical_ac_power, + self.nominal_power_W + ) + + clipping = max( + theoretical_ac_power + - self.nominal_power_W, + 0.0 + ) + + # Rumore della misura + measured_ac_power = ( + ac_power + * random.gauss( + 1.0, + self.sensor_noise + ) + ) + + return { + "ac_power_W": + max( + measured_ac_power, + 0.0 + ), + + "efficiency": + efficiency, + + "clipping_W": + clipping + } + + # ------------------------------------------------------------------ + # Tensione AC + # ------------------------------------------------------------------ + + def calculate_ac_voltage( + self, + ac_power_W: float + ) -> float: + """ + Simula la tensione AC dell'inverter. + """ + + if ac_power_W <= 0: + + return 0.0 + + voltage = ( + self.ac_voltage_V + * random.gauss( + 1.0, + 0.002 + ) + ) + + return voltage + + # ------------------------------------------------------------------ + # Corrente AC + # ------------------------------------------------------------------ + + def calculate_ac_current( + self, + ac_power_W: float, + voltage_V: float + ) -> float: + """ + Calcola la corrente AC. + + Per semplicità viene utilizzato un modello trifase + semplificato. + """ + + if ( + ac_power_W <= 0 + or voltage_V <= 0 + ): + + return 0.0 + + power_factor = 0.98 + + current = ( + ac_power_W + / + ( + 3 ** 0.5 + * voltage_V + * power_factor + ) + ) + + return current + + # ------------------------------------------------------------------ + # Frequenza + # ------------------------------------------------------------------ + + def calculate_frequency( + self + ) -> float: + """ + Simula la frequenza della rete AC. + """ + + return random.gauss( + self.grid_frequency_Hz, + 0.01 + ) + + # ------------------------------------------------------------------ + # Temperatura + # ------------------------------------------------------------------ + + def calculate_temperature( + self, + dc_power_W: float, + ambient_temperature: float + ) -> float: + """ + Stima la temperatura interna dell'inverter. + + La temperatura aumenta in funzione + della potenza dissipata. + """ + + if not self.enabled: + + return ambient_temperature + + efficiency = self.calculate_efficiency( + dc_power_W + ) + + if efficiency <= 0: + + losses_W = 0.0 + + else: + + losses_W = ( + dc_power_W + * (1.0 - efficiency) + ) + + thermal_effect = ( + losses_W + / 500.0 + ) + + temperature = ( + ambient_temperature + + thermal_effect + ) + + return temperature + + # ------------------------------------------------------------------ + # Aggiornamento principale + # ------------------------------------------------------------------ + + def update( + self, + timestamp: datetime, + irradiance: float, + ambient_temperature: float, + years_from_start: float, + timestep_minutes: float + ) -> Dict: + """ + Aggiorna l'inverter e tutte le Combiner Box collegate. + + Returns + ------- + Dict + Misure aggregate dell'inverter. + """ + + # -------------------------------------------------------------- + # Inverter offline + # -------------------------------------------------------------- + + if not self.enabled: + + return { + + "timestamp": + timestamp, + + "inverter_id": + self.inverter_id, + + "dc_power_W": + 0.0, + + "ac_power_W": + 0.0, + + "efficiency": + 0.0, + + "clipping_W": + 0.0, + + "ac_voltage_V": + 0.0, + + "ac_current_A": + 0.0, + + "frequency_Hz": + 0.0, + + "energy_Wh": + 0.0, + + "dc_energy_Wh": + 0.0, + + "ac_energy_Wh": + 0.0, + + "total_energy_Wh": + self.total_energy_Wh, + + "active_combiners": + 0, + + "total_combiners": + self.get_combiner_count(), + + "internal_temperature_C": + ambient_temperature, + + "enabled": + False + } + + # -------------------------------------------------------------- + # Aggiornamento Combiner Box + # -------------------------------------------------------------- + + combiner_data = [] + + for combiner in self.combiners: + + data = combiner.update( + + timestamp=timestamp, + + irradiance=irradiance, + + ambient_temperature= + ambient_temperature, + + years_from_start= + years_from_start, + + timestep_minutes= + timestep_minutes + ) + + combiner_data.append( + data + ) + + # -------------------------------------------------------------- + # Potenza DC totale + # -------------------------------------------------------------- + + dc_power = ( + self.calculate_dc_power( + combiner_data + ) + ) + + # -------------------------------------------------------------- + # Conversione DC -> AC + # -------------------------------------------------------------- + + conversion = ( + self.convert_dc_to_ac( + dc_power + ) + ) + + ac_power = ( + conversion[ + "ac_power_W" + ] + ) + + efficiency = ( + conversion[ + "efficiency" + ] + ) + + clipping = ( + conversion[ + "clipping_W" + ] + ) + + # -------------------------------------------------------------- + # Misure AC + # -------------------------------------------------------------- + + ac_voltage = ( + self.calculate_ac_voltage( + ac_power + ) + ) + + ac_current = ( + self.calculate_ac_current( + ac_power, + ac_voltage + ) + ) + + frequency = ( + self.calculate_frequency() + if ac_power > 0 + else 0.0 + ) + + # -------------------------------------------------------------- + # Temperatura + # -------------------------------------------------------------- + + temperature = ( + self.calculate_temperature( + dc_power, + ambient_temperature + ) + ) + + self.internal_temperature_C = ( + temperature + ) + + # -------------------------------------------------------------- + # Energia + # -------------------------------------------------------------- + + dc_energy = ( + dc_power + * timestep_minutes + / 60.0 + ) + + ac_energy = ( + ac_power + * timestep_minutes + / 60.0 + ) + + self.total_dc_energy_Wh += ( + dc_energy + ) + + self.total_ac_energy_Wh += ( + ac_energy + ) + + self.total_energy_Wh += ( + ac_energy + ) + + # -------------------------------------------------------------- + # Output + # -------------------------------------------------------------- + + return { + + "timestamp": + timestamp, + + "inverter_id": + self.inverter_id, + + "dc_power_W": + dc_power, + + "ac_power_W": + ac_power, + + "efficiency": + efficiency, + + "clipping_W": + clipping, + + "ac_voltage_V": + ac_voltage, + + "ac_current_A": + ac_current, + + "frequency_Hz": + frequency, + + "energy_Wh": + ac_energy, + + "dc_energy_Wh": + dc_energy, + + "ac_energy_Wh": + ac_energy, + + "total_energy_Wh": + self.total_energy_Wh, + + "dc_total_energy_Wh": + self.total_dc_energy_Wh, + + "ac_total_energy_Wh": + self.total_ac_energy_Wh, + + "active_combiners": + self.get_active_combiner_count(), + + "total_combiners": + self.get_combiner_count(), + + "internal_temperature_C": + temperature, + + "enabled": + self.enabled + } + + # ------------------------------------------------------------------ + # Gestione guasti + # ------------------------------------------------------------------ + + def fail(self) -> None: + """ + Simula un guasto dell'inverter. + + L'inverter smette di produrre energia AC. + """ + + self.enabled = False + + # ------------------------------------------------------------------ + + def repair(self) -> None: + """ + Ripristina l'inverter. + """ + + self.enabled = True + + # ------------------------------------------------------------------ + + def __repr__(self) -> str: + """ + Rappresentazione leggibile dell'inverter. + """ + + return ( + + f"Inverter(" + f"id={self.inverter_id}, " + f"nominal_power=" + f"{self.nominal_power_kW}kW, " + f"combiners=" + f"{len(self.combiners)}, " + f"enabled=" + f"{self.enabled})" + ) +```