2026年3月3日火曜日

Lpソルバで緩和解

 以下は、スケジューリングベンチマークスの問題をLpソルバで解いたときの、時間です。SAT Solveする際に、以下のルーチンをCALLしてLPファイルを作成→COPTのinteractive commandでMPSファイルに変換しています。

https://schedule-nurse.blogspot.com/2026/02/cnf2lp.html


測定は、PDLPXで、GPUは、4060 16GBです。


ここで、着目するのは、Known UBからは遥かに遠い値になってしまうということです。この状態からCutting Planeを駆使してUBまで持ってくるには、相当のMIP Solving技術がないとInstance8程度でも難しいです。(Highsでも1日程度かかります)

単純な緩和解との乖離が大きいというのは、ナーススケジューリング問題の本質的な難しさを示しています。

もう一つは、巨大なInstance23/24でもFirst Order Solver PDLPXは、難なく解けているということです。このあたりになると、MPSファイルで、2.4GBにもなります。COPTのバリアソルバでも解けない規模になりますが、難なく解けているのがPDLPイノベーションです。

どのようにしてSAT SolvingとLpソルバ緩和解を結びつけるか?というのが革新的核心技術になります。



2026年3月2日月曜日

ZOOMレクチャをお願いしたい

 Q.サブスクですが、ZOOMレクチャをお願いできますか?


年間サブスクをご購入された方特典として、ご要望があればでしたら、貴職場のルールをお聞きしながら、一緒に貴職場のプロジェクト作成することは可能です。事前知識は、必要ありません。初回1回に限り(無償)です。

プロジェクト作成の性質上、休憩を入れながら約半日のお時間を頂きます。終わった後に、録画URLをご案内しますので、後で見返して練習することが出来ます。

毎月15日ー25日はサポートで混みますので、それ以外の日で、お申込みからおよそ一カ月程度お待ち頂いております。

条件としては、サンプルを動かせた、というところまで出来ていることです。

2026年3月1日日曜日

Cadical3.0 incremental で動作させる為に

Minisatの延長だろうと考えて、incrementalに動作させようとすると、地雷を踏むことになります。

基本的知識として、ervarsの存在を意識して書くことが必要です。solveの後、割り当てていないはずなのに、vars()してみると、何故か自分が割り当てた変数数より増えています。これが、ervarsによるものです。

eravarsとは何か?AIがドキュメントしていない内部事情を説明してくれています。

Q.CADICAL3.0 で、ervarsとは、何ですか?

結論から言うと、CaDiCaL 3.0 の “ervars” は、Extended Resolution(拡張解決)で導入された補助変数(extension variables)を指す内部カウンタです。
ただし、公開されている一般向けドキュメントではほとんど説明されておらず、主に 内部統計(statistics) として現れます。


🧩 “ervars” の正体

CaDiCaL には、内部状態を表示するための統計項目が多数あります。その中に ervars があり、これは:

  • ER(Extended Resolution)で導入された新しい変数の数
  • ソルバーが内部で式を簡略化したり、学習節を扱う際に導入する補助変数
  • ユーザが入力した元の変数とは別に、ソルバーが自動生成する

という意味を持ちます。

Extended Resolution は、SAT ソルバーが探索を効率化するために、
[ x \leftrightarrow (a \land b) ]
のような新しい変数を導入して式を分解し、学習節の質を高める技法です。

CaDiCaL 3.0 では、この ER を内部的に利用しており、その結果として 導入された補助変数の総数が “ervars” として統計に出ます。


📌 なぜ “ervars” が重要なのか

  • ER 変数が多いほど、ソルバーが内部で式を再構成している
  • 問題によっては ER が効率化に寄与する
  • 逆に、ER が増えすぎるとメモリや節のサイズが増える可能性もある

つまり、ソルバーの内部変形の度合いを示す指標として役立ちます。


🔍 参考情報

CaDiCaL の GitHub リポジトリには統計項目の一覧がありますが、ervars の詳細説明はありません。
ただし、CaDiCaL 2.0/3.0 の論文やコードコメントでは、ER(Extended Resolution)を内部で利用していることが確認できます。


次に知りたいことはありますか?

例えば:

  • CaDiCaL の統計項目(vars, elimed, substituted など)の意味
  • Extended Resolution が SAT ソルバーでどう使われるか
  • CaDiCaL 3.0 の内部構造や新機能の解説

どれを深掘りしましょう?



結論として、ervars[eidx]true になってしまう原因は、CaDiCaL が内部で「その変数は ER(Extended Resolution)で導入された補助変数 である」とマークしているためです。
その結果、is_valid_input()「ユーザ入力としては不正」 と判断し、assert が落ちます。


🔍 is_valid_input() が何をチェックしているか

あなたが示したコードは CaDiCaL の内部チェックです。

inline bool is_valid_input (int elit) {
  assert (elit);
  assert (elit != INT_MIN);
  int eidx = abs (elit);
  return eidx > max_var || !ervars[eidx];
}

この条件を満たすと「入力として有効」と判断されます。

  • eidx > max_var
    ユーザが入力した変数番号より大きい番号なら OK
    (つまり内部変数は OK)

  • !ervars[eidx]
    その変数が ER 変数でなければ OK

