Source code for cgsmiles.graph_layout

"""
Generate positions for 2D graphs of molecules.
"""
from collections import OrderedDict
import math
import numpy as np
import networkx as nx
from .graph_layout_utils import _force_minimize, _generate_circle_coordinates, check_and_fix_cis_trans
from .linalg_functions import rotate_to_axis

[docs] def vespr_layout(graph, default_bond=1, align_with=None): """ Generate VSEPR-like layout for a molecule graph. Parameters ---------- graph: networkx.Graph the molecule to draw default_bond: float the default bond length align_with: :class:`numpy.ndarray` axis to align longest axis with Returns ------- dict a dict of positions """ dist = dict(nx.shortest_path_length(graph, weight=None)) for source, dest_d in dist.items(): for dest, d in dest_d.items(): if d % 2 == 1: # odd # sqrt(1 + (n*x)**2 + n*x**2) n = (d + 1)/2 dest_d[dest] = math.sqrt(3*n**2 - 3*n + 1) else: # even dest_d[dest] = d / 2 * math.sqrt(3) # do we need this try except???? # the spring layout helps to disentangle large molecules # for which KK otherwise produces a less optimal layout try: p0 = nx.fruchterman_reingold_layout(graph, iterations=100) except nx.NetworkXException: p0 = None pos = nx.kamada_kawai_layout(graph, pos=p0, weight=None, dist=dist, scale=1.0) pos = check_and_fix_cis_trans(graph, pos) # rotate molecule to fit into bounding box if align_with is not None: pos_arr = np.array(list(pos.values())) pos_aligned = rotate_to_axis(pos_arr, align_with) for idx, node in enumerate(pos): pos[node] = pos_aligned[idx] avg_dist = 0 for edge in graph.edges: avg_dist += np.linalg.norm(pos[edge[0]]-pos[edge[1]]) avg_dist = avg_dist / len(graph.edges) for node in pos: pos[node] *= default_bond / avg_dist return pos
[docs] def circular_layout(graph, radius, align_with=None): """ Generate circular layout for a molecule graph. Parameters ---------- graph: networkx.Graph the molecule to draw radius: float the radius of the circle align_with: :class:`numpy.ndarray` axis to align longest axis with Returns ------- dict a dict of positions """ positions = _generate_circle_coordinates(radius=radius, num_points=len(graph)) # rotate molecule to fit into bounding box if align_with is not None: pos_aligned = rotate_to_axis(pos, align_with) pos = {} start = list(graph.nodes)[0] for idx, (node, _) in enumerate(nx.find_cycle(graph, source=start)): pos[node] = positions[idx] return pos
[docs] def vespr_refined_layout(graph, default_bond=1, align_with=None, default_angle=120, target_energy=100000, lbfgs_options={}): """ Generate VESPR layout but run an optimization to refine the intial positions according to a pseudo energy minization scheme. This is method is considerably much slower than just doing the regular VESPR layout. However, it usually gives publication ready crisp molecule layouts. Parameters ---------- graph: networkx.Graph the molecule to draw default_bond: float the default bond length align_with: :class:`numpy.ndarray` axis to align longest axis with default_angle: float the default angle in degrees target_energy: float a target energy until which to minimize lbfgs_options: keyword arguments given to LBFGS.minimize Returns ------- dict a dict of positions """ atom_to_idx = OrderedDict(zip(list(graph.nodes), range(0, len(graph.nodes)))) max_iter = 5 counter = 0 while counter < max_iter: pos = vespr_layout(graph, default_bond) pos, energy = _force_minimize(graph, pos, default_bond, default_angle, atom_to_idx, lbfgs_options) if energy < target_energy: break counter += 1 # rotate molecule to fit into bounding box if align_with is not None: pos_aligned = rotate_to_axis(pos, align_with) else: pos_aligned = pos # write positions as dict of graph node keys positions = {} for node_key, idx in atom_to_idx.items(): positions[node_key] = pos_aligned[idx] return positions
LAYOUT_METHODS = {'vespr_refined': vespr_refined_layout, 'vespr': vespr_layout, 'circular': circular_layout}