Skip to content

Commit 7cffd87

Browse files
committed
Initial Commit of Surface Charge Script SCI-1315
1 parent 12688df commit 7cffd87

File tree

2 files changed

+414
-0
lines changed

2 files changed

+414
-0
lines changed
Lines changed: 144 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,144 @@
1+
#
2+
# This script can be used for any purpose without limitation subject to the
3+
# conditions at http://www.ccdc.cam.ac.uk/Community/Pages/Licences/v2.aspx
4+
#
5+
# This permission notice and the following statement of attribution must be
6+
# included in all copies or substantial portions of this script.
7+
#
8+
# The following line states a licence feature that is required to show this script in Mercury and Hermes script menus.
9+
# Created 18/08/2024 by Alex Moldovan
10+
# ccdc-licence-features not-applicable
11+
12+
import os
13+
import warnings
14+
from typing import List, Tuple
15+
16+
import numpy as np
17+
from ccdc.io import CrystalReader
18+
from ccdc.particle import Surface
19+
from ccdc.utilities import HTMLReport
20+
21+
22+
class SurfaceCharge:
23+
def __init__(self, crystal, use_existing_charges: bool = False):
24+
if use_existing_charges:
25+
# if crystal.molecule.assign_partial_charges() is False:
26+
# warnings.warn(f"Gasteiger charges could not be assigned to molecule: {crystal.identifier}",
27+
# RuntimeWarning)
28+
raise NotImplementedError("Use existing charges instead. Current implementation only supports Gasteiger.")
29+
self.crystal = crystal
30+
self.surface = None
31+
self._surface_charge = None
32+
33+
def calculate_surface_charge(self, hkl: Tuple[int, int, int], offset: float):
34+
self.surface = Surface(self.crystal, miller_indices=hkl, offset=offset)
35+
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)
37+
return
38+
warnings.warn(f"Gasteiger charges could not be assigned to molecule: {self.crystal.identifier}",
39+
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+
52+
53+
class SurfaceChargeController:
54+
def __init__(self, structure: str, hkl_and_offsets: List[Tuple[int, int, int, float]],
55+
output_directory: str = None, use_existing_charges: bool = False):
56+
self.surface_charges_per_area = []
57+
self.surface_charges = []
58+
self.projected_area = []
59+
self.crystal = None
60+
if output_directory is None:
61+
output_directory = os.getcwd()
62+
self.output_directory = output_directory
63+
self.structure = structure
64+
self._surfaces = None
65+
self.get_structure()
66+
self.identifier = self.crystal.identifier
67+
self._surfaces = hkl_and_offsets
68+
self.use_existing_charges = use_existing_charges
69+
70+
def get_structure(self):
71+
if "." not in self.structure:
72+
self.crystal = CrystalReader('CSD').crystal(self.structure)
73+
elif ".mol2" in self.structure:
74+
self.crystal = CrystalReader(self.structure)[0]
75+
else:
76+
raise IOError(" \n ERROR : Please supply ref code mol2")
77+
78+
@property
79+
def surfaces(self):
80+
if self._surfaces:
81+
return self._surfaces
82+
83+
def calculate_surface_charge(self):
84+
for surface in self.surfaces:
85+
charges = SurfaceCharge(crystal=self.crystal, use_existing_charges=self.use_existing_charges)
86+
charges.calculate_surface_charge(hkl=surface[:3], offset=surface[3])
87+
self.surface_charges.append(charges.surface_charge)
88+
self.projected_area.append(charges.surface.descriptors.projected_area)
89+
self.surface_charges_per_area.append(charges.surface_charge_per_area)
90+
91+
def make_report(self):
92+
html_content = self.generate_html_table()
93+
output_file = os.path.join(self.output_directory, self.identifier + "_surface_charge.html")
94+
with HTMLReport(file_name=output_file,
95+
ccdc_header=True, append=False, embed_images=False,
96+
report_title=f'Surface Charge Calculations - {self.identifier}') as self.report:
97+
self.report.write(html_content)
98+
99+
print(f"Results saved to {output_file}")
100+
101+
def generate_html_table(self):
102+
# HTML Table Header
103+
html = """
104+
<html>
105+
<head>
106+
<title>Calculation Results</title>
107+
<style>
108+
table { width: 100%; border-collapse: collapse; }
109+
th, td { border: 1px solid black; padding: 8px; text-align: center; }
110+
th { background-color: #f2f2f2; }
111+
</style>
112+
</head>
113+
<body>
114+
<h2>Calculation Results</h2>
115+
<table>
116+
<tr>
117+
<th>hkl</th>
118+
<th>Offset</th>
119+
<th>Projected Area</th>
120+
<th>Surface Charge</th>
121+
<th>Surface Charge per Projected Area</th>
122+
</tr>
123+
"""
124+
125+
# HTML Table Rows
126+
for i, (h, k, l, offset) in enumerate(self.surfaces):
127+
hkl = "{" + f"{h}, {k}, {l}" + "}"
128+
html += f"""
129+
<tr>
130+
<td>{hkl}</td>
131+
<td>{offset:.2f}</td>
132+
<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>
135+
</tr>
136+
"""
137+
138+
# HTML Table Footer
139+
html += """
140+
</table>
141+
</body>
142+
</html>
143+
"""
144+
return html

0 commit comments

Comments
 (0)