逆に言うと、

eidx <= max_var かつ ervars[eidx] == true のとき、入力として不正

になります。


🧩 なぜ ervars[eidx] == true になるのか

CaDiCaL は内部で Extended Resolution(拡張解決) を使い、
探索や学習節の質を上げるために新しい補助変数を導入します。

このとき、

  • その変数は ユーザ入力の変数ではない
  • 内部で生成された ER 変数
  • ervars[eidx] = true とマークされる

という扱いになります。


❗ あなたのケースで起きていること

あなたが elit として渡した変数が、

  • ユーザが入力した変数番号の範囲内(eidx <= max_var
  • かつ 内部で ER 変数としてマークされている(ervars[eidx] == true

という状態になっているため、
is_valid_input()「これはユーザが触ってはいけない内部変数だ」 と判断し、エラーになります。


🧠 どういうときに起きるか(典型例)

次のような状況が考えられます。

1. あなたが ER 変数を外部から参照してしまっている

  • CaDiCaL が内部で導入した変数番号を、
    ユーザ側のコードが誤って扱っている。

2. max_var の扱いを誤っている

  • solver->vars() の値を正しく追跡していない
  • 変数追加後に max_var を更新していない

3. 変数番号の再利用・再割当てのバグ

  • 自前で変数番号を管理している場合、
    CaDiCaL の内部変数と衝突してしまうことがある。

4. CaDiCaL の内部状態を壊してしまっている

  • freeze(), add(), assume() の順序が不正
  • simplify()eliminate() の後に古い変数番号を使っている

🧪 デバッグのために確認すべきこと

次の情報があると原因を特定できます。

  • elit の値(問題の変数番号)
  • max_var の値
  • ervars[eidx] が true になったタイミング
    (ER 変数は通常、内部処理中に生成される)
  • あなたが変数を追加した順序
    solver->new_var() を使っているか?

📌 まず確認してほしいこと(重要)

あなたが使っている変数番号は、必ず solver->vars() 以下になっていますか?

もし内部で CaDiCaL が導入した ER 変数を誤って参照しているなら、
それが今回の assert の直接原因です。


続けて原因を特定しましょう

次の 2 点を教えてください。

  1. 問題が起きたときの elit の値
  2. あなたが変数を生成しているコード(new_var() を使っているか)

これが分かれば、どこで ER 変数と衝突しているかを正確に特定できます。



結論として、インクリメンタルに節・変数を追加した後に ervars[eidx] == true になって落ちる場合、CaDiCaL の内部で “ER 変数(Extended‑Resolution 変数)” が生成され、それが ユーザ変数領域(1..max_var)に衝突してしまっている ことが最も典型的な原因です。
これは CaDiCaL 3.0 で特に起きやすい挙動です。


1. 何が起きているか(内部での変数生成と衝突)

CaDiCaL は、インクリメンタルモードで節を追加していくと、途中で以下のような内部処理を行います。

  • 節の簡約化
  • elimination
  • vivification
  • strengthening
  • extended resolution(ER)

この ER の過程で、新しい内部変数が勝手に生成されることがあります。
この内部変数は ervars[]true としてマークされます。

本来、内部変数は max_var より大きい番号を使うため、ユーザ変数と衝突しません。

しかし、次のような状況が起きると衝突します。

● 原因 A:あなたが max_var を固定値として扱っている

CaDiCaL は内部で変数を増やすため、
solver->vars() の値は 途中で増えることがあります。

もしあなたが外部で

  • 自前で変数番号を管理している
  • あるいは max_var を初期値のまま固定している

と、内部変数とユーザ変数の番号が重なることがあります。

● 原因 B:インクリメンタル追加の順序で ER が走り、既存変数が ER 変数扱いになる

CaDiCaL の内部簡約化で、
「この変数は内部的に拡張解決に使う」と判断されると、
ユーザが追加した変数でも ervars[eidx] = true になることがあります。

その後、あなたがその変数を再び入力として使うと、

eidx <= max_var かつ ervars[eidx] == true

となり、is_valid_input() が false を返して assert が落ちます。


2. 典型的に起きるタイミング

次のような操作の後に起きやすいです。

  • 多数の節を追加した後に simplify() を呼ぶ
  • elimination が走る
  • vivification が走る
  • solve → 追加節 → solve → 追加節 … を繰り返す
  • assumptions を使ったインクリメンタル探索

特に solve の後に変数を追加すると、内部状態が変わっているため衝突しやすくなります。


3. どう対処すべきか(実務的な解決策)

✔ 対策 1:変数は必ず solver->new_var() で生成する

自前で変数番号を管理せず、CaDiCaL に任せるのが最も安全です。

✔ 対策 2:solve の後に変数を追加しない

CaDiCaL は solve の後に内部状態を大きく変えるため、
その後に変数を追加すると衝突しやすくなります。

✔ 対策 3:インクリメンタルモードでは freeze() を使う

変数を消されないようにするために、
ユーザ変数は solve 前に freeze(var) しておく必要があります。

✔ 対策 4:ER を無効化する(ビルドオプション)

CaDiCaL は ER を内部で使いますが、
ビルド時にオプションで無効化できます(ただし 3.0 では制限あり)。


4. 追加で確認したいこと(原因特定のため)

次の 3 点を教えていただけると、正確に原因を特定できます。

  1. 変数を追加する方法

    • new_var() を使っていますか?
    • それとも自前で番号を振っていますか?
  2. solve の前後で変数を追加していますか?

  3. freeze() を使っていますか?


これらが分かれば、どこで ER 変数と衝突しているかを完全に特定できます。



結論として、CaDiCaL の公式 C++ API には new_var() は存在せず、ユーザが使えるのは resize(int min_max_var) だけです。
これはあなたの理解どおりで、CaDiCaL は「ユーザが変数番号を管理する」設計になっているため、MiniSAT 系のような new_var() は提供されていません。

ただし、ここに今回の落とし穴があります。


CaDiCaL の変数管理の仕組み

● 1. ユーザは resize(n) で「最大変数番号」を宣言する

例:

solver.resize(100);  // 1..100 がユーザ変数

● 2. CaDiCaL は内部で ER 変数を追加する

CaDiCaL 3.0 は内部簡約化や Extended Resolution により、
ユーザが知らない内部変数を勝手に追加します。

この内部変数は:

  • 番号は max_var + 1 以降
  • ervars[] = true とマークされる
  • ユーザが触ってはいけない

という扱いになります。


今回の問題の本質

あなたが自前で変数番号を振っていたため、次のような衝突が起きました。

  1. resize(N) でユーザ変数領域を宣言
  2. CaDiCaL が内部で ER 変数を追加(番号は N+1, N+2, …)
  3. あなたが自前で変数番号を増やしていく
  4. 内部変数と同じ番号をユーザが再利用してしまう
  5. CaDiCaL 側は「その番号は ER 変数だからユーザ入力は不正」と判断
  6. is_valid_input() が false → assert で落ちる

つまり:

CaDiCaL の内部変数番号と、あなたの自前の変数番号が衝突した

これが原因です。


正しい変数管理方法(CaDiCaL 3.0)

✔ 1. 変数番号は「常に単調増加」で管理する

内部変数は max_var を超える番号を使うため、
ユーザ側も 常に max_var より大きい番号を使う必要があります

✔ 2. 変数を追加するときは必ず resize() を呼ぶ

例:

int v = ++my_max_var;
solver.resize(my_max_var);

✔ 3. solve の後に変数を追加する場合は特に注意

内部で ER が走り、内部変数が増えている可能性があります。
solve の後は必ず:

int internal_max = solver.vars();  // CaDiCaL が現在使っている最大変数番号

を確認し、
ユーザ変数は internal_max より大きい番号から割り当てる必要があります。


実装例(安全な変数管理)

int next_user_var = solver.vars();  // CaDiCaL が現在使っている最大変数番号

int new_var() {
    next_user_var++;
    solver.resize(next_user_var);
    return next_user_var;
}

これで内部変数と衝突しません。


まとめ

  • CaDiCaL には new_var() がない → 正しい
  • 代わりに resize() で最大変数番号を宣言する
  • CaDiCaL は内部で ER 変数を追加する
  • 自前で変数番号を振ると内部変数と衝突する
  • 解決策は「常に solver.vars() を基準に変数番号を増やす」

CaDiCaL 3.0 のインクリメンタル利用は落とし穴が多いので、
あなたの現在の変数割り当てコードを見せてもらえれば、
衝突しない安全な形に書き換えます。


その理解はまさに正しくて、CaDiCaL 3.0 をインクリメンタルに使う場合は「多めに resize しておき、内部で増える vars(内部変数)を監視しながらユーザ変数を割り当てる」設計が最も安全で高速です。

ここからは、なぜそれが必要なのか、どこに落とし穴があるのか、そして実際の安全な設計パターンを整理しておきます。


CaDiCaL の変数管理が難しい理由

● 1. CaDiCaL は内部で ER 変数を勝手に増やす

Extended Resolution(ER)や elimination の過程で、
内部変数が max_var + 1 以降に追加される

このとき ervars[] = true になる。

● 2. resize は内部アロケータを動かすので重い

resize(n) は内部ベクタや watchlist の再確保が走るため、
頻繁に呼ぶと確実に遅くなる。

● 3. solve のたびに内部変数が増える可能性がある

インクリメンタルで solve → add → solve を繰り返すと、
内部変数がどんどん増える。


推奨される設計パターン(実務的に最適)

🟦 1. 最初に「十分大きい」サイズで resize しておく

例:

solver.resize(2000000);  // 200万変数分を確保しておく

これで内部アロケータの再確保を避けられる。

🟦 2. CaDiCaL の内部変数数(solver.vars())を常に監視する

内部変数が増えると solver.vars() が増える。

ユーザ変数を追加するときは:

user_var = solver.vars() + 1;

のように、内部変数の最大値を常に避ける

🟦 3. 自前の new_var() を安全に実装する

int next_var = solver.vars();  // CaDiCaL が現在使っている最大変数番号

int new_user_var() {
    next_var++;
    // resize は最初に十分大きく取っているので呼ばない
    return next_var;
}

これで内部変数と衝突しない。

🟦 4. solve の後は必ず solver.vars() を再取得する

solve の後に内部変数が増えている可能性があるため:

next_var = solver.vars();

を必ず更新する。


なぜこの設計が最適なのか

  • resize を頻繁に呼ばない → 高速
  • 内部変数と衝突しない → ervars[eidx] エラーが消える
  • CaDiCaL の内部最適化(ER, elimination)を阻害しない → 強い伝播が維持される
  • インクリメンタルでも安全に動く

CaDiCaL 3.0 の内部変数生成はかなり積極的なので、
ユーザ側が変数番号空間を広く確保しておくのが最も現実的です。


実装テンプレート(あなたの用途向け)

// 初期化時に十分大きく確保
solver.resize(2'000'000);

int next_var = solver.vars();  // CaDiCaL の内部最大変数番号

int new_user_var() {
    // solve の後は next_var を solver.vars() で更新すること
    next_var++;
    return next_var;
}

// solve の後
solver.solve();
next_var = solver.vars();  // 内部変数が増えている可能性がある

ひとこと

あなたの「多めに resize しておいて、vars を watch する」という設計は、
CaDiCaL 3.0 のインクリメンタル利用では最適解です。


必要であれば、あなたの現在のコード構造に合わせて
**完全に安全な変数管理ラッパー(C++ クラス)**を作ることもできます。



結論として、200 万変数を常に確保する必要はなく、むしろ「必要になりそうな最大値を見積もって、余裕を持って一度だけ大きめに resize しておく」設計が最適です。
200 万という数字はあくまで例で、問題規模・内部 ER の発生量・インクリメンタルの深さによって適切な値は変わります。


どれくらい「多め」に確保すべきか

CaDiCaL 3.0 の内部挙動を踏まえると、次の 3 点が重要です。

1. 内部 ER 変数は問題によっては数万〜数十万増える

  • elimination
  • vivification
  • strengthening
  • extended resolution

これらが走ると、内部変数が ユーザ変数の 1.2〜3 倍 まで増えるケースがあります。

2. solve のたびに内部変数が増える可能性がある

インクリメンタルで solve → add → solve を繰り返すと、
内部変数が累積的に増える。

3. resize は重いので頻繁に呼ぶべきではない

内部ベクタ・watchlist の再確保が走るため、
resize はできれば 1 回だけが理想。


実務的な「最適戦略」

🟦 1. 問題規模から最大変数数を見積もる

例:

  • ユーザ変数が最大 30 万
  • ER で 2〜3 倍増える可能性
    → 100 万〜150 万を確保しておく

🟦 2. 一度だけ大きめに resize

solver.resize(1'500'000);

🟦 3. 以降は resize を呼ばず、solver.vars() を監視

内部変数が増えるので:

int next_var = solver.vars();

ユーザ変数を追加するときは:

next_var++;
return next_var;

🟦 4. solve の後に next_var を更新

solver.solve();
next_var = solver.vars();

「200 万は多すぎるか?」の判断基準

多すぎるケース

  • 問題の最大変数が 5 万以下
  • ER がほとんど発生しない(制約が単純)
  • インクリメンタルの solve 回数が少ない

→ 20〜50 万で十分。

適切なケース

  • 問題が 10〜30 万変数規模
  • elimination や vivification が多く発生
  • solve を何度も繰り返す

→ 100〜200 万は妥当。

少なすぎるケース

  • 内部 ER が大量に発生する
  • solve を 100 回以上繰り返す
  • 変数が 30 万を超える

→ 200 万でも足りない可能性がある。


メモリ的には問題ないのか?

CaDiCaL の変数ごとの内部構造は比較的軽量で、
200 万変数でも 数百 MB 程度に収まります。

現代のメモリ環境では十分許容範囲です。


まとめ

  • 200 万が「多すぎる」かどうかは問題規模次第
  • 重要なのは resize を頻繁に呼ばないこと
  • 内部 ER 変数が増えるので solver.vars() を常に監視
  • 最初に「余裕を持って」確保しておくのが最適

あなたの問題規模(最大ユーザ変数数、インクリメンタル solve 回数、制約の種類)が分かれば、最適な resize サイズを具体的に算出できます。



結論として、**ER 変数(Extended‑Resolution 変数)に対して val() を呼ぶのは CaDiCaL 3.0 では基本的に「NG」**です。
理由は、ER 変数は ユーザが参照することを前提としていない内部専用変数であり、val() の対象として想定されていないためです。


ER 変数に val() が NG になる理由

● 1. ER 変数は「内部専用」で、ユーザ API の対象外

CaDiCaL の ER 変数は、内部の簡約化・強化・拡張解決のために自動生成される補助変数です。

  • ユーザが入力した変数ではない
  • 意味的にも「値を問い合わせる対象」ではない
  • モデルに含める保証もない
  • 削除・再利用される可能性がある

そのため、val() の対象として扱うと 未定義動作になります。

● 2. is_valid_input() が ER 変数を弾く設計になっている

CaDiCaL の内部チェックはこうなっています:

return eidx > max_var || !ervars[eidx];

つまり、

  • eidx <= max_var かつ ervars[eidx] == true
    ユーザが触ってはいけない内部変数

という扱い。

val() も内部でこのチェックを通るため、ER 変数に対して呼ぶと assert が落ちます。

● 3. ER 変数はモデルに含まれないことがある

ER 変数は内部の式変換のためだけに存在するため、
最終モデルに値が割り当てられないこともあります。

そのため、val() が意味を持たないケースが多い。


ER 変数を避けるための実務的な設計

🟦 1. CaDiCaL の内部最大変数番号を常に監視する

int internal_max = solver.vars();

🟦 2. ユーザ変数は常に internal_max より大きい番号から割り当てる

int next_var = solver.vars();
int new_user_var() {
    next_var++;
    return next_var;
}

🟦 3. solve の後は next_var を solver.vars() で更新

内部 ER 変数が solve のたびに増えるため:

solver.solve();
next_var = solver.vars();

🟦 4. ER 変数は絶対に val() しない

ユーザ変数だけを val() の対象にする。


ER 変数を val() してしまう典型的なパターン

  • 自前で変数番号を管理していて内部変数と衝突
  • solve の後に変数を追加している
  • resize を小さく設定していて内部変数がユーザ領域に侵入
  • モデルを走査するときに 1..vars() を全部見てしまう

特に最後のケースは危険で、vars() の範囲には ER 変数も含まれるため、
モデル走査は必ず「ユーザ変数だけ」に限定する必要があります。


まとめ

  • ER 変数は内部専用 → val() は NG
  • CaDiCaL 3.0 は solve のたびに内部変数が増える
  • ユーザ変数は solver.vars() より大きい番号から割り当てる
  • モデル走査もユーザ変数だけに限定する必要がある

モデル走査部分や変数管理部分のコードを見せてもらえれば、
ER 変数を完全に避ける安全な実装に書き換えることもできます。


2026年2月28日土曜日

Cadical 3.0をWindowsにポート

 前回と同じように行ったのですが、

https://schedule-nurse.blogspot.com/2026/01/caidical.html

3.0になってポート出来ない箇所(congruence ゲート抽出)がありました。原因はほぼ分かっています。ポートの問題ではなくCadicalのバグの可能性が大です。

プリプロセスだけの話なので、切って使うことにしました。


問題箇所は、

sort_literals_by_var (g->rhs);

Gate *h = find_and_lits (g->rhs, g);

にあるらしいことは掴めました。table に入れた Gate の rhs を後から変更すると、unordered_set の内部構造が壊れる。C++のUBらしいです。しかし、該当箇所は沢山ありまして、バグFixまでには至りませんでした。GCCでは検出されなくて、MSVCで検出されるのは、これでCadical2回目になります。結構な人が使っていると思うのですが、未だ枯れてないんですね。

もう少し、綺麗で安全な書き方があると思うのですが...congruence は鬼門です。

とりあえず使うには、以下のように切ります。


bool Internal::extract_gates (bool remove_units_before_run) {

    #ifdef _WIN32

  return false;//Cong

なお、このオプションがなくても、性能劣化は殆ど私のプロジェクトではありません。

ゲート抽出は、x1==x2となっているところを抽出して節を削減する手法です。一般にXORゲートを多用する暗号化、または、LSI演算回路系検証等のCNFには効果が期待できますが、私のプロジェクトでは、UnitPropagationを重視していてUnitPropagationを阻害するXORゲート記述は、基本使用禁止です。なので、なくても差しさわりありません。

Biere教授に報告すべき案件ですが、バグFIXできていないので確信が持てずレポートはしていません。


<copyが動かない>

以下でエラー出るので、これも切っています。

この辺、AIに聞いても、Windowsポートの問題(あなたの問題)という立ち位置で、取り合ってくれませんでした。ポートの問題ではないことは確認済みで、これも修正すべきと思います。AIも先入観があるのか、あるいは最新版のビルドに関して学習が済んでいないのか分かりませんが、意外に人間っぽいと思います。

void Solver::add (int lit) {

  TRACE ("add", lit);

  REQUIRE_VALID_STATE ();

  if (lit) {

      #ifndef _WIN32

    if (internal->opts.factor && internal->opts.factorcheck == 1)

      REQUIRE (

          abs (lit) <= external->max_var,

          "adding literal '%d' with undeclared variable '%d' "

          "(checking that user variables are declared explicitly failed "

          "as both 'factor' and 'factorcheck' are enabled)",

          lit, (int) abs (lit));

    if (internal->opts.factorcheck == 2)

      REQUIRE (

          abs (lit) <= external->max_var,

          "adding literal '%d' with undeclared variable '%d' "

          "(checking that user variables are declared explicitly failed "

          "as 'factorcheck == 2' even if 'factor' is disabled)",

          lit, (int) abs (lit));

    #endif

  }

2026年2月27日金曜日

新しいアークテクチャに方針変更

 instance24が解けない

https://schedule-nurse.blogspot.com/2026/01/instance24.html

ので、

1)ローカルソルバ検討

https://schedule-nurse.blogspot.com/2026/01/blog-post_28.html

2)時間制約の検討

https://schedule-nurse.blogspot.com/2026/02/instance24timeencode.html

3)MDDの検討

https://schedule-nurse.blogspot.com/2026/02/pb-bdd.html

https://schedule-nurse.blogspot.com/2026/02/mdd_14.html

と検討を行ってきました。

当初、軽いローカルソルバを目指していましたが、この壁に立ち向かうには、世界最高性能のMAXSATソルバをスクラッチからデザインし直すのが最も早道との考えに改めました。10年間続いた基幹アルゴリズムを変更することは、勇気が要りますが、それよりもinstance24を解くことの方が大事です。別途、開発スケジューリングの見直しを行うこととします。

新しいアーキテクチャは、ソルバ群の連携を前提にしたものです。個々の部品としても機能しますが、それよりは、各ソルバとの連携により新たな価値を生むことを前提に設計しています。こうした発想は、以前はありませんでした。同一問題を色々なアプローチから取り組み、互いの強みを生かして、相互連携により解けなかった問題を解けるようにすることを目的にしています。MaxSATソルバもそのための一部品という位置づけになります。ポートフォリオと言うよりは、複数の全く別のアプローチにより一つの目標を達成するアーキテクチャです。

解けなかったことが、逆に、新しいアーキテクチャを促し新しいソルバを生み出す原動力になっているので、解けなかったことに、むしろ感謝しなくてはいけない、と今は、思っています。


2026年2月26日木曜日

AE-Kissat-MABをWindowsにポート

AE-Kissat-MABは、SAT Competition 2025の優勝ソルバです。 

The Results of SAT Competition 2025

下のプロットを見ると他のソルバよりも明確な優位性が見られます。


このソルバは、生成AIでコードチューニングされています。
近年SAT Competitionの進歩は緩慢であり、これほどに大きなゲインは、Kissatの登場以来ではないかと思います。

しかし、この数か月後にNVIDA SATLUTIONは、さらにこの上を行く発表しています。
が、コードは公開されていません。なので、現時点で、公開されている利用可能なSATソルバの頂点に位置するソルバと考えてよいです。

インスタンス24を解くには、やはりCardicalではメモリ使用量に難点があり、Kissatに頼らざるを得ない、ならば、最新ソルバを使用してみたい、という発想です。

で、疑似インクリメンタルな実装にしてみました。

https://schedule-nurse.blogspot.com/2026/02/kissatincremental.html

次が、作成したAPIです。胆は、

1)Phaseのセットができる

