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
|
## 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)
|
||||||
|
|||||||
@@ -1,4 +1,3 @@
|
|||||||
```json
|
|
||||||
{
|
{
|
||||||
"plant": {
|
"plant": {
|
||||||
"name": "PV_Simulator",
|
"name": "PV_Simulator",
|
||||||
|
|||||||
@@ -478,4 +478,3 @@ class CombinerBox:
|
|||||||
f"panels={len(self.panels)}, "
|
f"panels={len(self.panels)}, "
|
||||||
f"enabled={self.enabled})"
|
f"enabled={self.enabled})"
|
||||||
)
|
)
|
||||||
```
|
|
||||||
|
|||||||
@@ -597,4 +597,3 @@ __all__ = [
|
|||||||
|
|
||||||
"export_simulation",
|
"export_simulation",
|
||||||
]
|
]
|
||||||
```
|
|
||||||
|
|||||||
137
pvsim/faults.py
137
pvsim/faults.py
@@ -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
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -1131,4 +1249,3 @@ class FaultManager:
|
|||||||
f"active="
|
f"active="
|
||||||
f"{active})"
|
f"{active})"
|
||||||
)
|
)
|
||||||
```
|
|
||||||
|
|||||||
@@ -764,4 +764,3 @@ class Inverter:
|
|||||||
f"enabled="
|
f"enabled="
|
||||||
f"{self.enabled})"
|
f"{self.enabled})"
|
||||||
)
|
)
|
||||||
```
|
|
||||||
|
|||||||
@@ -1123,4 +1123,3 @@ __all__ = [
|
|||||||
|
|
||||||
"records_to_dataframe"
|
"records_to_dataframe"
|
||||||
]
|
]
|
||||||
```
|
|
||||||
|
|||||||
@@ -887,4 +887,3 @@ class PVPlant:
|
|||||||
f"panels="
|
f"panels="
|
||||||
f"{self.get_panel_count()})"
|
f"{self.get_panel_count()})"
|
||||||
)
|
)
|
||||||
```
|
|
||||||
|
|||||||
@@ -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 = (
|
||||||
|
|||||||
@@ -837,4 +837,3 @@ __all__ = [
|
|||||||
|
|
||||||
"calculate_statistics",
|
"calculate_statistics",
|
||||||
]
|
]
|
||||||
```
|
|
||||||
|
|||||||
@@ -1441,4 +1441,4 @@ __all__ = [
|
|||||||
"SimulationStorage",
|
"SimulationStorage",
|
||||||
|
|
||||||
"save_simulation",
|
"save_simulation",
|
||||||
]"""
|
]
|
||||||
|
|||||||
49
pvsim/sun.py
49
pvsim/sun.py
@@ -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)
|
||||||
@@ -587,4 +587,3 @@ __all__ = [
|
|||||||
|
|
||||||
"save_json",
|
"save_json",
|
||||||
]
|
]
|
||||||
```
|
|
||||||
|
|||||||
@@ -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
|
||||||
|
)
|
||||||
34
run.py
34
run.py
@@ -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
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user