Engee documentation
Notebook

Automating code verification

I do a lot of code generation and checking that it works the same way with the model. This whole process takes a lot of manual labor, and at some point I realized that I was tired of doing the same job with my hands. Therefore, I automated it, and in this project we will look at automating the testing of the generated code in the IN-Loop (SIL) mode.

What is it all about?

The method of IN-Loop verification (SIL testing) is that we compare the results of the work of the code on the host relative to the work of the model using the same test vectors. Schematically, the testing looks like this:

image.png

A code is generated from the algorithm model, and then the algorithm model is replaced by calls to the generated code. The code itself is compiled on the host. Thus, SIL testing allows you to check the behavior of the code relative to the model without switching to target computers (testing the code on the target computer is a separate testing stage called Processor-in-circuit, PIL).

Example

To show the automation of SIL testing, I took an example from this [publication] (https://engee.com/community/ru/catalogs/projects/integratsiia-c-koda-v-modeli-engee ). For ease of testing, I have integrated the Kalman filter model into a large model using the link model.:

image.png

The link model provides the following advantages:

  1. Isolation of the tested component in a separate model. When initializing the binding, the interfaces of the link model are initialized separately.
  2. Easy replacement of the tested component with a Si function unit for the SIL test via software control.

Test automation

To write automation, we will need to use software management. At the same time, I want to make my automation as reusable as possible. So I wrote my own module that automates most of the work. Moreover, the module was written taking into account that its functions will be used in tests that I will create in Test.jl.

Let's connect all the necessary packages and download the module!

In [ ]:
import Pkg; Pkg.add("Test")
using Test

include("SILAutomation.jl");
MIL_Harness = "ABfilter_Harness";
SIL_Harness = "ABfilter_Harness_SIL";
CUT = "alphabetafilter";
   Resolving package versions...
   Installed ArrayLayouts ───────────── v1.12.0
   Installed DifferentialEquations ──── v7.16.1
   Installed LazyArrays ─────────────── v2.8.0
   Installed EnzymeCore ─────────────── v0.8.15
   Installed LineSearches ───────────── v7.4.0
   Installed ExponentialUtilities ───── v1.27.0
   Installed Krylov ─────────────────── v0.10.2
   Installed DifferentiationInterface ─ v0.7.10
   Installed RData ──────────────────── v0.8.3
   Installed Optim ──────────────────── v1.13.2
   Installed RDatasets ──────────────── v0.7.7
   Installed JuliaInterpreter ───────── v0.10.6
   Installed BenchmarkTools ─────────── v1.6.2
   Installed BandedMatrices ─────────── v1.10.1
   Installed SparseConnectivityTracer ─ v1.1.2
   Installed TimeZones ──────────────── v1.22.1
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`

Let's look at the module code.:

In [ ]:
;cat SILAutomation.jl
module SILAutomotion

import Main.engee
export runSims,buildCUT,buildSILHarness,compare_signals

using DataFrames

function runSims(MIL_Harness::String,SIL_Harness::String)
    MIL_mdl = engee.load("$MIL_Harness.engee", force = true)
    SIL_mdl = engee.load("$SIL_Harness.engee", force = true)
    SILres = engee.run(SIL_mdl)
    MILres = engee.run(MIL_mdl)
    return (MILres,SILres)
end

function buildCUT(CUT::String)::Bool
    status = false;
    try
        abfilter = engee.load("$(CUT).engee")
        engee.set_param!(engee.gcm(),"CreateCFunction"=>true)
        engee.generate_code("$(CUT).engee", "$(CUT)_cg")
        include("$(CUT)_cg/$(CUT)_verification.jl")
        status = true;
    catch ex
        return status
    end
    return status
end

function buildSILHarness(SIL_Harness::String,CUT::String,MIL_Harness::String)
# Create harness (copy contents)
# Guard for old harnesses
    model_names = [m.name for m in engee.get_all_models()];
    if any(model_names .== SIL_Harness)
        engee.close(SIL_Harness,force=true)
    end
    if isfile("$SIL_Harness.engee")
        rm("$SIL_Harness.engee")
    end

    SIL_mdl = engee.create("$SIL_Harness")
    engee.load("$(MIL_Harness).engee")
    engee.copy_contents(MIL_Harness,SIL_Harness)
    engee.delete_block("$SIL_Harness/$CUT")
    try
        engee.copy_block("$(CUT)_verification/C Function","$SIL_Harness/C Function")
    catch error
        show(error)
    end

    engee.open(SIL_Harness)
    #connect SIL Function And prepare simulation
    engee.add_line("ProcessNoiseVariance/1","C Function/1")
    engee.add_line("MeasurementNoiseVariance/1","C Function/2")
    engee.add_line("Add-1/1","C Function/3")
    engee.add_line("C Function/1","Add/2")
    engee.add_line("C Function/1","SysOut/1")
    engee.set_log("C Function/1") 
    engee.set_param!(SIL_mdl,"FixedStep"=>engee.get_param(MIL_Harness,"FixedStep"))
    engee.arrange_system()
    engee.save("$SIL_Harness.engee")
end

function compare_signals(sig_one,sig_two)
    Ds = collect(sig_one);
    Rs = collect(sig_two);
    Cmp = isapprox.(Ds, Rs)
    issynched = all(Cmp.time)
    issimilar = all(Cmp.value) 
    return (issynched, issimilar)
end

end

Generating a verification Si function

To generate a Si verification function from the component under test, you must enable the appropriate setting in the model and generate the code. The buildCUT function in my module is responsible for this.:

function buildCUT(CUT::String)::Bool
    status = false;
    try
        abfilter = engee.load("$(CUT).engee")
        engee.set_param!(engee.gcm(),"CreateCFunction"=>true)
        engee.generate_code("$(CUT).engee", "$(CUT)_cg")
        include("$(CUT)_cg/$(CUT)_verification.jl")
        status = true;
    catch ex
        return status
    end
    return status
end

Для того чтобы эта функция была тестируема, будем возвращать статус как булеву переменную.

Автоматизация сборки SIL-обвязки

Я не хочу изменять обвязку для MIL-теста, но мне надо обеспечить эквивалентность тестовых векторов, поэтому я создам новую модель для SIL-теста. Посмотрим на код функции buildSILHarness:

function buildSILHarness(SIL_Harness::String,CUT::String,MIL_Harness::String)
# Create harness (copy contents)
# Guard for old harnesses
    model_names = [m.name for m in engee.get_all_models()];
    if any(model_names .== SIL_Harness)
        engee.close(SIL_Harness,force=true)
    end
    if isfile("$SIL_Harness.engee")
        rm("$SIL_Harness.engee")
    end

    SIL_mdl = engee.create("$SIL_Harness")
    engee.load("$(MIL_Harness).engee")
    engee.copy_contents(MIL_Harness,SIL_Harness)
    engee.delete_block("$SIL_Harness/$CUT")
    try
        engee.copy_block("$(CUT)_verification/C Function","$SIL_Harness/C Function")
    catch error
        show(error)
    end

    engee.open(SIL_Harness)
    #connect SIL Function And prepare simulation
    engee.add_line("ProcessNoiseVariance/1","C Function/1")
    engee.add_line("MeasurementNoiseVariance/1","C Function/2")
    engee.add_line("Add-1/1","C Function/3")
    engee.add_line("C Function/1","Add/2")
    engee.add_line("C Function/1","SysOut/1")
    engee.set_log("C Function/1") 
    engee.set_param!(SIL_mdl,"FixedStep"=>engee.get_param(MIL_Harness,"FixedStep"))
    engee.arrange_model()
    engee.save("$SIL_Harness.engee")
end

First, using engee.create() I create a new empty model, and then copy the contents of the original model to the new one using engee.copy_contents(). Next, I delete the link model and substitute the verification C function using engee.delete_block() and engee.copy_block(). Then I restore the signals and specify which signal to record. Then I organize the model and save it. The final model will look like this:

image.png

How to compare signals correctly?

We are all used to comparing two numbers using the operator ==. For example:

In [ ]:
1.0 == 1.0
Out[0]:
true

But there are two problems here.:

  1. It is better not to compare floating-point numbers directly, but to compare the modulus of their difference with a certain margin of error.
  2. If we talk about signals, then again we want to take into account that the signals may not be the same and we want to take into account a small error.

Additionally, it is necessary to check that the signals are synchronized.

Let's look at the code of the comparison function:

function compare_signals(sig_one,sig_two)
    Ds = collect(sig_one);
    Rs = collect(sig_two);
    Cmp = isapprox.(Ds, Rs)
    issynched = all(Cmp.time)
    issimilar = all(Cmp.value) 
    return (issynched, issimilar)
end

We are comparing two signals, which are represented as a DataFrame with two columns: time and value. The comparison will be done through the isapprox() function. We will return two Boolean variables.:

  • issynched - indicates that the signals are synchronized in time
  • issimilar - shows that the signals are the same with a certain accuracy

Creating a test suite and running it

Now that we have everything we need to automate testing, let's create test suites using Test.jl. Moreover, I will post tests for the assembly of the verification model and the equivalence of the code and the model. Let's run the tests and see their results.:

In [ ]:
@testset verbose = true "SIL" begin
    @testset "Code Generation" begin
        @test SILAutomotion.buildCUT(CUT)==true
        @test isfile(CUT*"_verification.engee")
    end
    
    SILAutomotion.buildCUT(CUT)
    SILAutomotion.buildSILHarness(SIL_Harness,CUT, MIL_Harness)
    
    @testset "SIL Equality" begin
        (MR,SR) = SILAutomotion.runSims(MIL_Harness,SIL_Harness)
        (sync,equal) = SILAutomotion.compare_signals(MR["filtered"],SR["C Function.1"])
        @test sync==true
        @test equal==true
    end
end 
Test Summary:     | Pass  Total   Time
SIL               |    4      4  40.3s
  Code Generation |    2      2   8.5s
  SIL Equality    |    2      2  22.3s
Out[0]:
Test.DefaultTestSet("SIL", Any[Test.DefaultTestSet("Code Generation", Any[], 2, false, false, true, 1.766386254735e9, 1.766386263245493e9, false, "In[4]"), Test.DefaultTestSet("SIL Equality", Any[], 2, false, false, true, 1.766386272794816e9, 1.76638629507223e9, false, "In[4]")], 0, false, true, true, 1.7663862547349e9, 1.766386295072248e9, false, "In[4]")

It can be seen that all the tests have passed, and my automation is working!

Conclusions and next steps

Using software management, I managed to automate the routine. We can further develop this automation by extending it, for example, to PIL testing. Written using Test.jl tests can also be extended for deeper testing. And finally, you can organize testing within a single binding using [Variant Source] blocks (https://engee.com/helpcenter/stable/ru/base-lib-signal-routing/variant-source.html )/Variant Sink. The latter requires a separate publication.