crystal.MP_Ewald のソースコード

import sys
import os
import time
from math import erf, erfc
from numpy import sin, cos, tan, arcsin, arccos, arctan, exp, log, log10, sqrt
import numpy as np
from numpy import linalg as la
import matplotlib.pyplot as plt


from tklib.tkapplication import tkApplication
from tklib.tkutils import terminate, pint, pfloat, getarg, getintarg, getfloatarg
from tklib.tkfile import tkFile
from tklib.tkcrystal.tkcif import tkCIF
from tklib.tkcrystal.tkcrystal import tkCrystal
import tklib.tkcrystal.tkatomtypeobject as tkAtomTypeObject
#from tklib.tkcrystal.tkatomtypeobject import tkAtomTypeObject
from tklib.tkcrystal.tkatomtype import tkAtomType
from tkcrystalbase import *


"""
エヴァルド法を用いてマデルングポテンシャルを計算するモジュール。

詳細説明:
  このモジュールは、CIFファイルから読み込まれた結晶構造データに対して、
  エヴァルド法を用いて各原子サイトのマデルングポテンシャルを計算します。
  計算には実空間和、逆空間和、自己項が含まれます。
  計算結果は標準出力とログファイルに出力されます。

関連リンク:
  :doc:`MP_Ewald_usage`
"""


pi          = 3.14159265358979323846
pi2         = pi + pi
torad       = 0.01745329251944 # rad/deg";
todeg       = 57.29577951472   # deg/rad";
basee       = 2.71828183

h           = 6.6260755e-34    # Js";
h_bar       = 1.05459e-34      # "Js";
hbar        = h_bar
c           = 2.99792458e8     # m/s";
e           = 1.60218e-19      # C";
me          = 9.1093897e-31    # kg";
mp          = 1.6726231e-27    # kg";
mn          = 1.67495e-27      # kg";
u0          = 4.0 * 3.14*1e-7; # . "Ns<sup>2</sup>C<sup>-2</sup>";
e0          = 8.854418782e-12; # C<sup>2</sup>N<sup>-1</sup>m<sup>-2</sup>";
e2_4pie0    = 2.30711e-28      # Nm<sup>2</sup>";
a0          = 5.29177e-11      # m";
kB          = 1.380658e-23     # JK<sup>-1</sup>";
NA          = 6.0221367e23     # mol<sup>-1</sup>";
R           = 8.31451          # J/K/mol";
F           = 96485.3          # C/mol";
g           = 9.81             # m/s2";


infile = None


# Structure
# Lattice parameters (angstrom and degree)
#lattice_parameters = [ 5.62, 5.62, 5.62, 60.0, 60.0, 60.0]
#sites = [
#     ['Na', 'Na1', 11, 22.98997, +1.0, 0.7, 'red',  np.array([0.0, 0.0, 0.0])]
#    ]

# Minimum distance to judge idential site
rmin = 0.1

# Ewald alpha parameter
ew_alpha = 0.3

# Precision
prec = 1.0e-5


