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

drawing utils #27

Merged
merged 39 commits into from
Dec 12, 2024
Merged
Show file tree
Hide file tree
Changes from 24 commits
Commits
Show all changes
39 commits
Select commit Hold shift + click to select a range
a61134a
drawing utils
fgrunewald Oct 20, 2024
2015d28
updated drawing
fgrunewald Oct 29, 2024
2f116b3
updated drawing
fgrunewald Oct 31, 2024
be4dc0b
Merge branch 'master' into drawing
fgrunewald Nov 14, 2024
e023021
return positions
fgrunewald Nov 20, 2024
b849fc4
Merge branch 'master' into drawing
fgrunewald Nov 26, 2024
d6a7f5d
refactor drawing
fgrunewald Nov 28, 2024
1f32705
refactor drawing utils
fgrunewald Nov 28, 2024
d388ed4
refactor graph layout
fgrunewald Nov 28, 2024
7b0278e
set some defaults
fgrunewald Nov 28, 2024
be4e5aa
implement correct cis trans drawing
fgrunewald Dec 2, 2024
a0a5a97
Merge branch 'master' into drawing
fgrunewald Dec 2, 2024
1ee9177
CIS trans aware drawing
fgrunewald Dec 3, 2024
e599a69
take care of align api
fgrunewald Dec 3, 2024
c82d1ee
fix cis trans test
fgrunewald Dec 3, 2024
5d1309e
address comments
fgrunewald Dec 3, 2024
bd6ba1e
update dependency
fgrunewald Dec 3, 2024
72ac7d0
validate input
fgrunewald Dec 3, 2024
a8c98ab
fix default labels and provide example
fgrunewald Dec 3, 2024
fdeb00c
add linalg functions
fgrunewald Dec 3, 2024
bb3916a
expose drawing
fgrunewald Dec 3, 2024
891b156
bug fix
fgrunewald Dec 3, 2024
e78eb3f
add mpl to dep list
fgrunewald Dec 3, 2024
9b7f8f7
add scipy to dep list
fgrunewald Dec 3, 2024
3dd073f
address comments
fgrunewald Dec 11, 2024
c643397
Apply suggestions from code review
fgrunewald Dec 11, 2024
30d9a27
Update cgsmiles/graph_layout_utils.py
fgrunewald Dec 11, 2024
bfcfd2a
remove import from _init_
fgrunewald Dec 11, 2024
8bfdec3
raise scale error
fgrunewald Dec 11, 2024
47a2775
fix docstring
fgrunewald Dec 11, 2024
9a45ba2
fix docstring
fgrunewald Dec 11, 2024
0331407
remove duplicate function
fgrunewald Dec 11, 2024
aacb737
fix docstring
fgrunewald Dec 11, 2024
4798657
have default drawing method
fgrunewald Dec 11, 2024
22a91d6
allow flexible axis rotation
fgrunewald Dec 11, 2024
073c854
Apply suggestions from code review
fgrunewald Dec 12, 2024
d0e1778
assing alignment by keyword based on dict
fgrunewald Dec 12, 2024
c403ada
update docstirng
fgrunewald Dec 12, 2024
4a63be1
Update cgsmiles/drawing.py
fgrunewald Dec 12, 2024
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
1 change: 1 addition & 0 deletions cgsmiles/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,3 +6,4 @@
from .read_fragments import read_fragments
from .resolve import MoleculeResolver
from .sample import MoleculeSampler
from .drawing import draw_molecule
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved
244 changes: 244 additions & 0 deletions cgsmiles/drawing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,244 @@
"""
Utilities for drawing molecules and cgsmiles graphs.
"""
import matplotlib.pyplot as plt
from matplotlib.collections import LineCollection
import networkx as nx
from .graph_layout import LAYOUT_METHODS
from .drawing_utils import make_graph_edges, make_mapped_edges, make_node_pies

# loosely follow Avogadro / VMD color scheme
ELE_TO_COLOR = {"F": "lightblue",
"P": "tab:orange",
"H": "gray",
"Cl": "tab:green",
"O": "tab:red",
"C": "cyan",
"Na": "pink",
"N": "blue",
"Br": "darkred",
"I": "purple",
"S": "yellow",
"Mg": "lightgreen"}
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved

# this dict sets the colors for CG fragments
FRAGID_TO_COLOR = {0: "tab:blue",
1: "tab:red",
2: "tab:orange",
3: "tab:pink",
4: "tab:purple",
5: "tab:cyan",
6: "tab:green",
7: "tab:olive",
8: "tab:brown",
9: "tab:gray"}

DEFAULT_COLOR = "orchid"
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved

