Simulinkモデルのサブシステム間のつながりを可視化する

#Simulinkモデルのサブシステム間のつながりを可視化する
[前回](https://qiita.com/a_miyazaki/items/3a427112f82bbc449956)作成したスクリプトをリバース対象のモデルに適用したが、信号線が凄まじいことになって、結局モデルの構造がどうなっているかよくわからなかった・・・。一個一個のサブシステムの入出力ポートの数が多すぎるのが一因だと思う。
そこで、サブシステムを頂点、サブシステム間の接続を辺にした有向グラフを作成すれば見通しは良くなるはずだと思い、そのようなグラフを出力するスクリプトを作成することを目標とする。

>良く考えると、タイトルの「Simulinkモデルのサブシステム間のつながりを可視化する」って意味がわからないな。
>ソフトウェアの構造を可視化して視覚的にわかるようにしてくれるのが、Simulinkの利点の一つのはずなのに・・・。
>なんでこんなことしてんだろ？

#検証用のSimulinkモデル
スクリプトの検証用として、以下のSimulinkモデルを使用する。
サブシステム間の接続関係を取得する時、各サブシステムの出力ポート側から信号をたどって接続先のサブシステムを取得しようと考えている。
そのため、リバース対象のモデルと同じように、以下のブロックがサブシステムの出力ポートの先に存在するように検証用モデルを作成した。

* Outport
* Terminator
* UnitDelay
* Goto
* Switch
* Demux
* BusCreator
* Busselector
![target_modl.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693510/f76c7298-a7b0-5fd6-827b-c917ccdd620d.png)

#方針
matlabのdigraph関数を用いて、以下のようなコードを実行すると有向グラフを出力してくれる。
(https://jp.mathworks.com/help/matlab/ref/digraph.toposort.html)

```MATLAB:検証用モデルのサブシステム間のつながりを表す有向グラフを出力
names = {'Subsystem', 'Subsystem1', 'Subsystem2', 'Subsystem3'}; % 頂点名の配列

A = [0 1 1 0
     0 0 0 0
     0 0 0 0
     0 1 0 0]; %隣接行列

G = digraph(A, names);
plot(G)
```

上のコードの実行結果は下の図のようになる。
検証用モデルにおいて、

* Subsystem1において、SubsystemとSubsystem3の出力結果(を使った計算の結果)が入力となっている
* Subsystem2において、Subsystemの出力結果(を使った計算の結果)が入力となっている

ため、この有向グラフが今回の期待結果である。


![directed_graph.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/693510/c8d74b77-40d5-f380-226c-3c187d38cba6.png)

Simulinkモデルから、サブシステム名のリストとサブシステム間の接続関係を表す隣接行列が作れれば、digraph関数で有向グラフを出力できる。
そこで、全体としては、以下の流れで処理する。


```
1. サブシステム一覧を取得
2. 各サブシステムについて、接続先のサブシステムを取得
3. 2.の結果を用いて、隣接行列を作成
4. digraph関数で有向グラフを出力
```

#コード

###対象のシステムを設定
```MATLAB:
%% 対象のシステムを設定
model_name = "sample.slx";
target_system = "sample/Subsystem1";

%% モデル読み込み
open_system(model_name);
```

###各サブシステムについて、接続先に存在するサブシステムを取得
サブシステムの数だけ要素数を持つセル配列で、各セル配列はサブシステム名のstring配列のようなデータを作成する。
また、string配列の1番目に接続元もサブシステム名を、2番目以降に接続先のサブシステム名を格納する。
本当は、

```c++:C++で書くと
using namespace std;
map<string, vector<string>> subsys_relation; //キー:サブシステム名、バリュー:キーのサブシステムの接続先のサブシステム名の配列
```

のようなデータ構造にしたかったが、matlabでどうやるのかわからなかった。
また、直接隣接行列を作成した方がコードがスッキリして良いと思うが、matlabでコードを書くことに慣れてなくて、書けそうな書き方をしてしまった・・・。

処理内容としては、Subsystem、utport、UnitDelay、Delay、Gotoにあたるまで再帰的に信号をたどっていき、Subsystemにあたった時だけstring配列にサブシステム名を追加する。

```MATLAB:
%% サブシステムの接続先のサブシステムを取得
%   string配列の1番目に接続元のサブシステムを、2番目以降に接続先のサブシステム格納
%   string配列の要素が一つのときは、接続先のサブシステムがないことを表す
subsystem_list = find_system(target_system, 'SearchDepth', '1', 'BlockType', 'SubSystem');

subsystem_relationship_list = cell(1, length(subsystem_list)-1);
index = 1;
for i = 1:length(subsystem_list)
    if strcmp(subsystem_list(i), target_system)
        continue;
    end
    subsystem_name = string(get_param(subsystem_list{i}, 'Name'));
    input = [subsystem_name];
    ph = get_param(subsystem_list{i}, 'PortHandles');
    for j = 1:length(ph.Outport)
        line = get_param(ph.Outport(j), 'Line');
        if line == -1
            continue
        end
        dst = get_param(line, 'DstBlockHandle');
        for k = 1:length(dst)
            dst_subsystemlist = get_source_subsystem_list(dst(k), input);
            input = dst_subsystemlist;    
        end
    end
    subsystem_relationship_list{index} = input;
    index = index + 1;
end

%% 接続先のサブシステム名を取得する
function subsystem_list = get_source_subsystem_list(block, input_list)
    block_type = get_param(block, 'BlockType');
    %ブロックタイプを取得し、Subsystemの場合はsubsystem_listにサブシステム名を追加し、returnする
    if strcmp(block_type, "SubSystem")
        subsystem_name = string(get_param(block, 'Name'));
        f = true;
        for i = 1:length(input_list)
            if strcmp(subsystem_name, input_list(i))
                f = false;
                break;
            end
        end
        if f == true
            subsystem_list = [input_list, subsystem_name];
        else
            subsystem_list = input_list;
        end
        return
    %ブロックタイプがOutport、UnitDelay、Delay、Gotoの時は、input_listをreturnする
    elseif strcmp(block_type, "Outport")   ||...
           strcmp(block_type, "UnitDelay") ||...
           strcmp(block_type, "Delay")     ||...
           strcmp(block_type, "Goto")
        subsystem_list = input_list;
        return
    end
    
    %blockの接続先のブロックを取得し、再帰的に処理
    ph = get_param(block, 'PortHandles');
    for i = 1:length(ph.Outport)
        line = get_param(ph.Outport(i), 'Line');
        if line == -1
            continue;
        end
        dst = get_param(line, 'DstBlockHandle');
        for j = 1:length(dst)
            res = get_source_subsystem_list(dst(j), input_list);
            input_list = res;
        end
    end
    subsystem_list = input_list;
end
```

###隣接行列を作成
上の処理で得られた結果をもとに、サブシステム名のリストと隣接行列を作成する。

```MATLAB:
A = zeros(length(subsystem_relationship_list), length(subsystem_relationship_list)); %隣接行列

names = cell(1, length(subsystem_relationship_list)); %ノード名のリスト
for i = 1:length(subsystem_relationship_list)
    names{i} = char(subsystem_relationship_list{i}(1));
end

% subsystem_relationship_listから隣接行列を作成する
for i = 1:length(subsystem_relationship_list)
    list = subsystem_relationship_list{i};
    if length(list) == 1
        continue;
    end
    for j = 1:length(names)
        if string(names{j}) == list(1)
            row = j;
            break;
        end
    end
    for j = 2:length(list)
        for k = 1:length(names)
            if string(names{k}) == list(j)
                A(row, k) = 1;
                break;
            end
        end
    end
end
```

###可視化
隣接行列とサブシステム名のリストをdigraphに入力して有向グラフを得る。

```MATLAB:
G = digraph(A,names);
plot(G)
```

###全体
最後にコード全体を再掲。
このコードを実行すると、期待する有向グラフを得ることができた。

```MATLAB:visualize_subsystem_relationship.m
%% 初期化
clear;
clc;

%% 対象のシステムを設定
model_name = "sample.slx";
target_system = "sample/Subsystem1";

%% モデル読み込み
open_system(model_name);

%% サブシステムの接続先のサブシステムを取得
%   string配列の1番目に接続元のサブシステムを、2番目以降に接続先のサブシステム格納
%   string配列の要素が一つのときは、接続先のサブシステムがないことを表す
subsystem_list = find_system(target_system, 'SearchDepth', '1', 'BlockType', 'SubSystem');

subsystem_relationship_list = cell(1, length(subsystem_list)-1);
index = 1;
for i = 1:length(subsystem_list)
    if strcmp(subsystem_list(i), target_system)
        continue;
    end
    subsystem_name = string(get_param(subsystem_list{i}, 'Name'));
    input = [subsystem_name];
    ph = get_param(subsystem_list{i}, 'PortHandles');
    for j = 1:length(ph.Outport)
        line = get_param(ph.Outport(j), 'Line');
        if line == -1
            continue
        end
        dst = get_param(line, 'DstBlockHandle');
        for k = 1:length(dst)
            dst_subsystemlist = get_source_subsystem_list(dst(k), input);
            input = dst_subsystemlist;    
        end
    end
    subsystem_relationship_list{index} = input;
    index = index + 1;
end

%% モデルの依存関係を可視化する
A = zeros(length(subsystem_relationship_list), length(subsystem_relationship_list)); %隣接行列

names = cell(1, length(subsystem_relationship_list)); %ノード名のリスト
for i = 1:length(subsystem_relationship_list)
    names{i} = char(subsystem_relationship_list{i}(1));
end

% subsystem_relationship_listから隣接行列を作成する
for i = 1:length(subsystem_relationship_list)
    list = subsystem_relationship_list{i};
    if length(list) == 1
        continue;
    end
    for j = 1:length(names)
        if string(names{j}) == list(1)
            row = j;
            break;
        end
    end
    for j = 2:length(list)
        for k = 1:length(names)
            if string(names{k}) == list(j)
                A(row, k) = 1;
                break;
            end
        end
    end
end

% 有向グラフを出力
G = digraph(A,names);
plot(G)

%% 接続先のサブシステム名を取得する
function subsystem_list = get_source_subsystem_list(block, input_list)
    block_type = get_param(block, 'BlockType');
    %ブロックタイプを取得し、Subsystemの場合はsubsystem_listにサブシステム名を追加し、returnする
    if strcmp(block_type, "SubSystem")
        subsystem_name = string(get_param(block, 'Name'));
        f = true;
        for i = 1:length(input_list)
            if strcmp(subsystem_name, input_list(i))
                f = false;
                break;
            end
        end
        if f == true
            subsystem_list = [input_list, subsystem_name];
        else
            subsystem_list = input_list;
        end
        return
    %ブロックタイプがOutport、UnitDelay、Delay、Gotoの時は、input_listをreturnする
    elseif strcmp(block_type, "Outport")   ||...
           strcmp(block_type, "UnitDelay") ||...
           strcmp(block_type, "Delay")     ||...
           strcmp(block_type, "Goto")
        subsystem_list = input_list;
        return
    end
    
    %blockの接続先のブロックを取得し、再帰的に処理
    ph = get_param(block, 'PortHandles');
    for i = 1:length(ph.Outport)
        line = get_param(ph.Outport(i), 'Line');
        if line == -1
            continue;
        end
        dst = get_param(line, 'DstBlockHandle');
        for j = 1:length(dst)
            res = get_source_subsystem_list(dst(j), input_list);
            input_list = res;
        end
    end
    subsystem_list = input_list;
end
```

#おわりに

今回作成したコードを、期待を込めてリバース対象のモデルに対して実行したところ、ソーシャルグラフみたいなグラフが出力されて絶望した・・・。
トポロジカルソートすれば、視覚的にはわからなくてもサブシステムの実行順序は得られるかもと思いたつ。
matlabだと

```MATLAB:トポロジカルソート
N = toposort(G)
```

でトポロジカルソートできるらしいので実行したところ、「有向グラフは非循環でなければなりません。」のエラーメッセージが出力された。
今回作成したグラフをよく見るとループしている箇所があったのでモデルを確認すると、subsystemの内部でUnitDelayされていた・・・。
orz

今回作成したコードと、検証用モデルは以下にあります。

https://github.com/myzk-a/Matlab/tree/main/visualize_subsystem_relationships

#参考にした情報

https://jp.mathworks.com/help/matlab/ref/digraph.toposort.html