Skip to content

Commit e6bf9da

Browse files
committed
Adding functions to display HoukMol figures in Matplotlib plots
1 parent 1ebc525 commit e6bf9da

File tree

3 files changed

+146
-1
lines changed

3 files changed

+146
-1
lines changed

atoms.py

+38
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
SATURATION,
1717
TMETAL,
1818
VDW_RADII,
19+
COLORS,
1920
)
2021

2122
warn_LJ = set([])
@@ -1248,3 +1249,40 @@ def get_vsepr(self):
12481249
best_shape = shape
12491250

12501251
return best_shape, best_score
1252+
1253+
1254+
def draw_atom(self, ax, fp = 40, ascale = 0.5, linewidth=0.1):
1255+
"""
1256+
Draw HoukMol style atom and add to Matplotlib axis ax
1257+
1258+
:param matplotlib.pyplot.Axis ax: Matplotlib axis object
1259+
:param float fp: z-value for focal point to add simple perspective (Default: 40)
1260+
:param float ascale: scaling factor for covalent radii (Default: 0.5)
1261+
:param float linewidth: linewidth (in points) for atom details (default: 0.1)
1262+
"""
1263+
1264+
try:
1265+
import matplotlib.patches as patches
1266+
except:
1267+
ls.LOG.error("Must install matplot lib")
1268+
return None
1269+
1270+
x, y, z = self.coords
1271+
r = RADII[self.element]*ascale*(fp + z)/fp
1272+
color=COLORS[self.element]
1273+
1274+
# draw circle and two ellipses for atom plus two circles for specular highlight
1275+
circle = patches.Circle((x, y), r, facecolor=color, edgecolor='black', lw=linewidth, zorder=z)
1276+
acc = patches.Circle((x+r/6, y+r/4), 0.2*r, alpha=0.7, facecolor='white', edgecolor="None", zorder=z)
1277+
acc2 = patches.Circle((x+r/6, y+r/4), 0.3*r, alpha=0.3, facecolor='white', edgecolor="None", zorder=z)
1278+
arc1 = patches.Arc((x, y), r, 2*r, theta1=270, theta2=90, edgecolor='black', lw=linewidth, zorder=z)
1279+
arc2 = patches.Arc((x, y), 2*r, r, theta1=180, theta2=360, edgecolor='black', lw=linewidth, zorder=z)
1280+
1281+
# add atom componets to axis
1282+
ax.add_patch(circle)
1283+
ax.add_patch(acc)
1284+
ax.add_patch(acc2)
1285+
ax.add_patch(arc1)
1286+
ax.add_patch(arc2)
1287+
1288+

const.py

+21
Original file line numberDiff line numberDiff line change
@@ -2796,6 +2796,27 @@
27962796
"Au",
27972797
)
27982798

2799+
# Rasmol colors from jmol up to Cl
2800+
COLORS={
2801+
"H":"#FFFFFF",
2802+
"He": "#FFC0CB",
2803+
"Li": "#B22222",
2804+
"Be": "#FF1493",
2805+
"B": "#00FF00",
2806+
"C": "#C8C8C8",
2807+
"N": "#8F8FFF",
2808+
"O": "#F00000",
2809+
"F": "#DAA520",
2810+
"Ne": "#FF1493",
2811+
"Na": "#0000FF",
2812+
"Mg": "#228B22",
2813+
"Al": "#808090",
2814+
"Si": "#DAA520",
2815+
"P": "#FFA500",
2816+
"S": "#FFC832",
2817+
"Cl": "00FF00"
2818+
}
2819+
27992820
class PHYSICAL:
28002821
# Physical constants
28012822
BOLTZMANN = 0.001987204 # kcal/mol

geometry.py

+87-1
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,7 @@
1616
from AaronTools import addlogger
1717
from AaronTools.atoms import Atom, BondOrder
1818
from AaronTools.config import Config
19-
from AaronTools.const import AARONLIB, AARONTOOLS, BONDI_RADII, D_CUTOFF, ELEMENTS, TMETAL, VDW_RADII
19+
from AaronTools.const import AARONLIB, AARONTOOLS, BONDI_RADII, D_CUTOFF, ELEMENTS, TMETAL, VDW_RADII, RADII
2020
from AaronTools.fileIO import FileReader, FileWriter, read_types
2121
from AaronTools.finders import Finder, OfType, WithinRadiusFromPoint, WithinRadiusFromAtom
2222
from AaronTools.utils.prime_numbers import Primes
@@ -1013,6 +1013,92 @@ def convert_to_Psi4(self, charge=0, mult=1, fix_com=True, fix_orientation=True):
10131013
return mol
10141014

