Initial commit: add iodide-PS4 sharing analysis script

This commit is contained in:
佐久間美波
2026-06-29 13:14:45 +09:00
commit 79d3f8ff26
8 changed files with 2485231 additions and 0 deletions
+16
View File
@@ -0,0 +1,16 @@
# Ignore everything
*
# But keep this .gitignore and README
!.gitignore
!README.org
# Keep analysis scripts
!calc_share.py*
# Keep selected trajectory files
!050Li3PS4-050LiI_thin100.lammpstrj
!060Li3PS4-040LiI_thin100.lammpstrj
!070Li3PS4-030LiI_thin100.lammpstrj
!080Li3PS4-020LiI_thin100.lammpstrj
!090Li3PS4-010LiI_thin100.lammpstrj
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
+121
View File
@@ -0,0 +1,121 @@
#+TITLE: LPSI Iodide--PS4 Sharing Analysis
#+AUTHOR:
#+DATE:
* Overview
This repository contains a Python script for analyzing the local environment of iodide ions in Li3PS4--LiI glass systems from LAMMPS trajectory files.
The script classifies each iodine atom based on how many nearby sulfur atoms belong to neighboring PS4 units. In particular, it counts whether an iodine atom is associated with PS4 units through edge-sharing-like or corner-sharing-like configurations.
The analysis is intended for LPSI compositions such as:
- 050Li3PS4-050LiI
- 060Li3PS4-040LiI
- 070Li3PS4-030LiI
- 080Li3PS4-020LiI
* Repository contents
This repository manages the following files:
- =calc_share.py= or =calc_share.py*= :: Python script for the analysis.
- =050Li3PS4-050LiI_thin100.lammpstrj= :: LAMMPS trajectory file.
- =060Li3PS4-040LiI_thin100.lammpstrj= :: LAMMPS trajectory file.
- =070Li3PS4-030LiI_thin100.lammpstrj= :: LAMMPS trajectory file.
- =080Li3PS4-020LiI_thin100.lammpstrj= :: LAMMPS trajectory file.
- =README.org= :: This document.
- =.gitignore= :: Git ignore settings.
* Analysis method
For each timestep in the trajectory, the script performs the following procedure.
1. Read the atomic coordinates of P, S, and I atoms from a LAMMPS trajectory file.
2. Assign each S atom to the nearest P atom, thereby identifying the corresponding PS4 unit.
3. For each I atom, search for S atoms within a cutoff distance.
4. Count how many S atoms belonging to the same PS4 unit are found around the I atom.
5. Classify the local environment of each I atom using the number of edge-like and corner-like contacts.
In the current script, the I--S cutoff distance is set to:
#+begin_src python
I_S_CUTOFF = 4.7
#+end_src
If an iodine atom has two nearby S atoms belonging to the same PS4 unit, this is counted as an edge-like contact.
If it has one nearby S atom belonging to a PS4 unit, this is counted as a corner-like contact.
The final output is a LaTeX table summarizing the fraction of each local environment for each composition.
* Requirements
The script requires Python 3 and the following Python packages:
- numpy
- pandas
These can be installed using pip:
#+begin_src sh
pip install numpy pandas
#+end_src
* Usage
Run the script in the directory containing the target =.lammpstrj= files.
For example:
#+begin_src sh
python calc_share.py -i "*_thin100.lammpstrj"
#+end_src
or, if the script filename is =calc_share_v2.py=:
#+begin_src sh
python calc_share_v2.py -i "*_thin100.lammpstrj"
#+end_src
By default, the output LaTeX table is saved as:
#+begin_src text
summary_table.tex
#+end_src
The output filename can be changed using the =-o= option:
#+begin_src sh
python calc_share.py -i "*_thin100.lammpstrj" -o summary_table.tex
#+end_src
* Output
The script generates a LaTeX table in which each row corresponds to an iodine local environment label, such as:
#+begin_src text
edge_0_corner_1
edge_1_corner_0
edge_1_corner_2
#+end_src
Each column corresponds to a composition.
The values are given as percentages.
Values smaller than or equal to 1% are shown as =trace=.
* Notes
The current implementation assumes that the LAMMPS trajectory files have a fixed format compatible with the parser in =calc_share.py=. In particular, the script assumes that atomic species labels such as =P=, =S=, and =I= are included in the atom data section.
The periodic boundary condition is treated using the minimum-image convention based on the box lengths.
If the trajectory format is changed, the parsing part of the script may need to be modified.
* Example
#+begin_src sh
python calc_share.py -i "*.lammpstrj" -o summary_table.tex
#+end_src
This command analyzes all =.lammpstrj= files in the current directory and writes the summarized result to =summary_table.tex=.
Executable
+194
View File
@@ -0,0 +1,194 @@
#!/usr/bin/env python
# ./calc_share_v2.py -i "*.lammpstrj"
import numpy as np
import argparse
import os
import glob
from collections import Counter
import pandas as pd
class LoadData():
def __init__(self, trjfile):
self.trjfile = trjfile
self.loadtrj()
def loadtrj(self):
print(f" -> [処理中] {os.path.basename(self.trjfile)} を読み込み中...")
with open(self.trjfile) as o:
d = o.read()
lines = d.splitlines()
self.atoms = int(lines[3])
x_bnd = [float(x) for x in lines[5].split()]
y_bnd = [float(x) for x in lines[6].split()]
z_bnd = [float(x) for x in lines[7].split()]
self.box_size = np.array(
[x_bnd[1]-x_bnd[0], y_bnd[1]-y_bnd[0], z_bnd[1]-z_bnd[0]])
data = np.array(" ".join(lines).split())
self.data = data.reshape(-1, 28+self.atoms*6)
self.step = self.data.shape[0]
data = self.data[:, 28:].reshape(self.step, self.atoms, -1)
self.p_data = data[data[:, :, 2] == "P"].reshape(self.step, -1, 6)
self.p_xyz = self.p_data[:, :, 3:].astype(float)
self.i_data = data[data[:, :, 2] == "I"].reshape(self.step, -1, 6)
self.i_xyz = self.i_data[:, :, 3:].astype(float)
self.s_data = data[data[:, :, 2] == "S"].reshape(self.step, -1, 6)
self.s_xyz = self.s_data[:, :, 3:].astype(float)
return data
def get_pbc_distance(self, xyz1, xyz2):
d = xyz1[:, np.newaxis, :] - xyz2[np.newaxis, :, :]
d -= self.box_size * np.round(d / self.box_size)
return np.linalg.norm(d, axis=-1)
def classify_I(self):
I_S_CUTOFF = 4.7
total_counts = Counter()
total_I_count = 0
for step in range(self.step):
p_xyz = self.p_xyz[step]
s_xyz = self.s_xyz[step]
i_xyz = self.i_xyz[step]
total_I_count += len(i_xyz)
dist_ps = self.get_pbc_distance(s_xyz, p_xyz)
s_to_p = np.argmin(dist_ps, axis=1)
dist_is = self.get_pbc_distance(i_xyz, s_xyz)
for i in range(len(i_xyz)):
close_s_indices = np.where(dist_is[i] <= I_S_CUTOFF)[0]
associated_ps4 = s_to_p[close_s_indices]
ps4_counts = Counter(associated_ps4)
edge = 0
corner = 0
for p_idx, s_count in ps4_counts.items():
if s_count == 2:
edge += 1
elif s_count == 1:
corner += 1
state = (edge, corner)
total_counts[state] += 1
avg_counts = {}
for state, count in total_counts.items():
column_name = f"edge_{state[0]}_corner_{state[1]}"
if total_I_count > 0:
avg_counts[column_name] = count / total_I_count
else:
avg_counts[column_name] = 0.0
return avg_counts
if __name__ == "__main__":
par = argparse.ArgumentParser()
par.add_argument('-i', '--pattern', default="*.lammpstrj",
help='input file pattern (e.g., "*.lammpstrj")')
par.add_argument('-o', '--output', default="summary_table.tex",
help='output tex file name')
args = par.parse_args()
filepaths = sorted(glob.glob(args.pattern))
if not filepaths:
print(f"エラー: パターン '{args.pattern}' に一致するファイルが見つかりませんでした。")
exit()
print(f"{len(filepaths)} 個のファイルを順番に処理します...")
all_results = []
for index, filepath in enumerate(filepaths, start=1):
filename = os.path.splitext(os.path.basename(filepath))[0]
print(f"[{index}/{len(filepaths)}] ファイル処理中...")
try:
calc = LoadData(filepath)
result = calc.classify_I()
result['Composition'] = filename
all_results.append(result)
print(f" -> [完了] {filename} の解析が成功しました。")
except Exception as e:
print(f" -> [エラー] {filepath} の処理中に問題が発生しました: {e}")
# --- TeX形式の出力処理 ---
if all_results:
print("\nTeXコードの生成中...")
df = pd.DataFrame(all_results)
# 欠損値を0で埋め、Compositionをインデックスに設定
df = df.fillna(0)
df.set_index('Composition', inplace=True)
# 列名(状態)をアルファベット・数字順にソート
state_cols = sorted(df.columns)
df = df[state_cols]
# 行と列を入れ替える (Transpose)
df_t = df.T
df_t.index.name = 'Label'
df_t = df_t.reset_index() # Labelをデータ列に戻す
# 列数からフォーマット指定子を生成
num_cols = len(df_t.columns)
tabular_format = "wl{2cm}" + "wc{2cm}" * (num_cols - 1)
# TeXテキストの構築
tex_lines = []
tex_lines.append("\\begin{table}[H]")
tex_lines.append(f" \\begin{{tabular}}\n {{{tabular_format}}}")
tex_lines.append(" \\hline")
# ヘッダー行
headers = [str(h).replace('_', r'\_') for h in df_t.columns]
tex_lines.append(" " + " & ".join(headers) + " \\\\")
tex_lines.append(" \\hline")
# データ行
for _, row in df_t.iterrows():
row_vals = []
for i, val in enumerate(row.values):
if i == 0:
# Label列(状態名)のアンダースコアをエスケープ
row_vals.append(str(val).replace('_', r'\_'))
else:
# 数値列の処理(百分率化、四捨五入、1%以下の処理)
try:
num = float(val)
pct = num * 100
if pct == 0.0:
row_vals.append("0.0\\%")
elif pct <= 1.0:
# 1%以下の場合は数値ではなく「trace(微量)」という英語にする
row_vals.append("trace")
else:
# 小数点1桁で四捨五入
row_vals.append(f"{pct:.1f}\\%")
except ValueError:
row_vals.append(str(val).replace('_', r'\_'))
tex_lines.append(" " + " & ".join(row_vals) + " \\\\")
tex_lines.append(" \\end{tabular}")
tex_lines.append("\\end{table}")
tex_output = "\n".join(tex_lines)
# ファイルへの保存
with open(args.output, "w", encoding="utf-8") as f:
f.write(tex_output)
print("\n=== TeX Output ===")
print(tex_output)
print(f"\n結果は '{args.output}' に保存されました。")