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

@@ -110,7 +110,7 @@ pip install -r requirements.txt
## Configuration ## Configuration
The entire plant is described in `config/plant.json`. No code changes are needed to resize or reconfigure the plant. The entire plant is described in `pv_simulator/config/plant.json`. No code changes are needed to resize or reconfigure the plant.
```json ```json
{ {
@@ -289,7 +289,7 @@ Each simulation timestep produces records at every level of the hierarchy.
## Simulation Pipeline ## Simulation Pipeline
``` ```
config/plant.json pv_simulator/config/plant.json
PVPlant (built from config) PVPlant (built from config)

View File

@@ -1,4 +1,3 @@
```json
{ {
"plant": { "plant": {
"name": "PV_Simulator", "name": "PV_Simulator",

View File

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

View File

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

View File

@@ -317,6 +317,76 @@ class FaultConfig:
random_seed: Optional[int] = None 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 # FAULT MANAGER
@@ -671,11 +741,40 @@ class FaultManager:
# Generazione fault casuale # 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( def generate_random_fault(
self, self,
component_id: str, component_id: str,
component_level: ComponentLevel, component_level: ComponentLevel,
timestamp: datetime timestamp: datetime,
timestep_minutes: float = 5.0
) -> Optional[FaultEvent]: ) -> Optional[FaultEvent]:
""" """
Genera casualmente un fault. Genera casualmente un fault.
@@ -684,7 +783,9 @@ class FaultManager:
durante ogni timestep della simulazione. durante ogni timestep della simulazione.
La probabilità viene scelta in base 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: if not self.config.enabled:
@@ -696,7 +797,7 @@ class FaultManager:
== ComponentLevel.PANEL == ComponentLevel.PANEL
): ):
probability = ( daily_probability = (
self.config self.config
.panel_fault_probability .panel_fault_probability
@@ -707,7 +808,7 @@ class FaultManager:
== ComponentLevel.COMBINER == ComponentLevel.COMBINER
): ):
probability = ( daily_probability = (
self.config self.config
.combiner_fault_probability .combiner_fault_probability
@@ -715,12 +816,19 @@ class FaultManager:
else: else:
probability = ( daily_probability = (
self.config self.config
.inverter_fault_probability .inverter_fault_probability
) )
probability = (
self._daily_to_timestep_probability(
daily_probability,
timestep_minutes
)
)
# Nessun fault # Nessun fault
if ( if (
random.random() random.random()
@@ -905,7 +1013,8 @@ class FaultManager:
timestamp: datetime, timestamp: datetime,
panel_ids: List[str], panel_ids: List[str],
combiner_ids: List[str], combiner_ids: List[str],
inverter_ids: List[str] inverter_ids: List[str],
timestep_minutes: float = 5.0
) -> List[FaultEvent]: ) -> List[FaultEvent]:
""" """
Esegue la generazione casuale dei fault Esegue la generazione casuale dei fault
@@ -931,7 +1040,10 @@ class FaultManager:
ComponentLevel.PANEL, ComponentLevel.PANEL,
timestamp= timestamp=
timestamp timestamp,
timestep_minutes=
timestep_minutes
) )
) )
@@ -953,7 +1065,10 @@ class FaultManager:
ComponentLevel.COMBINER, ComponentLevel.COMBINER,
timestamp= timestamp=
timestamp timestamp,
timestep_minutes=
timestep_minutes
) )
) )
@@ -975,7 +1090,10 @@ class FaultManager:
ComponentLevel.INVERTER, ComponentLevel.INVERTER,
timestamp= timestamp=
timestamp timestamp,
timestep_minutes=
timestep_minutes
) )
) )
@@ -1130,5 +1248,4 @@ class FaultManager:
f"{len(self.faults)}, " f"{len(self.faults)}, "
f"active=" f"active="
f"{active})" f"{active})"
) )
```

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@@ -675,4 +675,51 @@ class SunModel:
f"timezone='{self.timezone}', " f"timezone='{self.timezone}', "
f"altitude={self.altitude}m)" 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", "save_json",
] ]
```

View File

@@ -917,4 +917,49 @@ class WeatherModel:
f"rain=" f"rain="
f"{self.rain_enabled})" 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
)

36
run.py
View File

@@ -1,10 +1,21 @@
import json
from datetime import datetime from datetime import datetime
from pvsim.simulator import ( from pvsim.simulator import (
PVSimulator, PVSimulator,
SimulationConfig, SimulationConfig,
SunModel,
WeatherModel,
) )
from pvsim.faults import (
FaultConfig,
FaultManager,
)
from pvsim.plant import PVPlant
from pvsim.exporter import ( from pvsim.exporter import (
SimulationExporter, SimulationExporter,
) )
@@ -13,12 +24,16 @@ from pvsim.statistics import (
SimulationStatistics, SimulationStatistics,
) )
CONFIG_PATH = 'pv_simulator/config/plant.json'
with open(CONFIG_PATH, 'r') as f:
raw_config = json.load(f)
# ============================================================ # ============================================================
# SIMULAZIONE # SIMULAZIONE
# ============================================================ # ============================================================
config = SimulationConfig( simulation_config = SimulationConfig(
start=datetime( start=datetime(
2026, 2026,
@@ -41,10 +56,25 @@ config = SimulationConfig(
generate_random_faults=True, generate_random_faults=True,
) )
plant = PVPlant.from_json(CONFIG_PATH)
sun_model = SunModel.from_config(raw_config)
weather_model = WeatherModel.from_config(raw_config, sun_model)
fault_config = FaultConfig.from_config(raw_config)
fault_manager = FaultManager(config=fault_config)
simulator = PVSimulator(
plant=plant,
sun=sun_model,
weather=weather_model,
fault_manager=fault_manager,
)
result = simulator.run( result = simulator.run(
config simulation_config
) )
@@ -98,4 +128,4 @@ exporter.export_all(
json=True, json=True,
statistics=True, statistics=True,
) )