packmol-util 0.2.4__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
__init__.py ADDED
File without changes
@@ -0,0 +1,3 @@
1
+ from packmol_util.builder import AmorphousBuilder
2
+ from packmol_util.config import Config
3
+ from packmol_util.packmol import Packmol
@@ -0,0 +1,312 @@
1
+ #!/usr/bin/env python3
2
+ """
3
+ packmol_util/builder.py
4
+
5
+ 詳細なファイルのバケツリレーを隠蔽し、
6
+ 数行でアモルファス構造を作成するための高レベル統合モジュール。
7
+
8
+ 以下の 3 点を外部入力の基準とする:
9
+ 1. 化学量論比(整数比)
10
+ 2. 密度 [g/cm^3] または 数密度 [1/Å^3] のいずれか一方
11
+ 3. セル内の総原子数 total_atom_count
12
+
13
+ 【重要】指定した total_atom_count が ratios の合計で割り切れない(組成比を維持できない)場合、
14
+ プログラムが破綻するのを防ぐため、指定された原子数に最も近い「割り切れる総原子数」へ自動補正を行う。
15
+ 内部で計算された全体のスケール倍率(multiplier)を外部に公開(プロパティ化)することで、
16
+ 利用側のスクリプトがユニット構成単位でのパッキング個数を一元管理できるように設計されている。
17
+ """
18
+
19
+ from typing import Dict, List, Optional, Tuple
20
+ from packmol_util.packmol import Packmol
21
+ from packmol_util.packmol_inp import PackmolInp
22
+
23
+
24
+ class AmorphousBuilder:
25
+
26
+ def __init__(
27
+ self,
28
+ symbols: List[str],
29
+ ratios: List[int],
30
+ total_atom_count: int, # 希望するベースの総原子数
31
+ minimum_separation: float,
32
+ mass_density: Optional[float] = None,
33
+ number_density: Optional[float] = None,
34
+ packmol_bin: str = "packmol",
35
+ ):
36
+ if len(symbols) != len(ratios):
37
+ raise ValueError("symbols と ratios の長さが一致していません。")
38
+
39
+ if total_atom_count <= 0:
40
+ raise ValueError("total_atom_count は正の整数である必要があります。")
41
+
42
+ self.symbols = symbols
43
+ self.ratios = ratios
44
+ self.minimum_separation = minimum_separation
45
+ self.packmol_bin = packmol_bin
46
+
47
+ # --- 【自動補正ロジック】指定できない比率(割り切れない数)のときの自動補正 ---
48
+ ratio_sum = sum(ratios)
49
+ if total_atom_count % ratio_sum != 0:
50
+ # 最も近い整数倍(倍率)を計算
51
+ multiplier = round(total_atom_count / ratio_sum)
52
+ if multiplier < 1:
53
+ multiplier = 1
54
+ adjusted_atom_count = ratio_sum * multiplier
55
+ print(
56
+ f"[Warning] 指定された総原子数 {total_atom_count} は組成比の合計 {ratio_sum} で割り切れません。"
57
+ )
58
+ print(
59
+ f" 組成比を維持するため、総原子数を {adjusted_atom_count} (倍率: {multiplier}) に自動調整しました。"
60
+ )
61
+ self.total_atom_count = adjusted_atom_count
62
+ self._multiplier = multiplier
63
+ else:
64
+ self.total_atom_count = total_atom_count
65
+ self._multiplier = total_atom_count // ratio_sum
66
+
67
+ # 密度の計算とセル一辺の長さ決定
68
+ self.packmol_runner = Packmol()
69
+ if number_density is not None:
70
+ self.box_length = self.packmol_runner.cell_length_from_number_density(
71
+ self.total_atom_count, number_density
72
+ )
73
+ elif mass_density is not None:
74
+ n_density = self.packmol_runner.number_density_from_mass_density(
75
+ symbols, ratios, mass_density
76
+ )
77
+ self.box_length = self.packmol_runner.cell_length_from_number_density(
78
+ self.total_atom_count, n_density
79
+ )
80
+ else:
81
+ raise ValueError(
82
+ "mass_density または number_density のどちらかを指定してください。"
83
+ )
84
+
85
+ self.templates: List[dict] = []
86
+
87
+ @property
88
+ def multiplier(self) -> int:
89
+ """
90
+ 全体の化学量論比に対するセルのスケール倍率を返す。
91
+ 利用側のスクリプトは、この倍率をユニットの1式あたり構成比に乗算することで、
92
+ Packmol に引き渡すべき実際の複製個数(number)を算出する。
93
+ """
94
+ return self._multiplier
95
+
96
+ def add_template(
97
+ self,
98
+ xyz_file: str,
99
+ number: int, # そのテンプレートを Packmol がセル内に何個配置するかを表す実際の複製個数
100
+ packing_radii: Optional[Dict[str, float]] = None,
101
+ fnc_pairs_in_template: Optional[List[Tuple[int, int, int]]] = None,
102
+ fnc_distance_ranges: Optional[Dict[int, Tuple[float, float]]] = None,
103
+ # packmol後に、FNC作成での形状固定の選択
104
+ fnc_output_constraint_types: Optional[List[int]] = None,
105
+ ):
106
+ """
107
+ Packmol の 1 つの structure ブロックに対応するテンプレート情報を登録する。
108
+
109
+ 引数の説明:
110
+ xyz_file: Packmol に渡す単原子または分子ユニットテンプレートの座標ファイル名 (.xyz)
111
+ number: そのテンプレートを何個複製して配置するかを指定する [int]
112
+ packing_radii: Packmol 配置時に使う元素記号別の排除半径の辞書(任意)。
113
+ 分子ユニット内部の結合距離や形状を作る値ではない。
114
+ fnc_pairs_in_template: テンプレート内の局所原子番号で指定した FNC ペア。
115
+ (local_i, local_j, constraint_type) のリストで指定する。
116
+ fnc_distance_ranges: FNC constraint type ごとの距離範囲。
117
+ {type: (rmin, rmax)} で指定する。
118
+ fnc_output_constraint_types: .fnc に実際に出力する constraint type のリスト。
119
+ None の場合は全 type を出力する。
120
+ 例: [1] を指定すると type 1 のペアだけを出力する。
121
+ Packmol 用 XYZ テンプレートの形状や配置には影響しない。
122
+ """
123
+ # builder では、テンプレートをまだ Packmol 入力へ変換しない。
124
+ # ここでは「どの XYZ テンプレートを何個置くか」と、必要なら配置用 packing 半径だけを保存する。
125
+ # 具体的な structure ... end structure への変換は build() 内の PackmolInp が担当する。
126
+ self.templates.append(
127
+ {
128
+ "xyz": xyz_file,
129
+ "number": number,
130
+ "radii": packing_radii,
131
+ "fnc_pairs_in_template": fnc_pairs_in_template,
132
+ "fnc_distance_ranges": fnc_distance_ranges,
133
+ # Packmol は xyz_file のテンプレート形状をそのまま配置する。
134
+ # この指定は Packmol には渡さず、Packmol 後に .fnc を作る段階だけで使う。
135
+ # None: 全 FNC type を出力する。
136
+ # [1]: type 1 の拘束だけを .fnc に出力する。
137
+ "fnc_output_constraint_types": fnc_output_constraint_types,
138
+ }
139
+ )
140
+
141
+ def build(self, output_prefix: str, fnc_file: Optional[str] = None):
142
+ # output_prefix は、一連の生成ファイル名の接頭辞として使う。
143
+ # 例: output_prefix="Li3PO4" の場合、Li3PO4_packmol.inp / Li3PO4_packmol.xyz / Li3PO4_packmol.log を作る。
144
+ input_file = f"{output_prefix}_packmol.inp"
145
+ output_xyz = f"{output_prefix}_packmol.xyz"
146
+ output_log = f"{output_prefix}_packmol.log"
147
+ poscar_file = "POSCAR"
148
+ vasp_file = "POSCAR.vasp"
149
+ if fnc_file is None:
150
+ fnc_file = f"{output_prefix}.fnc"
151
+
152
+ # 1. PackmolInp を構築する。
153
+ # AmorphousBuilder が持つ情報は Python 側の抽象表現である。
154
+ # PackmolInp は、それを Packmol の .inp 文法へ落とす役割を持つ。
155
+ inp_builder = PackmolInp(
156
+ minimum_separation_distance=self.minimum_separation,
157
+ output_xyz=output_xyz,
158
+ box_length=self.box_length,
159
+ use_pbc=True,
160
+ )
161
+ for t in self.templates:
162
+ # 1 件の template は Packmol 入力の 1 つの structure ブロックに対応する。
163
+ # 単原子テンプレートでも PO4 のような分子ユニットでも、ここでは同じ登録処理を通す。
164
+ inp_builder.add_structure(
165
+ xyz_file=t["xyz"],
166
+ number=t["number"],
167
+ packing_radii_by_atom_symbol=t["radii"],
168
+ )
169
+ # 登録した template 群を Packmol 入力ファイルとして書き出す。
170
+ inp_builder.write(input_file)
171
+
172
+ # 2. Packmol を実行し、生成された XYZ を POSCAR に変換する。
173
+ self.packmol_runner.run_packmol(
174
+ input_file=input_file,
175
+ output_log=output_log,
176
+ packmol_bin=self.packmol_bin,
177
+ )
178
+
179
+ # Packmol 出力は Cartesian 座標なので、VASP POSCAR 用に Direct 座標へ変換する。
180
+ xyz_to_poscar_index = self.packmol_runner.xyz_to_poscar(
181
+ xyz_file=output_xyz,
182
+ poscar_file=poscar_file,
183
+ packmol_inp=input_file,
184
+ comment=f"{output_prefix} amorphous initial model generated by Packmol (Total Atoms: {self.total_atom_count})",
185
+ elements=self.symbols,
186
+ )
187
+
188
+ self._write_fnc_from_templates(
189
+ output_xyz=output_xyz,
190
+ xyz_to_poscar_index=xyz_to_poscar_index,
191
+ fnc_file=fnc_file,
192
+ output_prefix=output_prefix,
193
+ )
194
+
195
+ # 外部ツール連携用に vasp.xyz(あるいは POSCAR.vasp)も同様に出力する。
196
+ self.packmol_runner.xyz_to_poscar(
197
+ xyz_file=output_xyz,
198
+ poscar_file=vasp_file,
199
+ packmol_inp=input_file,
200
+ comment=f"{output_prefix} amorphous initial model vasp format",
201
+ elements=self.symbols,
202
+ )
203
+
204
+ def _write_fnc_from_templates(
205
+ self,
206
+ output_xyz: str,
207
+ xyz_to_poscar_index: Dict[int, int],
208
+ fnc_file: str,
209
+ output_prefix: str,
210
+ ) -> None:
211
+ """
212
+ 登録済みテンプレートの FNC 局所ペアを、最終 POSCAR/cfg 番号へ変換して *.fnc を書く。
213
+
214
+ Packmol 出力 XYZ ではテンプレート単位の原子順が残る。一方、POSCAR 変換では
215
+ 元素ごとに座標を並べ替えるため、PO4 の P と O の番号は離れる。
216
+ このメソッドは、Packmol XYZ 番号を中間番号として使い、
217
+ テンプレート内局所番号から最終 POSCAR/cfg 番号の FNC ペアを作る。
218
+ """
219
+ all_fnc_pairs: List[Tuple[int, int, int]] = []
220
+ merged_distance_ranges: Dict[int, Tuple[float, float]] = {}
221
+
222
+ xyz_start_index = 1
223
+ for template in self.templates:
224
+ xyz_file = str(template["xyz"])
225
+ number = int(template["number"])
226
+ fnc_pairs_in_template = template.get("fnc_pairs_in_template")
227
+ fnc_distance_ranges = template.get("fnc_distance_ranges")
228
+ # packmol後に形状固定でのFNCの選択有無
229
+ fnc_output_constraint_types = template.get("fnc_output_constraint_types")
230
+
231
+ atom_count_in_template = self.packmol_runner.count_atoms_in_xyz(
232
+ xyz_file=xyz_file,
233
+ )
234
+
235
+ if fnc_pairs_in_template is not None:
236
+ if fnc_distance_ranges is None:
237
+ raise ValueError(
238
+ f"{xyz_file} に fnc_pairs_in_template が指定されているが、"
239
+ "fnc_distance_ranges が指定されていない。"
240
+ )
241
+ ########################################
242
+ # Packmol 用テンプレート構造は変更しない。
243
+ # ここでは Packmol 後の最終構造から .fnc を作る段階だけで、
244
+ # RMC に渡す constraint type を選別する。
245
+ if fnc_output_constraint_types is None:
246
+ active_constraint_types = list(fnc_distance_ranges.keys())
247
+ else:
248
+ active_constraint_types = list(fnc_output_constraint_types)
249
+
250
+ active_constraint_type_set = set(active_constraint_types)
251
+
252
+ for type_index in active_constraint_types:
253
+ if type_index not in fnc_distance_ranges:
254
+ raise ValueError(
255
+ f"{xyz_file} の fnc_output_constraint_types に type={type_index} "
256
+ "が指定されているが、fnc_distance_ranges に存在しない。"
257
+ )
258
+
259
+ active_fnc_pairs_in_template = [
260
+ pair
261
+ for pair in fnc_pairs_in_template
262
+ if pair[2] in active_constraint_type_set
263
+ ]
264
+
265
+ active_fnc_distance_ranges = {
266
+ type_index: fnc_distance_ranges[type_index]
267
+ for type_index in active_constraint_types
268
+ }
269
+ ############################
270
+
271
+ for type_index, distance_range in active_fnc_distance_ranges.items():
272
+ if type_index in merged_distance_ranges:
273
+ if merged_distance_ranges[type_index] != distance_range:
274
+ raise ValueError(
275
+ "同じ FNC constraint type に異なる距離範囲が指定されている。"
276
+ f" type={type_index}"
277
+ )
278
+ else:
279
+ merged_distance_ranges[type_index] = distance_range
280
+
281
+ copy_index = 0
282
+ while copy_index < number:
283
+ molecule_xyz_start = (
284
+ xyz_start_index + copy_index * atom_count_in_template
285
+ )
286
+ for (
287
+ local_i,
288
+ local_j,
289
+ constraint_type,
290
+ ) in active_fnc_pairs_in_template:
291
+ xyz_i = molecule_xyz_start + local_i - 1
292
+ xyz_j = molecule_xyz_start + local_j - 1
293
+ poscar_i = xyz_to_poscar_index[xyz_i]
294
+ poscar_j = xyz_to_poscar_index[xyz_j]
295
+ all_fnc_pairs.append((poscar_i, poscar_j, constraint_type))
296
+ copy_index = copy_index + 1
297
+
298
+ xyz_start_index = xyz_start_index + number * atom_count_in_template
299
+
300
+ if len(all_fnc_pairs) == 0:
301
+ return
302
+
303
+ self.packmol_runner.write_fnc_file(
304
+ fnc_file=fnc_file,
305
+ total_atom_count=self.total_atom_count,
306
+ fnc_pairs=all_fnc_pairs,
307
+ distance_ranges_by_type=merged_distance_ranges,
308
+ title=f"{output_prefix} FNC generated from Packmol template mapping",
309
+ )
310
+
311
+
312
+ #
packmol_util/config.py ADDED
@@ -0,0 +1,79 @@
1
+ #!/usr/bin/env python3
2
+ # -*- coding: utf-8 -*-
3
+ """packmol_util の設定保持クラス。"""
4
+
5
+ from typing import Optional, Sequence, Union
6
+ import os
7
+
8
+ import yaml
9
+ from x_logger import XLogger
10
+
11
+
12
+ class Config:
13
+ """packmol_util 用の設定値を保持する。"""
14
+
15
+ N_tot: int = 2000
16
+ mass_density_g_cm3: float = 7.3336
17
+ number_density_A3: float = 0.09119
18
+ element_info: Union[str, Sequence[str]] = ["Fe", "Si", "B"]
19
+ ratios: Sequence[int] = [82, 4, 14]
20
+
21
+ packmol_bin: str = "packmol"
22
+ minimum_separation: float = 2.0
23
+ output_prefix: str = "amorphous"
24
+
25
+ def __init__(
26
+ self,
27
+ yaml_path: Optional[str] = None,
28
+ logger: Optional[XLogger] = None,
29
+ ) -> None:
30
+ if logger is None:
31
+ logger = XLogger(logger_name=__name__)
32
+ self._logger = logger
33
+
34
+ if yaml_path:
35
+ self._load_yaml(yaml_path)
36
+
37
+ @classmethod
38
+ def load_yaml(cls, yaml_path: Optional[str] = None) -> None:
39
+ """YAML の内容でクラス変数を上書きする。"""
40
+ if yaml_path is None:
41
+ yaml_path = os.environ.get("PACKMOL_UTIL_CONFIG_YAML")
42
+ if yaml_path is None:
43
+ return
44
+
45
+ with open(yaml_path, "r") as f:
46
+ data = yaml.safe_load(f)
47
+
48
+ if data is None:
49
+ return
50
+
51
+ for key, value in data.items():
52
+ if hasattr(cls, key):
53
+ setattr(cls, key, value)
54
+ else:
55
+ print("NO SUCH VAR:", key)
56
+
57
+ def _load_yaml(self, yaml_path: Optional[str] = None) -> None:
58
+ """YAML の内容でインスタンス属性を上書きする。"""
59
+ self._logger.info(f"load_yaml: {yaml_path}")
60
+
61
+ if yaml_path is None:
62
+ yaml_path = os.environ.get("PACKMOL_UTIL_CONFIG_YAML")
63
+ if yaml_path is None:
64
+ return
65
+
66
+ yaml_path = os.path.abspath(yaml_path)
67
+ self._logger.info(f"abspath: {yaml_path}")
68
+
69
+ with open(yaml_path, "r") as f:
70
+ data = yaml.safe_load(f)
71
+
72
+ if data is None:
73
+ return
74
+
75
+ for key, value in data.items():
76
+ if hasattr(self, key):
77
+ setattr(self, key, value)
78
+ else:
79
+ print("NO SUCH VAR:", key)