Simulatore Inverter
This commit is contained in:
768
pvsim/inverter.py
Normal file
768
pvsim/inverter.py
Normal file
@@ -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})"
|
||||
)
|
||||
```
|
||||
Reference in New Issue
Block a user