【MATLAB/Simulink】変数(ライン)の名前に自動で型を付加するスクリプトを作成

# あらすじ

ビルドしたsimulinkモデルを用いて制御対象を動かすと同時にデータをロギングする際、ロギングツールによっては変数名にデータ型が付記されていた方が便利になる。
何故なら、そのロギングツールでロギング対象の変数を選択する際、正しいデータ型を選ばないと正常にロギングできないため。よって、ロギングする変数を追加する場合は、正しい型を指定できるように常にsimulinkモデルを見て型を確認しながらロギングリストに追加していく必要がある。これはかなり骨が折れる作業である。

本記事では、変数名にデータ型を付記することで、いちいちsimulinkモデルを確認しなくても、変数名からデータ型が分かるようにする。
手動で変数名を付記していくのは当然面倒なので、matlabでデータ型を自動で付記するスクリプトを作成する。

例えば

上記simulinkモデルのライン名に、それぞれのデータ型を付記する
（subsystem内部のライン（2枚目）も含め）
ただし、ライン名が最初から存在しないものには、何もしない。

![実行前_上位.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/13d2ca07-da5c-47ae-49a1-02719a927e42.png)

![実行前_下位.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/a2096f24-ba8d-a63e-0ca2-1a95db8506ee.png)

# ① 変数のデータ型を取得する

所望のスクリプトを実現するため、まずは変数名のデータ型を取得する方法について考える。

方法としては、単純にそれぞれのラインのハンドルを取得し、ハンドルからデータ型のプロパティにアクセスすることで、それぞれのラインのデータ型を取得しようと考えた。

```matlab
% 各ラインのハンドルを取得
lhd = find_system('test', 'FindAll', 'on', 'Type', 'line');  
```

※ ハンドル … simulink上のオブジェクトのID

しかし、ここで問題が。
ブロックのパラメータにアクセスするための共通プロパティ（下記）は存在するが、ライン用のプロパティが見当たらない。
https://jp.mathworks.com/help/simulink/slref/common-block-parameters.html

ただ、試してみると上記のページもブロック用プロパティと言いながらも、ラインも同様のプロパティを保持している場合があるようだ。
例えば下記のように、取得したラインのハンドルを引数として、各ラインの名前を取得することができる。

```matlab
% 各ラインのハンドルを取得
lhd = find_system('test', 'FindAll', 'on', 'Type', 'line');  
lname = get_param(lhd, 'Name'); % 各ラインの名前を取得
```

データ型を取得できるプロパティを探すと、「CompiledPortDataTypes」なるプロパティの説明に「ブロック線図を更新した後の端子信号のデータ型」とある。
試しにやってみた。

```matlab
lhd = find_system('test', 'FindAll', 'on', 'Type', 'line');  

test([],[],[],'compile'); % コンパイルを開始(「CompiledPortDataTypes」を使用するため)

bpara = zeros(1, length(lhd)); % 実行速度を上げるため、for文中の変数の配列サイズを予め指定

% 各ブロックの出力のデータ型を取得
for i = 1:size(lhd) % ライン数の分だけ処理を実行

    bpara = get_param(lhd(i), 'CompiledPortDataTypes'); % コンパイル後の各ラインのパラメータを取得

end
```

結果は、エラーが出た。エラーメッセージは
「line に 'CompiledPortDataTypes' という名前のパラメーターがありません」

ラインのデータ型は、出力となるブロックの設定に依存するため、ライン自体がデータ型のプロパティを持っていなくても不自然ではない。

ラインにデータ型のプロパティがないのなら、もうブロックの入出力のプロパティにアクセスしてデータ型を取得するしかない。

その場合、「ブロックの入出力ラインから取得したデータ型」と「ブロックの入出力となっているラインのハンドル」を紐付けなくてはならない。

