Home
4544 words
23 minutes
【計算化学】自作pythonライブラリで遷移状態構造を求めてみる(BH9データセットの4. Hydrogen atom transfer, No. 38の化学反応, NNP(UMA)使用)

最終更新:2025-11-08

概要#

本記事では、自作ライブラリ(MultiOptPy)で、BH9データセットの4. Hydrogen atom transfer, No. 38の素過程の遷移状態構造を算出してみる。計算レベルは、Meta社のFAIR Chemistryが開発したニューラルネットワークポテンシャル(NNP)であるUMA(Meta’s Universal Model for Atoms)とした。

MultiOptPyは電子状態計算ソフトウェアを用いた分子構造最適化手法の勉強を目的として作成したpythonライブラリである。

MultiOptPyのレポジトリ:https://github.com/ss0832/MultiOptPy

BH9のデータセットについて:

  • J. Chem. Theory Comput. 2022, 18, 1, 151–166

https://doi.org/10.1021/acs.jctc.1c00694

この文献のSupporting Informationから、データセットの詳細を確認できる。

有機金属錯体が関わる反応を除いたさまざまなカテゴリの反応がまとめられたデータセットである。DFTの汎関数の電子エネルギーの精度の比較などのベンチマークに主に使われる。

今回使用したニューラルネットワークポテンシャルについて:

使用した自作ライブラリMultiOptPyのバージョン#

v1.19.4b

環境#

Windows 11

※Windows 11環境下でAnaconda PowerShell Promptを使用した。

Source codeのダウンロード(Unixコマンド)#

wget https://github.com/ss0832/MultiOptPy/archive/refs/tags/v1.19.4b.zip
unzip v1.19.4b.zip
cd MultiOptPy-v1.19.4b

https://github.com/ss0832/MultiOptPy/releases/tag/v1.19.4b にアクセスしてzipファイルをダウンロードする。Unixコマンドの場合とはディレクトリ名が異なるので都度読み替えていただけると良い。

移動先のディレクトリでrequirements.txtを参照することで、本ソースコードで必要なモジュールを把握することが出来る。導入方法は各自の状況に合わせて適宜LLMとの対話などで調べると良い。

次に述べる環境構築手順を使用する場合は、環境構築が終わった後、pip install -r requirements.txtで本自作モジュールが動作させるために最低限必要なモジュールを導入することが可能である。

環境構築手順#

今回は、Windows 11のPower Shellを使用した。初めに、NNPを使用できる環境が整ったAnaconda PowerShell Promptを用意する手順を説明する。

1, https://repo.anaconda.com/archive/ より、Anaconda3-2025.06-1-Windows-x86_64.exeでAnacondaをインストールする。

2, 検索機能を使い、スタートからAnaconda PowerShell Promptを開く。

3, 以下のコマンドを実行し、仮想環境を作成する。

conda create -n (任意の仮想環境名) python=3.12.7

4, 先ほど作成した仮想環境をconda activate (仮想環境名)で起動させる。

5, 以下のコマンドを実行し、必要なライブラリを導入する。

pip install ase==3.26.0 fairchem-core==2.7.1 torch==2.6.0
  • fairchem-coreは、FAIR Chemistryが管理しているNNPを動作させるために必要なライブラリである。
  • aseはNNPに電子エネルギーを算出したい分子構造を渡すために必要なインターフェイスの役割を果たすために必要なライブラリである。
  • torchはPyTorchライブラリを指す。これはニューラルネットワークなどの機械学習を行ったり、学習結果を扱ったりするために必須なライブラリである。

これで、Anaconda PowerShell Promptから仮想環境を立ち上げることで、NNPを使用する準備が整えることが出来る。

次に、NNPを使用するために必要なModelの情報が保存されている.ptファイルのダウンロードおよびNNPの自作ライブラリへの導入方法について説明する。

1, 以下のサイトにアクセスして、uma-s-1p1.ptをダウンロードする。(使用許諾が下りていれば可能である。)

https://huggingface.co/facebook/UMA

2, ダウンロード後、MultiOptPy-v1.19.4bディレクトリ内に存在するsoftware_path.confに対して、uma-s-1p1.ptの絶対パスを用いて以下を追記する。

uma-s-1p1::(uma-s-1p1.ptの絶対パス)

