Files
PV_Simulator/pvsim/simulator.py

2270 lines
44 KiB
Python

```python
"""
pvsim/simulator.py
Motore centrale della simulazione fotovoltaica.
Il simulatore coordina:
SunModel
|
v
WeatherModel
|
v
FaultManager
|
v
PVPlant
|
+-- Inverter
| |
| +-- CombinerBox
| |
| +-- PVPanel
|
v
SimulationStep
|
+-- WeatherData
+-- PanelData[]
+-- CombinerData[]
+-- InverterData[]
+-- PlantData
+-- FaultData[]
Il simulatore non utilizza più dizionari generici
per rappresentare la telemetria.
Tutti i dati prodotti vengono convertiti nei modelli
definiti in pvsim.models.
Output gerarchico:
Plant
|
+-- Inverter
| |
| +-- Combiner
| |
| +-- Panel
|
+-- Faults
|
+-- Weather
Il risultato finale è contenuto in SimulationResult.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime, timedelta
from typing import Any, Dict, List
import pandas as pd
from .plant import PVPlant
from .sun import SunModel
from .weather import WeatherModel
from .faults import (
FaultManager,
ComponentLevel,
)
from .models import (
WeatherData,
PanelData,
CombinerData,
InverterData,
PlantData,
FaultData,
SimulationStep,
create_panel_data,
)
# ======================================================================
# CONFIGURAZIONE
# ======================================================================
@dataclass
class SimulationConfig:
"""
Configurazione temporale della simulazione.
Parameters
----------
start:
Timestamp iniziale.
end:
Timestamp finale.
timestep_minutes:
Durata del timestep in minuti.
years_from_start:
Anni trascorsi dall'installazione dell'impianto.
generate_random_faults:
Abilita la generazione automatica dei fault.
store_panel_data:
Memorizza i dati dei pannelli.
store_combiner_data:
Memorizza i dati dei combiner.
store_inverter_data:
Memorizza i dati degli inverter.
store_plant_data:
Memorizza i dati dell'impianto.
store_weather_data:
Memorizza i dati meteorologici.
store_fault_data:
Memorizza i dati dei fault.
"""
start: datetime
end: datetime
timestep_minutes: int = 5
years_from_start: float = 0.0
generate_random_faults: bool = False
store_panel_data: bool = True
store_combiner_data: bool = True
store_inverter_data: bool = True
store_plant_data: bool = True
store_weather_data: bool = True
store_fault_data: bool = True
def __post_init__(
self,
) -> None:
if self.end <= self.start:
raise ValueError(
"end deve essere maggiore di start"
)
if self.timestep_minutes <= 0:
raise ValueError(
"timestep_minutes deve essere maggiore di zero"
)
# ======================================================================
# RISULTATI
# ======================================================================
@dataclass
class SimulationResult:
"""
Risultato completo della simulazione.
La struttura principale è:
steps
|
+-- SimulationStep
|
+-- WeatherData
+-- PanelData[]
+-- CombinerData[]
+-- InverterData[]
+-- PlantData
+-- FaultData[]
Per comodità vengono inoltre mantenuti metodi
per ottenere direttamente DataFrame tabellari.
"""
steps: List[
SimulationStep
] = field(
default_factory=list
)
# ------------------------------------------------------------------
# Proprietà
# ------------------------------------------------------------------
@property
def panel_records(
self,
) -> List[PanelData]:
records = []
for step in self.steps:
records.extend(
step.panels
)
return records
# ------------------------------------------------------------------
@property
def combiner_records(
self,
) -> List[CombinerData]:
records = []
for step in self.steps:
records.extend(
step.combiners
)
return records
# ------------------------------------------------------------------
@property
def inverter_records(
self,
) -> List[InverterData]:
records = []
for step in self.steps:
records.extend(
step.inverters
)
return records
# ------------------------------------------------------------------
@property
def plant_records(
self,
) -> List[PlantData]:
records = []
for step in self.steps:
if step.plant is not None:
records.append(
step.plant
)
return records
# ------------------------------------------------------------------
@property
def weather_records(
self,
) -> List[WeatherData]:
records = []
for step in self.steps:
if step.weather is not None:
records.append(
step.weather
)
return records
# ------------------------------------------------------------------
@property
def fault_records(
self,
) -> List[FaultData]:
records = []
for step in self.steps:
records.extend(
step.faults
)
return records
# ==================================================================
# DATAFRAME
# ==================================================================
def _records_to_dataframe(
self,
records: List[Any],
) -> pd.DataFrame:
"""
Converte una lista di modelli in DataFrame.
"""
if not records:
return pd.DataFrame()
return pd.DataFrame(
[
record.to_dict()
for record
in records
]
)
# ------------------------------------------------------------------
def panel_dataframe(
self,
) -> pd.DataFrame:
return self._records_to_dataframe(
self.panel_records
)
# ------------------------------------------------------------------
def combiner_dataframe(
self,
) -> pd.DataFrame:
return self._records_to_dataframe(
self.combiner_records
)
# ------------------------------------------------------------------
def inverter_dataframe(
self,
) -> pd.DataFrame:
return self._records_to_dataframe(
self.inverter_records
)
# ------------------------------------------------------------------
def plant_dataframe(
self,
) -> pd.DataFrame:
return self._records_to_dataframe(
self.plant_records
)
# ------------------------------------------------------------------
def weather_dataframe(
self,
) -> pd.DataFrame:
return self._records_to_dataframe(
self.weather_records
)
# ------------------------------------------------------------------
def faults_dataframe(
self,
) -> pd.DataFrame:
return self._records_to_dataframe(
self.fault_records
)
# ==================================================================
# EXPORT CSV
# ==================================================================
def export_csv(
self,
output_dir: str,
) -> None:
"""
Esporta i dati della simulazione in CSV.
"""
from pathlib import Path
output_path = Path(
output_dir
)
output_path.mkdir(
parents=True,
exist_ok=True
)
datasets = {
"weather":
self.weather_dataframe(),
"panel":
self.panel_dataframe(),
"combiner":
self.combiner_dataframe(),
"inverter":
self.inverter_dataframe(),
"plant":
self.plant_dataframe(),
"faults":
self.faults_dataframe(),
}
for name, dataframe in datasets.items():
if dataframe.empty:
continue
dataframe.to_csv(
output_path
/ f"{name}.csv",
index=False
)
# ==================================================================
# EXPORT PARQUET
# ==================================================================
def export_parquet(
self,
output_dir: str,
) -> None:
"""
Esporta i dati in formato Parquet.
Richiede pyarrow o fastparquet.
"""
from pathlib import Path
output_path = Path(
output_dir
)
output_path.mkdir(
parents=True,
exist_ok=True
)
datasets = {
"weather":
self.weather_dataframe(),
"panel":
self.panel_dataframe(),
"combiner":
self.combiner_dataframe(),
"inverter":
self.inverter_dataframe(),
"plant":
self.plant_dataframe(),
"faults":
self.faults_dataframe(),
}
for name, dataframe in datasets.items():
if dataframe.empty:
continue
dataframe.to_parquet(
output_path
/ f"{name}.parquet",
index=False
)
# ==================================================================
# SUMMARY
# ==================================================================
def summary(
self,
) -> Dict[str, Any]:
"""
Restituisce un riepilogo del risultato.
"""
return {
"timesteps":
len(
self.steps
),
"weather_records":
len(
self.weather_records
),
"panel_records":
len(
self.panel_records
),
"combiner_records":
len(
self.combiner_records
),
"inverter_records":
len(
self.inverter_records
),
"plant_records":
len(
self.plant_records
),
"fault_records":
len(
self.fault_records
),
}
# ======================================================================
# SIMULATORE
# ======================================================================
@dataclass
class PVSimulator:
"""
Motore principale della simulazione.
Parameters
----------
plant:
Modello gerarchico dell'impianto.
sun:
Modello solare.
weather:
Modello meteorologico.
fault_manager:
Gestore dei fault.
"""
plant: PVPlant
sun: SunModel
weather: WeatherModel
fault_manager: FaultManager
# ==================================================================
# ID COMPONENTI
# ==================================================================
def _get_component_ids(
self,
) -> Dict[str, List[str]]:
"""
Estrae gli ID di tutti i componenti.
Gerarchia:
inverter
|
+-- combiner
|
+-- panel
"""
panel_ids = []
combiner_ids = []
inverter_ids = []
for inverter in self.plant.inverters:
inverter_ids.append(
inverter.inverter_id
)
for combiner in inverter.combiners:
combiner_ids.append(
combiner.combiner_id
)
for panel in combiner.panels:
panel_ids.append(
panel.panel_id
)
return {
"panel":
panel_ids,
"combiner":
combiner_ids,
"inverter":
inverter_ids,
}
# ==================================================================
# WEATHER MODEL
# ==================================================================
def _build_weather_data(
self,
timestamp: datetime,
) -> WeatherData:
"""
Converte l'output del WeatherModel
nel modello WeatherData.
"""
conditions = (
self.weather.get_conditions(
timestamp=
timestamp
)
)
return WeatherData(
timestamp=
timestamp,
ambient_temperature_C=
conditions[
"ambient_temperature_C"
],
solar_elevation_deg=
conditions[
"solar_elevation_deg"
],
solar_azimuth_deg=
conditions[
"solar_azimuth_deg"
],
solar_zenith_deg=
conditions[
"solar_zenith_deg"
],
angle_of_incidence_deg=
conditions[
"angle_of_incidence_deg"
],
ghi_Wm2=
conditions[
"ghi_Wm2"
],
dni_Wm2=
conditions[
"dni_Wm2"
],
dhi_Wm2=
conditions[
"dhi_Wm2"
],
poa_global_Wm2=
conditions[
"poa_global_Wm2"
],
poa_direct_Wm2=
conditions[
"poa_direct_Wm2"
],
poa_diffuse_Wm2=
conditions[
"poa_diffuse_Wm2"
],
cloud_factor=
conditions[
"cloud_factor"
],
rain_factor=
conditions[
"rain_factor"
],
rain_active=
conditions[
"rain_active"
],
daylight=
conditions[
"daylight"
],
)
# ==================================================================
# FAULT DATA
# ==================================================================
def _build_fault_data(
self,
timestamp: datetime,
) -> List[FaultData]:
"""
Converte i fault attivi in FaultData.
"""
records = []
active_faults = (
self.fault_manager
.get_active_faults(
timestamp
)
)
for fault in active_faults:
records.append(
FaultData(
timestamp=
timestamp,
fault_id=
fault.fault_id,
fault_type=
fault.fault_type.value,
component_level=
fault.component_level.value,
component_id=
fault.component_id,
severity=
fault.severity,
reduction_factor=
fault.reduction_factor(),
description=
fault.description,
)
)
return records
# ==================================================================
# FAULT AUTOMATICI
# ==================================================================
def _generate_random_faults(
self,
timestamp: datetime,
) -> None:
"""
Genera eventuali fault casuali.
"""
ids = (
self._get_component_ids()
)
self.fault_manager.simulate_random_faults(
timestamp=
timestamp,
panel_ids=
ids[
"panel"
],
combiner_ids=
ids[
"combiner"
],
inverter_ids=
ids[
"inverter"
],
)
# ==================================================================
# PANEL DATA
# ==================================================================
def _build_panel_data(
self,
timestamp: datetime,
weather: WeatherData,
) -> List[PanelData]:
"""
Genera la telemetria di tutti i pannelli.
La produzione del pannello viene calcolata
dal modello PVPanel.
Il fault factor viene applicato dopo
il calcolo della produzione fisica.
"""
records = []
for inverter in self.plant.inverters:
for combiner in inverter.combiners:
for panel in combiner.panels:
# --------------------------------------------------
# Fault
# --------------------------------------------------
fault_factor = (
self.fault_manager
.get_component_factor(
panel.panel_id,
timestamp
)
)
# --------------------------------------------------
# Produzione fisica
# --------------------------------------------------
try:
panel_result = (
panel.simulate(
irradiance=
weather
.poa_global_Wm2,
ambient_temperature=
weather
.ambient_temperature_C,
timestamp=
timestamp
)
)
dc_power = (
panel_result[
"dc_power_W"
]
)
panel_temperature = (
panel_result.get(
"panel_temperature_C",
weather
.ambient_temperature_C
)
)
except (
AttributeError,
TypeError,
KeyError,
):
# --------------------------------------------------
# Fallback compatibilità
# --------------------------------------------------
dc_power = (
panel.nominal_power
* (
weather
.poa_global_Wm2
/ 1000.0
)
)
panel_temperature = (
weather
.ambient_temperature_C
)
# --------------------------------------------------
# Fault
# --------------------------------------------------
effective_power = (
dc_power
* fault_factor
)
# --------------------------------------------------
# Model
# --------------------------------------------------
record = create_panel_data(
timestamp=
timestamp,
panel_id=
panel.panel_id,
combiner_id=
combiner.combiner_id,
inverter_id=
inverter.inverter_id,
dc_power_W=
effective_power,
nominal_power_W=
panel.nominal_power,
irradiance_Wm2=
weather
.poa_global_Wm2,
panel_temperature_C=
panel_temperature,
fault_factor=
fault_factor,
enabled=
panel.enabled,
)
records.append(
record
)
return records
# ==================================================================
# COMBINER DATA
# ==================================================================
def _build_combiner_data(
self,
timestamp: datetime,
panel_records: List[PanelData],
) -> List[CombinerData]:
"""
Aggrega i dati dei pannelli a livello Combiner Box.
"""
records = []
for inverter in self.plant.inverters:
for combiner in inverter.combiners:
panels = [
panel
for panel
in panel_records
if (
panel.combiner_id
== combiner.combiner_id
)
]
if not panels:
continue
dc_power = sum(
panel.dc_power_W
for panel in panels
)
nominal_power = sum(
panel.nominal_power_W
for panel in panels
)
active_panels = sum(
1
for panel
in panels
if panel.enabled
and panel.dc_power_W > 0
)
fault_factor = (
self.fault_manager
.get_component_factor(
combiner.combiner_id,
timestamp
)
)
# ------------------------------------------------------
# Applica fault Combiner
# ------------------------------------------------------
effective_power = (
dc_power
* fault_factor
)
# ------------------------------------------------------
# Corrente e tensione
# ------------------------------------------------------
dc_voltage = 0.0
dc_current = 0.0
if effective_power > 0:
dc_voltage = 400.0
dc_current = (
effective_power
/ dc_voltage
)
# ------------------------------------------------------
# Stato
# ------------------------------------------------------
fault_active = (
fault_factor < 1.0
)
if fault_active:
status = "fault"
elif active_panels == 0:
status = "idle"
else:
status = "normal"
availability = (
active_panels
/ len(panels)
)
record = CombinerData(
timestamp=
timestamp,
combiner_id=
combiner.combiner_id,
inverter_id=
inverter.inverter_id,
dc_power_W=
effective_power,
dc_voltage_V=
dc_voltage,
dc_current_A=
dc_current,
nominal_power_W=
nominal_power,
panel_count=
len(panels),
active_panel_count=
active_panels,
fault_factor=
fault_factor,
fault_active=
fault_active,
availability=
availability,
status=
status,
)
records.append(
record
)
return records
# ==================================================================
# INVERTER DATA
# ==================================================================
def _build_inverter_data(
self,
timestamp: datetime,
combiner_records: List[CombinerData],
weather: WeatherData,
) -> List[InverterData]:
"""
Aggrega i dati delle Combiner Box
a livello inverter.
"""
records = []
for inverter in self.plant.inverters:
combiners = [
combiner
for combiner
in combiner_records
if (
combiner.inverter_id
== inverter.inverter_id
)
]
if not combiners:
continue
dc_power = sum(
combiner.dc_power_W
for combiner
in combiners
)
nominal_power = sum(
combiner.nominal_power_W
for combiner
in combiners
)
active_combiners = sum(
1
for combiner
in combiners
if combiner.status
!= "fault"
)
# ----------------------------------------------------------
# Fault inverter
# ----------------------------------------------------------
fault_factor = (
self.fault_manager
.get_component_factor(
inverter.inverter_id,
timestamp
)
)
dc_power *= fault_factor
# ----------------------------------------------------------
# Efficienza
# ----------------------------------------------------------
inverter_efficiency = 0.97
ac_power = (
dc_power
* inverter_efficiency
)
# ----------------------------------------------------------
# Clipping
# ----------------------------------------------------------
inverter_nominal = (
inverter.nominal_power_kW
* 1000.0
)
clipping_loss = 0.0
if ac_power > inverter_nominal:
clipping_loss = (
ac_power
- inverter_nominal
)
ac_power = (
inverter_nominal
)
# ----------------------------------------------------------
# Fault inverter
# ----------------------------------------------------------
fault_active = (
fault_factor < 1.0
)
if fault_active:
status = "fault"
elif ac_power <= 0:
status = "idle"
else:
status = "normal"
# ----------------------------------------------------------
# Elettrico
# ----------------------------------------------------------
dc_voltage = 800.0
if dc_voltage > 0:
dc_current = (
dc_power
/ dc_voltage
)
else:
dc_current = 0.0
ac_voltage = 400.0
if ac_voltage > 0:
ac_current = (
ac_power
/ ac_voltage
)
else:
ac_current = 0.0
# ----------------------------------------------------------
# Temperatura inverter
# ----------------------------------------------------------
inverter_temperature = (
weather.ambient_temperature_C
+ (
ac_power
/ max(
inverter_nominal,
1.0
)
)
* 20.0
)
availability = (
active_combiners
/ len(combiners)
)
record = InverterData(
timestamp=
timestamp,
inverter_id=
inverter.inverter_id,
dc_power_W=
dc_power,
ac_power_W=
ac_power,
dc_voltage_V=
dc_voltage,
dc_current_A=
dc_current,
ac_voltage_V=
ac_voltage,
ac_current_A=
ac_current,
efficiency=
inverter_efficiency,
nominal_power_W=
inverter_nominal,
clipping_loss_W=
clipping_loss,
temperature_C=
inverter_temperature,
fault_factor=
fault_factor,
combiner_count=
len(combiners),
active_combiner_count=
active_combiners,
fault_active=
fault_active,
availability=
availability,
status=
status,
)
records.append(
record
)
return records
# ==================================================================
# PLANT DATA
# ==================================================================
def _build_plant_data(
self,
timestamp: datetime,
panel_records: List[PanelData],
combiner_records: List[CombinerData],
inverter_records: List[InverterData],
config: SimulationConfig,
) -> PlantData:
"""
Aggrega tutti i dati a livello di impianto.
"""
dc_power = sum(
inverter.dc_power_W
for inverter
in inverter_records
)
ac_power = sum(
inverter.ac_power_W
for inverter
in inverter_records
)
nominal_power = sum(
inverter.nominal_power_W
for inverter
in inverter_records
)
# --------------------------------------------------------------
# Energia del timestep
# --------------------------------------------------------------
timestep_hours = (
config.timestep_minutes
/ 60.0
)
energy_Wh = (
ac_power
* timestep_hours
)
# --------------------------------------------------------------
# Conteggi
# --------------------------------------------------------------
inverter_count = (
len(
inverter_records
)
)
active_inverters = sum(
1
for inverter
in inverter_records
if inverter.status
!= "fault"
)
combiner_count = (
len(
combiner_records
)
)
active_combiners = sum(
1
for combiner
in combiner_records
if combiner.status
!= "fault"
)
panel_count = (
len(
panel_records
)
)
active_panels = sum(
1
for panel
in panel_records
if panel.enabled
)
# --------------------------------------------------------------
# Efficienza
# --------------------------------------------------------------
if dc_power > 0:
efficiency = (
ac_power
/ dc_power
)
else:
efficiency = 0.0
# --------------------------------------------------------------
# Performance Ratio
# --------------------------------------------------------------
if nominal_power > 0:
performance_ratio = (
ac_power
/ nominal_power
)
else:
performance_ratio = 0.0
# --------------------------------------------------------------
# Availability
# --------------------------------------------------------------
if panel_count > 0:
availability = (
active_panels
/ panel_count
)
else:
availability = 0.0
# --------------------------------------------------------------
# Fault factor
# --------------------------------------------------------------
if panel_count > 0:
fault_factor = (
sum(
panel.fault_factor
for panel
in panel_records
)
/ panel_count
)
else:
fault_factor = 1.0
fault_active = (
fault_factor < 1.0
)
if fault_active:
status = "fault"
elif ac_power <= 0:
status = "idle"
else:
status = "normal"
return PlantData(
timestamp=
timestamp,
plant_id=
self.plant.plant_id,
dc_power_W=
dc_power,
ac_power_W=
ac_power,
effective_ac_power_W=
ac_power,
nominal_power_W=
nominal_power,
energy_Wh=
energy_Wh,
cumulative_energy_Wh=
getattr(
self.plant,
"total_ac_energy_Wh",
0.0
),
inverter_count=
inverter_count,
active_inverter_count=
active_inverters,
combiner_count=
combiner_count,
active_combiner_count=
active_combiners,
panel_count=
panel_count,
active_panel_count=
active_panels,
efficiency=
efficiency,
performance_ratio=
performance_ratio,
availability=
availability,
fault_factor=
fault_factor,
fault_active=
fault_active,
status=
status,
)
# ==================================================================
# SINGOLO TIMESTEP
# ==================================================================
def simulate_timestep(
self,
timestamp: datetime,
config: SimulationConfig,
) -> SimulationStep:
"""
Esegue un singolo timestep e restituisce
un SimulationStep completamente tipizzato.
"""
# --------------------------------------------------------------
# Weather
# --------------------------------------------------------------
weather = (
self._build_weather_data(
timestamp
)
)
# --------------------------------------------------------------
# Random faults
# --------------------------------------------------------------
if config.generate_random_faults:
self._generate_random_faults(
timestamp
)
# --------------------------------------------------------------
# Panel
# --------------------------------------------------------------
panel_records = (
self._build_panel_data(
timestamp,
weather
)
if config.store_panel_data
else []
)
# --------------------------------------------------------------
# Combiner
# --------------------------------------------------------------
combiner_records = (
self._build_combiner_data(
timestamp,
panel_records
)
if config.store_combiner_data
else []
)
# --------------------------------------------------------------
# Inverter
# --------------------------------------------------------------
inverter_records = (
self._build_inverter_data(
timestamp,
combiner_records,
weather
)
if config.store_inverter_data
else []
)
# --------------------------------------------------------------
# Plant
# --------------------------------------------------------------
plant_record = (
self._build_plant_data(
timestamp,
panel_records,
combiner_records,
inverter_records,
config
)
if config.store_plant_data
else None
)
# --------------------------------------------------------------
# Faults
# --------------------------------------------------------------
fault_records = (
self._build_fault_data(
timestamp
)
if config.store_fault_data
else []
)
# --------------------------------------------------------------
# SimulationStep
# --------------------------------------------------------------
return SimulationStep(
timestamp=
timestamp,
weather=(
weather
if config.store_weather_data
else None
),
panels=
panel_records,
combiners=
combiner_records,
inverters=
inverter_records,
plant=
plant_record,
faults=
fault_records,
)
# ==================================================================
# RUN
# ==================================================================
def run(
self,
config: SimulationConfig,
) -> SimulationResult:
"""
Esegue la simulazione completa.
"""
result = (
SimulationResult()
)
timestamp = (
config.start
)
while timestamp < config.end:
# ----------------------------------------------------------
# Simula timestep
# ----------------------------------------------------------
step = (
self.simulate_timestep(
timestamp,
config
)
)
result.steps.append(
step
)
# ----------------------------------------------------------
# Incremento temporale
# ----------------------------------------------------------
timestamp += timedelta(
minutes=
config.timestep_minutes
)
return result
# ==================================================================
# RUN SINGLE DAY
# ==================================================================
def run_single_day(
self,
date: datetime,
timestep_minutes: int = 5,
generate_random_faults: bool = False,
) -> SimulationResult:
"""
Esegue la simulazione di una giornata.
"""
start = datetime(
year=
date.year,
month=
date.month,
day=
date.day
)
end = (
start
+ timedelta(
days=1
)
)
config = SimulationConfig(
start=
start,
end=
end,
timestep_minutes=
timestep_minutes,
generate_random_faults=
generate_random_faults,
)
return self.run(
config
)
# ==================================================================
# RUN YEAR
# ==================================================================
def run_year(
self,
year: int,
timestep_minutes: int = 15,
generate_random_faults: bool = False,
) -> SimulationResult:
"""
Esegue una simulazione annuale.
"""
start = datetime(
year=
year,
month=1,
day=1
)
end = datetime(
year=
year + 1,
month=1,
day=1
)
config = SimulationConfig(
start=
start,
end=
end,
timestep_minutes=
timestep_minutes,
generate_random_faults=
generate_random_faults,
)
return self.run(
config
)
# ==================================================================
# SUMMARY
# ==================================================================
def summary(
self,
result: SimulationResult,
) -> Dict[str, Any]:
"""
Restituisce un riepilogo della simulazione.
"""
return {
"plant_id":
self.plant.plant_id,
"inverters":
self.plant.get_inverter_count(),
"combiners":
self.plant.get_combiner_count(),
"panels":
self.plant.get_panel_count(),
**result.summary(),
}
# ==================================================================
# REPR
# ==================================================================
def __repr__(
self,
) -> str:
return (
f"PVSimulator("
f"plant="
f"{self.plant.plant_id}, "
f"inverters="
f"{self.plant.get_inverter_count()}, "
f"combiners="
f"{self.plant.get_combiner_count()}, "
f"panels="
f"{self.plant.get_panel_count()})"
)
# ======================================================================
# EXPORT
# ======================================================================
__all__ = [
"SimulationConfig",
"SimulationResult",
"PVSimulator",
]