Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Surface Charge Script #67

Merged
merged 8 commits into from
Aug 20, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions scripts/ReadMe.md
Original file line number Diff line number Diff line change
Expand Up @@ -44,6 +44,10 @@ This folder contains scripts submitted by users or CCDC scientists for anyone to

- Calculates the simulated BFDH particle rugosity weighted by facet area.

## Surface Charge

- Calculates the surface charge for a given structure and surface terminations. Runs both from CMD and Mercury.

## Tips

A section for top tips in using the repository and GitHub.
Expand Down
50 changes: 50 additions & 0 deletions scripts/surface_charge/ReadMe.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,50 @@
# Surface Charge Calculator

## Summary

This tool returns the total surface charges for a given structure and list of supplied hkl indices and offsets.
The script provides a GUI that can be used from Mercury or from the command line.

The output is an HTML file with a table for all the selected surfaces and their associated charges, projected surface areas, and normalised surface charges (surface charge per projected area).

Charges are currently calculated using the Gasteiger charge model. Further development could be made to use user derived charges. Please let us know if that is of interest: [support@ccdc.cam.ac.uk](support@ccdc.cam.ac.uk).

Example Output:

![Example Output](assets/example_output.png)

> **Note** - When comparing charges for non-CSD structures and structures from mol2 files the values might be different as the bonding might not be the same. When importing a mol2 file the bonding and charges may have to be calculated on the fly, whereas this information is assigned for CSD entries.
Alex-AMC marked this conversation as resolved.
Show resolved Hide resolved
## Requirements

- Requires a minimum of CSD 2022.2

## Licensing Requirements

- CSD-Particle Licence

## Instructions for use

- To Run from command line:

```commandline
# With an activated environment
> python surface_charge.py
```

- To run from mercury:
Add the folder containing the script to your Python API menu. Mercury -> CSD Python API-> Options -> Add Location. Then select the `surface_charge.py` script from the drop down menu
![Adding_Locations](assets/adding_location.png)
![Selecting Scripts](assets/selecting_script.png)

Running from either the command line or Mercury will show the same interface allowing you to select a refcode from the CSD or input a mol2 file directly.

Example Input:

![Example Input](assets/example_input.png)

## Author

Alex Moldovan (2024)

> For feedback or to report any issues please contact [support@ccdc.cam.ac.uk](mailto:support@ccdc.cam.ac.uk)
Alex-AMC marked this conversation as resolved.
Show resolved Hide resolved
Alex-AMC marked this conversation as resolved.
Show resolved Hide resolved
Binary file added scripts/surface_charge/assets/adding_location.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added scripts/surface_charge/assets/example_input.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added scripts/surface_charge/assets/example_output.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
270 changes: 270 additions & 0 deletions scripts/surface_charge/surface_charge.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,270 @@
#
# This script can be used for any purpose without limitation subject to the
# conditions at http://www.ccdc.cam.ac.uk/Community/Pages/Licences/v2.aspx
#
# This permission notice and the following statement of attribution must be
# included in all copies or substantial portions of this script.
#
# The following line states a licence feature that is required to show this script in Mercury and Hermes script menus.
# Created 18/08/2024 by Alex Moldovan (https://orcid.org/0000-0003-2776-3879)


import os
import sys
import tkinter as tk
from tkinter import ttk, messagebox, filedialog

from ccdc.utilities import ApplicationInterface

from surface_charge_calculator import SurfaceChargeController


class SurfaceChargeGUI:
Alex-AMC marked this conversation as resolved.
Show resolved Hide resolved
def __init__(self, initial_file_path=None):
self.root = tk.Tk()
self.root.title("Surface Charge Calculator")
try:
photo = tk.PhotoImage(file=os.path.join(os.path.dirname(__file__), 'assets/csd-python-api-logo.png'))
self.root.wm_iconphoto(False, photo)
except FileNotFoundError:
print("Could not find icon file for app.")
except Exception as e:
print("Unable to load icon")
print(e) # This doesn't seem to work with X11 port forwarding 🤷‍♀️
# Disable window resizing
self.root.resizable(False, False)

self.initial_file_path = initial_file_path
self.create_string_file_inputs()
self.create_input_fields()
self.create_buttons()
self.create_treeview()
self.create_directory_selection()
self.configure_grid() # Ensure grid configuration
if self.initial_file_path:
self.handle_initial_file_path(self.initial_file_path)

def handle_initial_file_path(self, file_path):
"""Handles the initial file path by disabling the input fields and setting the file path."""
self.file_var.set(file_path) # Set the provided file path
self.string_var.set("") # Clear the string input

# Disable the input fields
self.string_entry.config(state='disabled')
self.file_entry.config(state='readonly')
self.browse_button.config(state='disabled')

