▲UNDOでエラーが出ている.UNDOBASE::CommandStartで (!UndoCurptr->topUndo)のエラーが出ている.このエラーはリリースモードでは発生しない.⇒_ASSERT_NEVERはデバッグ専用のアサーションだ.⇒UNDOBASE::MakeNewCommandでは出口で明示的にtopUndoにNULLを代入している.その後実行されるBackupPointDataでバックアップが実行された場合にのみ値が入る.UPDATETBLでは更新されるカードデータがバックアップされるが,インポートの場合にはオブジェクトが不在の場合があり,このようなときにはtopUndoは空のままになる.⇒オブジェクトが空の場合は停止しないようにした.
▲VS2017の開発環境でゼルコバの木を起動してほとんど止まってしまう.リリースモードでは動いていたのだが,どこか壊してしまったのかと思って少し古いパッケージに戻って試してみたところ,リリースモードでも立ち上がらなくなってしまった.リリースモードでは起動時に表示しているスプラッシュウィンドウのところでフリーズしてしまう.デバッグモードの場合はほとんどどこでハングしているかわからない状態だ.ブレークを掛けてみるとほとんどつねにコンストラクタのマクロの中で止まっている.昨日まではほとんど問題なく動作していたように思われるのだが…まったく心当たりがないので,とりあえずWindows 11をダウングレードしてWindows 10に戻してみた.
ダウングレードはWindows 11にアップグレードしてから10日以内に行わないと手遅れになる.アプリケーションの再インストールなどは必要ない.VS2017を起動して,ゼルコバの木を実行してみる.⇒何事もなかったかのようにあっさり立ち上がってきた.リリースモードでもデバッグモードでもまったく問題なく走っている.Windows 11では何かひどいデグレード(劣化)が内部で起きているように思われる.リフォームの志向する方向(ポリシー)はそれほど悪くないので,残念だ.
バックアップから元の仕掛り版に戻して実行しようとしたところ,障害が再発してしまった.プログラムのコードに何か致命的な不良があるのだろうか?⇒リリース版は立ち上がってくる.デバッグモードではコマンドライン引数で起動するようになっているので,リリースモードと同じノーマルモードで起動するようにしてみた.⇒これなら,リリースモードと同等に実行できる.ADLのインポート処理に何か問題が起きているのだろうか?⇒UNDOを復活させたのが過負荷になっているのではないか?⇒どうも,そういうことのようだ.ということは,Windows 11でも動作していたということだろうか?
確かに,それは言えるかもしれないが,「遅くなっている」というのは事実なのではないか?単純なファイルコピーでもWindows 10より遅くなったという印象がある.そのため,重いサンプルを読み込むときには,それが隠しきれなくなるのではないか?少なくともリリースモードでは(UNDOが入っていたとしても)開けないとおかしい.⇒リリースモードでチェックしているとき,スプラッシュウィンドウが開いたままになっていたという事象があったが,開発環境ではスプラッシュウィンドウは表示されないのではなかったろうか?⇒確かに,それはおかしいね.何か勘違いしていたかな?しかし,通常,スプラッシュウィンドウは2, 3秒で閉じることになっているのに,開きっぱなしというのはやはり動作不良と見るしかない.
いずれにしても,動作するようになったので作業を再開できる.UNDOが入るとほとんど止まってしまうという問題は別途考えることにしよう.というか,インポートはUNDOの対象外なのではないか?ファイルのオープンではUNDOのバックアップを取っていない.原理的にはそれと同じはずだ.インポートは一覧表のレコードの更新機能を使って実現している.レコード更新はUNDOの対象なので,それに合わせているが,これは廃止すべきではないか?⇒それが妥当だと思う.インポートでUNDOしないことにすれば,UNDOを復活させても支障ないはずだ.
インポートはSendUpdateDataという関数を反復呼び出すことで実現している.この関数ではインポートから呼び出されているのか,そうでないのかを判別できない.インポートでは処理を開始する前に,Z.mImportStartという通知を送り,Z.mImportEndで閉じるようになっている.これで状態を判別できるのではないか?LINKTABLE::ImportStartでは,ImportFileNameにファイル名をコピーしているだけで,フラグは立てていない.⇒ImportFlagというフラグを作っておこう.⇒これでようやく,懸案の「難問」に戻れる.
呪われたサンプルTC3000(4720点)を1353点まで圧縮した「極小サンプルファイル」を作ってあったはずだが,見つからない.残っているのはCT 3000-1906.ZELまでだ.「GOO3000-1303.ZEL」というのはあるが,これは反例ではない.間違えてゴミ箱に入れてしまった可能性はあるが,空にしてしまったので取り戻すことはできない.「BUG3000-1906.ZEL」というファイルもある.「CT 3000-1906.ZEL」とどう違うのだろう?「53直系血族図.ZEL」というのもある.1931点で最小という訳ではないが,最後に扱っていたサンプルだ.⇒これを継承したのが「CT 3000-1906.ZEL」で「BUG3000-1906.ZEL」はそれと同じだ.「BUG3000-1906.ZEL」は下図のようなファイルで,先祖ノードの「35」は画面の下の方に隠れている.
明らかにまだ,かなり削れる余地は残っている.この際なので,完全に「極小」な反例サンプル,つまり,どの1点を取り除いても解けてしまうようなサンプルを追求してみることにしよう.UNDOが使えるようになっているので,それほど厄介な作業ではない.ノードを削って,解けてしまった場合にはUNDOで戻せばよいだけだ.先祖ノードの「35」には{ 1493, 23, 373 }の子ども3人がいる.多分2系統残せば十分と思われる.前回は「23」と「373」の2人を残している.最大系統は「23」だ.「1493」は6点しかない.
「373」はもう少し大きく,49点ある.
まず,この系統を全削除してみよう.やはり,この系統を外すと再現できなくなってしまうようだ.とりあえず,「1493」の直系血族図を取って全削除.⇒失敗した.親族図に切り替えてからソートすればよかった.親族図なら短時間に描画完了できるが,全体図ではループカウントオーバーするまで待たなくてはならない.⇒おかしい.先祖ノードの「35」の子どもがいなくなってしまった.
▲BUG3000-1906.ZELで1493の系統を全削除して,先祖ノード35とその子どもの23, 373のリンクが切れてしまった.⇒全削除ではなく,1個づつの削除でも同じだ.
全削除のバグと思われるが,「35」に「23」と「373」を接続して先に進むことにしよう.⇒かなり,まずい.35→{23,373}の接続ができない.登録ボタンを押しても子ども欄が空欄になってしまう.
どうも,この辺りは全面的な見直しが必要なようだ.UNDOで初期状態まで戻れるだろうか?ちょっと心もとない気がするが…⇒ダメなようだ.もう一度読み直そう.
▲氏名並び替えを実行しただけで,系統並び替えが発動されている.⇒カード並び替えはUNDOの対象になっている.UNDOではつねに系統並び替えを実施するようになっていたのではないか?
▲35の子ども欄に23と373を入力→登録で名前が消えてしまう.23の父欄に35を入力→登録でも名前が消えてしまう.つまり,親子関係の登録が完全に壊れている.⇒UNDOで復元はできた.⇒全体図上で全削除を実行したら,正しく動作した.カードの登録・削除の動作が開いている画面のモードに影響されるなどということがあるのだろうか?
終端ノードは事象に関わりがない可能性があるので,一括除去してみよう.3倍数は女子として登録されているので,まとめて削除できる.438点ある.削除はできたが,解けてしまった.昨日の手順で裾刈りをしてみよう.昨日のメモに「1335」を含むとあるので,それより下の世代をカットしてみる.⇒1473点まで削除したので,一旦保存しておこう.BUG3000-1473.ZELとしておく.⇒少し欲張って,あと2世代裾刈りしてみよう.いや,ダメだ.だめということは昨日確認されている.
「23」は5人子どもをもっている.{15, 245, 3925, 61, 981}だ.「373」の子どもは2人で{1989, 497}だ.これらのいずれかの子どもの系統を切断できるかどうかやってみよう.まず,「373」の子どもから見てみる.2人の子どものうち,1人は女子なので,おそらく削れるだろう.もう一人の子どもは残すしかないことは間違いない.「23」の子どものうち,2人(15, 981)は女子なので,まずこれからカットしてみる.⇒OKのようだ.残り3人を試してみる.まず,「245」⇒loopcnt=25で解けてしまった.⇒他の2つも同じだ.この手順を自動化することは不可能ではない.たとえば,以下が考えられる.
- 対象グラフをTとする
- TのすべてのノードNについて,以下を行う
- ノードNをTから削除して,検定がカウントオーバーするか否かを検査
- カウントオーバー→Nの下流系を除去したTの部分グラフをT’とする
- 検査に合格したT’のうちの最小のものを選んで,Tとする
- これをどの1点を除去してもカウントオーバーしなくなるまで繰り返す
このアルゴリズムによって,最適ないし最小であるか否かは別として,「極小な反例サンプル」が得られることは間違いないと考えられる.問題は計算コストだが,実効的には多項式時間で可能なのではないかと思われる.ステップ4で1点除去するたびにグラフは確実に小さくなってゆくからだ.前回は1353点まで圧縮したので,まだまだ削れることは確実だが,手操作で「極小な反例サンプル」を獲得するのはかなり難しい.「極小な反例サンプルを自動生成」するプログラムを書くのはそれほど難しくないとは思われるが,デバッグのコストも考慮しなくてはならないので,ここでは保留して,もう少し考えてみることにする.
▲BUG3000-1471.ZELに孤立カード「1989」が残っている.⇒どこかで手順に手抜かりがあったのだろう.※このカードを削除して,BUG3000-1470.ZELとして保存した.
Windows 10にダウングレードしてから,VS2017を管理者権限で起動できなくなった.2022/04/14で「Windows 11にアップグレードした」ときと同じ事象だ.ただし,今度は①タスクバー上のアイコンを右クリック→Visual Studio 2017を右クリック→管理者として起動では起動できる.②Visual Studio 2017のショートカットのプロパティ→詳細設定→管理者として実行はオンになっている.前回は,①では起動できなかったが,②を設定して起動できるようになった.今回はその逆で,②では起動できないのに,①では起動できる.タスクマネージャが開いていると起動できるという事象は共通だ.⇒とりあえず,①の方法で起動できることがわかったので,このまま進むことにする.
あれこれやってきたが,呪われたCT3000反例の難問を解くためには,どうしても「極小な反例サンプル」を作るしかないという雰囲気に傾いている.原理的にはそれほど難しくはないが…実際に構築する際の難易度を考えてみよう.外部アプリとして完全に外に出すというのも少し無理がある.現在コラッツツールから直接ゼルコバの木を起動→インポートはできているが,極小反例サンプルを作るためには,少なくとも,①任意のカードNを指定して削除,②系統並び替えの実行,③カウントオーバーしたら処理を中断して結果を返す,④Nの部分木を削除,⑤残りの部分木をファイルに出力…などのことができなくてはならない.
これらのことをすべて外部でやると言うのは現実的ではないので,DLLに作り付けで組み込むしかない.DLL上であれば,ゼルコバの木データのすべてのデータにアクセスして,任意の操作を実行することが可能だから,できない話ではないだろう.VBに組み込むということはあり得るだろうか?現在手操作で行っている反例サンプルの圧縮はすべてゼルコバの木というVBアプリ上でやっているのだから,原理的には実装可能なのではないか?どういうストーリーになるか,書いてみよう.
- VBアプリに以下のような処理Pを組み込む
- 反例サンプルファイルFをロードする
- カードテーブル上のカード数をNとし,反例カウントを0とする
- カードテーブルのすべてのレコードRについて以下を実施する
- カードRを削除する
- 系統並び替えでループカウントオーバーが発生したという結果を何らかの方法で受理して,事象Aとする
- 事象Aが発生していないときは,UNDOを実行して,ステップ2に戻る
- 事象Aが発生したとき
- (1)反例カウントをインクリメントする
- (2)Rを基準ノードとして系統並び替えを実施する
- (3)Rの直系血族を選択して全削除する
- (4)残ったカード数Mをカウントして(R, M)の対をテーブルTに記録
- (5)UNDOを実行して,ステップ2に戻る
- すべてのレコードを処理するまでステップ4から繰り返す
- テーブルTの記録をスキャンして最小のCを与えるRを決定する
- カードRを対象にステップ5~11までを実行する
- 反例カウント>0ならばステップ3に戻る
- ファイルFは極小な反例サンプルである:処理P完了
基本的にステップ6を除けば,すべての手順は現行コードを使って実装可能と考えられる.「カード削除」の戻り値で「ループカウントオーバー」を返すという処理は現行には存在しないが,不可能ではない.現行では「カード削除」の戻り値は次の(主選択)カードの参照番号になっているが,エラーが発生した場合には負値を返すようになっているはずだから,何か適当なエラーコードを決めて,それを返すようにすればよい.ただし,エラー発生の地点から「カード削除」処理の出口までそのエラーコードを受け渡しするのは難しいかもしれないが,グローバル変数に入れておいて,「カード削除」の出口でチェックすればよい.
この処理全体をDLLに組み込むことは難しくないし,その方が効率的でもあるが,あえてVBに独立の処理として組み込む方がベターだと思う.そうすれば,DLL側の既存コードはまったくいじらなくて済むので,既存コードのデバッグとツールのデバッグを完全に切り離すことができる.これが実装できるとかなりおもしろいことになる.「極小な反例サンプル」はいろいろな場面で必要になるが,これまでは手操作でしこしこと作ってきたところが完全に自動化できる可能性がある.「障害」の種別はさまざまだが,ある特定の「障害」が発生しているとき,そのエラーコードを受け渡すだけで,その障害を再現する「極小反例サンプル」が作れるとしたら,これはかなりすごいことではないかと思う.これはやってもやらなくてもよいことではなくて,「MUST」だと思う.
実装に移ることにしよう.VBにはすでにこの種のテスト用ツールのセットが「包括自動テスト」として確立している.たとえばその中には,全体図テスト,親族図テスト,完全木テスト,パックマン全点/単点テスト,ハーレム全点/単点テスト,ランダムインポートテスト,ファイルオープンテストなどがある.「極小反例サンプル」生成ツールはテストツールではないが,ある意味でそれらよりも強力なツールになるだろう.VBアプリは通常の使い方もできなくてはならないので,やはり最初からメニューで起動するようにしておく必要がある.
エラーコードとしては,ERR_COMPLETETRIBEというのがあるので,これを使うことにしよう.TRIBEBOX::CompleteTribeBoxはブール値を返す関数なので,戻り値には代入できない.ここで例外をスローしたらどうなるのか動作を見てみよう.これまで見た限りではノーマルに描画できるときにはループカウントが50を超えることはなかったと思われるので,REDLINEも50にしておこう.⇒StackTribeGeneのトラップでエラーコードがERR_STACKTRIBEGENEに切り替わっている.また,TOPOLOGY::SetPhaseでフェーズ遷移エラーが起きる.⇒例外をスローすると,「検定に失敗しました」でアプリ終了してしまう.やはり,グローバル変数で渡すしかなさそうだ.⇒fatalerrorという変数があるので,流用してみよう.(わたしはミニマリストなのでできるだけ変数の数は増やしたくない…)
UNDOBASE::CommandStartでエラーコードをリセットし,CommandEndで渡すようにしようとしたが,UNDOBASE::CommandEndではまだ系統並び替えが実行されていない.それを行っているのはUNDOSYSTEM::CommandEndだ.fatalerrorではダメなようだ.どこかでリセットしているのだろう.KAKEIZU::closeFamilyBaseでゼロを代入している.ERR_COMPLETETRIBE=-3051だ.⇒mZelkova::mDeleteCardで戻り値を書き換えている.⇒いや,違う.FAMILYTREE::DeleteCardDataはCommandEndの戻り値を無視している.⇒いや,ほとんどのコマンドがそうだ.⇒Z.mDeleteCardまではエラーコードが通った.
カード削除を実行しているVBのDeleteFuncでは,Z.mDeleteCardの戻り値をCurrefnumに格納し,Z.DeleteCountを返している.極小反例ツールではZ.DeleteCountを直接呼び出す作りになると思われるので,ここまででよいことにしよう.次に,メニューコマンドを整備しておこう.包括自動テストの下に,「極小反例サンプル生成」というコマンドを新設しておく.この実行メソッドはOpenFileTest.vbにおくことにする.メソッド名はBuildMinimalCounterSampleとし,現在開いているファイルを対象とすることにする.
VBが持っているカードテーブルは実際には一覧表のことだが,カードを削除したり,それをUNDOで復元したりしたとき,カードの並びが保存されているかどうか?ということは当てにできないので,別にカード番号だけを集めた配列を作った方がよい.これを2次元配列として残留カード数を格納できるようにしておけばよいのではないか?
ステップ11で「Rの直系血族を選択して全削除する」としているが,Rの直系血族に属するカード数をカウントするだけでよい.残留カードが最小となるのは,直系血族最大の場合だからだ.ただし,直系血族のすべてのカードではなく,Rの下流系のみをカウントしなくてはならない.つまり,Rの親との関係を切断した上でその直系血族をカウントするようになる.ちょっと厄介だ.親子関係を切断するためにはカードRの個人情報を読み出した上,カード登録操作を行わなくてはならない.(VB上には直接親子関係を切断するようなコマンドはない)
カードRの下流系に含まれるカード数をカウントする手続き:
- カードRのカード情報を取得する
- Rのカードの父母欄を空欄にして登録する
- 図面種別:親族図,親族範囲:直系血族に設定する
- Rを基準ノードとして系統並び替えを実行する
- 一覧表:表示範囲:系図画面上のカードを一覧表にロードする
- 一覧表のレコード数を取得する
これを実装する前に前段の動作を確認してみる.つまり,カード単点を削除してカウントオーバーが,最初のループでどの程度発生するかを見てみることにする.どうも,この方法は非現実的なものであるような気がする.1400点余りの反例サンプルを一度描画するだけで少なくとも1分近くは掛かっている.検定が一周するのに1400回系統並び替えを実行するとすると,それだけで1400分,24時間掛かってしまう.実際には1点を処理するのに3回程度系統並び替えをやり直すので,おそらく,2日ないしそれ以上掛かることになりそうだ.最後の極小解を得るまでにそれを何段繰り返せばよいのか分からないが,少なくとも2, 300日,おそらく1年以上掛かってしまうのではないかという気がする.
必ずしも最適解が得られるとは限らないが,もう少し効率的なアルゴリズムも考えられる.カード単点を削除して,それが反例だと判定された時点でそのカードセットを開始点に切り替えればよい.現行でも,すでに「3」と「22」は反例を生成するという結果が出ているので,それを使えばもっと急速に収束するような動きになるはずだ.「3」ないし「22」が削除できるということを手操作で確認してみよう.⇒1400点もあって,「3」を目視で見つけることができない.メニューバーの検索ボックスはあいまい検索なので,同名カードが663件も出てくる.「検索パネル」では「姓名の完全一致」検索ができるが,悲しいことに,「検索条件にマッチングするカードはありません」.
どうにもならないので,氏名でソートしたものを保存しておこう.mmm…確かに「3」というカードは載っていない.「22」も同様だ.⇒間違えていた.現在行っているテストでは,参照番号ではなく,「氏名」のうちの「姓」を読まなくてはならなかった.作り直しが必要だ.
▲UNDOSYSTEM::CommandEndで,UNDOBASE::CommandEndを呼び出した後,UndoCurptrが空で停止した.これまでこのようなエラーは出たことがない.⇒修正を誤っている.確かに「姓」には「ノード番号」が入っているが,カード操作では「参照番号」を使わなくてはならない.つまり,「3」ないし「22」のカードではなく,#3ないし,#22のカードでなくてはならない.
#3は「8195」,#22は「8213」,いずれも単身の子どもを1人持つカードだ.確かに,このようなカードは削除できる.これを進めれば,高々Nステップで極小解に達することができるだろう.これなら丸一日で完了する可能性はある.しかし,子孫系をカットする手順が問題だ.要は始系列に属さないノードセットを構成すればよいのだが… この操作をDLL側でやっているのなら方法はいくらでもあるのだが…
VBには先祖並び替えというのがあるから,先祖リストを取ることはできる.先祖リストの先頭を除いて,それに続くノードを対象に,直系血族図を作ってそれを全削除でよいのではないだろうか?この方法の利点は,全体木の先祖ノードを削除して反例になる場合にも対処できるという点だ.(通常はこういうことは起こらないが…)
先祖リストの取得関数 mGetSortList を呼び出すのに,引数に参照番号を入れて渡しているが,これは何に使っているのだろう?⇒この関数は先祖リストだけでなく,子ども並び替えや配偶者並び替えなどにも使う汎用関数だ.先祖リスト取得の場合にはこの値は参照されていない.リストはSortListに格納され,バックアップを指定すると,InitialListにも同じ値が保全される.
どうもUNDOが壊れてしまっているようだ.初回2枚のカードを削除したあと,新たに開始したテーブルで12番目ノードを処理中にUNDOで無限ループしている.まとめて大量削除したものをUNDOしているとすればそのようなことも起こり得るが…いや,これはUNDO動作ではなく,UNDOBASE::CommandEndの中で「今回保全したノードの値が更新されているときには今回リストに追記する」ということをやっているところだ.何が起きているのかさっぱり見当も付かない…この処理はUNDOに全面的に依存しているので,UNDOが動かないことには始まらない.UNDOをリセットするというコマンドはあったろうか?AutoMergeTestではresetUndoChainを実行しているが,VBから直接リセットするコードは存在しない.AutoMergeTestでは今回と同様UNDOを使って処理を組み立てている.
UNDONODEやUNDOCOMMANDは,noduleから直接派生したプリミティブなオブジェクトでLISTなどのような要素管理の仕組みは持っていない.DumpUndoChainという関数がある.これであるUNDOコマンドの下にあるチェーンをダンプできる.この関数はUndoProcessから呼び出されている.UNDOBASE::CommandEndにも仕掛けてあった.⇒なぜだろう?確かにむちゃくちゃ長いチェーンが生成されている.コマンドはDELALLCARDだが,完全に無限ループしているように思われる.チェーンには主にCARDLINKとMARGLINKのコピーが保全されているが,カード数は1500より少ないのだから,すべてのカードを削除したとしても,3000を超えるはずはないはずなのに,30000を超えている.一つのカードを削除するとその周辺もバックアップされるから,この数倍,いや,10倍とすれば30000というのは不思議な数ではない.
単点を削除したときのバックアップが14点くらいあるので,確かにそのくらいにはなるかもしれない….当然その大半は重複とみられる.どうすればよいか?保全の対象ノードに何か目印を付けるしかないのではないか?しかし,UNDOで保全されていると言っても,時間経過によって変化するから,そのときの「現時点」においてすでにバックアップされているか否かを判定できるようになっていなくてはならない.従って,フラグのようなものではなく,何か計数的なものが必要だ.UNDO開始のときにNOを決定し,開始から終了までの間に保全されたオブジェクトにはそのマークNOを書き込んでおくというのはどうか?
これはシステムに1個だから,UNDOBASEないし,UNDOSYSTEMの静的変数とすべきだろう.グローバル変数でもよいかもしれない.UndoCounterというファイル内の静的変数を定義してみた.初期化はUNDOリセットでよいだろう.いや,現行でもUNDOBASE:etUndoListでは,「Undoリスト上にobjectの複製がすでに存在する場合はFalseを返す」ということになっているのだが…SetUndoListには正・負・ゼロの3モードがあり,正がオブジェクトの生成,負がオブジェクトの削除,ゼロがオブジェクトの更新に割り当てられている.SetUndoListでは更新の場合以外は,無条件ですべてのオブジェクトを保全している.これは見落としを避けるためという理由ではないかと考えられるが,一つのコマンドチェーン上に同じオブジェクトが複数存在する意味はないと考えられるので,すべて弾くということにしてみたい.
「部分図オブジェクトは更新する@20180207」というコメントもある.これはSetUndoListの第3引数がUNDO_COPYの場合には上書き更新するというものだ.よくわからないが,ちょっと無視しておこう.
▲上記エラーが再発した.UNDOSYSTEM::CommandEndで,UndoCurptrが空で停止.
確かに,事後にオブジェクトを更新するということはやっている.なぜこういうことが必要なのか調べる必要がある.UndoCurptr->topUndoが空のときには,PergeZenpoChainで「前方コマンドチェーンをカット」している.このケースでは初回のUNDO処理であり,かつtopUndoが空であるため,UndoCurptrまで削除されているものと思われる.つまり,このような状態はあり得ると見るべきではないか?
▲BaseLink空により,FAMILYTREE::RestoreBaseCardで停止した.その後,バックアップタイマーでファイルをバックアップ中(!topology->SenzoOffList->count)で停止した.