# 外部函数的高级特性


本文主要介绍了外部函数的调用方式以及调用过程中对外部函数的跟踪和控制形式。

# 数组类型输入输出调用

模型中可以借助外部函数计算获取数组型数据,外部函数形式如下:

void ArrayTest(double  *input_data,double *output_data)
{
	output_data[0] = input_data[0] + 10;
	output_data[1] = input_data[1] + 10;
}

input_data 为传入参数,output_data 为输出参数,用来接收函数计算结果。对外部函数的包装和调用过程如下所示:

示例1

(需使用 32 位的编译器仿真)

  1. 将外部函数封装成 function。
function ArrayTest
  input Real input_data[2];
  output Real output_data[2];
external "C" ArrayTest(input_data, output_data)
annotation (IncludeDirectory = 
  "modelica://ExternFunc/Include", 
  Include = "#include\"array.c\"");
end ArrayTest;
  1. 直接在模型中调用 function。
model Arrayuse "调用外部函数,输入一个 real 数组参数获取 real 数组数据"
  Real x[2] = {1, 2};
  Real y[2];
equation 
  y = ExternFunc.functions.ArrayTest(x);
end Arrayuse;

# 条件调用

当外部函数的调用对时间或次数有要求时,应该使用条件调用,即在 when 语句和 if 语句中调用外部函数,其中,在 when 和 if 中调用的区别如下:

  • When 语句中调用外部函数

    示例2 :

    (需使用 32 位的编译器仿真)

    model getReal_inwhen "调用外部函数,输入一个 real 参数获取 real 数据,getreal_arg(x):y=2x"
      Real x = time;
      Real y;
    equation 
      when time > 0.5 then 
        y = ExternFunc.functions.IncTest1(x);
      elsewhen time <= 0.5 then 
        y = -1 * time;
      end when;
    end getReal_inwhen;
    
  • if 语句中调用外部函数

    示例3 :

    (需使用 32 位的编译器仿真)

    model getReal_inif "调用外部函数,输入一个 real 参数获取 real 数据,getreal_arg(x):y=2x"
      Real x = time;
      Real y;
    equation 
      y = if time > 0.5 then 
        ExternFunc.functions.IncTest1(x) else 
        -1 * time;
    end getReal_inif;
    

    从图中可以看出,在 when 条件语句中调用外部函数时,仅在 when 条件成立时被调用一次。而在 if 条件语句中调用外部函数,则在满足 if 条件的所有时间点均会被调用。因此在使用外部函数时,应注意两者之间的区别,当严格限制外部函数的执行次数和执行条件时,应在 when 条件语句中调用。

# 外部函数库中调用了其他函数库

libraryA.dll中的部分函数依赖libraryB.dll,当模型中使用了外部函数库libraryA.dll中的函数时,需将libraryBibraryA放在同一目录下。

# 跟踪外部函数调用点

由于 Modelica 陈述式建模的特点,平台自动把模型转换为可执行程序,用户往往难以观察函数调用过程。如果想要像 C 语言那样跟踪程序,可以在模型中手动添加打印代码,对可能出现的异常以打印求解器输出信息的方式直观检测,在模型中添加如下代码:

Modelica.Utilities.Streams.print("变量y: " + y);

以下面模型为例,我们想观察函数testfunc被调用的情况,可以在函数执行语句前面(或后面)添加打印信息,模型代码如下:

示例4:

model testlog
  function testfunc
  external "C" testfunc()
  annotation (Include = "void testfunc(){return;}");
  end testfunc;
  Real i(start = 0);
equation 
  i = time;
  testfunc();
  Modelica.Utilities.Streams.print("testfunc函数被调用:当前仿真时间为" + String(i));
end testlog;

该模型在输出栏中打印出如下信息:

上例中由于算法语句在同一个行为段,因此他们的执行时机完全一致。通过在 testfunc 接口后增加了一行打印代码后,就可以在求解器输出信息中观察函数的调用情况。建议仅在调试时使用算法语句,正常建模时尽量不使用。

结合前面介绍的观察打印信息的方法,观察到函数被调用几次,可以发现,函数调用的次数跟求解步数没有必然的联系。

# 控制外部函数调用顺序

当模型需要多次调用外部函数,且规定了外部函数的调用顺序时,例如:

model incorrect_example
  Real x = time;
  Real y(start = 13);
  Real z(start = 31);
equation 
  y = ExternFunc.functions.IncTest1(x);//语句 1
  z = ExternFunc.functions.IncTest1(x);//语句 2
end incorrect_example;

则语句 1 和语句 2 的执行顺序是不确定的,为严格限定函数执行顺序为“语句 1 →语句 2 ”,可以在语句 2 中调用语句 1 的返回值。

具体解决方法如下:对原函数进行修改,即修改 function IncTest1。

【修改前】

function IncTest1
  input Real a1;
  output Real c1;
external "C" c1 = getreal_arg(a1)
annotation (IncludeDirectory = 
  "modelica://ExternFunc/Include", 
  Include = "#include\"getreal_arg.c\"");
end IncTest1;

【修改后】

function IncTest2
  input Real a1;
  input Real a2 = 0;
  output Real c1;
