fix: Fix syntax errors, wire config-driven simulation in run.py, and correct fault-rate/availability KPI bugs

This commit is contained in:
2026-07-28 14:32:53 +02:00
parent 36a875b115
commit b10c5bdbef
15 changed files with 268 additions and 29 deletions

View File

@@ -478,4 +478,3 @@ class CombinerBox:
f"panels={len(self.panels)}, "
f"enabled={self.enabled})"
)
```

View File

@@ -597,4 +597,3 @@ __all__ = [
"export_simulation",
]
```

View File

@@ -317,6 +317,76 @@ class FaultConfig:
random_seed: Optional[int] = None
@classmethod
def from_config(
cls,
config: Dict[str, object]
) -> FaultConfig:
"""
Crea un'istanza di FaultConfig
a partire da un dizionario di configurazione.
"""
fault_config = config["faults"]
return cls(
enabled=
fault_config["enabled"],
panel_fault_probability=
fault_config["panel"]["failure_probability_daily"],
combiner_fault_probability=
fault_config["combiner"]["failure_probability_daily"],
inverter_fault_probability=
fault_config["inverter"]["failure_probability_daily"],
intermittent_probability=
fault_config.get(
"intermittent_probability",
0.30
),
default_fault_duration_hours=
fault_config.get(
"default_fault_duration_hours",
4.0
),
random_seed=
fault_config.get(
"random_seed",
None
)
)
@classmethod
def from_json(
cls,
json_path: str
) -> FaultConfig:
"""
Crea un'istanza di FaultConfig
a partire da un file JSON.
"""
import json
with open(
json_path,
"r"
) as f:
config = json.load(
f
)
return cls.from_config(
config
)
# ======================================================================
# FAULT MANAGER
@@ -671,11 +741,40 @@ class FaultManager:
# Generazione fault casuale
# ------------------------------------------------------------------
@staticmethod
def _daily_to_timestep_probability(
daily_probability: float,
timestep_minutes: float
) -> float:
"""
Converte una probabilità di fault giornaliera
nella probabilità equivalente per un singolo timestep.
Le probabilità configurate (*_probability_daily) sono
riferite a un'intera giornata, ma generate_random_fault
viene chiamato una volta per timestep: senza questa
conversione la probabilità giornaliera verrebbe applicata
una volta per ogni timestep, inflazionando il tasso di
fault di un fattore pari al numero di timestep al giorno
(ad es. 288x per timestep da 5 minuti).
"""
timesteps_per_day = (
24 * 60
) / timestep_minutes
return 1 - (
1 - daily_probability
) ** (
1 / timesteps_per_day
)
def generate_random_fault(
self,
component_id: str,
component_level: ComponentLevel,
timestamp: datetime
timestamp: datetime,
timestep_minutes: float = 5.0
) -> Optional[FaultEvent]:
"""
Genera casualmente un fault.
@@ -684,7 +783,9 @@ class FaultManager:
durante ogni timestep della simulazione.
La probabilità viene scelta in base
al livello gerarchico.
al livello gerarchico, e convertita da
probabilità giornaliera a probabilità
per il singolo timestep.
"""
if not self.config.enabled:
@@ -696,7 +797,7 @@ class FaultManager:
== ComponentLevel.PANEL
):
probability = (
daily_probability = (
self.config
.panel_fault_probability
@@ -707,7 +808,7 @@ class FaultManager:
== ComponentLevel.COMBINER
):
probability = (
daily_probability = (
self.config
.combiner_fault_probability
@@ -715,12 +816,19 @@ class FaultManager:
else:
probability = (
daily_probability = (
self.config
.inverter_fault_probability
)
probability = (
self._daily_to_timestep_probability(
daily_probability,
timestep_minutes
)
)
# Nessun fault
if (
random.random()
@@ -905,7 +1013,8 @@ class FaultManager:
timestamp: datetime,
panel_ids: List[str],
combiner_ids: List[str],
inverter_ids: List[str]
inverter_ids: List[str],
timestep_minutes: float = 5.0
) -> List[FaultEvent]:
"""
Esegue la generazione casuale dei fault
@@ -931,7 +1040,10 @@ class FaultManager:
ComponentLevel.PANEL,
timestamp=
timestamp
timestamp,
timestep_minutes=
timestep_minutes
)
)
@@ -953,7 +1065,10 @@ class FaultManager:
ComponentLevel.COMBINER,
timestamp=
timestamp
timestamp,
timestep_minutes=
timestep_minutes
)
)
@@ -975,7 +1090,10 @@ class FaultManager:
ComponentLevel.INVERTER,
timestamp=
timestamp
timestamp,
timestep_minutes=
timestep_minutes
)
)
@@ -1130,5 +1248,4 @@ class FaultManager:
f"{len(self.faults)}, "
f"active="
f"{active})"
)
```
)

View File

@@ -764,4 +764,3 @@ class Inverter:
f"enabled="
f"{self.enabled})"
)
```

View File

@@ -1123,4 +1123,3 @@ __all__ = [
"records_to_dataframe"
]
```

View File

@@ -887,4 +887,3 @@ class PVPlant:
f"panels="
f"{self.get_panel_count()})"
)
```

View File

@@ -589,6 +589,7 @@ class PVSimulator:
def _generate_faults(
self,
timestamp: datetime,
timestep_minutes: int,
) -> None:
ids = self._component_ids()
@@ -616,6 +617,10 @@ class PVSimulator:
ids[
"inverter"
],
timestep_minutes=
timestep_minutes,
)
# ==================================================================
@@ -755,7 +760,8 @@ class PVSimulator:
enabled=
panel.enabled,
panel.enabled
and fault_factor > 0.0,
)
records.append(
@@ -1530,7 +1536,9 @@ class PVSimulator:
self._generate_faults(
timestamp
timestamp,
config.timestep_minutes,
)
panels = (

View File

@@ -837,4 +837,3 @@ __all__ = [
"calculate_statistics",
]
```

View File

@@ -1441,4 +1441,4 @@ __all__ = [
"SimulationStorage",
"save_simulation",
]"""
]

View File

@@ -675,4 +675,51 @@ class SunModel:
f"timezone='{self.timezone}', "
f"altitude={self.altitude}m)"
)
```
@classmethod
def from_config(cls, config: Dict) -> SunModel:
"""
Crea un'istanza di SunModel da un dizionario di configurazione.
Parameters
----------
config : Dict
Dizionario contenente i parametri di configurazione.
Returns
-------
SunModel
Istanza di SunModel.
"""
location = config['plant']['location']
return cls(
latitude=location['latitude'],
longitude=location['longitude'],
timezone=location['timezone'],
altitude=location['altitude']
)
@classmethod
def from_json(cls, json_path: str) -> SunModel:
"""
Crea un'istanza di SunModel da un file JSON.
Parameters
----------
json_path : str
Percorso del file JSON contenente i parametri di configurazione.
Returns
-------
SunModel
Istanza di SunModel.
"""
import json
with open(json_path, 'r') as f:
config = json.load(f)
return cls.from_config(config)

View File

@@ -587,4 +587,3 @@ __all__ = [
"save_json",
]
```

View File

@@ -917,4 +917,49 @@ class WeatherModel:
f"rain="
f"{self.rain_enabled})"
)
```
@classmethod
def from_config(
cls,
config: Dict,
sun: SunModel
) -> WeatherModel:
"""
Crea un'istanza di WeatherModel da un dizionario di configurazione.
"""
weather_data = config['weather']
return cls(
sun=sun,
clear_sky_model=weather_data['clear_sky_model'],
mean_annual_temperature_C=weather_data['temperature']['mean_annual_C'],
daily_temperature_variation_C=weather_data['temperature']['daily_variation_C'],
seasonal_temperature_variation_C=weather_data['temperature'].get('seasonal_variation_C', 12.0),
cloud_enabled=weather_data['clouds']['enabled'],
mean_cloud_factor=weather_data['clouds']['mean_cloud_factor'],
cloud_variability=weather_data['clouds']['random_variability'],
rain_enabled=weather_data['rain']['enabled'],
rain_probability_daily=weather_data['rain']['probability_daily'],
)
@classmethod
def from_json(
cls,
json_path: str,
sun: SunModel
) -> WeatherModel:
"""
Crea un'istanza di WeatherModel da un file JSON.
"""
import json
with open(json_path, 'r') as f:
config = json.load(f)
return cls.from_config(
config=config,
sun=sun
)