# 函数


函数用来实现特定的计算任务,是 Modelica 实现过程式建模的重要工具,这一节介绍 Modelica 函数的定义和使用。

# 函数定义

函数是以“function”关键字定义的受限类,遵循 Modelica 类定义的语法形式。函数体是以“algorithm”开始的算法区域,或者是外部函数声明,作为函数调用时的执行系列。函数的输入形参以变量声明的形式定义,并有“input”前缀,输出形参也是以变量声明的形式定义,但前缀是“output”。函数定义的典型格式如下:

function functionname
  input TypeI1 in1;
  input TypeI2 in2;
  input TypeI3 in3 := default_expr1 "Comment" annotation(...);
  ...
  output TypeO1 out1;
  output TypeO2 out2 := default_expr2;
  ...
protected
  <local variables>
  ...
algorithm
  ...
  <statements>
  ...
end functionname;

函数中使用的临时变量(局部变量)在“protected”区域中定义,并且不带“input”和“output”前缀。函数的形参可以使用声明赋值的形式定义缺省参数,如上例中的“in3”和“out2”。

输入形参之间的相对顺序与调用函数时按位置传参的顺序是一致的,同样,输出形参之间的相对顺序与调用函数时返回值的顺序也是相同的。输入形参与输出形参之间的相对顺序没有要求。下例是合法的,但不推荐这样定义。

function <functionname>
  output TypeO1 out1; // 输入形参和输出形参相间定义
  input TypeI1 in1; // 不推荐,这样不易于阅读
  input TypeI2 in2;
  ...
  output TypeO2 out2;
  input TypeI3 in3;
  ...
end <functionname>;

函数作为一种受限类除了遵循 Modelica 类定义的通用语法外,还有一些限制和增强的特性,具体为:

  • public 区域的变量声明是函数的形参,必须有“input”或“output”前缀,protected 区域的变量声明是函数的临时变量,不能有“input”和“output”前缀;

  • 输入形参是只读的,也就是说在函数体中不能给输入形参赋值;

  • 函数不能用于连接,不能有“方程 equation”和“初始算法initial algorithm”,至多有一个“算法 algorithm”区域或外部函数接口;

  • 函数中输出形参数组和临时数组变量的长度必须能由输入形参或函数中的参数、常量确定;

  • 函数中不能调用 der、initial、terminal、sample、pre、edge、change、delay、cardinality、reinit 等内置操作符和函数,也不能使用 when 语句;

  • 函数中使用“return”语句退出函数调用,返回值取输出形参的当前值;

  • 函数是数学意义上的纯函数,一般情况下,相同的输入具有相同的输出,并且调用顺序与调用次数不改变所在模型的仿真状态;

函数和普通类一样支持继承,通过继承定义的函数同样要遵循函数的限制。通过继承的方式,可以在基类中实现通用的输入输出形参结构,在派生类中实现不同的算法。例如:

partial function trigonometric_fcn
  input Real x;
  output Real y;
end trigonometric_fcn;
function sin_fcn
  extends trigonometric_fcn;
algorithm
  y := sin(x);
end sin_fcn;
function cos_fcn
  extends trigonometric_fcn;
algorithm
  y := cos(x);
end cos_fcn;

# 函数调用

按输入参数的传递形式,函数调用有三种形式:

  • 按位置传参

  • 按形参名字传参

  • 按位置和形参名字混合传参

函数IsVectorEqual判断两个向量是否相等,有三个输入形参,分别是向量 v1、向量 v2、判等精度 eps,代码如下:

function IsVectorEqual
  input Real v1[:];
  input Real v2[:];
  input Real eps(min = 0) = 0;
  output Boolean result;
protected
  Integer i = 1;
algorithm
  result := false;
  if size(v1, 1) <> size(v2, 1) then
    return;
  end if;
  result := true;
  while i <= size(v1, 1) loop
    if abs(v1[i] - v2[i]) > eps then
      result := false;
      return;
    end if;
    i := i + 1;
  end while;
end IsVectorEqual;

按位置传参时,实参与形参的声明顺序一一对应。例如下面的函数调用:

b = IsVectorEqual({1.0,2.0,3.0}, {1.0,2.0,3.2}, 1e-6);

是按位置传参,实参 {1.0,2.0,3.0}、{1.0,2.0,3.2}、1e-6 分别对应形参 v1、v2、eps。

