Skip to content

Commit 2279f8e

Browse files
committed
Added Topology Surface Charge SCI-1404
1 parent 51b4344 commit 2279f8e

File tree

1 file changed

+105
-22
lines changed

1 file changed

+105
-22
lines changed

scripts/surface_charge/surface_charge_calculator.py

+105-22
Original file line numberDiff line numberDiff line change
@@ -8,10 +8,9 @@
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-
1211
import os
1312
import warnings
14-
from typing import List, Tuple
13+
from typing import List, Tuple, Dict
1514

1615
import numpy as np
1716
from 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

53124
class 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 &#8491;<sup>2</sup></th>
199+
<th>S<sub>Area</sub>-Surface Area &#8491;<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

Comments
 (0)