これで、MultiOptPy-v1.19.4bがNNPuma-s-1p1を使用できるようになる。

使用するNNPに関する具体的な説明#

今回使用するNNPについて具体的に説明する。

  • UMAのModel Checkpointはuma-s-1p1を使用した。
  • 小分子系のトレーニングセットであるOmol25(omol)を使用して学習したニューラルネットワークポテンシャルを使用する。

※自作ライブラリでの具体的な使用の仕方に関しては、ase_calculation_tools.py を参照すると良い。omol以外のモデルを使用したい場合は、現バージョンでは、multioptpy/Calculator/ase_tools/firechem.py内の、self.task_nameを編集することで対応可能である。

手順#

1. 初期構造の準備#

モデル反応系として、以下の構造を用意した。今回はファイルの名前をbh9_4_38.xyzとした。 初期構造は以下のものを使用した。

27
OptimizedStructure
C     -0.487927360673      0.897825639654      1.339833932367
C     -1.725169865606      1.275286305543      0.533197742850
O     -2.017485019643      0.100192853333     -0.245033062977
C     -1.711422308579     -1.049724550288      0.522849802749
C     -0.647927647359     -0.592567229120      1.519572626200
H     -0.383919584570      1.406967459457      2.296060551349
C     -1.551056613773      2.440757527194     -0.407294324224
H     -2.571320795373      1.454955675826      1.210331906771
H     -2.451754881016      2.588294830357     -1.002700703431
H     -0.709100134577      2.255968959436     -1.075327327967
H     -1.356605687530      3.353310750141      0.157873883407
C     -1.206962964514     -2.139017809397     -0.399644531869
H     -2.587677039136     -1.402172498706      1.085119799710
O     -0.079393551594     -1.695533542479     -1.129263562788
H     -2.014574975151     -2.452787814636     -1.068710631088
H     -0.892508988098     -2.993567745060      0.198900138504
H     -0.323549584283     -0.861653674454     -1.553833212730
O     -0.070493879257     -1.312355140443      2.288996389717
C      3.039482511037     -0.467341228512      0.105749506835
C      2.587165801928     -0.173009996900     -1.312339535147
H      2.258934797698     -1.007692290800      0.641752164444
H      3.259224811013      0.458211603204      0.639063103835
H      3.938423432240     -1.085286148702      0.098501213391
H      3.379169318853      0.354774560533     -1.882199722756
H      2.397581972601     -1.088240722228     -1.890578329612
H      0.413801416292      1.065561662333      0.741960565137
O      1.515066819070      0.668842564715     -1.412838382676
初期経路を求めるための初期構造

2. 遷移状態構造最適化#

 run_autots.pyを適切に使用することで、自動的に遷移状態構造が得られる。以下にその手順を説明していく。

初期構造をカレントディレクトリにbh9_4_38.xyzとして保存する。その後、同じディレクトリ内で、config_bh9_4_38.jsonを作成し、以下のように記述する。

config_bh9_4_38.json

{
  "work_dir": "bh9_4_38",
  "top_n_candidates": 3,
  "multioptpy_version": "v1.19.4b",
  
  "step1_settings": {
    "othersoft": "uma-s-1p1",
    "opt_method": ["rsirfo_block_fsb"],
    "use_model_hessian": "fischerd3",
    "spin_multiplicity": 2,
    "electronic_charge": 0,
	"manual_AFIR": ["300", "27", "26"]
  },
  
  "step2_settings": {
    "othersoft": "uma-s-1p1",
    "NSTEP": 15,
    "ANEB": [3, 5],
    "QSM": true,
    "use_model_hessian": "fischerd3",
    "save_pict": true,
    "node_distance_bernstein": 0.80,
    "align_distances": 9999,
    "spin_multiplicity": 2,
    "electronic_charge": 0
  },
  
  "step3_settings": {
    "othersoft": "uma-s-1p1",
    "opt_method": ["rsirfo_block_bofill"],
    "calc_exact_hess": 5,
    "tight_convergence_criteria": true,
    "max_trust_radius": 0.2,
    "frequency_analysis": true,
    "spin_multiplicity": 2,
    "electronic_charge": 0
  },

  "step4_settings": {
    "othersoft": "uma-s-1p1",
	"opt_method": ["rsirfo_block_bofill"],
    "spin_multiplicity": 2,
    "electronic_charge": 0,
	"calc_exact_hess": 10,
    "tight_convergence_criteria": true,
    "frequency_analysis": true,
    
    "intrinsic_reaction_coordinates": ["0.5", "200", "lqa"],

    "step4b_opt_method": ["rsirfo_block_fsb"]
  }
}

