# 快速使用自定义的积分算法
ODE 初值问题的通用形式为:
其中,
接下来,我们将通过一个快速示例来演示如何运行您的第一个自定义积分算法。
# 拷贝文件
备份以下文件:
%Sysplorer安装目录%\Simulator\Src\mws_user_alg.c;打开示例目录:
%Sysplorer安装目录%\Docs\Samples\ExternAlgoDemo\;将示例目录下的
mws_user_alg.c文件拷贝到%Sysplorer安装目录%\Simulator\Src中替换同名文件(需要管理员权限)。
提示
本文档使用%Sysplorer安装目录%来指代您的 Sysplorer 实际安装路径,例如:C:\Program Files\MWORKS\Sysplorer 2024b\。
# 设置自定义算法
打开 Sysplorer 的用户界面,在建模标签页的模型浏览器中双击选择一个包含状态变量的 Modelica 标准库模型。选取的示例模型为
Modelica.Blocks.Examples.PID_Controller,如图所示:
如果约简后的模型状态变量数大于 0,则表明该模型包含状态变量。选中
Modelica.Blocks.Examples.PID_Controller模型后,在菜单栏中单击翻译,并在输出窗口的建模标签页中查看日志,显示该模型中包含 6 个状态变量。如图所示:
如果找不到输出窗口,可以在界面右上角单击窗口按钮,然后在下拉框中选择输出窗口,即可将其打开,其他窗口也可以按照此方法打开。如图所示:

切换到仿真标签页,单击仿真设置,会弹出仿真设置窗口;
在算法类型下拉框中选择自定义,算法名称下拉框中选择
Custom,最后单击确定。此时,仿真模型和自定义算法都已准备就绪,如图所示:
提示
若选择自定义算法时出现如图所示弹窗,请首先检查示例目录下的
mws_user_alg.c文件是否被正确拷贝,以及该文件内容是否被错误修改。
# 开始仿真
最后一步,单击仿真标签页上方的仿真按钮,Sysplorer 将调用自定义算法开始仿真。从输出窗口的仿真标签页可以看到,本次仿真使用的是MyEuler算法。至此,您的第一个自定义积分算法已经成功运行,如图所示:
# MyEuler 算法实现解析
上述快速示例的成功运行归功于您在拷贝文件章节中拷贝的mws_user_alg.c文件,正是它在幕后完成了所有必要的工作。事实上,mws_user_alg.c文件内实现了一个简单的显式欧拉算法,名为MyEuler,并将其注册到了 Sysplorer 的求解器中。接下来,我们将深入探讨MyEuler实现细节,为您后续扩展自定义积分算法打下坚实的基础。
mws_user_alg.c中与MyEuler算法相关的内容列表如下:
| 名称 | 类型 | 必须实现 | 含义 |
|---|---|---|---|
| UserEulerData | 结构体 | 是 | MyEuler 算法的积分器定义 |
| UserIVPSolverInstantiate | 函数 | 是 | 创建积分器对象 |
| UserIVPSolverGetFeature | 函数 | 是 | 获取积分算法的特性 |
| UserIVPSolverInitialize | 函数 | 是 | 初始化积分器 |
| UserIVPSolverDostep | 函数 | 是 | 向前积分一步 |
| UserIVPSolverInterpolate | 函数 | 是 | 获取状态变量的插值结果 |
| UserIVPSolverSetDebugLogging | 函数 | 否 | 设置调试日志输出选项 |
| UserIVPSolverFreeInstance | 函数 | 是 | 释放积分器对象 |
| UserIVPSolverGetWorkData | 函数 | 否 | 获取积分器的工作数据 |
| UserIVPSolverSetWorkData | 函数 | 否 | 设置积分器的工作数据 |
| UserIVPSolverSerializeWorkData | 函数 | 否 | 序列化积分器的工作数据 |
| UserIVPSolverDeSerializeWorkData | 函数 | 否 | 反序列化积分器的工作数据 |
| MwsRegisterExternalIVPSolver | 函数 | 是 | 注册 MyEuler 算法 |
提示
在mws_user_alg.c中实现的所有函数,其函数接口(原型)均遵循 Sysplorer 的内部规范。这些函数的参数和返回类型是固定的,但函数名可以由用户自定义。您只需利用提供的输入参数实现函数体,计算并赋值输出参数,最后返回指定类型的变量即可。
# 常用数据类型
Sysplorer 常用的内部数据类型实际都是 C 语言基本类型的别名,二者的对照表如下:
| Sysplorer 内部类型 | 对应的 C 语言基本类型 |
|---|---|
| MwsReal | double |
| MwsInteger | int |
| MwsBoolean | int |
| MwsChar | char |
| MwsString | const char* |
| MwsByte | char |
| MwsSize | unsiged int |
| MwsLongSize | size_t |
Sysplorer 常用的宏值如下:
| Sysplorer 内部宏 | 对应值 |
|---|---|
| mwsTrue | 1 |
| mwsFalse | 0 |
| MWSnullptr | 0 |
MwsBoolean类型变量的取值就是mwsTrue(真)或mwsFalse(假)。MWSnullptr用于空指针。完整的 Sysplorer 内部数据类型和宏的定义参见:%Sysplorer安装目录%\Simulator\Src\mws_common_decl.h。
# 积分算法对象的定义
typedef struct
{
MwsSize n;
MwsReal tlast;
MwsReal hlast;
MwsReal* ynew;
MwsReal* ylast;
MwsIVPCallbackFns callbackFns;
MwsUserContext userContext;
} UserEulerData;
UserEulerData 各个成员的含义如下:
| 名称 | 含义 |
|---|---|
| n | 状态变量的个数 |
| tlast | 上一个积分步到达的时间 |
| hlast | 上一个积分步的步长 |
| ynew | 当前积分步到达时间的状态变量值 |
| ylast | 上一积分步到达时间的状态变量值 |
| callbackFns | MyEuler 可直接使用的回调函数集,由求解器提供 |
| userContext | 用户数据环境,调用回调函数时需要此参数 |
UserEulerData 的成员可划分为两类:一类是用户自定义的,用于保存算法工作过程中需要的数据(如ynew等);另一类是由求解器传递过来的,用于辅助用户完成求解过程(如callbackFns等)。
实现自定义算法时,您可以在积分算法对象中自由地定义所需的变量。需要特别注意的是,callbackFns和userContext是所有积分算法对象的定义中必须包含的成员,分别用于保存求解器传递过来的回调函数集和用户数据环境。
# 创建积分算法的实例
static MwsIVPSolverType UserIVPSolverInstantiate(MwsSize n, const MwsIVPCallbackFns* cbfns, MwsBoolean loggingOn, MwsLocale locale, MwsUserContext userContext)
{
UserEulerData* eulerData = (*cbfns->allocateMemory)(userContext, 1, sizeof(UserEulerData));
eulerData->callbackFns = *cbfns;
eulerData->userContext = userContext;
eulerData->n = n;
eulerData->ynew = (*cbfns->allocateMemory)(userContext, n, sizeof(MwsReal));
eulerData->ylast = (*cbfns->allocateMemory)(userContext, n, sizeof(MwsReal));
return eulerData;
}
UserIVPSolverInstantiate函数用于创建一个UserEulerData类型的结构体实例eulerData,并为其成员分配内存空间和赋值。在该函数中,我们看到了回调函数allocateMemory的调用示例,该函数负责内存分配。allocateMemory是回调函数集cbfns中的一员,后续我们还会见到其他的回调函数。赋值语句
eulerData->callbackFns = *cbfns
将求解器传递的回调函数保存到算法实例eulerData中,以便在后续的计算中按需调用这些回调函数。
UserIVPSolverInstantiate函数的返回值类型为MwsIVPSolverType,这是专门用于指代积分算法实例的通用类型。因此,函数可以直接返回积分算法实例eulerData。
# 获取积分算法的特性
static MwsInteger UserIVPSolverGetFeature(MwsIVPSolverType inst, MwsIVPSolverFeature* solverFeature, void* reserve)
{
MwsInteger rst = 0;
solverFeature->fixedStepMethod = mwsTrue;
solverFeature->implicitMethod = mwsFalse;
solverFeature->canSolveImplicitEquation = mwsFalse;
solverFeature->canSolveDAE = mwsFalse;
solverFeature->canSetMaximalStepSize = mwsFalse;
solverFeature->canHitExpectedOutputTime = mwsFalse;
solverFeature->canResetWorkData = mwsFalse;
solverFeature->canSerializeWorkData = mwsFalse;
return rst;
}
UserIVPSolverGetFeature函数用于获取积分算法的特性。通常情况下,用户只需修改函数体中赋值运算符右侧的mwsTrue或mwsFalse。例如,对于MyEuler算法,它是一个显式定步长的算法,因此solverFeature->fixedStepMethod赋值为mwsTrue,而solverFeature->implicitMethod赋值为mwsFalse。
# 初始化积分算法实例
static MwsInteger UserIVPSolverInitialize(MwsIVPSolverType inst, MwsReal t0, const MwsReal y0[], const MwsReal yp0[],
MwsBoolean reinit, const MwsIVPExperiment* setting, void* reserve)
{
UserEulerData* eulerData = (UserEulerData*)(inst);
MwsInteger rst = 0;
eulerData->tlast = t0;
return rst;
}
UserIVPSolverInitialize函数用于为积分算法实例的成员变量赋初值。求解器传入的参数t0、y0[]和yp0[]分别对应积分初始时刻、初始时刻的状态变量值及状态变量导数值。用户可以按需将这些输入参数的值保存到积分算法实例inst中。
# 单步积分的实现
static MwsInteger UserIVPSolverDostep(MwsIVPSolverType inst, const MwsIVPSolverCommand* cmd, MwsReal tcur, MwsReal tout,
MwsReal* tret, MwsReal yret[], MwsReal ypret[], void* reserve)
{
UserEulerData* eulerData = (UserEulerData*)(inst);
MwsInteger rst = 0;
MwsSize i;
MwsReal h = tout - tcur;
eulerData->hlast = h;
eulerData->tlast = tcur;
memcpy(eulerData->ylast, yret, eulerData->n * sizeof(MwsReal));
for (i = 0; i < eulerData->n; i++)
{
yret[i] += h * ypret[i];
}
memcpy(eulerData->ynew, yret, eulerData->n * sizeof(MwsReal));
*tret = tout;
if (cmd->returnDerivative)
{
rst= eulerData->callbackFns.fncEvaluation(eulerData->userContext, tout, yret, MWSnullptr, ypret);
}
return rst;
}
UserIVPSolverDostep函数用于积分器向前积分一步,实现了MyEuler算法向前积分的核心逻辑:
函数各个参数的意义如下:
| 参数 | 含义 |
|---|---|
[in] inst | 积分算法对象(积分器) |
[in] cmd | 积分指令 |
[in] tcur | 当前时间 |
[in] tout | 预期输出时间 |
[out]tret | 向前积分一步实际到达(返回)时间(对于定步长方法 *tret=tout) |
[in|out] yret | 状态变量值(in:tcur 时刻的状态变量;out:*tret 时刻的状态变量) |
[in|out] ypret | 导数变量值(由 cmd 参数中的 returnDerivative 决定是否需要计算并返回 *tret 时刻的导数) |
[in] reserve | 保留参数 |
这里我们第一次见到了积分指令cmd,其成员returnDerivative指示积分器要不要计算返回时间的状态变量的导数。如果要计算的话,则调用函数值计算函数fncEvaluation来计算导数值。
# 状态变量插值
static MwsInteger UserIVPSolverInterpolate(MwsIVPSolverType inst, MwsReal tout, MwsReal yout[], MwsReal ypout[], void* reserve)
{
UserEulerData* eulerData = (UserEulerData*)(inst);
MwsInteger rst = 0;
MwsSize i;
for (i = 0; i < eulerData->n; i++)
{
yout[i] = (tout - eulerData->tlast) * (eulerData->ynew[i] - eulerData->ylast[i]) / eulerData->hlast + eulerData->ylast[i];
}
return rst;
}
UserIVPSolverInterpolate函数用于插值,当积分器实际积分到达的时间,越过输出时间tout时,系统就会自动调用此函数将状态变量及其导数插值到输出时间tout。此函数对于积分算法很重要,用户需要精心实现。
作为示例,上述代码中MyEuler算法实现的插值函数使用的是最简单的线性插值:
# 释放算法实例
static void UserIVPSolverFreeInstance(MwsIVPSolverType inst)
{
UserEulerData* eulerData = (UserEulerData*)(inst);
if (eulerData != MWSnullptr)
{
eulerData->callbackFns.freeMemory(eulerData->userContext, eulerData->ynew);
eulerData->callbackFns.freeMemory(eulerData->userContext, eulerData->ylast);
eulerData->callbackFns.freeMemory(eulerData->userContext, eulerData);
eulerData = MWSnullptr;
}
}
UserIVPSolverFreeInstance函数用于释放积分器的内存。这里出现了新的回调函数freeMemory,专门用来释放allocateMemory函数之前分配的内存空间。
# 其他函数
| 名称 | 含义 |
|---|---|
| UserIVPSolverSetDebugLogging | 设置调试日志输出选项 |
| UserIVPSolverGetWorkData | 获取积分器工作数据 |
| UserIVPSolverSetWorkData | 设置积分器工作数据 |
| UserIVPSolverSerializeWorkData | 积分器工作数据序列化 |
| UserIVPSolverDeSerializeWorkData | 积分器工作数据反序列化 |
上述函数是并非必须实现。在MyEuler算法的实现中,这些函数的函数体都为空,直接返回0。若有使用上述函数的需求,请参考用户手册中对应章节的详细接口说明。
# 注册积分算法
MwsStatus MwsRegisterExternalIVPSolver(void* inst)
{
/* Register user defined IVP solver by calling following function. */
/* MwsStatus MwsRegisterUserIVPSolver(void* inst, MwsString solverName, const MwsIVPSolverFcns* ivpSolverFcns) */
/* you must provide valid callback functions in ivpSolverFcns */
/* you can register several different IVP solvers */
MwsStatus rst = mwsOK;
MwsIVPSolverFcns ivpSolverFcns;
MwsString solverName = "MyEuler";
ivpSolverFcns.solverInstantiate = UserIVPSolverInstantiate;
ivpSolverFcns.solverInitialize = UserIVPSolverInitialize;
ivpSolverFcns.solverGetFeature = UserIVPSolverGetFeature;
ivpSolverFcns.solverDoStep = UserIVPSolverDostep;
ivpSolverFcns.solverInterpolate = UserIVPSolverInterpolate;
ivpSolverFcns.solverSetDebugLogging= UserIVPSolverSetDebugLogging;
ivpSolverFcns.solverFreeInstance = UserIVPSolverFreeInstance;
ivpSolverFcns.solverGetWorkData = UserIVPSolverGetWorkData;
ivpSolverFcns.solverSetWorkData = UserIVPSolverSetWorkData;
ivpSolverFcns.solverSerializeWorkData= UserIVPSolverSerializeWorkData;
ivpSolverFcns.solverDeSerializeWorkData= UserIVPSolverDeSerializeWorkData;
rst = MwsRegisterUserIVPSolver(inst, solverName, &ivpSolverFcns);
return rst;
}
MwsRegisterExternalIVPSolver函数用于将自定义的MyEuler算法注册到 Sysplorer 求解器中。只有在积分算法成功注册后,Sysplorer 才能正确识别并执行用户自定义的算法。注册积分算法主要包含四个步骤:
创建
MwsIVPSolverFcns类型的结构体ivpSolverFcns,该结构体用来存放积分算法实现的所有算法函数;为积分算法命名,将算法名
MyEuler赋给solverName;将
MyEuler算法实现的所有算法函数名,赋给ivpSolverFcns的相应成员;调用求解器提供的
MwsRegisterUserIVPSolver函数,将算法实例inst、算法名称solverName和结构体ivpSolverFcns作为实参传入,以注册积分算法。
提示
在注册自定义积分算法时,请特别注意自定义的积分算法名称不可与 Sysplorer 内置的积分算法重名,并确保您实现的算法函数已正确分配给ivpSolverFcns结构体的相应成员。
有关MwsRegisterExternalIVPSolver函数的使用说明,请参见用户手册的注册函数的模板。