Files
PV_Simulator/pvsim/faults.py

1136 lines
22 KiB
Python

```python
"""
faults.py
Motore di simulazione dei guasti e delle anomalie
per il simulatore di campo fotovoltaico.
Il modulo permette di simulare eventi a diversi livelli:
PVPanel
|
+-- degradation
+-- soiling
+-- partial_shading
+-- intermittent_fault
+-- permanent_fault
|
v
CombinerBox
|
+-- string_loss
+-- fuse_fault
+-- communication_fault
+-- complete_failure
|
v
Inverter
|
+-- derating
+-- overheating
+-- shutdown
+-- clipping
+-- communication_fault
Ogni fault produce un fattore di riduzione della produzione.
Esempio:
normal power = 1000 W
soiling factor = 0.90
shading factor = 0.80
effective power =
1000 * 0.90 * 0.80
= 720 W
Il modulo non modifica direttamente i componenti PVPanel,
CombinerBox o Inverter.
Fornisce invece un livello indipendente di simulazione
dei fault che può essere interrogato durante ogni timestep.
"""
from __future__ import annotations
from dataclasses import dataclass, field
from datetime import datetime
from enum import Enum
from typing import Dict, List, Optional
import random
# ======================================================================
# ENUMERAZIONE DEI TIPI DI FAULT
# ======================================================================
class FaultType(str, Enum):
"""
Tipologie di guasto disponibili.
"""
# --------------------------------------------------------------
# Pannello
# --------------------------------------------------------------
PANEL_DEGRADATION = (
"panel_degradation"
)
SOILING = (
"soiling"
)
PARTIAL_SHADING = (
"partial_shading"
)
PANEL_INTERMITTENT = (
"panel_intermittent"
)
PANEL_FAILURE = (
"panel_failure"
)
# --------------------------------------------------------------
# Combiner
# --------------------------------------------------------------
COMBINER_STRING_LOSS = (
"combiner_string_loss"
)
COMBINER_FUSE_FAULT = (
"combiner_fuse_fault"
)
COMBINER_COMMUNICATION = (
"combiner_communication"
)
COMBINER_FAILURE = (
"combiner_failure"
)
# --------------------------------------------------------------
# Inverter
# --------------------------------------------------------------
INVERTER_DERATING = (
"inverter_derating"
)
INVERTER_OVERHEATING = (
"inverter_overheating"
)
INVERTER_SHUTDOWN = (
"inverter_shutdown"
)
INVERTER_CLIPPING = (
"inverter_clipping"
)
INVERTER_COMMUNICATION = (
"inverter_communication"
)
# ======================================================================
# LIVELLO DEL COMPONENTE
# ======================================================================
class ComponentLevel(str, Enum):
"""
Livello gerarchico del componente affetto.
"""
PANEL = "panel"
COMBINER = "combiner"
INVERTER = "inverter"
# ======================================================================
# EVENTO DI FAULT
# ======================================================================
@dataclass
class FaultEvent:
"""
Rappresenta un singolo evento di guasto.
Parameters
----------
fault_id:
Identificativo univoco del fault.
fault_type:
Tipo di guasto.
component_level:
Livello gerarchico del componente.
component_id:
ID del componente interessato.
start_time:
Inizio del fault.
end_time:
Fine del fault.
None = fault permanente.
severity:
Severità del fault da 0.0 a 1.0.
enabled:
Indica se il fault è attivo.
description:
Descrizione opzionale.
"""
fault_id: str
fault_type: FaultType
component_level: ComponentLevel
component_id: str
start_time: datetime
end_time: Optional[datetime] = None
severity: float = 1.0
enabled: bool = True
description: str = ""
# --------------------------------------------------------------
# Validazione
# --------------------------------------------------------------
def __post_init__(
self
) -> None:
self.severity = min(
max(
self.severity,
0.0
),
1.0
)
# --------------------------------------------------------------
# Stato
# --------------------------------------------------------------
def is_active(
self,
timestamp: datetime
) -> bool:
"""
Verifica se il fault è attivo al timestamp indicato.
"""
if not self.enabled:
return False
if timestamp < self.start_time:
return False
if (
self.end_time is not None
and timestamp >= self.end_time
):
return False
return True
# --------------------------------------------------------------
# Fattore di riduzione
# --------------------------------------------------------------
def reduction_factor(
self
) -> float:
"""
Restituisce il fattore di produzione associato
alla severità del fault.
severity = 0.0
-> nessuna perdita
severity = 1.0
-> perdita completa
Returns
-------
float
Fattore compreso tra 0.0 e 1.0.
"""
return 1.0 - self.severity
# ======================================================================
# CONFIGURAZIONE FAULT
# ======================================================================
@dataclass
class FaultConfig:
"""
Configurazione per la generazione automatica dei fault.
"""
enabled: bool = True
panel_fault_probability: float = 0.0001
combiner_fault_probability: float = 0.00002
inverter_fault_probability: float = 0.00001
intermittent_probability: float = 0.30
default_fault_duration_hours: float = 4.0
random_seed: Optional[int] = None
# ======================================================================
# FAULT MANAGER
# ======================================================================
@dataclass
class FaultManager:
"""
Gestore centrale dei fault dell'impianto.
Mantiene tutti gli eventi attivi e storici.
Il FaultManager può:
- aggiungere fault manualmente;
- rimuovere fault;
- verificare i fault attivi;
- calcolare il fattore di produzione;
- generare fault casuali;
- fornire informazioni diagnostiche.
"""
config: FaultConfig = field(
default_factory=FaultConfig
)
faults: List[FaultEvent] = field(
default_factory=list
)
_fault_counter: int = 0
# ------------------------------------------------------------------
# Inizializzazione
# ------------------------------------------------------------------
def __post_init__(
self
) -> None:
if (
self.config.random_seed
is not None
):
random.seed(
self.config.random_seed
)
# ------------------------------------------------------------------
# Generazione ID
# ------------------------------------------------------------------
def _generate_fault_id(
self
) -> str:
self._fault_counter += 1
return (
f"FAULT_"
f"{self._fault_counter:06d}"
)
# ------------------------------------------------------------------
# Aggiunta fault
# ------------------------------------------------------------------
def add_fault(
self,
fault_type: FaultType,
component_level: ComponentLevel,
component_id: str,
start_time: datetime,
severity: float = 1.0,
duration_hours: Optional[float] = None,
description: str = ""
) -> FaultEvent:
"""
Crea e registra un nuovo fault.
"""
end_time = None
if duration_hours is not None:
from datetime import timedelta
end_time = (
start_time
+ timedelta(
hours=
duration_hours
)
)
fault = FaultEvent(
fault_id=
self._generate_fault_id(),
fault_type=
fault_type,
component_level=
component_level,
component_id=
component_id,
start_time=
start_time,
end_time=
end_time,
severity=
severity,
description=
description
)
self.faults.append(
fault
)
return fault
# ------------------------------------------------------------------
# Rimozione fault
# ------------------------------------------------------------------
def remove_fault(
self,
fault_id: str
) -> bool:
"""
Rimuove un fault tramite ID.
"""
for fault in self.faults:
if (
fault.fault_id
== fault_id
):
self.faults.remove(
fault
)
return True
return False
# ------------------------------------------------------------------
# Disabilitazione fault
# ------------------------------------------------------------------
def disable_fault(
self,
fault_id: str
) -> bool:
"""
Disabilita un fault senza eliminarlo dallo storico.
"""
for fault in self.faults:
if (
fault.fault_id
== fault_id
):
fault.enabled = False
return True
return False
# ------------------------------------------------------------------
# Fault attivi
# ------------------------------------------------------------------
def get_active_faults(
self,
timestamp: datetime
) -> List[FaultEvent]:
"""
Restituisce tutti i fault attivi.
"""
return [
fault
for fault in self.faults
if fault.is_active(
timestamp
)
]
# ------------------------------------------------------------------
# Fault di un componente
# ------------------------------------------------------------------
def get_component_faults(
self,
component_id: str,
timestamp: datetime
) -> List[FaultEvent]:
"""
Restituisce i fault attivi associati
a uno specifico componente.
"""
return [
fault
for fault in self.faults
if (
fault.component_id
== component_id
and fault.is_active(
timestamp
)
)
]
# ------------------------------------------------------------------
# Fattore componente
# ------------------------------------------------------------------
def get_component_factor(
self,
component_id: str,
timestamp: datetime
) -> float:
"""
Calcola il fattore di produzione complessivo
di un componente.
Se sono presenti più fault contemporaneamente,
i fattori vengono moltiplicati.
Esempio:
Soiling = 0.90
Shading = 0.80
Factor =
0.90 * 0.80
= 0.72
"""
active_faults = (
self.get_component_faults(
component_id,
timestamp
)
)
factor = 1.0
for fault in active_faults:
factor *= (
fault.reduction_factor()
)
return min(
max(
factor,
0.0
),
1.0
)
# ------------------------------------------------------------------
# Stato componente
# ------------------------------------------------------------------
def get_component_status(
self,
component_id: str,
timestamp: datetime
) -> Dict[str, object]:
"""
Restituisce lo stato diagnostico di un componente.
"""
faults = (
self.get_component_faults(
component_id,
timestamp
)
)
factor = (
self.get_component_factor(
component_id,
timestamp
)
)
return {
"component_id":
component_id,
"healthy":
len(
faults
) == 0,
"fault_count":
len(
faults
),
"production_factor":
factor,
"faults":
faults
}
# ------------------------------------------------------------------
# Generazione fault casuale
# ------------------------------------------------------------------
def generate_random_fault(
self,
component_id: str,
component_level: ComponentLevel,
timestamp: datetime
) -> Optional[FaultEvent]:
"""
Genera casualmente un fault.
Il metodo è pensato per essere chiamato
durante ogni timestep della simulazione.
La probabilità viene scelta in base
al livello gerarchico.
"""
if not self.config.enabled:
return None
if (
component_level
== ComponentLevel.PANEL
):
probability = (
self.config
.panel_fault_probability
)
elif (
component_level
== ComponentLevel.COMBINER
):
probability = (
self.config
.combiner_fault_probability
)
else:
probability = (
self.config
.inverter_fault_probability
)
# Nessun fault
if (
random.random()
>= probability
):
return None
# --------------------------------------------------------------
# Selezione tipologia
# --------------------------------------------------------------
if (
component_level
== ComponentLevel.PANEL
):
fault_type = random.choice([
FaultType.SOILING,
FaultType.PARTIAL_SHADING,
FaultType.PANEL_INTERMITTENT,
FaultType.PANEL_FAILURE
])
elif (
component_level
== ComponentLevel.COMBINER
):
fault_type = random.choice([
FaultType.COMBINER_STRING_LOSS,
FaultType.COMBINER_FUSE_FAULT,
FaultType.COMBINER_COMMUNICATION,
FaultType.COMBINER_FAILURE
])
else:
fault_type = random.choice([
FaultType.INVERTER_DERATING,
FaultType.INVERTER_OVERHEATING,
FaultType.INVERTER_SHUTDOWN,
FaultType.INVERTER_CLIPPING,
FaultType.INVERTER_COMMUNICATION
])
# --------------------------------------------------------------
# Intermittent fault
# --------------------------------------------------------------
is_intermittent = (
random.random()
< self.config
.intermittent_probability
)
if is_intermittent:
duration = (
random.uniform(
0.5,
self.config
.default_fault_duration_hours
)
)
else:
duration = None
# --------------------------------------------------------------
# Severità
# --------------------------------------------------------------
if (
fault_type
in [
FaultType.PANEL_FAILURE,
FaultType.COMBINER_FAILURE,
FaultType.INVERTER_SHUTDOWN
]
):
severity = 1.0
elif (
fault_type
== FaultType.SOILING
):
severity = random.uniform(
0.05,
0.30
)
elif (
fault_type
== FaultType.PARTIAL_SHADING
):
severity = random.uniform(
0.10,
0.70
)
elif (
fault_type
== FaultType.INVERTER_DERATING
):
severity = random.uniform(
0.10,
0.50
)
else:
severity = random.uniform(
0.10,
0.40
)
return self.add_fault(
fault_type=
fault_type,
component_level=
component_level,
component_id=
component_id,
start_time=
timestamp,
severity=
severity,
duration_hours=
duration,
description=
"Automatically generated fault"
)
# ------------------------------------------------------------------
# Simulazione automatica
# ------------------------------------------------------------------
def simulate_random_faults(
self,
timestamp: datetime,
panel_ids: List[str],
combiner_ids: List[str],
inverter_ids: List[str]
) -> List[FaultEvent]:
"""
Esegue la generazione casuale dei fault
per tutti i componenti dell'impianto.
Returns
-------
List[FaultEvent]
Fault generati durante il timestep.
"""
generated = []
for panel_id in panel_ids:
fault = (
self.generate_random_fault(
component_id=
panel_id,
component_level=
ComponentLevel.PANEL,
timestamp=
timestamp
)
)
if fault is not None:
generated.append(
fault
)
for combiner_id in combiner_ids:
fault = (
self.generate_random_fault(
component_id=
combiner_id,
component_level=
ComponentLevel.COMBINER,
timestamp=
timestamp
)
)
if fault is not None:
generated.append(
fault
)
for inverter_id in inverter_ids:
fault = (
self.generate_random_fault(
component_id=
inverter_id,
component_level=
ComponentLevel.INVERTER,
timestamp=
timestamp
)
)
if fault is not None:
generated.append(
fault
)
return generated
# ------------------------------------------------------------------
# Diagnostica
# ------------------------------------------------------------------
def diagnostics(
self,
timestamp: datetime
) -> Dict[str, object]:
"""
Restituisce una panoramica diagnostica
dei fault attivi.
"""
active = (
self.get_active_faults(
timestamp
)
)
by_level = {
"panel": 0,
"combiner": 0,
"inverter": 0
}
by_type = {}
for fault in active:
by_level[
fault.component_level.value
] += 1
fault_name = (
fault.fault_type.value
)
by_type[
fault_name
] = (
by_type.get(
fault_name,
0
)
+ 1
)
return {
"timestamp":
timestamp,
"active_faults":
len(
active
),
"by_level":
by_level,
"by_type":
by_type,
"faults":
active
}
# ------------------------------------------------------------------
# Pulizia fault scaduti
# ------------------------------------------------------------------
def cleanup_expired_faults(
self,
timestamp: datetime
) -> int:
"""
Rimuove i fault terminati.
Returns
-------
int
Numero di fault rimossi.
"""
before = len(
self.faults
)
self.faults = [
fault
for fault in self.faults
if (
fault.end_time is None
or fault.end_time > timestamp
)
]
return (
before
- len(
self.faults
)
)
# ------------------------------------------------------------------
# Rappresentazione
# ------------------------------------------------------------------
def __repr__(
self
) -> str:
"""
Rappresentazione leggibile.
"""
active = sum(
1
for fault
in self.faults
if fault.enabled
)
return (
f"FaultManager("
f"total_faults="
f"{len(self.faults)}, "
f"active="
f"{active})"
)
```