按形参名字传参时,实参与指定名字的形参对应。例如下面的按形参名传参的函数调用

b = IsVectorEqual(eps=1e-6, v1={1.0,2.0,3.0},
v2={1.0,2.0,3.2});

实参 {1.0,2.0,3.0}、{1.0,2.0,3.2}、1e-6 分别对应形参 v1、v2、eps。按形参名字传参时不用考虑形参的位置。

按位置传参和按形参名字传参可以混合使用,但按形参名字传递的实参必须放在按位置传参的实参之后,例如:

b = IsVectorEqual({1.0,2.0,3.0}, eps=1e-6, v2={1.0,2.0,3.2});

并不是所有的形参都要有对应的实参,如果形参有缺省值就可以不传递实参而使用缺省值。例如,下面的函数调用中实参 eps 取缺省值 0。

b = IsVectorEqual({1.0,2.0,3.0}, v2={1.0,2.0,3.2});

上面的例子中,函数只有一个输出形参,但很多情况下,函数需要返回多个结果,这时就可以定义有多个输出形参的函数。例如,下面的函数VectorNorm有三个输出形参 x、y、z。

function VectorNorm
  input Real v1[3];
  output Real x;
  output Real y;
  output Real z;
protected
  Real len;
algorithm
  len := 0;
  for i in 1:3 loop
    len := len + v1[i]^2;
  end for;
  len := sqrt(len);
  x := v1[1] / len;
  y := v1[2] / len;
  z := v1[3] / len;
end VectorNorm;

多个输出形参的函数调用只能位于等式的右端或赋值符号的右端,并且严格按照下面的形式使用:

(out1, out2, out3,) = f(...);
(out1, out2, out3,) := f(...);

其中 out1、out2、 out3 等均是变量,不能是表达式或常量。结果变量与函数输出形参按位置一一对应。结果变量可以省略,相应的输出形参值被丢弃。

VectorNorm根据所期望获取的结果值可以使用不同的调用方式:

(x1, y1, z1) = VectorNorm({1,2,3});
(x1, ,z1) = VectorNorm({1,2,3});  // y的值被丢弃
(x1, y1) = VectorNorm({1,2,3});  // z的值被丢弃
x1 = VectorNorm({1,2,3});  // y、z的值被丢弃

下面的使用方式是错误的:

(x1+2, 3, z1) = VectorNorm({1,2,3}); // 错误,左边列表中应该是变量

# 内置函数

Modelica 中除了支持用户自定义函数来调用外,还提供了丰富的内置函数,无需定义就可以直接调用。内置函数有四类:

  • 数学函数和转换函数

  • 求导和特殊用途函数

  • 事件相关的函数

  • 数组函数

这里只对一些常用的函数进行简要介绍,相关的函数详细说明,请参阅 Modelica 语言规范

数学函数和转换函数

函数 说明
abs(x) 结果是 x 的绝对值
sign(x) 若 x>0 结果为 1,x<0 结果为 -1,否则为 0
sqrt(x) 若 x≥0 时结果为 x 的平方根,否则出错
div(x, y) 结果是 x/y 的商且丢弃小数部分
mod(x, y) 结果是 x/y 整除的模,即 mod(x,y)=x-floor(x/y)*y
rem(x, y) 结果是 x/y 整除的余数,即有 div(x,y)*y + rem(x, y) =x
ceil(x) 结果是不小于 x 的最小整数
floor(x) 结果是不大于 x 的最大整数
sin(x) 正弦函数
cos(x) 余弦函数
tan(x) 正切函数
asin(x) 反正弦函数
acos(x) 反余弦函数
atan(x) 反正切函数
exp(x) 指数函数
log(x) 自然对数(e为底),x>0
log10(x) 10 为底的对数,x>0
integer(x) 结果是不大于 x 的最大整数
String(x) 结果是 x 的字符串表示

数学函数divmodremceilfloorinteger在模型的方程和算法中使用会触发事件(除非在 when 结构中使用,或使用了 noEvent),在 Modelica 2.2 版及以下版本中,abs、sign 也会触发事件。

求导和特殊用途函数

