767 lines
17 KiB
Python
767 lines
17 KiB
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})"
|
|
)
|