DISLIN解説②

 解説①に引き続き、本稿ではまだるっこしいことはどうでもいいからグラフを描いてみよう、という趣旨の解説を行う。ちなみに③の予定はない。私がDISLINに愛想を尽かし、使うことを諦めてしまったからだ。
 というわけで、ありがちなsin、cos関数をプロットしてみることにする。次のサンプルコードは、タイトルや軸のラベルなどややこしいものは全部排除して、最小限の関数呼び出しのみでグラフを描くものである。DISLIN公式のExpample1を元にしている。

int main()
{
    int n = 100, i, ic;
    double fpi = 3.1415926 / 180.0;
    double step = 360. / (n - 1);
    double xray[100], y1ray[100], y2ray[100];

    for (i = 0; i < n; i++)
    {
        xray[i] = i * step;
        double x = xray[i] * fpi;
        y1ray[i] = sin(x);
        y2ray[i] = cos(x);
    }

    Dislin g;
    g.metafl("cons");
    g.disini();

    g.graf(0.0, 360.0, 0.0, 90.0, -1.0, 1.0, -1.0, 0.5);

    g.curve(xray, y1ray, n);
    g.curve(xray, y2ray, n);
    g.disfin();

    return 0;
}

 これを実行すると、何かウィンドウが現れ、次の画像のようなものが表示されるはずだ。 f:id:thayakawa:20191012225049p:plain:w500

 背景が黒い!フォントが汚い!曲線がどちらも白色で区別できない!軸のラベルは!?タイトルは!?tics少なくね!?
 安心してほしい。修正可能だ。まずは上のコードについて順を追って説明していこう。

Dislin g
 単なる宣言である。流石に誰も躓くまい。

g.metafl("cons")
 恐ろしいことに早速意味不明な関数が登場してしまった。いやいや、何のことはない、これは単にグラフの出力先を指定しているだけである(関数名はmetafileの意味だと思われる)。"cons"や"xwin"、"gl"などを与えるとさきほど同じようにウィンドウにグラフを表示する。他にも"png"や"pdf"、"eps"などを指定すればその形式の画像ファイルを生成してくれる。その他の対応形式はオンラインマニュアルの6.1.4を参照。
 なお後述するsetfil関数でファイル名を指定しなかった場合、作成されるファイルはdislin.(拡張子)となる。

g.disini()
 初期化関数。前記事を思い出してほしいのだが、この関数を呼び出すとdislinが初期化され、levelが0から1へと移行し、改めてグラフやキャンバスの設定を行っていく段階に入る。コンストラクタみたいなものだ。

g.graf(double xmin, double xmax, double xor, double xstep, double ymin, double ymax, double yor, double ystep)
 これは“XY軸の2次元グラフを描画する”と意思表示し、その軸の範囲などを指定するための関数である。xmin、xmaxなどは軸の上下限、xorは軸の目盛り数値の最初の値で、ここからxstep刻みに数値が表示される。y軸についても同様である。この関数を呼び出すとlevelが2となり、各曲線や散布図などの設定を行う段階となる。
 なお、これ以外にもgrafp、grafrなどの関数があるが、これらはそれぞれ極座標、スミスチャートを描くためのもの。また3次元ないしカラーバー付きグラフを描きたいときなどはまた別の関数が必要である。
 また、グラフのstepやら目盛りやらを全部指定するのが大変だ、という時のために、自動計算する方法がいくらか用意されている。gaxparなどがそれに相当する。とは言っても、あらゆる状況で全パラメータを自動計算してくれるほど便利なものではないらしい。

g.curve(const double* xlist, const double* ylist, int ndata)
 これが曲線のデータを与えている関数である。引数にはそれぞれ、x座標リスト、y座標リスト、データ点の数を与える。curveという関数名ではあるが、これはXY平面上の任意の点をリストで与えるだけのもので、デフォルトでは曲線として描画されるものの、散布図にしたり、点の形状を指定したりすることもできる。その指定方法はincmrkおよびmarker関数を参照。

