Ligral

路由类型

Ligral 是一个面向对象的仿真语言,您可以在 ligral 中创建路由类型,然后进行若干次实例化从而得到多个实例。我们相信这在仿真场景中是非常实用的。例如您设计了一个弹簧类,在进行某个机构仿真的时候,您可能需要引入多个弹簧模型,此时您可以声明多个弹簧实例,而不是把代码复制一遍。

路由与类/方法的异同

路由这一概念和 c++ 中的类有些相似,但又有所不同。相同之处在于:

  • 它们都规定了一类承载数据、包含功能的物体;
  • 都允许从类型本身克隆出实例;
  • 都能根据参数的不同而在具体功能实现上保留差异。

不同之处在于:

  • 路由所包含的功能逻辑是比较单一的,即根据输入计算输出,不存在成员方法;
  • 路由仅有输入输出端口是公共的,其他成员是内部私有的,不允许修改成员的访问属性。

从类的角度来说,路由实例的声明就相当于调用构造函数,下面代码对比了 ligral 语法的实例声明语法(详见 节点声明 )和 c++ 风格的实例声明语法:

# ligral
Spring[spring]{K:10};
// c++
auto spring = new Spring(10);

可以看到两条语句承载的信息是一样的(ligral 仅支持显式指定参数名的方式定义参数,而 c++ 默认使用位置参数,因此 ligral 语句多了一个 K )。在之后的代码中,均可以通过 spring 标识符来引用这个实例。Ligral 的实例声明语句之所以如此设计,是方便您在调用时声明,且仍能将实例绑定到标识符上,这在 c++ 中是无法做到的。

# ligral
Step[src] -> Model;
src -> Print;
// c++
auto src = new Step();
src.to(new Model());
src.to(new Print());

从类克隆出实例后,路由看起来更像是一个方法。事实上,路由的作用很多时候就是实现了一个方法(函数),根据输入计算输出。不考虑求解器循环调用,下面两个语句的作用效果是等效的:

# ligral
x -> Sin -> y;
// c++
y = sin(x);

如果您只熟悉数学公式,那么 c++ 的写法显然更直观;如果您已经习惯于使用 Simulink,ligral 的语法也很简单明了。Ligral 之所以这么设计,是方便您级联调用而不产生任何后果;反观 c++ 的语法,会让程序末尾多了很多括号,影响可读性。Ligral 的调用方式和 shell 脚本中 pipe 调用有些类似,不过 shell 语法采用了一种混合式的风格。

# ligral
x -> Sin -> Abs -> y;
// c++
y = abs(sin(x));
# shell
y=$(echo $x | sin | abs)

从这个角度看,路由像是一种元方法,即创建方法的方法,通过一些参数的选择,生成略有不同的方法,这和 python 的装饰器有点像。

路由的定义

路由的定义语句从 route 关键字开始,到 end 关键字结束,其中包含头部的属性信息以及主体部分的逻辑信息。

路由的属性

通常路由定义语句的第一行包含了路由的属性,包括路由的名称、签名、参数、输入输出端口,其中父类、参数和输入输出都是可省的,语法如下:

route RouteName(;;) ...
route RouteName:Interface(;;) ...
route RouteName(parameters; inputs; outputs) ...

第一行显示了省略所有可省部分的语法。第二行展示了如何声明一个路由声明签名,关于签名的具体使用请查看 接口签名 小节,此处仅介绍语法。

第三行说明了参数和输入的顺序,它们都在一对圆括号里,被两个分号分别分开。即使其中任何一个被省略了,分号也不能省略。输入和输出的语法十分简单,就是用逗号分隔端口名,端口的书写顺序就是编号顺序。

参数的语法稍微复杂一些。首先,参数从可省性上分为可省参数和不可省参数,可省参数需要指定参数名和默认参数值,不可省参数只要指定参数名;其次,从参数类型又可分为常量参数和节点参数,顾名思义,常量参数接受一个常量,节点参数接受声明特定签名的节点。节点参数必须指定签名,常量参数无需指定。

# 常量不可省参数
param1,
# 常量可省参数
param2 = 0,
# 节点不可省参数
param3:NodeSignature, ...

节点参数只能是不可省参数,因为参数的默认值在路由类型(而不是其实例)被创建的时候就被创建了,因此全局仅有一份拷贝。如果您为这个路由声明了两个实例,两个实例的这个参数均绑定到同一个节点上,必然会造成 Duplicated binding of InPort. 错误。

路由的主体

路由的主体部分语法和整个 ligral 程序一致。在路由内部,您可以使用常量声明语句定义局部变量,除此之外,路由的所有参数都是局部变量。更重要的是您可以声明节点,描述节点之间的连接方式,确保除了输入以外所有的节点都是被连接的,所有输入节点都是未连接的。

此外,目前设置语句和导入语句也是可以在路由内部使用的,不过未来很可能会禁用设置语句,因为没有实际应用场景,而且会让人产生困扰。

一个完整的例子

本节暂不讨论和接口签名有关的内容。以一个 PID 控制器为例,展示如何定义一个路由( TransferFunction 模块尚未实现):

route PIDController(Kp, Ki=0, Kd=0, tau=0.01; x; u)
    x -> Integrator[xi];
    x -> TransferFunction[xdot]{num=[1, 0], den=[tau, 1]};
    x * Kp + xi * Ki + xdot * Kd -> u;
end

这个路由的名字为 PIDController ,有四个参数,除了 Kp 之外其他的为可选参数。前三个是控制器增益,如果您忽略了 Ki 参数,那么您实际上声明了一个 PD 控制器的实例,忽略了 Kd 则得到 PI 控制器。第四个参数是微分所需的一个时间常数。输入和输出各只有一个,输出由输入本身、输入的积分和微分乘上对应的增益相加而得到,写成传递函数的形式就是:

u ( s ) = K p x ( s ) + K i x ( s ) 1 s + K d x ( s ) s τ s + 1 u(s) = K_px(s)+K_ix(s)\frac{1}{s}+K_dx(s)\frac{s}{\tau s+1}