fix: Fix syntax errors, wire config-driven simulation in run.py, and correct fault-rate/availability KPI bugs
This commit is contained in:
@@ -110,7 +110,7 @@ pip install -r requirements.txt
|
||||
|
||||
## 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
|
||||
{
|
||||
@@ -289,7 +289,7 @@ Each simulation timestep produces records at every level of the hierarchy.
|
||||
## Simulation Pipeline
|
||||
|
||||
```
|
||||
config/plant.json
|
||||
pv_simulator/config/plant.json
|
||||
│
|
||||
▼
|
||||
PVPlant (built from config)
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
```json
|
||||
{
|
||||
"plant": {
|
||||
"name": "PV_Simulator",
|
||||
|
||||
@@ -478,4 +478,3 @@ class CombinerBox:
|
||||
f"panels={len(self.panels)}, "
|
||||
f"enabled={self.enabled})"
|
||||
)
|
||||
```
|
||||
|
||||
@@ -597,4 +597,3 @@ __all__ = [
|
||||
|
||||
"export_simulation",
|
||||
]
|
||||
```
|
||||
|
||||
137
pvsim/faults.py
137
pvsim/faults.py
@@ -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
|
||||
)
|
||||
)
|
||||
|
||||
@@ -1131,4 +1249,3 @@ class FaultManager:
|
||||
f"active="
|
||||
f"{active})"
|
||||
)
|
||||
```
|
||||
|
||||
@@ -764,4 +764,3 @@ class Inverter:
|
||||
f"enabled="
|
||||
f"{self.enabled})"
|
||||
)
|
||||
```
|
||||
|
||||
@@ -1123,4 +1123,3 @@ __all__ = [
|
||||
|
||||
"records_to_dataframe"
|
||||
]
|
||||
```
|
||||
|
||||
@@ -887,4 +887,3 @@ class PVPlant:
|
||||
f"panels="
|
||||
f"{self.get_panel_count()})"
|
||||
)
|
||||
```
|
||||
|
||||
@@ -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 = (
|
||||
|
||||
@@ -837,4 +837,3 @@ __all__ = [
|
||||
|
||||
"calculate_statistics",
|
||||
]
|
||||
```
|
||||
|
||||
@@ -1441,4 +1441,4 @@ __all__ = [
|
||||
"SimulationStorage",
|
||||
|
||||
"save_simulation",
|
||||
]"""
|
||||
]
|
||||
|
||||
49
pvsim/sun.py
49
pvsim/sun.py
@@ -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)
|
||||
@@ -587,4 +587,3 @@ __all__ = [
|
||||
|
||||
"save_json",
|
||||
]
|
||||
```
|
||||
|
||||
@@ -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
|
||||
)
|
||||
34
run.py
34
run.py
@@ -1,10 +1,21 @@
|
||||
import json
|
||||
|
||||
from datetime import datetime
|
||||
|
||||
from pvsim.simulator import (
|
||||
PVSimulator,
|
||||
SimulationConfig,
|
||||
SunModel,
|
||||
WeatherModel,
|
||||
)
|
||||
|
||||
from pvsim.faults import (
|
||||
FaultConfig,
|
||||
FaultManager,
|
||||
)
|
||||
|
||||
from pvsim.plant import PVPlant
|
||||
|
||||
from pvsim.exporter import (
|
||||
SimulationExporter,
|
||||
)
|
||||
@@ -13,12 +24,16 @@ from pvsim.statistics import (
|
||||
SimulationStatistics,
|
||||
)
|
||||
|
||||
CONFIG_PATH = 'pv_simulator/config/plant.json'
|
||||
|
||||
with open(CONFIG_PATH, 'r') as f:
|
||||
raw_config = json.load(f)
|
||||
|
||||
# ============================================================
|
||||
# SIMULAZIONE
|
||||
# ============================================================
|
||||
|
||||
config = SimulationConfig(
|
||||
simulation_config = SimulationConfig(
|
||||
|
||||
start=datetime(
|
||||
2026,
|
||||
@@ -41,10 +56,25 @@ config = SimulationConfig(
|
||||
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(
|
||||
|
||||
config
|
||||
simulation_config
|
||||
)
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user