以下ページを見ると、ラインのハンドルを引数として、そのラインに接続されている
ブロックのハンドルを取得できるようだ。
https://jp.mathworks.com/matlabcentral/answers/94271-simulink-line

これにより、以下手順を踏めば、各ライン（=変数）のデータ型を取得できると考えた。
１．ラインのハンドルを取得
２．１で取得したラインのハンドルを引数とし、ラインに接続されている
　　ブロックのハンドルを取得
３．２で取得したブロックのハンドルを引数とし、ブロックの出力となる
　　ラインのデータ型を取得

```matlab
lhd = find_system('test', 'FindAll', 'on', 'Type', 'line'); % ラインのハンドルを取得
bhd = get_param(lhd, 'SrcBlockHandle'); % 各ラインのハンドルから各ブロックのハンドルを取得
lname  = get_param(lhd, 'Name'); % 各ラインの名前を取得

bdtype = num2cell(zeros(1, length(lhd))); % 実行速度を上げるため、for文中の変数の配列サイズを予め指定

test([],[],[],'compile'); % コンパイルを開始(「CompiledPortDataTypes」を使用するため)

% 各ブロックの出力のデータ型を取得
for i = 1:size(lhd) % ライン数の分だけ処理を実行
    
    if ~(strcmp(lname(i), '')) % 名前がない(空欄の)ラインは除外
        bdtype(i) = get_param(cell2mat(bhd(i)), 'CompiledPortDataTypes'); % コンパイル後の各ラインのデータ型を取得
    end
end
```

これでbdtypeにデータ型を取得できたはず。。と思いきやエラーが。メッセージは「struct から cell に変換できません。」
どうやらget_paramで取得したはずのデータ型が、構造体になっているようだ。
試しに対話モードでコマンドウィンドウにget_param(cell2mat(bhd(i)), 'CompiledPortDataTypes')を出力してみると

```
>> bdtype(1)

ans = 

      Inport: {'int32'  'double'}
     Outport: {'int32'}
      Enable: []
     Trigger: []
       State: []
       LConn: []
       RConn: []
    Ifaction: []
       Reset: []
```


どうやら、bdtypeに格納されたパラメータから更にInport/Outportのフィールドにアクセスすることで、データ型を取得できるようだ。
（下記ページにも書いてあった）

https://jp.mathworks.com/matlabcentral/answers/97565-simulink

```matlab

lhd = find_system('test', 'FindAll', 'on', 'Type', 'line');  % 各ラインのハンドルを取得
bhd = get_param(lhd, 'SrcBlockHandle'); % 各ラインのハンドルから各ブロックのハンドルを取得

bdtype = num2cell(zeros(1, length(lhd))); % 実行速度を上げるため、for文中の変数の配列サイズを予め指定

test([],[],[],'compile'); % コンパイルを開始(「CompiledPortDataTypes」を使用するため)

% 各ブロックの出力のデータ型を取得
for i = 1:size(lhd) % ライン数の分だけ処理を実行

    bpara = get_param(cell2mat(bhd(i)), 'CompiledPortDataTypes'); % コンパイル後の各ブロックのパラメータを取得

    if ~(isempty(bpara.Outport)) %「Display」や「Out」など出力を持たないオブジェクトを除く
    
        bdtype(i) = bpara.Outport; % 各ブロックの出力のデータ型を取得
    end
end
```

これで各ラインのデータ型を取得することができた。
※ 因みにこれらの検証は前に実行した時の変数がワークスペース上に残っていて
　 エラーを引き起こすので、実行の都度"clear all"でワークスペース上の
　 変数をクリアするとうまくいく。

# ② 変数名を自動で変更する

次に、ライン（=変数）を任意の名前に自動で変更する方法を考える。

まずは、ラインの名前を取得する。

```matlab
lhd = find_system('test', 'FindAll', 'on', 'Type', 'line');  % 各ラインのハンドルを取得
lname  = get_param(lhd, 'Name'); % 各ラインの名前を取得
```

