| marp | true |
|---|---|
| theme | fhooe |
| header | Kapitel 6: Hybride Dynamische Modelle (2025-12-05) |
| footer | Dr. Georg Hackenberg, Professor für Informatik und Industriesysteme |
| paginate | true |
| math | mathjax |
Dieses Kapitel beinhaltet Folgendes:
- Fallbeispiel "Bouncing Ball" und "Digitaler Sensor".
- Allgemeine Definition und Formalismus hybrider Systeme.
- Erweiterung der Softwarearchitektur für hybride Simulationen.
- Algorithmen zur Nulldurchgangs-detektion (Zero-Crossing Detection).
- Implementierung diskreter und variabler Abtastzeiten.
Dieser Abschnitt beinhaltet Folgendes:
- Mathematische Modellierung der Freiflugphase (kontinuierlich).
- Modellierung des Aufpralls als diskretes Ereignis.
- Herleitung der analytischen Lösung für die hybride Trajektorie.
- Simulation der Abfolge von kontinuierlichen Phasen und diskreten Sprüngen.
Das System wird durch folgende Annahmen vereinfacht:
- Ein-dimensionales System: Bewegung nur entlang der y-Achse.
-
Konstante Erdbeschleunigung:
$g = 9.81 , \text{m/s}^2$ (oder als positiv betrachtet für die Bewegung nach unten). - Kein Luftwiderstand.
- Instantane Kollisionen: Der Aufprall auf den Boden findet ohne Zeitverzögerung statt.
-
Restitutionskoeffizient: Ein konstanter Wert
$e \in [0, 1]$ , der den Energieverlust bei jeder Kollision beschreibt.
Während der Freiflugphase (kein Bodenkontakt) wird die Bewegung des Balls durch folgende Differentialgleichungen beschrieben:
Position (
-
$y(t)$ : Position des Balls zur Zeit$t$ . -
$v(t)$ : Geschwindigkeit des Balls zur Zeit$t$ . -
$g$ : Erdbeschleunigung (positiv angenommen).
Ein Aufprall auf den Boden (bei
-
Position:
$y(t) = 0$
(Ball berührt den Boden) -
Geschwindigkeit:
$v(t) < 0$
(Ball bewegt sich nach unten)
Diese Bedingungen definieren ein sogenanntes Nulldurchgangsereignis, bei dem die kontinuierliche Dynamik unterbrochen wird und eine diskrete Zustandsänderung (Kollision) stattfindet.
Zum Zeitpunkt des Aufpralls
-
$v(t_e^-)$ : Geschwindigkeit unmittelbar vor dem Aufprall. -
$v(t_e^+)$ : Geschwindigkeit unmittelbar nach dem Aufprall. -
$e$ : Restitutionskoeffizient ($0 \le e \le 1$ ). Für$e=1$ ist die Kollision elastisch (kein Energieverlust), für$e=0$ ist sie vollkommen unelastisch (Ball bleibt am Boden liegen).
Die Bewegungsgleichungen
Mit den Anfangsbedingungen
Geschwindigkeit: $$ v(t) = v_0 - g(t - t_0) $$
Position: $$ y(t) = y_0 + v_0(t - t_0) - \frac{1}{2}g(t - t_0)^2 $$
Diese Gleichungen gelten, solange der Ball sich in der Luft befindet (
Um den Zeitpunkt des nächsten Aufpralls (
Ausgehend von
Dies ist eine quadratische Gleichung für
Abfolge von Freiflugphasen und Kollisionsereignissen:
-
Initialisierung: Startposition
$y_0$ und Startgeschwindigkeit$v_0$ zum Zeitpunkt$t_0$ . -
Berechnung der Freiflugphase: Solange
$y(t) > 0$ , wird die Bewegung mit den analytischen Lösungen für$y(t)$ und$v(t)$ berechnet. -
Detektion des Aufpralls: Bestimme den Zeitpunkt
$t_e$ wann$y(t_e^-) = 0$ (unter der Bedingung$v(t_e^-) < 0$ ). -
Zustandsupdate: Berechne die neue Geschwindigkeit
$v(t_e^+) = -e \cdot v(t_e^-)$ und setze$y(t_e^+) = 0$ . -
Iteration: Setze
$t_0 = t_e$ ,$y_0 = y(t_e^+)$ ,$v_0 = v(t_e^+)$ und wiederhole ab Schritt 2, bis die Simulationszeit endet oder der Ball zur Ruhe kommt ($v \approx 0$ und$y \approx 0$ ).
Dieser Abschnitt beinhaltet Folgendes:
- Mathematische Beschreibung eines digitalen Sensors mit diskreter Abtastzeit.
- Diskussion der hybriden Systemeigenschaften.
- Erweiterung des Modells auf eine variable, signalgesteuerte Abtastzeit.
In diesem Beispiel modellieren wir einen einfachen digitalen Sensor, der ein kontinuierliches physikalisches Signal (z.B. eine Spannung) in ein digitales, zeitdiskretes Signal umwandelt.
- Eingang: Ein kontinuierliches, sich stetig änderndes Signal.
- Verarbeitung: Der Sensor misst den Wert des Eingangs zu festen, periodischen Zeitpunkten (Abtastung).
- Ausgang: Der zuletzt gemessene Wert wird bis zur nächsten Abtastung konstant gehalten (Zero-Order Hold).
Dieses System ist ein klassisches Beispiel für ein zeitgesteuertes hybrides System, bei dem diskrete Ereignisse (die Abtastungen) in regelmäßigen Abständen auftreten.
1. Kontinuierliches Eingangssignal
Das zu messende Signal sei eine sinusförmige Spannung
-
$A$ : Amplitude -
$\omega = 2\pi f$ : Kreisfrequenz -
$\phi$ : Phasenverschiebung
Dieses Signal existiert und ändert sich kontinuierlich über die Zeit.
2. Diskreter Abtastvorgang
Der Sensor misst den Wert von
-
$T_s$ : Abtastperiode (z.B. 100 ms)
3. Ausgangssignal (Zero-Order Hold)
Zwischen zwei Abtastungen hält der Sensor den zuletzt gemessenen Wert konstant. Dies wird als Zero-Order Hold (ZOH) bezeichnet. Das Ausgangssignal
Das Gesamtsystem ist ein klassisches hybrides System:
-
Kontinuierliche Dynamik: Das Eingangssignal
$U(t)$ entwickelt sich kontinuierlich. -
Diskrete Ereignisse: Die Abtastzeitpunkte
$t_k$ sind diskrete Ereignisse, die periodisch auftreten. -
Diskrete Zustandsänderung: Bei jedem Abtastereignis wird der interne Zustand des Sensors (der gespeicherte Messwert) aktualisiert. Dies führt zu einer sprunghaften Änderung des Ausgangssignals
$U_{sens}(t)$ .
Im Gegensatz zum Bouncing Ball sind die Ereignisse hier zeitgesteuert (periodisch) und nicht zustandsgesteuert (abhängig von
Eine starre, periodische Abtastung kann ineffizient sein:
- Zu langsame Abtastung: Wichtige Signaländerungen werden verpasst (Aliasing).
- Zu schnelle Abtastung: Redundante Daten werden erzeugt, wenn sich das Signal kaum ändert.
Ansatz: Die Abtastrate wird dynamisch angepasst:
- Hohe Abtastrate bei schnellen Änderungen.
- Niedrige Abtastrate bei langsamen Änderungen.
Dies führt zu einem ereignisgesteuerten hybriden System, bei dem die Abtastereignisse nicht mehr rein zeitgesteuert, sondern auch vom Zustand des Systems abhängig sind.
Die Abtastrate wird durch ein zweites, kontinuierliches Signal gesteuert, das die Änderungsrate des Eingangssignals repräsentiert.
1. Eingangssignal
2. Steuer-Signal (Änderungsrate)
3. Variable Abtastperiode
- Schnelle Änderung (hohes $c(t)$)
$\implies$ kurze Abtastperiode. - Langsame Änderung (niedriges $c(t)$)
$\implies$ lange Abtastperiode.
-
Eingangssignal
$U(t)$ : Das zu messende Signal. In diesem Fall eine Sinuswelle. -
Steuersignal
$c(t)$ : Die Änderungsrate von$U(t)$ , hier der Absolutwert der Ableitung. Hohe Werte von$c(t)$ bedeuten, dass sich$U(t)$ schnell ändert.
Die variable Abtastlogik nutzt
- Hoch bei schnellen Änderungen (hohes $c(t)$).
- Niedrig bei langsamen Änderungen (niedriges $c(t)$).
Ablauf bei jedem Abtast-Schritt
-
Messen: Der Wert des Eingangssignals
$U(t_k)$ wird erfasst. -
Bestimmen der Änderungsrate: Der Wert des Steuersignals
$c(t_k)$ wird ermittelt. -
Berechnen des nächsten Zeitpunkts: Die nächste Abtastperiode
$\Delta t_k$ wird berechnet, z.B. über eine Formel wie: $$ \Delta t_k = \frac{T_{max}}{1 + \text{gain} \cdot c(t_k)} $$ Der nächste Abtastzeitpunkt ist dann$t_{k+1} = t_k + \Delta t_k$ . -
Halten des Werts: Der Ausgang
$U_{sens}(t)$ wird für das Intervall$[t_k, t_{k+1})$ auf den Wert$U(t_k)$ gesetzt.
Dieser Abschnitt beinhaltet Folgendes:
- Definition und Abgrenzung hybrider Modelle.
- Typische Anwendungsbereiche (z.B. Regelungstechnik, Kontaktmechanik).
- Herausforderungen bei der Simulation (Ereignisbehandlung, Stabilität).
- Erweiterung des mathematischen Formalismus um diskrete Zustände, Update-Funktionen und Nulldurchgangsfunktionen.
- Kombination aus kontinuierlichen und diskreten Dynamiken.
- Zustandsänderungen können kontinuierlich (beschrieben durch Differentialgleichungen) oder sprunghaft (ausgelöst durch Ereignisse) sein.
- Beispiele: Systeme mit physikalischen Prozessen, die durch Software gesteuert werden, oder mechanische Systeme mit Kontaktphänomenen.
- Modellierung: Oft durch Differential-Algebraische Gleichungen (DAEs) und endliche Automaten (State Machines).
- Rein kontinuierliche Modelle:
- Zustand ändert sich stetig über die Zeit.
- Beschrieben durch Differentialgleichungen.
- Beispiel: Feder-Masse-Dämpfer-System.
- Rein diskrete Modelle:
- Zustand ändert sich nur zu bestimmten Zeitpunkten.
- Beschrieben durch Ereignisse / Zustandsübergänge.
- Beispiel: Warteschlangensysteme, Petri-Netze.
- Hybride Modelle: Überbrücken die Lücke zwischen diesen beiden Welten, indem sie beide Arten von Dynamiken integrieren.
- Regelungssysteme:
- Kontinuierlicher physikalischer Prozess (z.B. Temperaturregelung).
- Diskret arbeitender digitaler Regler (z.B. PID-Regler in einem Mikrocontroller).
- Schaltvorgänge:
- Elektrische Schaltungen, die ihren Zustand sprunghaft ändern (z.B. Relais, Transistoren).
- Hydraulische oder pneumatische Ventile.
- Physikalische Kontakte:
- Aufprall eines Balls auf eine Oberfläche.
- Öffnen und Schließen von Türen oder Klappen.
- Produktionssysteme:
- Kontinuierliche Materialflüsse (z.B. Flüssigkeiten in Rohren).
- Diskrete Fertigungsschritte (z.B. Montage, Qualitätskontrolle).
- Ereignisbehandlung:
- Präzise Lokalisierung von Ereigniszeitpunkten (z.B. Nulldurchgänge).
- Vermeidung von "Event Missing" (Ereignis wird übersehen) oder "Event Jitter" (ungenauer Zeitpunkt).
- Zustandskonsistenz:
- Sicherstellung korrekter Zustandsübergänge bei Ereignissen.
- Behandlung von Zustands-Resets oder -Modifikationen.
- Numerische Stabilität:
- Integratoren können bei steifen Systemen oder häufigen Ereignissen instabil werden.
- Anpassung der Schrittweite.
- Komplexität:
- Höherer Modellierungs- und Implementierungsaufwand.
- Kombination von kontinuierlichen Solvern und Ereignis-Dispatchern.
Bisheriger Formalismus (rein kontinuierlich)
Zustandsgleichung (Ableitungen): $$ \dot{x}(t) = f(t, x(t), u(t)) $$
Ausgangsgleichung: $$ y(t) = g(t, x(t), u(t)) $$
-
$x(t)$ : Vektor der kontinuierlichen Zustände -
$u(t)$ : Vektor der Eingänge -
$y(t)$ : Vektor der Ausgänge
Erweiterung für hybride Systeme
-
Diskrete Zustände (
$x_d$ ): Speichern von Zustandsinformation, die sich nur sprunghaft ändert. -
Update-Funktion (
$h$ ): Definiert die sprung-hafte Änderung der Zustände bei einem Ereignis. -
Zero-Crossing-Funktion (
$z$ ): Erkennt, wann ein diskretes Ereignis eintritt (z.B. Kollision, Schwellwert erreicht). -
Funktion für Abtastzeitpunkte (
$T_{next}$ ): Definiert den nächsten Zeitpunkt einer diskreten oder variablen Aktualisierung.
Zustände: Ein hybrides System besitzt zwei Arten von Zuständen:
-
Kontinuierliche Zustände
$x_c(t) \in \mathbb{R}^{n_c}$ : Ändern sich stetig (z.B. Position, Geschwindigkeit). -
Diskrete Zustände
$x_d(t) \in \mathbb{R}^{n_d}$ : Ändern sich nur zu diskreten Zeitpunkten (z.B. Schaltzustand, Zähler).
Funktionen: Die Dynamik wird durch fünf Kernfunktionen beschrieben:
-
Ableitungsfunktion
$f$ : Definiert die kontinuierliche Dynamik. $$ \dot{x}_c(t) = f(t, x_c(t), x_d(t), u(t)) $$ -
Ausgangsfunktion
$g$ : Berechnet die Ausgänge des Blocks. $$ y(t) = g(t, x_c(t), x_d(t), u(t)) $$
-
Zero-Crossing-Funktion
$z$ : Eine Vektor-wertige Funktion, deren Nulldurchgänge Ereignisse signalisieren. $$ z(t, x_c(t), x_d(t), u(t)) \in \mathbb{R}^{n_z} $$ Ein Ereignis tritt zum Zeitpunkt$t_e$ auf, wenn eine Komponente$z_i$ das Vorzeichen wechselt (d.h.$z_i(t_e) = 0$ ). -
Update-Funktion
$h$ : Wird zum Ereigniszeitpunkt$t_e$ aufgerufen und berechnet die neuen Zustände$x_c^+$ und$x_d^+$ unmittelbar nach dem Ereignis. $$ (x_c(t_e^+), x_d(t_e^+)) = h(t_e, x_c(t_e^-), x_d(t_e^-), u(t_e)) $$-
$x_c(t_e^-), x_d(t_e^-)$ : Zustände unmittelbar vor dem Ereignis.
-
-
Funktion für nächsten Abtastzeitpunkt
$T_{next}$ : Diese Funktion bestimmt den nächsten Zeitpunkt, zu dem der Block eine diskrete Aktion ausführen muss. $$ t_{next} = T_{next}(t, x_c(t), x_d(t), u(t)) $$- Für kontinuierliche Blöcke wird per Konvention der Wert
0zurückgegeben, um anzuzeigen, dass keine explizite diskrete Abtastung erforderlich ist. - Für Blöcke mit fester Abtastzeit ist der nächste Zeitpunkt deterministisch (
$t_k = t_0 + k \cdot T_s$ ) und muss nicht über diese Funktion berechnet werden. - Für Blöcke mit variabler Abtastzeit wird der nächste Zeitpunkt dynamisch in Abhängigkeit vom aktuellen Zustand und den Eingängen berechnet.
- Für kontinuierliche Blöcke wird per Konvention der Wert
Dieser erweiterte Formalismus, angelehnt an die S-Function-Spezifikation von MATLAB/Simulink, erlaubt die Modellierung des komplexen Zusammenspiels von kontinuierlicher Entwicklung und sprunghaften Zustandsänderungen.
Dieser Abschnitt beinhaltet Folgendes:
- Erweiterung der
Block-Klasse umDiscreteStatesundZeroCrossings. - Einführung des Konzepts der
SampleTime(Diskret, Variabel, Kontinuierlich). - Vorstellung neuer Block-Typen (z.B.
ZeroOrderHold,HitLowerLimit). - Anpassung der Solver-Architektur für hybride Schritte.
Die Block-Klasse, der zentrale Baustein unserer Simulationsumgebung, wurde erheblich erweitert, um diskrete Zustände, Ereignisse und flexible Abtastzeiten verwalten zu können:
DiscreteStates: Eine Liste von diskreten Zustandsvariablen, die sich nur sprunghaft ändern.ZeroCrossings: Eine Liste von Ereignissignalen, deren Nulldurchgänge diskrete Ereignisse auslösen.SampleTime: Ein Objekt, das die Abtasteigenschaften des Blocks definiert (kontinuierlich, diskret, variabel).GetNextVariableHitTime(...): Eine Methode zur Bestimmung des nächsten Zeitpunkts, zu dem ein Block mit variabler Abtastzeit aktiv werden muss.CalculateZeroCrossings(...): Eine Methode zur Berechnung der Werte der Zero-Crossing-Funktionen.UpdateStates(...): Eine Methode, die bei diskret getakteten Blöcken oder nach dem Erkennen eines Nulldurchgangs aufgerufen wird, um die Zustände (kontinuierlich und/oder diskret) sprunghaft anzupassen.
Die Grafik auf der rechten Seite zeigt das UML-Klassendiagramm für die erweiterte Block-Klasse.
Zu beachten sind vor allen die beiden neuen Verbindungen zwischen der Klasse Block und den Klassen ZeroCrossingDeclaration und SampleTime.
Außerdem wurden die Methoden CalcualteZeroCrossings und Update-Stateseingefügt.
public abstract class Block
{
public List<StateDeclaration> ContinuousStates { get; }
public List<StateDeclaration> DiscreteStates { get; }
public List<InputDeclaration> Inputs { get; }
public List<OutputDeclaration> Outputs { get; }
public List<ZeroCrossingDeclaration> ZeroCrossings { get; }
public SampleTime SampleTime { get; }
virtual public void InitializeStates(
double[] cStates, double[] dStates);
virtual public double GetNextVariableHitTime(
double time, double[] cStates, double[] dStates, double[] inputs);
virtual public void CalculateOutputs(
double time, double[] cStates, double[] dStates, double[] inputs, double[] outputs);
virtual public void CalculateDerivatives(
double time, double[] cStates, double[] dStates, double[] inputs, double[] derivatives);
virtual public void CalculateZeroCrossings(
double time, double[] cStates, double[] dStates, double[] inputs, double[] zeroCrossings);
virtual public void UpdateStates(
double time, double[] cStates, double[] dStates, double[] inputs);
}Neben den bereits bekannten Deklarationen für Zustände (StateDeclaration), Eingänge (InputDeclaration) und Ausgänge (OutputDeclaration) wurde eine neue ZeroCrossingDeclaration hinzugefügt, um die Zero-Crossing-Signale eines Blocks zu beschreiben.
namespace SFunctionHybrid.Framework.Declarations
{
public class ZeroCrossingDeclaration : Declaration
{
public ZeroCrossingDeclaration(string name) : base(name)
{
}
}
}Das Konzept der Abtastzeit (SampleTime) wurde eingeführt, um dem Solver mitzuteilen, wie und wann ein Block seine Zustände aktualisieren und Ausgänge berechnen soll. Dies wird durch eine Klassenhierarchie abgebildet:
ContinuousSampleTime: Der Block ist kontinuierlich und wird bei jedem Integrationsschritt des Solvers berechnet (Standard für kontinuierliche Blöcke).DiscreteSampleTime: Der Block ist diskret und wird in festen, periodischen Intervallen aktualisiert. Definiert durchOffset(Startzeitpunkt) undPeriod(Abtastperiode).VariableSampleTime: Der Block ist ereignisgesteuert und wird zu variablen Zeitpunkten aktualisiert, die der Block selbst überGetNextVariableHitTimemeldet. Definiert durch einen initialenOffset.ConstantSampleTime: Für Blöcke, die konstante Werte liefern und keine Abtastzeit im eigentlichen Sinne benötigen (z.B.ConstantBlock).InheritedSampleTime: Die Abtastzeit wird vom übergeordneten System oder den Eingangsblöcken geerbt (Standard für algebraische Blöcke).
public abstract class SampleTime { }
public class ContinuousSampleTime : SampleTime { }
public class DiscreteSampleTime : SampleTime
{
public double Offset { get; }
public double Period { get; }
public DiscreteSampleTime(double offset, double period) { /* ... */ }
}
public class VariableSampleTime : SampleTime
{
public double Offset { get; }
public VariableSampleTime(double offset) { /* ... */ }
}
public class ConstantSampleTime : SampleTime { }
public class InheritedSampleTime : SampleTime { }Der ZeroOrderHoldBlock realisiert die klassische Abtastung und Haltefunktion (Sample & Hold):
-
Abtastzeit:
DiscreteSampleTime. Der Block wird nur zu diskreten Zeitpunkten$t_k = t_0 + k \cdot T_s$ aktiv. -
Zustand: Ein diskreter Zustand
X, der den zuletzt abgetasteten Wert speichert. -
Verhalten:
- Bei jedem Abtastschritt (
UpdateStates) wird der aktuelle Eingangswert in den internen Zustand übernommen ($x_{diskret} = u$ ). - Der Ausgang (
CalculateOutputs) ist immer der aktuelle Wert des Zustands.
- Bei jedem Abtastschritt (
public override void UpdateStates(double time, double[] cStates, double[] dStates, double[] inputs)
{
// Speichern des aktuellen Eingangswerts im diskreten Zustand
dStates[0] = inputs[0];
}Der DiscreteTimeIntegratorBlock führt eine Integration über die Zeit in diskreten Schritten durch (Summation):
- Abtastzeit:
DiscreteSampleTime. - Zustand: Ein diskreter Zustand
X, der das Integral akkumuliert. - Verhalten:
- Nutzt das explizite Euler-Verfahren für die diskrete Integration.
- Bei jedem Schritt wird das Produkt aus Eingangswert und Periodendauer zum Zustand addiert.
public override void UpdateStates(double time, double[] cStates, double[] dStates, double[] inputs)
{
// Integration: Neuer Zustand = Alter Zustand + Eingang * Zeitschritt
double period = ((DiscreteSampleTime)SampleTime).Period;
dStates[0] += inputs[0] * period;
}Dieser Block dient der Detektion von Schwellwert-Unterschreitungen (Events), ohne den Zustand selbst zu ändern:
- Funktion: Meldet ein Ereignis, wenn das Eingangssignal eine untere Schranke unterschreitet.
-
Zero-Crossing: Definiert eine Funktion
$z$ , deren Vorzeichenwechsel vom Solver überwacht wird. $$ z = u - \text{Limit} $$ - Der Solver kann durch Interpolation den exakten Zeitpunkt
$t_e$ des Nulldurchgangs ($z=0$ ) finden ("Zero Crossing Detection").
public override void CalculateZeroCrossings(..., double[] inputs, double[] zeroCrossings)
{
// Das Ereignis tritt auf, wenn diese Funktion das Vorzeichen wechselt
zeroCrossings[0] = inputs[0] - LowerLimit;
}Ein kontinuierlicher Integrator, dessen Zustand durch ein diskretes Ereignis zurückgesetzt werden kann:
-
Kontinuierlicher Zustand:
$x_c$ (das Integral). -
Zero-Crossing: Überwacht ein Trigger-Signal (z.B.
$u_{trigger} - 1$ ). - UpdateStates: Wird ausgeführt, wenn der Solver einen Nulldurchgang detektiert. Setzt den Integrator-Zustand sprunghaft auf einen neuen Wert.
public override void UpdateStates(double time, double[] cStates, ..., double[] inputs)
{
// Wenn das Trigger-Signal aktiv ist (hier == 1)
if (inputs[1] == 1)
{
// ... wird der kontinuierliche Zustand hart zurückgesetzt
cStates[0] = inputs[2];
}
}Ein Integrator mit einer harten unteren Schranke (wichtig z.B. beim Bouncing Ball, damit dieser nicht "durch den Boden" fällt):
- Kombination: Integriert kontinuierlich, überwacht aber gleichzeitig das Limit.
-
Zero-Crossing:
$z = x_c - \text{Limit}$ . Ermöglicht dem Solver, den Zeitpunkt des Aufsetzens exakt zu finden. - UpdateStates: Korrigiert den Zustand beim Erreichen oder Unterschreiten des Limits.
public override void UpdateStates(double time, double[] cStates, ..., double[] inputs)
{
// Falls der Zustand unter das Limit gefallen ist
if (cStates[0] < LowerLimit)
{
// ... wird er auf einen korrigierten Wert (z.B. Position beim Stoß) gesetzt
cStates[0] = inputs[1];
}
}Ein Block, der seine Abtastzeit dynamisch anpassen kann (Ereignissteuerung):
- Abtastzeit:
VariableSampleTime. - GetNextVariableHitTime: Eine spezielle Methode, die vom Solver aufgerufen wird, um zu erfahren, wann dieser Block das nächste Mal berechnet werden muss.
- Anwendung: Ermöglicht adaptive Schrittweitensteuerung durch den Block selbst, z.B. häufige Abtastung bei schnellen Signaländerungen.
public override double GetNextVariableHitTime(double time, ..., double[] inputs)
{
// Der nächste Aufrufzeitpunkt wird dynamisch berechnet
// Hier: Aktuelle Zeit + ein durch den Eingang bestimmtes Delta
double deltaT = inputs[0];
return time + deltaT;
}Die ursprüngliche Solver-Implementierung (siehe Kapitel 4) wurde um die folgenden Punkte erweitert, um mit den diskreten Zustandsübergängen umgehen zu können:
- Sie berücksichtigt nun
DiscreteStatesbei der Initialisierung und Zustandsspeicherung. - Die Logik zur Nulldurchgangsdetektion wurde erweitert und nutzt die
ZeroCrossings-Deklarationen der Blöcke. - Die
UpdateStates-Methode des Solvers ruft nun dieUpdateStates-Methode der Blöcke basierend auf ihrenSampleTime-Eigenschaften (diskret, variabel) oder erkannten Zero-Crossings auf.
Dieser Abschnitt beinhaltet Folgendes:
- Funktionsweise der Nulldurchgangsdetektion (Bisektionsverfahren).
- Integration der Ereignisbehandlung in den Simulationsalgorithmus.
- Vergleich naiver und robuster Implementierungen am Beispiel des Bouncing Balls.
- Auswirkungen auf explizite und implizite Lösungsverfahren.
Der Solver nutzt einen iterativen Prozess, um den genauen Zeitpunkt eines Ereignisses zu finden:
-
Erkennung: Der Solver stellt am Ende eines Integrationsschritts fest, dass eine Zero-Crossing-Funktion
$z$ das Vorzeichen gewechselt hat ($z(t) \cdot z(t+\Delta t) < 0$ ). -
Lokalisierung: Der Zeitschritt wird iterativ halbiert (Bisektion), um den Zeitpunkt
$t_e$ zu finden, an dem$z(t_e) \approx 0$ ist. -
Behandlung:
- Die Simulation wird bis exakt
$t_e$ fortgeführt. -
UpdateStateswird aufgerufen, um diskrete Zustandsänderungen durchzuführen. - Die Simulation wird von
$t_e$ aus fortgesetzt (oft mit Neustart der Integrationsschrittweite).
- Die Simulation wird bis exakt
timeStep = timeStepMax * 2; RememberStates();
while (zeroCrossingValue > Threshold && iteration++ < Limit)
{
timeStep /= 2; ResetStates();
IntegrateContinuousStates(timeStep);
CalculateOutputs(time + timeStep);
zeroCrossingValue = CalculateZeroCrossings(time + timeStep);
}
UpdateStates(time + timeStep);
CalculateOutputs(time + timeStep);
CalculateDerivatives(time + timeStep);
CalculateZeroCrossings(time + timeStep);Wir betrachten ein einfaches System, um die Ereignisbehandlung zu testen:
- Ein konstanter negativer Eingang ("Schwerkraft") zieht den Zustand nach unten.
- Ein
IntegrateWithLowerLimitBlockintegriert diesen Wert. - Das Limit ist bei
$0$ . - Beim Erreichen des Limits, wird der Zustand auf einen Vorgabewert zurückgesetzt.
- Erwartung: Der Zustand sinkt linear ab, trifft exakt auf 0, und wird dann auf den Vorgabewert zurückgesetzt.
Dies demonstriert die Fähigkeit des Solvers, harte physikalische Grenzen (Hard Stops) korrekt abzubilden.
Die blauen Punkte markieren die Berechnungsschritte des Solvers. Man erkennt deutlich, wie der Solver sich an das Limit "herantastet":
- Große Schritte während der linearen Abwärtsbewegung.
- Verdichtung der Schritte unmittelbar vor dem Erreichen der Nulllinie.
- Der Solver findet den exakten Zeitpunkt des Aufpralls und setzt den Zustand auf den Vorgabewert zurück.
Implementierung des Bouncing Balls ohne spezielle Zero-Crossing-Unterstützung im Integrator selbst (nutzt IntegrateWithReset und externen HitLowerLimit):
- Die Beschleunigung ist konstant (Erdbeschleunigung).
- Die Geschwindigkeit wird integriert und kann zurückgesetzt werden.
- Die Position wird normal integriert (kein Zurücksetzen möglich).
- Ein separater Block prüft auf
$y \leq 0$ und löst den Reset der Geschwindigkeit aus. - Bei einem Reset wird die aktuelle Geschwindigkeit mit einem negativen Dämpfungsfaktor multipliziert.
Das Ergebnis sieht auf den ersten Blick akzeptabel aus:
- Die Geschwindigkeitkurve während der Freiflugphase entspricht einer Geraden.
- Die Positionskurve während der Freiflugphase entspricht einer Parabel.
- Wenn die Positionskurve Null erreicht, wird die Geschwindigkeit zurückgesetzt.
- Die Sprünge werden immer kleiner, bis ein minimaler Wert erreicht ist.
- Ab dann wiederholt sich die Sprung-höhe immer wieder.
Hier zeigt sich ein Problem, das vielleicht nicht zu erwarten war:
- Die ersten drei Sprünge werden normal und korrekt berechnet.
- Beim vierten Sprung klebt der Ball auch noch korrekt am Boden.
- Beim fünften Sprung durchbricht der Ball jedoch den Boden, da der Ball bereits am Boden ist (d.h.
$y \leq 0$ ) und das implizite Verfahren für den nächsten Zeitpunkt eine negative Geschwindigkeit ($v < 0$ ) berechnet.
Verwendung des IntegrateWithLowerLimitBlock für die Berechnung der Position:
- Der Integrator für die Position selbst kennt das Limit (d.h.
$y=0$ ). - Er meldet dem Solver proaktiv die Entfernung zum Limit via
CalculateZeroCrossings. - Der Solver kann den exakten Zeitpunkt des Aufpralls
$t_e$ finden, bevor die Integration fortgesetzt wird. - Beim Aufprall wird die Position hart auf 0 gehalten und die Geschwindigkeit (im anderen Integrator) invertiert.
- Das Zurücksetzen der Geschwindigkeit erfolgt wieder mit einem
HitLowerLimitBlock.
Ähnlich wie zuvor gute Ergebnisse, aber mit garantierter Einhaltung der Randbedingung
- Bei der naiven Implementierung ist der Ball bei einem Nulldurchgang immer leicht unterhalb der Nulllinie.
- Bei der erweiterten Implementierung wird die Position des Balls in diesem Fall auf Null zurückgesetzt.
- Somit wird erzwungen, dass sich der Ball niemals unterhalb der Nulllinie befinden kann.
Auch das Ergebnis des impliziten Solvers sieht gut besser aus, da die Randbedingung eingehalten wird:
- Der Ball durchbricht die Nulllinie nicht mehr (im Gegensatz zur naiven Implementierung).
- Die Simulation ist physikalisch robust und numerisch stabil, auch bei variablen Schrittweiten.
- Der Ball bleibt nach einer Weile tatsächlich am Boden liegen, und macht keine Srünge mehr.
In diesem Abschnitt haben wir gesehen:
- Wie diskrete Abtastzeiten (
DiscreteSampleTime) im Framework definiert werden. - Wie der Solver die Integrationsschrittweite anpasst, um Abtastzeitpunkte exakt zu treffen.
- Wie Zustandsaktualisierungen (
UpdateStates) für diskrete Blöcke periodisch ausgeführt werden. - Ein konkretes Beispiel mit Zero-Order Hold und diskretisiertem Integrator.
Der Solver muss wissen, wann der erste diskrete Zeitschritt stattfindet. Dies geschieht in der InitializeStates-Methode.
Für Blöcke mit diskreter Abtastzeit wird der Startzeitpunkt direkt aus der Offset-Eigenschaft übernommen.
// In Solver.InitializeStates()
if (f.SampleTime is DiscreteSampleTime)
{
// Der erste "Hit" ist durch den statischen Offset definiert
NextVariableHitTimes[f] = ((DiscreteSampleTime)f.SampleTime).Offset;
}Der Solver muss sicherstellen, dass er exakt auf einem diskreten Abtastzeitpunkt landet und nicht "darüber hinweg" integriert.
Daher wird die maximale Schrittweite timeStep in jedem Schritt angepasst:
// In EulerExplicitSolver.Solve(...)
foreach (Block b in Blocks)
{
if (b.SampleTime is DiscreteSampleTime)
{
// Reduziere den Zeitschritt, um den nächsten "Hit" nicht zu verpassen
timeStep = Math.Min(timeStep, NextVariableHitTimes[b] - time);
}
}NextVariableHitTimes speichert für jeden Block den absoluten Zeitpunkt der nächsten Ausführung.
Nachdem der Integrationsschritt durchgeführt wurde (und die Zeit t erreicht ist), prüft der Solver, welche diskreten Blöcke "fällig" sind.
// In Solver.UpdateStates(double t)
else if (f.SampleTime is DiscreteSampleTime)
{
// Prüfen, ob der aktuelle Zeitpunkt dem nächsten geplanten Hit entspricht
// (mit einer kleinen Toleranz für Fließkommaungenauigkeiten)
if (Math.Abs(t - NextVariableHitTimes[f]) < 1e-9)
{
// Führe die diskrete Zustandsaktualisierung aus
f.UpdateStates(t, ContinuousStates[f], DiscreteStates[f], Inputs[f]);
// ... (Berechnung des nächsten Zeitpunkts, siehe nächste Folie)
}
}Bei diskreten Abtastzeiten ist der nächste Zeitpunkt streng deterministisch und periodisch.
Nach der Ausführung von UpdateStates wird der nächste Zeitpunkt einfach durch Addition der Periode berechnet:
// In Solver.UpdateStates(double t)
// Bestimmung der nächsten Abtastzeit
NextVariableHitTimes[f] += ((DiscreteSampleTime)f.SampleTime).Period;Dies garantiert ein festes Raster von Ausführungszeitpunkten:
Das Modell kombiniert kontinuierliche Integration mit diskreter Abtastung:
-
Constant: Liefert den Wert
1. -
Integrate 1: Integriert kontinuierlich
$\to$ Rampe$t$ . -
Integrate 2: Integriert die Rampe
$\to$ Parabel$0.5 t^2$ . - Zero Order Hold: Tastet das Ergebnis von "Integrate 2" diskret ab (z.B. alle 1s) und hält den Wert.
Das Diagramm zeigt die kontinuierliche Parabel (blau/orange) und das treppenförmige Ausgangssignal des Zero-Order Holds (grün).
- Der Solver verkleinert seine Schritte exakt so, dass er die Abtastzeitpunkte (1s, 2s, 3s...) trifft.
- Zwischen den Abtastzeitpunkten bleibt das grüne Signal konstant.
- Zum Abtastzeitpunkt springt es auf den aktuellen Wert der kontinuierlichen Eingabe.
In diesem Abschnitt haben wir gesehen:
- Wie variable Abtastzeiten (
VariableSampleTime) definiert werden. - Die Rolle der Methode
GetNextVariableHitTimezur dynamischen Planung. - Wie Blöcke ihre eigene Ausführungsfrequenz basierend auf Prozesseingängen steuern können.
Auch bei variabler Abtastzeit muss der erste Aufrufzeitpunkt bekannt sein, um die Ereigniskette zu starten.
Da die Methode GetNextVariableHitTime oft von aktuellen Eingängen abhängt (die zu Beginn noch nicht vorliegen), wird der erste Zeitpunkt statisch über den Offset der VariableSampleTime definiert.
// In Solver.InitializeStates()
else if (f.SampleTime is VariableSampleTime)
{
// Initialisierung des ersten variablen Zeitpunkts
NextVariableHitTimes[f] = ((VariableSampleTime)f.SampleTime).Offset;
}Der Solver behandelt variable Abtastzeiten bei der Schrittweitensteuerung identisch zu diskreten Abtastzeiten.
Er prüft den Eintrag in NextVariableHitTimes (der dynamisch aktualisiert wurde) und reduziert bei Bedarf den Zeitschritt, um exakt auf dem Ereignis zu landen.
// In EulerExplicitSolver.Solve(...)
foreach (Block b in Blocks)
{
if (b.SampleTime is DiscreteSampleTime || b.SampleTime is VariableSampleTime)
{
// Reduziere den Zeitschritt, um den nächsten "Hit" nicht zu verpassen
timeStep = Math.Min(timeStep, NextVariableHitTimes[b] - time);
}
}Auch hier ist die Logik analog, aber sie delegiert die Berechnung des nächsten Zeitpunkts an den Block selbst (statt der Berechnung mittels Abtastperiode).
// In Solver.UpdateStates(double t)
else if (f.SampleTime is VariableSampleTime)
{
if (Math.Abs(t - NextVariableHitTimes[f]) < 1e-9)
{
// Diskrete Zustandsänderung ausführen
f.UpdateStates(t, ContinuousStates[f], DiscreteStates[f], Inputs[f]);
// Den Block fragen, wann er das nächste Mal aufgerufen werden möchte
NextVariableHitTimes[f] = f.GetNextVariableHitTime(
t, ContinuousStates[f], DiscreteStates[f], Inputs[f]);
}
}Die Klasse Block stellt die virtuelle Methode GetNextVariableHitTime bereit, die von spezifischen Blöcken überschrieben werden kann.
Beispiel VariableSampleTimeBlock:
Der nächste Aufrufzeitpunkt wird durch ein Eingangssignal (inputs[0]) bestimmt.
public override double GetNextVariableHitTime(double time, ..., double[] inputs)
{
// Das Eingangssignal bestimmt das Delta bis zum nächsten Hit
double deltaT = inputs[0];
if (deltaT > 0)
{
return time + deltaT;
}
return double.MaxValue; // Keine weiteren Hits
}Ein System, bei dem die Abtastrate kontinuierlich abnimmt (d.h. das Zeitintervall
- Slope (0.1): Konstante Steigung, die durch den Integrator integriert wird.
-
Integrator: Integriert die Steigung
$\to$ Linear ansteigendes Signal (das$\Delta t$ ). -
VariableSampler: Ein Block, der nur zu den Zeitpunkten aktiv wird, die durch das Integral bestimmt werden. Er zeichnet den Zeitpunkt des "Hits" auf.
- Die Abtastzeiten werden in diesem Beispiel über die Zeit immer länge, da sie durch das Ausgangssignal des Integrators bestimmt werden.
-
Orange Kurve: Das kontinuierlich ansteigende
$\Delta t$ (Ausgang des Integrators). -
Blaue Punkte: Die Zeitpunkte, zu denen der
VariableSamplerdas letzte Mal aktiv wurde.
Man sieht deutlich, dass der zeitliche Abstand zwischen den blauen Punkten (Stufen) immer größer wird, da das
Der Block steuert seine eigene Ausführungs-frequenz dynamisch basierend auf dem Systemzustand.

