external "C" c1 = getreal_arg(a1)
annotation (IncludeDirectory = 
  "modelica://ExternFunc/Include", 
  Include = "#include\"getreal_arg.c\"");
end IncTest2;

修改 function ,为其添加了一个输入参数 a2 ,并赋初始值 0 ,该初始值对函数功能不会产生影响,修改原模型如下:

model incorrect_example2
  Real x = time;
  Real y(start = 13);
  Real z(start = 31);
equation 
  y = ExternFunc.functions.IncTest2(x, x);//语句 1
  z = ExternFunc.functions.IncTest2(x, y);//语句 2
end incorrect_example2;

即在语句 2 中调用语句 1 的返回值作为函数的第二个参数传入,则模型求解过程中,将首先计算语句 1 方程,然后计算语句 2 方程。

# 控制外部函数调用频率

如果要控制函数调用频率(或调用时机)可以采用如下两种方法。

  • 方法一:条件调用

    Modelica 模型中的函数调用受模型特点以及求解算法影响,可能在一步求解过程中被多次调用,这往往不是用户期望的行为。可以通过条件调用来实现精确控制函数调用时机。下面具体举例说明。

    例如,某 dll 模块ExtLib.dll,其中有两个接口 Initial() 、StepRun() ,希望能够在求解前调用初始化接口 Initial() ,在求解过程中,每隔 2s 调用一次 StepRun ,那么 Modelica 代码可以如下编写:

    示例5:

    model testlog1
      function ExtLib_Initial
      external "C" Initial()
      annotation (Include = "void Initial (){return;}");
      end ExtLib_Initial;
      function ExtLib_StepRun
        input Real x;
      external "C" StepRun()
      annotation (Include = "void StepRun (){return;}");
      end ExtLib_StepRun;
      Integer i(start = 0);
      annotation (experiment(StartTime = 0, StopTime = 10));
    initial algorithm 
      ExtLib_Initial();// 初始算法段中初始化外部函数
      Modelica.Utilities.Streams.print("Initial has been called.");
    algorithm 
      when sample(0, 2) then // 通过 when 控制函数的调用时机
        ExtLib_StepRun(1);
        i := i + 1;// 用 i 做计数器
        Modelica.Utilities.Streams.print(String(i));// 打印i 以观察调用次数
      end when;
    end testlog1;
    

    如果是初始化函数,可以放置到 initial algorithm 算法段中,保证只在求解前被调用一次。用 sample(0, 2) 作为 when 条件,可以保证 when 中的语句每隔 2s 行为才被执行 1 次。在 Sysplorer 中把求解时间设置为 10s ,然后求解运行此模型,可以看到,初始化函数 Initial 仅执行了 1 次,SetpRun 从 0 时刻开始,每隔 2s 执行一次,共执行了 6 次。

    通过以上方法,可以精确控制函数的调用频率。除此之外,可以通过 if 、when 等条件组合,实现特定的控制逻辑。

  • 方法二:避免非线性迭代

    在求解器中非线性方程需要迭代求解,如外部函数出现在非线性方程块中,会导致在一次求解过程中外部函数被多次调用,因此需要避免外部函数出现在非线性方程块中。

    示例6:

    model testlog2
      function ExtLib_StepRun
        input Real x;
        output Real y;
        output Real z;
      external "C" y = StepRun(x, z)annotation (Include = "double i = 0; 
    double StepRun(double x, double *z){i++; *z = i;return 1;}");
      end ExtLib_StepRun;
      Real x;
      Real y;
    equation 
      when sample(0, 0.1) then 
        (x,y) = ExtLib_StepRun(x);
      end when;
    end testlog2;
    

    该模型仿真时间设定为 0~1s ,仿真步长设定为 0.002 ,即仿真 500 步,在此条件下 when 的条件将触发 10 次,即方程“(x,y) = ExtLib_StepRun(x) ”被求解十次。

    方程“(x,y) = ExtLib_StepRun(x) ”存在代数环(函数输出依赖输入,函数输入又依赖函数输出,出现循环依赖),从模型编译打印信息中可看到该非线性方程信息,如下图所示:

    模型变量 y 记录了外部函数调用次数,由上图可知,方程“(x,y) = ExtLib_StepRun(x) ”在求解十次的过程中外部函数被调用了 43 次(外部函数被调用次数与求解器以及求解器设置有关),其原因就是该函数出现在非线性方程块中。

    示例7:

    model testlog3
       function ExtLib_StepRun
         input Real x;
         output Real y;
         output Real z;
       external "C" y = StepRun(x, z)annotation (Include = "double i = 0; 
    double StepRun(double x, double *z){i++; *z = i;return 1;}");
         end ExtLib_StepRun;
         Real x;
         Real y;
       equation 
         when sample(0, 0.1) then 
           (x,y) = ExtLib_StepRun(pre(x));
         end when;
    end testlog3;
    

    testlog3testlog2的主要区别为方程“(x,y) = ExtLib_StepRun(pre(x)) ”,即通过添加 pre 打破代数环。该例子编译打印信息和结果如下图所示:

    由以上两图可知,通过打破代数环,消除了非线性方程,避免了迭代求解,保证了外部函数的调用次数在可控范围内。