次に、取得したラインの名前に適当な文字列を付記して、ラインの名前を
変更する。

```matlab
lhd = find_system('test', 'FindAll', 'on', 'Type', 'line');  % 各ラインのハンドルを取得
lname  = get_param(lhd, 'Name'); % 各ラインの名前を取得

newlname = zeros(1, length(lhd));   % 実行速度を上げるため、for文中の変数の配列サイズを予め指定

for i = 1:size(lname) % ライン数の分だけ処理を実行

    newlname = horzcat(lname(i,1), '_', 'datatype'); % ライン名と任意の文字列を結合

    set_param(lhd(i,1), 'Name', newlname); % 各ラインの名前を上書き
end
```

しかし、エラーがでてしまった。

新しい変数名を格納したnewlnameを見てみると

```
newlname = 

    '(ハンドルで指定したラインの名前)'    '_'    'datatype'
```

なんだこれは…。

どうやら取得したライン名はセル配列のようで、そのセル配列の要素をhorzcatで普通に連結しようとしても、要素の中身を連結せず、配列の要素として連結（というか追加）してしまうようだ。

セル配列 … 文字通りExcelのセルのようなイメージで、各セルに
　　　　　 任意の型のデータが格納できる配列のこと。

下記より、strjoinを使用すれば、文字ベクトルのセル配列を結合して単体の文字ベクトルにすることができるようだ。

https://jp.mathworks.com/help/matlab/ref/strjoin.html

strjoinを使用して、試してみる。

```matlab
lhd = find_system('test', 'FindAll', 'on', 'Type', 'line');  % 各ラインのハンドルを取得
lname  = get_param(lhd, 'Name'); % 各ラインの名前を取得

newlname = zeros(1, length(lhd));   % 実行速度を上げるため、for文中の変数の配列サイズを予め指定

% 変数名の頭or末尾にデータ型を追加
for i = 1:size(lname) % ライン数の分だけ処理を実行

    newlname = strjoin(horzcat(lname(i,1), '_', 'datatype')); % ライン名と任意の文字列を結合(セル配列)
    
    if ~(strcmp(lname(i), '')) % 名前がない(空欄の)ラインは除外
        set_param(lhd(i,1), 'Name', newlname); % 各ラインの名前を上書き
    end
end
```
    
今度はエラーが出なかった。
再度、newlnameを見てみると

```matlab
newlname =

(ハンドルで指定したラインの名前) _ datatype
```

アンダースコアの前後にスペースが挿入されてしまっている。strjoinでセル配列要素の文字列を連結すると、スペースが入ってしまうのだろうか。不要なので、スペースを削除する処理を入れる。

```matlab
lhd = find_system('test', 'FindAll', 'on', 'Type', 'line');  % 各ラインのハンドルを取得
lname  = get_param(lhd, 'Name'); % 各ラインの名前を取得

newlname = zeros(1, length(lhd));   % 実行速度を上げるため、for文中の変数の配列サイズを予め指定

% 変数名の頭or末尾にデータ型を追加
for i = 1:size(lname) % ライン数の分だけ処理を実行

    newlname = strjoin(horzcat(lname(i,1), '_', 'datatype')); % ライン名と任意の文字列を結合(セル配列)
    
    % セル配列要素の文字列をstrjoin&horzcatで繋げると、間に勝手にスペースが入ってしまうため、削除 
    newlname(length(cell2mat(lname(i,1))) + 1) = [];
    newlname(length(cell2mat(lname(i,1))) + 2) = [];
    
    if ~(strcmp(lname(i), '')) % 名前がない(空欄の)ラインは除外
        set_param(lhd(i,1), 'Name', newlname); % 各ラインの名前を上書き
    end
end
```

これでようやくうまくいった。

# ③ ①と②を組み合わせ、変数のデータ型を変数名に自動で追加する

