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

Переводим функции Julia в код на C

В этом примере мы продемонстрируем один прием, который позволяет генерировать код на Си из функции, созданной в Julia.

Описание задачи

Предположим, что у нас есть код на Julia, который мы хотим перенести на другую платформу, для которой нам неудобно компилировать код на платформе. Например, на микроконтроллер. При этом хорошо было бы сохранить возможность запускать тот же код в модельном контуре внутри Engee. Как поступить?

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

In [ ]:
Pkg.add( "Symbolics" )

Генерация кода из простой функции

Рассмотрим генерацию кода из очень простой функции, которая возвращает нам одно число:

In [ ]:
simple_fcn(x1, x2) = 2 .* x1 + sin( x2 )
simple_fcn(1, 2)
Out[0]:
2.909297426825682

Объявим, на глобальном уровне, две переменные x1 и x2, с которыми можно делать символьные операции.

In [ ]:
using Symbolics
@variables x_1 x_2
Out[0]:
$$ \begin{equation} \left[ \begin{array}{c} x_{1} \\ x_{2} \\ \end{array} \right] \end{equation} $$

Если подставить символьные переменные в качестве аргументов нашей простой функции, ничего необычного мы не увидим (разве что увидим уравнение, заложенное в функцию):

In [ ]:
simple_fcn( x_1, x_2 )
Out[0]:
$$ \begin{equation} 2 x_{1} + \sin\left( x_{2} \right) \end{equation} $$

Но зато мы получим хорошо структурированный объект, из которого можно генерировать код:

In [ ]:
c_model_code = build_function( simple_fcn( x1, x2 ), [x1, x2]; target=Symbolics.CTarget(), fname="SIMPLE_FCN", lhsname=:c, rhsnames=[:x1, :x2] )
println( c_model_code )
#include <math.h>
void SIMPLE_FCN(double* c, const double* x1) {
  c[0] = 2 * x1[0] + sin(x1[1]);
}

Мы получили код, оптимизированный для определенного "таргета" CTarget.

Этому коду могут потребоваться различные доработки. Например, перевод в тип float или смена названий функций (exp на expf) под требования стандарта, используемого в вашей платформе. Или добавление функции main.

Дальше этот код можно сохранить в файл simple_function.c, скомпилировать командой gcc -o out simple_function.c -lm.

In [ ]:
c_code_standalone = """
#include <stdio.h>

$c_model_code

int main(int argc, char const *argv[]){
    double out[1];
    double in[] = {1, 2};

    SIMPLE_FCN( out, in );

    printf( "%f\\n", out[0] );
    return 0;
}""";

# Сохраняем в файл доработанный код
open("$(@__DIR__)/simple_function.c", "w") do f
    println( f, "$c_code_standalone" )
end

Компилируем полученный код:

In [ ]:
;gcc -o out simple_function.c -lm

И проверим, что полученный двоичный файл выдает то же значение, что и исходная функция:

In [ ]:
;./out
2.909297

Результат совпадает с тем, что мы вычислили в начале примера.

Заключение

Мы сгенерироавли код для очень простой функции на Julia, скомпилировали и выполнили его в Engee, могли бы также поместить его в блок C Function для упрощения полунатурных испытания.

Стоит отметить, что не всякий код легко переводится в Си-код этим методом, часто процедуру перевода нужно будет существенно изменить, чтобы она позволяла коду принимать на вход матрицы. Но так можно сгенерировать достаточно большой подкласс алгоритмов, полезных в полунатурном сценарии моделирования.