# 综合案例介绍
# 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 个特性:
跨平台同时支持静态链接 Python 和动态链接 Python;
支持 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 的每个点处的值 |