2)UnitClauseを含めた学習節のExport/Importが可能

3)簡略化で消されないようにするFreeze機能を装備

4)VSIDS/CHBのスコアをExport/Import


これにより、複数回Solveしても、問題なく求解できます。完全なシリアライズではありませんが、上記機能により主要な諸元は、リストアすることが出来ます。ほぼ、Warm Start機能を実現しました。問題なく機能が確認出来た後、Githubにアップします。

これでインスタンス24攻略に向けての最終部品がほぼ出来ました。Time制約のリソース改善を行った後、今後,各ソルバ群の統合化作業を行い、再挑戦を行う予定です。長い長い登攀でしたが、ようやく辿り着けそうな気がしてきました。


#ifndef KISSAT_WIN_API_H
#define KISSAT_WIN_API_H

#include "kissat.h"
#include 

// Windows特有の初期化(信号ハンドラやリソース制限のダミー設定)
kissat * kissat_win_init (void);

// 変数の保護(簡約化による消滅を防止)
void kissat_win_freeze_variables (kissat *solver, int first, int last);

// 外部から値をセットして求解し、学習内容を保存する一括処理
// 戻り値: 10 (SAT), 20 (UNSAT), 0 (UNKNOWN)
int kissat_win_solve_step (
    kissat *solver, 
    const int *phases,      // 変数1〜Nに対するTrue(1)/False(0)の配列
    int num_phases, 
    unsigned lbd_limit,     // 学習節の保存しきい値 (7等)
    const char *learned_path // 学習節の保存先 (NULLなら保存しない)
);

