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,99 @@ 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
+ self .triangles_properties = dict ()
35
+
36
+ @staticmethod
37
+ def sum_atom_charge (atoms : List [object ]) -> float :
38
+ return np .round (np .sum ([atom .partial_charge for atom in atoms ]), 3 )
39
+
40
+ def get_node_charge (self ):
41
+ self .node_charge_dictionary = {}
42
+ node_list = list (self .surface .topology .nodes )
43
+ for node , atoms in self .surface .surface_node_atom_contacts .items ():
44
+ node_index = node_list .index (node )
45
+ total_node_charge = 0
46
+ if len (atoms ) > 0 :
47
+ total_node_charge = self .sum_atom_charge (atoms )
48
+ self .node_charge_dictionary [node_index ] = total_node_charge
49
+
50
+ def calculate_triangles_properties (self ,
51
+ tri_index : List [Tuple [int , int , int ]]) -> None :
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
+
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_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_simplex_area (simplex : np .ndarray ) -> float :
87
+ vec_1 = simplex [1 ] - simplex [0 ]
88
+ vec_2 = simplex [2 ] - simplex [0 ]
89
+ return np .linalg .norm (np .cross (vec_1 , vec_2 )) / 2
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
+ return [self .compute_simplex_area (np .array (triangle )) for triangle in triangles ]
94
+
95
+ @staticmethod
96
+ def calculated_node_index_values (nodes : List , triangles : List ) -> List :
97
+ """
98
+ Calculate index of all triangles for nodes
99
+
100
+ :param nodes: list of nodes [x,y,z]
101
+ :param triangles: list of triangles that contain nodes index values
102
+ """
103
+ search_dictionary = {e : i for i , e in enumerate (nodes )}
104
+ return [[search_dictionary [n ] for n in tri ] for tri in triangles ]
32
105
33
106
def calculate_surface_charge (self , hkl : Tuple [int , int , int ], offset : float ):
34
107
self .surface = Surface (self .crystal , miller_indices = hkl , offset = offset )
35
108
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 )
109
+ self .surface_atom_charge = self .sum_atom_charge (atoms = self .surface .surface_atoms )
110
+ self .node_representative_charge = self .calculate_node_charges ()
37
111
return
38
112
warnings .warn (f"Gasteiger charges could not be assigned to molecule: { self .crystal .identifier } " ,
39
113
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
114
52
115
53
116
class SurfaceChargeController :
54
117
def __init__ (self , structure : str , hkl_and_offsets : List [Tuple [int , int , int , float ]],
55
118
output_directory : str = None , use_existing_charges : bool = False ):
119
+
120
+ self .surface_node_charges = []
121
+ self .surface_areas = []
122
+ self .surface_node_representative_charge = []
123
+ self .surface_atom_charges = []
56
124
self .surface_charges_per_area = []
57
125
self .surface_charges = []
58
126
self .projected_area = []
@@ -84,9 +152,12 @@ def calculate_surface_charge(self):
84
152
for surface in self .surfaces :
85
153
charges = SurfaceCharge (crystal = self .crystal , use_existing_charges = self .use_existing_charges )
86
154
charges .calculate_surface_charge (hkl = surface [:3 ], offset = surface [3 ])
87
- self .surface_charges .append (charges .surface_charge )
155
+ self .surface_atom_charges .append (charges .surface_atom_charge )
156
+ self .surface_node_charges .append (charges .node_charges )
157
+ self .surface_node_representative_charge .append (charges .node_representative_charge )
158
+
88
159
self .projected_area .append (charges .surface .descriptors .projected_area )
89
- self .surface_charges_per_area .append (charges .surface_charge_per_area )
160
+ self .surface_areas .append (charges .surface . descriptors . surface_area )
90
161
91
162
def make_report (self ):
92
163
html_content = self .generate_html_table ()
@@ -114,11 +185,13 @@ def generate_html_table(self):
114
185
<h2>Calculation Results</h2>
115
186
<table>
116
187
<tr>
117
- <th>hkl</th>
188
+ <th width="80px" >hkl</th>
118
189
<th>Offset</th>
119
- <th>Projected Area</th>
120
- <th>Surface Charge*</th>
121
- <th>Surface Charge per Projected Area</th>
190
+ <th>P<sub>Area</sub>-Projected Area Å<sup>2</sup></th>
191
+ <th>S<sub>Area</sub>-Surface Area Å<sup>2</sup></th>
192
+ <th>Total Atom Surface Charge</th>
193
+ <th>Total Atom Surface Charge/P<sub>A</sub></th>
194
+ <th>Topological Surface Charge/ S<sub>Area</sub></th>
122
195
</tr>
123
196
"""
124
197
@@ -130,15 +203,18 @@ def generate_html_table(self):
130
203
<td>{ hkl } </td>
131
204
<td>{ offset :.2f} </td>
132
205
<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>
206
+ <td>{ self .surface_areas [i ]:.3f} </td>
207
+ <td>{ self .surface_atom_charges [i ]:.3f} </td>
208
+ <td>{ self .surface_atom_charges [i ] / self .projected_area [i ]:.4f} </td>
209
+ <td>{ self .surface_node_representative_charge [i ]:.4f} </td>
135
210
</tr>
136
211
"""
137
212
138
213
# HTML Table Footer
139
214
html += """
140
215
</table>
141
216
<p><i> *-Surface charge is based on gasteiger partial charges <a href="https://www.sciencedirect.com/science/article/pii/S0040403901949779?via%3Dihub">10.1016/S0040-4039(01)94977-9</a></i> </p>
217
+ <p> Topological surface charge is defined as the average triangle charge on the surface multiplied by the % area contribution towards the total surface. </p>
142
218
</body>
143
219
</html>
144
220
"""
0 commit comments