Simulatore dell'impianto
This commit is contained in:
891
pvsim/plant.py
Normal file
891
pvsim/plant.py
Normal file
@@ -0,0 +1,891 @@
|
||||
```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()})"
|
||||
)
|
||||
```
|
||||
Reference in New Issue
Block a user