その後、以下のコマンドを実行する。

python run_autots.py bh9_4_38.xyz -cfg config_bh9_4_38.json

これにより、これまでの似た内容の記事で行ってきたコマンドの操作をまとめ、遷移状態構造を求める処理を自動的に行う。

具体的な処理の流れは、

Step1. バイアスポテンシャルによるNEB法のための初期経路の作成

Step2. NEB法による経路の緩和

Step3. NEB法により得られた経路のエネルギー極大値を示す構造のうち、エネルギー値が上位の最大で3個
(`run_autots.py`にて、`--top_n X`で最大値を変更可能)の構造を初期構造とした遷移状態構造の算出

(Step4.得られた遷移状態構造に対するIRC計算とIRC経路の末端に存在する構造に対する構造最適化。
こちらは`--run_step4`をコマンドで追記しなければ行わない。)

となっている。

run_autots.pyのオプションの説明:

  • -cfg YYY.jsonは、workflowを実行するためのオプションが記されたJSONファイルの読み込み先を指定する。

これらの一連の結果は、(jsonファイルの"work_dir"にて指定した名前)のディレクトリの中に存在するファイルを開いて確認できる。

以下にすべてのstepで共通のオプションに関する説明を載せる。

  • "opt_method": ["rsirfo_block_fsb"]は準ニュートン法であるRS-I-RFO法を構造最適化に使用することを示す。初期のへシアンに関しては、特にオプションで指定しない限り、単位行列が使われる。(以前のHessian更新法とは細かな点で異なる方法を使用している。具体的には、複数の座標変位や勾配変位を用いてHessianの更新を行う。)
  • "spin_multiplicity": Zはスピン多重度の指定である。PySCFを使用するときは目的とするスピン多重度に1を引いた値を指定する。(デフォルトでは1が指定される。)
  • "electronic_charge": 0は形式電荷をMとすることを示す。(デフォルトでは0が指定される。)
  • "othersoft": "uma-s-1p1"は今回使用するNNPを指定している。これを使用する際にASEライブラリが必要である。
  • "use_model_hessian": "fischerd3"は、計算コストが非常に低い数式を使用して、近似したHessianを生成する機能を呼び出すオプションである。デフォルトではこの機能は使用されない。

※オプションの説明はMultiOptPy-v1.19.4b/OPTION_README.mdにて示されている。

Step 1#

Step1では、omolのデータセットを使用したuma-s-1p1モデルのNNPで得たエネルギーに対して、指定した人工力ポテンシャルを加えた上で初期構造を構造最適化を行っている。

