Документация Engee
Notebook

Типы данных с фиксированной точкой

В данном примере рассмотрим возможности взаимодействия с числами с фиксированной точкой как в скриптах, так и в моделях.

Числа с фиксированной точкой – это числа, бинарное представление которых ограничено размерами их целой и дробной части. Данные числа не имеют возможности изменения положения точки в битовом представлении числа.

Для взаимодействия с такими числами в Julia используется библиотека FixedPointNumbers, а в Engee есть свой встроенный метод для реализации фиксированной точки, поэтому наша демонстрация будет нацелена на сравнение этих двух вариантов задания таких чисел.

FixedPointNumbers

В библиотеке FixedPointNumbers для задания чисел с фиксированной точкой имеются две основные команды Nifj и Qifj, где:

  1. i и j – это количество бит, выделяемое на целую и дробную часть;

  2. N – указывает, на то, что число беззнаковое;

  3. Q – указывает, на то, что один бит числа выделяется на знак.

Примечание: суммарное количество всех бит, выделяемых на число, должно быть равно 8, 16, 32 или 64 битам

In [ ]:
Pkg.add(["FixedPointNumbers", "CSV"])
   Resolving package versions...
  No Changes to `~/.project/Project.toml`
  No Changes to `~/.project/Manifest.toml`
In [ ]:
using FixedPointNumbers # Подключение библиотеки

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

In [ ]:
x = N0f8(0.8)
Out[0]:
0.8N0f8

Далее прибавим к этому числу целое число с выделенными 2 битами на целую часть. Как мы видим, результирующий тип данных имеет 8 бит на целую и на дробную части. Тем самым мы можем наблюдать автоматическое увелечения разрядности числа, которое реализовано для предотвращения переполнений.

In [ ]:
x + N2f6(2.8)
Out[0]:
3.592N8f8

Далее рассмотрим число со знаком, на целую часть которого выделено 2 бита, 5 бита на дробную часть и один бит соответственно на знак.

In [ ]:
x = Q2f5(-1.8)
Out[0]:
-1.81Q2f5

Увеличив данное число в три раза, мы получим переполнение по целой части числа, тем самым потеряем знак числа.

In [ ]:
3x
Out[0]:
2.56Q2f5

Дабы избежать переполнения, мы можем принудительно назначить тип. В данном варианте мы выполним сложение с числом с другим расположением точки и другой размерностью числа. При сложении двух чисел результирующий тип данных будет назначен, опираясь на число с большей разрядностью.

In [ ]:
Q4f11(2.8)-x
Out[0]:
4.6123Q4f11

Если мы хотим выполнить действия над числом без знака и числом со знаком, нам необходимо беззнаковое число привести к виду числа с выделенным битом на знак. Для этого используется команда float.

In [ ]:
x=Q2f5(float(N2f6(2.8)))
Out[0]:
2.78Q2f5
In [ ]:
Q4f11(2.8)+x
Out[0]:
5.5811Q4f11

Fixed point Engee

Теперь рассмотрим реализации, представленные в Engee. Для определения числа с фиксированной точкой используется функция fi(Value, Sign, Total_bits, Fractional_bits). Она более универсальна по сравнению с библиотекой Julia и позволяет задавать любые числа внутри одной команды, а также в ней отсутствует привязка к фиксировонному количиству бит на слово, что, соответственно, позволяет получить алгоритм с оптимальными затратами по памяти.

Теперь рассмотрим, что значат параметры этой функции.

  1. Value – значения исходного числа.
  2. Sign – знак (1-знаковый, 0-беззнаковый типы).
  3. Total_bits – полная разрядность слова.
  4. Fractional_bits – размер дробной части.
In [ ]:
Value = 128.9
Sign = 1;
Total_bits = 16;
Fractional_bits = 7;

println("fi: $(fi(Value, Sign, Total_bits, Fractional_bits))")
fi: 128.8984375

Повторим тесты, ранее описанные для библиотеки Julia, и убедимся в том, что fi() – более универсальная команда.

