# 综合案例介绍

# Julia 调用平台层 API


本案例使用 FFT 分析周期性数据,通过 Julia 代码调用平台层 API 函数。该案例代码在科学计算环境中可直接运行,案例文件路径为:<Syslab安装路径>\Examples\04 数学\07 傅里叶分析与滤波\01 使用FFT分析周期性数据.jl

可以使用傅里叶变换来分析数据中的变化,例如一个时间段内的自然事件。下面的代码是天文学家使用苏黎世太阳黑子相对数对大约 300 年的太阳黑子的数量和大小进行统计,定对大约 1700 至 2000 年间的苏黎世数绘图。

include("01 sunspot.jl")
year = sunspot[:, 1]
relNums = sunspot[:, 2]
figure(1)
plot(year, relNums)
xlabel("Year")
ylabel("Zurich Number")
title("Sunspot Data")

为了更详细地看太阳黑子活动的周期特性,将对前 50 年的数据绘图。

figure(2)
plot(year[1:50], relNums[1:50], "b.-", markerfacecolor = "auto", markeredgecolor = "none")
xlabel("Year")
ylabel("Zurich Number")
title("Sunspot Data")

傅里叶变换是一种基础的信号处理工具,可确定数据中的频率分量。使用 fft 函数获取苏黎世数据的傅里叶变换。删除存储数据总和的输出的第一个元素。绘制该输出的其余部分,其中包含复傅里叶系数关于实轴的镜像图像。

y = fft(relNums)
y = y[2:end]
figure(3)
plot(real(y), imag(y), "ro")
ylabel("imag(y)")
title("Fourier Coefficients")

单独的傅里叶系数难以解释。计算系数更有意义的方法是计算其平方幅值,即计算幂。由于一半的系数在幅值中是重复的,因此您只需要对一半的系数计算幂。以频率函数的形式绘制功率谱图,以每年的周期数为测量单位。

n = length(y)
power = abs.(y[1:Int(floor(n / 2))]) .^ 2  # power of first half of transform data
maxfreq = 1 / 2                    # maximum frequency
freq = [1:n/2...] / (n / 2) * maxfreq     # equally spaced frequency grid
figure(4)
plot(freq, power)
xlabel("Cycles/Year")
ylabel("Power")

太阳黑子活动发生的最大频率低于每年一次。为了查看更易解释的周期活动,以周期函数形式绘制幂图,以每周期的年数为测量单位。该绘图揭示了太阳黑子活动约每 11 年出现一次高峰。

period = 1 ./ freq
figure(5)
plot(period, power)
xlim([0 50]); #zoom in on max power
xlabel("Years/Cycle")
ylabel("Power")

上述算法代码中使用了平台层 API 中以下函数:

序号 函数名 简介
1 fft 用快速傅里叶变换 (FFT) 算法计算 X 的离散傅里叶变换 (DFT)
2 figure 创建图窗窗口
3 plot 二维线图
4 xlabel 为 x 轴添加标签
5 ylabel 为 y 轴添加标签
6 title 添加图窗标题
7 length 返回集合元素数量
8 abs 绝对值
9 floor 将 X 的每个元素四舍五入到小于或等于该元素的最接近整数
10 xlim 设置或查询 x 坐标轴范围

# C++ 调用平台层 API

# 开发规范