g.disfin()
 Dislinを終了する。おそらくこの関数によってグラフが出力される。同時に、Dislinは未初期化のlevel0へと移行する。

 さて、これがDislinでグラフを書くための最小限の関数呼び出しである。しかしこのグラフは見栄えとしても酷いので、次の定型文的なコードを追加しよう。いっそこのあたりは、Dislinを継承しコンストラクタなどで自動設定してしまってもいいかもしれない。

g.scrmod("revers")
 level0の段階で呼び出す。scrmodはbackground、foregroundの色を設定する関数で、"revers"は背景を白に、前景を黒にする。デフォルトの逆の色設定なので、多分reverseの意味なんだろう。

g.imgfmr("rgb")
 level0の段階で呼び出す。metaflで指定した出力先がvirt、tiffpngbmp、imageである場合、この指定を行っておかないと8bit色で出力されてしまう。それ以外のフォーマットである場合は指定しなくても問題ないか効果がない。

g.setfil("filename")
 ファイルに出力する場合、その名前を指定する。

g.hwfont() g.ttfont("arial.ttf") g.bmpfnt("COMPLX")、g.helve()他、g.shdchar()
 フォントに関する設定。level1-3いずれかで呼び出す。
 DISLINでフォントを設定する方法はいくらかあって、出力先に応じて指定を変える必要がある。
 ttfontはTrueTypeフォントを直接指定するものだ。引数はttfファイルを直接指定しなければならない。ただし、色々試したもののどうも輪郭線がベタ塗りっぽく潰れてしまい、汚らしい文字にしかならなかった。
 hwfontはハードウェアフォントに設定するもの。xwinまたはconsに出力する場合は随一の美しさとなるが、フォント自体を選択できないのが悩みどころ。
 bmpfntはDISLINが持っているビットマップフォントを表示するためのもの。ラスター画像で文字が潰れてしまうのを避けたい場合に使う。ただしDISLINのパスが正しく通っていないとフォントを見つけてくれないので注意(パスの設定はインストールフォルダのreadmeを参照)。
 psfontはベクター画像用のフォント設定関数で、PDFやPostScriptを使うつもりならこれで設定すればよい。十分に美しい。
 他にも、winfnt(WMFファイルまたはWindowsディスプレイ)、x11fnt(x11ディスプレイ)と、出力先によっても個別にフォント設定関数が用意されている。
 最後に、shdcharはフォントの塗りつぶしである。shaded fontの場合これを呼んでおかないと、フォントは輪郭のみが表示され白抜きになる。

g.color("red")
 曲線の色を指定している。といっても、“曲線の”と理解するのは望ましくない。正確に言えばforegroundの色を指定している。
 DISLINはforegroundとbackgroundの2色のパレットがあり、上述のscrmodもその組み合わせを指定したものだ。グラフの軸や曲線、文字などは基本的にこのforegroundを参照し、その色で描画される。しかし現実にはもちろん、曲線や点ごとに色を変えたいという要求は自然に生じるため、この関数で逐一foregroundを変更しながらプロットするのである。
 サンプルのようにcurveなどの関数を呼ぶ直前に指定しておけば問題ないし、ある特定のcurveやtitleのみ色を変えたいのであれば、

g.color("red");
g.curve(x, y, n);
g.color("fore");

 とすればよい。color("fore")を呼ぶことで、foregroundはscrmodで指定した初期値にリセットされる。  色のリストは6.3.1を参照。また色の名前ではなくRGBで与えたい、カラーテーブルから番号で与えたい、という場合、それぞれsetrgb、setclrという関数が用意されている。同じく6.3.1参照。

g.incmrk(5)
 これは挙動の理解にだいぶ悩んだのだが、どうやらcurve関数に与えたデータ点を線で結びつつ、点5つごとにシンボルを表示せよ、という意味であるらしい。引数を0にすればシンボルが表示されなくなり、負の数(-n)にすれば線が表示されなくなった上でn点ごとにシンボルが表示される。つまりこの関数で、表示を点にするか、シンボルにするか、線にするか、などを指定するわけである。

