# 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 numpy as np
from matplotlib import pyplot as plt
from mpl_toolkits.mplot3d import Axes3D
from mpl_toolkits.mplot3d.art3d import Poly3DCollection
import plotly.graph_objects as go
from octadist.src import elements, plane, projection, util
[docs]class DrawComplex_Matplotlib:
"""
Display 3D structure of octahedral complex with label for each atoms using Matplotlib.
Parameters
----------
atom : list
Atomic symbols of octahedral structure.
Default is None.
coord : list or array_like or tuple or bool
Atomic coordinates of octahedral structure.
Default is None.
cutoff_global : int or float
Global cutoff for screening bonds.
Default is 2.0.
cutoff_hydrogen : int or float
Cutoff for screening hydrogen bonds.
Default is 1.2.
See Also
--------
draw.DrawComplex_Plotly :
Use Plotly engine to draw a complex.
Examples
--------
>>> atom = ['Fe', 'N', 'N', 'N', 'O', 'O', 'O']
>>> coord = [[2.298354000, 5.161785000, 7.971898000],
[1.885657000, 4.804777000, 6.183726000],
[1.747515000, 6.960963000, 7.932784000],
[4.094380000, 5.807257000, 7.588689000],
[0.539005000, 4.482809000, 8.460004000],
[2.812425000, 3.266553000, 8.131637000],
[2.886404000, 5.392925000, 9.848966000]]
>>> test = DrawComplex_Matplotlib(atom=atom, coord=coord)
>>> test.add_atom()
>>> test.add_bond()
>>> test.add_legend()
>>> test.show_plot()
"""
def __init__(self, atom=None, coord=None, cutoff_global=2.0, cutoff_hydrogen=1.2):
self.atom = atom
self.coord = coord
self.cutoff_global = cutoff_global
self.cutoff_hydrogen = cutoff_hydrogen
if self.atom is None:
raise TypeError("atom is not specified")
if self.coord is None:
raise TypeError("coord is not specified")
self.title_name = "Display Complex"
self.title_size = "12"
self.label_size = "10"
self.show_title = True
self.show_axis = True
self.show_grid = True
self.atoms_pair = []
self.bond_list = None
self.start_plot()
self.plot_title()
[docs] def start_plot(self):
"""
Introduce figure to plot.
"""
self.fig = plt.figure()
self.ax = Axes3D(self.fig)
# ax = fig.add_subplot(111, projection='3d')
[docs] def plot_title(self, title="Full complex", font_size="12"):
"""
Add plot title at top position.
Parameters
----------
title : str
Top title of the plot.
Default is "Full complex".
fontsize : int or float or str
Font size of title.
Default is "12".
"""
self.st = self.fig.suptitle(title, fontsize=font_size)
[docs] def add_atom(self):
"""
Add all atoms to show in figure.
"""
for i in range(len(self.coord)):
# Determine atomic number
n = elements.number_to_symbol(self.atom[i])
self.ax.scatter(
self.coord[i][0],
self.coord[i][1],
self.coord[i][2],
marker="o",
linewidths=0.5,
edgecolors="black",
color=elements.number_to_color(n),
label=f"{self.atom[i]}",
s=elements.number_to_radii(n) * 300,
)
[docs] def add_symbol(self):
"""
Add symbol of atoms to show in figure.
"""
for j in range(len(self.atom)):
self.ax.text(
self.coord[j][0] + 0.1,
self.coord[j][1] + 0.1,
self.coord[j][2] + 0.1,
f"{self.atom[j]},{j}",
fontsize=9,
)
[docs] def add_bond(self):
"""
Calculate bond distance, screen bond, and add them to show in figure.
See Also
--------
octadist.src.util.find_bonds :
Find atomic bonds.
"""
_, self.bond_list = util.find_bonds(
self.atom, self.coord, self.cutoff_global, self.cutoff_hydrogen
)
for i in range(len(self.bond_list)):
get_atoms = self.bond_list[i]
x, y, z = zip(*get_atoms)
atoms = list(zip(x, y, z))
self.atoms_pair.append(atoms)
for i in range(len(self.atoms_pair)):
merge = list(zip(self.atoms_pair[i][0], self.atoms_pair[i][1]))
x, y, z = merge
self.ax.plot(x, y, z, "k-", color="black", linewidth=2)
[docs] def add_face(self, coord):
"""
Find the faces of octahedral structure and add those faces to show in figure.
See Also
--------
octadist.src.util.find_faces_octa :
Find all faces of octahedron.
"""
_, c_ref, _, _ = util.find_faces_octa(coord)
# Added faces
color_list = [
"red",
"blue",
"green",
"yellow",
"violet",
"cyan",
"brown",
"grey",
]
for i in range(8):
# Create array of vertices for 8 faces
get_vertices = c_ref[i].tolist()
x, y, z = zip(*get_vertices)
vertices = [list(zip(x, y, z))]
self.ax.add_collection3d(
Poly3DCollection(vertices, alpha=0.5, color=color_list[i])
)
[docs] def add_legend(self):
"""
Add atoms legend to show in figure.
References
----------
1. Remove duplicate labels in legend.
Ref: https://stackoverflow.com/a/26550501/6596684.
2. Fix size of point in legend.
Ref: https://stackoverflow.com/a/24707567/6596684.
"""
# remove duplicate labels
handles, labels = self.ax.get_legend_handles_labels()
handle_list, label_list = [], []
for handle, label in zip(handles, labels):
if label not in label_list:
handle_list.append(handle)
label_list.append(label)
leg = plt.legend(
handle_list, label_list, loc="lower left", scatterpoints=1, fontsize=12
)
# fix size of point in legend
for i in range(len(leg.legendHandles)):
leg.legendHandles[i]._sizes = [90]
[docs] def config_plot(self, show_title=True, show_axis=True, show_grid=True, **kwargs):
"""
Setting configuration for figure.
Parameters
----------
show_title : bool
If True, show title of figure.
If False, not show title of figure.
show_axis : bool
If True, show axis of figure.
If False, not show axis of figure.
show_grid : bool
If True, show grid of figure.
If False, not show grid of figure.
kwargs : dict, optional
title_name : title name of figure.
title_size : text size of title.
label_size : text size of axis labels.
"""
title_name_user = kwargs.get("title_name")
self.title_size = kwargs.get("title_size")
self.label_size = kwargs.get("label_size")
self.show_title = show_title
self.show_axis = show_axis
self.show_grid = show_grid
if title_name_user is not None:
self.ax.set_title(title_name_user)
if self.title_size is not None:
if title_name_user is None:
title_name_user = self.title_name
self.ax.set_title(title_name_user, fontsize=self.title_size)
if self.label_size is not None:
self.ax.set_xlabel(r"X", fontsize=self.label_size)
self.ax.set_ylabel(r"Y", fontsize=self.label_size)
self.ax.set_zlabel(r"Z", fontsize=self.label_size)
if not self.show_title:
self.ax.set_title("")
if not self.show_axis:
plt.axis("off")
if not self.show_grid:
self.ax.grid(False)
[docs] @staticmethod
def save_img(save="Complex_saved_by_OctaDist", file="png"):
"""
Save figure as an image.
Parameters
----------
save : str
Name of image file.
Default is "Complex_saved_by_OctaDist".
file : str
Image type.
Default is "png".
"""
plt.savefig(f"{save}.{file}")
[docs] @staticmethod
def show_plot():
"""
Show plot.
"""
plt.show()
[docs]class DrawComplex_Plotly:
"""
Display 3D structure of octahedral complex in web browser using Plotly.
Parameters
----------
atom : list
Atomic symbols of octahedral structure.
Default is None.
coord : list or array_like or tuple or bool
Atomic coordinates of octahedral structure.
Default is None.
cutoff_global : int or float
Global cutoff for screening bonds.
Default is 2.0.
cutoff_hydrogen : int or float
Cutoff for screening hydrogen bonds.
Default is 1.2.
See Also
--------
draw.DrawComplex_Matplotlib :
Use Matplotlib engine to draw a complex.
Examples
--------
>>> atom = ['Fe', 'N', 'N', 'N', 'O', 'O', 'O']
>>> coord = [[2.298354000, 5.161785000, 7.971898000],
[1.885657000, 4.804777000, 6.183726000],
[1.747515000, 6.960963000, 7.932784000],
[4.094380000, 5.807257000, 7.588689000],
[0.539005000, 4.482809000, 8.460004000],
[2.812425000, 3.266553000, 8.131637000],
[2.886404000, 5.392925000, 9.848966000]]
>>> test = DrawComplex_Plotly(atom=atom, coord=coord)
>>> test.add_atom()
>>> test.add_bond()
>>> test.show_plot()
"""
def __init__(self, atom=None, coord=None, cutoff_global=2.0, cutoff_hydrogen=1.2):
self.atom = atom
self.coord = coord
self.cutoff_global = cutoff_global
self.cutoff_hydrogen = cutoff_hydrogen
if self.atom is None:
raise TypeError("atom is not specified")
if self.coord is None:
raise TypeError("coord is not specified")
# Make sure that coord is a NumPy array
self.coord = np.asarray(self.coord, dtype=np.float32)
self.title_name = "Display Complex"
self.title_size = "12"
self.label_size = "10"
self.show_title = True
self.show_axis = True
self.show_grid = True
self.atoms_pair = []
self.bond_list = None
self.start_plot()
self.plot_title()
[docs] def start_plot(self):
"""
Introduce figure to plot.
"""
self.fig = plt.figure()
self.ax = Axes3D(self.fig)
# ax = fig.add_subplot(111, projection='3d')
[docs] def plot_title(self, title="Full complex", font_size="12"):
"""
Add plot title at top position.
Parameters
----------
title : str
Top title of the plot.
Default is "Full complex".
fontsize : int or float or str
Font size of title.
Default is "12".
"""
self.st = self.fig.suptitle(title, fontsize=font_size)
[docs] def add_atom(self):
"""
Add all atoms to show in figure.
"""
n = [elements.number_to_symbol(i) for i in self.atom]
s = [elements.number_to_radii(i) * 100 for i in n]
c = [elements.number_to_color(i) for i in n]
marker_data = go.Scatter3d(
x=self.coord[:, 0],
y=self.coord[:, 1],
z=self.coord[:, 2],
marker=dict(
size=s,
color=c,
colorscale='Viridis',
opacity=0.9,
line=dict(
width=10,
color='rgb(0,0,0)'
)
# linewidths=0.5,
# edgecolors="black",
),
mode='markers',
name='atoms'
)
self.fig = go.Figure(data=[marker_data])
plotly_warning = "Generated by DrawComplex_Plotly in OctaDist"
self.fig.update_layout(title=self.title_name + " : " + plotly_warning,
scene=dict(
xaxis=dict(range=[np.min(self.coord[:, 0]) -
0.5, np.max(self.coord[:, 0]) + 0.5]),
yaxis=dict(range=[np.min(self.coord[:, 1]) -
0.5, np.max(self.coord[:, 1]) + 0.5]),
zaxis=dict(range=[np.min(self.coord[:, 2]) -
0.5, np.max(self.coord[:, 2]) + 0.5])
)
# autosize=True,
# width=500, height=500,
# margin=dict(l=65, r=50, b=65, t=90),
)
[docs] def add_bond(self):
"""
Calculate bond distance, screen bond, and add them to show in figure.
See Also
--------
octadist.src.util.find_bonds :
Find atomic bonds.
"""
_, self.bond_list = util.find_bonds(
self.atom, self.coord, self.cutoff_global, self.cutoff_hydrogen
)
for i in range(len(self.bond_list)):
get_atoms = self.bond_list[i]
x, y, z = zip(*get_atoms)
atoms = list(zip(x, y, z))
self.atoms_pair.append(atoms)
for i in range(len(self.atoms_pair)):
merge = list(zip(self.atoms_pair[i][0], self.atoms_pair[i][1]))
x, y, z = merge
line = go.Scatter3d(
x=x,
y=y,
z=z,
line=dict(
width=20,
color="black",
),
opacity=0.7,
mode="lines",
name="bond-" + str(i + 1)
)
self.fig.add_trace(line)
# def add_legend(self):
# """
# Add atoms legend to show in figure.
# References
# ----------
# 1. Remove duplicate labels in legend.
# Ref: https://stackoverflow.com/a/26550501/6596684.
# 2. Fix size of point in legend.
# Ref: https://stackoverflow.com/a/24707567/6596684.
# """
# # remove duplicate labels
# handles, labels = self.ax.get_legend_handles_labels()
# handle_list, label_list = [], []
# for handle, label in zip(handles, labels):
# if label not in label_list:
# handle_list.append(handle)
# label_list.append(label)
# leg = plt.legend(
# handle_list, label_list, loc="lower left", scatterpoints=1, fontsize=12
# )
# # fix size of point in legend
# for i in range(len(leg.legendHandles)):
# leg.legendHandles[i]._sizes = [90]
[docs] def save_img(self, save="Complex_saved_by_OctaDist", file="png"):
"""
Save figure as an image. Note that psutil and plotly-orca are needed for saving Plotly plot as image.
Parameters
----------
save : str
Name of image file.
Default is "Complex_saved_by_OctaDist".
file : str
Image type.
Default is "png".
"""
self.fig.write_image(f"{save}.{file}")
[docs] def show_plot(self):
"""
Show plot.
"""
self.fig.show()
[docs]class DrawProjection:
"""
Display the selected 4 faces of octahedral complex.
Parameters
----------
atom : list
Atomic symbols of octahedral structure.
Default is None.
coord : list or array_like or tuple
Atomic coordinates of octahedral structure.
Default is None.
Examples
--------
>>> atom = ['Fe', 'N', 'N', 'N', 'O', 'O', 'O']
>>> coord = [[2.298354000, 5.161785000, 7.971898000],
[1.885657000, 4.804777000, 6.183726000],
[1.747515000, 6.960963000, 7.932784000],
[4.094380000, 5.807257000, 7.588689000],
[0.539005000, 4.482809000, 8.460004000],
[2.812425000, 3.266553000, 8.131637000],
[2.886404000, 5.392925000, 9.848966000]]
>>> test = DrawProjection(atom=atom, coord=coord)
>>> test.add_atom()
>>> test.add_symbol()
>>> test.add_plane()
>>> test.show_plot()
"""
def __init__(self, atom=None, coord=None):
self.atom = atom
self.coord = coord
if self.atom is None:
raise TypeError("atom is not specified")
if self.coord is None:
raise TypeError("coord is not specified")
self.sub_plot = []
self.start_plot()
self.plot_title()
self.shift_plot()
[docs] def start_plot(self):
"""
Introduce figure to plot.
"""
self.fig = plt.figure()
for i in range(4):
ax = self.fig.add_subplot(2, 2, int(i + 1), projection="3d")
ax.set_title(f"Pair {i + 1}")
self.sub_plot.append(ax)
[docs] def plot_title(self, title="4 pairs of opposite planes", font_size="x-large"):
"""
Add plot title at top position.
Parameters
----------
title : str
Top title of the plot.
Default is "Full complex".
fontsize : int or float or str
Font size of title.
Default is "x-large".
"""
# self.st = self.fig.suptitle(, fontsize="")
self.st = self.fig.suptitle(title, fontsize=font_size)
[docs] def shift_plot(self):
"""
Shift subplots down.
Default is 0.25.
"""
self.fig.subplots_adjust(top=0.25)
self.st.set_y(1.0)
[docs] def add_atom(self):
"""
Add all atoms to show in figure.
"""
for i in range(4):
ax = self.sub_plot[i]
# Metal
ax.scatter(
self.coord[0][0],
self.coord[0][1],
self.coord[0][2],
color="yellow",
marker="o",
s=100,
linewidths=1,
edgecolors="black",
label="Metal center",
)
# Ligand
for j in range(1, 7):
ax.scatter(
self.coord[j][0],
self.coord[j][1],
self.coord[j][2],
color="red",
marker="o",
s=50,
linewidths=1,
edgecolors="black",
label="Ligand atoms",
)
[docs] def add_symbol(self):
"""
Add all atoms to show in figure.
"""
for i in range(4):
ax = self.sub_plot[i]
# Metal
ax.text(
self.coord[0][0] + 0.1,
self.coord[0][1] + 0.1,
self.coord[0][2] + 0.1,
self.atom[0],
fontsize=9,
)
# Ligand
for j in range(1, 7):
ax.text(
self.coord[j][0] + 0.1,
self.coord[j][1] + 0.1,
self.coord[j][2] + 0.1,
f"{self.atom[j]},{j}",
fontsize=9,
)
[docs] def add_plane(self):
"""
Add the projection planes to show in figure.
See Also
--------
octadist.src.util.find_faces_octa :
Find all faces of octahedron.
"""
_, c_ref, _, c_oppo = util.find_faces_octa(self.coord)
color_1 = ["red", "blue", "orange", "magenta"]
color_2 = ["green", "yellow", "cyan", "brown"]
for i in range(4):
ax = self.sub_plot[i]
# reference face
get_vertices = c_ref[i].tolist()
x, y, z = zip(*get_vertices)
vertices_ref = [list(zip(x, y, z))]
# opposite face
x, y, z = zip(*c_oppo[i])
vertices_oppo = [list(zip(x, y, z))]
ax.add_collection3d(
Poly3DCollection(vertices_ref, alpha=0.5, color=color_1[i])
)
ax.add_collection3d(
Poly3DCollection(vertices_oppo, alpha=0.5, color=color_2[i])
)
[docs] @staticmethod
def save_img(save="Complex_saved_by_OctaDist", file="png"):
"""
Save figure as an image.
Parameters
----------
save : str
Name of image file.
Default is "Complex_saved_by_OctaDist".
file : file
Image type.
Default is "png".
"""
plt.savefig(f"{save}.{file}")
[docs] @staticmethod
def show_plot():
"""
Show plot.
"""
plt.tight_layout()
plt.show()
[docs]class DrawTwistingPlane:
"""
Display twisting triangular faces and vector projection.
Parameters
----------
atom : list
Atomic symbols of octahedral structure.
Default is None.
coord : list or array or tuple
Atomic coordinates of octahedral structure.
Default is None.
Examples
--------
>>> atom = ['Fe', 'N', 'N', 'N', 'O', 'O', 'O']
>>> coord = [[2.298354000, 5.161785000, 7.971898000],
[1.885657000, 4.804777000, 6.183726000],
[1.747515000, 6.960963000, 7.932784000],
[4.094380000, 5.807257000, 7.588689000],
[0.539005000, 4.482809000, 8.460004000],
[2.812425000, 3.266553000, 8.131637000],
[2.886404000, 5.392925000, 9.848966000]]
>>> test = DrawTwistingPlane(atom=atom, coord=coord)
>>> test.add_plane()
>>> test.add_symbol()
>>> test.add_bond()
>>> test.show_plot()
"""
def __init__(self, atom=None, coord=None, symbol_fontsize=15):
self.atom = atom
self.coord = coord
self.symbol_fontsize = symbol_fontsize
if self.atom is None:
raise TypeError("atom is not specified")
if self.coord is None:
raise TypeError("coord is not specified")
_, self.c_ref, _, self.c_oppo = util.find_faces_octa(self.coord)
self.all_ax = []
self.all_m = []
self.all_proj_ligs = []
self.start_plot()
self.plot_title()
self.shift_plot()
self.create_subplots()
[docs] def start_plot(self):
"""
Introduce figure to plot.
"""
self.fig = plt.figure()
[docs] def plot_title(self, title="Projected twisting triangular faces", font_size="x-large"):
"""
Add plot title at top position.
Parameters
----------
title : str
Top title of the plot.
Default is "Projected twisting triangular faces".
fontsize : int or float or str
Font size of title.
Default is "x-large".
"""
self.st = self.fig.suptitle(title, fontsize=font_size)
[docs] def shift_plot(self):
"""
Shift subplots down.
Default is 0.25.
"""
self.fig.subplots_adjust(top=0.25)
self.st.set_y(1.0)
[docs] def create_subplots(self):
"""
Create subplots.
"""
for i in range(4):
ax = self.fig.add_subplot(2, 2, int(i + 1), projection="3d")
ax.set_title(f"Projection plane {i + 1}", fontsize="10")
self.all_ax.append(ax)
[docs] def add_plane(self):
"""
Add the projection planes to show in figure.
See Also
--------
octadist.src.plane.find_eq_of_plane :
Find the equation of the plane.
octadist.src.projection.project_atom_onto_plane :
Orthogonal projection of point onto the plane.
"""
for i in range(4):
a, b, c, d = plane.find_eq_of_plane(
self.c_ref[i][0], self.c_ref[i][1], self.c_ref[i][2]
)
m = projection.project_atom_onto_plane(self.coord[0], a, b, c, d)
self.all_m.append(m)
ax = self.all_ax[i]
# Projected metal center atom
ax.scatter(
m[0],
m[1],
m[2],
color="orange",
s=100,
marker="o",
linewidths=1,
edgecolors="black",
label="Metal center",
)
# Reference atoms
all_proj_lig = []
for j in range(3):
ax.scatter(
self.c_ref[i][j][0],
self.c_ref[i][j][1],
self.c_ref[i][j][2],
color="red",
s=50,
marker="o",
linewidths=1,
edgecolors="black",
label="Reference atom",
)
# Project ligand atom onto the reference face
proj_lig = projection.project_atom_onto_plane(
self.c_oppo[i][j], a, b, c, d
)
all_proj_lig.append(proj_lig)
# Projected opposite atoms
ax.scatter(
proj_lig[0],
proj_lig[1],
proj_lig[2],
color="blue",
s=50,
marker="o",
linewidths=1,
edgecolors="black",
label="Projected ligand atom",
)
self.all_proj_ligs.append(all_proj_lig)
# Draw plane
get_vertices = self.c_ref[i].tolist()
x, y, z = zip(*get_vertices)
vertices = [list(zip(x, y, z))]
x, y, z = zip(*self.all_proj_ligs[i])
projected_oppo_vertices_list = [list(zip(x, y, z))]
ax.add_collection3d(Poly3DCollection(vertices, alpha=0.5, color="yellow"))
ax.add_collection3d(
Poly3DCollection(projected_oppo_vertices_list, alpha=0.5, color="blue")
)
# Adjust tick spacing
ax.set_xticks(ax.get_xticks()[::1])
ax.set_yticks(ax.get_yticks()[::1])
ax.set_zticks(ax.get_zticks()[::1])
[docs] def add_symbol(self):
"""
Add all atoms to show in figure.
"""
for i in range(4):
ax = self.all_ax[i]
ax.text(
self.all_m[i][0] + 0.2,
self.all_m[i][1] + 0.2,
self.all_m[i][2] + 0.2,
f"{self.atom[0]}'",
fontsize=self.symbol_fontsize,
)
for j in range(3):
ax.text(
self.c_ref[i][j][0] + 0.2,
self.c_ref[i][j][1] + 0.2,
self.c_ref[i][j][2] + 0.2,
f"{j + 1}",
fontsize=self.symbol_fontsize,
)
ax.text(
self.all_proj_ligs[i][j][0] + 0.2,
self.all_proj_ligs[i][j][1] + 0.2,
self.all_proj_ligs[i][j][2] + 0.2,
f"{j + 1}'",
fontsize=self.symbol_fontsize,
)
[docs] def add_bond(self):
"""
Calculate bond distance, screen bond, and add them to show in figure.
"""
for i in range(4):
for j in range(3):
merge = list(zip(self.all_m[i].tolist(), self.c_ref[i][j].tolist()))
x, y, z = merge
self.all_ax[i].plot(x, y, z, "k-", color="black")
merge = list(
zip(self.all_m[i].tolist(), self.all_proj_ligs[i][j].tolist())
)
x, y, z = merge
self.all_ax[i].plot(x, y, z, "k->", color="black")
[docs] @staticmethod
def save_img(save="Complex_saved_by_OctaDist", file="png"):
"""
Save figure as an image.
Parameters
----------
save : str
Name of image file.
Default is "Complex_saved_by_OctaDist".
file : str
Image type.
Default is "png".
"""
plt.savefig(f"{save}.{file}")
[docs] @staticmethod
def show_plot():
"""
Show plot.
"""
# plt.legend(bbox_to_anchor=(1.05, 1), loc=2)
plt.tight_layout()
plt.show()