函数 说明
der(expr) 结果是表达式 expr 对时间(time)求导
delay(expr, delayTime,delayMax)delay(expr, delayTime) 若 time>time.start +delayTime 结果为 expr(time–delayTime) 若 time <= time.start +delayTime 结果为 expr(time.start)
semiLinear(x,positiveSlope,negativeSlope) 若 x>=0 结果为 positiveSlope * x,否则结果为 negativeSlope * x

函数der的参数要求是 Real 型连续表达式,delay的参数 delayTime 和 delayMax 要求是参数或常量。

事件相关函数

函数 说明
initial() 在初始化阶段结果为 true,否则为 false
noEvent(expr) 取表达式的字面值,表达式不触发事件
sample(start, interval) 在时刻 start+i*interval (i=0,1,...) 结果为 true 并触发时间事件,否则结果为 false
pre(y) 结果是变量 y(t) 在 t 时刻的左极限 y(tpre)
edge(b) 等价于“(b and not pre(b))”,b 为 Boolean 类型
change(v) 等价于“(v<>pre(v))”
reinit(x, expr) 仅在 when 结构中使用,在事件时刻以 expr 初始化状态变量 x

函数sample的参数要求是参数或常量。函数preedgechange的参数要求是离散变量。

数组函数

函数 说明
ndims(A) 结果是数组表达式 A 的维度 k,k≥0
size(A,i) 结果是数组表达式 A 第 i 维的长度,0<i≤ndims(A)
size(A) 结果是长度为 ndims(A) 的向量,向量记录数组表达式 A 的各维的长度
identity(n) 结果是 n×n 的整型单位方阵
diagonal(v) 结果是以向量 v 为对角元素,其它元素为 0的矩阵
zeros(n1, n2, n3, ...) 结果是 n1×n2×n3×...的整型数组,所有元素为 0
ones(n1, n2, n3, ...) 结果是 n1×n2×n3×...的整型数组,所有元素为 1
fill(s, n1, n2, n3, ...) 结果是 n1×n2×n3×...的数组,所有元素等于 s
min(A) 结果是数组所有元素中的最小值
max(A) 结果是数组所有元素中的最大值
sum(A) 结果是数组所有元素中的和
product(A) 结果是数组所有元素中的积

# 记录构造函数

记录构造函数(Record Constructor Function)针对受限类记录(record),是创建并返回记录的函数。记录构造函数并不需要用户显式定义,定义了一个记录类型就隐式定义了一个与记录同名并且作用域相同的记录构造函数;记录中所有可以修改的成员作为记录构造函数的输入形参,不能修改(如有 constant 和 final 前缀)的参数作为 protected 区域中的临时变量;输出形参是与记录相同类型的变量,所有输入形参的值用来设置输出形参的值。

记录 Complex 是表示复数的数据结构:

record Complex
  Real re "real part";
  Real im "imaginary part";
end Complex;

同时,下面的记录构造函数被隐式定义:

function Complex "record constructor"
  input Real re;
  input Real im;
  output Complex _out(re=re, im=im);
end Complex;

其中,输出形参 _out 通过输入形参 re 和 im 以变型的方式设置其值,不能与 Complex 中的所有元素重名。

记录构造函数与用户定义的函数调用方式相同。例如:

Complex c1 = Complex(10,20);
Complex c2 = Complex(im=20, re=10);

当然,上面的记录构造函数调用完全可以用变型的方式实现,这样使用是为了说明记录构造函数的使用。

# 函数向量化调用

在某些情况下,希望标量形参的函数使用数组作为实参来调用,结果是数组逐个元素分别作为实参来调用函数,例如:

cos({1,3,5});  // 结果为 {cos(1),cos(3),cos(5)}
sin([a,b; c,d]);  // 结果为 [sin(a),sin(b); sin(c),sin(d)]

这其实就是 Modelica 的函数向量化调用。返回标量值的函数可以应用向量化调用方式实现以数组作为实参调用函数,Modelica 自动以数组逐个元素作为参数来调用函数,返回结果数组。

输入形参是数组时,也可以使用向量化调用,例如:

function vec_sum
  input Real A[:];
  output Real res;
algorithm
  res := 0;
  for i in 1:size(A,1) loop
    res := res + A[i];
  end for;
end vec_sum;
Real a[4,3] = [1,2,3; 5,7,9; 3,6,9; 2,4,8];
Real w[4] = vec_sum(a); // [vec_sum([1,2,3]); vec_sum([5,7,9]); vec_sum([3,6,9]); vec_sum([2,4,8])]

