Arraybasierte NumPy-Traceschrittvorlagen erstellen

Das Vorgehen zum Anlegen einer neuen Traceschrittvorlage wurde bereits im Abschnitt Allgemeines Vorgehen erläutert. Während das Anlegen von Signalen und Parametern für alle Vorlagearten identisch ist, unterscheidet sich die Implementierung der eigentlichen Analysevorschrift deutlich.

Mit arraybasierten NumPy-Traceschrittvorlagen kann man eigenen Python-Code implementieren, dem die Werte und Zeitstempel aller Ein- und Ausgangssignale in Form von NumPy-Arrays zur Verfügung gestellt werden. Zentraler Bestandteil einer arraybasierten NumPy-Traceschrittvorlage ist die Methode Process(), die einmal pro Analysedurchlauf aufgerufen wird. Dabei werden der Methode folgende Parameter übergeben:

parameters:

Ein Python-Dictionary, in dem zu jedem in der Vorlage definierten Parameter der im Traceschritt angegebene Parameter-Wert hinterlegt ist.

report:

Ein Objekt, das den Zugriff auf den Testreport erlaubt.

timeAxis:

Die gemeinsame Zeitachse in Form eines NumPy-Arrays von Abtastzeitpunkten.

ranges:

Eine Liste von Triggerbereichen. Bei einer ungetriggerten Analyse wird der gesamte durch die gemeinsame Zeitachse vorgegebene Definitionsbereich als einziger Bereich übergeben.

signals:

Ein Python-Dictionary, in dem zu jedem in der Vorlage definierten Ein- und Ausgangssignal ein Signal-Objekt hinterlegt ist, über das auf Werte und Zeitstempel aus dem gesamten Definitionsbereich des jeweiligen Signals zugegriffen werden kann.

Eine vollständige Beschreibung aller relevanten Methoden und Schnittstellen für arraybasierte NumPy-Traceschrittvorlagen sowie ein Hinweis zur Aktivierung der Typüberprüfung und Autovervollständigung in Ihrem externen Editor finden Sie in der Dokumentation zur Trace Analysis API.

Empfehlungen für den weiteren Einstieg

Nachfolgend sind einige Tutorials und Beispiele aufgeführt, die sich als Einstieg in das Thema arraybasierte NumPy-Traceschrittvorlage eignen. Dabei werden Kenntnisse in der Programmierung mit Python vorausgesetzt. Von Vorteil sind außerdem Grundkenntnisse im Umgang mit der Python-Bibliothek NumPy, die durch Lesen des Abschnitts Umgang mit NumPy erworben werden können.

  • Im Tutorial Erstellen einer eigenen arraybasierten NumPy-Traceschrittvorlage wird aufbauend auf dem Tutorial Wie ein Package mit Traceanalyse erstellt wird die Erstellung der arraybasierten NumPy-Traceschrittvorlage CheckTreshold, die auch als Beispiel-Vorlage mit ausgeliefert wird, erklärt.

  • CountSamples ist eine weitere ausführlich dokumentierte Beispiel-Vorlage, anhand der ein Einblick in die Erstellung arraybasierter NumPy-Traceschrittvorlagen gewonnen werden kann. (Die Beispiel-Vorlagen werden im Unterordner ExampleTraceStepTemplates/NumPyArrayBased des Traceschrittvorlagen-Ordners abgelegt, wenn man die Option zum Kopieren von Beispieldateien beim Öffnen des ecu.test-Workspaces anwählt.)

  • Im nachfolgenden Beispiel Berechnung eines Ausgangssignals wird das Berechnen von Signalen durch arraybasierte NumPy-Traceschrittvorlagen demonstriert.

  • Im darauffolgenden Beispiel Zeitachsendefinition werden die Optionen zur Handhabung von Zeitachsen in arraybasierten NumPy-Traceschrittvorlagen beleuchtet.

Beispiel: Berechnung eines Ausgangssignals

