# 模型重用
“重用”是面向对象语言的最大特点之一。Modelica 作为面向对象建模语言,提供继承(Extends)、变型(Modification)和重声明(Redeclaration)等机制,能够方便地支持模型重用。继承是对已有类型的重用,结合变型与重声明,实现对基类的定制与扩展。本节介绍如何利用这些机制来建立可重用的模型。
# 抽象与继承
# 从示例讲起
定义抽象类型并继承使用,是实现模型重用的一种重要手段。例如许多电子元件都具有一个共性,即都具有两个端口。根据这一共性,我们可以定义一个抽象的元器件类型 OnePort,具有两个端口 p 与 n,还具有一个物理量 v 用于表示这个组件两端的电势差。其定义如下:
partial model OnePort
Voltage v;
Current i;
PositivePin p;
NegativePin n;
equation
v = p.v - n.v;
0 = p.i + n.i;
i = p.i;
end OnePort;
注意到 OnePort 类具有 partial 属性,这表示类型是“抽象”的。作为抽象类,它仅描述了某类元器件的共性部分。抽象类不能直接定义一个组件,而是通过继承被使用。假设我们需要一个电阻类,就可以通过继承 OnePort 来实现:
model Resistor
extends OnePort;
parameter Resistance R(start=1);
equation
R*i = v;
end Resistor;
在实例化 Resistor 时,extends 语句使得 OnePort 模型中所有数据及算法均被自动拷贝到派生类 Resistor 中,就如同在 Resistor 中声明了这些数据一样。如果我们又需要一个电容类型,同样只需继承 OnePort 并补充电容的特性方程:
model Capacitor
extends OnePort;
parameter Capacitance C(start=1);
equation
i = C*der(v);
end Capacitor;
如果不使用继承,那么 Resistor 与 Capacitor 这两个模型就需要如下定义:
model Resistor
Voltage v;
Current i;
PositivePin p;
NegativePin n;
parameter Resistance R(start=1);
equation
v = p.v - n.v;
0 = p.i + n.i;
i = p.i;
R*i = v;
end Resistor;
model Capacitor
Voltage v;
Current i;
PositivePin p;
NegativePin n;
parameter Capacitance C(start=1);
equation
v = p.v - n.v;
0 = p.i + n.i;
i = p.i;
i = C*der(v);
end Capacitor;
可以看出,采用继承机制建立的模型更加简洁。通过对基类数据及算法的重用,避免了不必要的代码重复。如果要修改共有的特性,只需修改基类 OnePort 即可。Modelica 标准库中就大量使用了继承机制。如果需要对一系列物理组件进行建模,而这些组件之间又具备诸多共同特性时,就可以应用抽象与继承建立可重用的模型。
# 继承方式
一般继承的语法形式
一般继承的语法如下:
extends base_class_name [ class_modification ];方括号内的部分表示可选。可以在继承一个类的同时对基类进行变型,从而重新定义基类中的已有数据(关于参数变型,请参见重声明)。
派生类中的变量不允许与基类中的变量出现重名,除非它们的定义完全恒等,这时两个同名变量被当作一个变量。Modelica 允许多继承,即一个派生类同时具有多个基类。多继承时各个基类中的变量名同样不可以重名,除非它们恒等。
例如:
class A Real x = 1; Integer y; Boolean z = false; end A; class C extends A; Real x = 2; // 错误,跟基类A中x的定义不一致 Real y; // 错误,跟基类A中y的定义不一致 Boolean z = false; // 正确,跟基类A中z的定义一致 end C;另一种继承——简短类
简短类的语法如下:
class_keyword class_name = base_class_name [ array_dimension_descriptor ] [ class_modification ];其中,class_keyword 可以是关键字
class,也可以是受限类中的任意一种,例如connector、block等。以下两种类型定义是等价的:type Current = Real(unit = “A”); class Current extends Real(unit = “A”); // no more elements definition end Current;二者唯一的区别是简短类无需引入新的名字空间即可继承一个类型。
# 参数变型
继承是 Modelica 支持模型重用的关键,但仅有继承是不够的。例如派生类需要改变继承来的某个属性的值,或改变某个组件的类型,这时还需要结合变型机制来达到目的。
变型使得在声明一个对象的同时修改类的某些属性,例如为变量赋值、改变数组维度等,甚至重声明类型中的嵌套组件或嵌套类。本节结合实例介绍变型机制如何使得模型重用更加灵活(关于重声明形式的变型详细介绍,请参见重声明)。
# 变型的概念
在变量声明的同时给变量一个初始值,实质就是 Modelica 变型的一种:
parameter Real x = 0;
如果要对一个类型中的多个属性进行赋值,就由一对括号括起来的参数列表进行表示:
class Color
parameter Integer r, g, b;
end Color;
Color c(r = 255, g = 0, b = 125);
以上两个示例中,“= 0”以及“(r = 255, g = 0, b = 125)”都称为变型。变型机制可出现于以下三种场合:
- 组件声明
Real t(start = 2.0);
Resistor resistor(R(displayUnit = "Ohm") = 0.9); // 变型可以嵌套
- 继承语句
extends OnePort(v = 0.5, i = 1.0);
- 简短类(参见抽象与继承所述)
type Voltage = Real(unit = “V”);
# 变型的应用
对参数进行变型,用一个类型构造不同的实例
假定某电路模型需要连接两个电阻值大小不同的电阻器,不必由于电阻值的不同就建立两个电阻模型,而只需建立一个电阻模型,并将电阻值作为该模型的参数,然后用该模型声明两个不同的电阻组件,在声明电阻组件的同时对电阻值进行变型即可得到不同电阻值的电阻器组件。以抽象与继承中的电阻模型为例,可以如下使用:
model SimpleCircuit Resistor r1(R = 10); // 构造实例r1,通过变型设置阻值R为10 Resistor r2(R = 100); // 构造实例r2,通过变型设置阻值R为100 … equation connect(r1.p, r2.n); // 将电阻器r1的正极p与r2的负极n连接起来 … end SimpleCircuit;电路模型 SimpleCircuit 中,用 Resistor 声明了两个电阻器组件 r1 跟 r2,采用变型的方法将 Resistor 中的电阻值R设为指定阻值,即得到多个不同阻值的电阻器。
注意到被变型的电阻值 R 具有“parameter”前缀,这表示它是一个参数(关于参数的描述,请参见变量)。参数在模型仿真期间是保持不变的,也就是说尽管不同模型之间或两次仿真之间参数可以被修改,但对于某个模型的一次特定仿真来说参数是保持不变的。比如电阻模型中的电阻值、电容器模型中的电容值等对于特定电阻器、电容器来说必然是恒定的。
目前遇到的所有示例中,被变型的组件属性都是参数。尽管 Modelica 规范中没有明确禁止对变量、常量等进行变型,但鉴于 Modelica 参数的设计意图,在此强烈建议仅对模型中的参数进行变型。
对数组的变型
变型除应用于标量外,还可以应用于数组。变型可以对所有数组元素的某个共同属性进行变型,也可以对整个数组进行变型。为防止参差数组的出现,不可以对单个数组元素进行变型。例如:
class Array Real x[2]; Real y; end Array; Array A1[2](x = [1, 2; 1, 2], y = {2, 5}); Array A2[2] = A1; Array A3[2](x[1, 1] = 3); // 非法,不能对单个数组元素进行变型如果要对所有数组元素中的某个属性赋相同的值,可以采用
each关键字。例如:Array A4[2](each x = {1, 2}, each y = 2); // A4 中每个元素的 x 分量均为{1,2},y 分量均为 2继承或简短类定义中的变型
可以在继承一个类的同时对类型中某些元素进行变型。例如:
class A parameter Real x = 1; Real y = 2; end A; class B extends A(x = 10, y = 20); end B; type C = B(x = 100);
# 变型的优先级
在模型实例化过程中,变型按照外层覆盖内层的原则进行合并。下例说明了变型的这种合并规则。
class C1
parameter Real a;
end C1;
class C2
parameter Real b, c;
end C2;
class C3
parameter Real x1; //无缺省值
parameter Real x2 = 2; //缺省值为2
parameter C1 x3; //x3.a无缺省值
parameter C2 x4(b = 4); //x4.b缺省值为4
parameter C1 x5(a = 5); //x5.a缺省值为5
extends C1; //继承而来的元素a没有缺省值
extends C2(b = 6, c = 77); //继承得到的元素b缺省值为6
end C3;
class C4
extends C3(x2 = 22, x3(a = 33), x4(c = 44), x5 = x3, a = 55,
b = 66);
end C4;
外层变型覆盖内层变型,例如 b=66 覆盖了 extends C2(b=6) 中嵌套的类变型。这就是变型合并:合并 (b=66) 与 (b=6) 使得 b=66。
类 C4 的实例化得到如下结果:
| 变量 | 缺省值 |
|---|---|
| x1 | None |
| x2 | 22 |
| x3.a | 33 |
| x4.b | 4 |
| x4.c | 44 |
| x5.a | x3.a |
| a | 55 |
| b | 66 |
| c | 77 |
合并过程中,如果覆盖了组件的 final 属性则报错。每个变型保持其自身的 each 属性。一个变型语句不能对同一个元素或分量多次赋值。例如:
class C1
parameter Real x[3];
end C1;
class C2 = C1(x = 3 : 5, x = {1, 2, 3}); // 错误:x被赋值两次
class C3
class C4
parameter Real x(final unit = "V"); // final表示x的属性unit
// 不能被修改
end C4;
C4 a0(x(unit = "kV")); // 错误,C4.x.unit有final属性,
// 不能被修改
C4 a(x.displayUnit = "mV", x = 5.0);
//正确,赋值的属性不同(displayUnit和value)
//等同于
C4 b(x(displayUnit = "mV") = 5.0));
end C3;
# 重声明
除了继承与变型之外,Modelica 还提供了另外一种重用机制——重声明。相比继承机制的代码重用,变型机制的参数化功能,重声明机制能够有效地支持衍生设计。
重声明语句以“redeclare”前缀予以标识。变型中的 redeclare 结构使用另一个声明替换变型元素中局部类或组件的声明。重声明既可以针对组件,也可以针对类型。无论哪种方式,都使得类型作为模型的参数,从而让抽象模型更具柔性。
例如:
model HeatExchanger
replaceable parameter GeometryRecord geometry;
replaceable input Real u[2];
end HeatExchanger;
HeatExchanger heatExchanger(
redeclare parameter GeoHorizontal geometry;
redeclare input Modelica.SIunits.Angle u[2]);
上例中,在构造热交换器对象 heatExchanger 时重声明了其中的两个组件,从而使模型具备了新的特性。
下面详细介绍重声明的用法。
# 对组件进行重声明
以电机系统建模为例,假设已有控制模块,要求根据应用场景更改控制策略。使用重声明机制可以描述此类需求。
partial model MotorDrive
replaceable Controller controller;
…
end MotorDrive;
model MyMotorDrive
extends MotorDrive(redeclare AutoTuningPI controller);
end MyMotorDrive;
MyMotorDrive 继承 MotorDriver,同时用新的控制器 AutoTuningPI 替换原先的控制器 Controller。如果不采用重声明,MyMotorDrive 需要如下定义:
model MyMotorDriveWithoutRedeclare
AutoTuningPI controller;
…
end MyMotorDriveWithoutRedeclare;
上面的模型不仅需要重新定义 controller,而且要重写模型中的其余部分,即使这些部分可能跟基类 MotorDrive 中的完全相同。
可以看出,通过对可替换组件进行重声明可以重用模型 MotorDriver 的大部分代码,从而使得模型设计更为简洁,更具可维护性。
# 对类型进行重声明
在多数实际应用中,模型可能远不止一个控制器,那么上一小节中对单个组件重声明的方法就显得有点繁琐。假设有如下模型 MotorDrive:
partial model MotorDrive
replaceable Controller controller1;
replaceable Controller controller2;
replaceable Controller controller3;
…
end MotorDrive;
现在要建立模型 PIDMotorDrive ,该模型需要将三个 Controller 全部替换为 PIDController。如果仍采用对组件进行重声明的方法,新的模型可以如下定义:
model PIDMotorDrive
extends MotorDrive(
redeclare PIDController controller1,
redeclare PIDController controller2,
redeclare PIDController controller3);
……
end PIDMotorDrive;
尽管达到了目的,但这种写法还是显得繁琐。现在采用类型重声明的方法,一次将这三个控制器的类型全部替换,模型定义如下:
partial model MotorDrive
replaceable block ControllerModel = Controller;
ControllerModel controller1;
ControllerModel controller2;
ControllerModel controller3;
…
end MotorDrive;
该模型用到一个技巧,就是为这三个控制器的类型 Controller 引入一个类型别名 ControllerModel,以简短类的形式定义(相关信息,请参见抽象与继承)。在引入新的别名时,将类型声明为可替换的。那么新的模型可以很方便的定义如下:
model PIDMotorDriveWithTypeRedeclare
extends MotorDrive(
redeclare block ControllerModel = PIDController);
……
end PIDMotorDriveWithTypeRedeclare;
PIDMotorDriveWithTypeRedeclare 与 PIDMotorDirve 完全等价,在继承 MotorDrive 的同时,对 block 类型 ControllerModel 进行重声明,从而将所有 ControllerModel 类型的控制器模块一次性全部替换。可以看出,类型重声明的方法更适用于需要批量替换组件类型的情况。
重声明作为一种重要的重用机制,实质是将类型作为模型参数,在使用模型时,对参数化的类型进行重声明,从而重用已有框架,支持衍生设计。replaceable 关键字标识了哪些类型可以被替换,从而限定了重声明的范围,防止类型替换不当。
# 详细语法介绍
此页面只是对 Modelica 语言进行入门功能介绍,更详细的功能请参考 Modelica 语言规范的相关章节。
| 类型 | 对应 Modelica 规范章节 |
|---|---|
| 抽象与继承 | 继承-扩展子句 |
| 参数变型 | 修改 |
| 变型的概念 | 修改 |
| 变型的应用 | 修改 |
| 变型的优先级 | 修改 |
| 重声明 | 重声明 |