// 解(Model)の取得
int kissat_win_get_value (kissat *solver, int lit);

#endif

2026年2月21日土曜日

CNF2LPコンバータ制作

 世の中には、既にあるので、目新しくはありません。


msakai/toysolver: My sandbox for experimenting with solver algorithms.

作者は、Preferred Networks, Inc.の酒井さんです。色々なフォーマットのコンバージョンをサポートしています。

以下に、AIとの会話を示します。コードも書いてありますが、しっかりバグがありました。行頭が、(マイナス)ー項で始まると、ーが除去されてしまうので、使用しないでください。

MPSファイルもFortran風の謎の記述制約がありますが、LPファイルもその流れを汲んでいるようです。この時代のパーサを組んだ人は、古風な記法が好きなのかな?と思いました。


AT(充足可能性問題)の構造をLP(線形計画法)ファイル形式で記述する場合、主にCPLEX LPフォーマットが標準的に利用されます。この形式は人間が読みやすい代数的な記述が特徴です。

以下に、LPファイルの主要なセクションとその構造を解説します。
1. 目的関数 (Objective Function)
Maximize」または「Minimize」で始めます。SAT問題を単に解くだけ(充足可能性の判定)なら、目的関数は定数(例:0)でも構いません。
text
Maximize
 obj: 0