Im Tutorial Erstellen einer eigenen arraybasierten NumPy-Traceschrittvorlage wird die Traceschrittvorlage CheckTreshold entwickelt, mit der eine Schwellwertprüfung für ein Signal durchgeführt werden kann. Beispielhaft soll nun die Aufgabenstellung betrachtet werden, dass die Summe zweier Signale gebildet und anschließend mit einem Schwellwert verglichen werden soll.

Um die Vorlage CheckTreshold unverändert nachnutzen zu können, empfiehlt es sich, die Summenbildung in einem getrennten Traceschritt durchzuführen, das Ergebnis in einem berechneten Signal zu speichern und dieses anschließend per CheckTreshold zu prüfen. Dementsprechend wird in diesem Beispiel eine arraybasierte NumPy-Traceschrittvorlage AddSignals entwickelt, die die Summe zweier Signale in einem dritten Signal speichert. Erstellen Sie dazu - wie im Tutorial gezeigt - eine arraybasierte NumPy-Traceschrittvorlage mit der ID AddSignals und folgenden Signalen:

  • Signal1 und Signal2, Richtung IN

  • Signal3, Richtung OUT

Die Methode Process() der Vorlage ist wie folgt zu implementieren:

def Process(self, parameters, report, timeAxis, ranges, signals):
    """
    ...

    """
    outSignal = signals['Signal3']
    for triggerRange in ranges:
        timestamps = triggerRange.GetTimestamps()
        outValues = triggerRange.GetValues('Signal1') + triggerRange.GetValues('Signal2')

        outSignal.Emit(timestamps, outValues)

Um die Traceschrittvorlage AddSignals sinnvoll in einer Traceanalyse verwenden zu können, müssen in letzterer mindestens drei generische Signale deklariert sein - zwei, die addiert werden sollen und eines, das den berechneten Summenverlauf speichern wird. Letzteres Signal ist dann auch jenes, welches mittels CheckThreshold auf Schwellwertüberschreitungen überprüft werden soll.

Befolgen Sie nun folgende Schritte, um die Aufgabenstellung mithilfe der eben erstellten Traceschrittvorlage zu lösen:

  • Erstellen Sie ein neues Package.

  • Fügen Sie dem Package aus der Datei SigAB.csv (zu finden im Ordner ExampleTraces/NumPyArrayBased) beide Signale SigA und SigB als Signalaufnahmen hinzu (so wie im Abschnitt Wie ein Package mit Traceanalyse erstellt wird beschrieben).

  • Legen Sie einen neue Traceanalyse an.

  • Deklarieren Sie die generischen Signale Signal1, Signal2 und Signal3.

  • Binden Sie das aufgenommene SigA an Signal1 und das aufgenommene SigB an Signal2.

  • Erstellen Sie mit der eben erstellten Vorlage einen Traceschritt AddSignals. Binden Sie in dessen Konfigurationsdialog die drei Signale an die gleichnamigen generischen Signale der Traceanalyse.

  • Erstellen Sie mit der Vorlage aus dem Tutorial einen nachgelagerten Traceschritt CheckThreshold. Binden Sie das generische Signal3 an dessen Eingangssignal und legen Sie einen Threshold von 10 fest.

  • Fügen Sie einen dem Traceschritt CheckTreshold untergeordneten Plot ein. Sorgen Sie dafür, dass neben dem Eingangssignal von CheckTreshold auch die beiden generischen Signale Signal1 und Signal2 geplottet werden (Drag’n’Drop von nicht plotten zu Oberer Plot im Konfigurationsdialog des Plots). Stellen Sie außerdem für alle Signale den Markerstil ‚o‘ ein (Doppelklick auf das jeweilige Signal im Konfigurationsdialog des Plots, Auswahl des Markerstils).

Nach Ausführung der Traceanalyse wird als Teil des Testreports folgender Plot generiert:

../../../../../_images/Beispiel_AddSignals_Plot.png

Ergebnis der Schwellwertprüfung für ein Summensignal

