Files
PV_Simulator/pvsim/plant.py

890 lines
19 KiB
Python

"""
plant.py
Modello gerarchico completo dell'impianto fotovoltaico.
Gerarchia:
PVPlant
|
+-- Inverter
| |
| +-- CombinerBox
| |
| +-- PVPanel
| +-- PVPanel
| +-- ...
|
+-- Inverter
|
+-- CombinerBox
|
+-- PVPanel
Il modulo costruisce automaticamente l'intero impianto
a partire dal file di configurazione plant.json.
Responsabilità:
- creazione della gerarchia;
- gestione degli inverter;
- gestione delle Combiner Box;
- creazione automatica dei pannelli;
- aggiornamento dell'intero impianto;
- aggregazione della potenza;
- aggregazione dell'energia;
- calcolo della disponibilità;
- diagnostica della struttura.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from typing import Dict, List, Any
import json
from pathlib import Path
from .panel import PVPanel
from .combiner import CombinerBox
from .inverter import Inverter
@dataclass
class PVPlant:
"""
Rappresenta l'intero impianto fotovoltaico.
"""
plant_id: str
inverters: List[Inverter] = field(
default_factory=list
)
total_energy_Wh: float = 0.0
total_dc_energy_Wh: float = 0.0
total_ac_energy_Wh: float = 0.0
# Numero di timestep elaborati
timestep_count: int = 0
# ------------------------------------------------------------------
# Costruzione da configurazione
# ------------------------------------------------------------------
@classmethod
def from_config(
cls,
config: Dict[str, Any]
) -> "PVPlant":
"""
Crea un impianto completo leggendo la configurazione.
La configurazione deve avere la struttura:
layout
|
+-- inverters
|
+-- combiners
|
+-- strings
+-- panels_per_string
Returns
-------
PVPlant
Impianto completamente costruito.
"""
plant_name = (
config[
"plant"
][
"name"
]
)
plant = cls(
plant_id=plant_name
)
module_config = (
config[
"module"
]
)
layout_config = (
config[
"layout"
]
)
# --------------------------------------------------------------
# Creazione inverter
# --------------------------------------------------------------
for inverter_config in (
layout_config[
"inverters"
]
):
inverter = (
cls._create_inverter(
inverter_config,
module_config
)
)
plant.inverters.append(
inverter
)
return plant
# ------------------------------------------------------------------
@classmethod
def from_json(
cls,
config_path: str | Path
) -> "PVPlant":
"""
Carica la configurazione da un file JSON
e costruisce l'impianto.
"""
config_path = Path(
config_path
)
if not config_path.exists():
raise FileNotFoundError(
f"Configurazione non trovata: "
f"{config_path}"
)
with open(
config_path,
"r",
encoding="utf-8"
) as file:
config = json.load(
file
)
return cls.from_config(
config
)
# ------------------------------------------------------------------
# Creazione inverter
# ------------------------------------------------------------------
@staticmethod
def _create_inverter(
inverter_config: Dict[str, Any],
module_config: Dict[str, Any]
) -> Inverter:
"""
Crea un inverter e tutte le relative Combiner Box.
"""
inverter = Inverter(
inverter_id=
inverter_config[
"id"
],
nominal_power_kW=
inverter_config[
"nominal_power_kW"
],
nominal_efficiency=
inverter_config[
"efficiency"
][
"nominal"
],
night_consumption_W=
inverter_config[
"efficiency"
][
"night_consumption_W"
]
)
# --------------------------------------------------------------
# Creazione Combiner Box
# --------------------------------------------------------------
for combiner_config in (
inverter_config[
"combiners"
]
):
combiner = (
PVPlant._create_combiner(
combiner_config,
module_config
)
)
inverter.add_combiner(
combiner
)
return inverter
# ------------------------------------------------------------------
# Creazione Combiner Box
# ------------------------------------------------------------------
@staticmethod
def _create_combiner(
combiner_config: Dict[str, Any],
module_config: Dict[str, Any]
) -> CombinerBox:
"""
Crea una Combiner Box e tutti i relativi pannelli.
La configurazione definisce:
strings = numero di stringhe
panels_per_string =
pannelli per ogni stringa
In questa versione la struttura delle stringhe
viene rappresentata logicamente creando tutti i pannelli
nella Combiner Box.
L'ID del pannello contiene:
Combiner ID
String ID
Panel ID
"""
combiner = CombinerBox(
combiner_id=
combiner_config[
"id"
]
)
strings = (
combiner_config[
"strings"
]
)
panels_per_string = (
combiner_config[
"panels_per_string"
]
)
# --------------------------------------------------------------
# Parametri pannello
# --------------------------------------------------------------
nominal_power = (
module_config[
"nominal_power_W"
]
)
electrical = (
module_config[
"electrical"
]
)
physical = (
module_config[
"physical"
]
)
degradation = (
module_config[
"degradation"
]
)
# --------------------------------------------------------------
# Creazione pannelli
# --------------------------------------------------------------
for string_number in range(
1,
strings + 1
):
for panel_number in range(
1,
panels_per_string + 1
):
panel_id = (
f"{combiner_config['id']}"
f"_STR_{string_number:02d}"
f"_PANEL_{panel_number:02d}"
)
panel = PVPanel(
panel_id=
panel_id,
nominal_power=
nominal_power,
area=
physical[
"area_m2"
],
vmp=
electrical[
"vmp_V"
],
imp=
electrical[
"imp_A"
],
voc=
electrical[
"voc_V"
],
isc=
electrical[
"isc_A"
],
temp_coeff=
physical[
"temperature_coefficient_power"
],
nominal_temp=
physical[
"nominal_temperature_C"
],
annual_degradation=
degradation[
"annual_rate"
]
)
combiner.add_panel(
panel
)
return combiner
# ------------------------------------------------------------------
# Gestione inverter
# ------------------------------------------------------------------
def add_inverter(
self,
inverter: Inverter
) -> None:
"""
Aggiunge un inverter all'impianto.
"""
self.inverters.append(
inverter
)
# ------------------------------------------------------------------
def get_inverter_count(
self
) -> int:
"""
Restituisce il numero di inverter.
"""
return len(
self.inverters
)
# ------------------------------------------------------------------
def get_active_inverter_count(
self
) -> int:
"""
Restituisce il numero di inverter attivi.
"""
return sum(
1
for inverter
in self.inverters
if inverter.enabled
)
# ------------------------------------------------------------------
# Conteggi gerarchici
# ------------------------------------------------------------------
def get_combiner_count(
self
) -> int:
"""
Restituisce il numero totale di Combiner Box.
"""
return sum(
len(
inverter.combiners
)
for inverter
in self.inverters
)
# ------------------------------------------------------------------
def get_panel_count(
self
) -> int:
"""
Restituisce il numero totale di pannelli.
"""
return sum(
len(
combiner.panels
)
for inverter
in self.inverters
for combiner
in inverter.combiners
)
# ------------------------------------------------------------------
def get_active_panel_count(
self
) -> int:
"""
Restituisce il numero di pannelli attivi.
"""
return sum(
1
for inverter
in self.inverters
for combiner
in inverter.combiners
for panel
in combiner.panels
if panel.enabled
)
# ------------------------------------------------------------------
# Aggiornamento impianto
# ------------------------------------------------------------------
def update(
self,
timestamp: datetime,
irradiance: float,
ambient_temperature: float,
years_from_start: float,
timestep_minutes: float
) -> Dict[str, Any]:
"""
Esegue un timestep sull'intero impianto.
Flusso:
Plant
|
+-- Inverter
|
+-- Combiner
|
+-- Panel
Returns
-------
Dict
Dati aggregati dell'impianto.
"""
inverter_data = []
# --------------------------------------------------------------
# Aggiornamento inverter
# --------------------------------------------------------------
for inverter in self.inverters:
data = inverter.update(
timestamp=
timestamp,
irradiance=
irradiance,
ambient_temperature=
ambient_temperature,
years_from_start=
years_from_start,
timestep_minutes=
timestep_minutes
)
inverter_data.append(
data
)
# --------------------------------------------------------------
# Aggregazione potenza
# --------------------------------------------------------------
total_dc_power = sum(
data[
"dc_power_W"
]
for data
in inverter_data
)
total_ac_power = sum(
data[
"ac_power_W"
]
for data
in inverter_data
)
# --------------------------------------------------------------
# Aggregazione energia
# --------------------------------------------------------------
dc_energy = (
total_dc_power
* timestep_minutes
/ 60.0
)
ac_energy = (
total_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
)
self.timestep_count += 1
# --------------------------------------------------------------
# Disponibilità
# --------------------------------------------------------------
total_inverters = (
self.get_inverter_count()
)
active_inverters = (
self.get_active_inverter_count()
)
if total_inverters > 0:
availability = (
active_inverters
/ total_inverters
)
else:
availability = 0.0
# --------------------------------------------------------------
# Output
# --------------------------------------------------------------
return {
"timestamp":
timestamp,
"plant_id":
self.plant_id,
"dc_power_W":
total_dc_power,
"ac_power_W":
total_ac_power,
"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,
"inverters":
total_inverters,
"active_inverters":
active_inverters,
"combiners":
self.get_combiner_count(),
"panels":
self.get_panel_count(),
"active_panels":
self.get_active_panel_count(),
"availability":
availability,
"enabled":
True
}
# ------------------------------------------------------------------
# Struttura impianto
# ------------------------------------------------------------------
def describe(
self
) -> str:
"""
Restituisce una descrizione testuale
della gerarchia dell'impianto.
"""
lines = []
lines.append(
f"PV Plant: "
f"{self.plant_id}"
)
lines.append(
f" Inverters: "
f"{self.get_inverter_count()}"
)
lines.append(
f" Combiners: "
f"{self.get_combiner_count()}"
)
lines.append(
f" Panels: "
f"{self.get_panel_count()}"
)
lines.append("")
for inverter in self.inverters:
lines.append(
f" ├── "
f"{inverter.inverter_id} "
f"({inverter.nominal_power_kW} kW)"
)
for combiner in (
inverter.combiners
):
lines.append(
f" │ ├── "
f"{combiner.combiner_id} "
f"({len(combiner.panels)} panels)"
)
# Raggruppamento logico delle stringhe
panel_count = (
len(
combiner.panels
)
)
lines.append(
f" │ │ └── "
f"{panel_count} panels"
)
return "\n".join(
lines
)
# ------------------------------------------------------------------
# Ricerca componenti
# ------------------------------------------------------------------
def find_inverter(
self,
inverter_id: str
) -> Inverter | None:
"""
Cerca un inverter tramite ID.
"""
for inverter in self.inverters:
if (
inverter.inverter_id
== inverter_id
):
return inverter
return None
# ------------------------------------------------------------------
def find_combiner(
self,
combiner_id: str
) -> CombinerBox | None:
"""
Cerca una Combiner Box tramite ID.
"""
for inverter in self.inverters:
for combiner in (
inverter.combiners
):
if (
combiner.combiner_id
== combiner_id
):
return combiner
return None
# ------------------------------------------------------------------
def find_panel(
self,
panel_id: str
) -> PVPanel | None:
"""
Cerca un pannello tramite ID.
"""
for inverter in self.inverters:
for combiner in (
inverter.combiners
):
for panel in (
combiner.panels
):
if (
panel.panel_id
== panel_id
):
return panel
return None
# ------------------------------------------------------------------
def __repr__(
self
) -> str:
"""
Rappresentazione leggibile dell'impianto.
"""
return (
f"PVPlant("
f"id={self.plant_id}, "
f"inverters="
f"{self.get_inverter_count()}, "
f"combiners="
f"{self.get_combiner_count()}, "
f"panels="
f"{self.get_panel_count()})"
)