あとは①と②で作成したスクリプトを、変数名などに気を付けながら結合する。
（実際には、単純に結合するだけではなく細かいところを変更している）

また、データ型をライン名の頭につけるか、末尾につけるかを選択するためのダイアログ機能を追加した。

```matlab
lhd = find_system('test', 'FindAll', 'on', 'Type', 'line');  % 各ラインのハンドルを取得
bhd = get_param(lhd, 'SrcBlockHandle'); % 各ラインのハンドルから各ブロックのハンドルを取得
lname  = get_param(lhd, 'Name'); % 各ラインの名前を取得

bdtype = num2cell(zeros(1, length(lhd)));   % 実行速度を上げるため、for文中の変数の配列サイズを予め指定
newlname = num2cell(zeros(1, length(lhd))); % 同上

addplace = questdlg('データ型は変数名の文頭と末尾、どちらに追加しますか？', 'データ型の追加場所', '文頭','末尾', 'cancel');

test([],[],[],'compile'); % コンパイルを開始(「CompiledPortDataTypes」を使用するため)

% 各ブロックの出力のデータ型を取得
for i = 1:size(lhd) % ライン数の分だけ処理を実行

    bpara = get_param(cell2mat(bhd(i)), 'CompiledPortDataTypes'); % コンパイル後の各ブロックのパラメータを取得

    if ~(isempty(bpara.Outport)) %「Display」や「Out」など出力を持たないオブジェクトを除く
    
        bdtype(i) = bpara.Outport; % 各ブロックの出力のデータ型を取得
    end
end

test([],[],[],'term'); % シミュレーションを停止

% 変数名の頭or末尾にデータ型を追加
for i = 1:size(lname) % ライン数の分だけ処理を実行

    if strcmp(addplace, '末尾') % データ型を追加する場所が変数名の末尾だった場合
        
        newlname = strjoin(horzcat(lname(i,1), '_', bdtype(i))); % ライン名と任意の文字列を結合(セル配列)

        % セル配列要素の文字列をstrjoin&horzcatで繋げると、間に勝手にスペースが入ってしまうため、削除 
        sps = length(cell2mat(lname(i,1))) + 1;
        
    else % データ型を追加する場所が変数名の頭だった場合

        newlname = strjoin(horzcat(bdtype(i), '_', lname(i,1))); % ライン名と任意の文字列を結合(セル配列)

        % セル配列要素の文字列をstrjoin&horzcatで繋げると、間に勝手にスペースが入ってしまうため、削除 
        sps = length(cell2mat(bdtype(i))) + 1;    
    end
    
    if ~(strcmp(lname(i), '')) % 名前がない(空欄の)ラインは除外
        newlname(sps)   = [];
        newlname(sps+1) = [];
        set_param(lhd(i,1), 'Name', newlname); % 各ラインの名前を上書き
    end
end

disp('終了しました。');
```

スクリプトを、以下のsimulinkモデル"test.slx"に対して実行した。


![実行前_上位.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/de8aa50c-50a8-eb87-8017-419406d91f4d.png)


![実行前_下位.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/52bf3c77-2b8a-c5ca-1c42-9403222267bd.png)

結果は、下記のようになった。
まずはライン名の頭に追加した場合：

![実行後_頭_上位.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/2611cf3b-49ac-6a97-1518-c7bee7b81d62.png)

![実行後_頭_下位.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/41c5ce89-e9e6-67eb-8ef7-104fe7df64b5.png)

次に、ラインの名の末尾に追加した場合：

![実行後_末尾_上位.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/265c6bcd-b670-cbec-24c9-20ec9b52faec.png)

![実行後_末尾_下位.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/271833/f0f88952-ed1e-2f0a-6bf9-dd445f7f6962.png)

どちらの場合も、データ型をうまく追加できた。

# 最後に
　
ここはこうしたほうがいいぞ！ここ間違っているぞ！等あれば、お手数ですがご指摘いただけますと大変喜びます。