In dem Plot sind in blau und grau die Verläufe der Signalaufnahmen SigA und SigB und in orange das berechnete Summensignal zu sehen. Ferner ist der Bereich ab Zeitstempel 7 rot hinterlegt, weil ab diesem Zeitpunkt die an das Summensignal gestellte Schwellwertbedingung verletzt wurde.

Beispiel: Zeitachsendefinition

Im vorangegangenen Beispiel stammten die beiden Signale SigA und SigB aus derselben CSV-Datei SigAB.csv, und wurden daher zu identischen Zeitpunkten abgetastet. Ein anderes Bild ergibt sich, wenn man die Datei SigC.csv (ebenfalls zu finden im Ordner ExampleTraces/NumPyArrayBased) als weitere Signalaufnahme eingefügt und das enthaltene Signal SigC an das generische Signal Signal2 bindet (sodass SigA und SigC addiert werden):

../../../../../_images/Beispiel_TimeAxis_or.png

Schwellwertprüfung für Summensignal mit Standard-Zeitachsendefinition (‚Signal1 or Signal2‘)

In der Abbildung sind die Abtastzeitpunkte als ausgefüllte Kreise hervorgehoben. Es ist erkennbar, dass Signal3 für alle Zeitpunkte berechnet wurde, bei denen sich entweder Signal1 oder Signal2 geändert hat. Anders ausgedrückt: Es wurde eine gemeinsame Zeitachse gebildet, die sowohl die Abtastzeitpunkte von Signal1 als auch die von Signal2 enthält. Diese Standardvorschrift zur Bildung der gemeinsamen Zeitachse kann auch durch den Ausdruck Signal1 or Signal2 beschrieben werden. Kreuzt man im Wizard zum Erstellen neuer arraybasierter NumPy-Traceschrittvorlagen die Option Zeitachsendefinition an, wird diese Vorschrift voreingetragen:

../../../../../_images/Beispiel_TimeAxis_Wizard.png

Wizard zum Anlegen arraybasierter NumPy-Traceschrittvorlagen mit expliziter Zeitachsendefinition

Allerdings können dort (oder nachträglich in der Methode GetTimeAxisDefinition() der generierten Vorlage AddSignals) auch andere Vorschriften festgelegt werden. Durch

def GetTimeAxisDefinition(cls):
    return 'Signal1 and Signal2'

wird eine gemeinsame Zeitachse definiert, die nur jene Zeitpunkte enthält, zu denen sowohl für Signal1 als auch für Signal2 ein Wert abgetastet wurde. Im Beispiel ergibt sich folgendes Bild:

../../../../../_images/Beispiel_TimeAxis_and.png

Schwellwertprüfung für Summensignal mit Zeitachsendefinition ‚Signal1 and Signal2‘

Die nachfolgende Zeitachsendefinition enthält schließlich nur die Abtastzeitpunkte von Signal2:

def GetTimeAxisDefinition(cls):
    return 'Signal2'

Der Plot im Testreport sieht dann wie folgt aus:

../../../../../_images/Beispiel_TimeAxis_SigC.png

Schwellwertprüfung für Summensignal mit Zeitachsendefinition ‚Signal2‘

Info

Innerhalb der Methode Process() einer arraybasierten NumPy-Traceschrittvorlage beziehen sich die Methoden triggerRange.GetValues(...) und triggerRange.GetTimestamps() eines Triggerbereiches triggerRange immer auf die gemeinsame Zeitachse, liefern also unter Umständen zusätzliche, nach dem Sample-and-Hold-Prinzip eingefügte Werte für ein Signal, dass ursprünglich weniger oft abgetastet wurde.

Die Methoden signal.GetValues(...) und signals.GetTimestamps(...) eines Signalobjekts signal liefern immer die ursprünglichen Abtastwerte. Über das optionale Argument fromTo können die Rückgabewerte auch wieder auf einen Zeitbereich eingegrenzt werden.

Details dazu sind in der Dokumentation zur Trace Analysis API zu finden.

Beispiel: Helferfunktionen