In [ ]:
# x = N0f8(0.8)
x = fi(0.8,0,8,8)
Out[0]:
fi(0.80078125, 0, 8, 8)
In [ ]:
# x + N2f6(2.8)
x + fi(2.8,0,8,6)
Out[0]:
fi(3.59765625, 0, 11, 8)
In [ ]:
# x = Q2f5(-1.8)
x = fi(-1.8,1,8,5)
Out[0]:
fi(-1.8125, 1, 8, 5)
In [ ]:
3x
Out[0]:
fi(-5.4375, 1, 72, 5)
In [ ]:
# Q4f11(2.8)-x
fi(2.8,1,16,11)-x
Out[0]:
fi(4.6123046875, 1, 17, 11)
In [ ]:
# x = Q2f5(float(N2f6(2.8)))
x = fi(fi(2.8,0,8,6),1,8,5)
Out[0]:
fi(2.8125, 1, 8, 5)
In [ ]:
# Q4f11(2.8) + x
fi(2.8,1,16,11) + x
Out[0]:
fi(5.6123046875, 1, 17, 11)

Как мы видим, представление, реализованное в Engee, действительно более универсальное, а также более усточивое к переполнениям, так как оно автоматически добавляет биты к слову в зависимости от типов данных, применяемых для того или иного числа.

Применение вычислений с фиксированной точкой в моделях

Теперь перейдём к применению чисел с фиксированной точкой в моделях. Для этого в Engee в настройках блоков предусмотрена возможность выбора выходного типа данных. На рисунке ниже показан интерфейс одного из таких блоков.

image_4.png

В данном примере реализована модель ПИД-регулятора с применением логики фиксированной точки. Пропорционально-интегрально-дифференцирующий регулятор — устройство в управляющем контуре с обратной связью. Оно используется в системах автоматического управления для формирования управляющего сигнала с целью получения необходимых точности и качества переходного процесса.

На рисунке ниже показана реализованная нами модель.

image_3.png

Перейдём к запуску этой модели.

In [ ]:
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
Out[0]:
run_model (generic function with 1 method)
In [ ]:
# Запуск модели
run_model( "simple_model_pid_fixed", @__DIR__ )
Building...
Progress 0%
Progress 78%
Progress 100%
Out[0]:
SimulationResult(
    "SubSystem.command" => WorkspaceArray{Fixed{1, 16, 13, Int16}}("simple_model_pid_fixed/SubSystem.command")

)

Далее для обработки данных моделирования, записанных в CSV, нам понадобится подключить две библиотеки – DataFrames и CSV. Для отображения этих данных задействуем библиотеку Plots.

In [ ]:
# Подключение библиотек
using CSV
using DataFrames
using Plots

Теперь выполним чтение данных из CSV, построим результирующий график и проведём анализ этих данных.

In [ ]:
command_fixed = Matrix(CSV.read("$(@__DIR__)/command_fixed.csv", DataFrame)); #загрузка данных
command_fixed = (command_fixed[:,2]);

Просмотрим структуру записанных данных. Как мы видим ниже, записанные данные представлены в стандартном формате и не требуют дополнительной обработки.

In [ ]:
dump(command_fixed[101])
Float64 3.0198974609375

Построим график записанных данных, а также проведём анализ на основе записанных в процессе моделирования данных с плавающей точкой и сравним их по точности.

In [ ]:
command = Matrix(CSV.read("$(@__DIR__)/command.csv", DataFrame)); #загрузка данных
# Построение графиков 
plot(command[:,2]) 
plot!(command_fixed)
Out[0]:

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

In [ ]:
sum(command[:,2]-command_fixed)
Out[0]:
0.0

Оказалось, что разница нулевая. Также видно, что в результате применяемая нами логика чисел с фиксированной точкой не повлияла на принцип работы ПИД-регулятора. Несмотря на потерю точности, система стремится к равновесию.

Вывод

В данном примере мы разобрали возможности применения логики с фиксированной точкой в скриптах и в моделях. Также выяснили, как их можно сохранить из модели и провести анализ или продолжить дальнейшую обработку в скриптах.