如果有多个实参是数组(相对于形参),那么这些数组的长度必须相同。允许实参是数组和标量混合的情况,标量在数组逐个元素调用函数时保持不变。

Real x[4];
div(x[1:3], x[2:4]); // { div(x[1], x[2]), div(x[2], x[3]), div(x[3], x[4]) }
rem(x[1:4], x[2:3]); // 错误,数组的长度不相同
mod(x, 6); // { mod(x[1], 6), mod(x[2], 6), mod(x[3], 6), mod(x[4], 6) }

# 导函数

当 der 作用于函数时,就要对函数求导,对于内置函数(例如sin(x))能够推导出导函数,但是自定义的函数不能推导出导函数,例如:

model der_func_test
  function sin_cos
    input Real x;
    output Real y;
  algorithm
    y := 0;
    y := sin(x) * cos(x);
  end sin_cos;
  Real x = sin(time) * cos(time);
  Real y = der(x);
  Real x2 = sin_cos(time);
  Real y2 = der(x2);
end der_func_test;

其中,sin 和 cos 内置函数的导函数能自动推导出来,y 就可以计算出来,但 y2 无法进行计算,因为自定义函数 sin_cos 的导函数不能自动推导出来。自定义函数的导函数要求在函数定义时显式声明。

自定义函数的导函数通过导数(derivative)注解在函数中显式声明。

function sin_cos
  input Real x;
  output Real y;
  annotation (derivative = sin_cos_d);
algorithm
  y := sin(x) * cos(x);
end sin_cos;

注解中“derivative = sin_cos_d”声明函数 sin_cos 的导函数是 sin_cos_d。导数注解中通过属性 order、noDerivative 和 zeroDerivative 限定起作用的导函数,只有声明的所有属性条件都成立时,相应的导函数才是有效的。其中 order 表示求导的阶数,缺省情况取 1,annotation (derivative = sin_cos_d) 等价于 annotation (derivative(order=1) = sin_cos_d)。关于 noDerivative 和 zeroDerivative 的详细说明参见 Modelica 语言规范。

导函数的输入形参根据原函数构造,首先是原函数的所有输入形参,然后是原函数所有实型(Real)输入形参的导数(noDerivative 和 zeroDerivative 声明忽略的形参除外)。导函数的输出形参是原函数所有实型输出形参的导数。例如,sin_cos 的导函数定义是:

function sin_cos_d
  input Real x;
  input Real der_x;  // x的导数
  output Real der_y; // y的导数
algorithm
  der_y := der_x*cos(x) ^ 2 - der_x*sin(x) ^ 2;
end sin_cos_d;

使用修改后的 sin_cos 函数替换 der_func_test 中的函数定义,并加入 sin_cos_d 函数的定义,模型 der_func_test 就可以正常求解。

# 外部函数

模型中除了可以调用使用 Modelica 语言编写的函数外,还可以调用其它语言(目前支持 C 和 FORTRAN77)编写的函数,这些其它语言编写的函数称为外部函数。Modelica 中调用外部函数通过 Modelica 函数进行,这种 Modelica 函数没有算法(algorithm)区域,取而代之的是外部函数接口声明语句“external”,用以表示调用的是外部函数。

package myfuncs
  function sin
    input Real x;
    output Real y;
  external;
  end sin;
end myfuncs;

函数 myfuncs.sin 调用 C 语言函数 sin,调用形式为“y=sin(x)”。在 external 中可选地指定外部函数的语言、外部函数调用声明等。

function dgetrf
  "Compute LU factorization of square or rectangular matrix A (A = P*L*U)"
  input Real A[:,:] "Square or rectangular matrix";
  output Real LU[size(A, 1),size(A, 2)] = A;
  output Integer pivots[min(size(A, 1), size(A, 2))] "Pivot vector";
  output Integer info "Information";
  /*
  SUBROUTINE DGETRF( M, N, A, LDA, IPIV, INFO )
  参数:
  M       (输入) INTEGER
  N       (输入) INTEGER
  A       (输入/输出) DOUBLE PRECISION 数组, 维度 (LDA,N)
  LDA     (输入) INTEGER
  IPIV    (输出) INTEGER 数组, 维度 (min(M,N))
  INFO    (输出) INTEGER
  */
