# OctaDist Copyright (C) 2019 Rangsiman Ketkaew et al.
#
# This program is free software: you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation, either version 3 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License
# along with this program. If not, see <https://www.gnu.org/licenses/>.
# ++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
import base64
import os
import platform
import subprocess
import tkinter as tk
import tkinter.scrolledtext as tkscrolled
import webbrowser
from datetime import datetime
from tkinter import filedialog
from tkinter import messagebox
from tkinter import ttk
from tkinter.messagebox import showinfo
from urllib.request import urlopen
import numpy as np
import octadist
from octadist.logo import Icon_Base64
from octadist.src import io, calc, draw, plot, popup, scripting, structure, tools
[docs]class OctaDist:
"""
OctaDist class initiates main program UI and create all widgets.
Program interface is structured as follows:
+-------------------+
| Program Menu |
+-------------------+
| Frame 1 |
+---------+---------+
| Frame 2 | Frame 3 |
+---------+---------+
| Frame 4 |
+-------------------+
- Frame 1 : Program name and short description
- Frame 2 : Program console
- Frame 3 : Textbox for showing summary output
- Frame 4 : textbox for showing detailed output
Examples
--------
>>> my_app = OctaDist()
>>> my_app.start_app()
"""
def __init__(self):
self.master = tk.Tk()
# Initialize parameters
self.file_list = [] # Full path of input files.
self.file_name = [] # File name.
self.octa_index = [] # Octahedral structure index.
self.atom_coord_full = [] # Coordinates of metal complex.
self.atom_coord_octa = [] # Coordinates of octahedral structures.
self.all_zeta = [] # Zeta of all octahedral structures.
self.all_delta = [] # Delta of all octahedral structures.
self.all_sigma = [] # Sigma of all octahedral structures.
self.all_theta = [] # Theta of all octahedral structures.
self.comp_result = [] # Distortion parameters.
self.octadist_icon = None
# Default cutoff values
self.cutoff_metal_ligand = 2.8
self.cutoff_global = 2.0
self.cutoff_hydrogen = 1.2
# Default executable of text editor
self.text_editor = "notepad.exe"
# Default molecular visualizer
self.visualizer = "Matplotlib"
# Default display settings
self.show_title = True
self.show_axis = True
self.show_grid = True
self.backup_cutoff_metal_ligand = self.cutoff_metal_ligand
self.backup_cutoff_global = self.cutoff_global
self.backup_cutoff_hydrogen = self.cutoff_hydrogen
self.backup_text_editor = self.text_editor
self.backup_visualizer = self.visualizer
self.backup_show_title = self.show_title
self.backup_show_axis = self.show_axis
self.backup_show_grid = self.show_grid
# Create master frame, sub-frames, add menu, and add widgets
self.create_logo()
self.start_master()
self.add_menu()
self.add_widgets()
self.welcome_msg()
self.start_app()
[docs] def create_logo(self):
"""
Create icon file from Base64 raw code.
This will be used only for Windows OS.
Other OS like Linux and macOS use default logo of Tkinter.
Examples
--------
>>> if self.octadist_icon is True:
>>> self.create_logo()
>>> else:
>>> pass
"""
if platform.system() == "Windows":
icon_data = base64.b64decode(Icon_Base64.icon_base64)
temp_file = "icon.ico"
save_path = os.path.expanduser("~/AppData/Local/Temp")
self.octadist_icon = os.path.join(save_path, temp_file)
icon_file = open(self.octadist_icon, "wb")
icon_file.write(icon_data)
icon_file.close()
self.master.wm_iconbitmap(self.octadist_icon)
[docs] def start_master(self):
"""
Start application with UI settings.
"""
self.master.title(f"OctaDist {octadist.__version__}")
font = "Arial 10"
self.master.option_add("*Font", font)
center_width = (self.master.winfo_screenwidth() / 2.0) - (550 / 2.0)
center_height = (self.master.winfo_screenheight() / 2.0) - (750 / 2.0)
self.master.geometry("525x635+%d+%d" % (center_width, center_height))
self.master.resizable(0, 0)
[docs] def show_text(self, text):
"""
Insert text to result box
Parameters
----------
text : str
Text to show in result box.
Returns
-------
None : None
"""
self.box_result.insert(tk.INSERT, text + "\n")
self.box_result.see(tk.END)
[docs] def welcome_msg(self):
"""
Show welcome message in result box:
1. Program name, version, and release.
2. Full author names.
3. Official website: https://octadist.github.io.
"""
full_version = octadist.__version__ + " " + f"({octadist.__release__})"
self.show_text(f"Welcome to OctaDist version {full_version}\n")
self.show_text(f"Developed by {octadist.__author_full__}.\n")
self.show_text(octadist.__website__ + "\n")
#####################
# Manipulating File #
#####################
[docs] def open_file(self):
"""
Open file dialog where the user will browse input files.
"""
self.clear_cache()
input_file = filedialog.askopenfilenames(
title="Choose input file",
filetypes=(
("CIF File", "*.cif"),
("XYZ File", "*.xyz"),
("Gaussian Output File", "*.out"),
("Gaussian Output File", "*.log"),
("NWChem Output File", "*.out"),
("NWChem Output File", "*.log"),
("ORCA Output File", "*.out"),
("ORCA Output File", "*.log"),
("Q-Chem Output File", "*.out"),
("Q-Chem Output File", "*.log"),
("All Files", "*.*"),
),
)
self.file_list = list(input_file)
self.search_coord()
[docs] def search_coord(self):
"""
Search and extract atomic symbols and coordinates from input file.
See Also
--------
octadist.src.io.extract_coord :
Extract atomic symbols and atomic coordinates from input file.
octadist.src.io.extract_octa :
Extract octahedral structure from complex.
"""
try:
open(self.file_list[0], "r")
except IndexError:
return 1
for i in range(len(self.file_list)):
########################################
# Extract atomic coordinates from file #
########################################
atom_full, coord_full = io.extract_coord(self.file_list[i])
self.atom_coord_full.append([atom_full, coord_full])
# If either lists is empty, then continue to next file
if len(list(atom_full)) == 0 or len(coord_full) == 0:
continue
#################################################
# Extract octahedral structure from the complex #
#################################################
total_metal, atom_metal, coord_metal = io.find_metal(atom_full, coord_full)
if total_metal == 0:
popup.warn_no_metal(i + 1)
# loop over number of metal found in complex
for j in range(total_metal):
atom_octa, coord_octa = io.extract_octa(atom_full, coord_full, j + 1, self.cutoff_metal_ligand)
# If no atomic coordinates inside, it will raise error
if np.any(coord_octa) == 0:
popup.err_no_coord(i + 1)
continue
if len(coord_octa) < 7:
self.clear_result_box()
popup.err_less_ligands(i + 1)
continue
# File number and file name
file_name = self.file_list[i].split("/")[-1]
self.file_name.append([i + 1, file_name])
# Metal center atom
self.octa_index.append(atom_octa[0])
# Atomic labels and atomic coordinates
# Example:
#
# atom = ['Fe', 'O', 'O', 'N', 'N', 'N', 'N']
# coord = [[2.298354, 5.161785, 7.971898],
# [1.885657, 4.804777, 6.183726],
# [1.747515, 6.960963, 7.932784],
# [4.094380, 5.807257, 7.588689],
# [0.539005, 4.482809, 8.460004],
# [2.812425, 3.266553, 8.131637],
# [2.886404, 5.392925, 9.848966]]
self.atom_coord_octa.append([atom_octa, coord_octa])
self.show_coord()
[docs] def show_coord(self):
"""
Show coordinates in box.
"""
# loop over complex
for i in range(len(self.atom_coord_octa)):
if i == 0:
self.show_text("XYZ coordinates of extracted octahedral structure")
self.show_text(f"File {self.file_name[i][0]}: {self.file_name[i][1]}")
self.show_text(f"Metal center atom: {self.octa_index[i]}")
self.show_text("Atom\t\tCartesian coordinate")
# loop over atoms in octahedron
for k in range(7):
self.show_text(
" {0:>2} {1:14.9f} {2:14.9f} {3:14.9f}".format(
self.atom_coord_octa[i][0][k],
self.atom_coord_octa[i][1][k][0],
self.atom_coord_octa[i][1][k][1],
self.atom_coord_octa[i][1][k][2],
)
)
self.show_text("")
[docs] def save_results(self):
"""
Save results as output file. Default file extension is .txt.
"""
f = filedialog.asksaveasfile(
mode="w",
defaultextension=".txt",
title="Save results",
filetypes=(("TXT File", "*.txt"), ("All Files", "*.*"))
)
f.write(f"{octadist.__copyright__}\n")
f.write("="*60 + "\n")
f.write("\n")
f.write(f"OctaDist version {octadist.__version__} ({octadist.__release__})\n")
f.write("Octahedral Distortion Calculator\n")
f.write(f"{octadist.__website__}\n")
today = datetime.now().strftime("%b-%d-%y %H:%M:%S")
f.write(f"{today}\n")
f.write("\n")
f.write("="*60 + "\n")
f.write("\n")
get_result = self.box_result.get("1.0", tk.END + "-1c")
f.write(get_result)
f.write("\n")
f.write("="*60 + "\n")
f.close()
popup.info_save_results(f.name)
[docs] def save_coord(self):
"""
Save atomic coordinates (Cartesian coordinate) of octahedral structure.
Default file extension is .xyz.
"""
if len(self.file_list) == 0:
popup.err_no_file()
return 1
if len(self.file_list) > 1:
popup.err_many_files()
return 1
f = filedialog.asksaveasfile(
mode="w",
defaultextension=".xyz",
title="Save atomic coordinates",
filetypes=(
("XYZ File", "*.xyz"),
("TXT File", "*.txt"),
("All Files", "*.*"))
)
file_name = self.file_list[0].split("/")[-1]
atoms = self.atom_coord_octa[0][0]
coord = self.atom_coord_octa[0][1]
num_atom = 7
f.write(f"{num_atom}\n")
full_version = octadist.__version__ + " " + f"({octadist.__release__})"
f.write(f"{file_name} : this file was generated by OctaDist version {full_version}.\n")
for i in range(num_atom):
f.write("{0:2s}\t{1:9.6f}\t{2:9.6f}\t{3:9.6f}\n".format(atoms[i], coord[i][0], coord[i][1], coord[i][2]))
f.write("\n")
f.close()
popup.info_save_results(f.name)
###################################
# Calculate distortion parameters #
###################################
[docs] def calc_distortion(self):
"""
Calculate all distortion parameters:
- D_mean
- Zeta
- Delta
- Sigma
- Theta
See Also
--------
octadist.src.calc.CalcDistortion.calc_d_mean :
Calculate mean metal-ligand bond length.
octadist.src.calc.CalcDistortion.calc_zeta :
Calculate Zeta parameter.
octadist.src.calc.CalcDistortion.calc_delta :
Calculate Delta parameter.
octadist.src.calc.CalcDistortion.calc_sigma :
Calculate Sigma parameter.
octadist.src.calc.CalcDistortion.calc_theta :
Calculate Theta parameter.
"""
if len(self.atom_coord_octa) >= 1:
self.clear_param_box()
else:
popup.err_no_file()
return 1
# if comp_result is not empty, clean it to avoid over loop.
if self.comp_result:
self.comp_result = []
# loop over number of metal complexes
for i in range(len(self.atom_coord_octa)):
atom_octa, coord_octa = self.atom_coord_octa[i]
# Calculate distortion parameters
calc_dist = calc.CalcDistortion(coord_octa)
d_mean = calc_dist.d_mean
zeta = calc_dist.zeta
delta = calc_dist.delta
sigma = calc_dist.sigma
theta = calc_dist.theta
non_octa = calc_dist.non_octa
if non_octa:
popup.warn_not_octa()
# Collect results
self.all_zeta.append(zeta)
self.all_delta.append(delta)
self.all_sigma.append(sigma)
self.all_theta.append(theta)
self.comp_result.append([d_mean, zeta, delta, sigma, theta])
# Print results to each unique box.
if len(self.atom_coord_octa) == 1:
d_mean, zeta, delta, sigma, theta = self.comp_result[0]
self.box_d_mean.insert(tk.INSERT, f"{d_mean:3.6f}")
self.box_zeta.insert(tk.INSERT, f"{zeta:3.6f}")
self.box_delta.insert(tk.INSERT, f"{delta:3.6f}")
self.box_sigma.insert(tk.INSERT, f"{sigma:3.6f}")
self.box_theta_mean.insert(tk.INSERT, f"{theta:3.6f}")
else:
self.box_d_mean.insert(tk.INSERT, "See below")
self.box_zeta.insert(tk.INSERT, "See below")
self.box_delta.insert(tk.INSERT, "See below")
self.box_sigma.insert(tk.INSERT, "See below")
self.box_theta_mean.insert(tk.INSERT, "See below")
# Print results to result box
self.show_text("Computed octahedral distortion parameters for all complexes\n")
self.show_text("No. - Metal\t\tD_mean\tZeta\tDelta\tSigma\tTheta")
self.show_text("*"*71)
for i in range(len(self.comp_result)):
self.show_text(
"{0:2d} - {1}\t\t{2:9.4f}\t{3:9.6f}\t{4:9.6f}\t{5:9.4f}\t{6:9.4f}".format(
i + 1,
self.octa_index[i],
self.comp_result[i][0],
self.comp_result[i][1],
self.comp_result[i][2],
self.comp_result[i][3],
self.comp_result[i][4]
)
)
###################
# Program Setting #
###################
[docs] def settings(self):
"""
Program settings allows the user to configure the values of variables
that used in molecular display function.
For example, cutoff distance for screening bond distance between atoms.
"""
def open_exe():
"""
Program setting: Open dialog in which the user will choose text editor.
"""
try:
input_file = filedialog.askopenfilename(
title="Choose text editor executable",
filetypes=[("EXE file", "*.exe")],
)
file_list = str(input_file)
entry_exe.delete(0, tk.END)
entry_exe.insert(tk.INSERT, file_list)
except IndexError:
return 1
def is_show_title():
"""
Check if title of figure will be set to show or not.
"""
if var_title.get():
var_title.set(True)
else:
var_title.set(False)
def is_show_axis():
"""
Check if axis of figure will be set to show or not.
"""
if var_axis.get():
var_axis.set(True)
else:
var_axis.set(False)
def is_show_grid():
"""
Check if grid of figure will be set to show or not.
"""
if var_grid.get():
var_grid.set(True)
else:
var_grid.set(False)
def restore_settings(self):
"""
Restore all settings.
"""
self.cutoff_metal_ligand = self.backup_cutoff_metal_ligand
self.cutoff_global = self.backup_cutoff_global
self.cutoff_hydrogen = self.backup_cutoff_hydrogen
self.text_editor = self.backup_text_editor
self.visualizer = self.backup_visualizer
self.show_title = self.backup_show_title
self.show_axis = self.backup_show_axis
self.show_grid = self.backup_show_grid
var_1.set(self.cutoff_metal_ligand)
var_2.set(self.cutoff_global)
var_3.set(self.cutoff_hydrogen)
var_title.set(self.show_title)
var_axis.set(self.show_axis)
var_grid.set(self.show_grid)
entry_exe.delete(0, tk.END)
entry_exe.insert(tk.INSERT, self.text_editor)
def click_ok(self):
"""
If the user click OK, it will save all settings and show info in output box.
"""
self.cutoff_metal_ligand = float(var_1.get())
self.cutoff_global = float(var_2.get())
self.cutoff_hydrogen = float(var_3.get())
self.text_editor = str(entry_exe.get())
self.visualizer = str(var_vis.get())
self.show_title = bool(var_title.get())
self.show_axis = bool(var_axis.get())
self.show_grid = bool(var_grid.get())
self.show_text("Updated program settings")
self.show_text("************************")
self.show_text(f"Metal-ligand bond cutoff\t\t\t{self.cutoff_metal_ligand}")
self.show_text(f"Global bond cutoff\t\t\t{self.cutoff_global}")
self.show_text(f"Hydrogen bond cutoff\t\t\t{self.cutoff_hydrogen}")
self.show_text(f"Text editor\t\t\t{self.text_editor}")
self.show_text(f"Molecular visualizer\t\t\t{self.visualizer}")
self.show_text(f"Show Title\t\t\t{self.show_title}")
self.show_text(f"Show Axis\t\t\t{self.show_axis}")
self.show_text(f"Show Grid\t\t\t{self.show_grid}\n")
wd.destroy()
def click_cancel():
"""
If the user click CANCEL, close window.
"""
wd.destroy()
###################
# Setting: Widget #
###################
wd = tk.Toplevel(self.master)
if self.octadist_icon is not None:
wd.wm_iconbitmap(self.octadist_icon)
wd.title("Program settings")
wd.option_add("*Font", "Arial 10")
frame = tk.Frame(wd)
frame.grid()
###################
# Setting: Cutoff #
###################
frame_cutoff = tk.LabelFrame(frame, text="Bond Cutoff:")
frame_cutoff.grid(padx=5, pady=5, ipadx=5, ipady=5, sticky="W", row=0, columnspan=4)
label_1 = tk.Label(frame_cutoff, text="Metal-Ligand Bond")
label_1.grid(padx="10", pady="5", ipadx="10", row=0, column=0)
var_1 = tk.DoubleVar()
var_1.set(self.cutoff_metal_ligand)
scale_1 = tk.Scale(frame_cutoff, orient="horizontal", variable=var_1, to=5, resolution=0.1)
scale_1.configure(width=20, length=100)
scale_1.grid(padx="10", pady="5", ipadx="10", row=1, column=0)
label_2 = tk.Label(frame_cutoff, text="Global Distance")
label_2.grid(padx="10", pady="5", ipadx="10", row=0, column=1)
var_2 = tk.DoubleVar()
var_2.set(self.cutoff_global)
scale_2 = tk.Scale(frame_cutoff, orient="horizontal", variable=var_2, to=5, resolution=0.1)
scale_2.configure(width=20, length=100)
scale_2.grid(padx="10", pady="5", ipadx="10", row=1, column=1)
label_3 = tk.Label(frame_cutoff, text="Hydrogen Distance")
label_3.grid(padx="10", pady="5", ipadx="10", row=0, column=2)
var_3 = tk.DoubleVar()
var_3.set(self.cutoff_hydrogen)
scale_3 = tk.Scale(frame_cutoff, orient="horizontal", variable=var_3, to=5, resolution=0.1)
scale_3.configure(width=20, length=100)
scale_3.grid(padx="10", pady="5", ipadx="10", row=1, column=2)
########################
# Setting: Text editor #
########################
frame_text_editor = tk.LabelFrame(frame, text="Text editor:")
frame_text_editor.grid(padx=5, pady=5, ipadx=5, ipady=5, sticky="W", row=1, columnspan=4)
label = tk.Label(frame_text_editor, text="Enter the EXE:")
label.grid(padx="5", sticky=tk.E, row=0, column=0)
entry_exe = tk.Entry(frame_text_editor, bd=2, width=60)
entry_exe.grid(row=0, column=1)
button = tk.Button(frame_text_editor, text="Browse...", command=open_exe)
button.grid(padx="5", pady="5", sticky=tk.W, row=0, column=2)
entry_exe.insert(tk.INSERT, self.text_editor)
########################
# Setting : Visualizer #
########################
frame_visualizer = tk.LabelFrame(frame, text="Visualizer:")
frame_visualizer.grid(padx=5, pady=5, ipadx=5, ipady=5, sticky="W", row=2)
visualizers = ['Matplotlib', 'Plotly']
var_vis = tk.StringVar()
var_vis.set(self.visualizer)
vis = ttk.Combobox(frame_visualizer, textvariable=var_vis, values=visualizers)
vis.grid(padx=5, pady=5, ipadx=5, ipady=5, sticky="W")
####################
# Setting: Figures #
####################
frame_figures = tk.LabelFrame(frame, text="Displays:")
frame_figures.grid(padx=5, pady=5, ipadx=5, ipady=5, sticky="W", row=3, columnspan=4)
# Show title of plot?
var_title = tk.BooleanVar()
var_title.set(self.show_title)
show_title = ttk.Checkbutton(
frame_figures,
text="Show Figure Title",
onvalue=True,
offvalue=False,
variable=var_title,
command=lambda: is_show_title(),
)
show_title.grid(padx="5", pady="5", ipadx="25", sticky=tk.E, row=0, column=0)
# Show axis?
var_axis = tk.BooleanVar()
var_axis.set(self.show_axis)
show_axis = ttk.Checkbutton(
frame_figures,
text="Show Axis",
onvalue=True,
offvalue=False,
variable=var_axis,
command=lambda: is_show_axis(),
)
show_axis.grid(padx="5", pady="5", ipadx="25", sticky=tk.E, row=0, column=1)
# Show grid?
var_grid = tk.BooleanVar()
var_grid.set(self.show_grid)
show_grid = ttk.Checkbutton(
frame_figures,
text="Show Gridlines",
onvalue=True,
offvalue=False,
variable=var_grid,
command=lambda: is_show_grid(),
)
show_grid.grid(padx="5", pady="5", ipadx="5", sticky=tk.E, row=0, column=2)
####################
# Setting: Console #
####################
button = tk.Button(frame, text="Restore settings", command=lambda: restore_settings(self))
button.configure(width=15)
button.grid(padx="10", pady="10", sticky=tk.W, row=4, column=0)
button = tk.Button(frame, text="OK", command=lambda: click_ok(self))
button.configure(width=15)
button.grid(padx="5", pady="10", sticky=tk.E, row=4, column=2)
button = tk.Button(frame, text="Cancel", command=lambda: click_cancel())
button.configure(width=15)
button.grid(padx="5", pady="10", row=4, column=3)
frame.mainloop()
#################
# Copy and Edit #
#################
[docs] def copy_name(self):
"""
Copy input file name to clipboard.
See Also
--------
copy_path :
Copy absolute path of input file to clibboard.
copy_results :
Copy results to clibboard.
copy_octa :
Copy octahedral structure coordinates to clibboard.
"""
if len(self.file_list) == 0:
popup.err_no_file()
return 1
name = self.file_list[0].split("/")[-1]
clip = tk.Tk()
clip.withdraw()
clip.clipboard_clear()
clip.clipboard_append(name)
clip.destroy()
[docs] def copy_path(self):
"""
Copy absolute path of input file to clipboard.
See Also
--------
copy_name
Copy input file name to clibboard.
copy_results :
Copy results to clibboard.
copy_octa :
Copy octahedral structure coordinates to clibboard.
"""
if len(self.file_list) == 0:
popup.err_no_file()
return 1
clip = tk.Tk()
clip.withdraw()
clip.clipboard_clear()
clip.clipboard_append(self.file_list[0])
clip.destroy()
[docs] def copy_results(self):
"""
Copy the results and computed distortion parameters to clipboard.
See Also
--------
copy_name
Copy input file name to clibboard.
copy_path :
Copy absolute path of input file to clibboard.
copy_octa :
Copy octahedral structure coordinates to clibboard.
"""
if len(self.file_list) == 0:
popup.err_no_file()
return 1
if len(self.all_zeta) == 0:
popup.err_no_calc()
return 1
results = (
"Zeta, Delta, Sigma, Gamma\n"
"{0:3.6f}, {1:3.6f}, {2:3.6f}, {3:3.6f}".format(
self.all_zeta[0],
self.all_delta[0],
self.all_sigma[0],
self.all_theta[0],
)
)
clip = tk.Tk()
clip.withdraw()
clip.clipboard_clear()
clip.clipboard_append(results)
clip.destroy()
[docs] def copy_octa(self):
"""
Copy atomic coordinates of octahedral structure to clipboard.
See Also
--------
copy_name
Copy input file name to clibboard.
copy_path :
Copy absolute path of input file to clibboard.
copy_results :
Copy results to clibboard.
"""
if len(self.file_list) == 0:
popup.err_no_file()
return 1
clip = tk.Tk()
clip.withdraw()
clip.clipboard_clear()
clip.clipboard_append(self.atom_coord_octa[0][2])
clip.destroy()
[docs] def edit_file(self):
"""
Edit file by specified text editor on Windows.
See Also
--------
settings
"""
if len(self.file_list) == 0:
popup.err_no_file()
return 1
if self.text_editor == "":
popup.err_no_editor()
return 1
try:
for i in range(len(self.file_list)):
program_name = self.text_editor
file_name = self.file_list[i]
subprocess.Popen([program_name, file_name])
except FileNotFoundError:
return 1
#########################
# Interactive scripting #
#########################
[docs] def scripting_console(self):
"""
Start scripting interface for an interactive code.
User can access to class variable (dynamic variable).
+------------+
| Output box |
+------------+
| Input box |
+------------+
See Also
--------
settings :
Program settings.
"""
my_app = scripting.ScriptingConsole(self)
my_app.scripting_start()
##################
# Visualizations #
##################
[docs] def draw_all_atom(self):
"""
Display 3D complex.
See Also
--------
octadist.src.draw.DrawComplex :
Show 3D molecule.
"""
if len(self.atom_coord_full) == 0:
popup.err_no_file()
return 1
elif len(self.atom_coord_full) > 1:
popup.err_many_files()
return 1
atom_full, coord_full = self.atom_coord_full[0]
if self.visualizer == "Matplotlib":
my_plot = draw.DrawComplex_Matplotlib(
atom=atom_full,
coord=coord_full,
cutoff_global=self.cutoff_global,
cutoff_hydrogen=self.cutoff_hydrogen,
)
my_plot.add_atom()
my_plot.add_bond()
my_plot.add_legend()
my_plot.config_plot(
show_title=self.show_title,
show_axis=self.show_axis,
show_grid=self.show_grid,
)
my_plot.show_plot()
elif self.visualizer == "Plotly":
my_plot = draw.DrawComplex_Plotly(
atom=atom_full,
coord=coord_full,
cutoff_global=self.cutoff_global,
cutoff_hydrogen=self.cutoff_hydrogen,
)
my_plot.add_atom()
my_plot.add_bond()
my_plot.show_plot()
else:
popup.err_visualizer_not_found()
[docs] def draw_all_atom_and_face(self):
"""
Display 3D complex with the faces.
See Also
--------
octadist.src.draw.DrawComplex :
Show 3D molecule.
"""
if len(self.atom_coord_full) == 0:
popup.err_no_file()
return 1
elif len(self.atom_coord_full) > 1:
popup.err_many_files()
return 1
atom_full, coord_full = self.atom_coord_full[0]
my_plot = draw.DrawComplex_Matplotlib(
atom=atom_full,
coord=coord_full,
cutoff_global=self.cutoff_global,
cutoff_hydrogen=self.cutoff_hydrogen,
)
my_plot.add_atom()
my_plot.add_bond()
for i in range(len(self.atom_coord_octa)):
_, coord_octa = self.atom_coord_octa[i]
my_plot.add_face(coord_octa)
my_plot.add_legend()
my_plot.config_plot(
show_title=self.show_title,
show_axis=self.show_axis,
show_grid=self.show_grid,
)
my_plot.show_plot()
[docs] def draw_octa(self):
"""
Display 3D octahedral structure.
See Also
--------
octadist.src.draw.DrawComplex :
Show 3D molecule.
"""
if len(self.atom_coord_octa) == 0:
popup.err_no_file()
return 1
elif len(self.atom_coord_octa) > 1:
popup.err_many_files()
return 1
atom_octa, coord_octa = self.atom_coord_octa[0]
my_plot = draw.DrawComplex_Matplotlib(
atom=atom_octa,
coord=coord_octa,
cutoff_global=self.cutoff_global,
cutoff_hydrogen=self.cutoff_hydrogen,
)
my_plot.add_atom()
my_plot.add_bond()
my_plot.add_legend()
my_plot.config_plot(
show_title=self.show_title,
show_axis=self.show_axis,
show_grid=self.show_grid,
)
my_plot.show_plot()
[docs] def draw_octa_and_face(self):
"""
Display 3D octahedral structure with the faces.
See Also
--------
octadist.src.draw.DrawComplex :
Show 3D molecule.
"""
if len(self.atom_coord_octa) == 0:
popup.err_no_file()
return 1
elif len(self.atom_coord_octa) > 1:
popup.err_many_files()
return 1
atom_octa, coord_octa = self.atom_coord_octa[0]
my_plot = draw.DrawComplex_Matplotlib(
atom=atom_octa,
coord=coord_octa,
cutoff_global=self.cutoff_global,
cutoff_hydrogen=self.cutoff_hydrogen,
)
my_plot.add_atom()
my_plot.add_bond()
my_plot.add_legend()
for i in range(len(self.atom_coord_octa)):
_, coord = self.atom_coord_octa[i]
my_plot.add_face(coord)
my_plot.config_plot(
show_title=self.show_title,
show_axis=self.show_axis,
show_grid=self.show_grid,
)
my_plot.show_plot()
[docs] def draw_projection(self):
"""
Draw projection planes.
See Also
--------
octadist.src.draw.DrawProjection :
Show graphical projections.
"""
if len(self.atom_coord_full) == 0:
popup.err_no_file()
return 1
elif len(self.atom_coord_full) > 1:
popup.err_many_files()
return 1
atom_full, coord_full = self.atom_coord_full[0]
my_plot = draw.DrawProjection(atom=atom_full, coord=coord_full)
my_plot.add_atom()
my_plot.add_symbol()
my_plot.add_plane()
my_plot.show_plot()
[docs] def draw_twisting_plane(self):
"""
Draw twisting triangular planes.
See Also
--------
octadist.src.draw.DrawTwistingPlane :
Show graphical triangular twisting planes.
"""
if len(self.atom_coord_full) == 0:
popup.err_no_file()
return 1
elif len(self.atom_coord_full) > 1:
popup.err_many_files()
return 1
atom_full, coord_full = self.atom_coord_full[0]
my_plot = draw.DrawTwistingPlane(atom=atom_full, coord=coord_full)
my_plot.add_plane()
my_plot.add_symbol()
my_plot.add_bond()
my_plot.show_plot()
#####################
# Show data summary #
#####################
[docs] def show_data_complex(self):
"""
Show info of input complex.
See Also
--------
octadist.src.structure.DataComplex :
Show data summary of complex.
"""
if len(self.file_list) == 0:
popup.err_no_file()
return 1
my_app = structure.DataComplex(master=self.master, icon=self.octadist_icon)
for i in range(len(self.file_list)):
atom = self.atom_coord_full[i][0]
coord = self.atom_coord_full[i][1]
my_app.add_name(self.file_list[i])
my_app.add_coord(atom, coord)
[docs] def show_param_octa(self):
"""
Show structural parameters of selected octahedral structure.
See Also
--------
octadist.src.structure.StructParam :
Show structural parameter symmary of complex.
"""
if len(self.atom_coord_octa) == 0:
popup.err_no_file()
return 1
my_app = structure.StructParam(master=self.master, icon=self.octadist_icon)
for i in range(len(self.atom_coord_octa)):
metal = self.octa_index[i]
atom, coord = self.atom_coord_octa[i]
my_app.add_metal(metal)
my_app.add_coord(atom, coord)
[docs] def show_surface_area(self):
"""
Calculate the area of eight triangular faces of octahedral structure.
See Also
--------
octadist.src.structure.SurfaceArea :
Show the area of the faces of octahedral structure.
"""
if len(self.atom_coord_octa) == 0:
popup.err_no_file()
return 1
my_app = structure.SurfaceArea(master=self.master, icon=self.octadist_icon)
for i in range(len(self.atom_coord_octa)):
metal = self.octa_index[i]
atom, coord = self.atom_coord_octa[i]
my_app.add_metal(metal)
my_app.add_octa(coord)
##############################
# Plot between two data sets #
##############################
[docs] def plot_zeta_sigma(self):
"""
Plot relationship between zeta and sigma.
See Also
--------
octadist.src.plot.Plot :
Show relationship plot.
"""
if len(self.all_sigma) == 0:
popup.err_no_calc()
return 1
my_plot = plot.Plot(self.all_zeta, self.all_sigma, name1="zeta", name2="sigma")
my_plot.add_point()
my_plot.add_text()
my_plot.add_legend()
my_plot.show_plot()
[docs] def plot_sigma_theta(self):
"""
Plot relationship between sigma and theta.
See Also
--------
octadist.src.plot.Plot :
Show relationship plot.
"""
if len(self.all_sigma) == 0:
popup.err_no_calc()
return 1
my_plot = plot.Plot(self.all_sigma, self.all_theta, name1="sigma", name2="theta")
my_plot.add_point()
my_plot.add_text()
my_plot.add_legend()
my_plot.show_plot()
##################
# Analysis tools #
##################
################
# Check Update #
################
[docs] @staticmethod
def check_update():
"""
Check program update by comparing version of program user is using with
that of the latest version released on github.
References
----------
File: https://www.github.com/OctaDist/OctaDist/version_update.txt.
"""
data = urlopen("https://raw.githubusercontent.com/OctaDist/OctaDist/master/version_update.txt").read()
# decode
data = data.decode("utf-8")
data = data.split()
user_rev = float(octadist.__revision__)
server_ver = data[1]
server_rev = float(data[3]) # code version
os_name = platform.system() # find the OS name
if server_rev > user_rev:
popup.info_new_update()
text = (
f"A new version {server_ver} is ready for download.\n\n"
"Do you want to download now?"
)
msg_box = messagebox.askquestion("Updates available", text, icon="warning")
if msg_box == "yes":
dl_link = "https://github.com/OctaDist/OctaDist/releases/download/"
main_link = dl_link + "v." + server_ver + "/OctaDist-" + server_ver
if os_name == "Windows":
link_windows = main_link + "-Win-x86-64.exe"
webbrowser.open_new_tab(link_windows)
elif os_name == "Darwin" or os_name == "Linux":
link_linux = main_link + "-src-x86-64.tar.gz"
webbrowser.open_new_tab(link_linux)
else:
popup.err_cannot_update()
# Open Thank You page at the same time download the program
webbrowser.open_new_tab("https://octadist.github.io/thanks.html")
else:
pass
elif server_rev < user_rev:
popup.info_using_dev()
else:
popup.info_no_update()
#####################
# Show program info #
#####################
[docs] @staticmethod
def callback(event):
"""
On-clink open web browser.
Parameters
----------
event : object
Event object for callback.
"""
webbrowser.open_new(event.widget.cget("text"))
[docs] @staticmethod
def show_about():
"""
Show author details on a sub-window.
1. Name of authors
2. Official program website
3. Citation
"""
text = (
f"OctaDist version {octadist.__version__} ({octadist.__release__})\n\n"
f"Authors: {octadist.__author_full__}.\n\n"
f"Website: {octadist.__website__}\n\n"
f"Please cite this project if you use OctaDist for scientific publication:\n\n"
"Ketkaew, R.; Tantirungrotechai, Y.; Harding, P.; Chastanet, G.; Guionneau, P.; Marchivie, M.; Harding, D. J.\n"
"OctaDist: A Tool for Calculating Distortion Parameters in Spin Crossover and Coordination Complexes. "
"Dalton Trans. 2021. \nhttps://doi.org/10.1039/D0DT03988H"
)
showinfo("About program", text)
[docs] @staticmethod
def show_license():
"""
Show license details on a sub-window.
GNU General Public License version 3.0.
References
----------
Link: https://www.gnu.org/licenses/gpl-3.0.en.html.
"""
text = """\
OctaDist Copyright (C) 2019 Rangsiman Ketkaew et al.
This program is free software: you can redistribute it and/or modify \
it under the terms of the GNU General Public License as published by \
the Free Software Foundation, either version 3 of the License, or \
(at your option) any later version.
This program is distributed in the hope that it will be useful, \
but WITHOUT ANY WARRANTY; without even the implied warranty of \
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the \
GNU General Public License for more details.
You should have received a copy of the GNU General Public License \
along with this program. If not, see <https://www.gnu.org/licenses/>.
"""
showinfo("License", text)
###################
# Clear All Cache #
###################
[docs] def clear_cache(self):
"""
Clear program cache by nullifying all default variables
and clear both of parameter and result boxes.
"""
for name in dir():
if not name.startswith("_"):
del locals()[name]
self.file_list = []
self.file_name = []
self.octa_index = []
self.atom_coord_full = []
self.atom_coord_octa = []
self.all_zeta = []
self.all_delta = []
self.all_sigma = []
self.all_theta = []
self.comp_result = []
self.clear_param_box()
self.clear_result_box()
[docs] def clear_param_box(self):
"""
Clear parameter box.
"""
self.box_delta.delete(0, tk.END)
self.box_sigma.delete(0, tk.END)
self.box_d_mean.delete(0, tk.END)
self.box_zeta.delete(0, tk.END)
self.box_theta_mean.delete(0, tk.END)
[docs] def clear_result_box(self):
"""
Clear result box.
"""
self.box_result.delete(1.0, tk.END)
[docs] def start_app(self):
"""
Start application.
"""
self.master.mainloop()
[docs]def main():
app = OctaDist()
app.start_app()
# Delete icon after closing app
if app.octadist_icon is not None:
os.remove(app.octadist_icon)
if __name__ == "__main__":
main()