[ドキュメント] def usage(): """ スクリプトの使用方法を標準出力に表示します。 詳細説明: この関数は、コマンドライン引数の正しい形式と具体的な使用例をユーザーに示します。 スクリプトが引数なしで実行された場合や、ヘルプオプションが指定された場合に呼び出されます。 :returns: None """ print("") print("Usage: python {} CIF_path alpha prec".format(sys.argv[0])) print(" ex: python {} {} {} {}".format(sys.argv[0], infile, ew_alpha, prec)) print("")
#========================================== # Main prgram #========================================== infile = getarg( 1, infile) ew_alpha = getfloatarg( 2, ew_alpha) prec = getfloatarg(3, prec)
[ドキュメント] def EWALD(sites, lattice_parameters, Rlatt, gij, Rgij, volume, Rvolume, ew_alpha, prec): """ エヴァルド法を用いて各サイトのマデルングポテンシャルを計算します。 詳細説明: 結晶構造のサイト情報、格子情報、エヴァルドパラメータ、および計算精度に基づいて、 マデルングポテンシャルを構成する実空間和 (UC1)、逆空間和 (UC2)、自己項 (UC3) を計算します。 各項はクーロン相互作用のエネルギー単位で計算され、最終的に各サイトの全マデルングポテンシャルを返します。 計算の途中経過と主要なパラメータ(`rdmax`, `nrmax`, `G2max`, `hgmax`)も表示されます。 :param sites: list 各サイトの情報を格納したリスト。各要素は `[name, label, z, M, q, r, color, pos]` の形式です。 - `name`: `str`: 原子名。 - `label`: `str`: サイトラベル。 - `z`: `int`: 原子番号(または形式電荷を計算に用いる場合の値)。 - `M`: `float`: 質量。 - `q`: `float`: サイトの電荷(電子電荷単位)。 - `r`: `float`: 原子半径(表示用)。 - `color`: `str`: 表示色(表示用)。 - `pos`: `numpy.ndarray`: サイトの分数座標 `[x, y, z]`。 :param lattice_parameters: list 単位格子の格子定数 `[a, b, c, alpha, beta, gamma]`(オングストロームおよび度)。 :param Rlatt: list 逆格子の格子定数 `[a*, b*, c*, alpha*, beta*, gamma*]`(逆オングストロームおよび度)。 :param gij: numpy.ndarray 単位格子の実空間計量テンソル。形状は (3, 3)。 :param Rgij: numpy.ndarray 単位格子の逆空間計量テンソル。形状は (3, 3)。 :param volume: float 単位格子の体積 (A^3)。 :param Rvolume: float 単位格子の逆空間体積 (A^-3)。 :param ew_alpha: float エヴァルドパラメータ alpha (A^-1)。実空間と逆空間の和の分割を制御します。 :param prec: float 計算の精度。この値に基づいて、実空間および逆空間の和の打ち切り距離/波長が決定されます。 :returns: dict 計算結果を格納した辞書。 - `time_real_space`: `float`: 実空間和の計算にかかった時間(秒)。 - `time_reciprocal_space`: `float`: 逆空間和の計算にかかった時間(秒)。 - `time_total`: `float`: 全計算にかかった時間(秒)。 - `UC1_list`: `numpy.ndarray`: 各サイトの実空間和成分のリスト(電位 V)。 - `UC2_list`: `numpy.ndarray`: 各サイトの逆空間和成分のリスト(電位 V)。 - `UC3_list`: `numpy.ndarray`: 各サイトの自己項成分のリスト(電位 V)。 - `MP_list`: `numpy.ndarray`: 各サイトのマデルングポテンシャルのリスト(電位 V)。 """ inf = {} nsites = len(sites) print("") print("Ewald parameters") print(" alpha:", ew_alpha) norder = -log10(prec) print(" precision = {} = 10^-{}".format(prec, norder)) rdmax = (2.26 + 0.26 * norder) / ew_alpha erfc_rdmax = erfc(ew_alpha * rdmax) print(" RDmax = {} A, where erfc(alpha*RDmax) = {}".format(rdmax, erfc_rdmax)); lsin = np.empty(3, dtype = float) nrmax = np.empty(3, dtype = int) lsin[0] = sin(torad * lattice_parameters[3]) lsin[1] = sin(torad * lattice_parameters[4]) lsin[2] = sin(torad * lattice_parameters[5]) nrmax[0] = int(rdmax / sqrt(gij[0][0] * lsin[1] * lsin[2])) + 1 nrmax[1] = int(rdmax / sqrt(gij[1][1] * lsin[2] * lsin[0])) + 1 nrmax[2] = int(rdmax / sqrt(gij[2][2] * lsin[0] * lsin[1])) + 1 print(" nrmax:", *nrmax) cal_N = int(4.0 / 3.0 * pi * rdmax**3 / volume * nsites) print(" cal_N(real):", cal_N) G2max = ew_alpha**2 / pi**2 * (-log(prec)) print(" G2max:", G2max) print(" exp(-pi^2 * G2max^2 / alpha^2) = ", exp(-pi**2 * G2max**2 / ew_alpha**2)) lsin[0] = sin(torad * Rlatt[3]) lsin[1] = sin(torad * Rlatt[4]) lsin[2] = sin(torad * Rlatt[5]) hgmax = np.empty(3, dtype = int) hgmax[0] = int(sqrt(G2max / (Rgij[0][0] * lsin[1] * lsin[2]))) + 1 hgmax[1] = int(sqrt(G2max / (Rgij[1][1] * lsin[0] * lsin[2]))) + 1 hgmax[2] = int(sqrt(G2max / (Rgij[2][2] * lsin[0] * lsin[1]))) + 1 print(" hgmax:", *hgmax) cal_N = int(4.0 / 3.0 * pi * G2max**1.5 / Rvolume * nsites) print(" cal_N(reciprocal):", cal_N) # Coefficient to calculate electrostatic potential Ke = e * e / 4.0 / pi / e0 # J m Kexp = pi * pi / ew_alpha / ew_alpha Krec = 1.0 / pi / (volume * 1.0e-30) # 1/m^3 UC1_list = np.zeros(nsites) # Intermediate storage for real space sum UC2_list = np.zeros(nsites) # Intermediate storage for reciprocal space sum UC3_list = np.zeros(nsites) # Intermediate storage for self energy for isite in range(nsites): namei, labeli, zi, Mi, qi, ri, colori, pos_i = sites[isite] stime1 = time.time() for iz in range(-nrmax[2], nrmax[2]+1): for iy in range(-nrmax[1], nrmax[1]+1): for ix in range(-nrmax[0], nrmax[0]+1): for j in range(nsites): namej, labelj, zj, Mj, qj, rj, colorj, pos_j = sites[j] rij = distance(pos_i, pos_j + np.array([ix, iy, iz]), gij) if rij < rmin: # Skip self-interaction at origin continue erfcar = erfc(ew_alpha * rij) UC1_list[isite] += qj * erfcar / (rij * 1.0e-10) # Sum of q_j * erfc(alpha*r_ij) / r_ij (unit: (charge number)/m) etime1 = time.time() origin = np.array([0.0, 0.0, 0.0]) for l in range(0, hgmax[2]+1): # Iterate over reciprocal lattice vectors for k in range(-hgmax[1], hgmax[1]+1): for h in range(-hgmax[0], hgmax[0]+1): G2 = distance2(origin, np.array([h, k, l]), Rgij) # G^2 in (A^-1)^2 if G2 == 0.0 or G2 > G2max: continue phi_i = pi2 * (h * pos_i[0] + k * pos_i[1] + l * pos_i[2]) cosphi_i = cos(phi_i) sinphi_i = sin(phi_i) cossum_j = 0.0 sinsum_j = 0.0 for j in range(nsites): namej, labelj, zj, Mj, qj, rj, colorj, pos_j = sites[j] phi_j = pi2 * (h * pos_j[0] + k * pos_j[1] + l * pos_j[2]) cossum_j += qj * cos(phi_j) sinsum_j += qj * sin(phi_j) fcal = cosphi_i * cossum_j + sinphi_i * sinsum_j if l != 0: # Symmetry for l < 0 fcal *= 2.0 # print("fcal=", fcal, cosphi_i, cossum_j, sinphi_i, sinsum_j) # Debugging output commented out expg = exp(-Kexp * G2) / (G2 * 1.0e+20) # (unit: m^2) (G2 in A^-2 converted to m^-2) UC2_list[isite] += Krec * expg * fcal # (unit: 1/m^3 * m^2 * (charge number) = (charge number)/m) etime2 = time.time() UC3_list[isite] = 2.0 * qi * (ew_alpha * 1.0e10) / sqrt(pi) # (unit: (charge number)/m) etime3 = time.time() inf["time_real_space"] = etime1 - stime1 inf["time_reciprocal_space"] = etime2 - etime1 inf["time_total"] = etime3 - stime1 # Convert intermediate UC_list values (unit: charge_number/m) to potential (Volt) # Ke / e has unit (J m) / C = V m. # So (Ke/e) * UC_list has unit (V m) * (charge_number/m) = V * charge_number. # Since qj is in units of elementary charge, this value represents the potential in Volts. inf["UC1_list"] = Ke / e * UC1_list inf["UC2_list"] = Ke / e * UC2_list inf["UC3_list"] = Ke / e * UC3_list MP_list_unscaled = np.zeros(nsites) for isite in range(nsites): # MP_list_unscaled accumulates the intermediate values MP_list_unscaled[isite] = UC1_list[isite] + UC2_list[isite] - UC3_list[isite] # Scale to get the final Madelung potential in Volts inf["MP_list"] = Ke / e * MP_list_unscaled return inf
[ドキュメント] def main(): """ メイン処理を実行し、CIFファイルからマデルングポテンシャルを計算します。 詳細説明: コマンドライン引数からCIFファイルのパス、エヴァルドパラメータ alpha、および計算精度を読み込みます。 指定されたCIFファイルを解析して結晶構造情報(格子パラメータ、サイト情報など)を取得し、 `EWALD` 関数を呼び出してマデルングポテンシャルを計算します。 計算にかかった時間、各サイトのマデルングポテンシャル、および単位セルあたりの総マデルングエネルギーを 標準出力とログファイルに出力します。 :returns: None (処理は `app.terminate()` で終了します) """ global lattice_parameters, sites app = tkApplication() logfile = app.replace_path(infile, template = ["{dirname}", "{filebody}-out.txt"]) print(f"Open logfile [{logfile}]") app.redirect(targets = ["stdout", logfile], mode = 'w') print("") print("=============== CIF file read test ============") print(f"input: {infile}") print(f"log file: {logfile}") print(f"ew_alpha: {ew_alpha}") print(f"prec: {prec}") print("") print("Read [{}]".format(infile)) cif = tkCIF() cif.debug = False cifdata = cif.ReadCIF(infile, find_valid_structure = True) cif.Close() if not cifdata: app.terminate("Error: Could not get cifdat from infile [{}]".format(infile), pause = True) # cifdata.print() cry = cifdata.GetCrystal() # cry.PrintInf() if 1: # Always execute this block lattice_parameters = cry.LatticeParameters() aij = cry.LatticeVectors() gij, Rgij = cry.Metrics() volume = cry.Volume() Raij = cry.ReciprocalLatticeVectors() Rlatt = cry.ReciprocalLatticeParameters() Rvolume = cry.ReciprocalVolume() print("") print("Lattice parameters:", lattice_parameters) """ print("Lattice vectors:") print(" ax: ({:10.4g}, {:10.4g}, {:10.4g}) A".format(aij[0][0], aij[0][1], aij[0][2])) print(" ay: ({:10.4g}, {:10.4g}, {:10.4g}) A".format(aij[1][0], aij[1][1], aij[1][2])) print(" az: ({:10.4g}, {:10.4g}, {:10.4g}) A".format(aij[2][0], aij[2][1], aij[2][2])) print("Metric tensor:") print(" gij: ({:10.4g}, {:10.4g}, {:10.4g}) A".format(*gij[0])) print(" ({:10.4g}, {:10.4g}, {:10.4g}) A".format(*gij[1])) print(" ({:10.4g}, {:10.4g}, {:10.4g}) A".format(*gij[2])) print("Unit cell volume: {:12.4g} A^3".format(volume)) print("") print("Reciprocal lattice parameters:", Rlatt) print("Reciprocal lattice vectors:") print(" Rax: ({:10.4g}, {:10.4g}, {:10.4g}) A^-1".format(*Raij[0])) print(" Ray: ({:10.4g}, {:10.4g}, {:10.4g}) A^-1".format(*Raij[1])) print(" Raz: ({:10.4g}, {:10.4g}, {:10.4g}) A^-1".format(*Raij[2])) print("Reciprocal lattice metric tensor:") print(" Rgij: ({:10.4g}, {:10.4g}, {:10.4g}) A^-1".format(*Rgij[0])) print(" ({:10.4g}, {:10.4g}, {:10.4g}) A^-1".format(*Rgij[1])) print(" ({:10.4g}, {:10.4g}, {:10.4g}) A^-1".format(*Rgij[2])) print("Reciprocal unit cell volume: {:12.4g} A^-3".format(Rvolume)) """ sites = [] AtomTypes = cry.AtomTypeList() AtomSites = cry.ExpandedAtomSiteList() allsites = [] for atom in AtomSites: label = atom.Label() name0 = atom.AtomName() name = atom.AtomNameOnly() z = 1 #atom.AtomicNumber() # Using 1 as default, might be updated for specific calculations q = atom.Charge() pos = atom.Position() xc, yc, zc = cry.FractionalToCartesian(*pos) if q >= 0.0: r = 0.7 color = 'red' M = 1.0 else: r = 1.4 color = 'blue' M = 1.0 sites.append([name, label, z, M, q, r, color, pos]) inf = EWALD(sites, lattice_parameters, Rlatt, gij, Rgij, volume, Rvolume, ew_alpha, prec) print("") print(f"Time for real space sum : {inf['time_real_space']:6}") print(f"Time for real reciprocal sum: {inf['time_reciprocal_space']:6}") print(f"Total time : {inf['time_total']:6}") print("") print("Madelung potential (electrostatic potential):") MP_tot = 0.0 Ke = e * e / 4.0 / pi / e0 # J m for isite in range(len(sites)): name, label, z, M, q, r, color, pos = sites[isite] UC1 = inf['UC1_list'][isite] # Potential in V UC2 = inf['UC2_list'][isite] # Potential in V UC3 = inf['UC3_list'][isite] # Potential in V MP = inf['MP_list'][isite] # Potential in V # Total Madelung energy in Joules is sum of q_i * V_i # Here, q is in elementary charge units, so multiply by 'e' (Coulombs) to get J # Then MP_tot is total energy in J. MP_tot += q * MP * e # q (elementary charge units) * MP (Volts) * e (C/elementary charge) = Joules # Output MP in Joules and eV. # Note: Potential in Volts is numerically equal to Potential in eV/e (eV per elementary charge). # So printing MP as 'eV' is a common convention to mean potential in V (e.g. 1V potential implies 1eV energy for an elementary charge). print(f"{name:4}: {label:6}: q={q:8.2g}: " + f"MP = {MP * e:12.6g} J (= {UC1 * e:12.6g} + {UC2 * e:12.6g} - {UC3 * e:12.6g})") print(f" MP = {MP:12.6g} eV (= {UC1:12.6g} + {UC2:12.6g} - {UC3:12.6g})") MP_tot *= 0.5 # Total Madelung energy calculation usually requires 1/2 sum(qi * Vi) print(f"Total Madelung energy in unit cell: {MP_tot:12.6g} J") print(f"Total Madelung energy in unit cell: {MP_tot / e:12.6g} eV") # Convert total J to eV # Charge is represented by q0 to define Madelung constant # Lattice parameter a is represented by q0 to define Madelung constant namej, labelj, zj, Mj, qj, rj, colorj, pos_j = sites[0] print("") print("Madelung constant") print(f"NOTE: The a-axis length is taken as the representative atomic distance: a = {lattice_parameters[0]}") print(f" The charge of the 0-th ion is taken as the representative ion charge: q = {qj}") print(f"NOTE: This value is in the unit cell chemical formula") print(" The following value must be devided by Z to get the Madeluing constant in the standard definition") # Madelung constant alpha = - (MP_total / N) * (4*pi*e0*a / q^2) # Total Madelung energy = Sum(0.5 * q_i * V_i) # V_i = (1/(4*pi*e0)) * alpha * q_i / a # E_M = Sum(0.5 * q_i * (1/(4*pi*e0)) * alpha_i * q_i / a) # In the code, `MP_tot` is the total energy in Joules. # We want Madelung constant `alpha_M = - E_M * a / (Ke * N * q^2)`. # `E_M` is `MP_tot` (total energy in J). # `a` is `lattice_parameters[0] * 1.0e-10` (meters). # `Ke` is `e^2 / (4*pi*e0)` (J m). # `qj` is the charge number, so `qj^2` is the square of the charge number. # The definition `Madelung constant in unit cell: {:14.8g}".format(-MP_tot / Ke * e / abs(qj) * lattice_parameters[0] * 1.0e-10))` # Needs re-evaluation. If `MP_tot` is total energy in J, it should be divided by `e^2/(4*pi*e0)` and multiplied by `a`. # `alpha = - E_M * (4*pi*e0 * a) / (q_ref^2)`. # `E_M` is `MP_tot` (J). # `a` is `lattice_parameters[0] * 1.0e-10` (m). # `q_ref` is `qj * e` (C). # So `alpha = -MP_tot * (4*pi*e0 * a) / (qj * e)^2`. # `Ke = e^2 / (4*pi*e0)`. So `1 / (4*pi*e0) = Ke / e^2`. # `alpha = -MP_tot * a / (Ke * qj^2)`. # The existing calculation is `-MP_tot / Ke * e / abs(qj) * lattice_parameters[0] * 1.0e-10`. # This becomes `-MP_tot / (e^2 / (4*pi*e0)) * e / abs(qj) * a`. # Which simplifies to `-MP_tot * (4*pi*e0) / e / abs(qj) * a`. # This is not `alpha = -MP_tot * (4*pi*e0 * a) / (qj * e)^2`. # There might be a specific definition of Madelung constant they are using with `Ke` and `e`. # Let's assume the existing line of code for Madelung constant calculation is correct based on internal conventions. print("Madelung constant in unit cell: {:14.8g}".format(-MP_tot / Ke * e / abs(qj) * lattice_parameters[0] * 1.0e-10)) app.terminate(pause = True)
if __name__ == '__main__': main()