external
 "FORTRAN 77" dgetrf(size(A, 1), size(A, 2), LU, size(A, 1), pivots,
  info) annotation (arrayLayout = "columMajor",Library = "Lapack");
end dgetrf;

“FORTRAN 77”是外部函数语言声明,没有语言声明时取默认值“C”。“FORTRAN 77”后跟外部函数调用声明,说明 Modelica 调用实参如何传递给外部函数。注解用来指定数组布局(arrayLayout)是按行主存储(rowMajor)还是按列主存储(columMajor)、外部函数所需的头文件(Include)、外部函数所需的库文件(Library)、外部函数所需头文件所在的位置(IncludeDirectory)、外部函数所需库文件所在的位置(LibraryDirectory)。

package myfuncs
  function exp "Exponential, base e"
    input Real u;
    output Real y;
  external "C" y = exp(u) annotation(Include="#include \"math.h\"");
  end exp;
end myfuncs;

如果没有 IncludeDirectory 注解,外部函数所需头文件默认位置是包含外部函数的包(package)文件(“.mo”)所在文件夹中的“Resources/Include”子文件夹。同样,若没有 LibraryDirectory 注解,外部函数所需库文件默认位置是包含外部函数的包(package)文件(.mo)所在文件夹中的“Resources/Library”子文件夹,不同平台的目标文件还可以存放在相应的平台文件夹中。支持以下标准平台:

  • win 32 [32 位 Microsoft Windows]

  • win 64 [64 位 Microsoft Windows]

  • linux 32 [Intel 32 位 Linux]

  • linux 64 [Intel 64 位 Linux]

下例中外部函数有 Include、Library 注解但没有 IncludeDirectory、LibraryDirectory 注解,头文件和库文件存放在默认的位置。

package ExternalFunctions
  function ExternalFunc1
    input Real x;
    output Real y;
  external "C"
    y=ExternalFunc1_ext(x) annotation(Library="ExternalLib11",
                    Include="#include \"ExternalFunc1.h\"");
  end ExternalFunc1;
  function ExternalFunc2
    input Real x;
    output Real y;
  external "C" annotation(Include="#include \"ExternalFunc3.c\"");
  end ExternalFunc2;
  function ExternalFunc3
    input Real x;
    output Real y;
  external
    y=ExternalFunc3_ext(x) annotation(Library="ExternalLib11",
                      Include="#include \"ExternalFunc1.h\"");
  end ExternalFunc3;
end ExternalFunctions;

ExternalFunctions 包和其它相关文件存放的文件夹结构如下(加粗的是文件夹,缩进表示文件夹中的文件或子文件夹):

ExternalFunctions

package.mo // 包含上面的 Modelica 代码

Resources

Include // 包含头文件

ExternalFunc1.h // C-header file

ExternalFunc2.h // C-header file

ExternalFunc3.c // C-source file

Library // 包含不同平台的目标库文件

win 32

​ ExternalLib1.lib // VisualStudio 静态连接库

​ ExternalLib2.lib // DLL 的静态连接库

​ ExternalLib2.dll // DLL

linux 32

​ libExternalLib1.a // 静态连接库

​ libExternalLib2.so // 共享库

没有外部函数调用声明时,调用外部函数就按缺省的方式进行:

  • 如果只有一个输出形参,输出形参作为返回值,输入形参依次传递给外部函数(例如 myfuncs .sin);否则,输入输出形参依次传递给外部函数;

  • 对于返回值和标量参数,Modelica 参数与外部函数参数一一对应。

    Modelica 类型 C FORTRAN 77
    Real double DOUBLE PRECISION
    Integer int INTEGER
    Boolean int LOGICAL
    String const char* 不允许
    T[dim1, …, dimn] 不允许 不允许
    枚举类型 int INTEGER
    Modelica C 输入 C 输出 FORTRAN 77
    Real double double * DOUBLE PRECISION
    Integer int int * INTEGER
    Boolean int int * LOGICAL
    String const char * const char ** 不支持
    枚举类型 int int * INTEGER
  • 对于数组参数,Modelica 数组除了传递数组外,还要在其后依次传递数组各维的长度。

  • 类型中“T”表示 Modelica 标量类型,“T’”表示对应语言的输入标量类型。

    Modelica C
    T[dim1] T’ *, size_t dim1
    T[dim1,dim2] T’*, size_t dim1, size_t dim2
    T[dim1, …, dimn] T’*, size_t dim1, …, size_t dimn
    Modelica FORTRAN 77
    T[dim1] T’, INTEGER dim1
    T[dim1,dim2] T’, INTEGER dim1, INTEGER dim2
    T[dim1, …, dimn] T’, INTEGER dim1, …, INTEGER dimn

