# 事件与 pre 操作符
本节主要介绍事件与 pre 操作符在工程建模中的特性及使用。
# 事件
在Modelica规范中事件是离散事件的简称,事件在物理系统建模中有其特定的含义。从物理上而言,事件导致系统状态发生改变。从数学上来说,事件导致系统出现不连续特性,致使系统中某些变量值发生跳变或者走向发生改变。数学上将这一现象称之为连续性间断点。例如:跳球模型中在小球接触地面的瞬间,事件发生,小球从向下运动状态改变为向上运动状态。从仿真曲线图中可见,小球在垂直方向的位移与速度曲线在事件发生前后均表现出明显的不连续特性。物理系统中常见的事件包括:二极管导通、静摩擦到动摩擦、溢流阀打开,等等。过程式的非因果特性。
model BouncingBall "跳球模型"
Real h(start = 10);
Real v(start = 0);
Boolean flying;
parameter Real c = 0.9;
constant Real g = 9.81;
equation
der(h) = v;
der(v) = if flying then -g else 0;
flying = not (h <= 0 and v <= 0);
when h <= 0 then
reinit(v, -c * v);
end when;
when not (flying or pre(flying)) then
terminate("Bouncing Stop!");
end when;
end BouncingBall;
事件表达式包括:sample表达式、包含连续时间变量的关系表达式(>,>=,<,<=)、包含连续时间变量的取整相关函数表达式(ceil, floor, integer, mod, rem, div),这些事件表达式默认情况下是激发事件的,事件表达式值(或值的连续性)发生改变的时刻称为事件时间。在事件时间,离散时间变量的值发生改变,或者连续时间变量的连续性发生变化。
从发生时间来说,事件可以分为采样事件、时间事件与状态事件。采样事件是一种周期性事件,由sample表达式激发。时间事件是指事件发生时间可以提前预知的事件,如:time>=0.5或time < 0.9,事件发生时间可预知为 0.5 秒与 0.9 秒。状态事件是指事件发生时间不可预知的事件,如跳球模型中小球触地时刻发生的事件,其发生时间与小球下落的初始高度与速度相关。
为了确保仿真结果的精确性,在仿真过程中需要监测状态事件是否发生,并定位出事件时间。以跳球模型为例,在仿真过程中,需要检测小球是否触地,并准确定位出触地时间,及时重置速度变量v,以便获得高精度的仿真结果。仿真过程中,状态事件的监测与定位需要耗费一定的时间,事件发生(系统连续性发生改变)导致的积分过程中断与重启尤为影响求解效率。鉴于事件对求解效率的影响,建模时需要慎重处理模型中的事件表达式。合理地屏蔽无实质性意义的事件可有效提高模型的仿真速度。
# pre 操作符
在常量、参量与变量专题中曾有提到,离散时间变量只在事件时间发生改变。换而言之,如果没有事件发生,离散时间变量值则一直保持不变。接下来将要介绍的 pre 操作符就涉及到离散时间变量仅在事件时间发生改变的这一特性。
model Case6_1
Real x(start=0);
Real y1;
Real y2;
Real z1;
Real z2;
parameter Real tstart=0.1;
parameter Real tinterval=0.1;
equation
when sample(tstart, tinterval) then
x = time;
y1 = x;
y2 = pre(x);
end when;
z1 = x;
z2 = pre(x);
end Case6_1;
在示例模型 Case6_1中,从仿真结果曲线图上看,变量 z1 与z2 完全一样,变量 y1 与 y2 是不一样的。在解释这种差异之前,需要首先了解事件迭代这个概念。
# 事件迭代
事件迭代是指在事件时间,对于任意离散时间变量
以上示例中表明,通常情况下when语句之外的 pre 打破环路,不然尽量不要在 when 语句之外使用 pre 操作符,因为 pre 操作符的存在可能引发无实质意义的事件迭代。总的来说,pre 操作符的常用情况包括:
(1)有意打破离散变量环路(实质上不存在环路而形式上构成了环路的情况);
(2)在 when 语句中使用,用于在事件时间获取离散时间变量在上一个事件时刻的值或事件迭代过程中上一次迭代的计算值。下面将通过使用 pre 操作符,将Case6_1中的sample等效替换为一个时间事件表达式。
model Case6_2
Real x(start=0);
Real y1;
Real y2;
Real z1;
Real z2;
Real nextEvent(start=tstart);
parameter Real tstart=0.1;
parameter Real tinterval=0.1;
equation
when time>=pre(nextEvent) then
nextEvent=time+tinterval;
x = time;
y1 = x;
y2 = pre(x);
end when;
z1 = x;
z2 = pre(x);
end Case6_2;
只要Case6_2中的参量tstart大于仿真起始时间(仿真区间的开始时间),Case6_1与Case6_2即是等价的。不等价的情况在介绍when语句时再详细解释。
如果是在when语句内部使用pre操作符,则可以简单地认为是取相应变量在该when语句上一次生效时间的值。例如以上示例Case6_1中的pre(x)是取变量x在when语句上一次生效时的值,故而y1与y2是不相等的。
从事件迭代的角度来说,在事件时刻(对于Case6_1为sample(tstart, tinterval)的采样时刻,对于Case6_2为time>=pre(nextEvent) 的时间事件触发时刻),在第一次模型计算后,y1与y2是不相等的,x1与x2也是不相等的。由于事件时刻变量x被更新,故 Modelica语义,when语句仅在其条件为false变为true的瞬间生效一次,因此when语句此刻不再生效,变量y2不再被更新,而变量x2则被再次更新,且此时x与pre(x)是相等的,所以变量z1与z2是相等的,而y1与y2是不相等的。
下面结合示例讲述一下实质上不存在环路而形式上构成了环路的情况。
model Case6_3
Real u = sin(10 * time) + 0.1;
Real v = 100 * time;
Integer n = if u > 0 then k else integer(v);
Integer k = if u > 0 then integer(-v) else n;
end Case6_3;
对于Case6_3,模型翻译时会报错,提示模型中存在整型代数环。笼统地看,变量k与n确实相互依赖。但实际上当u>0时有如下计算关系:
u := sin(10 * time) + 0.1;
v := 100 * time;
k := integer(-v);
n := k;
而当u<=0时有如下计算关系:
u := sin(10 * time) + 0.1;
v := 100 * time;
n := integer(v);
k := n;
由此可见,不管关系表达式u>0是否成立,变量k与n均是相互依赖的。只是在u>0成立时变量n依赖于变量k,在u>0不成立时变量k依赖于变量n。此情形即是实质上不存在环路(相互依赖)而形式上构成了环路的情况。对于这种实质上不存在环路的情况,如Case6_4所示,可以使用pre操作符将该形式上的环路打破,使得模型可以成功翻译与求解。
时间事件表达式。
model Case6_4
Real u = sin(10 * time) + 0.1;
Real v = 100 * time;
Integer n = if u > 0 then pre(k) else integer(v);
Integer k = if u > 0 then integer(-v) else n;
end Case6_4;
由于没有掌握好pre操作符的使用,曾经有工程部同事出现过类似下面when语句的问题,即在when语句的条件中直接使用when语句的求解变量,造成自依赖环路而报错。
when time>=nextEvent then
nextEvent=time+tinterval;
end when;
这一错误是过程式语言的思维结果。在过程式语言中,程序按顺序执行,对于先引用后赋值情况,则可自行约定为上一次计算的值。在陈述式语言中,不管一个变量出现在模型中的何处,均是指同一时间的变量。对于以上when语句,若要检查when语句是否生效,则要先得到变量nextEvent的值,而要计算变量nextEvent的值,则要先检查条件以判断when语句是否生效,故此造成了自依赖环路。遇到问题的这位同事于是尝试着将when语句放置到算法中试试看,发现不再报错,而且结果符合预期,很是有点纳闷。其实是将when语句放置到算法中之后,由于算法的执行是过程式的,在模型翻译过程中检查出来存在先引用后赋值情况,故而为其补充了一个默认赋值时,使得算法变成了如下形式。
nextEvent:=pre(nextEvent);
when time>=nextEvent then
nextEvent:=time+tinterval;
end when;
以上算法与正确的when方程是一致的,等效于when条件中使用了pre操作符。同样的问题在最近正在实施的某个分布式仿真项目中也出现了,反馈给内核部说模型求解失败。究其原因,是将 C 程序中的写法复制过来,而没有正确使用pre操作符。
特别说明一下,只有在when语句内可以对连续时间变量使用操pre作符,此时表示取when语句上一次生效时的连续变量值。同时也再次重申下,在Modelica语言中,没有“取上一步的变量值”的内置操作符。对于离散时间变量,可以使用pre操作符获取变量在上一个事件时间的结果。但需要理解Modelica语言的事件迭代机制,在示例模型Case6_1中,变量y2与z2的结果是不同的。变量z2是先获取变量x在上一个事件时间的值,然后当变量x发生变化时,造成 when语句不再生效,变量x与变量y2保持不变,但变量z2重新被计算一次,故其值与变量x等同。从最终结果上来说,变量z1与z2是一致的,但更推荐使用变量z1的赋值方程形式。因为变量z2的赋值方程中使用了pre操作符,可能引发无用的事件迭代而降低仿真效率。因此,在when语句之外使用pre操作符,需要慎重考虑其必要性。
# 事件的可检测性
在此专题的最后,再讲述下事件的可检测性问题。事件的可检测性是指事件发生的时间是可以精确定位出来。下面举一个在程序语言思维不太能理解的例子。
y=if sin(time)>=0 then sqrt(sin(time)) else 0;
在Modelica模型中这样写,是会报错的,将出现对负数值求算术平方根。为了确保事件的可检测性,在非事件时间的连续仿真过程中,事件表达式的结果值是被锁定的,直至检测到事件并定位出事件发生时间,进入事件时间之后,事件表达式才进行值的更新。对于以上方程,在仿真起始时间(假定为time=0开始仿真),if表达式的条件是成立的,故仿真开始后,直至检测到事件而进入事件时间,sin(time)>=0的值被锁定为true,即实际上有y=sqrt(sin(time))。而在事件检测与定位过程中,可能出现sin(time)<0,故而求解器会报错。
以上面的示例为例,若不需要关注条件发生变化的准确时间,即条件何时发生变化对仿真结果影响不大,则可以使用noEvent操作符屏蔽事件,如下所示。
y=if noEvent(sin(time)>=0) then sqrt(sin(time)) else 0;
若需要关注条件发生变化的准确时间,则需要对sqrt的参数进行限制,如下所示。
y=if sin(time)>=0 then sqrt(max(sin(time),0)) else 0;
前面已经指出,在非事件时间会对事件表达式的结果值进行锁定,这是为了确保可以准确检测到事件。倘若不对事件表达式的结果值予以锁定,将可能出现下面示例中的情形。
y=if noEvent(x>0) then x else if sin(time)>=0 then sqrt(max(sin(time),0)) else 0;
在以上代码中,事件表达式sin(time)>=0依赖于表达式noEvent(x>0),而表达式noEvent(x>0)中的事件被屏蔽,其值按实际情况计算,当其值为false时,事件表达式sin(time)>=0才被执行。在检测该事件的过程中,表达式noEvent(x>0)有可能随时从false变换为true,而导致表达式sin(time)>=0不再被执行,故而使得事件无法检测到。因此,这样的代码在语义没有确保事件的可检测性,故而是错误的。
# 事件屏蔽
除了可使用noEvent操作符显式地屏蔽事件之外,Modelica规范明确规定when语句与自定义函数中均屏蔽事件。when语句由于只在事件时间生效,其中事件显然是不可检测的,故而Modelica规范明确规定屏蔽其中的事件。而屏蔽自定义函数中的事件,则可能有多方面的原因。Modelica规范没有予以明确说明,在此也不加详细解释,因为阐述起来比较复杂。对于工程应用而言,记住自定义函数屏蔽事件这一结论即可。
另外在模型算法中,还有两种情况也是屏蔽事件的,尽管Modelica规范没有明确指出这两种情况,但根据事件的可检测性要求,可以推断出应屏蔽事件。
其一,while循环语句。由于事件表达式结果值的锁定机制,可能导致其陷入死循环。其二,循环次数不确定的for循环语句。循环次数的不确定可能导致事件个数不确定,使得求解代码无法表示。当然,对于物理系统建模而言,这两种情况是很少涉及的。
# 小结
本专题讲述的事件与pre操作符,是存在紧密关联的两个主题,对工程建模具有重要影响。曾经调试过好些个模型仿真慢的问题,结果均是无实质意义的事件导致的。频繁的事件检测与事件更新,严重影响仿真求解效率。合理地使用pre操作符,是应用好离散时间变量的关键,可有效避免某些涉及离散时间变量的模型求解失败或者仿真结果不符合预期等问题。