g.marker(2)
 シンボルの形状を指定する。例えば2を与えれば三角形になり、20なら塗りつぶされた逆三角形となる。シンボルの形状一覧はDISLINのexamplesに載っている。
 ここで指定したmarkerの形状は、ここでは2と20を指定しているが、いちいち指定しなくてもcurve関数などを呼ぶごとに自動で切り替えていってくれる。適用されるシンボルの番号が1ずつ増えていくのだ。なので、きちんと全曲線にシンボルを指定したい場合はcurveを呼ぶ前に毎回markerを呼べばよいし、形を気にしないのなら呼ばなくてもよい。

g.hsymbl(18)
 シンボルの大きさを指定している。

 ここまでの挙動を理解できれば想像がつくかとは思うが、DISLINは“n番目の曲線や点集団に対して色やシンボル、大きさなどを指定する”というような方法を用いない。基本的には“色やシンボル、大きさなどの現在の設定”がおそらくグローバル変数か何かに格納されており、curveなどの関数が呼ばれたときは“現時点での設定”を参照してプロットするのだ。不可思議な構造である。6文字以内に制限された関数名といい、何というか、全体的にとてもロースペックな環境で動かすことを想定しているかのような、何十年前のライブラリなんだろうと首を傾げてしまうような設計になっている気がする。最初のリリースが1987年とあるので、それなりに古いライブラリには違いないのだが。
 しかしここまでのコードを見ても分かるように、満足のいく図にしたい場合の記述コストはとても大きい。C++を使っているのなら、自分好みの設定を自動的に施してくれるようなラッパーでも作っておくとよいだろう。

DISLIN解説①

 最近、C++でのグラフ描画のためにDISLIN(マックス・プランク研究所開発)を試していた私だが、C++からグラフを描くには多くの場合Gnuplotが選択されるようで、そうでない場合もROOT(CERN開発の統計解析ライブラリ)やせいぜいPLplot(多言語対応のグラフ描画ライブラリ)あたりが使われる。DISLINはどうやらほとんど普及していないようだ。
 私はGnuplotを長らく利用してきたからこそその混沌とした仕様にうんざりしているし、素粒子宇宙系に属しておりしばしば強制的に利用させられるからこそROOTなんぞゴミだとしか思っていないし、PLplotも依存関係が多く出来は半端だ。導入が簡単で依存ライブラリもなく機能もよく纏められているDISLINはC++プログラマにとっては一つの選択肢だと思っている。ただ致命的な問題があって、ラスター画像への出力が恐ろしく不得手であり、とても汚らしくなってしまう。これについてはもう、ベクター画像中心に使うのが正しい選択だと思っている(実はベクター画像にも多少問題があるのだが)。
 私はとうとうDISLINを使うのを諦めてしまったが(今はGnuplotをラップした自作ライブラリを使っている)、しかしツールとしての簡潔さと機能の豊富さには大きな魅力もあるし、使ってみたいと思う人が出てくるかもしれない。折角なのでこれを試す間に理解できたことなどをちょっとばかり纏めておこうかと思う。

1.1. ライセンスと環境について

 DISLINのライセンスは非商用と商用とがあり、非商用ライセンスはフリーで、販売を行わない個人または研究機関での用途であれば自由に利用してもよい。商用は費用が設定されており、販売などを目的とする場合はいくらか必要である。GPLとかではないので、非商用目的なら特に何も気にしなくていいはず。  DISLINは多様な言語(C/C++FortranPerlPythonJavaRuby、Tcl)とプラットフォームに対応している。私はVisual Studio 2017からC++で利用しているのでそれを想定して解説するが、他の言語でも理屈は同じである。