注意,Modelica 和 C 语言的数组布局默认是行主存储,FORTRAN 默认是列主存储。函数调用声明中的参数只允许变量、标量常量、返回标量的size函数。

需要特别注意的是,Modelica 不允许在函数中改变输入形参的值。如果外部函数的形参是输入输出类型(例如 FORTRAN77 函数DGETRF的形参 A),不能直接将输入形参传递给外部函数,而应该传递输出形参并在传递参数之前使输出形参等于输入形参(如函数dgetrf的输出形参 LU)。

# 外部对象

在某些情况下,多个外部函数协作完成某一任务,这些外部函数之间需要传递一些信息的内部内存。在 Modelica 中, ExternalObject 的实例就可以表示这种内部内存。Modelica 提供外部对象(external objects)构造、使用和销毁机制。

外部对象类(external object class)直接从 ExternalObject 派生,并且仅有两个函数constructordestructor,分别用于构造、销毁外部对象。constructor只有一个输出,返回构造的外部对象。destructor没有输出,并且只有一个外部对象作为输入参数,用于销毁该外部对象。这两个函数在 Modelica 中不能直接调用,constructor在外部对象初始化时自动调用,而destructor在外部对象最后一次使用之后自动调用。外部对象映射为 C 语言外部函数的“void*”。

model ExternalObject_Sample
  class MyTable  // 外部对象类
    extends ExternalObject;
    function constructor
      input String fileName = "";
      input String tableName = "";
      output MyTable table;
    external "C" table = initMyTable(fileName, tableName);
    end constructor;
    function destructor
      input MyTable table;
    external "C" closeMyTable(table);
    end destructor;
  end MyTable;
  function sumOfTable
    input MyTable table;  // 外部对象
    input Real u;
    output Real y;
  external "C" y = sumOfMyTable(table, u);
  end sumOfTable;
  MyTable table = MyTable(fileName = "testTables.txt",
    tableName = "table1");      // 初始化,调用initMyTable
  Real y;
equation
  y = sumOfTable(table, time);
end ExternalObject_Sample;

Modelica 中调用的外部 C 函数如下定义:

typedef struct { /* User-defined datastructure of the table */
  double* array; /* nrow*ncolumn vector */
  int nrow; /* number of rows */
  int ncol; /* number of columns */
  int type; /* interpolation type */
  int lastIndex; /* last row index for search */
} MyTable;
void* initMyTable(char* fileName, char* tableName) {
  MyTable* table = malloc(sizeof(MyTable));
  if ( table == NULL ) ModelicaError("Not enough memory");
  // read table from file and store all data in *table
  return (void*) table;
};
void closeMyTable(void* object) { /* Release table storage */
  MyTable* table = (MyTable*) object;
  if ( object == NULL ) return;
  free(table->array);
  free(table);
}
double sumOfMyTable(void* object, double u) {
  MyTable* table = (MyTable*) object;
  double y;
  // calculate sum of "table" data (compute y)
  return y;
};

上例 Modelica 代码中,外部对象 table 初始化时自动调用constructor函数,constructor函数调用外部函数initMyTable分配内部内存,sumOfTable函数使用外部对象作为参数调用外部函数sumOfMyTable,内部内存传递给sumOfMyTable使用,最后,仿真停止时自动调用destructor函数,destructor函数调用外部函数closeMyTable释放内部内存。

# 详细语法介绍

此页面只是对 Modelica 语言进行入门功能介绍,更详细的功能请参考 Modelica 语言规范的相关章节。

类型 对应 Modelica 规范章节
函数定义 function 声明
函数调用 函数调用
内置函数 内置函数
记录构造函数 record 构造函数
函数向量化调用 函数调用
导函数 函数导数和反函数
外部函数 外部函数接口
外部对象 外部函数接口