Warum findet das Modell bei echten Daten null Verletzungen?
In meiner fortlaufenden Serie zur Netzresilienz – von thermischen Engpässen bis hin zur Kurzschlussstromanalyse – geht es in diesem Beitrag um ein Ergebnis, das wie ein Fehler aussah und sich als Herkunftsproblem herausstellte: Eine Streaming-Spannungsüberwachungs-Pipeline, die bei ihrer eigenen Standard-Betriebszustandstabelle Hunderte von Verletzungen findet und exakt null, sobald man echte, unabhängig veröffentlichte Lastdaten einwechselt.
Die Pipeline in Kürze
Das Setup: Ein Jahr echter Redispatch-Anweisungen deutscher Übertragungsnetzbetreiber (20.586 Ereignisse), die über einen Streaming-Screener auf dem CIGRE-Mittelspannungs-Benchmark-Netz abgespielt werden. Ein günstiger statistischer Selektor (Perzentil- und Änderungsraten-Schwellenwerte für die Redispatch-Größe) markiert die extremsten 4.055 Ereignisse als "kritisch" – keine Lastflussberechnung, nur Arithmetik im Stream. Nur diese markierten Ereignisse erhalten eine vollständige Newton-Raphson-AC-Berechnung, um zu überprüfen, ob sie tatsächlich eine Verletzung der Spannungsgrenze verursachen (U < 0,90 p.u.).
Unter dem Standard-Betriebszustandsmodell der Pipeline – einer deterministischen, gehashten Tabelle stündlicher Lastmultiplikatoren – findet diese AC-Validierung 239 bestätigte Verletzungen unter den 4.055 markierten Ereignissen. Eine Regression gegen die konvergierten Berechnungen zeigt warum: Die Hintergrundlast korreliert mit der Spannung mit r = −0,99, während die Größe des Redispatch-Ereignisses selbst nur mit r = 0,06 korreliert. Das Ereignis spielt kaum eine Rolle; der Hintergrundzustand des Netzes leistet fast die gesamte Arbeit.
Das ist ein sauberes, physikalisch sinnvolles Ergebnis. Das Problem begann, als ich versuchte zu bestätigen, dass es sich nicht um ein Artefakt der Konstruktion der synthetischen Tabelle handelte.
Der Austausch, der alles kaputt machte
SimBench veröffentlicht echte, unabhängig kuratierte deutsche Lastprofile – nicht synthetisch, nicht auf dieses Projekt abgestimmt. Ich habe zwei Betriebszustands-Provider darum herum gebaut: einen, der SimBench-Werte i.i.d. pro Ereignis zieht (was jede Korrelation mit der Ereignisreihenfolge aufbricht), und einen, der sie in ihrer ursprünglichen zeitlichen Abfolge abspielt. Beide sind direkter Ersatz für dieselben 4.055 Ereignisse auf dem kritischen Pfad, derselbe Selektor, dasselbe Netz.
Die Korrelation zwischen Last und Spannung replizierte sich fast exakt: r = −0,991 bei echten SimBench-Daten gegenüber −0,994 beim synthetischen Standard. Das ist beruhigend – die zugrunde liegende Physik ist kein Artefakt davon, wie die Standardtabelle erstellt wurde.
Die Anzahl der Verletzungen ließ sich nicht replizieren. Null. Nicht "weniger" – null, über beide Echtdaten-Varianten hinweg, bei denselben 4.055 Ereignissen, die unter der Standardtabelle 239 Verletzungen erzeugen.
Spurensuche
Der Instinkt hierbei ist, dies als Ärgernis zu behandeln und weiterzumachen – wählen Sie die Version, die die Schlagzeilen-Zahl des Papiers stützt. Genau diesem Instinkt sollte man widerstehen. Wenn der Mechanismus (die Last dominiert die Spannung) verallgemeinerbar ist, die Anzahl der Verletzungen aber nicht, besteht das einzig Ehrliche darin, herauszufinden warum, und nicht nur die Diskrepanz anzumerken.
Die Herkunft der synthetischen Standardtabelle lässt sich drei Stationen zurückverfolgen:
- Rudion et al., 2006 – das Originalpapier der CIGRE Task Force C6.04.02, das genau dieses Mittelspannungs-Benchmark-Netz definiert, einschließlich seiner dokumentierten Spezifikation für die Spitzenlast.
- Porsinger et al., 2017 – saisonale Lastkurven, die für dieselbe Netztopologie simuliert und in Energies veröffentlicht wurden.
- Ein Kurator eines öffentlichen Repositorys, der spezifische mehrtägige Zeitfenster aus diesen Daten ausgewählt hat, die ausdrücklich so dokumentiert sind, dass sie Netzüberlastungen und Spannungsgrenzprobleme verursachen – ein Stresstest-Szenario, kein repräsentatives.
Die Zahl, die aus dieser Kette herausfällt: Die Spitzenlast der Standardtabelle liegt 26 % über der netzeigenen dokumentierten Spitzenlast-Spezifikation von Rudion et al. Das ist kein Modellierungsfehler. Es handelt sich um ein absichtlich kuratiertes Überlastungsfenster, drei Zitationen von den eigenen Zahlen des Benchmarks entfernt, das still und leise die Arbeit verrichtet, "Verletzungen zu produzieren", und zwar in einer Pipeline, die es als Standard geerbt hat.
Was das eigentlich bedeutet
Echte, repräsentative Lastdaten – die Art, die SimBench bereitstellen soll – überschreiten diesen dokumentierten Spitzenwert in diesem Benchmark nie. Genau deshalb produzieren sie null Verletzungen: Bei diesem Netz, mit dieser dezentralen Erzeugungskonfiguration und diesem realen Ereignis-Stream treten bestätigte Verletzungen nur auf, wenn die Last über die spezifizierte Hüllkurve des Benchmarks hinaus getrieben wird. Die Zahl von 239 Verletzungen ist nicht falsch, aber sie ist auch nicht die "typische Schwere" – es ist das, was speziell unter einer dokumentierten, kuratierten Überlastung passiert.
Diese Unterscheidung – Mechanismus verallgemeinerbar, Gesamtzahl nicht – wurde zum roten Faden für den Rest der Untersuchung. Der nächste Beitrag behandelt ein zweites, schlimmeres Problem, das beim Versuch aufgetaucht ist, dieses zu beheben: eine Behauptung über eine Überwachungsregel, die wasserdicht aussah und sich als zirkulär konstruiert herausstellte.
Reproduzierbarkeit: Verfolgung der Korrelation unter beiden Betriebszustandsmodellen
from engine import (
GridSimulator, LocalCsvIngestionLayer,
SyntheticLoadProvider, TimeSeriesLoadProvider,
load_simbench_profile,
)
simulator = GridSimulator()
stream = LocalCsvIngestionLayer().fetch_stream()
# Standard-Betriebszustandsmodell (E2): 239 Verletzungen, r(Last, U) = -0.994
default_provider = SyntheticLoadProvider(simulator.load_multipliers)
cycle_default = simulator.run_streaming_pipeline(stream, load_provider=default_provider)
# Reales SimBench-Lastprofil, zeitlich geordnet (E4): 0 Verletzungen, r(Last, U) = -0.991
simbench_vals = load_simbench_profile()
real_provider = TimeSeriesLoadProvider(simbench_vals)
cycle_real = simulator.run_streaming_pipeline(stream, load_provider=real_provider)
for name, cycle_df, provider in [("default (E2)", cycle_default, default_provider),
("real SimBench (E4)", cycle_real, real_provider)]:
critical = cycle_df[cycle_df["critical_event"] == True]
solved = critical.dropna(subset=["raw_vm_ref_pu"]).copy()
# "load_mw" ist die Größe des Redispatch-Ereignisses selbst, nicht die
# Hintergrundlast -- der Proxy für die Hintergrundlast ist "multiplier",
# unten pro Ereignis nachgeschlagen.
solved["multiplier"] = solved["event_idx"].apply(lambda i: provider.get_multiplier(int(i)))
n_violations = (solved["raw_vm_ref_pu"] < 0.90).sum()
r_last = solved["multiplier"].corr(solved["raw_vm_ref_pu"])
r_groesse = solved["load_mw"].corr(solved["raw_vm_ref_pu"])
print(f"{name}: {n_violations} Verletzungen, "
f"r(Last, U) = {r_last:.3f}, r(Ereignisgröße, U) = {r_groesse:.3f}")