def configure_grid(self):
self.root.grid_rowconfigure(8, weight=1)
self.root.grid_rowconfigure(9, weight=0)
self.root.grid_rowconfigure(10, weight=0)

self.root.grid_columnconfigure(0, weight=1)
self.root.grid_columnconfigure(1, weight=1)
self.root.grid_columnconfigure(2, weight=1)
self.root.grid_columnconfigure(3, weight=1)
self.root.grid_columnconfigure(4, weight=1)
self.root.grid_columnconfigure(5, weight=1)
self.root.grid_columnconfigure(6, weight=1)
self.root.grid_columnconfigure(7, weight=1)

def create_string_file_inputs(self):
tk.Label(self.root, text="Structure").grid(row=0, column=0, columnspan=2, sticky='w')

tk.Label(self.root, text="Refcode:").grid(row=1, column=0, padx=5, pady=5, sticky='e')
self.string_var = tk.StringVar()
self.string_entry = tk.Entry(self.root, textvariable=self.string_var, validate="key",
validatecommand=(self.root.register(self.on_string_input), "%P"))
self.string_entry.grid(row=1, column=1, padx=5, pady=5, columnspan=2, sticky='ew')

tk.Label(self.root, text="Select File:").grid(row=2, column=0, padx=5, pady=5, sticky='e')
self.file_var = tk.StringVar()
self.file_entry = tk.Entry(self.root, textvariable=self.file_var, state='readonly')
self.file_entry.grid(row=2, column=1, padx=5, pady=5, columnspan=2, sticky='ew')
self.browse_button = tk.Button(self.root, text="Browse", command=self.browse_file)
self.browse_button.grid(row=2, column=3, padx=5, pady=5, sticky='ew')

def on_string_input(self, input_value):
if input_value.strip():
self.browse_button.config(state='disabled')
else:
self.browse_button.config(state='normal')
return True

def create_input_fields(self):
tk.Label(self.root, text="Select hkl and offset").grid(row=3, column=0, columnspan=2, sticky='w')

input_frame = tk.Frame(self.root)
input_frame.grid(row=4, column=0, columnspan=8, padx=5, pady=5, sticky='ew')

input_frame.grid_columnconfigure(0, weight=1)
input_frame.grid_columnconfigure(1, weight=1)
input_frame.grid_columnconfigure(2, weight=1)
input_frame.grid_columnconfigure(3, weight=1)
input_frame.grid_columnconfigure(4, weight=1)
input_frame.grid_columnconfigure(5, weight=1)
input_frame.grid_columnconfigure(6, weight=1)
input_frame.grid_columnconfigure(7, weight=1)

tk.Label(input_frame, text="h:").grid(row=0, column=0, padx=2, pady=5, sticky='e')
tk.Label(input_frame, text="k:").grid(row=0, column=2, padx=2, pady=5, sticky='e')
tk.Label(input_frame, text="l:").grid(row=0, column=4, padx=2, pady=5, sticky='e')
tk.Label(input_frame, text="offset:").grid(row=0, column=6, padx=2, pady=5, sticky='e')

self.h_var = tk.IntVar()
self.spin_h = tk.Spinbox(input_frame, from_=-9, to=9, width=2, textvariable=self.h_var)
self.spin_h.grid(row=0, column=1, padx=2, pady=5, sticky='ew')

self.k_var = tk.IntVar()
self.spin_k = tk.Spinbox(input_frame, from_=-9, to=9, width=2, textvariable=self.k_var)
self.spin_k.grid(row=0, column=3, padx=2, pady=5, sticky='ew')

self.l_var = tk.IntVar()
self.spin_z = tk.Spinbox(input_frame, from_=-9, to=9, width=2, textvariable=self.l_var)
self.spin_z.grid(row=0, column=5, padx=2, pady=5, sticky='ew')

self.offset_var = tk.DoubleVar()
self.entry_offset = tk.Entry(input_frame, width=10, textvariable=self.offset_var)
self.entry_offset.grid(row=0, column=7, padx=2, pady=5, sticky='ew')

def create_buttons(self):
self.add_button = tk.Button(self.root, text="Add Surface", command=self.add_combination)
self.add_button.grid(row=5, column=0, columnspan=2, pady=10, sticky='ew')

self.delete_button = tk.Button(self.root, text="Delete Selected", command=self.delete_combination)
self.delete_button.grid(row=5, column=2, pady=5, sticky='ew')

self.reset_button = tk.Button(self.root, text="Reset Fields", command=self.reset_fields)
self.reset_button.grid(row=5, column=3, pady=5, sticky='ew')

self.create_directory_selection()

def create_directory_selection(self):
tk.Label(self.root, text="Output Directory:").grid(row=9, column=0, padx=5, pady=5, sticky='e')