以下のJSON内で記述したバイアスポテンシャルで、次の経路緩和アルゴリズムの初期経路として用いるトラジェクトリーを生成する。

  • "manual_AFIR": ["yyy", "a", "b]:yyykJ/molの活性化障壁を超えうるペア同士を近づける力を原子のラベル番号aとbのペアに構造最適化時に加えることを示す。

Step1が正常終了していれば作成されたwork_dirディレクトリ中に、bh9_4_38_step1_traj.xyzが存在する。必要に応じて確認し、目的に沿った初期経路が得られているか確認する。もし想定とは異なる場合は、プロセスをkillして再度設定を見直してやり直す。

bh9_4_38_step1_traj.xyzは構造最適化の過程をAvogadro(公式ページ:https://avogadro.cc/ )等で可視化して確認できるようにしている。このbh9_4_38_step1_traj.xyzはStep2のNEB計算に使用している。

bh9_4_38_step1_traj.xyzをアニメーションとして表示したい場合は、[https://github.com/ss0832/molecule_movie] を使うと良い。

Step 2#

Step2では、NEB法を用いることで、先ほど得られたbh9_4_38_step1_traj.xyz全体のエネルギーを下げることができる。これにより、パスのエネルギー極大値を持つ構造を遷移状態構造に近づける。(この時点ではまだ正確な遷移状態構造は求められていない。)

Step2固有のオプションについて以下に示す。

  • "NSTEP": nはn回分NEB法による経路の緩和を行うことを示す。
  • "align_distances": Xは線形補間で、各ノード間の距離を全て等しくするための処理である。X回の反復計算ごとに本処理を行う。Xを"NSTEP": nよりも大きな数値を指定することで、初期経路に対してのみ処理を行うことが出来る。
  • "node_distance_bernstein": Nはノード間の距離をN Åとして初期経路を作成することを示す。経路作成時に元のノードをベルンシュタイン多項式を用いてがたついた経路を滑らかにする。

→プログラムの仕様上"align_distances": Xの処理を行った後に、"node_distance_bernstein": Nの処理を行うようになっている。

  • "save_pict": trueは緩和中のパスのエネルギープロファイルや各ノードの勾配のRMS値をmatplotlibで可視化するオプションである。
  • "ANEB": [A, B]これを指定すると、(B+1)回の緩和ごとに、エネルギー極大値を示すノードと前後のノードの間に線形補間でA個の新規ノードを内挿するようにできる。デフォルトではこのような操作は行われない。このオプションを使用するとノードの数が徐々に増えるため、計算コストが使用しない場合と比べて増加する。一方で、エネルギー極大値を示すノード周辺にノードを増加させるため、緩和している経路中のノードが遷移状態構造付近に存在する可能性が高くすることが出来る。

MultiOptPy-v1.19.4b/"work_dir"と同じディレクトリ内に、NEBという名前を含むディレクトリが生成されている。 そのディレクトリ内のenergy_plot.csvを確認し、緩和後のパスのエネルギー極大値を示す構造を確認する。

経路の緩和後の各ノードのエネルギー一覧(単位) (energy_plot.csvに保存されている。)

NEB計算の結果の可視化
NEB計算の結果の可視化

bias_force_rms.csvにて、各Iterationごとのすべてのノードの勾配のRMS値を確認できる。

経路緩和の結果、以下の構造がstep3の初期構造として用いられることとなった。“work_dir”内のbh9_4_38_step3_TS_Opt_Inputs内に保存されたbh9_4_38_ts_guess_X.xyzにて確認が可能である。ts_guessの番号が小さい順にエネルギー値が高い構造を示すようになっている。

※こちら[https://ss0832.github.io/molecule_viewer/] を使うことでも可視化は可能である。

bh9_4_38_ts_guess_1.xyz

27
0 2
C      -0.320604355901      0.801156036312      1.122040817862
C      -1.605211621623      1.240492574891      0.451902910061
O      -2.028777743480      0.077244529039     -0.275044283045
C      -1.709417484723     -1.071566548475      0.501936839830
C      -0.583849128734     -0.624591533714      1.433725597927
H       0.034983626848      1.399373579705      1.955253778300
C      -1.537475158973      2.434227860797     -0.467818588881
H      -2.350420042991      1.430773030241      1.245104016024
H      -2.483527570927      2.569938359364     -0.993260315418
H      -0.737797960938      2.304205958648     -1.193870014157
H      -1.341297305409      3.333557201192      0.118232824009
C      -1.242559144719     -2.177604121758     -0.417837464240
H      -2.567779818184     -1.397527776288      1.102932404941
O      -0.124600938876     -1.752677811340     -1.167875943142
H      -2.063251978557     -2.489240954887     -1.070132133919
H      -0.924015930472     -3.025352636703      0.189138671968
H      -0.323237669317     -0.866064500945     -1.501671330221
O      -0.065227586450     -1.309273489894      2.277334580551
C       3.037557035192     -0.454465123879      0.207906617481
C       2.344077065707     -0.033616534247     -1.075722760917
H       2.364281108790     -1.057105111088      0.818003501757
H       3.344749034251      0.426075018557      0.771445267693
H       3.923929058200     -1.052343740099     -0.012510519058
H       3.038786300642      0.558112061234     -1.683806680101
H       2.038856697246     -0.907803812170     -1.655684935782
H       0.622753806541      0.784854962123      0.199078881378
O       1.259077706858      0.859222523384     -0.878801740904
NEB法により緩和した経路から得られた遷移状態構造を求めるための初期構造 (No.1)

Step 3#

step3のオプションで、追加での説明を要するものを以下に示す。

  • "opt_method": ["rsirfo_block_bofill"]は遷移状態構造の最適化向けのoptimizerを指定することを意味する。準ニュートン法であるRS-I-RFO法を使用する。今回は-fcで正確なHessianを計算するようにしているので、初期Hessianは正確なHessianを使用するようになっている。(Bofill法によるHessianの更新法を細かい点で変更している。具体的には、複数の座標変位や勾配変位を用いてHessianの更新を行う。)
  • "saddle_order": 1は一次の鞍点を求めることを指定する。(step3のデフォルトでは一次の鞍点を指定する。それ以外の値の指定は、プログラムの使用目的上想定していないので、行わないことを勧める。)
  • "calc_exact_hess": 5は5回の反復回数当たり1回正確なHessianを計算することを指定する。
  • "frequency_analysis": trueは収束条件を満たした後に基準振動解析を行うことを示す。(自前で実装しているため、あくまで目安として使用することを推奨する。各振動モードをvibration_animation内のxyzファイルで可視化できる。)UMAモデルから算出されるHessianは数値微分により求めているため、原子数Zが多いとZの二乗オーダーで計算コストが急増する。
  • "tight_convergence_criteria": trueは収束条件を厳しくすることを示す。(Gaussianのtightと同等)
  • "max_trust_radius": Dは一回の反復計算ごとの計算されるステップ幅の最大値をDÅ以下にすることを示す。デフォルトでは、"saddle_order": 1を指定すると0.1Åが指定される。

実行して得られた正確な遷移状態構造と思われる構造を以下に示す。

(実行して得られた正確な遷移状態構造は計算開始時に、MultiOptPy-v1.19.4b/"work_dir"ディレクトリ内に生成された新規ディレクトリ内のbh9_4_38_ts_final_X.xyzとして保存されている。)

bh9_4_38_ts_final_1.xyz

27
OptimizedStructure
C     -0.243830591022      0.684076637331      1.037739698812
C     -1.561552955360      1.195637703162      0.507480606842
O     -2.102960453218      0.082496033911     -0.218668114682
C     -1.709093693340     -1.127457136720      0.413856542650
C     -0.427989321190     -0.780504791700      1.171406176845
H      0.242303171624      1.200625134517      1.859760676369
C     -1.495043268833      2.393589130841     -0.409588741443
H     -2.224766205609      1.413111212163      1.359759208432
H     -2.489218273462      2.634061428055     -0.784230315906
H     -0.835766137293      2.186336462961     -1.251888604584
H     -1.109308210924      3.257998171948      0.132190620198
C     -1.492994138187     -2.185857103520     -0.645677574676
H     -2.464334265886     -1.459774027256      1.139763446065
O     -0.499798730332     -1.782275800331     -1.566244564326
H     -2.443027025942     -2.394337283520     -1.148285644464
H     -1.140473551395     -3.099305582636     -0.167261773321
H     -0.730660325476     -0.894885146153     -1.869643926079
O      0.271688075986     -1.568391008033      1.757294040284
C      3.235228135257     -0.004380228952      0.428400419588
C      2.280954107329     -0.192007343446     -0.732939320543
H      2.692201651221     -0.042714529400      1.374243342782
H      3.753097896490      0.952012609422      0.353735388299
H      3.977191075260     -0.804044383300      0.441541208002
H      2.820462502409     -0.201798727789     -1.689024610789
H      1.746858668113     -1.145449610680     -0.674239073379
H      0.582335177733      0.812310619081      0.065110590633
O      1.368496686048      0.870927560044     -0.884589701610
遷移状態構造 (No.1)

停留点に収束した構造が得られた。-freqオプションにより生成されたnormal_modes.txtvibration_animationディレクトリ内の振動モードのアニメーションを確認した。

以下に-freqオプションで生成されたnormal_modes.txtの一部を示す。

Mode                                 0                   1                   2
Freq [cm^-1]                    -2057.4191             44.1557             73.0856
Reduced mass [au]                   1.0896              3.4754              4.3250
Force const [Dyne/A]               -2.7175              0.0040              0.0136
Char temp [K]                       0.0000             63.5303            105.1539
Normal mode                   x         y         z            x         y         z            x         y         z     
       C                -0.03656   -0.00988    0.03789   -0.00509    0.02251   -0.01423   -0.03038   -0.00375    0.03768
       C                 0.00931   -0.00422   -0.01008   -0.01461   -0.00379   -0.01683   -0.01160   -0.00600   -0.01292
       O                -0.00015    0.00217    0.00085   -0.01416   -0.02440    0.01301    0.02420   -0.00560   -0.04091
       C                -0.00030   -0.00038    0.00124    0.01763   -0.00920    0.02092   -0.00730   -0.00339   -0.01742
       C                 0.00467    0.00277   -0.01132    0.02635    0.01976   -0.00849   -0.04060   -0.00246    0.03971
       H                 0.10713    0.03448   -0.08506   -0.02161    0.03388   -0.01149   -0.05981   -0.00175    0.05393
       C                 0.00034    0.00236   -0.00152   -0.03318   -0.02524   -0.04646    0.01630    0.00139   -0.00105
       H                 0.02901   -0.00032    0.02093   -0.00974    0.01042   -0.01692   -0.04717   -0.01644   -0.03782
       H                -0.00116   -0.00246    0.00359   -0.03806   -0.04576   -0.04674    0.02858   -0.00085   -0.03514
       H                 0.00065   -0.00112    0.00171   -0.03455   -0.03842   -0.04440    0.04878    0.01215    0.02222
       H                 0.00004   -0.00007    0.00007   -0.03965   -0.00749   -0.07020   -0.00883   -0.00119    0.02100
       C                 0.00102    0.00031   -0.00166    0.00983   -0.01989    0.02955    0.03370   -0.01063   -0.00205
       H                -0.00151    0.00042   -0.00067    0.03858   -0.01189    0.04163   -0.03987    0.00286   -0.04842
       O                -0.00126   -0.00052    0.00100   -0.05095    0.00773   -0.02454    0.05329   -0.00550    0.02098
       H                -0.00191    0.00007    0.00391   -0.00429   -0.06868    0.07679    0.05057   -0.02654   -0.02717
       H                -0.00098    0.00057    0.00131    0.07065    0.00345    0.02916    0.03131   -0.00216    0.01590
       H                 0.01140    0.00041   -0.00506   -0.11853   -0.01505   -0.03917    0.04857   -0.00953    0.01188
       O                 0.00107    0.00135    0.00172    0.05341    0.03398   -0.02109   -0.07660    0.00037    0.08611
       C                -0.00091   -0.00119   -0.00021   -0.03893   -0.13482    0.03900    0.06515   -0.05565   -0.11860
       C                 0.00480   -0.00732   -0.00434   -0.00113    0.03538   -0.01986   -0.03028    0.02666   -0.05200
       H                 0.00093    0.00305    0.00571   -0.07893   -0.20718    0.01277    0.13343   -0.05893   -0.07898
       H                -0.00239    0.00060   -0.00181    0.01470   -0.15444    0.15861    0.10452   -0.07750   -0.12442
       H                 0.00386    0.00126    0.00630   -0.08137   -0.17527   -0.01576    0.02888   -0.09079   -0.20087
       H                 0.01534   -0.00838    0.02156    0.03900    0.10733    0.00194   -0.10629    0.04258   -0.09518
       H                 0.02288   -0.01064    0.02595   -0.05284    0.05691   -0.13999   -0.05817    0.04242   -0.05109
       H                 0.60403    0.03552   -0.72261    0.01427    0.04655    0.00259    0.00116    0.00455    0.06308
       O                -0.03605    0.00680    0.03457    0.06011    0.09852    0.04879   -0.00695    0.06253    0.06104
       
(...snip...)

その結果、虚振動が1つであることが確認できた。つまりこの構造は遷移状態構造である。

次に、vibration_animation内の虚振動を示す分子振動が示されたxyzファイル(mode_1_XXXi_wave_number.xyz)をAvogadroで確認すると、求められた遷移状態構造の中に、想定される反応系と生成系をつなぐ方向に振動している構造が存在することを確認できた。

終わりに#

   自作ライブラリで、UMAモデルのニューラルネットワークポテンシャル(NNP, uma-s-1p1)を用いて、BH9データセットの4. Hydrogen atom transfer, No. 38の反応のある1つの遷移状態構造を算出する手順を説明した。

参考#

【計算化学】自作pythonライブラリで遷移状態構造を求めてみる(BH9データセットの4. Hydrogen atom transfer, No. 38の化学反応, NNP(UMA)使用)
https://ss0832.github.io/posts/20251108_mop_usage_bh9_4_38/
Author
ss0832
Published at
2025-11-08