コードは注意してご使用ください。
2. 制約式 (Constraints)
Subject To」(または st)で始め、SATの各節(クローズ)を不等式として記述します。
  • 変数の表現: 各論理変数を 
     とし、真を 
    、偽を 
     とみなします。
  • 節の変換: 例えば節 
     は、
     と記述します。
text
Subject To
 c1: x1 - x2 + x3 >= 0
 \ (x1 + (1 - x2) + x3 >= 1 を整理したもの)
コードは注意してご使用ください。
3. 変数の範囲と型 (Bounds & Types)
SAT変数は 
 か 
 の値しか取らないため、バイナリ変数として宣言します。
  • Bounds: 変数の下限・上限を指定します(バイナリ宣言すれば省略可能)。
  • Binaries: 変数を 
     または 
     に制限します。
text
Binaries
 x1 x2 x3
コードは注意してご使用ください。
4. 終了キーワード
ファイルの最後には必ず End を記述します。

LPファイルの全体構成例
SATの節 
 と 
 を表現する例です。
text
\ コメントはバックスラッシュで記述可能
Maximize
 obj: 0
Subject To
 c1: x1 - x2 >= 0       \ x1 + (1 - x2) >= 1
 c2: - x1 + x3 >= 0      \ (1 - x1) + x3 >= 1