1.2. インストール

 Windows 64bit Visual Studio用は公式サイトのDownloads->Distributions->Windows 64-bit->dl_11_vc.zip。解凍後、setup.exeを実行してインストールすれば良い。ビルド済みの状態で配布されているので、面倒な作業は特になにもない。

1.3. 大まかなDISLINのルーチン

 DISLINのルーチンにはlevel0~3の段階があり、各段階でどのような設定を行うのかが厳密に定められている。DISLINのオンラインマニュアルには関数名の横にlevel1,2,3みたいな表示があるが、これはこの関数を呼び出すことのできるlevelを示している。
 各段階では大体次のような設定を行いつつ、必要な設定を終えたら特定の関数を呼び次の段階へ移動、または前段階へ戻る。

  • level0 ... まだ初期化されていない最初の段階。出力時のページフォーマット、ファイルフォーマットやファイル名など、初期化に必要なパラメータを与える。必要なパラメータを与えたらdisinit関数を呼ぶことで初期化し、level1に移行する。
  • level1 ... level1でないと呼べない関数というのは多くないが、境界線やページ塗りつぶし、各軸のラベルなどおおよそキャンバス全体の設定を行う箇所。grafなどの関数を呼ぶことで“どのような座標系でグラフを描くか”を明示し、level2または3に移行する。
  • level2 ... 2次元グラフの形状(線や点、シンボルなど)と具体的なデータ点などを与えるところ。普通の2次元グラフであればこのlevelまでで完結する。
  • level3 ... 3次元ないしカラーバー付きグラフを扱う段階。level1から直接こちらへ移行する。3D surfaceやカラーマップや色付き散布図、色付きvector fieldなどもここで設定する。共存可能なものなら、2次元グラフの関数も呼び出せるらしい。

 こんな面倒くさい設定したくない!って人のために、DISLINは簡略化された機能も用意している。詳しくはオンラインマニュアルのChapter 16: Quick Plotsを参照されたい。面倒な設定をすっ飛ばして、単にデータ点を与えるだけで描画してくれる便利機能の一覧である(たぶん)。

1.4. DISLINの利点、欠点

 DISLINはC++から利用できる数少ないグラフ描画ライブラリである。導入の手軽さは素晴らしいし機能も豊富で、他のライブラリにできてDISLINにできないことはほとんどないだろう。また英語のみだがドキュメントはきちんと纏められており、設定の組み合わせによる影響なども細かく触れてくれているため、隠れた仕様で悩むこともそうそうないだろう。
 ただし上述のように、ラスター画像がとても汚いという欠点がある。DISLINは線の描画がいい加減で、デフォルトではアンチエイリアスさえかかっていない。ラスター画像へのアンチエイリアス機能自体は存在するが、はっきり言って“ないよりはマシ”程度にしか改善しない。フォントは様々なものが用意されているけれども描画方法に問題があるためか輪郭が潰れてしまうので、Gnuplotのような美しい文字を表示するのは諦めたほうがよい。ベクター画像であればこうした劣化は起きないので気にすることはないが、しかしベクター画像ではAlpha Blendingを行えないので、半透明色を重ねていくような表現はできなくなる。
 また機能そのものはよくまとまっているものの、設計がどうにも古めかしく、オブジェクト指向が体に染み付いているC++プログラマから見て直感的とは言い難い。パレットが2色のペイントツールで曲線を一本一本描いていくかのように命令するのだ。
 総じて、見た目や使いやすさよりもとにかく“意図したようなグラフを描けるかどうか”を重視する場合は良い選択肢になるだろう。挙動の理解は難しくなく、理解してしまえば大体のことはできる。ただ出力される画像のクォリティには文句言うな。

 解説2へ。