self.dir_var = tk.StringVar(value=os.getcwd()) # Default to current working directory
self.dir_entry = tk.Entry(self.root, textvariable=self.dir_var, state='readonly', width=50)
self.dir_entry.grid(row=9, column=1, padx=5, pady=5, columnspan=3, sticky='ew')

self.browse_dir_button = tk.Button(self.root, text="Browse", command=self.select_directory)
self.browse_dir_button.grid(row=9, column=4, padx=5, pady=5, sticky='ew')

self.calculate_button = tk.Button(self.root, text="Calculate", command=self.calculate)
self.calculate_button.grid(row=10, column=0, columnspan=5, pady=10, sticky='ew')

def select_directory(self):
selected_dir = filedialog.askdirectory(initialdir=self.dir_var.get(), title="Select Output Directory")
if selected_dir:
self.dir_var.set(selected_dir)

def create_treeview(self):

tk.Label(self.root, text="Current Selections").grid(row=7, column=0, padx=5, pady=5, columnspan=8,
sticky='w')
self.combination_tree = ttk.Treeview(self.root, columns=("h", "k", "l", "Offset"), show='headings')
self.combination_tree.grid(row=8, column=0, columnspan=8, padx=10, pady=10, sticky='nsew')

self.combination_tree.heading("h", text="h")
self.combination_tree.heading("k", text="k")
self.combination_tree.heading("l", text="l")
self.combination_tree.heading("Offset", text="Offset")

self.combination_tree.column("h", width=50, anchor=tk.CENTER)
self.combination_tree.column("k", width=50, anchor=tk.CENTER)
self.combination_tree.column("l", width=50, anchor=tk.CENTER)
self.combination_tree.column("Offset", width=100, anchor=tk.CENTER)

def browse_file(self):
file_path = filedialog.askopenfilename(filetypes=[("mol2 files", "*.mol2")])
if file_path:
self.file_var.set(file_path)

def add_combination(self):
try:
h = self.h_var.get()
k = self.k_var.get()
l = self.l_var.get()
Alex-AMC marked this conversation as resolved.
Show resolved Hide resolved
Alex-AMC marked this conversation as resolved.
Show resolved Hide resolved
if (h, k, l) == (0, 0, 0):
messagebox.showerror("Invalid input", "Please enter valid integers for h, k, l and a float for offset.")
return
offset = self.offset_var.get()
combination = (h, k, l, offset)
if not self.is_duplicate(combination):
self.combination_tree.insert('', tk.END, values=combination)
else:
messagebox.showwarning("Duplicate Entry", "This hkl and offset already exists.")
except tk.TclError:
messagebox.showerror("Invalid input", "Please enter valid integers for h, k, l and a float for offset.")

def is_duplicate(self, combination):
combination_converted = tuple((str(i) for i in combination))
for row_id in self.combination_tree.get_children():
row_values = self.combination_tree.item(row_id, 'values')
if tuple(row_values) == combination_converted:
return True
return False

def delete_combination(self):
selected_item = self.combination_tree.selection()
if selected_item:
self.combination_tree.delete(selected_item)
else:
messagebox.showwarning("No selection", "Please select a surface to delete.")

def reset_fields(self):
self.h_var.set(0)
self.k_var.set(0)
self.l_var.set(0)
self.offset_var.set(0.0)
self.string_var.set("")
self.file_var.set("")
self.browse_button.config(state='normal')

def calculate(self):
string_input = self.string_var.get().strip()
file_input = self.file_var.get().strip()
if not (string_input or file_input):
tk.messagebox.showerror("Input Error", "Please provide a refcode or select a file.")
return

if not self.combination_tree.get_children():
tk.messagebox.showerror("Selection Error", "There must be at least one surface in the list.")
return

items = self.combination_tree.get_children()
data = []
for item in items:
values = self.combination_tree.item(item, 'values')
try:
h = int(values[0])
k = int(values[1])
l = int(values[2])
Alex-AMC marked this conversation as resolved.
Show resolved Hide resolved
Alex-AMC marked this conversation as resolved.
Show resolved Hide resolved
offset = float(values[3])
data.append((h, k, l, offset))
except ValueError as e:
print(f"Error converting data: {e}")
continue
if string_input:
input_string = string_input # Use string input if available
elif file_input:
input_string = file_input

output_dir = self.dir_var.get()

surface_charge_controller = SurfaceChargeController(structure=input_string, output_directory=output_dir,
hkl_and_offsets=data)
surface_charge_controller.calculate_surface_charge()
surface_charge_controller.make_report()
self.root.destroy()


if __name__ == "__main__":
if len(sys.argv) > 3 and sys.argv[3].endswith(".m2a"):
mercury = ApplicationInterface()
run_from_mercury = True
input_structure = mercury.input_mol2_file
app = SurfaceChargeGUI(initial_file_path=input_structure)
app.root.mainloop()
else:
app = SurfaceChargeGUI()
app.root.mainloop()
Loading