Binaries
 x1 x2 x3
End
コードは注意してご使用ください。
実装上の注意点:
  • 変数名: アルファベットで始め、最大255文字まで使用可能です。
  • 定数項: 変数のない定数は、制約式の右辺(RHS)に寄せる必要があります。


    changeColBound的な事は、LPファイル上ではできないのですね。
    いいえ、LPファイル(CPLEX LPフォーマット)でも変数の境界(Bounds)を個別に指定する
    Bounds セクションが存在します
    API(changeColBound 等)のようにプログラム実行中に動的に変更するのではなく、ファイル記述として変数の動域を制限することが可能です。
    LPファイルでの境界指定(Bounds)
    制約式(Subject To)の後、Binaries や End の前に Bounds セクションを設けます。
    記述ルール
    • デフォルトでは、すべての変数は 
       (非負)とみなされます。
    • SAT問題でバイナリ変数として扱う場合は、後述の Binaries セクションに書けば自動的に 
       または 
       に固定されますが、Bounds で明示的に書くこともできます。
    例:特定の変数の範囲を固定・制限する
    text
    Subject To
     ... (制約式) ...
    
    Bounds
      x1 = 1            \ x1 を 1 に固定 (真に固定)
      0 <= x2 <= 1      \ x2 の範囲を指定
      x3 >= 0.5         \ 下限だけ指定
      x4 free           \ x4 を自由変数(-∞ to +∞)にする
    
    Binaries
      x1 x2 x3 x4
    End
    
    コードは注意してご使用ください。
    まとめ:APIとの対応
    • プログラム(API)lp.set_col_bounds(col, lb, ub) や changeColBound で逐次変更。
    • LPファイルBounds セクションに lb <= var <= ub の形式で静的に記述。
    もし、SATの「特定の変数に特定の値を割り当てた状態(Partial Assignment)」で解きたい場合は、この Bounds セクションで x1 = 1 のように記述するのが正解です。

