Ligral

节点连接

在 ligral 中,所有的连接都是从一个输出端口到一个输入端口的有向边,表示从输出端口发出来的信号原封不动地注入至输入端口。因此,无论多么复杂的仿真模型,只要节点数目是有限的,ligral 总能以如下最基本的形式描述节点的连接情况:

SomeNode:input -> OtherNode:output;

上述代码很直观地表述了信号的流向,以至于在前几节曾出现过一些连接的例子,虽然那时尚未介绍连接地语法,却也不影响理解。这种从显式指定的输出端口到输入端口的连接称之为端口连接。如果每一个连接都需要这样显式指定端口且分行,必然会丧失代码的简洁和可读性。Ligral 在连接方式的简化上也不遗余力地引入许多便捷的表示。

全连接

如果源节点的输出端口数目和目标节点的输入端口数目一致,且源节点的每一个输出端口正好依次连接到目标节点对应的输入端口,则成这些连接的集合为一个全连接。全连接是最常见的一种连接方式,因为许多节点是单输入输出节点,它们之间的连接全是全连接。

声明全连接时不需要指定端口,只需要在节点之间直接连接即可。在之前举国的例子中, Step 是单输出模块, Print 是单输出模块,因此可以使用全连接:

Step -> Print;

利用节点组实现全连接

回顾定义,节点组可以用来组合输入和输出端口。在某些不满足全连接的情况中,可以利用节点组来凑成全连接,从而简化代码。

Calculate 是一个双输入模块,如果您希望将两个 Constant 模块分别连接到两个端口上,您可以将两个 Constant 模块并连成一个模块组。声明模块组的语法很简单,只要用逗号分隔模块链,再用圆括号括起来即可,如下所示(这个例子在上一节展示过):

(1, 1) -> Calculate{type:'+'};

上面的例子如前所示可以简化成 1+1 ,不再赘述。模块组除了可以作为源节点,也可以作为目标节点,甚至可以同时充当源节点和目标节点,虽然从语义的角度而言没有意义,比如:

(1, 2) -> (Print, Print);

这样的语句虽然是合法的,但是未必比分开写更紧凑或者更清晰:

1 -> Print; 2 -> Print;

使用端口来组成节点组也是很常见的,这适用于多输入输出节点之间的互相连接,且端口顺序不一定对应得上的时候。例如如果您研究的一个模型有速度 v v 和位置 x x 两个状态,外加一个加速度 a a ,但是您设计的速度控制器可能仅需要 v v a a ,此时您有两种写法。其一,将模型重组为节点组:

(model:v, model:a) -> controller;

其二,使用弃元(弃元是一个特殊的单输入模块,它声明放弃指向它的信号,详见模块文档):

model -> (_, controller);

在没有优化的时候,第二种写法确实会多浪费一点计算资源,但是日后实现优化后两者是等效的。但是第二种写法默认了 model 模块和 controller 模块的端口顺序是一致的,即 model 的输出端口为 x v a controller 的输入端口为 v a 。相比之下,第一种写法更具灵活性。

半全连接

在一个节点为单输入输出,另一个为多输入输出时,半全连接也很常见。半全连接指的是端口和单输入输出节点之间的连接,此时单输入输出节点不需要显式指定端口。

这种情况一般多用于多输出节点和单输入节点之间的连接,因为输出端口不一定需要连接,可能只有部分端口被连接到一些新的节点上。但是对于多输入节点,每个输入端口都必须被连接,因此利用节点组实现全连接会是更好的选择。

顺次连接

目标节点和源节点不一定是对立的,而是相对的概念。一个连接中的目标节点也可以是其他连接中的源节点,ligral 的语法允许这些连接顺次出现,从而减少重复,包括行数、节点的引用以及可能因为减少到引用而可以省略的 ID 分配等等。

如果前后两个连接都是全连接(包括利用节点组实现的全连接),直接合并中间节点即可。下面的代码求解了 -1 的绝对值:

-1 -> Abs -> Print;

如果上游的连接时全连接,下游是半全连接或者端口连接,也可以直接合并中间节点,如:

(model:v, model:a) -> controller -> model:u;

如果 model 的输入端口只有一个,还可以改写成:

controller -> model:v -> Print;

这个例子咋看起来很容易理解,但是深究起来可能会有些困惑。实际上 ligral 会把它解释成这样:

controller -> model; model:v -> Print;

因为下游的连接虽然指定了输出端口,但是对输入端口没有影响,因此 controller 的输出还是连接到了 model 的输入端口 u 上而不是 v 上。

但是如果中间节点被上游连接指定了输入端口,下面的第二行语句就是非法的:

model:a -> controller:a;
model:v -> controller:v -> model;

原因有二。其一,如果这个语句是合法的,则 ligral 会把它解释成:

model:v -> controller:v; controller -> model;

这种拆解方式远不如指定输出端口时的拆解方式来得自然,容易引起误解。其二,输出端口是可以被忽略的,也常常被忽略,因此鼓励引用端口,而输入端口是不能被忽略的,更倾向于用全连接的方式进行连接。

最后一种情况是上游连接的源节点指定了输出端口,但是目标节点没有,不影响下游连接。例如:

controller:u -> model -> (_, controller);

虽然如果 controller 的输出端口仅有一个 u 的话,指定端口语句完全可以被省略,此处只是举例需要。

语句的终止

虽然 ligral 鼓励使用者将连接串起来提高语言的简洁性,但是如果语句逻辑太过复杂,或者语义已经中断时,建议您拆分长语句,让代码更符合人的阅读习惯。必要的时候,将一些内部相关的语句整理到一个路由中,降低模块的耦合性。例如以下代码:

(f - k*x - c*v -> Integrator -> v -> Integrator -> x)*Kp + (r-x -> Integrator)*Ki + v*Kd -> f;

以上一行代码描述了用 PID 控制器控制一个二阶系统得到的闭环模型,由于逻辑过于复杂反而丧失了可读性。好的代码往往这么写:

# model
f - k*x - c*v -> Integrator -> v;
v -> Integrator -> x;

# reference tracking
r-x -> Integrator -> ei;

# pid controller
x*Kp + v*Kd + ei*Ki -> f;