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,107 @@ 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+
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 ]
32113
33114 def calculate_surface_charge (self , hkl : Tuple [int , int , int ], offset : float ):
34115 self .surface = Surface (self .crystal , miller_indices = hkl , offset = offset )
35116 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 ()
37119 return
38120 warnings .warn (f"Gasteiger charges could not be assigned to molecule: { self .crystal .identifier } " ,
39121 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
51122
52123
53124class SurfaceChargeController :
54125 def __init__ (self , structure : str , hkl_and_offsets : List [Tuple [int , int , int , float ]],
55126 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 = []
56132 self .surface_charges_per_area = []
57133 self .surface_charges = []
58134 self .projected_area = []
@@ -84,9 +160,12 @@ def calculate_surface_charge(self):
84160 for surface in self .surfaces :
85161 charges = SurfaceCharge (crystal = self .crystal , use_existing_charges = self .use_existing_charges )
86162 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+
88167 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 )
90169
91170 def make_report (self ):
92171 html_content = self .generate_html_table ()
@@ -114,11 +193,13 @@ def generate_html_table(self):
114193 <h2>Calculation Results</h2>
115194 <table>
116195 <tr>
117- <th>hkl</th>
196+ <th width="80px" >hkl</th>
118197 <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>
122203 </tr>
123204 """
124205
@@ -130,8 +211,10 @@ def generate_html_table(self):
130211 <td>{ hkl } </td>
131212 <td>{ offset :.2f} </td>
132213 <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>
135218 </tr>
136219 """
137220
0 commit comments