import ase
from ase import Atoms
from ase.io import write
from ase.neb import NEB,interpolate
from ase.units import Bohr,Rydberg,kJ,kB,fs,Hartree,mol,kcal
from ase.geometry.analysis import Analysis
from plotly.subplots import make_subplots
import plotly.graph_objects as go
import math,pickle
import matlanticgrrm.atomslist as ma
import pickle
from scipy.signal import find_peaks
import itertools
import numpy as np
from .geometry import _calc_max_distance, Geometries
[ドキュメント]class ListNEB():
def __init__(self,imagess,k=0.1,climb=False,parallel=False,remove_rotation_and_translation=False,
world=None,method='aseneb',allow_shared_calculator=False,precon=None):
self.imagess = imagess
self.neb = [NEB(images, k=k, climb=climb, parallel=parallel,
remove_rotation_and_translation=remove_rotation_and_translation,
world=world, method=method,
allow_shared_calculator=allow_shared_calculator,
precon=precon) if images else None for images in self.imagess]
for images in imagess:
if images:
self.n_images = len(images)
def __iter__(self):
"""Optimizerを使用するためには__iter__を実装する必要がある"""
return iter(self.neb)
def __len__(self):
"""Optimizerを使用するためには__len__を実装する必要がある"""
return len(self.imagess)
def __getitem__(self, item):
return self.neb[item]
[ドキュメント]def calc_image_numbers(ini, fin, indices=None, divide_per=0.1, min_images=8):
"""始状態と終状態からNEB計算に最適なimageの数を算出する
Paramaters:
ini: Atoms
始状態のAtomsオブジェクト
fin: Atoms
終状態のAtomsオブジェクト
indices: List of integer
着目する原子のインデックス番号
divide_per: float
最も大きく動いた原子の移動距離をこの距離で割ってイメージ数を定める(Å)
min_images: int
最小のイメージ数を定める
"""
number_of_images = min_images
maxd = _calc_max_distance(ini, fin, indices)
inumber = int((maxd / divide_per) // 2) # 奇数,偶数の場合がある
inumber = inumber if inumber%2==0 else inumber+1 # 奇数の場合は1を足して偶数にする
if inumber > min_images:
number_of_images = inumber
return number_of_images
[ドキュメント]def make_images(eq_list,connections,n_images=None, name="ReactionsSummary",
indices=None, divide_per=0.1, min_images=8,mic=True):
"""NEB計算のImagesを作成する.
| 不要な反応のImageは作成しない. 例えば,同じini,finの組み合わせを持つ反応は1つしかImagesを作らない.
| 作成されなかったImageの理由をHTMLファイル,またはlogファイルに出力する.
| eq_listをGeometryで与えた場合HTMLファイル,Atomsリストで与えた場合logファイルが作成される.
Parameters:
eq_list: list of Atoms or matlanticgrrm.geometry.Geometry
| EQ構造
| Atomsのリストの場合: 同じCONNECTIONの反応のimagesは作らない.
| matlanticgrrm.geometry.Geometryの場合: 同じ構造と判断された反応のimagesは作らない(構造のグループ化)
connections: list of tuple of interger
| CONNECTIONのリスト
| ex) [(0,1),(2,4),(3,5)]
n_images: interger or list or None
| Imageの数.
| intの場合: 全てintで指定したImage数で作成
| listの場合:
| Noneの場合: iniとfinの構造の差から最適なImage数を算出し作成.
indices:
| indicesに指定した原子の移動距離から適切なimages数を決定する.
| Noneの場合,全ての原子の移動距離を見る
divide_per: float
最も大きく動いた原子の移動距離をこの距離で割ってイメージ数を定める(Å)
min_images: int
最小のイメージ数を定める
"""
if type(eq_list) == list:
#計算するTS番号と移動距離の取得&Log出力
indexes = make_nebmakelog(eq_list,connections)
if type(n_images) == int:
n_images = [n_images if i in indexes else None for i in range(len(eq_list))]
elif type(n_images) == list:
pass # なにもしない(n_images=n_images)
elif n_images is None:
n_images = [calc_image_numbers(eq_list[ini],eq_list[fin],indices,divide_per,min_images)
if type(ini)!=str and type(fin)!=str else None
for ini,fin in connections]
else:
raise TypeError("n_imagesはint,list,Noneのいずれか")
elif type(eq_list) == Geometries:
geometry = eq_list
eq_list = geometry.atoms_list
#計算するTS番号と移動距離の取得&HTML出力
indexes_and_distance = geometry.get_valid_reaction_and_summary(connections,name+".html")
indexes = indexes_and_distance.keys()
distance = indexes_and_distance.values()
if type(n_images) == int:
n_images = [n_images if i in indexes else None for i in range(len(geometry))]
elif type(n_images) == list:
pass # なにもしない(n_images=n_images)
elif n_images is None:
indices = geometry.target0+geometry.target1
n_images = [calc_image_numbers(eq_list[ini],eq_list[fin],indices,divide_per,min_images)
if type(ini)!=str and type(fin)!=str else None
for ini,fin in connections]
else:
raise TypeError("n_imagesはint,list,Noneのいずれか")
else:
raise TypeError("eq_listはAtomsのリスト,matlanticgrrm.geometry.Geometryのいずれか")
return _make_images(eq_list,connections,indexes,n_images,mic)
def _make_images(eq_list:list,connections,indexes,n_images:list,mic=True):
imagess = []
for i,((ini_idx,fin_idx),n_image) in enumerate(zip(connections,n_images)):
if i in indexes:
images=[eq_list[ini_idx].copy()]+[eq_list[ini_idx].copy() for _ in range(n_image)]+[eq_list[fin_idx].copy()]
interpolate(images,mic=mic)
else:
images=None
imagess.append(images)
return imagess
def make_nebmakelog(eq,connections,ts=None,same_react=False,same_connect=False,log="nebmake.log"):
connect_copy = [] #重複したini,finの組み合わせがないか探すため
indexes = []
text = ""
for i,(ini,fin) in enumerate(connections):
if type(ini)!= int or type(fin)!= int:
text += f"i={i} ({ini},{fin})\n"
elif ini == fin:
if same_connect:
text += f"i={i} imageを作成({ini},{fin})\n"
indexes.append(i)
else:
text += f"i={i} iniとfinが同じ({ini},{fin})\n"
elif (ini,fin) in connect_copy:
if same_react or ts:
text += f"i={i} imageを作成({ini},{fin})\n"
indexes.append(i)
else:
index = connect_copy.index((ini,fin))
text += f"i={i} i={index}と同じ組み合わせ({ini},{fin})\n"
elif (fin,ini) in connect_copy:
if same_react or ts:
text += f"i={i} imageを作成({ini},{fin})\n"
indexes.append(i)
else:
index = connect_copy.index((fin,ini))
text += f"i={i} i={index}と同じ組み合わせ({ini},{fin})\n"
elif not eq[ini]:
text += f"i={i} EQ{ini}の構造がない({ini},{fin})\n"
elif not eq[fin]:
text += f"i={i} EQ{fin}の構造がない({ini},{fin})\n"
else:
text += f"i={i} imageを作成({ini},{fin})\n"
indexes.append(i)
connect_copy.append((ini,fin))
if log == "-":
print(text)
elif log is not None:
with open(log,"w") as f:
f.write(text)
return indexes
[ドキュメント]def neb_graphs(imagess,html=None,traj=None,pkl=None):
"""NEBのエネルギーダイアグラムを作成
Parameters:
imagess: list of images (2D list of Atoms)
imagesのリスト(Atomsの2次元リスト)
html: str or path object
| htmlのパス
| htmlにグラフを作成する.Noneの場合は標準出力(非推奨).
traj: str or path object
| trajのパス
| TS構造のtrajファイルを作成する. TS構造の存在しない要素は空のAtomsオブジェクト
pkl: str or path object
TS構造のindex番号, TS構造が存在しない要素はNone
"""
energiess = [ma.get_potential_energy(images) if images else None for images in imagess]
for images_e in energiess:
if images_e:
for image_e in images_e:
image_e#*mol/kJ
sample_n = len(energiess)
cols = 3 #3列
rows = math.ceil(sample_n/cols)
subplot_titles = [f"TS{i}" for i in range(sample_n)]
"""データをcol個ずつに分割する"""
div_sample = []
for i in range(0, sample_n, cols):
div_sample.append(energiess[i: i+cols])
def add_fig(fig,x,y,row,col,color):
fig.add_trace(go.Scatter(x=x, y=y,
line=dict(width=2,color=color)),
row=row, col=col)
fig = make_subplots(
rows=rows, cols=cols,
subplot_titles=subplot_titles)
idx = []
for i, row_data in enumerate(div_sample,start=1):
for j, data in enumerate(row_data,start=1):
if data:
x = [i for i in range(len(data))]
y = [(i-data[0])*mol/kJ for i in data]
max_y = max(y)
min_y = min(y)
add_fig(fig,x,y,i,j,'black')
if max_y-min_y > 0.1: # エネルギーの幅が0.1以下の時はグラフがガタついている場合はほとんどなので無視
peaks_i, _ = find_peaks(y)
peaks_eneegies = [(i,y[i]) for i in peaks_i]
ts_energy = sorted(peaks_eneegies, key=lambda x:x[1])
add_fig(fig,[ts_energy[-1][0]],[ts_energy[-1][1]],i,j,'red')
idx.append(ts_energy[0][0])
else:
idx.append(None)
else:
idx.append(None)
fig.update_layout(height=300*rows, width=1000,
title_text="エネルギーダイアグラム")
if pkl:
with open(pkl,"wb") as f:
pickle.dump(idx,f)
if traj:
atoms_list = [images[n] if n is not None else Atoms() for n,images in zip(idx,imagess)]
write(traj,atoms_list)
if html:
fig.write_html(html)
else:
fig.show()
return idx
# def neb_graph(image,calculator=None):
# if calculator:
# ma.set_calculator(calculator)
def make_connections(eq_list,target:list,n=2):
"""EQ構造のAtomsリストから反応しそうなCONNECTIONを返す
Parameter:
eq_list: list of Atoms
EQ構造のAtomsリスト
n:
nを大きくするとより多くのCONNECTIONを返す(その分反応しづらいCONNECTIONが含まれる)
"""
eq_list= _extract_target_atoms(eq_list,target) # targetに指定した原子のみを抽出する
similes = ma.get_smiles(eq_list)
group, bond_dict = _atoms_matrix_dict(eq_list)
conv = []
for (a_key,a_mat),(b_key,b_mat) in itertools.combinations(bond_dict.items(), 2):
dif = (a_mat-b_mat).data # 構造Aと構造Bの結合の差を計算
if np.count_nonzero(dif < 0) <= n and np.count_nonzero(dif > 0) <= 2:
"""反応しそうな組み合わせ"""
if similes[group[a_key][0]] != similes[group[b_key][0]]:
"""SMILES的に違う構造の時"""
min_distance_conbination = None
distance = float('inf')
for ini,fin in itertools.product(group[a_key], group[b_key]):
"""最も動く距離の短いものを算出"""
ini_structure = eq_list[ini]
fin_structure = eq_list[fin]
d = ase.geometry.distance(ini_structure, fin_structure)
if d < distance:
distance = d
min_distance_conbination = (ini,fin)
conv.append(min_distance_conbination)
return conv
def _atoms_matrix_dict(atoms_list):
"""ユニークなbondの行列を作成する
bond_dict = {hashableな行列:adjacency_matrix}
group = {hashableな行列:[idx]}
"""
group = {}
bond_dict = {}
for i, atoms in enumerate(atoms_list):
ana = Analysis(atoms)
bond_mat = ana.adjacency_matrix[0]
hashable_mat = tuple(bond_mat.items())
if not hashable_mat in group.keys():
bond_dict[hashable_mat] = bond_mat
group[hashable_mat] = [i]
else:
group[hashable_mat].append(i)
return group, bond_dict
def _extract_target_atoms(atoms_list,target):
"""targetに指定した原子のみを抽出する"""
atoms_list = ma.copy(atoms_list)
atoms_n = len(atoms_list[0])
del_idx = [i for i in range(atoms_n) if i not in target]
for atoms in atoms_list:
del atoms [del_idx]
return atoms_list