Eine arraybasierten NumPy-Traceschrittvorlage erbt immer von der Klasse NumPyBasedTraceStep. Neben der Definition des allgemeinen Interfaces bietet diese Klasse einige nützliche Funktionen, welche man in der eigenen Vorlage entweder über self. oder NumPyBasedTraceStep. aufrufen kann. Im Folgenden wird in einer Process-Methode beispielhaft mit diesen Helfer-Methoden gearbeitet.

def Process(self, parameters, report, timeAxis, ranges, signals):
    """
    ...

    """
    # Nicht interpolierte Zeitstempel und Werte von Signal1
    signal1 = signals['Signal1']
    actualTimestamps = signal1.GetTimestamps()
    actualValues = signal1.GetValues()

    ############################################################################################
    # FitToAxis
    ############################################################################################
    # Beispiel: Werte von Signal1 per Sample and Hold auf eine andere Zeitachse interpolieren
    #
    # Im vorliegenden Beispiel wird als Zielzeitachse timeAxis, also die gemeinsame Zeitachse
    # aller Eingangssignale des vorlagenbasierten Traceschrittes verwendet.
    interpValues = self.FitToAxis(actualTimestamps, actualValues, timeAxis)

    print(actualTimestamps)  # [0.0,      1.0,      2.0]
    print(actualValues)      # [1.0,      5.0,      9.0]
    print(timeAxis)          # [0.0, 0.5, 1.0, 1.5, 2.0, 2.5]
    print(interpValues)      # [1.0, 1.0, 5.0, 5.0, 9.0, 9.0]

    # Hinweis: Eine solche Interpolation wird für alle Triggerbereiche automatisch vorgenommen,
    # d.h. für einen Triggerbereich triggerRange liefert triggerRange.GetTimestamps() die
    # gemeinsame Zeitachse und triggerRange.GetValues('Signal1') dazu passende, ggf.
    # interpolierte, Signalwerte.


    ############################################################################################
    # CalculateMaskForRange
    ############################################################################################
    # Beispiel: Maske für alle Werte von Signal1 bestimmen, welche im rechtsoffenen Intervall
    # [1.0, 9.0) liegen
    mask = self.CalculateMaskForRange(actualValues, minVal=1.0, maxVal=9.0, rightOpen=True)
    resultValues = actualValues[mask]
    resultTimestamps = actualTimestamps[mask]

    print(actualTimestamps)     # [0.0,      1.0,      2.0]
    print(actualValues)         # [1.0,      5.0,      9.0]
    print(mask)                 # [True,     True,     False]

    print(resultTimestamps)     # [0.0,      1.0]
    print(resultValues)         # [1.0,      5.0]

    for triggerRange in ranges:
        timestamps = triggerRange.GetTimestamps()
        interpValuesInRange = triggerRange.GetValues('Signal1')

        ########################################################################################
        # CalculateRanges
        ########################################################################################
        # Beispiel: Prüfen, zu welchen Zeitpunkten Signal1 == 5.0 gilt, und im Report
        # Teilbereiche aufführen, für die die Erwartung erfüllt bzw. nicht erfüllt war
        #
        # Die Beispielausgaben gehen von einem Triggerbereich von 0.5 bis 2.0 aus (rechts offen,
        # daher wird 2.0 nicht mehr geprüft).
        print(timestamps)           # [0.5, 1.0, 1.5]
        print(interpValuesInRange)  # [1.0, 5.0, 5.0]

        reportElements = {
            True: ('Check OK', report.RESULT_ID_SUCCESS),
            False: ('Check FAILED', report.RESULT_ID_FAILED),
        }

        result = interpValuesInRange == 5.0
        resultSubRanges = self.CalculateRanges(result)

        print(result)           # [False, True, True]
        print(resultSubRanges)  # [(0, 1, False), (1, 3, True)]
        for subRange in resultSubRanges:
            triggerRange.Range(
                timestamps[subRange[0]], timestamps[subRange[1]-1],
                reportElements[subRange[2]][0], reportElements[subRange[2]][1])