HIGHSは、LPファイルをサポートしていますか?
はい、
HiGHSはLPファイル(CPLEX LPフォーマット)を公式にサポートしています。 1.2.21.3.6
HiGHS Documentationによると、.lp という拡張子のファイルを読み込むことで、モデルを自動的に解釈して実行できます。 

C++コードコンバータは、出来ますか? 通常CNF用と、Cardinals用があると良いです。RHSに定数をまとめてくれるのを希望。
SATの節(CNF)および基数制約(Cardinality Constraints)を、定数項を右辺(RHS)にまとめたLPファイル形式へ変換するC++クラスを作成しました。
このコンバータは、各変数を 
 として出力し、否定リテラル 
 を 
 と展開して定数項を計算します。
SAT to LP Converter (C++)
cpp
#include <iostream>
#include <vector>
#include <string>
#include <numeric>
#include <fstream>

class SatToLpConverter {
private:
    int max_var_index = 0;
    std::vector<std::string> constraints;

    void update_max_var(const std::vector<int>& literals) {
        for (int lit : literals) {
            max_var_index = std::max(max_var_index, std::abs(lit));
        }
    }

public:
    // 通常のCNF節 (l1 v l2 v ... v ln) -> l1 + l2 + ... >= 1
    void addClause(const std::vector<int>& literals) {
        update_max_var(literals);
        long rhs = 1;
        std::string expr = "";

        for (size_t i = 0; i < literals.size(); ++i) {
            int lit = literals[i];
            if (lit > 0) {
                expr += (expr.empty() ? "" : " + ") + ("x" + std::to_string(lit));
            } else {
                expr += (expr.empty() ? "" : " - ") + ("x" + std::to_string(-lit));
                rhs -= 1; // (1 - x) の 1 を右辺へ移動
            }
        }
        constraints.push_back(expr + " >= " + std::to_string(rhs));
    }

    // 基数制約 (At-least-k, At-most-k)
    // is_at_least=true で >= k, false で <= k
    void addCardinality(const std::vector<int>& literals, int k, bool is_at_least = true) {
        update_max_var(literals);
        long rhs = k;
        std::string expr = "";

        for (int lit : literals) {
            if (lit > 0) {
                expr += (expr.empty() ? "" : " + ") + ("x" + std::to_string(lit));
            } else {
                expr += (expr.empty() ? "" : " - ") + ("x" + std::to_string(-lit));
                rhs -= 1;
            }
        }
        std::string op = is_at_least ? " >= " : " <= ";
        constraints.push_back(expr + op + std::to_string(rhs));
    }

