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