数组函数

Modelica自带了大量和数组有关的函数。在本节,我们会浏览不同类型的函数,并介绍这些函数的使用方法。

数据建构函数

我们已经讨论过数组的建构。我们看到用以建立向量和矩阵的不同语法结构。此外,我们看到了如何从其他矩阵来创建新矩阵。Modelica的另外有几个函数可以用来构造向量、矩阵和更高维数组。这些函数可以替代或补充先前介绍的方法。

fill

函数fill用于创建拥有唯一元素值的数组。fill的参数有:

fill(v, d1, d2, ..., dN)

其中v是数组中每个元素的值,而剩下的参数则是每一维的尺寸。所得到的数组中的元素将具有和v相同的类型。因此,若要用1.7这个值填充一个5×7实数数组,我们可以使用以下语句:

parameter Real x[5,7] = fill(1.7, 5, 7);

这会让矩阵用以下方式被填充:

\[\begin{split}\left[ \begin{array}{ccccccc} 1.7 & 1.7 & 1.7 & 1.7 & 1.7 & 1.7 & 1.7 \\ 1.7 & 1.7 & 1.7 & 1.7 & 1.7 & 1.7 & 1.7 \\ 1.7 & 1.7 & 1.7 & 1.7 & 1.7 & 1.7 & 1.7 \\ 1.7 & 1.7 & 1.7 & 1.7 & 1.7 & 1.7 & 1.7 \\ 1.7 & 1.7 & 1.7 & 1.7 & 1.7 & 1.7 & 1.7 \end{array} \right]\end{split}\]

zeros

在使用数组时,一个常见的用例是创建一个只包含零元素的数组。实际上,这实现了和fill函数相同的功能。不过,由于需要填充的值为已知的,用户只需要指定大小。我们可以使用zeros以如下形式初始化数组:

parameter Real y[2,3,5] = zeros(2, 3, 5);

ones

ones的函数与zeros函数几乎相同。唯一的不同是,生成的数组中的每个元素取值为\(1\)。因此,例如:

parameter Real z[3,5] = ones(3, 5);

这会让矩阵用以下方式被填充:

\[\begin{split}\left[ \begin{array}{ccccc} 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \\ 1 & 1 & 1 & 1 & 1 \end{array} \right]\end{split}\]

identity

另一个常见的需求是方便地建立单位矩阵。单位矩阵的对角线元素都是\(1\),而所有其他元素均为\(0\)。This can be done very easily with the identity.通过identity函数,这可以很容易完成的。单位矩阵函数需要一个整数参数。这个参数确定在生成矩阵的行数和列数。So, invoking identity as:因此,调用identity如下:

identity(5);

会得到下列矩阵:

\[\begin{split}\left[ \begin{array}{ccccc} 1 & 0 & 0 & 0 & 0 \\ 0 & 1 & 0 & 0 & 0 \\ 0 & 0 & 1 & 0 & 0 \\ 0 & 0 & 0 & 1 & 0 \\ 0 & 0 & 0 & 0 & 1 \\ \end{array} \right]\end{split}\]

diagonal

diagonal函数用来创建一个所有的非对角元素是\(0\)的矩阵。对角函数的唯一参数是包含一个对角线元素值的数组。所以,要创建以下对角矩阵

\[\begin{split}\left[ \begin{array}{cccc} 2.0 & 0 & 0 & 0 \\ 0 & 3.0 & 0 & 0 \\ 0 & 0 & 4.0 & 0 \\ 0 & 0 & 0 & 5.0 \end{array} \right]\end{split}\]

用户可以使用下列Modelica代码:

diagonal({2.0, 3.0, 4.0, 5.0});

linspace

linspace函数可以创建一个其元素的取值在一定间隔内线性分布的向量。linspace函数的调用方法如下:

linspace(v0, v1, n);

其中v0是向量的首个元素,v1是向量的最后一个元素,而n向量的元素个数。因此,例如以如下方式调用linspace

linspace(1.0, 5.0, 9);

会得到向量:

{1.0, 1.5, 2.0, 3.5, 3.0, 3.5, 4.0, 4.5, 5.0}

转换函数

下面的函数提供了变换数组的方法。

scalar

scalar函数以如下方式被调用:

scalar(A)

其中A为有任意维数的数组,前提是每个维度的大小均为\(1\)scalar函数(仅)返回包含在数组中的标量值。例如,

scalar([5]) // Argument is a two-dimensional array (matrix)

以及

scalar({5}) // Argument is a one-dimensional array (vector)

均会得到标量值5

vector

vector函数以如下方式被调用:

vector(A)

其中A为有任意维数的数组,前提是仅有一个维度的大小大于\(1\)。在vector函数将数组内容返回为一个矢量(即仅有单个维度的数组)。因此,若我们传入列矩阵或行矩阵,如:

vector([1;2;3;4]) // Argument is a column matrix

vector([1,2,3,4]) // Argument is a row matrix

我们会得到:

{1,2,3,4}

matrix

matrix函数以如下方式被调用:

matrix(A)

其中A为一任意维数的数组,且只有两个大小大于\(1\)的维度。matrix函数将数组的内容返回为一个矩阵(即只有两个维度的数组)。

数学操作

在线性代数里,有许多不同类型的数学运算通常可以应用在向量和矩阵上。Modelica语言提供了函数来执行大多数的上述操作。以这种方式, Modelica的等式可以看起来非常像其线性代数里数学对应版本。

让我们从加法,减法,乘法,除法和求幂等运算开始。在大多数情况下,这些操作符应用在标量,矢量和矩阵的各种组合时,和其在数学上的定义是一样的。但是,为了介绍的完整性和以及提供读者以作参考,下表总结了这些操作的定义。

符号说明

以下所述的每个操作涉及两个参数\(a\)\(b\),以及一个结果\(c\)。如果参数表示一个标量就不会有下标。如果参数是一个向量,就会有一个下标。如果参数是一个矩阵,则会有两个下标。如果该操作定义在任意的数组上,则参数会包含三个下标。如果某个特定的组合没有出现,那么这种组合是无效的。

