MATLAB: 時間のかかる計算処理結果を保存しておき、次回からは保存データを読み込むか再計算するか尋ねるようなワークフローを実現する関数

MATLABでデータ解析をする場合、大雑把に言って次のような手順を踏むのは、一般的な方針だと思う。

1. パラメータの設定
2. データの読み込み
3. 時間のかかる計算処理
4. 上記計算結果に基づいた図の描画あるいは統計検定

このとき、3.の時間のかかる計算処理は定義上当たり前だが時間がかかるので、できれば計算結果を保存しておき、それを読み込むことで次回からの処理時間を短縮するのが有益である。特に論文用のデータ解析のような場合、何度も何度も修正を加えてやり直すのが常なので、2. で**読み込むデータに変更が生じた時**、あるいは3.の**計算処理の中身を変えたい場合のみ、再計算を選ぶ**ようにしたいところである。

プログラムからすれば、その都度ユーザー（私）が再計算を求めているか、保存データの読み込みを希望しているかは、あらかじめ知ることが出来ないので、インタラクティブな設計にする必要があると考えた。

きわめて単純な話であるが、このような作業をどのようにMATLABで実現するかという話は公式のヘルプにもあまり出てこないし、他でも目にしたことがない。そこでこの春でMATLAB使用歴もついに5年を迎えてしまう私が、試行錯誤の末にようやくたどり着いた解を共有しようと思う。

# 1. inputによる方法

私が最初に採用したのは、内臓の `input()` 関数を利用して、ランタイムでユーザーからの入力によって、読み込みか再計算かを選択する方法だった。このために `inputYN()`という関数を書いた。これはこれで堅牢でなかなかよいのだが、いちいちキーボードで `y` や `n` を探して打つのが意外と手間だということと、タイマー機能がないためにユーザー入力が無ければスクリプトの実行がそこで止まったままになってしまうという問題があった。なのでスクリプトを放ったらかしにしてパンを買いに行くというようなことがやりにくい。

### 使用例

```matlab
filename = fullfile(resdir,'data1.mat');
if ~exist(filename,'file') || inputYN('Do you want to recompute A?')

    % ここで任意の計算処理を行う
    A = rand(10);
    B = A^2;

    save(filename,'A','B'); % 計算結果の変数を保存する

else
    load(filename)
end
```

`~exist(filename,'file') `でファイル`filename`が存在するか確認し、存在しない場合は有無を言わさず計算を実行する。存在する場合は、コマンドウィンドウにメッセージが表示され（下図）、キーボード入力が促される。`y`は`true`を返し再計算, `n`は`false`を返し保存データの読み込みを行う。

