Simulatore Combiner
This commit is contained in:
482
pvsim/combiner.py
Normal file
482
pvsim/combiner.py
Normal file
@@ -0,0 +1,482 @@
|
||||
```python
|
||||
"""
|
||||
combiner.py
|
||||
|
||||
Modello di una Combiner Box fotovoltaica.
|
||||
|
||||
La Combiner Box raccoglie la produzione di più pannelli fotovoltaici
|
||||
e fornisce una misura aggregata verso l'inverter.
|
||||
|
||||
Gerarchia:
|
||||
|
||||
PV Plant
|
||||
|
|
||||
+-- Inverter
|
||||
|
|
||||
+-- Combiner Box
|
||||
|
|
||||
+-- Panel
|
||||
+-- Panel
|
||||
+-- ...
|
||||
|
||||
Responsabilità principali:
|
||||
|
||||
- gestione dei pannelli collegati;
|
||||
- aggregazione della potenza DC;
|
||||
- aggregazione della corrente;
|
||||
- calcolo della tensione equivalente;
|
||||
- conteggio dei pannelli attivi;
|
||||
- simulazione di guasto della combiner;
|
||||
- simulazione delle perdite elettriche;
|
||||
- simulazione della temperatura interna;
|
||||
- accumulo dell'energia prodotta.
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from dataclasses import dataclass, field
|
||||
from datetime import datetime
|
||||
from typing import Dict, List
|
||||
import random
|
||||
|
||||
from .panel import PVPanel
|
||||
|
||||
|
||||
@dataclass
|
||||
class CombinerBox:
|
||||
"""
|
||||
Rappresenta una Combiner Box.
|
||||
|
||||
Parameters
|
||||
----------
|
||||
combiner_id:
|
||||
Identificativo univoco della combiner.
|
||||
|
||||
panels:
|
||||
Lista dei pannelli collegati alla combiner.
|
||||
|
||||
nominal_voltage:
|
||||
Tensione nominale del bus DC [V].
|
||||
|
||||
cable_loss_factor:
|
||||
Fattore di perdita dovuto ai cavi e alle connessioni.
|
||||
Esempio: 0.99 significa una perdita dell'1%.
|
||||
|
||||
sensor_noise:
|
||||
Rumore relativo delle misure.
|
||||
|
||||
enabled:
|
||||
Stato operativo della combiner.
|
||||
|
||||
internal_temperature:
|
||||
Temperatura interna simulata della combiner [°C].
|
||||
"""
|
||||
|
||||
combiner_id: str
|
||||
|
||||
panels: List[PVPanel] = field(default_factory=list)
|
||||
|
||||
nominal_voltage: float = 800.0
|
||||
|
||||
cable_loss_factor: float = 0.99
|
||||
|
||||
sensor_noise: float = 0.005
|
||||
|
||||
enabled: bool = True
|
||||
|
||||
internal_temperature: float = 25.0
|
||||
|
||||
total_energy_Wh: float = 0.0
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Gestione pannelli
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def add_panel(self, panel: PVPanel) -> None:
|
||||
"""
|
||||
Aggiunge un pannello alla Combiner Box.
|
||||
"""
|
||||
|
||||
self.panels.append(panel)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def remove_panel(self, panel_id: str) -> None:
|
||||
"""
|
||||
Rimuove un pannello dalla Combiner Box utilizzando il suo ID.
|
||||
"""
|
||||
|
||||
self.panels = [
|
||||
panel
|
||||
for panel in self.panels
|
||||
if panel.panel_id != panel_id
|
||||
]
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_panel_count(self) -> int:
|
||||
"""
|
||||
Restituisce il numero totale di pannelli collegati.
|
||||
"""
|
||||
|
||||
return len(self.panels)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def get_active_panel_count(self) -> int:
|
||||
"""
|
||||
Restituisce il numero di pannelli attualmente attivi.
|
||||
"""
|
||||
|
||||
return sum(
|
||||
1
|
||||
for panel in self.panels
|
||||
if panel.enabled
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Calcolo grandezze elettriche
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def calculate_voltage(
|
||||
self,
|
||||
panel_data: List[Dict]
|
||||
) -> float:
|
||||
"""
|
||||
Calcola la tensione equivalente della Combiner Box.
|
||||
|
||||
Per una simulazione semplificata viene utilizzata la tensione
|
||||
media dei pannelli attivi.
|
||||
|
||||
In una futura versione sarà possibile modellare esplicitamente
|
||||
stringhe serie/parallelo.
|
||||
"""
|
||||
|
||||
active_voltages = [
|
||||
data["voltage_V"]
|
||||
for data in panel_data
|
||||
if data["voltage_V"] > 0
|
||||
]
|
||||
|
||||
if not active_voltages:
|
||||
return 0.0
|
||||
|
||||
average_voltage = (
|
||||
sum(active_voltages)
|
||||
/ len(active_voltages)
|
||||
)
|
||||
|
||||
# Rumore del sensore
|
||||
noise = random.gauss(
|
||||
1.0,
|
||||
self.sensor_noise
|
||||
)
|
||||
|
||||
return average_voltage * noise
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def calculate_current(
|
||||
self,
|
||||
panel_data: List[Dict]
|
||||
) -> float:
|
||||
"""
|
||||
Calcola la corrente totale della Combiner Box.
|
||||
|
||||
La corrente dei pannelli viene sommata.
|
||||
"""
|
||||
|
||||
total_current = sum(
|
||||
data["current_A"]
|
||||
for data in panel_data
|
||||
)
|
||||
|
||||
return total_current
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def calculate_power(
|
||||
self,
|
||||
panel_data: List[Dict]
|
||||
) -> float:
|
||||
"""
|
||||
Calcola la potenza DC aggregata.
|
||||
|
||||
La potenza dei pannelli viene sommata e successivamente
|
||||
vengono applicate le perdite dei cavi e delle connessioni.
|
||||
"""
|
||||
|
||||
raw_power = sum(
|
||||
data["power_W"]
|
||||
for data in panel_data
|
||||
)
|
||||
|
||||
power_after_losses = (
|
||||
raw_power
|
||||
* self.cable_loss_factor
|
||||
)
|
||||
|
||||
if power_after_losses <= 0:
|
||||
return 0.0
|
||||
|
||||
# Rumore della misura
|
||||
noise = random.gauss(
|
||||
1.0,
|
||||
self.sensor_noise
|
||||
)
|
||||
|
||||
return max(
|
||||
0.0,
|
||||
power_after_losses * noise
|
||||
)
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def calculate_temperature(
|
||||
self,
|
||||
panel_data: List[Dict],
|
||||
ambient_temperature: float
|
||||
) -> float:
|
||||
"""
|
||||
Stima la temperatura interna della Combiner Box.
|
||||
|
||||
La temperatura aumenta in funzione della potenza dissipata
|
||||
e della temperatura ambiente.
|
||||
"""
|
||||
|
||||
power = sum(
|
||||
data["power_W"]
|
||||
for data in panel_data
|
||||
)
|
||||
|
||||
# Riscaldamento semplificato dell'elettronica
|
||||
thermal_effect = (
|
||||
power / 10000.0
|
||||
)
|
||||
|
||||
temperature = (
|
||||
ambient_temperature
|
||||
+ thermal_effect
|
||||
)
|
||||
|
||||
return temperature
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Ciclo principale
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def update(
|
||||
self,
|
||||
timestamp: datetime,
|
||||
irradiance: float,
|
||||
ambient_temperature: float,
|
||||
years_from_start: float,
|
||||
timestep_minutes: float
|
||||
) -> Dict:
|
||||
"""
|
||||
Aggiorna lo stato della Combiner Box.
|
||||
|
||||
Per ogni pannello:
|
||||
|
||||
1. viene chiamato panel.update();
|
||||
2. i dati vengono raccolti;
|
||||
3. le grandezze vengono aggregate;
|
||||
4. viene restituito un dizionario con le misure.
|
||||
|
||||
Returns
|
||||
-------
|
||||
Dict
|
||||
Dati aggregati della Combiner Box.
|
||||
"""
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Combiner offline
|
||||
# --------------------------------------------------------------
|
||||
|
||||
if not self.enabled:
|
||||
|
||||
return {
|
||||
"timestamp": timestamp,
|
||||
|
||||
"combiner_id":
|
||||
self.combiner_id,
|
||||
|
||||
"power_W": 0.0,
|
||||
|
||||
"current_A": 0.0,
|
||||
|
||||
"voltage_V": 0.0,
|
||||
|
||||
"energy_Wh": 0.0,
|
||||
|
||||
"total_energy_Wh":
|
||||
self.total_energy_Wh,
|
||||
|
||||
"active_panels": 0,
|
||||
|
||||
"total_panels":
|
||||
self.get_panel_count(),
|
||||
|
||||
"internal_temperature_C":
|
||||
ambient_temperature,
|
||||
|
||||
"enabled": False
|
||||
}
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Aggiornamento pannelli
|
||||
# --------------------------------------------------------------
|
||||
|
||||
panel_data = []
|
||||
|
||||
for panel in self.panels:
|
||||
|
||||
data = panel.update(
|
||||
timestamp=timestamp,
|
||||
irradiance=irradiance,
|
||||
ambient_temperature=ambient_temperature,
|
||||
years_from_start=years_from_start,
|
||||
timestep_minutes=timestep_minutes
|
||||
)
|
||||
|
||||
panel_data.append(data)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Aggregazione
|
||||
# --------------------------------------------------------------
|
||||
|
||||
power = self.calculate_power(
|
||||
panel_data
|
||||
)
|
||||
|
||||
voltage = self.calculate_voltage(
|
||||
panel_data
|
||||
)
|
||||
|
||||
current = self.calculate_current(
|
||||
panel_data
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Temperatura
|
||||
# --------------------------------------------------------------
|
||||
|
||||
internal_temperature = (
|
||||
self.calculate_temperature(
|
||||
panel_data,
|
||||
ambient_temperature
|
||||
)
|
||||
)
|
||||
|
||||
self.internal_temperature = (
|
||||
internal_temperature
|
||||
)
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Energia
|
||||
# --------------------------------------------------------------
|
||||
|
||||
energy = (
|
||||
power
|
||||
* timestep_minutes
|
||||
/ 60.0
|
||||
)
|
||||
|
||||
self.total_energy_Wh += energy
|
||||
|
||||
# --------------------------------------------------------------
|
||||
# Output
|
||||
# --------------------------------------------------------------
|
||||
|
||||
return {
|
||||
|
||||
"timestamp":
|
||||
timestamp,
|
||||
|
||||
"combiner_id":
|
||||
self.combiner_id,
|
||||
|
||||
"power_W":
|
||||
power,
|
||||
|
||||
"current_A":
|
||||
current,
|
||||
|
||||
"voltage_V":
|
||||
voltage,
|
||||
|
||||
"energy_Wh":
|
||||
energy,
|
||||
|
||||
"total_energy_Wh":
|
||||
self.total_energy_Wh,
|
||||
|
||||
"active_panels":
|
||||
self.get_active_panel_count(),
|
||||
|
||||
"total_panels":
|
||||
self.get_panel_count(),
|
||||
|
||||
"internal_temperature_C":
|
||||
internal_temperature,
|
||||
|
||||
"enabled":
|
||||
self.enabled
|
||||
}
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
# Gestione guasti
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def fail(self) -> None:
|
||||
"""
|
||||
Mette la Combiner Box offline.
|
||||
|
||||
Tutti i pannelli collegati continueranno a mantenere il
|
||||
proprio stato interno, ma la Combiner non trasferirà
|
||||
potenza verso l'inverter.
|
||||
"""
|
||||
|
||||
self.enabled = False
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def repair(self) -> None:
|
||||
"""
|
||||
Riporta la Combiner Box in servizio.
|
||||
"""
|
||||
|
||||
self.enabled = True
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def active_power(self) -> float:
|
||||
"""
|
||||
Restituisce una stima della potenza attiva corrente.
|
||||
|
||||
Questo metodo è principalmente utile per diagnostica.
|
||||
"""
|
||||
|
||||
if not self.enabled:
|
||||
return 0.0
|
||||
|
||||
return sum(
|
||||
panel.nominal_power
|
||||
for panel in self.panels
|
||||
if panel.enabled
|
||||
) * self.cable_loss_factor
|
||||
|
||||
# ------------------------------------------------------------------
|
||||
|
||||
def __repr__(self) -> str:
|
||||
"""
|
||||
Rappresentazione leggibile della Combiner Box.
|
||||
"""
|
||||
|
||||
return (
|
||||
f"CombinerBox("
|
||||
f"id={self.combiner_id}, "
|
||||
f"panels={len(self.panels)}, "
|
||||
f"enabled={self.enabled})"
|
||||
)
|
||||
```
|
||||
Reference in New Issue
Block a user