Esempio n. 1
0
    def __getTextSize(self, text, padding=2, format='pdf'):
        try:
            import cairocffi as cairo
        except ImportError:
            import cairo

        # Use dummy surface to determine text extents
        surface = createNewSurface(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]
Esempio n. 2
0
    def draw(self, network, format, path=None):
        """
        Draw the potential energy surface for the given `network` as a Cairo
        surface of the given `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.network = network

        # The order of wells is as follows:
        #   - Reactant channels come first (to the left)
        #   - Isomers are in the middle
        #   - Product channels come last (to the right)
        # This is done because most people will read the PES from left to right
        wells = []
        wells.extend(network.reactants)
        wells.extend(network.isomers)
        wells.extend(network.products)

        # Generate the bounding rectangles for each configuration label
        labelRects = []
        for well in wells:
            labelRects.append(self.__getLabelSize(well, format=format))

        # Get energy range (use kJ/mol internally)
        E0min, E0max = self.__getEnergyRange()
        E0min *= 0.001; E0max *= 0.001
        
        # Drawing parameters
        padding = self.options['padding']
        wellWidth = self.options['wellWidth']
        wellSpacing = self.options['wellSpacing']
        Eslope = self.options['Eslope']
        TSwidth = self.options['TSwidth']
        
        E0_offset = self.options['E0offset'] * 0.001
        
        # Choose multiplier to convert energies to desired units (on figure only)
        Eunits = self.options['Eunits']
        try:
            Emult = {'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}[Eunits]
        except KeyError:
            raise Exception('Invalid value "{0}" for Eunits parameter.'.format(Eunits))
            
        # Determine height required for drawing
        Eheight = self.__getTextSize('0.0', format=format)[3] + 6
        y_E0 = (E0max - 0.0) * Eslope + padding + Eheight
        height = (E0max - E0min) * Eslope + 2 * padding + Eheight + 6
        for i in range(len(wells)):
            if 0.001 * wells[i].E0 == E0min:
                height += labelRects[i][3]
                break
        
        # Determine naive position of each well (one per column)
        coordinates = numpy.zeros((len(wells), 2), numpy.float64)
        x = padding
        for i in range(len(wells)):
            well = wells[i]
            rect = labelRects[i]
            thisWellWidth = max(wellWidth, rect[2])
            E0 = 0.001 * well.E0
            y = y_E0 - E0 * Eslope
            coordinates[i] = [x + 0.5 * thisWellWidth, y]
            x += thisWellWidth + wellSpacing
        width = x + padding - wellSpacing
        
        # Determine the rectangles taken up by each well
        # We'll use this to merge columns safely so that wells don't overlap
        wellRects = []
        for i in range(len(wells)):
            l, t, w, h = labelRects[i]
            x, y = coordinates[i,:]
            if w < wellWidth: w = wellWidth
            t -= 6 + Eheight
            h += 6 + Eheight
            wellRects.append([l + x - 0.5 * w, t + y + 6, w, h])
        
        # Squish columns together from the left where possible until an isomer is encountered
        oldLeft = numpy.min(coordinates[:,0])
        Nleft = wells.index(network.isomers[0])-1
        columns = []
        for i in range(Nleft, -1, -1):
            top = wellRects[i][1]
            bottom = top + wellRects[i][3]
            for j in range(len(columns)):
                for c in columns[j]:
                    top0 = wellRects[c][1]
                    bottom0 = top + wellRects[c][3]
                    if (top >= top0 and top <= bottom0) or (top <= top0 and top0 <= bottom):
                        # Can't put it in this column
                        break
                else:
                    # Can put it in this column
                    columns[j].append(i)
                    break
            else:
                # Needs a new column
                columns.append([i])
        for column in columns:
            columnWidth = max([wellRects[c][2] for c in column])
            x = coordinates[column[0]+1,0] - 0.5 * wellRects[column[0]+1][2] - wellSpacing - 0.5 * columnWidth
            for c in column:
                delta = x - coordinates[c,0]
                wellRects[c][0] += delta
                coordinates[c,0] += delta
        newLeft = numpy.min(coordinates[:,0])
        coordinates[:,0] -= newLeft - oldLeft

        # Squish columns together from the right where possible until an isomer is encountered
        Nright = wells.index(network.isomers[-1])+1
        columns = []
        for i in range(Nright, len(wells)):
            top = wellRects[i][1]
            bottom = top + wellRects[i][3]
            for j in range(len(columns)):
                for c in columns[j]:
                    top0 = wellRects[c][1]
                    bottom0 = top0 + wellRects[c][3]
                    if (top >= top0 and top <= bottom0) or (top <= top0 and top0 <= bottom):
                        # Can't put it in this column
                        break
                else:
                    # Can put it in this column
                    columns[j].append(i)
                    break
            else:
                # Needs a new column
                columns.append([i])
        for column in columns:
            columnWidth = max([wellRects[c][2] for c in column])
            x = coordinates[column[0]-1,0] + 0.5 * wellRects[column[0]-1][2] + wellSpacing + 0.5 * columnWidth
            for c in column:
                delta = x - coordinates[c,0]
                wellRects[c][0] += delta
                coordinates[c,0] += delta

        width = max([rect[2]+rect[0] for rect in wellRects]) - min([rect[0] for rect in wellRects]) + 2 * padding
        
        # Draw to the final surface
        surface = createNewSurface(format=format, path=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()

#        # DEBUG: Draw well bounding rectangles
#        cr.save()
#        cr.set_line_width(1.0)
#        for rect in wellRects:
#            cr.rectangle(*rect)
#            cr.set_source_rgba(0.0, 0.0, 1.0, 0.5)
#            cr.stroke()  
#        cr.restore()

        # Draw path reactions
        for rxn in network.pathReactions:
            for reac in range(len(wells)):
                if wells[reac].species == rxn.reactants:
                    break
            else:
                raise Exception
            for prod in range(len(wells)):
                if wells[prod].species == rxn.products:
                    break
            else:
                raise Exception
            E0_reac = wells[reac].E0 * 0.001 - E0_offset
            E0_prod = wells[prod].E0 * 0.001 - E0_offset
            E0_TS = rxn.transitionState.conformer.E0.value_si * 0.001 - E0_offset
            if reac < prod:
                x1, y1 = coordinates[reac,:]
                x2, y2 = coordinates[prod,:]
            else:
                x1, y1 = coordinates[prod,:]
                x2, y2 = coordinates[reac,:]
            x1 += wellSpacing / 2.0; x2 -= wellSpacing / 2.0
            if abs(E0_TS - E0_reac) > 0.1 and abs(E0_TS - E0_prod) > 0.1:
                if len(rxn.reactants) == 2:
                    if reac < prod: x0 = x1 + wellSpacing * 0.5
                    else:           x0 = x2 - wellSpacing * 0.5
                elif len(rxn.products) == 2:
                    if reac < prod: x0 = x2 - wellSpacing * 0.5
                    else:           x0 = x1 + wellSpacing * 0.5
                else:
                    x0 = 0.5 * (x1 + x2)
                y0 = y_E0 - (E0_TS + E0_offset) * Eslope
                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 - TSwidth/2.0, y0)
                cr.line_to(x0+TSwidth/2.0, y0)
                cr.stroke()
                # Add background and text for energy
                E0 = "{0:.1f}".format(E0_TS * 1000. * Emult)
                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 - TSwidth/2.0, y0,   x0 - TSwidth/2.0, y0)
                cr.move_to(x0 + TSwidth/2.0, y0)
                cr.curve_to(x0 + width2/8.0 + TSwidth/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(wells):
            x0, y0 = coordinates[i,:]
            # Draw horizontal line for well
            cr.set_line_width(4.0)
            cr.move_to(x0 - wellWidth/2.0, y0)
            cr.line_to(x0 + wellWidth/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. * Emult)
            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 * labelRects[i][2]
            y = y0 + 6
            cr.rectangle(x, y, labelRects[i][2], labelRects[i][3])
            cr.set_source_rgba(1.0, 1.0, 1.0, 0.75)
            cr.fill()
            self.__drawLabel(well, cr, x, y, format=format)
        
        # Finish Cairo drawing
        if format == 'png':
            surface.write_to_png(path)
        else:
            surface.finish()