Initial commit: add iodide-PS4 sharing analysis script
This commit is contained in:
+16
@@ -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
@@ -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
@@ -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}' に保存されました。")
|
||||||
Reference in New Issue
Block a user