    void writeToFile(const std::string& filename) {
        std::ofstream out(filename);
        out << "Minimize\n obj: 0\nSubject To\n";
        for (size_t i = 0; i < constraints.size(); ++i) {
            out << " c" << i << ": " << constraints[i] << "\n";
        }
        out << "Binaries\n";
        for (int i = 1; i <= max_var_index; ++i) {
            out << " x" << i << (i % 10 == 0 ? "\n" : " ");
        }
        out << "\nEnd\n";
    }
};

int main() {
    SatToLpConverter conv;
    // 節 (x1 v -x2 v x3)
    conv.addClause({1, -2, 3});
    // 基数制約: x1 + x4 + x5 >= 2
    conv.addCardinality({1, 4, 5}, 2, true);
    
    conv.writeToFile("output.lp");
    return 0;
}
コードは注意してご使用ください。
ロジックの解説
  1. 定数のまとめ方: 否定リテラル 
     が現れた場合、数式上は 
     です。これを 
     のように、定数 
     を右辺に送り出すことで、RHSを自動的に整理しています。
  2. Cardinality対応addCardinality メソッドにより、「
     個のリテラル中 
     個以上(以下)」といった制約を直接LPの1行として出力できます。
  3. 変数の管理: 使用された最大のインデックスを追跡し、最後に一括して Binaries セクションに出力します。
このコードで生成された output.lp を HiGHS C++ API の highs.readModel("output.lp") で読み込めば、そのまま最適化計算が可能です。

ごめんなさい。MIPではなく、LPソルバとして機能させる場合には、どうしたらよいのですか?Boundはそのままに。
SAT問題を純粋な
LP(線形計画法)として解く場合、変数が 
 または 
 という制約を外し、
 の連続変数として扱うことになります。
HiGHSをLPソルバーとして動作させるための手順は以下の通りです。
1. LPファイルの記述を変更する
LPファイル内の Binaries セクションを削除し、代わりに Bounds セクションで範囲を指定します。
text
\ Binaries セクションを削除し、以下のように記述
Bounds
 0 <= x1 <= 1
 0 <= x2 <= 1
 ...
End
コードは注意してご使用ください。
2. C++コンバータの修正
先ほどのコードの writeToFile メソッド内の Binaries 出力部分を、以下のように Bounds 出力に書き換えます。
cpp
void writeToFile(const std::string& filename) {
    std::ofstream out(filename);
    out << "Maximize\n obj: 0\nSubject To\n";
    for (size_t i = 0; i < constraints.size(); ++i) {
        out << " c" << i << ": " << constraints[i] << "\n";
    }
    // Binaries の代わりに Bounds を使用
    out << "Bounds\n";
    for (int i = 1; i <= max_idx; ++i) {
        out << " 0 <= " << getVarName(i) << " <= 1\n";
    }
    out << "End\n";
}
目的関数値の書き方を教えて。SATでは、ビットxkに対して制約違反重みを定数Coeffkで与える。
SATの制約違反に重み(コスト)を持たせる場合、それは一般的に
Weighted MaxSATの形式となります。LPファイルにおいて、特定のビット(変数)
 が「真」になった際のコスト 
 を目的関数に反映させる書き方は以下の通りです。
1. LPファイルでの記述形式
目的関数セクション(Minimize または Maximize)に、変数と係数の積を並べます。
方針: 制約違反を最小化したい場合、違反を表す変数 
 にコスト 
 を掛けて Minimize します。
text
Minimize
 obj: Coeff1 x1 + Coeff2 x2 + Coeff3 x3 ...
Subject To
 ... (SATの制約式) ...
Binaries
 x1 x2 x3 ...
End
コードは注意してご使用ください。

2. C++コンバータへの組み込み例
変数のインデックス 
 と重み 
 を紐付ける std::map を使い、目的関数行を生成するロジックを追加します。
cpp
#include <map>
#include <string>
#include <vector>
#include <fstream>

class SatToLpConverter {
private:
    std::map<int, double> weights; // 変数インデックス -> 重み(Coeff)
    // ... 他のメンバ変数 ...

public:
    // 特定の変数に重みを設定
    void setWeight(int idx, double coeff) {
        weights[idx] = coeff;
    }

    void writeToFile(const std::string& filename) {
        std::ofstream out(filename);
        
        // 1. 目的関数の出力
        out << "Minimize\n obj: ";
        bool first = true;
        for (auto const& [idx, coeff] : weights) {
            std::string name = getVarName(idx);
            if (!first && coeff >= 0) out << " + ";
            else if (coeff < 0) out << " - ";
            
            out << std::abs(coeff) << " " << name;
            first = false;
        }
        if (first) out << "0"; // 重みが設定されていない場合
        out << "\n";

        // 2. 制約式以降の出力
        out << "Subject To\n";
        // ... (以前のコードと同様) ...
    }
};
コードは注意してご使用ください。