加法(+

表达式

结果

\(a + b\) \(c = a + b\)
\(a_{i} + b_{i}\) \(c_{i} = a_{i} + b_{i}\)
\(a_{ij} + b_{ij}\) \(c_{ij} = a_{ij} + b_{ij}\)
\(a_{ijk} + b_{ijk}\) \(c_{ijk} = a_{ijk} + b_{ijk}\)

Subtraction (-)

表达式

结果

\(a - b\) \(c = a - b\)
\(a_{i} - b_{i}\) \(c_{i} = a_{i} - b_{i}\)
\(a_{ij} - b_{ij}\) \(c_{ij} = a_{ij} - b_{ij}\)
\(a_{ijk} - b_{ijk}\) \(c_{ijk} = a_{ijk} - b_{ijk}\)

乘法(*.*

乘法运算符分为两种类型。第一种是正常的乘法运算符*。此运算符遵从线性代数里的一般数学定义,如矩阵向量积等等。*操作符的行为如下表所示:

表达式

结果

\(a * b\) \(c = a * b\)
\(a * b_i\) \(c_i = a * b_i\)
\(a * b_{ij}\) \(c_{ij} = a * b_{ij}\)
\(a * b_{ijk}\) \(c_{ijk} = a * b_{ijk}\)
\(a_i * b\) \(c_i = a_i * b\)
\(a_{ij} * b\) \(c_{ij} = a_{ij} * b\)
\(a_{ijk} * b\) \(c_{ijk} = a_{ijk} * b\)
\(a_{i} * b_{i}\) \(c = \sum_i a_{i} * b_{i}\)
\(a_{i} * b_{ij}\) \(c_j = \sum_i a_{i} * b_{ij}\)
\(a_{ij} * b_{j}\) \(c_i = \sum_j a_{ij} * b_{j}\)
\(a_{ik} * b_{kj}\) \(c_{ij} = \sum_k a_{ik} * b_{kj}\)

第二类乘法运算符是一种特殊的逐元素运算版本.*。此版本不进行任何求和运算而简单地将运算逐元素施加在的所有数组元素上。

表达式

结果

\(a\) .* \(b\) \(c = a * b\)
\(a_{i}\) .* \(b_{i}\) \(c_{i} = a_{i} * b_{i}\)
\(a_{ij}\) .* \(b_{ij}\) \(c_{ij} = a_{ij} * b_{ij}\)
\(a_{ijk}\) .* \(b_{ijk}\) \(c_{ijk} = a_{ijk} * b_{ijk}\)

除法(/./

正如乘法(*和.*),除法运算符也分为两种。第一种是通常的除法运算符/,用于求数组除以一个标量值的商。下表总结了其行为:

表达式

结果

\(a / b\) \(c = a / b\)
\(a_i / b\) \(c_i = a_i / b\)
\(a_{ij} / b\) \(c_{ij} = a_{ij} / b\)
\(a_{ijk} / b\) \(c_{ijk} = a_{ijk} / b\)

此外,除法运算符也有一个逐元素的版本./。其行为如下表所示:

表达式

结果

\(a\) ./ \(b\) \(c = a / b\)
\(a_{i}\) ./ \(b_{i}\) \(c_{i} = a_{i} / b_{i}\)
\(a_{ij}\) ./ \(b_{ij}\) \(c_{ij} = a_{ij} / b_{ij}\)
\(a_{ijk}\) ./ \(b_{ijk}\) \(c_{ijk} = a_{ijk} / b_{ijk}\)

幂指数(^.^

乘法(*和.*)以及除法(/及./)一样,指数操作符也有两种形式。第一种是标准的指数操作符^。此标准版有两种不同的使用方式。第一种使用方法是两个标量的乘方运算(即\(a\) ^ \(b\))。另外一种则是求方阵的标量次乘方(即\(a_{ij}\) ^ \(b\))。

另外一种求幂的形式是由.^操作符代表的逐元素形式。其行为如下表所示:

表达式

结果

\(a\) .^ \(b\) \(c = a^b\)
\(a_{i}\) .^ \(b_{i}\) \(c_{i} = a_{i}^{b_{i}}\)
\(a_{ij}\) .^ \(b_{ij}\) \(c_{ij} = a_{ij}^{b_{ij}}\)

\(a_{ijk}\).^\(b_{ijk}\)

\(c_{ijk} = a_{ijk}^{b_{ijk}}\)

相等(=

相等操作符=用于构造标量和数组方程,前提是只要两边的维数相等且每个维度的大小也一样。假设满足这一要求,那么操作符会逐元素地作用在数组上。这意味着操作符是应用在左边和右边的每个对应元素上。

赋值(:=

:=(赋值)操作符与相等(=)操作符一样,均是逐元素地作用在数组上的。

关系操作符

所有关系操作符(andornot>>=<=<)与相等(=)操作符一样,均是逐元素地作用在数组上的。

transpose

transpose函数接受一个矩阵作为参数,并返回该矩阵的转置版本。

outerProduct

outerProduct函数有两个参数。每个参数均必须是矢量,且须具有相同的尺寸。函数将返回其表示两向量外积的矩阵。从数学上来说,假设\(a\)\(b\)是相同大小的矢量。调用outerProduct(a,b)会返回有如下元素的矩阵\(c\)

\[c_{ij} = a_i * b_j\]

symmetric

symmetric函数接受一个方阵作为参数。函数返回一个相同大小的矩阵,其对角线下的元素被替换为原矩阵对角线上元素组成矩阵的转置。换句话说,

\[\begin{split}b_{ij} = \mathtt{symmetric(a)} = \left\{ \begin{array}{c} a_{ij}\ \ \mathrm{if}\ i<=j \\ a_{ji}\ \ \mathrm{otherwise} \end{array} \right.\end{split}\]

skew

skew函数用具有三个分量的矢量作为输入,并返回以下斜对称矩阵:

\[\begin{split}\mathtt{skew(x)} &= \left[ \begin{array}{ccc} 0 & -x_3 & x_2 \\ x_3 & 0 & -x_1 \\ -x_2 & x_1 & 0 \end{array} \right]\end{split}\]

cross

cross函数用两个矢量(各有3个分量)作为输入,并返回以下矢量(有3个分量):

\[\begin{split}\mathtt{cross(x,y)} = \left\{ \begin{array}{c} x_2 y_3 - x_3 y_2 \\ x_3 y_1 - x_1 y_3 \\ x_1 y_2 - x_2 y_1 \end{array} \right\}\end{split}\]

缩减运算符

缩减运算符把数组缩减为标量值。

min

min函数接受一个数组为参数,并返回数组中的最小值。例如:

min({10, 7, 2, 11})  // 2
min([1, 2; 3, -4])   // -4

max

max函数接受一个数组为参数,并返回数组中的最大值。例如:

max({10, 7, 2, 11})  // 11
max([1, 2; 3, -4])   // 3

sum

sum函数接受一个数组为参数,并返回该数组中所有元素的总和。例如:

sum({10, 7, 2, 11})  // 30
sum([1, 2; 3, -4])   // 2

product

product函数接受一个数组作为参数,并返回数组中所有元素的乘积。例如:

product({10, 7, 2, 11})  // 1540
product([1, 2; 3, -4])   // -24

其它函数

ndims

ndims函数接受一个数组作为参数,并返回该数组的维数。例如:

ndims({10, 7, 2, 11})  // 1
ndims([1, 2; 3, -4])   // 2

size

size函数可以通过两种不同方式调用。第一种方法是,使用一个数组作为唯一参数。在这种情况下,size返回一个矢量。这个矢量中各分量分别对应于数组中相应维度的大小。例如:

size({10, 7, 2, 11})       // {4}
size([1, 2, 3; 3, -4, 5])  // {2, 3}

另外,也可以通过附加可选参数的方法调用size,从而指定所需的特定维度标号。这种情况下,函数会将该特定维度的大小作为一个标量整数返回。例如:

size({10, 7, 2, 11}, 1)       // 4
size([1, 2, 3; 3, -4, 5], 1)  // 2
size([1, 2, 3; 3, -4, 5], 2)  // 3

向量化

本节中,我们已经讨论了Modelica语言里许多为了数组参数而设计的函数。不过,一个很常见的用法是将一个函数逐元素应用到向量的每个元素上。Modelica的“向量化(vectorization)”语言特性支持这种用例。如果一个函数设计为输入一个标量,但传值却为一个数组,那么Modelica语言编译器会自动将函数应用在向量的每个元素上。

为了了解这个功能,首先考虑abs函数的正常求值:

abs(-1.5)   // 1.5

显然,abs通常是接受标量参数,然后返回一个标量。但在Modelica里,我们也可以这样做:

abs({0, -1, 1, -2, 2})  // {0, 1, 1, 2, 2}

由于该函数是为变量设计的,Modelica语言编译器会将其从:

abs({0, -1, 1, -2, 2})

变换为

{abs(0), abs(-1), abs(1), abs(-2), abs(2)}

换句话说,Modelica把函数应用于由标量组成的向量这种情况,变换为输入标量后函数的取值所组成的向量。

此特性也适用多个参数的函数前提是仅仅1个预期为标量的参数为向量。要理解这个稍微复杂的功能,可以考虑取模函数mod。如果将函数应用于标量输入,我们会得到以下行为:

mod(5, 2)  // 1

倘若将首个参数改为向量,我们会得到:

mod({5, 6, 7, 8}, 2)  // {1, 0, 1, 0}

换言之,这个特性会将

mod({5, 6, 7, 8}, 2)

变换为

{mod(5,2), mod(6,2), mod(7,2), mod(8,2)}

不过,向量化特性不能用在有多于一个标量参数被改为矢量的情况。例如,下面的表达式将是错的:

mod({5, 6, 7, 8}, {2, 3}) // Illegal

因为mod需要两个标量参数,但得到的却是两个矢量参数。