10151015

1016+
# quick and dirty code to display HoukMol style figures in Matplotlib
1017+
# bond ends could be handled more elegently, but this looks fine for most molecules
1018+
1019+
def plot(self, ax, fig, fp=40, ascale=0.5, bwidth=0.15):
1020+
"""
1021+
displays HoukMol style molecule in Matplotlib
1022+
1023+
:param matplotlib.pyplot.Axis ax: Matplotlib Axis object
1024+
:param matplotlib.pyplot.Figure fig: Matplotlib Figure object:w
1025+
:param float fp: z-value (Angstroms) for focal point for adding perspective (Default: 40)
1026+
:param float ascale: scaling factor for covalent radii (default: 0.5)
1027+
:param float bwidth: scaling factor for bond radii (default: 0.15)
1028+
"""
1029+
1030+
try:
1031+
import matplotlib.patches as patches
1032+
except:
1033+
self.LOG.error("Must install matplot lib")
1034+
return None
1035+
1036+
def intersect(atom1, atom2, fp):
1037+
# returns intersection of line from atom1 to edge of scaled sphere for atom2
1038+
# from https://en.wikipedia.org/wiki/Line%E2%80%93sphere_intersection
1039+
1040+
c = atom2.coords # center of sphere
1041+
r = RADII[atom2.element]*0.5*(fp + atom2.coords[2])/fp # radius of sphere, adjusted for perspective
1042+
o = atom1.coords # starting point for line
1043+
u = atom1.bond(atom2) # vector along line
1044+
unorm = u/np.sqrt(np.linalg.norm(u)) # normalized vector
1045+
1046+
dot = np.dot(unorm, o - c)
1047+
delta = dot**2 - (np.linalg.norm(o - c)**2 - r**2)
1048+
1049+
if delta <= 0:
1050+
# catch cases where there is no intersection for some reason
1051+
return c
1052+
else:
1053+
d = dot + np.sqrt(delta)
1054+
return o - d*u*1.0 # adjust endpoints slightly to account for rounded capstyle
1055+
1056+
1057+
1058+
def draw_bond(x1, y1, x2, y2, z, fp, scale, ax, bwidth=0.15):
1059+
"""
1060+
Draw HoukMol style bond as black line with rounded ends from (x1, y1) to (x2, y2)
1061+
width of bond is controlled by width and scaled to z-value for perspective
1062+
"""
1063+
1064+
w=(bwidth*scale) * (fp + z)/fp
1065+
ax.plot([x1, x2], [y1, y2], lw=w, color='black', solid_capstyle='round', zorder=z)
1066+
1067+
1068+
# if xlim not set manually then I can't determine the overall size of the plot until
1069+
# after the molecule is drawn, but I need overall size to determine scale for bond
1070+
# widths
1071+
1072+
if ax.get_xaxis()._get_autoscale_on():
1073+
self.LOG.warning("You will probalby need to set xlim manually for correct bond widths.")
1074+
1075+
# get scale and size of plot set linewidths in terms of pts (1/72 inch per pt)
1076+
xmin, xmax = ax.get_xlim()
1077+
dx = xmax - xmin
1078+
ymin, ymax = ax.get_ylim()
1079+
dy = ymax - ymin
1080+
fw, fh = fig.get_size_inches()
1081+
bbox = ax.get_position()
1082+
ax_width = fw*bbox.width
1083+
scale = 72*ax_width/dx
1084+
1085+
# sort atoms by z-value
1086+
sorted_atoms = [atom for atom in sorted(self.atoms, key=lambda a: a.coords[2])]
1087+
1088+
for atom in sorted_atoms:
1089+
# note that I draw each bond twice to get both ends correct
1090+
# this avoids having to do the logic of figuring out the order of
1091+
# drawing bonds to a given atom--each atom obscures bonds originating
1092+
# from that atom, but this is corrected when the bond is drawn
1093+
# from the connected atom. Any way I've tried to fix this looks wrong
1094+
# for planar molecules, which are the only ones I actually care about.
1095+
1096+
for connected in atom.connected:
1097+
endpoint = intersect(atom, connected, fp)
1098+
draw_bond(atom.coords[0], atom.coords[1], endpoint[0], endpoint[1], atom.coords[2], fp, scale, ax, bwidth)
1099+
1100+
atom.draw_atom(ax, fp, ascale=ascale, linewidth=0.01*scale)
1101+
10161102
def copy(self, atoms=None, name=None, comment=None, copy_atoms=True):
10171103
"""
10181104
creates a new copy of the geometry

0 commit comments

Comments
 (0)