8
8
# The following line states a licence feature that is required to show this script in Mercury and Hermes script menus.
9
9
# Created 18/08/2024 by Alex Moldovan
10
10
# ccdc-licence-features not-applicable
11
-
12
11
import os
13
12
import warnings
14
- from typing import List , Tuple
13
+ from typing import List , Tuple , Dict
15
14
16
15
import numpy as np
17
16
from ccdc .io import CrystalReader
@@ -29,30 +28,107 @@ def __init__(self, crystal, use_existing_charges: bool = False):
29
28
self .crystal = crystal
30
29
self .surface = None
31
30
self ._surface_charge = None
31
+ self .surface_atom_charge = np .nan
32
+ self .node_projected_charge = np .nan
33
+ self .node_representative_charge = np .nan
34
+
35
+ @staticmethod
36
+ def sum_atom_charge (atoms : List [object ]) -> float :
37
+ return np .round (np .sum ([atom .partial_charge for atom in atoms ]), 3 )
38
+
39
+ def get_average_node_charge (self ):
40
+ self .node_charge_dictionary = {}
41
+ node_list = list (self .surface .topology .nodes )
42
+ for node , atoms in self .surface .surface_node_atom_contacts .items ():
43
+ node_index = node_list .index (node )
44
+ average_node_charge = 0
45
+ if len (atoms ) > 0 :
46
+ average_node_charge = self .sum_atom_charge (atoms )
47
+ self .node_charge_dictionary [node_index ] = average_node_charge
48
+
49
+ def calculate_triangles_properties (self ,
50
+ tri_index : List [Tuple [int , int , int ]]) -> Dict [
51
+ Tuple [int , int , int ], Dict [str , float ]]:
52
+ surface_area = self .surface .descriptors .surface_area
53
+ self .triangles_properties = {}
54
+ triangle_areas = self .calculate_area_of_triangles (list (self .surface .topology .triangles ))
55
+ total_triangle_area = sum (triangle_areas )
56
+ for node_index , triangle_area in zip (tri_index , triangle_areas ):
57
+ average_triangle_charge = np .mean ([self .node_charge_dictionary [i ] for i in node_index ])
58
+ triangle_representation = triangle_area / surface_area
59
+ projected_charge = 0
60
+ if np .isclose (triangle_area , 0.0 ) is False :
61
+ projected_charge = average_triangle_charge / triangle_area
62
+ self .triangles_properties [tuple (node_index )] = {'Average Charge' : average_triangle_charge ,
63
+ 'Triangle Area' : triangle_area ,
64
+ 'Percentage Area' : triangle_representation ,
65
+ 'Node Representative Charge' : average_triangle_charge * triangle_representation ,
66
+ 'Node Projected Surface Charge' : projected_charge }
67
+
68
+ def calculate_node_charges (self ):
69
+ tri_index = self .calculated_node_index_values (list (self .surface .topology .nodes ),
70
+ list (self .surface .topology .triangles ))
71
+ self .get_average_node_charge ()
72
+ self .calculate_triangles_properties (tri_index )
73
+ self .representative_charge = np .sum (
74
+ [triangle ['Node Representative Charge' ] for triangle in self .triangles_properties .values ()])
75
+ self .node_charges = np .sum ([triangle ['Average Charge' ] for triangle in self .triangles_properties .values ()])
76
+ return self .representative_charge
77
+
78
+ @staticmethod
79
+ def calculate_length (origin : np .ndarray , target : np .ndarray ) -> float :
80
+ """Returns distance between target and origin"""
81
+ if not isinstance (origin , np .ndarray ) or not isinstance (target , np .ndarray ):
82
+ raise TypeError ("Please supply numpy arrays for lengths." )
83
+ return np .linalg .norm (target - origin )
84
+
85
+ @staticmethod
86
+ def compute_triangle_area (a : float , b : float , c : float ) -> float :
87
+ """Calculates area of triangle using Heron's formula"""
88
+ s = (a + b + c ) / 2
89
+ return np .sqrt (s * (s - a ) * (s - b ) * (s - c ))
90
+
91
+ def calculate_area_of_triangles (self , triangles : List ) -> List :
92
+ """ Calculates area of individual triangles from node positions using Heron's formula"""
93
+ triangle_areas = []
94
+ for triangle in triangles :
95
+ pos_0 , pos_1 , pos_2 = np .array (triangle [0 ]), np .array (triangle [1 ]), np .array (triangle [2 ]),
96
+ a_dist = self .calculate_length (pos_0 , pos_1 )
97
+ b_dist = self .calculate_length (pos_0 , pos_2 )
98
+ c_dist = self .calculate_length (pos_1 , pos_2 )
99
+ triangle_areas .append (self .compute_triangle_area (a_dist , b_dist , c_dist ))
100
+
101
+ return triangle_areas
102
+
103
+ @staticmethod
104
+ def calculated_node_index_values (nodes : List , triangles : List ) -> List :
105
+ """
106
+ Calculate index of all triangles for nodes
107
+
108
+ :param nodes: list of nodes [x,y,z]
109
+ :param triangles: list of triangles that contain nodes index values
110
+ """
111
+ search_dictionary = {e : i for i , e in enumerate (nodes )}
112
+ return [[search_dictionary [n ] for n in tri ] for tri in triangles ]
32
113
33
114
def calculate_surface_charge (self , hkl : Tuple [int , int , int ], offset : float ):
34
115
self .surface = Surface (self .crystal , miller_indices = hkl , offset = offset )
35
116
if self .surface .slab .assign_partial_charges ():
36
- self ._surface_charge = np .round (np .sum ([atom .partial_charge for atom in self .surface .surface_atoms ]), 3 )
117
+ self .surface_atom_charge = self .sum_atom_charge (atoms = self .surface .surface_atoms )
118
+ self .node_representative_charge = self .calculate_node_charges ()
37
119
return
38
120
warnings .warn (f"Gasteiger charges could not be assigned to molecule: { self .crystal .identifier } " ,
39
121
RuntimeWarning )
40
- self ._surface_charge = np .nan
41
-
42
- @property
43
- def surface_charge (self ):
44
- if self ._surface_charge is None :
45
- raise ValueError ("Surface charge calculation not yet calculated, run calculate_surface_charge()" )
46
- return self ._surface_charge
47
-
48
- @property
49
- def surface_charge_per_area (self ):
50
- return self .surface_charge / self .surface .descriptors .projected_area
51
122
52
123
53
124
class SurfaceChargeController :
54
125
def __init__ (self , structure : str , hkl_and_offsets : List [Tuple [int , int , int , float ]],
55
126
output_directory : str = None , use_existing_charges : bool = False ):
127
+
128
+ self .surface_node_charges = []
129
+ self .surface_areas = []
130
+ self .surface_node_representative_charge = []
131
+ self .surface_atom_charges = []
56
132
self .surface_charges_per_area = []
57
133
self .surface_charges = []
58
134
self .projected_area = []
@@ -84,9 +160,12 @@ def calculate_surface_charge(self):
84
160
for surface in self .surfaces :
85
161
charges = SurfaceCharge (crystal = self .crystal , use_existing_charges = self .use_existing_charges )
86
162
charges .calculate_surface_charge (hkl = surface [:3 ], offset = surface [3 ])
87
- self .surface_charges .append (charges .surface_charge )
163
+ self .surface_atom_charges .append (charges .surface_atom_charge )
164
+ self .surface_node_charges .append (charges .node_charges )
165
+ self .surface_node_representative_charge .append (charges .node_representative_charge )
166
+
88
167
self .projected_area .append (charges .surface .descriptors .projected_area )
89
- self .surface_charges_per_area .append (charges .surface_charge_per_area )
168
+ self .surface_areas .append (charges .surface . descriptors . surface_area )
90
169
91
170
def make_report (self ):
92
171
html_content = self .generate_html_table ()
@@ -114,11 +193,13 @@ def generate_html_table(self):
114
193
<h2>Calculation Results</h2>
115
194
<table>
116
195
<tr>
117
- <th>hkl</th>
196
+ <th width="80px" >hkl</th>
118
197
<th>Offset</th>
119
- <th>Projected Area</th>
120
- <th>Surface Charge*</th>
121
- <th>Surface Charge per Projected Area</th>
198
+ <th>P<sub>Area</sub>-Projected Area Å<sup>2</sup></th>
199
+ <th>S<sub>Area</sub>-Surface Area Å<sup>2</sup></th>
200
+ <th>Total Atom Surface Charge</th>
201
+ <th>Total Atom Surface Charge/P<sub>A</sub></th>
202
+ <th>Topological Surface Charge/ S<sub>Area</sub></th>
122
203
</tr>
123
204
"""
124
205
@@ -130,8 +211,10 @@ def generate_html_table(self):
130
211
<td>{ hkl } </td>
131
212
<td>{ offset :.2f} </td>
132
213
<td>{ self .projected_area [i ]:.3f} </td>
133
- <td>{ self .surface_charges [i ]:.3f} </td>
134
- <td>{ self .surface_charges_per_area [i ]:.4f} </td>
214
+ <td>{ self .surface_areas [i ]:.3f} </td>
215
+ <td>{ self .surface_atom_charges [i ]:.3f} </td>
216
+ <td>{ self .surface_atom_charges [i ] / self .projected_area [i ]:.4f} </td>
217
+ <td>{ self .surface_node_representative_charge [i ]:.4f} </td>
135
218
</tr>
136
219
"""
137
220
0 commit comments