Fixed point data types¶
In this example, let's look at the possibilities of interacting with fixed-point numbers in both scripts and models.
Fixed-point numbers are numbers whose binary representation is limited by the size of their integer and fractional parts. These numbers have no possibility to change the position of the point in the bit representation of the number. To interact with such numbers Engee uses the FixedPointNumbers library.
In this library for setting fixed point numbers there are two main commands Nifj and Qifj, where:
i and j are the number of bits allocated to the integer and fractional part;
N - indicates that the number is unsigned;
Q - indicates that one bit of the number is allocated to the sign. No integer part is positive.
*Note: The total of all bits allocated to the number must be equal to: 8, 16, 32 or 64 bits.
Pkg.add(["FixedPointNumbers", "CSV"])
using FixedPointNumbers # Подключение библиотеки
Next, let's look at a few examples of defining and applying such numbers. The first variant is the definition of an unsigned fractional number, the integer part of which is allocated 0 bits.
x = N0f8(0.8)
Then we add to this number an integer number with 2 bits allocated for the integer part. As we can see, the resulting data type has 8 bits for integer and fractional parts. Thus we can observe automatic increase of digit capacity of the number, which is implemented to prevent overflows.
x + N2f6(2.8)
Next, let's consider a number with a sign, which has 2 bits for the integer part, 5 bits for the fractional part and one bit for the sign.
x = Q2f5(-1.8)
Increasing this number three times, we will get an overflow on the integer part of the number, thus losing the sign of the number.
3x
To avoid overflow, we can force the type. In this variant we will perform addition with a number with a different point location and a different number dimension. When adding two numbers, the resulting data type will be assigned based on the number with the larger digit.
Q4f11(2.8)-x
If we want to perform operations on a number without a sign and a number with a sign, we need to convert the unsigned number to a number with a sign bit allocated. The float command is used for this purpose.
x=Q2f5(float(N2f6(2.8)))
Q4f11(2.8)+x
Now let's move on to using fixed-point numbers in models. For this purpose in Engee in the block settings there is a possibility to select the output data type. The figure below shows the interface of one of such blocks.
This example implements a PID controller model using fixed point logic. A proportional-integral-differential controller is a device in a feedback control loop. It is used in automatic control systems to form a control signal in order to obtain the required accuracy and quality of the transient process.
The figure below shows the model we have implemented.
Let's move on to running this model.
function run_model( name_model, path_to_folder )
Path = path_to_folder * "/" * name_model * ".engee"
if name_model in [m.name for m in engee.get_all_models()] # Проверка условия загрузки модели в ядро
model = engee.open( name_model ) # Открыть модель
model_output = engee.run( model, verbose=true ); # Запустить модель
else
model = engee.load( Path, force=true ) # Загрузить модель
model_output = engee.run( model, verbose=true ); # Запустить модель
engee.close( name_model, force=true ); # Закрыть модель
end
return model_output
end
# Запуск модели
run_model( "simple_model_pid_fixed", @__DIR__ )
Next, to process modelling data written to CSV, we need to connect two libraries: DataFrames and CSV. To display this data, we use the Plots library.
# Подключение библиотек
using CSV
using DataFrames
using Plots
Now let's read the data from CSV, plot the resulting graph and analyse the data.
command_fixed = Matrix(CSV.read("$(@__DIR__)/command_fixed.csv", DataFrame)); #загрузка данных
command_fixed = (command_fixed[:,2]);
Let's look at the structure of the recorded data. As we can see, below the recorded data is represented in the String15 format.
dump(command_fixed[101])
Let's extract the part describing numbers from this data, separating it from the data type. Using the command below, we can view the original data type itself.
t = string(command_fixed)[findfirst("Q",string(command_fixed))[1]:end];
t[1:5]
Next, based on the principles of type extraction from a string, we will implement a function that extracts numbers in Float32 format from a string.
function str_fi2num(a)
str = string(a)[1:findfirst("Q",string(a))[1]-1];
num = parse.(Float32,str);
return num
end
Let's apply this function and plot the recorded data, as well as perform an analysis based on the floating-point data recorded during the simulation and compare their accuracy.
command_num = str_fi2num.(command_fixed);
command = Matrix(CSV.read("$(@__DIR__)/command.csv", DataFrame)); #загрузка данных
# Построение графиков
plot(command[:,2])
plot!(command_num)
As we can see, the graphs are approximately the same. In order to accurately determine the error, let's find their difference.
plot(command[:,2]-command_num)
The difference appears in the fifth decimal place. It can also be seen that as a result, the fixed point number logic we used did not affect the principle of operation of the PID controller. Despite the loss in accuracy, the system tends to equilibrium.
Conclusion¶
In this example, we have broken down the possibilities of using fixed-point logic in both scripts and models. We also found out how they can be saved from the model and analysed or further processed in scripts.