diff --git a/README.md b/README.md index b9c3a9a..91a60f4 100644 --- a/README.md +++ b/README.md @@ -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) diff --git a/pv_simulator/config/plant.json b/pv_simulator/config/plant.json index f221afe..abda526 100644 --- a/pv_simulator/config/plant.json +++ b/pv_simulator/config/plant.json @@ -1,4 +1,3 @@ -```json { "plant": { "name": "PV_Simulator", diff --git a/pvsim/combiner.py b/pvsim/combiner.py index d61d3af..b3bcd05 100644 --- a/pvsim/combiner.py +++ b/pvsim/combiner.py @@ -478,4 +478,3 @@ class CombinerBox: f"panels={len(self.panels)}, " f"enabled={self.enabled})" ) -``` diff --git a/pvsim/exporter.py b/pvsim/exporter.py index 2d09b1e..4c80efd 100644 --- a/pvsim/exporter.py +++ b/pvsim/exporter.py @@ -597,4 +597,3 @@ __all__ = [ "export_simulation", ] -``` diff --git a/pvsim/faults.py b/pvsim/faults.py index 0040a6a..a9c20e7 100644 --- a/pvsim/faults.py +++ b/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 ) ) @@ -1130,5 +1248,4 @@ class FaultManager: f"{len(self.faults)}, " f"active=" f"{active})" - ) -``` + ) \ No newline at end of file diff --git a/pvsim/inverter.py b/pvsim/inverter.py index 8a61921..65b037e 100644 --- a/pvsim/inverter.py +++ b/pvsim/inverter.py @@ -764,4 +764,3 @@ class Inverter: f"enabled=" f"{self.enabled})" ) -``` diff --git a/pvsim/models.py b/pvsim/models.py index e093e34..60dad1c 100644 --- a/pvsim/models.py +++ b/pvsim/models.py @@ -1123,4 +1123,3 @@ __all__ = [ "records_to_dataframe" ] -``` diff --git a/pvsim/plant.py b/pvsim/plant.py index 9e745cc..f4bc317 100644 --- a/pvsim/plant.py +++ b/pvsim/plant.py @@ -887,4 +887,3 @@ class PVPlant: f"panels=" f"{self.get_panel_count()})" ) -``` diff --git a/pvsim/simulator.py b/pvsim/simulator.py index 614e51c..a23aa12 100644 --- a/pvsim/simulator.py +++ b/pvsim/simulator.py @@ -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 = ( diff --git a/pvsim/statistics.py b/pvsim/statistics.py index dfda67b..109799b 100644 --- a/pvsim/statistics.py +++ b/pvsim/statistics.py @@ -837,4 +837,3 @@ __all__ = [ "calculate_statistics", ] -``` diff --git a/pvsim/storage.py b/pvsim/storage.py index 2094d16..35c976a 100644 --- a/pvsim/storage.py +++ b/pvsim/storage.py @@ -1441,4 +1441,4 @@ __all__ = [ "SimulationStorage", "save_simulation", -]""" +] diff --git a/pvsim/sun.py b/pvsim/sun.py index 9c5d1fa..e89931c 100644 --- a/pvsim/sun.py +++ b/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) \ No newline at end of file diff --git a/pvsim/utils.py b/pvsim/utils.py index 30e3ced..8996fef 100644 --- a/pvsim/utils.py +++ b/pvsim/utils.py @@ -587,4 +587,3 @@ __all__ = [ "save_json", ] -``` diff --git a/pvsim/weather.py b/pvsim/weather.py index 2972238..a064753 100644 --- a/pvsim/weather.py +++ b/pvsim/weather.py @@ -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 + ) \ No newline at end of file diff --git a/run.py b/run.py index a131502..3991f12 100644 --- a/run.py +++ b/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 ) @@ -98,4 +128,4 @@ exporter.export_all( json=True, statistics=True, -) \ No newline at end of file +)