Julia 提供了 C-API 可以用于将 Julia 代码集成到更大的 C/ C++ 项目中,而不需要用 C/ C++ 重写所有内容。因为几乎所有的编程语言都有调用 C 函数的方法,Julia C API 也可以用来建立进一步的语言桥梁(例如从 Python 或 C# 调用 Julia)。详细参考:https://docs.julialang.org/en/v1/manual/embedding/

# 示例代码

以下例子演示了如何开发一个简单的 C++ 工程 ArrayMaker,它调用了 Julia 函数库 TyMath 的 magic 函数。

# 创建 C++ 工程

使用 Visual Studio 创建 C++ 工程,其中包括 2 个项目 :ArrayMaker(算法项目)、ArrayMakerTest(测试项目),工程配置如下:

  • 开发环境:Microsoft Visual Studio

  • 环境变量:JULIA_DIR = (本平台Julia 程序安装路径,如 C:\Program Files\MWorks.Syslab 2022\Tools\julia-1.7.3)

  • $(JULIA_DIR)\bin添加到 PATH 路径

  • ArrayMarker 项目属性配置:

    • 常规 - 配置类型:动态链接库(.dll
    • C/C++ - 常规 - 附加包含目录:$(JULIA_DIR)\include\julia\
    • C/C++ - 预处理器 - 预处理器定义:添加宏ARRAYMAKER_LIB
    • 链接器 - 常规 - 附加库目录:$(JULIA_DIR)\lib
    • 链接器 - 输入 - 附加依赖项:libjulia.dll.a;libopenlibm.dll.a
  • ArrayMarkerTest 项目属性配置:

    • 常规 - 配置类型:应用程序(.exe

    • 链接器 - 常规 - 附加库目录:$(OutDir)

    • 链接器 - 输入 - 附加依赖项:ArrayMaker.lib

# 开发代码

开发好的 ArrayMaker 工程代码,其文件结构如下图所示:

  • ArrayMaker_global.h——动态库导出定义头文件;
#pragma once
#ifndef BUILD_STATIC
# if defined(ARRAYMAKER_LIB)
#  define ARRAYMAKER_EXPORT __declspec(dllexport)
# else
#  define ARRAYMAKER_EXPORT __declspec(dllimport)
# endif
#else
# define ARRAYMAKER_EXPORT
#endif
  • CallFunc.h——CallJulia 函数的头文件;
#ifndef ARRAYMAKER_H
#define ARRAYMAKER_H
#include "ArrayMaker_global.h"
extern "C" ARRAYMAKER_EXPORT void CallJulia();
#endif
  • CallFunc.cpp——CallJulia 函数的实现文件;
#include "CallFunc.h"
#include <uv.h>
#include <julia.h>
JULIA_DEFINE_FAST_TLS  // only define this once
void CallJulia()
{
	//初始化
	jl_init();

	//调用Julia数学库TyMath
	jl_eval_string("using TyMath");
	jl_function_t *func = jl_get_function((jl_module_t *)jl_eval_string("TyMath"), "magic");
	jl_value_t *argument1 = jl_box_int64(3);
	jl_array_t *ret = (jl_array_t*)jl_call1(func, argument1);
	int *yData = (int*)jl_array_data(ret);
	for (size_t i = 0; i < jl_array_len(ret); i++)
	{
		printf("%d ", yData[i]);
	}

	//退出
	jl_atexit_hook(0);
}
  • ArrayMakerTest.cpp——测试代码文件;
#include "../ArrayMaker/CallFunc.h"
int main()
{
    //调用同元Julia函数库TyMath求幻方矩阵magic
CallJulia();
}
  • start-env.bat——VS 工程启动脚本,因为该 VS 工程依赖同元 Julia 环境变量,所以需要通过 bat 脚本加载变量后启动 VS 工程。
set JULIA_DEPOT_PATH=C:/Users/Public/TongYuan/.julia
@echo off
REM julia
set KMP_DUPLICATE_LIB_OK=TRUE
REM PythonCall
set JULIA_CONDAPKG_BACKEND=Null
set PYTHON_JULIAPKG_OFFLINE=yes
set JULIA_PYTHONCALL_EXE=@PyCall
@echo on
start /B "" "./``ArrayMaker``.sln"

上述代码中使用了平台层 API 中以下函数:

函数名 简介
TyMath.magic 幻方矩阵

# Python 调用平台层 API

# 开发规范

# tyjuliacall 组件

tyjuliacall 组件是同元自研的用于实现 Python 调用 Julia 函数的 SDK。主要包含 2 个特性:

  1. 跨平台同时支持静态链接 Python 和动态链接 Python;

  2. 支持 Julia 系统镜像。

tyjuliacall 实现了将一些用于操控 Julia 自身的函数封装成 Python 对象,并将其挂载到 Python 可以访问的位置,此后 Python 使用这些对象,即可操控 Julia。其原理图如下所示:

虽然 tyjuliacall 允许在 Python 和 Julia 之间传递任意数据,但由于是两门不同的语言,数据转换的类型对应关系是复杂的。为了保证代码的后向兼容性,使得规范的代码在不同版本的 tyjuliacall 上都可以运行,建议只使用如下的数据类型转换。

# Python 数据传递到 Julia

Python 向 Julia 函数传参时,推荐只使用下表左边的数据类型,以保证代码的后向兼容。

Python 类型 Julia 类型
基本类型
int Int64
float Float64
bool Bool
complex ComplexF64
None nothing
str String
组合类型
numpy.ndarray (dtype为数字或字符串或bool) 原生Array
tuple,且元素均为表中数据类型 Tuple

# Julia 数据传递到 Python

当获取 Julia 函数返回值,或导入 Julia 模块的非函数对象时,将发生 Julia 到 Python 的数据传递。保证后向兼容的 Julia 到 Python 数据转换关系如下表所示:

Julia 类型 Python 类型
基本类型 /
Integer子类型 int
AbstractFloat子类型 float
Bool bool
Complex子类型 complex
nothing对象 None
AbstractString子类型 str
Vector{UInt8} bytearray
组合类型
AbstrctArray{T} (T见下方说明) numpy.ndarray
Tuple{T1, ..., Tn}, 且Ti为该表中的类型 tuple
其余 Julia 类型 tyjuliacall.JV

一个 Julia AbstrctArray 能转换为 numpy 数组,当且仅当其元素类型 T 是以下类型之一:

  • Int8, Int16, Int32, Int64, UInt8, UInt16, UInt32, UInt64

  • Float16, Float32, Float64

  • ComplexF16, ComplexF32, ComplexF64

  • Bool

提示

当类型为Vector{String}或者Array{String, 2}的 Julia 对象被返回给 Python 时,它被封装为一个tyjuliacall.JV类型。

# 示例代码

以下例子演示了开发一个简单的 Python 程序,它调用了 Julia 函数库 TySignalProcessing 和 TyPlot。其文件结构如下图所示:

  • __init__.py——Python 函数库入口文件;
name = "mypythonpkg"
version = __version__ = "0.0.1"
__all__ = ["tjc_common", "calljulia"]
  • tjc_common.py——使用 Julia 镜像的函数模块;
import os
from os import system
from os.path import expanduser
import platform
import time
import sys
def get_timespan():
    global _t0
    t1 = time.time()
    span = t1 - _t0
    _t0 = t1
    return span
_t0 = time.time()
# 使用系统映像文件,提升julia包加载速度
from tyjuliasetup import use_sysimage
need_use_sysimage = True
if need_use_sysimage:
    sysimage_path = ""
    if platform.system().lower() == 'windows':
        sysimage_path = "C:/Users/Public/TongYuan/.julia/environments/v1.7/JuliaSysimage.dll"
    elif platform.system().lower() == 'linux':
        sysimage_path = expanduser("~/TongYuan/.julia/environments/v1.7/JuliaSysimage.so")
    if os.path.exists(sysimage_path):
        use_sysimage(sysimage_path)
    else:
        print("%s 文件不存在!"%sysimage_path)        
import tyjuliacall
print(f"``导入tyjuliacall: {get_timespan():.2f} s")
  • calljulia.py——调用了 Julia 函数库 TySignalProcessing 和 TyPlot,绘制中值滤波信号曲线;
from tyjuliacall import TySignalProcessing as sp
from tyjuliacall import TyPlot as tp
import numpy as np
def Medfilt1_Plot():
    fs = 100
    t = np.arange(fs + 1) / fs
    print(t)
    x = np.sin(2 * np.pi * t*3) + 0.25*np.sin(2 * np.pi * t*40)
    # 调用信号库函数
    y = sp.medfilt1(x, 9)
    # 调用图形库函数
    tp.plot(t, x, t, y)
    tp.legend(np.asarray(["Original", "Filtered"]))
  tp.plt.show()
  • test.py——测试代码文件。
from tjc_common import *
from calljulia import *
# 调用Julia函数库:1.调用信号库进行中值滤波计算;2.调用图形库绘制结果曲线
Medfilt1_Plot()

上述代码中使用了平台层 API 中以下函数:

序号 函数名 简介
1 TySignalProcessing.medfilt1 一维中值滤波
2 TyPlot.plot 二维线图
3 TyPlot.legend 在坐标区上添加图例

# APP 调用平台层 API

本案例为使用 C++/Qt 图形应用开发环境开发的曲线拟合 APP。该 APP 分析了从 1790 到 1990 年间人口统计数据,通过多项式拟合算法预估未来的人口增长趋势。该案例文件路径为:<Syslab安装路径>\Examples\10 AppDemos\cpp\

下文阐述该 APP 的开发过程中对平台层 API 的调用方法。APP 通过更新按钮,获取 Syslab 工作区的变量列表,具体示例代码如下所示:

vector<VariableInfo> variables;  
if (m_syslabSdk)
  {
    m_syslabSdk->MwGetVariables(false, variables);
  }
  for (VariableInfo var : variables)
  {
    QString var_type = QString::fromStdString(var.GetType());
    if (var_type.contains("Vector{Int64}")
      || var_type.contains("Vector{Float64}"))
    {
      ui.comboBox_x->addItem(QString::fromStdString(var.GetName()));
      ui.comboBox_y->addItem(QString::fromStdString(var.GetName()));
    }
  }

X 数据Y 数据下拉框,选中具体的变量名后,实时从 Syslab 工作区获取变量值,具体示例代码如下所示:

    string value;
    m_syslabSdk->MwGetValue(var.toStdString(), value);
    QString qstr_value = QString::fromStdString(value);

曲线拟合计算算法过程,通过运行 Julia 脚本的方式,调用 Syslab 平台获取多项式拟合结果。第一步计算曲线拟合参数:

// p = vec(TyMathCore.polyfit(cdate, pop, 4))        
QString str = QString("using TyMathCore;curvefit_coefficient = vec(TyMathCore.polyfit(%1, %2, %3))").arg(x_datas_str).arg(y_datas_str).arg(n);             
m_syslabSdk->MwRunScript(str.toStdString().c_str());
string res;
m_syslabSdk->MwGetValue("curvefit_coefficient", res);

第二步计算曲线拟合结果:

// res = polyval(p, cdate)        
QString str = QString("using TyMathCore;curvefit_res = TyMathCore.polyval(curvefit_coefficient, %1)").arg(x_datas_str);
m_syslabSdk->MwRunScript(str.toStdString().c_str(), true, true);
String res;
m_syslabSdk->MwGetValue("curvefit_res", res);

上述算法代码中使用了平台层 API 中以下函数:

序号 函数名 简介
1 MwGetVariables 查询工作区变量名列表
2 MwGetValue 查询工作区变量 var 的值
3 MwRunScript 在工作区运行脚本,生成结果
4 TyMathCore.polyfit 返回次数为n的多项式 p(x) 的系数,该阶数是 y 中数据的最佳拟合
5 TyMathCore.polyval 计算多项式 p 在 x 的每个点处的值