[C++]std::functionに与える関数はcopy-constructibleでなければならない。

 std::functionは関数ポインタも関数オブジェクトもメンバ関数ポインタもまとめて管理することのできるとても便利な機能である。std::functionは"引数"と"戻り値"の型のみ指定されていればよく、それ以上は何も要求しないので、引数と戻り値の等しい関数オブジェクトや関数ポインタなどを纏めて配列にしてしまうなど、柔軟に使うことができる。与えた関数のインライン展開が難しく、関数実行時のオーバーヘッドが生じるデメリットはあるが、億単位の関数呼び出しが発生しない限りは無視して良いデメリットだ。

 ただし、std::functionは万能ではない。特に致命的な欠点はnon-copyableな関数を与えることが出来ない点である。関数がnon-copyableという状況自体が稀ではあるが、関数オブジェクトならあり得る。例えば次のように、non-copyableな変数をキャプチャしたラムダ式などだ。

#include <functional>

struct NonCopyable
{
    NonCopyable() {}
    NonCopyable(const NonCopyable&) = delete;
    NonCopyable(NonCopyable&&) = default;
};

int main()
{
    auto lambda = [t = NonCopyable()]() mutable {};
    std::function<void(void)> f(std::move(lambda));//compile error
    return 0;
}

 ラムダ式はキャプチャした変数をメンバ変数として保有する。今回のlambdaで言えば、non-copyableなNonCopyableクラスのインスタンス保有していることになる。それに伴い、lambda自体もnon-copyableとなる。すると、std::functionにlambdaを与えようとするもstd::functionは削除済みのNonCopyableのコピーコンストラクタを呼び出そうとしてコンパイルエラーになってしまうのだ。std::functionにnon-copyableな関数オブジェクトを与えると、このようにコンパイルそのものが通らなくなってしまう。

 これを解決する方法はいくつかある。

1. lambdaの定義を工夫してcopy-constructibleな形にする。

 例えばstd::shared_ptrに押し込んでしまう方法がある。std::shared_ptr\<NonCopyable>をコピーしても、その中のNonCopyableインスタンス自体は複製されず、それぞれの間で値が共有される。したがってNonCopyable自体のコピーコンストラクタが呼ばれることはないため、これは意図したように動作する。手間のかからない解決方法ではあるが、余計なメモリ確保のコストが発生し気持ちが悪いので私はやらない。

auto lambda = [t = std::make_shared<NonCopyable>()]() {};

2. copy-constructibleなオブジェクトに押し込む。

 lambdaそれ自体は変更せず、その関数オブジェクトを束縛しておくcopy-constructibleな関数オブジェクトを用意する方法。CopyableFunctionはコピーコンストラクタが呼ばれると例外を発生させるため、std::functionをコピーすることは不可能になるが、そもそもnon-copyableな関数オブジェクトを束縛しておきながらそれをコピーしなければならない状況などあるはずがない。あるとしたら設計者の脳味噌が腐っている場合だけである。 コピペすればよいCopyableFunctionの定義部分を除いてもコーディングのコストは1.と大差ないが、関数定義を変更できない場合などは有用かもしれない。

template <class Func>
struct CopyableFunction
{
    CopyableFunction(Func f)
        : func(std::move(f))
    {}
    CopyableFunction(const CopyableFunction&)
        : func(ThrowException())
    {}
    CopyableFunction(CopyableFunction&&) = default;
    Func ThrowException() { throw std::exception(); }

    template <class ...Args>
    decltype(auto) operator()(Args&& ...args) const { return func(std::forward<Args>(args)...); }
    template <class ...Args>
    decltype(auto) operator()(Args&& ...args) { return func(std::forward<Args>(args)...); }
    Func func;
};
template <class Func>
CopyableFunction<Func> MakeCopyableFunction(Func func) { return CopyableFunction<Func>(std::move(func)); }

int main()
{
    auto lambda = [t = NonCopyable()]() mutable {};
    std::function<void(void)> f(MakeCopyableFunction(std::move(lambda)));
    f();
    return 0;
}

 私はこのstd::functionの仕様に苦しめられ、ムーブセマンティクス対応のThreadPoolの設計にとても手間取った。標準ライブラリにも間抜けな機能はそれなりに多いらしい。