Files
PV_Simulator/pvsim/combiner.py

481 lines
11 KiB
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})"
)