def _get_text_size(self, text, padding=2, file_format='pdf'): try: import cairocffi as cairo except ImportError: import cairo # Use dummy surface to determine text extents surface = create_new_surface(file_format) cr = cairo.Context(surface) cr.set_font_size(self.options['fontSizeNormal']) extents = cr.text_extents(text) width = extents[2] + 2 * padding height = extents[3] + 2 * padding return [0, 0, width, height]
def draw(self, reaction, file_format, path=None): """ Draw the potential energy surface for the given `network` as a Cairo surface of the given `file_format`. If `path` is given, the surface is saved to that location on disk. """ try: import cairocffi as cairo except ImportError: try: import cairo except ImportError: logging.warning( 'Cairo not found; potential energy surface will not be drawn.' ) return self.reaction = reaction self.wells = [ Well(self.reaction.reactants), Well(self.reaction.products) ] # Generate the bounding rectangles for each configuration label label_rects = [] for well in self.wells: label_rects.append( self._get_label_size(well, file_format=file_format)) # Get energy range (use kJ/mol internally) e0_min, e0_max = self._get_energy_range() e0_min *= 0.001 e0_max *= 0.001 # Drawing parameters padding = self.options['padding'] well_width = self.options['wellWidth'] well_spacing = self.options['wellSpacing'] e_slope = self.options['Eslope'] ts_width = self.options['TSwidth'] e0_offset = self.options['E0offset'] * 0.001 # Choose multiplier to convert energies to desired units (on figure only) e_units = self.options['Eunits'] try: e_mult = { 'J/mol': 1.0, 'kJ/mol': 0.001, 'cal/mol': 1.0 / 4.184, 'kcal/mol': 1.0 / 4184., 'cm^-1': 1.0 / 11.962 }[e_units] except KeyError: raise InputError( 'Invalid value "{0}" for Eunits parameter.'.format(e_units)) # Determine height required for drawing e_height = self._get_text_size('0.0', file_format=file_format)[3] + 6 y_e0 = (e0_max - 0.0) * e_slope + padding + e_height height = (e0_max - e0_min) * e_slope + 2 * padding + e_height + 6 for i in range(len(self.wells)): if 0.001 * self.wells[i].E0 == e0_min: height += label_rects[i][3] break # Determine naive position of each well (one per column) coordinates = np.zeros((len(self.wells), 2), np.float64) x = padding for i in range(len(self.wells)): well = self.wells[i] rect = label_rects[i] this_well_width = max(well_width, rect[2]) e0 = 0.001 * well.E0 y = y_e0 - e0 * e_slope coordinates[i] = [x + 0.5 * this_well_width, y] x += this_well_width + well_spacing width = x + padding - well_spacing # Determine the rectangles taken up by each well # We'll use this to merge columns safely so that wells don't overlap well_rects = [] for i in range(len(self.wells)): l, t, w, h = label_rects[i] x, y = coordinates[i, :] if w < well_width: w = well_width t -= 6 + e_height h += 6 + e_height well_rects.append([l + x - 0.5 * w, t + y + 6, w, h]) # Squish columns together from the left where possible until an isomer is encountered old_left = np.min(coordinates[:, 0]) n_left = -1 columns = [] for i in range(n_left, -1, -1): top = well_rects[i][1] bottom = top + well_rects[i][3] for column in columns: for c in column: top0 = well_rects[c][1] bottom0 = top + well_rects[c][3] if (top0 <= top <= bottom0) or (top <= top0 <= bottom): # Can't put it in this column break else: # Can put it in this column column.append(i) break else: # Needs a new column columns.append([i]) for column in columns: column_width = max([well_rects[c][2] for c in column]) x = coordinates[column[0] + 1, 0] - 0.5 * well_rects[ column[0] + 1][2] - well_spacing - 0.5 * column_width for c in column: delta = x - coordinates[c, 0] well_rects[c][0] += delta coordinates[c, 0] += delta new_left = np.min(coordinates[:, 0]) coordinates[:, 0] -= new_left - old_left # Squish columns together from the right where possible until an isomer is encountered n_right = 3 columns = [] for i in range(n_right, len(self.wells)): top = well_rects[i][1] bottom = top + well_rects[i][3] for column in columns: for c in column: top0 = well_rects[c][1] bottom0 = top0 + well_rects[c][3] if (top0 <= top <= bottom0) or (top <= top0 <= bottom): # Can't put it in this column break else: # Can put it in this column column.append(i) break else: # Needs a new column columns.append([i]) for column in columns: column_width = max([well_rects[c][2] for c in column]) x = coordinates[column[0] - 1, 0] + 0.5 * well_rects[ column[0] - 1][2] + well_spacing + 0.5 * column_width for c in column: delta = x - coordinates[c, 0] well_rects[c][0] += delta coordinates[c, 0] += delta width = max([rect[2] + rect[0] for rect in well_rects]) - min( [rect[0] for rect in well_rects]) + 2 * padding # Draw to the final surface surface = create_new_surface(file_format=file_format, target=path, width=width, height=height) cr = cairo.Context(surface) # Some global settings cr.select_font_face("sans") cr.set_font_size(self.options['fontSizeNormal']) # Fill the background with white cr.set_source_rgba(1.0, 1.0, 1.0, 1.0) cr.paint() self._draw_text('E0 ({0})'.format(e_units), cr, 15, 10, padding=2) # write units # Draw reactions e0_reac = self.wells[0].E0 * 0.001 - e0_offset e0_prod = self.wells[1].E0 * 0.001 - e0_offset e0_ts = self.reaction.transition_state.conformer.E0.value_si * 0.001 - e0_offset x1, y1 = coordinates[0, :] x2, y2 = coordinates[1, :] x1 += well_spacing / 2.0 x2 -= well_spacing / 2.0 if abs(e0_ts - e0_reac) > 0.1 and abs(e0_ts - e0_prod) > 0.1: if len(self.reaction.reactants) == 2: if e0_reac < e0_prod: x0 = x1 + well_spacing * 0.5 else: x0 = x2 - well_spacing * 0.5 elif len(self.reaction.products) == 2: if e0_reac < e0_prod: x0 = x2 - well_spacing * 0.5 else: x0 = x1 + well_spacing * 0.5 else: x0 = 0.5 * (x1 + x2) y0 = y_e0 - (e0_ts + e0_offset) * e_slope width1 = (x0 - x1) width2 = (x2 - x0) # Draw horizontal line for TS cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) cr.set_line_width(2.0) cr.move_to(x0 - ts_width / 2.0, y0) cr.line_to(x0 + ts_width / 2.0, y0) cr.stroke() # Add background and text for energy e0 = "{0:.1f}".format(e0_ts * 1000. * e_mult) extents = cr.text_extents(e0) x = x0 - extents[2] / 2.0 y = y0 - 6.0 cr.rectangle(x + extents[0] - 2.0, y + extents[1] - 2.0, extents[2] + 4.0, extents[3] + 4.0) cr.set_source_rgba(1.0, 1.0, 1.0, 0.75) cr.fill() cr.move_to(x, y) cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) cr.show_text(e0) # Draw Bezier curve connecting reactants and products through TS cr.set_source_rgba(0.0, 0.0, 0.0, 0.5) cr.set_line_width(1.0) cr.move_to(x1, y1) cr.curve_to(x1 + width1 / 8.0, y1, x0 - width1 / 8.0 - ts_width / 2.0, y0, x0 - ts_width / 2.0, y0) cr.move_to(x0 + ts_width / 2.0, y0) cr.curve_to(x0 + width2 / 8.0 + ts_width / 2.0, y0, x2 - width2 / 8.0, y2, x2, y2) cr.stroke() else: width = (x2 - x1) # Draw Bezier curve connecting reactants and products through TS cr.set_source_rgba(0.0, 0.0, 0.0, 0.5) cr.set_line_width(1.0) cr.move_to(x1, y1) cr.curve_to(x1 + width / 4.0, y1, x2 - width / 4.0, y2, x2, y2) cr.stroke() # Draw wells (after path reactions so that they are on top) for i, well in enumerate(self.wells): x0, y0 = coordinates[i, :] # Draw horizontal line for well cr.set_line_width(4.0) cr.move_to(x0 - well_width / 2.0, y0) cr.line_to(x0 + well_width / 2.0, y0) cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) cr.stroke() # Add background and text for energy e0 = well.E0 * 0.001 - e0_offset e0 = "{0:.1f}".format(e0 * 1000. * e_mult) extents = cr.text_extents(e0) x = x0 - extents[2] / 2.0 y = y0 - 6.0 cr.rectangle(x + extents[0] - 2.0, y + extents[1] - 2.0, extents[2] + 4.0, extents[3] + 4.0) cr.set_source_rgba(1.0, 1.0, 1.0, 0.75) cr.fill() cr.move_to(x, y) cr.set_source_rgba(0.0, 0.0, 0.0, 1.0) cr.show_text(e0) # Draw background and text for label x = x0 - 0.5 * label_rects[i][2] y = y0 + 6 cr.rectangle(x, y, label_rects[i][2], label_rects[i][3]) cr.set_source_rgba(1.0, 1.0, 1.0, 0.75) cr.fill() self._draw_label(well, cr, x, y, file_format=file_format) # Finish Cairo drawing if file_format == 'png': surface.write_to_png(path) else: surface.finish()