88# The following line states a licence feature that is required to show this script in Mercury and Hermes script menus.
99# Created 18/08/2024 by Alex Moldovan
1010# ccdc-licence-features not-applicable
11-
1211import os
1312import warnings
14- from typing import List , Tuple
13+ from typing import List , Tuple , Dict
1514
1615import numpy as np
1716from ccdc .io import CrystalReader
@@ -29,30 +28,99 @@ def __init__(self, crystal, use_existing_charges: bool = False):
2928 self .crystal = crystal
3029 self .surface = None
3130 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 ]
32105
33106 def calculate_surface_charge (self , hkl : Tuple [int , int , int ], offset : float ):
34107 self .surface = Surface (self .crystal , miller_indices = hkl , offset = offset )
35108 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 ()
37111 return
38112 warnings .warn (f"Gasteiger charges could not be assigned to molecule: { self .crystal .identifier } " ,
39113 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
51114
52115
53116class SurfaceChargeController :
54117 def __init__ (self , structure : str , hkl_and_offsets : List [Tuple [int , int , int , float ]],
55118 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 = []
56124 self .surface_charges_per_area = []
57125 self .surface_charges = []
58126 self .projected_area = []
@@ -84,9 +152,12 @@ def calculate_surface_charge(self):
84152 for surface in self .surfaces :
85153 charges = SurfaceCharge (crystal = self .crystal , use_existing_charges = self .use_existing_charges )
86154 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+
88159 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 )
90161
91162 def make_report (self ):
92163 html_content = self .generate_html_table ()
@@ -114,11 +185,13 @@ def generate_html_table(self):
114185 <h2>Calculation Results</h2>
115186 <table>
116187 <tr>
117- <th>hkl</th>
188+ <th width="80px" >hkl</th>
118189 <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>
122195 </tr>
123196 """
124197
@@ -130,15 +203,18 @@ def generate_html_table(self):
130203 <td>{ hkl } </td>
131204 <td>{ offset :.2f} </td>
132205 <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>
135210 </tr>
136211 """
137212
138213 # HTML Table Footer
139214 html += """
140215 </table>
141216 <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>
142218 </body>
143219 </html>
144220 """
0 commit comments