def draw_molecule(graph,
ax=None,
pos=None,
layout_method=None,
cg_mapping=True,
colors=None,
labels=None,
scale=2,
outline=False,
use_weights=False,
align_with='diag',
fontsize=10,
text_color='black',
edge_widths=3,
mapped_edge_width=20,
default_bond=1,
layout_kwargs={}):
"""
Draw the graph of a molecule optionally with a coarse-grained
projection if `cg_mapping` is set to True. The membership of
atoms to the CG projection is taken from the 'fragid' attribute.
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved

Positions or one of three layout methods mus be specified. The
layout options are vespr_refined, vespr, and circular. Note
that the vespr_refined is slow but yields the best quality results.
For a quick look vespr is recommended. The drawing function also
accepts a layout_kwarg dictionary specifiying options to be given
to the three layout methods. See also `cgsmiles.graph_layout`.
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved

For example to draw Benzen using the Martini 3 mapping:
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved

>>> import matplotlib.pyplot as plt
>>> from cgsmiles import MoleculeResolver
>>> from cgsmiles import draw_molecule

>>> cgsmiles_str = "{[#TC5]1[#TC5][#TC5]1}.{#TC5=[$]cc[$]}"
>>> resolver = MoleculeResolver.from_string(cgsmiles_str)
>>> cg_mol, aa_mol = resolver.resolve()
>>> draw_molecule(aa_mol, layout_method="vespr_refined")
>>> plt.show()

The drawing is always of fixed size based on the canvas size. This
means that molecules drawn on a canvas with the same size will have
the same dimensions (i.e. bond length, atom size). Using the scale
argument a drawing can be scaled to make it larger or smaller on
a given canvas. That means if your molecule is too small or large
redraw it setting a different scale parameter.

Parameters
----------
ax: :class:`matplotlib.pyplot.axis`
mpl axis object
pos: dict
a dict mapping nodes to 2D positions
layout_method:
choice of vespr, vespr_refined, circular
cg_mapping: bool
draw outline for the CG mapping (default: True)
colors: dict
a dict mapping nodes to colors or fragids to colors
depending on if cg_mapping is True
labels: list
list of node_labels; must follow the order of nodes in graph
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved
scale: float
scale the drawing relative to the total canvas size
outline: bool
draw an outline around each node
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved
use_weights: bool
color nodes according to weight attribute (default: False)
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved
align_with: str
align the longest distance in molecule with one of x, y, diag
fontsize: float
fontsize of labels
text_color: str
color of the node labels (default: black)
edge_widths: float
the width of the bonds
mapped_edge_widths: float
the width of the mapped projection
default_bond: float
default bond length (default: 1)
layout_kwargs: dict
dict with arguments passed to the layout methods

Returns
-------
:class:`matplotlib.pyplot.axis`
the updated axis object
dict
a dict of positions
"""
# check input args
if pos and layout_method:
msg = "You cannot provide both positions and a layout method."
raise ValueError(msg)
if pos is None and layout_method is None:
msg = "You need to provide either positions or a layout method."
raise ValueError(msg)
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved

# scaling cannot be negative
assert scale > 0
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved

# generate figure in case we don't get one
if not ax:
ax = plt.gca()

# scale edge widths and fontsize
if scale:
edge_widths = edge_widths * scale
mapped_edge_width = mapped_edge_width * scale
fontsize = fontsize * scale

# default labels are the element names
if labels is None:
labels = nx.get_node_attributes(graph, 'element')

# collect the fragids if CG projection is to be drawn
if cg_mapping:
ids = nx.get_node_attributes(graph, 'fragid')
id_set = set()
for fragid in ids.values():
id_set |= set(fragid)

# assing color defaults
if colors is None and cg_mapping:
colors = {fragid: FRAGID_TO_COLOR.get(fragid) for fragid in id_set}
elif colors is None:
colors = {node: ELE_TO_COLOR.get(ele, DEFAULT_COLOR) for node, ele in nx.get_node_attributes(graph, 'element').items()}

# if no positions are given generate layout
bbox = ax.get_position(True)

# some axis magic
fig_width_inch, fig_height_inch = ax.figure.get_size_inches()
w = bbox.width*fig_width_inch/scale
h = bbox.height*fig_height_inch/scale

# generate inital positions
if not pos:
pos = LAYOUT_METHODS[layout_method](graph,
default_bond=default_bond,
bounding_box=[w, h],
align_with=align_with,
**layout_kwargs)
fgrunewald marked this conversation as resolved.
Show resolved Hide resolved



# generate starting and stop positions for edges
edges, arom_edges, plain_edges = make_graph_edges(graph, pos)

# draw the edges
ax.add_collection(LineCollection(edges,
color='black',
linewidths=edge_widths,
zorder=2))
ax.add_collection(LineCollection(arom_edges,
color='black',
linestyle='dotted',
linewidths=edge_widths, zorder=2))

# generate the edges from the mapping
if cg_mapping:
mapped_edges = make_mapped_edges(graph, plain_edges)
for fragid, frag_edges in mapped_edges.items():
color = colors[fragid]
ax.add_collection(LineCollection(frag_edges,
color=color,
linewidths=mapped_edge_width,
zorder=1,
alpha=0.5))

# now we draw nodes
for slices, pie_kwargs in make_node_pies(graph,
pos,
cg_mapping,
colors,
outline=outline,
radius=default_bond/3.,
use_weights=use_weights,
linewidth=edge_widths):
p, _ = ax.pie(slices, **pie_kwargs)
for pie in p:
pie.set_zorder(3)

# add node texts
zorder=4
for idx, label in labels.items():
x, y = pos[idx]
ax.text(x, y, label,
zorder=zorder,
fontsize=fontsize,
verticalalignment='center_baseline',
horizontalalignment='center',
color=text_color)
zorder+=1

# compute initial view
w = bbox.width
h = bbox.height

fig_width_inch, fig_height_inch = ax.figure.get_size_inches()
w = bbox.width*fig_width_inch/scale
h = bbox.height*fig_height_inch/scale
ax.set_xlim(-w, w)
ax.set_ylim(-h, h)

return ax, pos
Loading
Loading