![screenshot-1.png](https://qiita-image-store.s3.amazonaws.com/0/109898/72e0bf35-f9c8-516e-88d4-4bdd135eeb66.png)




### 関数
```matlab
function TF = inputYN(inputstr)
% inputYN is a wrapper of input. Ask question with Y or N for the answer to
% get true or false output, respectively. To interrupt, if you type
% 'keyboard' in response you can enter debugging mode.
%
%   TF = inputYN(inputstr)
%
%  inputstr     String for a question. ' (Y/N):' will be added at the end.
%
%  TF           A logical output. true is for Y or y, false is for N or n
%
%
% EXAMPLE
% if inputYN('Do yo want to proceed?')
%     dsip('OK')
% else
%     dsip('cancelled')
% end
%
% % the above code prints the following in the command window
% Do yo want to proceed? (Y/N):
%
% See also
% input, questdlgtimeout
%
% Written by
% Kouichi.C.Nakamura, Ph.D.
% Kyoto University
% kouichi.c.nakamura@gmail.com
% 16 Nov 2015

if ispc
    stopstr = 'Ctrl + C';
else
    stopstr = 'Cmd + .(period)';
end

while 1
    strResponse = input(sprintf('%s [Y/N] (%s to abort):',inputstr,stopstr),'s');
    if strcmpi('Y', strResponse)
        TF = true;
        break;
    elseif strcmpi('N', strResponse)
        TF = false;
        break;
    elseif strcmp('keyboard', strResponse)
        keyboard
        break;
    end
end


end
```
## 2. qestdlg とtimerによる方法
2つ目のアプローチは、`timer`オブジェクトを使って、一定時間経過後に自動的に処理を行うという機能を加えたもの。なのでスクリプトを放ったらかしにしてパンを買いに行くというようなことがやりやすくなる。

雛形となったのは、MathWorks社のチームによる[`timeoutDlg`という関数](https://uk.mathworks.com/matlabcentral/answers/96229-how-can-i-have-a-dialog-box-or-user-prompt-with-a-time-out-period
)である。 これと同じ原理を内蔵の `questdlg()`に適用して、`questdlgtimeout()`という関数を書いた。これは低レベルの関数で、そのままでは使いにくいので、さらに`questdlgtimeoutYN()`というwrapper（包み込み）関数を書いた。

`inputYN()`と比べると、デフォルト設定をあらかじめ決めておき、Returnキーでそれを選択することが出来て便利であるし、その場にいなくても一定時間経過後にデフォルトの選択肢を選んで進むので張り付いていなくてもよいのが利点である。また、Cancelボタンによって実行を強制的に停止することができる点も優れている。

### 限界
しかしながら、この関数の実装は完璧ではなく、内臓の`questdlg()`がダイアログのfigure objectを返さないことが原因で、推測に頼ってダイアログのfigureを同定しているので、場合によっては正常に動作しないことがある。たとえば、Live Scriptで動作していて、多数のグラフの描画が直前にあって、そのためにダイアログウィンドウの描画が遅れたような場合に（MATLAB起動後の最初の実行でも似たような状況に陥ることがある）、タイマーが動作したときにはもうダイアログが閉じているような事態が発生することがあり、その場合エラーとなって止まってしまう。これは`questdlg()`の内部をいじって、タイマー機能を内蔵するか、figure objectを返すように変更しないと根本的には解決できない問題で、MathWorksには機能改善の要望を出してある。


### 使用例
```matlab
filename = fullfile(resdir,'data1.mat');
if ~exist(filename,'file') || questdlgtimeoutYN(10,'Do you want to recompute A and B?','No')
% 6秒で自動的にNoを選ぶ

    % ここで任意の計算処理を行う
    A = rand(10);
    B = A^2;

    save(filename,'A','B'); % 計算結果の変数を保存する

else
    load(filename)
end
```
`~exist(filename,'file') `でファイル`filename`が存在するか確認し、存在しない場合は有無を言わさず計算を実行する。存在する場合は、ダイアログウィンドウが表示され（下図）、Yes, No, Cancelのボタンを選ぶ。Yesは`true`, Noは`false`を返し、Cancelはエラーを出してプログラムが停止する。**デフォルト設定のボタン**があらかじめ選択されていることで、Returnキーを押すだけでこれを選ぶことができる。一定時間が経過してもボタンが押されない場合は、デフォルト設定のボタンを選んで自動的に先へ進む（下の図の例ではNoが10秒後に選ばれ`false`が返される）。

![screenshot-2.png](https://qiita-image-store.s3.amazonaws.com/0/109898/87705a22-f416-c123-f2f6-9af0e404b39e.png)


### 関数
```matlab
function tf = questdlgtimeoutYN(delay,qstring,varargin)
% A wrapper of questdlgtimeout with Yes, No, anc Cancel buttons with time
% out option. Pressing Yes and No returns true and false, respectively,
% whereas choosing Cancel issues an error to force the program abort.
%
% tf = questdlgtimeoutYN(delay,qstring)
% tf = questdlgtimeoutYN(delay,qstring,default)
%
% INPUT ARGUMENTS
% delay       Scalar real positive number
%             Delay time for time out in second.
%
%
% qsting      Char row
%             Query text
%
% default     'No' (default) | 'Yes' | 'Cancel'
%
% OUTPUT ARGUMENTS
% tf          true | false
%             true for 'Yes', false for 'No'. An error for 'Cancel'
%
% EXAMPLE
%
% filename = fullfile(resdir,'data1.mat');
% if exist(file,'file') ||...
%         questdlgtimeoutYN(6,'Do you want to recompute A and B?','No')
%
%     % code for computation comes here
%     A = rand(10);
%     B = mean(A);
%
%     save(filename,'A','B') % recommend to use savevar(filename,A,B)
%
% else
%     load(filename)
%     fprintf('%s loaded\n',filename)
% end
%
%
% See also
% questdlgtimeout, questdlg, updatefileorload, inputYN
%
% Written by Kouichi C. Nakamura Ph.D.
% MRC Brain Network Dynamics Unit
% University of Oxford
% kouichi.c.nakamura@gmail.com
% 15-Apr-2017 19:43:46


narginchk(2,3);
p = inputParser;
p.addRequired('delay',@(x) isscalar(x));
p.addRequired('qstring',@(x) ischar(x));
p.addOptional('default','No',@(x) ismember(x,{'Yes','No','Cancel'}));
p.parse(delay,qstring,varargin{:});

default = p.Results.default;

title = sprintf('Close in %d seconds',delay);

button = questdlgtimeout(delay,qstring,title,default);

switch lower(button)
    case 'yes'
        tf = true;
    case 'no'
        tf = false;
    case 'cancel'
        error('User chose "Cancel".')
    otherwise
        error('Unexpected.')
end

end
```


```matlab
function button = questdlgtimeout(delay, varargin)
% questdlg function with timeout property. For more simpler syntax see
% questdlgtimeoutYN
%
% Based on timeoutDlg by MathWorks Support Team
% https://uk.mathworks.com/matlabcentral/answers/96229-how-can-i-have-a-dialog-box-or-user-prompt-with-a-time-out-period
%
% button = questdlgtimeout(delay,'qstring')
% button = questdlgtimeout(delay,qstring,title)
% button = questdlgtimeout(delay,qstring,title,default)
% button = questdlgtimeout(delay,qstring,title,str1,str2,default)
% button = questdlgtimeout(delay,qstring,title,str1,str2,str3,default)
% button = questdlgtimeout(delay,qstring,title, ..., options)
%
% INPUT ARGUMENTS
% delay       Duration in second during withich the dialog is maintained
%
% var1,var2,...
%             Accepts input arguments for builtin questdlg. See
%             documentation of questdlg
%
% OUTPUT ARGUMENTS
% button       The dialog has three default buttons, Yes, No, and Cancel.
%              If the user presses one of these three buttons, button is
%              set to the name of the button pressed. If the user presses
%              the close button on the dialog without making a choice,
%              button returns as an empty character vector (''). If the
%              user presses the Return key, button returns with a value of
%              'Yes'.
%
%              If you provide default or options, button will be the
%              default value.
%
% EXAMPLE 1
%
%   if strcmpi(questdlgtimeout(6,'Do you want to recompute A?','','No'),'yes')
%      A = rand(10);
%   end
%
% EXAMPLE 2
%
%   filename = fullfile(resdir,'filename1.mat');
%   if exist(filename,'file')
%       btn = lower(questdlgtimeout(10,'Do you want to recompute A and B?','','No'));
%   else
%       btn = 'yes';
%   end
%
%   switch btn
%       case 'yes'
%
%           % code for computation comes here
%           A = rand(10);
%           B = mean(A);
%
%           savevar(filename,A,B) % recommend to use savevar(filename,A)
%       case 'no'
%           load(filename)
%           fprintf('%s loaded\n',filename)
%       otherwise
%           error('Canceled')
%   end
%
% LIMITATION
% Although this works prety well, calling this function from Live Script
% with lots of figures tend to result in an error. Erro occurs because
% while dialog is shown, a couple of invisible cached figures are created
% and the identity of handle for the figure object of the dialog will be
% lost.
%
% See also
% questdlg, timer, questdlgtimeout_demo, savevar, updatefileorload, inputYN
%
% Written by Kouichi C. Nakamura Ph.D.
% MRC Brain Network Dynamics Unit
% University of Oxford
% kouichi.c.nakamura@gmail.com
% 08-Mar-2017 16:06:58

f1 = findall(groot, 'Type', 'figure');
t = timer('TimerFcn', {@closeit f1}, 'StartDelay', delay);
start(t);

dlg = @questdlg;

% Call the dialog
button = dlg(varargin{:});
%NOTE Sometimes error occurs at line 400 of questdlg
%
% Error using uicontrol
% Cannot set property to a deleted object
%
% Error in questdlg (line 400)
%     uicontrol(BtnHandle{DefaultButton});
%
% Error in questdlgtimeout (line 86)
% button = dlg(varargin{:});


if isempty(button)
    if length(varargin) >= 3
        if  isstruct(varargin{end})
            button = varargin{end}.Default;
        elseif ischar(varargin{end})

            button = varargin{end};

        else
            error('unexpected syntax')
        end
    else % no default provided
        % leave button empty
    end
end


% Delete the timer
if strcmp(t.Running, 'on')
    stop(t);
end
delete(t);
end

%--------------------------------------------------------------------------

function closeit(src, event, f1) %#ok<INUSL>
disp('Time out');

h = findall(groot, 'WindowStyle', 'modal');
close(h(1));
end
```

### まとめ
`inputYN()` も `questdlgtimeoutYN()` もまだ完璧とは言えない。しかし、これらを取り入れることで、オリジナルのデータから最終産物の図や統計検定までを単一のLiveScriptファイルで扱いつつ、時間のかかる計算処理については基本的に保存済みの計算結果を読み込むことで大幅に時間の節約となり、「ボタン一つでオリジナル・データから最終産物まで」、という理想に近いワークフローをようやくのこと実現することが出来た。

このようなプログラム工学的な話題というのは、どうも私の身の回りの神経科学者を見る限り、軽視されているか、知識が共有されていないためか、有益な話を聞く機会がとても少ないと感じている。5年前にMATLABによるデータ解析を始めた時点でこのような知識を得ていたらよほど効率的に「パイプライン」の構築ができたのではないかと思う次第である。他の方のご意見もぜひ伺いたいたいと思っており、思うところの有る方はコメントをよろしくお願いします。