Problem
HARK において自分でノードを作成したい.
Solution
自分で新しいノードを作成する場合, パッケージではなく,ソースコンパイルする必要がある. ソースコンパイルからインストールする方法については, HARK 講習会資料の「 HARK のインストール」の項を参照.
準備ができたら,作成したいノードのソースを記述する. 基本的なノードの作り方については HARK 講習会資料の「ノードの作成」に以下の事項が記載されている.
cc ファイル(ソースファイル)の基本的な書式
例( ChannelSelector )を用いた説明
パラメータの追加
Makefile.am の書き換え方法
本節では,さらに以下の項目
入力の追加
出力の追加
バッファ( Lookback Lookforward )
各変数型の入出力
入力本数をダイナミックに切り替える
について PublisherInt.cc,SubscriberInt.cc などの具体的なノードを作ることで,説明を行う.
PublisherInt.cc の作成
まずは,整数をパラメータとして読み込んで,そのまま吐き出す PublisherInt.cc を作ってみる. (注: hark_test ディレクトリをパッケージとしている場合を想定している.)
{$PACKAGE}/hark_test/src/PublisherInt.cc
に次のソースコードをカット&ペースト.
#include <iostream> #include <BufferedNode.h> #include <Buffer.h> using namespace std; using namespace FD; class PublisherInt; DECLARE_NODE(PublisherInt); /*Node * * @name PublisherInt * @category HARK_TEST * @description This block outputs the same integer as PARAM1. * * @output_name OUTPUT1 * @output_type int * @output_description This output the same integer as PARAM1. * * @parameter_name PARAM1 * @parameter_type int * @parameter_value 123 * @parameter_description Setting for OUTPUT1 * END*/ class PublisherInt : public BufferedNode { int output1ID; int output1; int param1; public: PublisherInt(string nodeName, ParameterSet params) : BufferedNode(nodeName, params) { output1ID = addOutput("OUTPUT1"); output1 = 0; param1 = dereference_cast<int>(parameters.get("PARAM1")); inOrder = true; } void calculate(int output_id, int count, Buffer &out) { // Main loop routine starts here. output1 = param1; cout << "Published : [" << count << " , " << output1 << "]" << endl; (*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(output1)); // Main loop routine ends here. } };
以下,部分ごとにソースコードを説明する.
#include <iostream> #include <BufferedNode.h> #include <Buffer.h>
標準出力のライブラリと,FlowDesigner 用のライブラリ. ノードを作る時は必ず include する.
using namespace std; using namespace FD;
名前空間の宣言. HARK の大元である FlowDesigner のクラスは 全て FD の名前空間の中で定義されているので, 略記したい時は必ず宣言しておく.
class PublisherInt;
このノードのクラス名. 下記で設定するノード名と一緒にする.
DECLARE_NODE(PublisherInt); /*Node * * @name PublisherInt * @category HARK_TEST * @description This block outputs the same integer as PARAM1. * * @output_name OUTPUT1 * @output_type int * @output_description This output the same integer as PARAM1. * * @parameter_name PARAM1 * @parameter_type int * @parameter_value 123 * @parameter_description Setting for OUTPUT1 * END*/
DECLARE_NODE において,PublisherInt クラスを一つのノードとして 定義されるよう宣言する(よって,クラス名と同じにしないとエラー). その下のコメントアウトに見える @name などが, FlowDesigner の GUI 上での,宣言されたノードの設定になるので, コメントとは思わず,必ず設定する. この設定値だが,「ノード本体の設定,ノード入力の設定,ノード出力の設定, ノード内部パラメータの設定」の4つがそれぞれ設定可能. ノード本体の設定以外は複数個設けることができる(複数設ける場合の設定方法については後述). 以下が具体的な設定値である.
ノード本体の設定
@name: FlowDesigner 上で表示されるノード名(クラス名と同じに)
@category: FlowDesigner の GUI で右クリックした時に,そのノードが属するカテゴリの設定.
@description: ノードの説明( FlowDesigner でそのノードをマウスオン時に表示.無くても可)
ノードの入力の設定
@input_name: ノードに表示される入力の名前
@input_type: 入力変数の型
@input_description: 入力変数の説明(省略可)
ノードの出力の設定
@output_name: ノードに表示される出力の名前
@output_type: 出力変数の型
@output_description: 出力変数の説明(省略可)
ノードの内部パラメータの設定
@parameter_name: ノードに表示される変数名(マウスオン時に黄色い窓で表示)
@parameter_type: パラメータの変数型
@parameter_value: パラメータの初期値(ソース内で変更可)
@parameter_description: (パラメータの説明.省略可)
今回のソースでは,一つの出力と,一つの内部パラメータを持っているので, FlowDesigner で表示すると次のようになる.
class PublisherInt : public BufferedNode { int output1ID; int output1; int param1;
BufferedNode クラスを継承した PublisherInt クラス定義する. ( BufferedNode クラスは
/flow_designer_hri/data-flow/
の中に定義されている.) outputID は出力ポートの ID を格納する整数であり, この ID をもとに,出力ポートに渡すべきポインタの対応を取っている.
public: PublisherInt(string nodeName, ParameterSet params) : BufferedNode(nodeName, params) { output1ID = addOutput("OUTPUT1"); output1 = 0; param1 = dereference_cast<int>(parameters.get("PARAM1")); inOrder = true; }
BufferedNode クラスを継承したコンストラクタ. 引数として,nodeName ( FlowDesigner の N ファイルにおけるクラスオブジェクト名), params ( Node クラスのメンバ変数 parameters の初期化子で, 内部パラメータが個数分定義された集合)をとる.
output1ID = addOutput("OUTPUT1"); が,FlowDesigner の GUI 側で設定した OUTPUT1 の ID を,クラス側で定義した output1ID に 格納する行になる.
param1 = dereference_cast<int>(parameters.get("PARAM1")); が, FlowDesigner の GUI 側で設定した内部パラメータを, int 型にキャストして 呼び出しているところである. @parameter_type の型だが,int 型,float 型,bool 型,string 型があり, それ以外は Object として呼び出す. ( string 型は Object として呼び出し, string にキャスト.) 例を示す.
int 型 (int param;)
param = dereference_cast<int>(parameters.get("PARAM"))
float 型 (float param;)
param = dereference_cast<float>(parameters.get("PARAM"))
bool 型 (bool param;)
param = dereference_cast<bool>(parameters.get("PARAM"))
string 型 (string param;)
param = object_cast<String>(parameters.get("PARAM"));
Vector 型 (Vector<int> param;)
param = object_cast<Vector<int> >(parameters.get("PARAM"));
String が std::string, Vector が std::vector でないのは, この型が FlowDesigner の入出力用の特殊な型のため. この型にして,情報をやりとりしないとエラーとなる.
inOrder = true; を設定すると, calculate が呼び出される度に count 値が1ずつ増加する(詳細は後述).
ソースの解説に戻る.
void calculate(int output_id, int count, Buffer &out) { // Main loop routine starts here. output1 = param1; cout << "Published : [" << count << " , " << output1 << "]" << endl; (*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(output1)); // Main loop routine ends here. }
ここがノードのメインルーチンであり, この中身が count 毎に繰り返し演算が行われる. 引数である count は,ループ回数.
今回のノードでは PARAM1 の値をそのまま次へ渡すだけなので, 現在のループの情報しか必要ないが, 複数のループにまたがって,平均値を取ったり,速度を計算したりしたい時は, その分だけバッファを取る方法がある. これについては,後ろの章で解説する.
(*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(output1)); がノードから出力される値. ( count 回目の ”output1ID” という ID で指定されるポートの出力が規定される.) このノードは出力が一つなので, (out[count] = ObjectRef(Int::alloc(output1)); と出力を書くこともできるが,一般型では上の書き方となる. (一出力の場合, *(outputs[output1ID].buffer) と out は等価となる.)
また, int 型であった output1 が,Int 型にキャストされていることに着目する. このように,全ての入出力に関する変数型は, Int 型,Float 型,String 型,Bool 型,Vector 型,Matrix 型,Map 型という FlowDesigner 独自の型にする必要がある. 例を示す.
int 型
(*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(output1));
float 型
(*(outputs[output1ID].buffer))[count] = ObjectRef(Float::alloc(output1));
bool 型
(*(outputs[output1ID].buffer))[count] = TrueObject;
string 型
(*(outputs[output1ID].buffer))[count] = ObjectRef(new String(output1));
Vector 型
RCPtr<Vector<float> > output1(new Vector<float>(rows));
(*(outputs[output1ID].buffer))[count] = output1;
(rows はベクトルの要素数.Vector<int> 等も定義可.Vector.h の include が必要)
Matrix 型
RCPtr<Matrix<float> > output1(new Matrix<float>(rows, cols));
(*(outputs[output1ID].buffer))[count] = output1;
(rows, cols は行列数.Matrix<int> 等も定義可.Matrix.h の include が必要)
ここで,RCPtr とは FlowDesigner 用のオブジェクトスマートポインタのこと. Matrix や Vector などの配列の入出力では,このポインタを渡す.
PublisherInt.cc をインストール
PublisherInt.cc を FlowDesigner で使えるようにソースコンパイルして インストールする.まずは,
{$PACKAGE}/hark_test/src/Makefile.am
の中の lib****_la_SOURCES 変数(****は任意のパッケージ.今回は hark_test )の適当な位置に
PublisherInt.cc \
を追加する.“ \ ”を忘れないようにする.
> cd {$PACKAGE}/hark_test/
で
> autoreconf; ./configure --prefix=${install dir}; make; make install;
とし,インストールする. ( ${install dir} は自分の設定にしたがう./usr などが相当する.)
FlowDesigner を起動.
> flowdesigner
GUI が起動したら,
右クリック > HARK_TEST > PublishInt
で作ったノードがあることを確認. これでインストールは終了.
ここで表示されない場合のいくつかのトラブルシュートを挙げておく.
./configure --prefix=/*** で,指定したディレクトリと, flow_designer_hri をインストールしたディレクトリが同じか確認する. FlowDesigner は,自分のインストールされているディレクトリの def ファイルを読みにいくようになっているので, 違うディレクトリにインストールした場合は無視される.
{$PACKAGE}/hark_test/src/Makefile の中に, 自分の作ったノードをコンパイルするスクリプトが正しく作られているか確認する. autoreconf が正しく行われているか. Makefile.am は正しく書き換えたか.
cc ファイルのソース中のクラス名とノード名が同じか確認. 同じでない場合,コンパイルはできても GUI で表示されない場合がある.
パスの設定は正しいか確認( which flowdesiner ). 過去に root 権限で /usr/bin などに FlowDesigner と HARK を インストールをしたことがあり, 今回はユーザー権限で自分のローカルにインストールした場合に特にこの問題に遭遇する.
SubscriberInt.cc の作成
PublisherInt.cc から出力された整数を入力し, そのまま吐き出す SubscriberInt.cc を作る.
{$PACKAGE}/hark_test/src/SubscriberInt.cc
に次のソースコードをカット&ペースト.
#include <iostream> #include <BufferedNode.h> #include <Buffer.h> using namespace std; using namespace FD; class SubscriberInt; DECLARE_NODE(SubscriberInt); /*Node * * @name SubscriberInt * @category HARK_TEST * @description This block inputs an integer and outputs the same number with print. * * @input_name INPUT1 * @input_type int * @input_description input for an integer * * @output_name OUTPUT1 * @output_type int * @output_description Same as input * END*/ class SubscriberInt : public BufferedNode { int input1ID; int output1ID; int input1; public: SubscriberInt(string nodeName, ParameterSet params) : BufferedNode(nodeName, params) { input1ID = addInput("INPUT1"); output1ID = addOutput("OUTPUT1"); input1 = 0; inOrder = true; } void calculate(int output_id, int count, Buffer &out) { // Main loop routine starts here. ObjectRef inputtmp = getInput(input1ID, count); input1 = dereference_cast<int> (inputtmp); cout << "Subscribed : [" << count << " , " << input1 << "]" << endl; (*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(input1)); // Main loop routine ends here. } };
PublisherInt.cc と似ているが, 違う点を見ていく.
* @input_name INPUT1 * @input_type int * @input_description input for an integer
PublisherInt.cc には入力ポートは存在しなかったが, 入力を設ける場合は,まず FlowDesigner の GUI の設定をここで行う. @output と基本的に書式は変わらない.
SubscriberInt.cc はパラメータを持たない代わりに一つの入力を持つので, FlowDesigner では以下のように表示される.
input1ID = addInput("INPUT1");
FlowDesigner の GUI 側で設定した INPUT1 の ID を, クラス側で定義した input1ID に登録し, GUI のポートと読み込む入力データの対応づけを行う.
ObjectRef inputtmp = getInput(input1ID, count); input1 = dereference_cast<int> (inputtmp);
ID に対応した入力ポートから,データを FlowDesigner の独自の型として受け取り, それを int 型にキャストしている. 出力側で FlowDesigner の独自の型である ObjectRef (Int 型,Float 型,Bool 型,String 型,etc...)を渡したように, 受け取る側でもこのような手順でキャストしなおす. 以下,他の型での例を挙げる.
int 型
ObjectRef inputtmp = getInput(input1ID, count);
input1 = dereference_cast<int> (inputtmp);
float 型
ObjectRef inputtmp = getInput(input1ID, count);
input1 = dereference_cast<float> (inputtmp);
bool 型
ObjectRef inputtmp = getInput(input1ID, count);
input1 = dereference_cast<bool> (inputtmp);
string 型
ObjectRef inputtmp = getInput(input1ID, count);
const String &input1 = object_cast<String> (inputtmp);
(これで input1 は string 型)
Vector 型
RCPtr<Vector<float> > input1 = getInput(input1ID, count);
(使用時,(*input1)[i].Vector<int> 型も同様.)
Matrix 型
RCPtr<Matrix<float> > input1 = getInput(input1ID, count);
(使用時,(*input1)(i,j).Matrix<int> 型も同様)
出力部分については PublisherInt.cc と同じ. 作り終えたら,前章と同様の手順で,ソースコンパイルからインストール. FlowDesigner を起動し,SubscriberInt.cc が正しくインストールされていることを確認されたい.
PublisherInt.cc と SubscriberInt.cc を使って N ファイルを作成
最後に作成した PublisherInt.cc と SubscriberInt.cc を使って, FlowDesigner のネットワークファイル( N ファイル)を作ってみる.
完成させたいネットワークファイルは以下の通り. この図がわかれば,あとは不要なので次章へ進んでもらいたい.
まずは FlowDesigner を起動. 新規ファイルの場合,起動時には MAIN というシートのみがあると思われる. 文字通りこれがプログラムのメインである.
Networks > Add Iterator
でループ処理シートを加える. (シート名は適当に決める.ここでは LOOP0 とする.)
まず MAIN シート側で,
右クリック > New Node > Subnet > LOOP0
でメイン側から LOOP0 の処理が実行できるようにする. 次に LOOP0 シートに移動し,繰り返し処理のサンプリング周期を定める. (イベントベースでも,時間ベースでも可能.) ここでは, Sleep を使って,時間周期的に繰り返し演算を行わせてみる.
右クリック > New Node > Flow > Sleep で Sleep ノードの配置 Sleep を左クリック > パラメータ SECONDS を適当に設定(例:10000) > OK Sleep の出力端子を "Ctrl" を押しながらクリック
Sleep の出力端子が CONDITION になったことを確認. これがループ処理のトリガとなっており, Sleep の処理が終了した時に新しいループが始まることを意味している. 次にメインの処理を記述する.
右クリック > New Node > HARK_TEST > PublisherInt 右クリック > New Node > HARK_TEST > SubscriberInt
で LOOP0 シートに2つのノードを配置する.
PublishInt を左クリック > PublisherInt のパラメータ PARAM1 を適当に設定(例:123) > OK PublisherInt の出力端子を SubscriberInt の入力に接続 SubscriberInt の出力端子を "Shift" を押しながらクリック
SubscriberInt の出力端子が OUTPUT1 になったことを確認. これがループ処理の出力になる. この OUTPUT1 を出力に加えたことによって, MAIN シートの LOOP0 ブロックに出力ポートが一つできる. MAIN シートに戻り,以下のようにする.
LOOP0 の出力端子を "Shift" を押しながらクリック
すると OUTPUT1 が MAIN シートからも出力されるようになり, 正しく演算をすることができる.
適当な名前をつけて保存 「実行」を押す
以下のような出力がコンソールに現れる.
Published : [0 , 123] Subscribed : [0 , 123] Published : [1 , 123] Subscribed : [1 , 123] Published : [2 , 123] Subscribed : [2 , 123] ...
これで,整数をやりとりできるネットワークファイルが完成した. ここまでが基本的なノードの作り方とノードの作り方である. ここからは,入出力の追加や,複数のフレーム間の処理など, ノードを作っていく上で重要な応用について触れていく.
ノードに内部パラメータを追加する
PublisherInt.cc では,PARAM1 という内部パラメータが一つ存在していた. ここではパラメータを複数設けるように PublisherInt.cc を改変してみる. int 型ではつまらないので,比較的難しい Vector 型を パラメータとして読み込む方法を紹介する. (名前は PARAM2 とする.)
目標は Vector 型変数をパラメータとして読み込んで,それを以下のように PARAM1 倍して, その結果を出力ポートから出すようにノードを改変することである.
PARAM1 * PARAM2
これには Vector 型の内部パラメータの追加と, Vector 型の出力ポートの追加の両者が 要素として含まれるので,二つの章に分割して考える.
本章では内部パラメータの追加の仕方を説明する.
{$PACKAGE}/hark_test/src/PublisherInt.cc
を開く. Vector 型変数を扱うので,まずは Vector.h を include する.
#include <Vector.h>
FlowDesigner の GUI の設定を改変する.
/*Node ... END*/
までの中に以下を追記する.
* * @parameter_name PARAM2 * @parameter_type Vector<int> * @parameter_value <Vector<int> 0 1 2> * @parameter_description OUTPUT2 = PARAM1 * PARAM2 *
ここで @parameter_value に注目されたい. この <Vector<int> 0 1 2> の書式が厳格であり, 特にスペースなどを必ず入れることには注意されたい.
次にクラスのメンバ変数に以下を追加する.
Vector<int> param2;
コンストラクタに以下を追記.
param2 = object_cast<Vector<int> >(parameters.get("PARAM2"));
あとは,次の例のように Vector として使用することができる.
for(int i = 0; i < param2.size(); i++){ cout << param2[i] << " "; }
本節では文章のみで改変部分を説明したが, 最終的なソースは次節の最後に掲載されている.
ノードに出力を追加する
前節では Vector 型の変数を内部パラメータとして読み込む方法を示した. 本節では,その Vector 型変数に PARAM1 を乗じて,Vector 型として出力できるように, さらに PublisherInt.cc を改変する.
まずは FlowDesigner の GUI の設定で出力を以下のように追加する.
* * @output_name OUTPUT2 * @output_type Vector<int> * @output_description OUTPUT2 = PARAM1 * PARAM2 *
次に,クラスのメンバ変数に出力ポートの ID 用の変数を加える.
int output2ID;
コンストラクタに以下を追記.
output2ID = addOutput("OUTPUT2");
calculate のメインルーチン内で,出力の設定.
RCPtr<Vector<int> > output2(new Vector<int>(param2.size())); (*(outputs[output2ID].buffer))[count] = output2;
ここで,PARAM1 * PARAM2 という演算を行って出力を行うことを 想定しているので,出力される成分数は PARAM2 と同じになる. よって,output2 は param2.size() 個のサイズとなる.
確保した output2 に PARAM1 * PARAM2 を代入.
for(int i = 0; i < param2.size(); i++){ (*output2)[i] = param1 * param2[i]; }
これで PARAM1 * PARAM2 の演算結果を OUTPUT2 から出力できる.
最終的な PublisherInt.cc の改訂版を以下に示す.
#include <iostream> #include <BufferedNode.h> #include <Buffer.h> #include <Vector.h> using namespace std; using namespace FD; class PublisherInt; DECLARE_NODE(PublisherInt); /*Node * * @name PublisherInt * @category HARK_TEST * @description This block outputs the same integer as PARAM1. * * @output_name OUTPUT1 * @output_type int * @output_description This output the same integer as PARAM1. * * @output_name OUTPUT2 * @output_type Vector<int> * @output_description OUTPUT2 = PARAM1 * PARAM2 * * @parameter_name PARAM1 * @parameter_type int * @parameter_value 123 * @parameter_description Setting for OUTPUT1 * * @parameter_name PARAM2 * @parameter_type Vector<int> * @parameter_value <Vector<int> 0 1 2> * @parameter_description OUTPUT2 = PARAM1 * PARAM2 * END*/ class PublisherInt : public BufferedNode { int output1ID; int output2ID; int output1; int param1; Vector<int> param2; public: PublisherInt(string nodeName, ParameterSet params) : BufferedNode(nodeName, params) { output1ID = addOutput("OUTPUT1"); output2ID = addOutput("OUTPUT2"); output1 = 0; param1 = dereference_cast<int>(parameters.get("PARAM1")); param2 = object_cast<Vector<int> >(parameters.get("PARAM2")); inOrder = true; } void calculate(int output_id, int count, Buffer &out) { // Main loop routine starts here. output1 = param1; cout << "Published : [" << count << " , " << output1 << "]" << endl; (*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(output1)); RCPtr<Vector<int> > output2(new Vector<int>(param2.size())); (*(outputs[output2ID].buffer))[count] = output2; cout << "Vector Published : ["; for(int i = 0; i < param2.size(); i++){ (*output2)[i] = param1 * param2[i]; cout << (*output2)[i] << " "; } cout << "]" << endl; // Main loop routine ends here. } };
ノードに入力を追加する
新しい PublisherInt.cc にて Vector 型の変数を出力できた. 本章では,その Vector 型変数を入力として受け取る新しい SubscriberInt.cc を作成する.
{$PACKAGE}/hark_test/src/SubscriberInt.cc
を適当なエディタで開く. Vector 型変数を扱うので,まずは Vector.h を include する.
#include <Vector.h>
FlowDesigner の GUI の設定を改変する.
/*Node ... END*/
までの中に以下を追記する.
* * @input_name INPUT2 * @input_type Vector<int> * @input_description input for a Vector *
クラスのメンバ変数を追加.
int input2ID;
コンストラクタに以下を追記.
input2ID = addInput("INPUT2");
calculate のメインルーチン内で入力データの読み込み.
RCPtr<Vector<int> > input2 = getInput(input2ID, count);
あとは,メインルーチン内で以下のように自由に input2 を用いることができる.
for(int i = 0; i < (*input2).size(); i++){ cout << (*input2)[i] << " "; }
最終的な SubscriberInt.cc の改訂版を以下に示す.
#include <iostream> #include <BufferedNode.h> #include <Buffer.h> #include <Vector.h> using namespace std; using namespace FD; class SubscriberTutorial; DECLARE_NODE(SubscriberTutorial); /*Node * * @name SubscriberTutorial * @category HARKD:Tutorial * @description This block inputs an integer and outputs the same number with print. * * @input_name INPUT1 * @input_type int * @input_description input for an integer * * @input_name INPUT2 * @input_type Vector<int> * @input_description input for a Vector * * @output_name OUTPUT1 * @output_type int * @output_description Same as input * END*/ class SubscriberTutorial : public BufferedNode { int input1ID; int input2ID; int output1ID; int input1; public: SubscriberTutorial(string nodeName, ParameterSet params) : BufferedNode(nodeName, params) { input1ID = addInput("INPUT1"); input2ID = addInput("INPUT2"); output1ID = addOutput("OUTPUT1"); input1 = 0; inOrder = true; } void calculate(int output_id, int count, Buffer &out) { // Main loop routine starts here. ObjectRef inputtmp = getInput(input1ID, count); input1 = dereference_cast<int> (inputtmp); cout << "Subscribed : [" << count << " , " << input1 << "]" << endl; (*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(input1)); RCPtr<Vector<int> > input2 = getInput(input2ID, count); cout << "Vector Received : ["; for(int i = 0; i < (*input2).size(); i++){ cout << (*input2)[i] << " "; } cout << "]" << endl; // Main loop routine ends here. } };
新しい PublisherInt.cc と SubscriberInt.cc を使って N ファイルを作成
基本的な手順は前節の N ファイル作成方法と同じ. まずはコンパイル・インストールが正しくできるか確認する. インストールできたら flowdesigner を起動.
前節で作った N ファイルを開く. PublisherInt ノードの出力部に OUTPUT2 が, SubsctiberInt ノードの入力部に INPUT2 が追加されているのが確認できる. (確認できなければ,インストールが正しくできていないので, 前節のトラブルシュートを参照) それらを接続する.
また以下の設定が必要になる.
PublishInt を左クリック > PublisherInt の PARAM2 の type を object に設定 > OK
これで保存して,「実行」を押す.
Published : [0 , 123] Vector Published : [0 123 246 ] Subscribed : [0 , 123] Vector Received : [0 123 246 ] Published : [1 , 123] Vector Published : [0 123 246 ] Subscribed : [1 , 123] Vector Received : [0 123 246 ] Published : [2 , 123] Vector Published : [0 123 246 ] Subscribed : [2 , 123] Vector Received : [0 123 246 ] ...
がコンソールに表示され,正しく Vector がやりとりされているのがわかる.
以下,作成したノードのキャプチャである.
複数のフレームにまたがる処理( lookBack, lookAhead オプション)
これまでは入力されたベクトルをそのまま定数倍するなど, そのフレームだけで処理できる計算を扱っていた. しかしながら,フレーム間で平均を取ったり微分を計算したりする時などは, フレームをまたいだ処理が必要になる. その場合の注意点についてこの章では触れる.
たとえば,今いるフレームから前後2フレーム,計5フレームのトータルを求める ブロックを作りたいとする.
\begin{equation} OUTPUT1(t) = INPUT1(t-2) + INPUT1(t-1) + INPUT1(t) + INPUT1(t+1) + INPUT1(t+2)| \end{equation} | (1) |
getInput は,入力ポートの ID と count 値それぞれに対して保持されているので, 以下のように計算することができる.
total = 0.0; for(int i = -2; i <= 2; i++){ ObjectRef inputtmp = getInput(input1ID, count + i); input1 = dereference_cast<int> (inputtmp); total += input1; }
ここで,getInput の中身が ”count + i” となっていることで, 前後2フレーム目までの処理が行える. しかしながらこれではエラーである.
一つ目の理由として,1フレーム目と2フレーム目で count 値が -2, -1 を要求してしまうため, それは存在しないというエラーである. これは単純に
if(count >= 2)
で囲むことで解決できる.
二つ目の理由は, FlowDesigner では,特に要求のない限り, 現在のフレームから前後1フレームしか保持しない仕様になっているため, 2フレーム以上離れたフレームを見ようとすると,それは存在しないというエラーを出すためである. これはコンストラクタに以下のように記述することで, その分のフレーム数の情報をバッファに保持してくれる.
inputsCache[input1ID].lookAhead = 2; inputsCache[input1ID].lookBack = 2;
上記は前後2フレームまでのバッファを確保するための宣言である. 自分の計算にあわせて適切に設定する.
これでフレーム間の計算が行える.
以下に,int 型の入力を, SUM_BACKWARD フレーム前から, SUM_FORWARD フレーム後までの合計を求める
“AccumulationInt” ノードを示す.
内部パラメータとして,上記の lookAhead と lookBack を 変更できるようにしているので, 十分に確保しなければエラーが起こることを確認されたい.
以下に FlowDesigner のノード構築例を示す.
#include <iostream> #include <BufferedNode.h> #include <Buffer.h> using namespace std; using namespace FD; class AccumulationInt; DECLARE_NODE(AccumulationInt); /*Node * * @name AccumulationInt * @category HARKD:Tutorial * @description This block takes a summation over several frames of the input. * * @input_name INPUT1 * @input_type int * @input_description input for an integer * * @output_name OUTPUT1 * @output_type int * @output_description total * * @parameter_name SUM_FORWARD * @parameter_type int * @parameter_value 5 * @parameter_description Forward buffer for summation * * @parameter_name SUM_BACKWARD * @parameter_type int * @parameter_value -5 * @parameter_description Backward buffer for summation * * @parameter_name LOOK_FORWARD * @parameter_type int * @parameter_value 0 * @parameter_description Forward buffer for summation * * @parameter_name LOOK_BACKWARD * @parameter_type int * @parameter_value 0 * @parameter_description Backward buffer for summation * * @parameter_name IN_ORDER * @parameter_type bool * @parameter_value true * @parameter_description inOrder setting * END*/ class AccumulationInt : public BufferedNode { int input1ID; int output1ID; int input1; int sum_forward; int sum_backward; int look_forward; int look_backward; int total; bool in_order; public: AccumulationInt(string nodeName, ParameterSet params) : BufferedNode(nodeName, params) { input1ID = addInput("INPUT1"); output1ID = addOutput("OUTPUT1"); input1 = 0; sum_forward = dereference_cast<int>(parameters.get("SUM_FORWARD")); sum_backward = dereference_cast<int>(parameters.get("SUM_BACKWARD")); look_forward = dereference_cast<int>(parameters.get("LOOK_FORWARD")); look_backward = dereference_cast<int>(parameters.get("LOOK_BACKWARD")); inputsCache[input1ID].lookAhead = look_forward; inputsCache[input1ID].lookBack = look_backward; in_order = dereference_cast<bool> (parameters.get("IN_ORDER")); inOrder = in_order; } void calculate(int output_id, int count, Buffer &out) { total = 0; if(count + sum_backward >= 0){ for(int i = sum_backward; i <= sum_forward; i++){ ObjectRef inputtmp = getInput(input1ID, count + i); input1 = dereference_cast<int> (inputtmp); total += input1; } } cout << "AccumulationInt : [" << count << " , " << input1 << " , " << total << "]" << endl; (*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(total)); } };
毎フレーム計算しない処理( inOrder オプション)
前節とは逆に,本章では,数フレームに一回しか行わない処理をする場合どうなるかを見ていく. 例えば以下のようなソースを考える.
if(count % 3 == 0){ ObjectRef inputtmp = getInput(input1ID, count); input1 = dereference_cast<int> (inputtmp); }
この場合,getInput は count 毎ではなく,3回に1回しか読まれないことになる. FlowDesigner では,仕様として, 前段のノードの処理が,後段の getInput の要求に合わせて処理されるという特徴を持つ.
つまり,上記のようなソースの場合, そのノードは3回に1回しか入力を要求しないので, その前のノードも,要求された時に3回分をまとめて1回の処理として演算する.
例を示す. まず,準備として, 以下のような SubscriberIntWithPeriod ノード を作成する.
#include <iostream> #include <BufferedNode.h> #include <Buffer.h> using namespace std; using namespace FD; class SubscriberIntWithPeriod; DECLARE_NODE(SubscriberIntWithPeriod); /*Node * * @name SubscriberIntWithPeriod * @category HARKD:Tutorial * @description This block inputs an integer and outputs the same number with print with specific period. * * @input_name INPUT1 * @input_type int * @input_description input for an integer * * @output_name OUTPUT1 * @output_type int * @output_description Same as input * * @parameter_name PERIOD * @parameter_type int * @parameter_value 1 * @parameter_description Period of INPUT1 subscription * * @parameter_name IN_ORDER * @parameter_type bool * @parameter_value true * @parameter_description inOrder setting * END*/ class SubscriberIntWithPeriod : public BufferedNode { int input1ID; int output1ID; int input1; int period; bool in_order; public: SubscriberIntWithPeriod(string nodeName, ParameterSet params) : BufferedNode(nodeName, params) { input1ID = addInput("INPUT1"); output1ID = addOutput("OUTPUT1"); input1 = 0; period = dereference_cast<int>(parameters.get("PERIOD")); in_order = dereference_cast<bool> (parameters.get("IN_ORDER")); inOrder = in_order; } void calculate(int output_id, int count, Buffer &out) { // Main loop routine starts here. if(count % period == 0){ ObjectRef inputtmp = getInput(input1ID, count); input1 = dereference_cast<int> (inputtmp); } cout << "Subscribed : [" << count << " , " << input1 << "]" << endl; (*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(input1)); // Main loop routine ends here. } };
SubscriberIntWithPeriod ノードでは 単純に PERIOD で設定した周期で getInput して, その値を表示する.
次に,現在の count 値をそのまま出力する CountOutput ノードを作成する.
#include <iostream> #include <BufferedNode.h> #include <Buffer.h> using namespace std; using namespace FD; class CountOutput; DECLARE_NODE(CountOutput); /*Node * * @name CountOutput * @category HARKD:Tutorial * @description This block outputs the count number * * @output_name OUTPUT1 * @output_type int * @output_description This output the same integer as PARAM1. * * @parameter_name IN_ORDER * @parameter_type bool * @parameter_value true * @parameter_description inOrder setting * END*/ class CountOutput : public BufferedNode { int output1ID; bool in_order; public: CountOutput(string nodeName, ParameterSet params) : BufferedNode(nodeName, params) { output1ID = addOutput("OUTPUT1"); in_order = dereference_cast<bool> (parameters.get("IN_ORDER")); inOrder = in_order; } void calculate(int output_id, int count, Buffer &out) { cout << "CountOut : [" << count << "]" << endl; (*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(count)); } };
二つのノードが完成したら, 以下のように FlowDesigner のネットワークファイルを構築する.
これで,準備完了.
まず,CountOutput ノードと, SubscriberIntWithPeriod ノードの 内部パラメータである IN_ORDER を両方とも ”false” に設定する. (これが FlowDesigner のデフォルト値.) SubscriberIntWithPeriod の PERIOD を3にし, ネットワークファイルを実行. すると,コンソールへの出力は次のようになる.
CountOut : [0] Subscribed : [0 , 0] Subscribed : [1 , 0] Subscribed : [2 , 0] CountOut : [3] Subscribed : [3 , 3] Subscribed : [4 , 3] Subscribed : [5 , 3] CountOut : [6] Subscribed : [6 , 6] Subscribed : [7 , 6] Subscribed : [8 , 6] CountOut : [9] Subscribed : [9 , 9] ...
このように,後段の SubscriberIntWithPeriod が3回に1回しか入力を要求しないので, CountOut は3回に1回しか処理されていないことがわかる.
これでは print デバッグもやりにくく, CountOut の中に微分やカウント演算などの処理が ある場合は正しく処理することができない. (この問題を確認したい場合は実際に CountOut ノードに calculate が一回呼び出されるごとにカウントするカウンタを実装すれば確認可能. 全ての count に対して,そのカウンタは動作しないことが確認できる.)
これを解決するのが ”inOrder” オプションである.
先ほどのネットワークファイルで IN_ORDER を true にする. 実行すると次のような結果が得られる.
CountOut : [0] Subscribed : [0 , 0] Subscribed : [1 , 0] Subscribed : [2 , 0] CountOut : [1] CountOut : [2] CountOut : [3] Subscribed : [3 , 3] Subscribed : [4 , 3] Subscribed : [5 , 3] CountOut : [4] CountOut : [5] CountOut : [6] Subscribed : [6 , 6] Subscribed : [7 , 6] Subscribed : [8 , 6] CountOut : [7] CountOut : [8] CountOut : [9] Subscribed : [9 , 9] ...
このように,CountOut も3回に1回の呼び出しに合わせて ループ処理が正しく3回実行されている.
全てのノードに対して,入力の呼び出しによらず, count 回正しく演算を行いたい場合は,このように,
inOrder = true;
をコンストラクタに必ず入れる.
入力ポートへ入る入力数を自由に変更したい(translateInput)
いきなり例を挙げる. 次のような,3つの int 型の整数を入力に取って,その合計を出力するノードを考える.
#include <iostream> #include <BufferedNode.h> #include <Buffer.h> using namespace std; using namespace FD; class SummationInt; DECLARE_NODE(SummationInt); /*Node * * @name SummationInt * @category HARKD:Tutorial * @description This block outputs INPUT1 + INPUT2 + INTPUT3 * * @input_name INPUT1 * @input_type int * @input_description input for an integer * * @input_name INPUT2 * @input_type int * @input_description input for an integer * * @input_name INPUT3 * @input_type int * @input_description input for an integer * * @output_name OUTPUT1 * @output_type int * @output_description Same as input * END*/ class SummationInt : public BufferedNode { int input1ID; int input2ID; int input3ID; int output1ID; int input1; int input2; int input3; int total; public: SummationInt(string nodeName, ParameterSet params) : BufferedNode(nodeName, params) { input1ID = addInput("INPUT1"); input2ID = addInput("INPUT2"); input3ID = addInput("INPUT3"); output1ID = addOutput("OUTPUT1"); inOrder = true; } void calculate(int output_id, int count, Buffer &out) { // Main loop routine starts here. input1 = 0; input2 = 0; input3 = 0; ObjectRef inputtmp1 = getInput(input1ID, count); input1 = dereference_cast<int> (inputtmp1); ObjectRef inputtmp2 = getInput(input2ID, count); input2 = dereference_cast<int> (inputtmp2); ObjectRef inputtmp3 = getInput(input3ID, count); input3 = dereference_cast<int> (inputtmp3); total = input1 + input2 + input3; cout << "SummationInt : [" << count << " , " << total << "]" << endl; (*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(total)); // Main loop routine ends here. } };
このノードはこれまで説明してきた方法で簡単に作ることができる. ノードが作成できたら,次のようにネットワークファイルを作る.
この場合は 1 + 1 + 1 なので3が出力されるのは当たり前である.
さて,ここで,このノードから3つの変数ではなく,2つの変数の足し算のみをやりたいとする. この場合,一つの入力ポートを開放して二つのポートに整数を入れて次の図のようにすれば演算できそうである. しかし,実行してみると残念ながら入力ポートに全部情報が入っていないということでエラーとなる.
FlowDesigner では,特別な場合を除いて,例えノードの中身の処理の中で使われないデータであっても, getInput で読まれる入力ポートについては全て前段から情報を受け取らなければならない仕様がある. これでは,汎用性がないし,型に合う全ての入力を用意するのは時として面倒な場合がある.
そこで,translateInput という BufferedNode クラスのさらに 親クラスである Node クラスのメンバ関数を使うことで, 入力ポート数をダイナミックに変更できる方法がある. 同じ例を用いて,入力ポート数が可変のノードを作る.
#include <iostream> #include <BufferedNode.h> #include <Buffer.h> using namespace std; using namespace FD; class SummationInt; DECLARE_NODE(SummationInt); /*Node * * @name SummationInt * @category HARKD:Tutorial * @description This block outputs INPUT1 + INPUT2 + INTPUT3 * * @input_name INPUT1 * @input_type int * @input_description input for an integer * * @input_name INPUT2 * @input_type int * @input_description input for an integer * * @input_name INPUT3 * @input_type int * @input_description input for an integer * * @output_name OUTPUT1 * @output_type int * @output_description Same as input * END*/ class SummationInt : public BufferedNode { int input1ID; int input2ID; int input3ID; int output1ID; int input1; int input2; int input3; int total; public: SummationInt(string nodeName, ParameterSet params) : BufferedNode(nodeName, params), input1ID(-1), input2ID(-1), input3ID(-1) { output1ID = addOutput("OUTPUT1"); inOrder = true; } virtual int translateInput(string inputName) { if (inputName == "INPUT1") {return input1ID = addInput(inputName);} else if (inputName == "INPUT2") {return input2ID = addInput(inputName);} else if (inputName == "INPUT3") {return input3ID = addInput(inputName);} else {throw new NodeException(this, inputName+ " is not supported.", __FILE__, __LINE__);} } void calculate(int output_id, int count, Buffer &out) { // Main loop routine starts here. input1 = 0; input2 = 0; input3 = 0; if (input1ID != -1){ ObjectRef inputtmp1 = getInput(input1ID, count); input1 = dereference_cast<int> (inputtmp1); } if (input2ID != -1){ ObjectRef inputtmp2 = getInput(input2ID, count); input2 = dereference_cast<int> (inputtmp2); } if (input3ID != -1){ ObjectRef inputtmp3 = getInput(input3ID, count); input3 = dereference_cast<int> (inputtmp3); } total = input1 + input2 + input3; cout << "SummationInt : [" << count << " , " << total << "]" << endl; (*(outputs[output1ID].buffer))[count] = ObjectRef(Int::alloc(total)); // Main loop routine ends here. } };
変更箇所は以下の通り.
コンストラクタで,入力ポート ID を-1で初期化した.
こうしておくことで,入力ポートが解放されている時に, ID が-1であるよう名前付けしておく.
translateInput を加えた.
この関数が接続されている入力ポートの数だけ呼び出され, 引数として,どのポートが接続されているのかが与えられる. これで,入力ポートが接続されていれば,本当の入力ポートの ID が取得できる.
calculate 関数の中で ID が-1以外の時に入力を読み込むように処理.
これで,入力ポートの開放,未開放に合わせた処理が実行できる.
このソースをコンパイルして FlowDesigner で使用すると, 入力の数によらず,正しく演算できていることがわかる.
See Also
ソースコンパイルからのインストール
HARK講習会資料「HARKのインストール」参照
基本的なノードの作り方
HARK講習会資料「ノードの作成」参照