[med-svn] [plip] 01/01: Imported Upstream version 1.3.1+dfsg

Alex Mestiashvili malex-guest at moszumanska.debian.org
Fri May 13 17:06:43 UTC 2016


This is an automated email from the git hooks/post-receive script.

malex-guest pushed a commit to annotated tag upstream/1.3.1+dfsg
in repository plip.

commit 895ae03776a0b7359559475960845c71fde168f6
Author: Alexandre Mestiashvili <alex at biotec.tu-dresden.de>
Date:   Fri May 13 17:42:52 2016 +0200

    Imported Upstream version 1.3.1+dfsg
---
 CHANGES.txt                  |   7 +
 plip/modules/chimeraplip.py  | 245 +++++++++++++++++++++
 plip/modules/config.py       |   3 +
 plip/modules/detection.py    |   2 +-
 plip/modules/plipremote.py   | 113 ++++++++++
 plip/modules/preparation.py  | 139 +++++++-----
 plip/modules/pymolplip.py    | 437 +++++++++++++++++++++++++++++++++++++
 plip/modules/supplemental.py | 112 +++++-----
 plip/modules/visualize.py    | 502 ++++---------------------------------------
 plip/plipcmd                 |  73 ++++---
 setup.py                     |   2 +-
 11 files changed, 1041 insertions(+), 594 deletions(-)

diff --git a/CHANGES.txt b/CHANGES.txt
index a044023..431fb7b 100644
--- a/CHANGES.txt
+++ b/CHANGES.txt
@@ -1,6 +1,13 @@
 Changelog
 ---------
 
+### 1.3.1
+* __Support for amino acids as ligands__
+* __Plugin-ready for PyMOL and Chimera__
+* Refactores code and optimized input
+* Improved verbose and debug log system
+* Bugfixes for problems affecting some structures with aromatic rings
+
 ### 1.3.0
 * __Batch processing__
 * Improvements to verbose mode and textual output
diff --git a/plip/modules/chimeraplip.py b/plip/modules/chimeraplip.py
new file mode 100644
index 0000000..1505a62
--- /dev/null
+++ b/plip/modules/chimeraplip.py
@@ -0,0 +1,245 @@
+"""
+Protein-Ligand Interaction Profiler - Analyze and visualize protein-ligand interactions in PDB files.
+chimeraplip.py - Visualization class for Chimera.
+Copyright 2014-2015 Sebastian Salentin
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
+class ChimeraVisualizer():
+    """Provides visualization for Chimera."""
+    def __init__(self, plcomplex, chimera_module, tid):
+        self.chimera = chimera_module
+        self.tid = tid
+        self.uid = plcomplex.uid
+        self.plipname = 'PLIP-%i' % self.tid
+        self.hetid, self.chain, self.pos = self.uid.split(':')
+        self.pos = int(self.pos)
+        Molecule = self.chimera.Molecule
+        self.colorbyname = self.chimera.colorTable.getColorByName
+        self.rc = self.chimera.runCommand
+        self.getPseudoBondGroup = self.chimera.misc.getPseudoBondGroup
+
+        if not plcomplex is None:
+            self.plcomplex = plcomplex
+            self.protname = plcomplex.pdbid  # Name of protein with binding site
+            self.ligname = plcomplex.hetid  # Name of ligand
+            self.metal_ids = plcomplex.metal_ids
+            self.water_ids = []
+            self.bs_res_ids = []
+            self.models = self.chimera.openModels
+
+            for md in self.models.list():
+                if md.name == self.plipname:
+                    self.model = md
+
+            self.atoms = self.atom_by_serialnumber()
+
+
+    def set_initial_representations(self):
+        """Set the initial representations"""
+        self.update_model_dict()
+        self.rc("background solid white")
+        self.rc("setattr g display 0")  # Hide all pseudobonds
+        self.rc("~display #%i & :/isHet & ~:%s" % (self.model_dict[self.plipname],self.hetid))
+
+    def update_model_dict(self):
+        """Updates the model dictionary"""
+        dct = {}
+        models = self.chimera.openModels
+        for md in models.list():
+            dct[md.name] = md.id
+        self.model_dict = dct
+
+    def atom_by_serialnumber(self):
+        """Provides a dictionary mapping serial numbers to their atom objects."""
+        atm_by_snum = {}
+        for atom in self.model.atoms:
+            atm_by_snum[atom.serialNumber] =  atom
+        return atm_by_snum
+
+    def show_hydrophobic(self):
+        """Visualizes hydrophobic contacts."""
+        grp = self.getPseudoBondGroup("Hydrophobic Interactions-%i" % self.tid, associateWith=[self.model])
+        grp.lineType = self.chimera.Dash
+        grp.lineWidth = 3
+        grp.color = self.colorbyname('gray')
+        for i in self.plcomplex.hydrophobic_contacts.pairs_ids:
+            b = grp.newPseudoBond(self.atoms[i[0]], self.atoms[i[1]])
+            self.bs_res_ids.append(i[0])
+
+    def show_hbonds(self):
+        """Visualizes hydrogen bonds."""
+        grp = self.getPseudoBondGroup("Hydrogen Bonds-%i" % self.tid, associateWith=[self.model])
+        grp.lineWidth = 3
+        for i in self.plcomplex.hbonds.ldon_id:
+            b = grp.newPseudoBond(self.atoms[i[0]], self.atoms[i[1]])
+            b.color = self.colorbyname('blue')
+            self.bs_res_ids.append(i[0])
+        for i in self.plcomplex.hbonds.pdon_id:
+            b = grp.newPseudoBond(self.atoms[i[0]], self.atoms[i[1]])
+            b.color = self.colorbyname('blue')
+            self.bs_res_ids.append(i[1])
+
+
+    def show_halogen(self):
+        """Visualizes halogen bonds."""
+        grp = self.getPseudoBondGroup("HalogenBonds-%i" % self.tid, associateWith=[self.model])
+        grp.lineWidth = 3
+        for i in self.plcomplex.halogen_bonds:
+            b = grp.newPseudoBond(self.atoms[i[0]], self.atoms[i[1]])
+            b.color = self.colorbyname('turquoise')
+
+            self.bs_res_ids.append(i.acc_id)
+
+    def show_stacking(self):
+        """Visualizes pi-stacking interactions."""
+        grp = self.getPseudoBondGroup("pi-Stacking-%i" % self.tid, associateWith=[self.model])
+        grp.lineWidth = 3
+        grp.lineType = self.chimera.Dash
+        for i, stack in enumerate(self.plcomplex.pistacking):
+
+            m = self.model
+            r = m.newResidue("pseudoatoms", " ", 1, " ")
+            centroid_prot = m.newAtom("CENTROID", self.chimera.Element("CENTROID"))
+            x, y, z = stack.proteinring_center
+            centroid_prot.setCoord(self.chimera.Coord(x, y, z))
+            r.addAtom(centroid_prot)
+
+            centroid_lig = m.newAtom("CENTROID", self.chimera.Element("CENTROID"))
+            x, y, z = stack.ligandring_center
+            centroid_lig.setCoord(self.chimera.Coord(x, y, z))
+            r.addAtom(centroid_lig)
+
+
+            b = grp.newPseudoBond(centroid_lig, centroid_prot)
+            b.color = self.colorbyname('forest green')
+
+            self.bs_res_ids += stack.proteinring_atoms
+
+    def show_cationpi(self):
+        """Visualizes cation-pi interactions"""
+        grp = self.getPseudoBondGroup("Cation-Pi-%i" % self.tid, associateWith=[self.model])
+        grp.lineWidth = 3
+        grp.lineType = self.chimera.Dash
+        for i, cat in enumerate(self.plcomplex.pication):
+
+            m = self.model
+            r = m.newResidue("pseudoatoms", " ", 1, " ")
+            chargecenter = m.newAtom("CHARGE", self.chimera.Element("CHARGE"))
+            x, y, z = cat.charge_center
+            chargecenter.setCoord(self.chimera.Coord(x, y, z))
+            r.addAtom(chargecenter)
+
+            centroid = m.newAtom("CENTROID", self.chimera.Element("CENTROID"))
+            x, y, z = cat.ring_center
+            centroid.setCoord(self.chimera.Coord(x, y, z))
+            r.addAtom(centroid)
+
+            b = grp.newPseudoBond(centroid, chargecenter)
+            b.color = self.colorbyname('orange')
+
+            if cat.protcharged:
+                self.bs_res_ids += cat.charge_atoms
+            else:
+                self.bs_res_ids += cat.ring_atoms
+
+    def show_sbridges(self):
+        """Visualizes salt bridges."""
+        # Salt Bridges
+        grp = self.getPseudoBondGroup("Salt Bridges-%i" % self.tid, associateWith=[self.model])
+        grp.lineWidth = 3
+        grp.lineType = self.chimera.Dash
+        for i, sbridge in enumerate(self.plcomplex.saltbridges):
+
+            m = self.model
+            r = m.newResidue("pseudoatoms", " ", 1, " ")
+            chargecenter1 = m.newAtom("CHARGE", self.chimera.Element("CHARGE"))
+            x, y, z = sbridge.positive_center
+            chargecenter1.setCoord(self.chimera.Coord(x, y, z))
+            r.addAtom(chargecenter1)
+
+            chargecenter2 = m.newAtom("CHARGE", self.chimera.Element("CHARGE"))
+            x, y, z = sbridge.negative_center
+            chargecenter2.setCoord(self.chimera.Coord(x, y, z))
+            r.addAtom(chargecenter2)
+
+            b = grp.newPseudoBond(chargecenter1, chargecenter2)
+            b.color = self.colorbyname('yellow')
+
+            if sbridge.protispos:
+                self.bs_res_ids += sbridge.positive_atoms
+            else:
+                self.bs_res_ids += sbridge.negative_atoms
+
+    def show_wbridges(self):
+        """Visualizes water bridges"""
+        grp = self.getPseudoBondGroup("Water Bridges-%i" % self.tid, associateWith=[self.model])
+        grp.lineWidth = 3
+        for i, wbridge in enumerate(self.plcomplex.waterbridges):
+            c = grp.newPseudoBond(self.atoms[wbridge.water_id], self.atoms[wbridge.acc_id])
+            c.color = self.colorbyname('cornflower blue')
+            self.water_ids.append(wbridge.water_id)
+            b = grp.newPseudoBond(self.atoms[wbridge.don_id], self.atoms[wbridge.water_id])
+            b.color = self.colorbyname('cornflower blue')
+            self.water_ids.append(wbridge.water_id)
+            if wbridge.protisdon:
+                self.bs_res_ids.append(wbridge.don_id)
+            else:
+                self.bs_res_ids.append(wbridge.acc_id)
+
+    def show_metal(self):
+        """Visualizes metal coordination."""
+        grp = self.getPseudoBondGroup("Metal Coordination-%i" % self.tid, associateWith=[self.model])
+        grp.lineWidth = 3
+        for i, metal in enumerate(self.plcomplex.metal_complexes):
+            c = grp.newPseudoBond(self.atoms[metal.metal_id], self.atoms[metal.target_id])
+            c.color = self.colorbyname('magenta')
+
+            if metal.location == 'water':
+                self.water_ids.append(metal.target_id)
+
+            if metal.location.startswith('protein'):
+                self.bs_res_ids.append(metal.target_id)
+
+    def cleanup(self):
+        """Clean up the visualization."""
+
+        if not len(self.water_ids) == 0:
+            # Hide all non-interacting water molecules
+            water_selection = []
+            for wid in self.water_ids:
+                water_selection.append('serialNumber=%i' % wid)
+            self.rc("~display :HOH")
+            self.rc("display :@/%s" % " or ".join(water_selection))
+
+        # Show all interacting binding site residues
+        self.rc("~display #%i & ~:/isHet" % self.model_dict[self.plipname])
+        self.rc("display :%s" % ",".join([str(self.atoms[bsid].residue.id) for bsid in self.bs_res_ids]))
+        self.rc("color lightblue :HOH")
+
+
+
+    def zoom_to_ligand(self):
+        """Centers the view on the ligand and its binding site residues."""
+        self.rc("center #%i & :%s" % (self.model_dict[self.plipname], self.hetid))
+
+    def refinements(self):
+        """Details for the visualization."""
+        self.rc("setattr a color gray @CENTROID")
+        self.rc("setattr a radius 0.3 @CENTROID")
+        self.rc("represent sphere @CENTROID")
+        self.rc("setattr a color orange @CHARGE")
+        self.rc("setattr a radius 0.4 @CHARGE")
+        self.rc("represent sphere @CHARGE")
+        self.rc("display :pseudoatoms")
diff --git a/plip/modules/config.py b/plip/modules/config.py
index 66a23ec..7f78ad4 100644
--- a/plip/modules/config.py
+++ b/plip/modules/config.py
@@ -23,10 +23,13 @@ XML = False
 TXT = False
 PICS = False
 PYMOL = False
+VISJSON = False  # JSON File of PyMOL helper class (for web service)
 OUTPATH = './'
 BASEPATH = './'
 BREAKCOMPOSITE = False  # Break up composite ligands with covalent bonds
 ALTLOC = False  # Consider alternate locations
+PLUGIN_MODE = False  # Special mode for PLIP in Plugins (e.g. PyMOL)
+NOFIX = False  # Turn off fixing of errors in PDB files
 
 # Configuration file for Protein-Ligand Interaction Profiler (PLIP)
 # Set thresholds for detection of interactions
diff --git a/plip/modules/detection.py b/plip/modules/detection.py
index 21cf11f..5aa64f9 100644
--- a/plip/modules/detection.py
+++ b/plip/modules/detection.py
@@ -378,7 +378,7 @@ def metal_complexation(metals, metal_binding_lig, metal_binding_bs):
         # Record all contact pairing, excluding those with targets superfluous for chosen geometry
         only_water = set([x[0].location for x in contact_pairs]) == {'water'}
         if not only_water:  # No complex if just with water as targets
-            message("Metal ion %s complexed with %s geometry (coo. number %r/ %i observed).\n"
+            write_message("Metal ion %s complexed with %s geometry (coo. number %r/ %i observed).\n"
                     % (metal.type, final_geom, final_coo, num_targets), indent=True)
             for contact_pair in contact_pairs:
                 target, distance = contact_pair
diff --git a/plip/modules/plipremote.py b/plip/modules/plipremote.py
new file mode 100644
index 0000000..f7e5b28
--- /dev/null
+++ b/plip/modules/plipremote.py
@@ -0,0 +1,113 @@
+"""
+Protein-Ligand Interaction Profiler - Analyze and visualize protein-ligand interactions in PDB files.
+plipremote.py - Modules involved in multiprocessing and remote computation.
+Copyright 2014-2015 Sebastian Salentin
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+
+# Python Standard Library
+from collections import namedtuple
+
+hbonds_info = namedtuple('hbonds_info', 'ldon_id lig_don_id prot_acc_id pdon_id prot_don_id lig_acc_id')
+hydrophobic_info = namedtuple('hydrophobic_info', 'bs_ids lig_ids pairs_ids')
+halogen_info = namedtuple('halogen_info', 'don_id acc_id')
+pistack_info = namedtuple('pistack_info', 'proteinring_atoms, proteinring_center ligandring_atoms '
+                                          'ligandring_center type')
+pication_info = namedtuple('pication_info', 'ring_center charge_center ring_atoms charge_atoms, protcharged')
+sbridge_info = namedtuple('sbridge_info', 'positive_atoms negative_atoms positive_center negative_center protispos')
+wbridge_info = namedtuple('wbridge_info', 'don_id acc_id water_id protisdon')
+metal_info = namedtuple('metal_info', 'metal_id, target_id location')
+
+class VisualizerData:
+    """Contains all information on a complex relevant for visualization. Can be pickled"""
+    def __init__(self, mol, site):
+        pcomp = mol
+        pli = mol.interaction_sets[site]
+        ligand = pli.ligand
+
+
+
+        # General Information
+        self.lig_members = sorted(pli.ligand.members)
+        self.sourcefile = pcomp.sourcefiles['pdbcomplex']
+        self.corrected_pdb = pcomp.corrected_pdb
+        self.pdbid = mol.pymol_name
+        self.hetid = ligand.hetid
+        self.chain = ligand.chain if not ligand.chain == "0" else ""  # #@todo Fix this
+        self.position = str(ligand.position)
+        self.uid = ":".join([self.hetid, self.chain, self.position])
+        self.outpath = mol.output_path
+        self.metal_ids = [x.m_orig_idx for x in pli.ligand.metals]
+        self.unpaired_hba_idx = pli.unpaired_hba_orig_idx
+        self.unpaired_hbd_idx = pli.unpaired_hbd_orig_idx
+        self.unpaired_hal_idx = pli.unpaired_hal_orig_idx
+
+        # Information on Interactions
+
+        # Hydrophobic Contacts
+        # Contains IDs of contributing binding site, ligand atoms and the pairings
+        hydroph_pairs_id = [(h.bsatom_orig_idx, h.ligatom_orig_idx) for h in pli.hydrophobic_contacts]
+        self.hydrophobic_contacts = hydrophobic_info(bs_ids=[hp[0] for hp in hydroph_pairs_id],
+                                                     lig_ids=[hp[1] for hp in hydroph_pairs_id],
+                                                     pairs_ids=hydroph_pairs_id)
+
+        # Hydrogen Bonds
+        # #@todo Don't use indices, simplify this code here
+        hbonds_ldon, hbonds_pdon = pli.hbonds_ldon, pli.hbonds_pdon
+        hbonds_ldon_id = [(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_ldon]
+        hbonds_pdon_id = [(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_pdon]
+        self.hbonds = hbonds_info(ldon_id=[(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_ldon],
+                                  lig_don_id=[hb[1] for hb in hbonds_ldon_id],
+                                  prot_acc_id=[hb[0] for hb in hbonds_ldon_id],
+                                  pdon_id=[(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_pdon],
+                                  prot_don_id=[hb[1] for hb in hbonds_pdon_id],
+                                  lig_acc_id=[hb[0] for hb in hbonds_pdon_id])
+
+        # Halogen Bonds
+        self.halogen_bonds = [halogen_info(don_id=h.don_orig_idx, acc_id=h.acc_orig_idx)
+                              for h in pli.halogen_bonds]
+
+        # Pistacking
+        self.pistacking = [pistack_info(proteinring_atoms=pistack.proteinring.atoms_orig_idx,
+                                        proteinring_center=pistack.proteinring.center,
+                                        ligandring_atoms=pistack.ligandring.atoms_orig_idx,
+                                        ligandring_center=pistack.ligandring.center,
+                                        type=pistack.type) for pistack in pli.pistacking]
+
+        # Pi-cation interactions
+        self.pication = [pication_info(ring_center=picat.ring.center,
+                                       charge_center=picat.charge.center,
+                                       ring_atoms=picat.ring.atoms_orig_idx,
+                                       charge_atoms=picat.charge.atoms_orig_idx,
+                                       protcharged=picat.protcharged)
+                         for picat in pli.pication_paro+pli.pication_laro]
+
+        # Salt Bridges
+        self.saltbridges = [sbridge_info(positive_atoms=sbridge.positive.atoms_orig_idx,
+                                         negative_atoms=sbridge.negative.atoms_orig_idx,
+                                         positive_center=sbridge.positive.center,
+                                         negative_center=sbridge.negative.center,
+                                         protispos=sbridge.protispos)
+                            for sbridge in pli.saltbridge_lneg+pli.saltbridge_pneg]
+
+        # Water Bridgese('wbridge_info', 'don_id acc_id water_id protisdon')
+        self.waterbridges = [wbridge_info(don_id=wbridge.d_orig_idx,
+                                          acc_id=wbridge.a_orig_idx,
+                                          water_id=wbridge.water_orig_idx,
+                                          protisdon=wbridge.protisdon) for wbridge in pli.water_bridges]
+
+        # Metal Complexes
+        self.metal_complexes = [metal_info(metal_id=metalc.metal_orig_idx,
+                                           target_id=metalc.target_orig_idx,
+                                           location=metalc.location) for metalc in pli.metal_complexes]
diff --git a/plip/modules/preparation.py b/plip/modules/preparation.py
index 3ad4f3a..d083c9b 100644
--- a/plip/modules/preparation.py
+++ b/plip/modules/preparation.py
@@ -31,7 +31,8 @@ import config
 ################
 
 class PDBParser:
-    def __init__(self, pdbpath):
+    def __init__(self, pdbpath, as_string):
+        self.as_string = as_string
         self.pdbpath = pdbpath
         self.num_fixed_lines = 0
         self.covlinkage = namedtuple("covlinkage", "id1 chain1 pos1 conf1 id2 chain2 pos2 conf2")
@@ -46,7 +47,10 @@ class PDBParser:
         III. Furthermore, covalent linkages between ligands and protein residues/other ligands are identified
         IV. Alternative conformations
         """
-        fil = read(self.pdbpath).readlines()
+        if self.as_string:
+            fil = self.pdbpath.split('\n')
+        else:
+            fil = read(self.pdbpath).readlines()
         # #@todo Also consider SSBOND entries here
         corrected_lines = []
         i, j = 0, 0  # idx and PDB numbering
@@ -56,15 +60,22 @@ class PDBParser:
         alt = []
         previous_ter = False
 
-        #New code : Do fixing first and then do mapping on fixed lines
-        #@TODO Test code
-        lastnum = 0 # Atom numbering (has to be consecutive)
-        for line in fil:
-            corrected_line, newnum = self.fix_pdbline(line, lastnum)
-            if corrected_line is not None:
-                corrected_lines.append(corrected_line)
-                lastnum = newnum
-        corrected_pdb = ''.join(corrected_lines)
+        # Standard without fixing
+        if not config.NOFIX:
+            if not config.PLUGIN_MODE:
+                lastnum = 0 # Atom numbering (has to be consecutive)
+                for line in fil:
+                    corrected_line, newnum = self.fix_pdbline(line, lastnum)
+                    if corrected_line is not None:
+                        corrected_lines.append(corrected_line)
+                        lastnum = newnum
+                corrected_pdb = ''.join(corrected_lines)
+            else:
+                corrected_pdb = self.pdbpath
+                corrected_lines = fil
+        else:
+            corrected_pdb = self.pdbpath
+            corrected_lines = fil
 
 
         for line in corrected_lines:
@@ -200,7 +211,7 @@ class LigandFinder:
         members = [(res.GetName(), res.GetChain(), int32_to_negative(res.GetNum())) for res in kmer]
         members = sorted(members, key=lambda x: (x[1], x[2]))
         rname, rchain, rnum = members[0]
-        debuglog("Finalizing extraction for ligand %s:%s:%s" % (rname, rchain, rnum))
+        write_message("Finalizing extraction for ligand %s:%s:%s\n" % (rname, rchain, rnum), mtype='debug')
         names = [x[0] for x in members]
         longname = '-'.join([x[0] for x in members])
 
@@ -263,17 +274,31 @@ class LigandFinder:
                       can_to_pdb=can_to_pdb)
         return ligand
 
+    def is_het_residue(self, obres):
+        """Given an OBResidue, determines if the residue is indeed a ligand
+        in the PDB file (any atoms has to be a HETATM entry)"""
+        het_atoms = []
+        for atm in pybel.ob.OBResidueAtomIter(obres):
+                het_atoms.append(obres.IsHetAtom(atm))
+        if True in het_atoms:
+            return True
+        else:
+            return False
+
+
     def filter_for_ligands(self):
         """Given an OpenBabel Molecule, get all ligands, their names, and water"""
 
-        candidates1 = [o for o in pybel.ob.OBResidueIter(self.proteincomplex.OBMol) if not (o.GetResidueProperty(9)
-                                                                                         or o.GetResidueProperty(0))]
+        #candidates1 = [o for o in pybel.ob.OBResidueIter(self.proteincomplex.OBMol) if not (o.GetResidueProperty(9)
+        #                                                                                or o.GetResidueProperty(0))]
+
+        candidates1 = [o for o in pybel.ob.OBResidueIter(self.proteincomplex.OBMol) if not o.GetResidueProperty(9) and self.is_het_residue(o)]
         all_lignames = set([a.GetName() for a in candidates1])
 
         water = [o for o in pybel.ob.OBResidueIter(self.proteincomplex.OBMol) if o.GetResidueProperty(9)]
         # Filter out non-ligands
         candidates2 = [a for a in candidates1 if is_lig(a.GetName()) and a.GetName() not in self.modresidues]
-        debuglog("%i ligand(s) after first filtering step." % len(candidates2))
+        write_message("%i ligand(s) after first filtering step.\n" % len(candidates2), mtype='debug')
 
         ############################################
         # Filtering by counting and artifacts list #
@@ -397,7 +422,7 @@ class Mol:
         rings = []
         aromatic_amino = ['TYR', 'TRP', 'HIS', 'PHE']
         ring_candidates = mol.OBMol.GetSSSR()
-        debuglog("Number of aromatic ring candidates: %i" % len(ring_candidates))
+        write_message("Number of aromatic ring candidates: %i\n" % len(ring_candidates), mtype="debug")
         # Check here first for ligand rings not being detected as aromatic by Babel and check for planarity
         for ring in ring_candidates:
             r_atoms = [a for a in all_atoms if ring.IsMember(a.OBAtom)]
@@ -510,7 +535,7 @@ class PLInteraction:
         self.interacting_res = list(set([''.join([str(i.resnr), str(i.reschain)]) for i in self.all_itypes
                                          if i.restype not in ['LIG', 'HOH']]))
         if len(self.interacting_res) != 0:
-            message('Ligand interacts with %i binding site residue(s) in chain(s) %s.\n'
+            write_message('Ligand interacts with %i binding site residue(s) in chain(s) %s.\n'
                     % (len(self.interacting_res), '/'.join(self.interacting_chains)), indent=True)
             interactions_list = []
             num_saltbridges = len(self.saltbridge_lneg + self.saltbridge_pneg)
@@ -532,9 +557,9 @@ class PLInteraction:
             if num_waterbridges != 0:
                 interactions_list.append('%i water bridge(s)' % num_waterbridges)
             if not len(interactions_list) == 0:
-                message('Complex uses %s.\n' % ', '.join(interactions_list), indent=True)
+                write_message('Complex uses %s.\n' % ', '.join(interactions_list), indent=True)
         else:
-            message('No interactions for this ligand.\n', indent=True)
+            write_message('No interactions for this ligand.\n', indent=True)
 
     def find_unpaired_ligand(self):
         """Identify unpaired functional in groups in ligands, involving H-Bond donors, acceptors, halogen bond donors.
@@ -627,7 +652,7 @@ class PLInteraction:
                 hydroph_final.append(min_h)
         before, reduced = len(all_h), len(hydroph_final)
         if not before == 0 and not before == reduced:
-            message('Reduced number of hydrophobic contacts from %i to %i.\n' % (before, reduced), indent=True)
+            write_message('Reduced number of hydrophobic contacts from %i to %i.\n' % (before, reduced), indent=True)
         return hydroph_final
 
     def refine_hbonds_ldon(self, all_hbonds, salt_lneg, salt_pneg):
@@ -853,7 +878,7 @@ class Ligand(Mol):
         if not len(self.smiles) == 0:
             self.smiles = self.smiles.split()[0]
         else:
-            message('[Warning] Could not write SMILES for this ligand.\n', indent=True)
+            write_message('Could not write SMILES for this ligand.\n', indent=True, mtype='warning')
             self.smiles = ''
         self.heavy_atoms = self.molecule.OBMol.NumHvyAtoms()  # Heavy atoms count
         self.all_atoms = self.molecule.atoms
@@ -863,7 +888,7 @@ class Ligand(Mol):
         self.hbond_acc_atoms = self.find_hba(self.all_atoms)
         self.num_rings = len(self.rings)
         if self.num_rings != 0:
-            message('Contains %i aromatic ring(s).\n' % self.num_rings, indent=True)
+            write_message('Contains %i aromatic ring(s).\n' % self.num_rings, indent=True)
         descvalues = self.molecule.calcdesc()
         self.molweight, self.logp = float(descvalues['MW']), float(descvalues['logP'])
         self.num_rot_bonds = int(self.molecule.OBMol.NumRotors())
@@ -971,7 +996,7 @@ class Ligand(Mol):
                 c_orig_idx = [self.Mapper.mapid(na.GetIdx(), mtype=self.mtype, bsid=self.bsid) for na in n_atoms]
                 a_set.append(data(x=a, x_orig_idx=x_orig_idx, c=pybel.Atom(n_atoms[0]), c_orig_idx=c_orig_idx))
         if len(a_set) != 0:
-            message('Ligand contains %i halogen atom(s).\n' % len(a_set), indent=True)
+            write_message('Ligand contains %i halogen atom(s).\n' % len(a_set), indent=True)
         return a_set
 
     def find_charged(self, all_atoms):
@@ -1127,12 +1152,18 @@ class PDBComplex:
         self.Mapper = Mapper()
         self.ligands = []
 
-    def load_pdb(self, pdbpath):
-        """Loads a pdb file with protein AND ligand(s), separates and prepares them."""
-        self.sourcefiles['pdbcomplex.original'] = pdbpath
-        self.sourcefiles['pdbcomplex'] = pdbpath
+    def load_pdb(self, pdbpath, as_string=False):
+        """Loads a pdb file with protein AND ligand(s), separates and prepares them.
+        If specified 'as_string', the input is a PDB string instead of a path."""
+        if as_string:
+            self.sourcefiles['pdbcomplex.original'] = None
+            self.sourcefiles['pdbcomplex'] = None
+            self.sourcefiles['pdbstring'] = pdbpath
+        else:
+            self.sourcefiles['pdbcomplex.original'] = pdbpath
+            self.sourcefiles['pdbcomplex'] = pdbpath
         self.information['pdbfixes'] = False
-        pdbparser = PDBParser(pdbpath)  # Parse PDB file to find errors and get additonal data
+        pdbparser = PDBParser(pdbpath, as_string=as_string)  # Parse PDB file to find errors and get additonal data
         # #@todo Refactor and rename here
         self.Mapper.proteinmap = pdbparser.proteinmap
         self.modres = pdbparser.modres
@@ -1140,21 +1171,25 @@ class PDBComplex:
         self.altconf = pdbparser.altconformations
         self.corrected_pdb = pdbparser.corrected_pdb
 
-        if pdbparser.num_fixed_lines > 0:
-            message('%i lines automatically fixed in PDB input file.\n' % pdbparser.num_fixed_lines)
-            # Save modified PDB file
-            basename = os.path.basename(pdbpath).split('.')[0]
-            pdbpath_fixed = tmpfile(prefix='plipfixed.' + basename + '_', direc=self.output_path)
-            create_folder_if_not_exists(self.output_path)
-            self.sourcefiles['pdbcomplex'] = pdbpath_fixed
-            self.corrected_pdb = re.sub(r'[^\x00-\x7F]+', ' ', self.corrected_pdb)  # Strip non-unicode chars
-            with open(pdbpath_fixed, 'w') as f:
-                f.write(self.corrected_pdb)
-            self.information['pdbfixes'] = True
-
-        self.sourcefiles['filename'] = os.path.basename(self.sourcefiles['pdbcomplex'])
-        self.protcomplex, self.filetype = read_pdb(self.sourcefiles['pdbcomplex'])
-        message('PDB structure successfully read.\n')
+        if not config.PLUGIN_MODE:
+            if pdbparser.num_fixed_lines > 0:
+                write_message('%i lines automatically fixed in PDB input file.\n' % pdbparser.num_fixed_lines)
+                # Save modified PDB file
+                basename = os.path.basename(pdbpath).split('.')[0]
+                pdbpath_fixed = tmpfile(prefix='plipfixed.' + basename + '_', direc=self.output_path)
+                create_folder_if_not_exists(self.output_path)
+                self.sourcefiles['pdbcomplex'] = pdbpath_fixed
+                self.corrected_pdb = re.sub(r'[^\x00-\x7F]+', ' ', self.corrected_pdb)  # Strip non-unicode chars
+                with open(pdbpath_fixed, 'w') as f:
+                    f.write(self.corrected_pdb)
+                self.information['pdbfixes'] = True
+
+        if as_string:
+            self.protcomplex, self.filetype = read_pdb(self.sourcefiles['pdbstring'], as_string=as_string)
+        else:
+            self.sourcefiles['filename'] = os.path.basename(self.sourcefiles['pdbcomplex'])
+            self.protcomplex, self.filetype = read_pdb(self.sourcefiles['pdbcomplex'], as_string=as_string)
+        write_message('PDB structure successfully read.\n')
 
         # Determine (temporary) PyMOL Name from Filename
         self.pymol_name = pdbpath.split('/')[-1].split('.')[0] + '-Protein'
@@ -1165,7 +1200,7 @@ class PDBComplex:
             potential_name = self.protcomplex.data['HEADER'][56:60].lower()
             if extract_pdbid(potential_name) != 'UnknownProtein':
                 self.pymol_name = potential_name
-        debuglog("Pymol Name set as: '%s'" % self.pymol_name)
+        write_message("Pymol Name set as: '%s'\n" % self.pymol_name, mtype='debug')
 
         self.protcomplex.OBMol.AddPolarHydrogens()
         for atm in self.protcomplex:
@@ -1177,17 +1212,17 @@ class PDBComplex:
         self.excluded = ligandfinder.excluded
 
         if len(self.excluded) != 0:
-            message("Excluded molecules as ligands: %s\n" % ','.join([lig for lig in self.excluded]))
+            write_message("Excluded molecules as ligands: %s\n" % ','.join([lig for lig in self.excluded]))
 
         self.resis = [obres for obres in pybel.ob.OBResidueIter(self.protcomplex.OBMol) if obres.GetResidueProperty(0)]
 
         num_ligs = len(self.ligands)
         if num_ligs == 1:
-            message("Analyzing one ligand...\n")
+            write_message("Analyzing one ligand...\n")
         elif num_ligs > 1:
-            message("Analyzing %i ligands...\n" % num_ligs)
+            write_message("Analyzing %i ligands...\n" % num_ligs)
         else:
-            message("Structure contains no ligands.\n\n")
+            write_message("Structure contains no ligands.\n\n")
 
     def characterize_complex(self, ligand):
         """Handles all basic functions for characterizing the interactions for one ligand"""
@@ -1201,11 +1236,11 @@ class PDBComplex:
         ligtype = 'Unspecified type' if ligand.type == 'UNSPECIFIED' else ligand.type
         ligtext = "\n%s [%s] -- %s" % (longname, ligtype, site)
         any_in_biolip = len(set([x[0] for x in ligand.members]).intersection(config.biolip_list)) != 0
-        message(ligtext)
-        message('\n' + '-' * len(ligtext) + '\n')
+        write_message(ligtext)
+        write_message('\n' + '-' * len(ligtext) + '\n')
 
         if ligtype not in ['POLYMER', 'DNA', 'ION', 'DNA+ION', 'RNA+ION', 'SMALLMOLECULE+ION'] and any_in_biolip:
-            message('  -> may be biologically irrelevant <-\n')
+            write_message('may be biologically irrelevant\n', mtype='info', indent=True)
 
         lig_obj = Ligand(self, ligand)
         cutoff = lig_obj.max_dist_to_center + config.BS_DIST
@@ -1230,7 +1265,7 @@ class PDBComplex:
                 if distance <= config.BS_DIST and r not in bs_atoms_refined:
                     bs_atoms_refined.append(r)
         num_bs_atoms = len(bs_atoms_refined)
-        message('Binding site atoms in vicinity (%.1f A max. dist: %i).\n' % (config.BS_DIST, num_bs_atoms),
+        write_message('Binding site atoms in vicinity (%.1f A max. dist: %i).\n' % (config.BS_DIST, num_bs_atoms),
                 indent=True)
 
         bs_obj = BindingSite(bs_atoms_refined, self.protcomplex, self, self.altconf, min_dist, self.Mapper)
diff --git a/plip/modules/pymolplip.py b/plip/modules/pymolplip.py
new file mode 100644
index 0000000..4012dcc
--- /dev/null
+++ b/plip/modules/pymolplip.py
@@ -0,0 +1,437 @@
+"""
+Protein-Ligand Interaction Profiler - Analyze and visualize protein-ligand interactions in PDB files.
+pymolplip.py - Visualization class for PyMOL.
+Copyright 2014-2015 Sebastian Salentin
+
+Licensed under the Apache License, Version 2.0 (the "License");
+you may not use this file except in compliance with the License.
+You may obtain a copy of the License at
+
+    http://www.apache.org/licenses/LICENSE-2.0
+
+Unless required by applicable law or agreed to in writing, software
+distributed under the License is distributed on an "AS IS" BASIS,
+WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+See the License for the specific language governing permissions and
+limitations under the License.
+"""
+from pymol import cmd
+import sys
+import os
+import subprocess
+
+class PyMOLVisualizer:
+
+    def __init__(self, plcomplex):
+        if not plcomplex is None:
+            self.plcomplex = plcomplex
+            self.protname = plcomplex.pdbid  # Name of protein with binding site
+            self.ligname = plcomplex.hetid  # Name of ligand
+            self.metal_ids = plcomplex.metal_ids
+
+    def set_initial_representations(self):
+        """General settings for PyMOL"""
+        self.standard_settings()
+        cmd.set('dash_gap', 0)  # Show not dashes, but lines for the pliprofiler
+        cmd.set('ray_shadow', 0)  # Turn on ray shadows for clearer ray-traced images
+        cmd.set('cartoon_color', 'mylightblue')
+
+        # Set clipping planes for full view
+        cmd.clip('far', -1000)
+        cmd.clip('near', 1000)
+
+    def make_initial_selections(self):
+        """Make empty selections for structures and interactions"""
+        for group in ['Hydrophobic-P', 'Hydrophobic-L', 'HBondDonor-P',
+        'HBondDonor-L', 'HBondAccept-P', 'HBondAccept-L',
+        'HalogenAccept', 'HalogenDonor', 'Water', 'MetalIons', 'StackRings-P',
+        'PosCharge-P', 'PosCharge-L', 'NegCharge-P', 'NegCharge-L',
+        'PiCatRing-P', 'StackRings-L', 'PiCatRing-L', 'Metal-M', 'Metal-P',
+        'Metal-W', 'Metal-L', 'Unpaired-HBA', 'Unpaired-HBD', 'Unpaired-HAL',
+        'Unpaired-RINGS']:
+            cmd.select(group, 'None')
+
+    def standard_settings(self):
+        """Sets up standard settings for a nice visualization."""
+        cmd.set('bg_rgb', [1.0, 1.0, 1.0])  # White background
+        cmd.set('depth_cue', 0)  # Turn off depth cueing (no fog)
+        cmd.set('cartoon_side_chain_helper', 1)  # Improve combined visualization of sticks and cartoon
+        cmd.set('cartoon_fancy_helices', 1)  # Nicer visualization of helices (using tapered ends)
+        cmd.set('transparency_mode', 1)  # Turn on multilayer transparency
+        cmd.set('dash_radius', 0.05)
+        self.set_custom_colorset()
+
+    def set_custom_colorset(self):
+        """Defines a colorset with matching colors. Provided by Joachim."""
+        cmd.set_color('myorange', '[253, 174, 97]')
+        cmd.set_color('mygreen', '[171, 221, 164]')
+        cmd.set_color('myred', '[215, 25, 28]')
+        cmd.set_color('myblue', '[43, 131, 186]')
+        cmd.set_color('mylightblue', '[158, 202, 225]')
+        cmd.set_color('mylightgreen', '[229, 245, 224]')
+
+    def select_by_ids(self, selname, idlist, selection_exists=False, chunksize=20, restrict=None):
+        """Selection with a large number of ids concatenated into a selection
+        list can cause buffer overflow in PyMOL. This function takes a selection
+        name and and list of IDs (list of integers) as input and makes a careful
+        step-by-step selection (packages of 20 by default)"""
+        idlist = list(set(idlist))  # Remove duplicates
+        if not selection_exists:
+            cmd.select(selname, 'None')  # Empty selection first
+        idchunks = [idlist[i:i+chunksize] for i in xrange(0, len(idlist), chunksize)]
+        for idchunk in idchunks:
+            cmd.select(selname, '%s or (id %s)' % (selname, '+'.join(map(str, idchunk))))
+        if restrict is not None:
+            cmd.select(selname, '%s and %s' % (selname, restrict))
+
+
+    def object_exists(self, object_name):
+        """Checks if an object exists in the open PyMOL session."""
+        return object_name in cmd.get_names("objects")
+
+    def show_hydrophobic(self):
+        """Visualizes hydrophobic contacts."""
+        hydroph = self.plcomplex.hydrophobic_contacts
+        if not len(hydroph.bs_ids) == 0:
+            self.select_by_ids('Hydrophobic-P', hydroph.bs_ids, restrict=self.protname)
+            self.select_by_ids('Hydrophobic-L', hydroph.lig_ids, restrict=self.ligname)
+            for i in hydroph.pairs_ids:
+                cmd.select('tmp_bs', 'id %i & %s' % (i[0], self.protname))
+                cmd.select('tmp_lig', 'id %i & %s' % (i[1], self.ligname))
+                cmd.distance('Hydrophobic', 'tmp_bs', 'tmp_lig')
+            if self.object_exists('Hydrophobic'):
+                cmd.set('dash_gap', 0.5, 'Hydrophobic')
+                cmd.set('dash_color', 'grey50', 'Hydrophobic')
+        else:
+            cmd.select('Hydrophobic-P', 'None')
+
+    def show_hbonds(self):
+        """Visualizes hydrogen bonds."""
+        hbonds = self.plcomplex.hbonds
+        for group in [['HBondDonor-P', hbonds.prot_don_id],
+        ['HBondAccept-P', hbonds.prot_acc_id]]:
+            if not len(group[1]) == 0:
+                self.select_by_ids(group[0], group[1], restrict=self.protname)
+        for group in [['HBondDonor-L', hbonds.lig_don_id],
+        ['HBondAccept-L', hbonds.lig_acc_id]]:
+            if not len(group[1]) == 0:
+                self.select_by_ids(group[0], group[1], restrict=self.ligname)
+        for i in hbonds.ldon_id:
+            cmd.select('tmp_bs', 'id %i & %s' % (i[0], self.protname))
+            cmd.select('tmp_lig', 'id %i & %s' % (i[1], self.ligname))
+            cmd.distance('HBonds', 'tmp_bs', 'tmp_lig')
+        for i in hbonds.pdon_id:
+            cmd.select('tmp_bs', 'id %i & %s' % (i[1], self.protname))
+            cmd.select('tmp_lig', 'id %i & %s' % (i[0], self.ligname))
+            cmd.distance('HBonds', 'tmp_bs', 'tmp_lig')
+        if self.object_exists('HBonds'):
+            cmd.set('dash_color', 'blue', 'HBonds')
+
+    def show_halogen(self):
+        """Visualize halogen bonds."""
+        halogen = self.plcomplex.halogen_bonds
+        all_don_x, all_acc_o = [], []
+        for h in halogen:
+            all_don_x.append(h.don_id)
+            all_acc_o.append(h.acc_id)
+            cmd.select('tmp_bs', 'id %i & %s' % (h.acc_id, self.protname))
+            cmd.select('tmp_lig', 'id %i & %s' % (h.don_id, self.ligname))
+
+            cmd.distance('HalogenBonds', 'tmp_bs', 'tmp_lig')
+        if not len(all_acc_o) == 0:
+            self.select_by_ids('HalogenAccept', all_acc_o, restrict=self.protname)
+            self.select_by_ids('HalogenDonor', all_don_x, restrict=self.ligname)
+        if self.object_exists('HalogenBonds'):
+            cmd.set('dash_color', 'greencyan', 'HalogenBonds')
+
+    def show_stacking(self):
+        """Visualize pi-stacking interactions."""
+        stacks = self.plcomplex.pistacking
+        for i, stack in enumerate(stacks):
+            pires_ids = '+'.join(map(str, stack.proteinring_atoms))
+            pilig_ids = '+'.join(map(str, stack.ligandring_atoms))
+            cmd.select('StackRings-P', 'StackRings-P or (id %s & %s)' % (pires_ids, self.protname))
+            cmd.select('StackRings-L', 'StackRings-L or (id %s & %s)' % (pilig_ids, self.ligname))
+            cmd.select('StackRings-P', 'byres StackRings-P')
+            cmd.show('sticks', 'StackRings-P')
+
+            cmd.pseudoatom('ps-pistack-1-%i' % i, pos=stack.proteinring_center)
+            cmd.pseudoatom('ps-pistack-2-%i' % i, pos=stack.ligandring_center)
+            cmd.pseudoatom('Centroids-P', pos=stack.proteinring_center)
+            cmd.pseudoatom('Centroids-L', pos=stack.ligandring_center)
+
+            if stack.type == 'P':
+                cmd.distance('PiStackingP', 'ps-pistack-1-%i' % i, 'ps-pistack-2-%i' % i)
+            if stack.type == 'T':
+                cmd.distance('PiStackingT', 'ps-pistack-1-%i' % i, 'ps-pistack-2-%i' % i)
+        if self.object_exists('PiStackingP'):
+            cmd.set('dash_color', 'green', 'PiStackingP')
+            cmd.set('dash_gap', 0.3, 'PiStackingP')
+            cmd.set('dash_length', 0.6, 'PiStackingP')
+        if self.object_exists('PiStackingT'):
+            cmd.set('dash_color', 'smudge', 'PiStackingT')
+            cmd.set('dash_gap', 0.3, 'PiStackingT')
+            cmd.set('dash_length', 0.6, 'PiStackingT')
+
+    def show_cationpi(self):
+        """Visualize cation-pi interactions."""
+        for i, p in enumerate(self.plcomplex.pication):
+            cmd.pseudoatom('ps-picat-1-%i' % i, pos=p.ring_center)
+            cmd.pseudoatom('ps-picat-2-%i' % i, pos=p.charge_center)
+            if p.protcharged:
+                cmd.pseudoatom('Chargecenter-P', pos=p.charge_center)
+                cmd.pseudoatom('Centroids-L', pos=p.ring_center)
+                pilig_ids = '+'.join(map(str, p.ring_atoms))
+                cmd.select('PiCatRing-L', 'PiCatRing-L or (id %s & %s)' % (pilig_ids, self.ligname))
+                for a in p.charge_atoms:
+                    cmd.select('PosCharge-P', 'PosCharge-P or (id %i & %s)' % (a, self.protname))
+            else:
+                cmd.pseudoatom('Chargecenter-L', pos=p.charge_center)
+                cmd.pseudoatom('Centroids-P', pos=p.ring_center)
+                pires_ids = '+'.join(map(str, p.ring_atoms))
+                cmd.select('PiCatRing-P', 'PiCatRing-P or (id %s & %s)' % (pires_ids, self.protname))
+                for a in p.charge_atoms:
+                    cmd.select('PosCharge-L', 'PosCharge-L or (id %i & %s)' % (a, self.ligname))
+            cmd.distance('PiCation', 'ps-picat-1-%i' % i, 'ps-picat-2-%i' % i)
+        if self.object_exists('PiCation'):
+            cmd.set('dash_color', 'orange', 'PiCation')
+            cmd.set('dash_gap', 0.3, 'PiCation')
+            cmd.set('dash_length', 0.6, 'PiCation')
+
+    def show_sbridges(self):
+        """Visualize salt bridges."""
+        for i, saltb in enumerate(self.plcomplex.saltbridges):
+            if saltb.protispos:
+                for patom in saltb.positive_atoms:
+                    cmd.select('PosCharge-P', 'PosCharge-P or (id %i & %s)' % (patom, self.protname))
+                for latom in saltb.negative_atoms:
+                    cmd.select('NegCharge-L', 'NegCharge-L or (id %i & %s)' % (latom, self.ligname))
+                for sbgroup in [['ps-sbl-1-%i' % i, 'Chargecenter-P', saltb.positive_center],
+                                ['ps-sbl-2-%i' % i, 'Chargecenter-L', saltb.negative_center]]:
+                    cmd.pseudoatom(sbgroup[0], pos=sbgroup[2])
+                    cmd.pseudoatom(sbgroup[1], pos=sbgroup[2])
+                cmd.distance('Saltbridges', 'ps-sbl-1-%i' % i, 'ps-sbl-2-%i' % i)
+            else:
+                for patom in saltb.negative_atoms:
+                    cmd.select('NegCharge-P', 'NegCharge-P or (id %i & %s)' % (patom, self.protname))
+                for latom in saltb.positive_atoms:
+                    cmd.select('PosCharge-L', 'PosCharge-L or (id %i & %s)' % (latom, self.ligname))
+                for sbgroup in [['ps-sbp-1-%i' % i, 'Chargecenter-P', saltb.negative_center],
+                                ['ps-sbp-2-%i' % i, 'Chargecenter-L', saltb.positive_center]]:
+                    cmd.pseudoatom(sbgroup[0], pos=sbgroup[2])
+                    cmd.pseudoatom(sbgroup[1], pos=sbgroup[2])
+                cmd.distance('Saltbridges', 'ps-sbp-1-%i' % i, 'ps-sbp-2-%i' % i)
+
+        if self.object_exists('Saltbridges'):
+            cmd.set('dash_color', 'yellow', 'Saltbridges')
+            cmd.set('dash_gap', 0.5, 'Saltbridges')
+
+    def show_wbridges(self):
+        """Visualize water bridges."""
+        for bridge in self.plcomplex.waterbridges:
+            if bridge.protisdon:
+                cmd.select('HBondDonor-P', 'HBondDonor-P or (id %i & %s)' % (bridge.don_id, self.protname))
+                cmd.select('HBondAccept-L', 'HBondAccept-L or (id %i & %s)' % (bridge.acc_id, self.ligname))
+                cmd.select('tmp_don', 'id %i & %s' % (bridge.don_id, self.protname))
+                cmd.select('tmp_acc', 'id %i & %s' % (bridge.acc_id, self.ligname))
+            else:
+                cmd.select('HBondDonor-L', 'HBondDonor-L or (id %i & %s)' % (bridge.don_id, self.ligname))
+                cmd.select('HBondAccept-P', 'HBondAccept-P or (id %i & %s)' % (bridge.acc_id, self.protname))
+                cmd.select('tmp_don', 'id %i & %s' % (bridge.don_id, self.ligname))
+                cmd.select('tmp_acc', 'id %i & %s' % (bridge.acc_id, self.protname))
+            cmd.select('Water', 'Water or (id %i & resn HOH)' % bridge.water_id)
+            cmd.select('tmp_water', 'id %i & resn HOH' % bridge.water_id)
+            cmd.distance('WaterBridges', 'tmp_acc', 'tmp_water')
+            cmd.distance('WaterBridges', 'tmp_don', 'tmp_water')
+        if self.object_exists('WaterBridges'):
+            cmd.set('dash_color', 'lightblue', 'WaterBridges')
+        cmd.delete('tmp_water or tmp_acc or tmp_don')
+        cmd.color('lightblue', 'Water')
+        cmd.show('spheres', 'Water')
+
+    def show_metal(self):
+        """Visualize metal coordination."""
+        metal_complexes = self.plcomplex.metal_complexes
+        if not len(metal_complexes) == 0:
+            self.select_by_ids('Metal-M', self.metal_ids)
+            for metal_complex in metal_complexes:
+                cmd.select('tmp_m', 'id %i' % metal_complex.metal_id)
+                cmd.select('tmp_t', 'id %i' % metal_complex.target_id)
+                if metal_complex.location == 'water':
+                    cmd.select('Metal-W', 'Metal-W or id %s' % metal_complex.target_id)
+                if metal_complex.location.startswith('protein'):
+                    cmd.select('tmp_t', 'tmp_t & %s' % self.protname)
+                    cmd.select('Metal-P', 'Metal-P or (id %s & %s)' % (metal_complex.target_id, self.protname))
+                if metal_complex.location == 'ligand':
+                    cmd.select('tmp_t', 'tmp_t & %s' % self.ligname)
+                    cmd.select('Metal-L', 'Metal-L or (id %s & %s)' % (metal_complex.target_id, self.ligname))
+                cmd.distance('MetalComplexes', 'tmp_m', 'tmp_t')
+                cmd.delete('tmp_m or tmp_t')
+        if self.object_exists('MetalComplexes'):
+            cmd.set('dash_color', 'violetpurple', 'MetalComplexes')
+            cmd.set('dash_gap', 0.5, 'MetalComplexes')
+            # Show water molecules for metal complexes
+            cmd.show('spheres', 'Metal-W')
+            cmd.color('lightblue', 'Metal-W')
+
+
+
+    def selections_cleanup(self):
+        """Cleans up non-used selections"""
+
+        if not len(self.plcomplex.unpaired_hba_idx) == 0:
+            self.select_by_ids('Unpaired-HBA', self.plcomplex.unpaired_hba_idx, selection_exists=True)
+        if not len(self.plcomplex.unpaired_hbd_idx) == 0:
+            self.select_by_ids('Unpaired-HBD', self.plcomplex.unpaired_hbd_idx, selection_exists=True)
+        if not len(self.plcomplex.unpaired_hal_idx) == 0:
+            self.select_by_ids('Unpaired-HAL', self.plcomplex.unpaired_hal_idx, selection_exists=True)
+
+        selections = cmd.get_names("selections")
+        for selection in selections:
+            if len(cmd.get_model(selection).atom) == 0:
+                cmd.delete(selection)
+        cmd.deselect()
+        cmd.delete('tmp*')
+        cmd.delete('ps-*')
+
+    def selections_group(self):
+        """Group all selections"""
+        cmd.group('Structures', '%s %s %sCartoon' % (self.protname, self.ligname, self.protname))
+        cmd.group('Interactions', 'Hydrophobic HBonds HalogenBonds WaterBridges PiCation PiStackingP PiStackingT '
+                                  'Saltbridges MetalComplexes')
+        cmd.group('Atoms', '')
+        cmd.group('Atoms.Protein', 'Hydrophobic-P HBondAccept-P HBondDonor-P HalogenAccept Centroids-P PiCatRing-P '
+                                   'StackRings-P PosCharge-P NegCharge-P AllBSRes Chargecenter-P  Metal-P')
+        cmd.group('Atoms.Ligand', 'Hydrophobic-L HBondAccept-L HBondDonor-L HalogenDonor Centroids-L NegCharge-L '
+                                  'PosCharge-L NegCharge-L ChargeCenter-L StackRings-L PiCatRing-L Metal-L Metal-M '
+                                  'Unpaired-HBA Unpaired-HBD Unpaired-HAL Unpaired-RINGS')
+        cmd.group('Atoms.Other', 'Water Metal-W')
+        cmd.order('*', 'y')
+
+    def additional_cleanup(self):
+        """Cleanup of various representations"""
+
+        cmd.remove('not alt ""+A')  # Remove alternate conformations
+        cmd.hide('labels', 'Interactions')  # Hide labels of lines
+        cmd.disable('%sCartoon' % self.protname)
+        cmd.hide('everything', 'hydrogens')
+
+    def zoom_to_ligand(self):
+        """Zoom in too ligand and its interactions."""
+        cmd.center(self.ligname)
+        cmd.orient(self.ligname)
+        cmd.turn('x', 110)  # If the ligand is aligned with the longest axis, aromatic rings are hidden
+        if 'AllBSRes' in cmd.get_names("selections"):
+            cmd.zoom('%s or AllBSRes' % self.ligname, 3)
+        else:
+            if self.object_exists(self.ligname):
+                cmd.zoom(self.ligname, 3)
+        cmd.origin(self.ligname)
+
+    def save_session(self, outfolder):
+        """Saves a PyMOL session file."""
+        filename = '%s_%s' % (self.protname.upper(), "_".join([self.ligname, self.plcomplex.chain, self.plcomplex.position]))
+        cmd.save("/".join([outfolder, "%s.pse" % filename]))
+
+    def png_workaround(self, filepath, width=1200, height=800):
+        """Workaround for (a) severe bug(s) in PyMOL preventing ray-traced images to be produced in command-line mode.
+        Use this function in case neither cmd.ray() or cmd.png() work.
+        """
+        sys.stdout = sys.__stdout__
+        cmd.feedback('disable', 'movie', 'everything')
+        cmd.viewport(width, height)
+        cmd.zoom('visible', 1.5)  # Adapt the zoom to the viewport
+        cmd.set('ray_trace_frames', 1)  # Frames are raytraced before saving an image.
+        cmd.mpng(filepath, 1, 1)  # Use batch png mode with 1 frame only
+        cmd.mplay()  # cmd.mpng needs the animation to 'run'
+        cmd.refresh()
+        originalfile = "".join([filepath, '0001.png'])
+        newfile = "".join([filepath, '.png'])
+
+        #################################################
+        # Wait for file for max. 1 second and rename it #
+        #################################################
+
+        attempts = 0
+        while not os.path.isfile(originalfile) and attempts <= 10:
+            sleep(0.1)
+            attempts += 1
+        if os.name == 'nt':  # In Windows, make sure there is no file of the same name, cannot be overwritten as in Unix
+            if os.path.isfile(newfile):
+                os.remove(newfile)
+        os.rename(originalfile, newfile)  # Remove frame number in filename
+
+        #  Check if imagemagick is available and crop + resize the images
+        if subprocess.call("type convert", shell=True, stdout=subprocess.PIPE, stderr=subprocess.PIPE) == 0:
+            attempts, ecode = 0, 1
+            # Check if file is truncated and wait if that's the case
+            while ecode != 0 and attempts <= 10:
+                ecode = subprocess.call(['convert', newfile, '/dev/null'], stdout=open('/dev/null', 'w'),
+                                        stderr=subprocess.STDOUT)
+                sleep(0.1)
+                attempts += 1
+            trim = 'convert -trim ' + newfile + ' -bordercolor White -border 20x20 ' + newfile + ';'  # Trim the image
+            os.system(trim)
+            getwidth = 'w=`convert ' + newfile + ' -ping -format "%w" info:`;'  # Get the width of the new image
+            getheight = 'h=`convert ' + newfile + ' -ping -format "%h" info:`;'  # Get the hight of the new image
+            newres = 'if [ "$w" -gt "$h" ]; then newr="${w%.*}x$w"; else newr="${h%.*}x$h"; fi;'  # Set quadratic ratio
+            quadratic = 'convert ' + newfile + ' -gravity center -extent "$newr" ' + newfile  # Fill with whitespace
+            os.system(getwidth + getheight + newres + quadratic)
+        else:
+            sys.stderr.write('Imagemagick not available. Images will not be resized or cropped.')
+
+    def save_picture(self, outfolder, filename):
+        """Saves a picture"""
+        self.set_fancy_ray()
+        self.png_workaround("/".join([outfolder, filename]))
+
+    def set_fancy_ray(self):
+        """Give the molecule a flat, modern look."""
+        cmd.set('light_count', 6)
+        cmd.set('spec_count', 1.5)
+        cmd.set('shininess', 4)
+        cmd.set('specular', 0.3)
+        cmd.set('reflect', 1.6)
+        cmd.set('ambient', 0)
+        cmd.set('direct', 0)
+        cmd.set('ray_shadow', 0)  # Gives the molecules a flat, modern look
+        cmd.set('ambient_occlusion_mode', 1)
+
+    def refinements(self):
+        """Refinements for the visualization"""
+
+        # Show sticks for all residues interacing with the ligand
+        cmd.select('AllBSRes', 'byres (Hydrophobic-P or HBondDonor-P or HBondAccept-P or PosCharge-P or NegCharge-P or '
+                               'StackRings-P or PiCatRing-P or HalogenAcc or Metal-P)')
+        cmd.show('sticks', 'AllBSRes')
+        # Show spheres for the ring centroids
+        cmd.hide('everything', 'centroids*')
+        cmd.show('nb_spheres', 'centroids*')
+        # Show spheres for centers of charge
+        if self.object_exists('Chargecenter-P') or self.object_exists('Chargecenter-L'):
+            cmd.hide('nonbonded', 'chargecenter*')
+            cmd.show('spheres', 'chargecenter*')
+            cmd.set('sphere_scale', 0.4, 'chargecenter*')
+            cmd.color('yellow', 'chargecenter*')
+
+        cmd.set('valence', 1)  # Show bond valency (e.g. double bonds)
+        # Optional cartoon representation of the protein
+        cmd.copy('%sCartoon' % self.protname, self.protname)
+        cmd.show('cartoon', '%sCartoon' % self.protname)
+        cmd.show('sticks', '%sCartoon' % self.protname)
+        cmd.set('stick_transparency', 1, '%sCartoon' % self.protname)
+
+
+        # Resize water molecules. Sometimes they are not heteroatoms HOH, but part of the protein
+        cmd.set('sphere_scale', 0.2, 'resn HOH or Water')  # Needs to be done here because of the copy made
+        cmd.set('sphere_transparency', 0.4, '!(resn HOH or Water)')
+
+        if 'Centroids*' in cmd.get_names("selections"):
+            cmd.color('grey80', 'Centroids*')
+        cmd.hide('spheres', '%sCartoon' % self.protname)
+        cmd.hide('cartoon', '%sCartoon and resn DA+DG+DC+DU+DT+A+G+C+U+T' % self.protname)  # Hide DNA/RNA Cartoon
+        if self.ligname == 'SF4':  # Special case for iron-sulfur clusters, can't be visualized with sticks
+            cmd.show('spheres', '%s' % self.ligname)
+
+        cmd.hide('everything', 'resn HOH &!Water')  # Hide all non-interacting water molecules
+        cmd.hide('sticks', '%s and !%s and !AllBSRes' % (self.protname, self.ligname))  # Hide all non-interacting residues
diff --git a/plip/modules/supplemental.py b/plip/modules/supplemental.py
index 859f42d..27398af 100644
--- a/plip/modules/supplemental.py
+++ b/plip/modules/supplemental.py
@@ -32,6 +32,7 @@ import subprocess
 import codecs
 import gzip
 import zipfile
+import platform
 
 # External libraries
 import pybel
@@ -228,9 +229,6 @@ def cmd_exists(c):
 ################
 
 
-def object_exists(object_name):
-    """Checks if an object exists in the open PyMOL session."""
-    return object_name in cmd.get_names("objects")
 
 
 def initialize_pymol(options):
@@ -250,28 +248,6 @@ def start_pymol(quiet=False, options='-p', run=False):
     if quiet:
         cmd.feedback('disable', 'all', 'everything')
 
-
-def standard_settings():
-    """Sets up standard settings for a nice visualization."""
-    cmd.set('bg_rgb', [1.0, 1.0, 1.0])  # White background
-    cmd.set('depth_cue', 0)  # Turn off depth cueing (no fog)
-    cmd.set('cartoon_side_chain_helper', 1)  # Improve combined visualization of sticks and cartoon
-    cmd.set('cartoon_fancy_helices', 1)  # Nicer visualization of helices (using tapered ends)
-    cmd.set('transparency_mode', 1)  # Turn on multilayer transparency
-    cmd.set('dash_radius', 0.05)
-    set_custom_colorset()
-
-
-def set_custom_colorset():
-    """Defines a colorset with matching colors. Provided by Joachim."""
-    cmd.set_color('myorange', '[253, 174, 97]')
-    cmd.set_color('mygreen', '[171, 221, 164]')
-    cmd.set_color('myred', '[215, 25, 28]')
-    cmd.set_color('myblue', '[43, 131, 186]')
-    cmd.set_color('mylightblue', '[158, 202, 225]')
-    cmd.set_color('mylightgreen', '[229, 245, 224]')
-
-
 def nucleotide_linkage(residues):
     """Support for DNA/RNA ligands by finding missing covalent linkages to stitch DNA/RNA together."""
 
@@ -354,7 +330,7 @@ def get_isomorphisms(reference, lig):
         isomorphs = pybel.ob.vpairUIntUInt()
         mappr.MapFirst(lig.OBMol, isomorphs)
         isomorphs = [isomorphs]
-    debuglog("Number of isomorphisms: %i" % len(isomorphs))
+    write_message("Number of isomorphisms: %i\n" % len(isomorphs), mtype='debug')
     # #@todo Check which isomorphism to take
     return isomorphs
 
@@ -405,14 +381,14 @@ def int32_to_negative(int32):
         return int32
 
 
-def read_pdb(pdbfname):
+def read_pdb(pdbfname, as_string=False):
     """Reads a given PDB file and returns a Pybel Molecule."""
     pybel.ob.obErrorLog.StopLogging()  # Suppress all OpenBabel warnings
     if os.name != 'nt':  # Resource module not available for Windows
         maxsize = resource.getrlimit(resource.RLIMIT_STACK)[-1]
         resource.setrlimit(resource.RLIMIT_STACK, (min(2 ** 28, maxsize), maxsize))
     sys.setrecursionlimit(10 ** 5)  # increase Python recoursion limit
-    return readmol(pdbfname)
+    return readmol(pdbfname, as_string=as_string)
 
 
 def read(fil):
@@ -430,48 +406,84 @@ def read(fil):
             return open(fil, 'r')
 
 
-def readmol(path):
+def readmol(path, as_string=False):
     """Reads the given molecule file and returns the corresponding Pybel molecule as well as the input file type.
     In contrast to the standard Pybel implementation, the file is closed properly."""
     supported_formats = ['pdb', 'pdbqt', 'mmcif']
     obc = pybel.ob.OBConversion()
 
-    with read(path) as f:
-        filestr = str(f.read())
-
+    if as_string:
+        filestr = path
+    else:
+        with read(path) as f:
+            filestr = str(f.read())
     for sformat in supported_formats:
         obc.SetInFormat(sformat)
         mol = pybel.ob.OBMol()
         obc.ReadString(mol, filestr)
         if not mol.Empty():
             if sformat == 'pdbqt':
-                message('[EXPERIMENTAL] Input is PDBQT file. Some features (especially visualization) might not '
+                write_message('[EXPERIMENTAL] Input is PDBQT file. Some features (especially visualization) might not '
                         'work as expected. Please consider using PDB format instead.\n')
             if sformat == 'mmcif':
-                message('[EXPERIMENTAL] Input is mmCIF file. Most features do currently not work with this format.\n')
+                write_message('[EXPERIMENTAL] Input is mmCIF file. Most features do currently not work with this format.\n')
             return pybel.Molecule(mol), sformat
     sysexit(4, 'No valid file format provided.')
 
 
 def sysexit(code, msg):
     """Exit using an custom error message and error code."""
-    sys.stderr.write(msg)
+    write_message(msg, mtype='error')
     sys.exit(code)
 
-
-def message(msg, indent=False):
+#####################
+# Verbose and Debug #
+#####################
+
+def colorlog(msg, color, bold=False, blink=False):
+    """Colors messages on non-Windows systems supporting ANSI escape."""
+
+    ## ANSI Escape Codes ##
+    PINK_COL = '\x1b[35m'
+    GREEN_COL = '\x1b[32m'
+    RED_COL = '\x1b[31m'
+    YELLOW_COL = '\x1b[33m'
+    BLINK = '\x1b[5m'
+    RESET = '\x1b[0m'
+
+    if platform.system() != 'Windows':
+        if blink:
+            msg = BLINK + msg + RESET
+        if color == 'yellow':
+            msg = YELLOW_COL + msg + RESET
+        if color == 'red':
+            msg = RED_COL + msg + RESET
+        if color == 'green':
+            msg = GREEN_COL + msg + RESET
+        if color == 'pink':
+            msg = PINK_COL + msg + RESET
+    return msg
+
+def write_message(msg, indent=False, mtype='standard', caption=False):
+    """Writes message if verbose mode is set."""
+    if (mtype=='debug' and config.DEBUG) or (mtype !='debug' and config.VERBOSE) or mtype=='error':
+        message(msg, indent=indent, mtype=mtype, caption=caption)
+
+def message(msg, indent=False, mtype='standard', caption=False):
     """Writes messages in verbose mode"""
-    if config.VERBOSE:
-        if indent:
-            msg = '  ' + msg
-        sys.stdout.write(msg)
-
-
-def debuglog(msg):
-    """Writes debug messages"""
-    if config.DEBUG:
-        msg = '    %% DEBUG: ' + msg
-        if len(msg) > 100:
-            msg = msg[:100] + ' ...'
-        msg += '\n'
+    if caption:
+        msg = msg + '\n' + '-'*len(msg)
+    if mtype == 'warning':
+        msg = colorlog('Warning:  ' + msg, 'yellow')
+    if mtype == 'error':
+        msg = colorlog('Error:  ' + msg, 'red')
+    if mtype == 'debug':
+        msg = colorlog('Debug:  ' + msg, 'pink')
+    if mtype == 'info':
+        msg = colorlog('Info:  ' + msg, 'green')
+    if indent:
+        msg = '  ' + msg
+    if mtype in ['error', 'warning']:
+        sys.stderr.write(msg)
+    else:
         sys.stdout.write(msg)
diff --git a/plip/modules/visualize.py b/plip/modules/visualize.py
index 423a833..5a72279 100644
--- a/plip/modules/visualize.py
+++ b/plip/modules/visualize.py
@@ -18,22 +18,20 @@ limitations under the License.
 
 
 # Own modules
-from supplemental import *
-from time import sleep
-from collections import namedtuple
+from supplemental import initialize_pymol, start_pymol, write_message, colorlog
+import config
+from pymolplip import PyMOLVisualizer
+from plipremote import VisualizerData
 
-hbonds_info = namedtuple('hbonds_info', 'ldon_id lig_don_id prot_acc_id pdon_id prot_don_id lig_acc_id')
-hydrophobic_info = namedtuple('hydrophobic_info', 'bs_ids lig_ids pairs_ids')
-halogen_info = namedtuple('halogen_info', 'don_id acc_id')
-pistack_info = namedtuple('pistack_info', 'proteinring_atoms, proteinring_center ligandring_atoms '
-                                          'ligandring_center type')
-pication_info = namedtuple('pication_info', 'ring_center charge_center ring_atoms charge_atoms, protcharged')
-sbridge_info = namedtuple('sbridge_info', 'positive_atoms negative_atoms positive_center negative_center protispos')
-wbridge_info = namedtuple('wbridge_info', 'don_id acc_id water_id protisdon')
-metal_info = namedtuple('metal_info', 'metal_id, target_id location')
+# Python Standard Library
+import json
+import sys
 
+# Special imports
+from pymol import cmd
 
-def select_by_ids(selname, idlist, selection_exists=False, chunksize=20):
+
+def select_by_ids(selname, idlist, selection_exists=False, chunksize=20, restrict=None):
     """Selection with a large number of ids concatenated into a selection
     list can cause buffer overflow in PyMOL. This function takes a selection
     name and and list of IDs (list of integers) as input and makes a careful
@@ -44,152 +42,17 @@ def select_by_ids(selname, idlist, selection_exists=False, chunksize=20):
     idchunks = [idlist[i:i+chunksize] for i in xrange(0, len(idlist), chunksize)]
     for idchunk in idchunks:
         cmd.select(selname, '%s or (id %s)' % (selname, '+'.join(map(str, idchunk))))
+    if restrict is not None:
+        cmd.select(selname, '%s and %s' % (selname, restrict))
 
-class PyMOLComplex:
-    """Contains all information on a complex relevant for visualization. Can be pickled"""
-    def __init__(self, mol, site):
-        pcomp = mol
-        pli = mol.interaction_sets[site]
-        ligand = pli.ligand
-
-        # General Information
-        self.lig_members = sorted(pli.ligand.members)
-        self.sourcefile = pcomp.sourcefiles['pdbcomplex']
-        self.corrected_pdb = pcomp.corrected_pdb
-        self.pdbid = mol.pymol_name
-        self.hetid = ligand.hetid
-        self.chain = ligand.chain if not ligand.chain == "0" else ""  # #@todo Fix this
-        self.position = str(ligand.position)
-        self.outpath = mol.output_path
-        self.metal_ids = [x.m_orig_idx for x in pli.ligand.metals]
-        self.unpaired_hba_idx = pli.unpaired_hba_orig_idx
-        self.unpaired_hbd_idx = pli.unpaired_hbd_orig_idx
-        self.unpaired_hal_idx = pli.unpaired_hal_orig_idx
-
-        # Information on Interactions
-
-        # Hydrophobic Contacts
-        # Contains IDs of contributing binding site, ligand atoms and the pairings
-        hydroph_pairs_id = [(h.bsatom_orig_idx, h.ligatom_orig_idx) for h in pli.hydrophobic_contacts]
-        self.hydrophobic_contacts = hydrophobic_info(bs_ids=[hp[0] for hp in hydroph_pairs_id],
-                                                     lig_ids=[hp[1] for hp in hydroph_pairs_id],
-                                                     pairs_ids=hydroph_pairs_id)
-
-        # Hydrogen Bonds
-        # #@todo Don't use indices, simplify this code here
-        hbonds_ldon, hbonds_pdon = pli.hbonds_ldon, pli.hbonds_pdon
-        hbonds_ldon_id = [(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_ldon]
-        hbonds_pdon_id = [(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_pdon]
-        self.hbonds = hbonds_info(ldon_id=[(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_ldon],
-                                  lig_don_id=[hb[1] for hb in hbonds_ldon_id],
-                                  prot_acc_id=[hb[0] for hb in hbonds_ldon_id],
-                                  pdon_id=[(hb.a_orig_idx, hb.d_orig_idx) for hb in hbonds_pdon],
-                                  prot_don_id=[hb[1] for hb in hbonds_pdon_id],
-                                  lig_acc_id=[hb[0] for hb in hbonds_pdon_id])
-
-        # Halogen Bonds
-        self.halogen_bonds = [halogen_info(don_id=h.don_orig_idx, acc_id=h.acc_orig_idx)
-                              for h in pli.halogen_bonds]
-
-        # Pistacking
-        self.pistacking = [pistack_info(proteinring_atoms=pistack.proteinring.atoms_orig_idx,
-                                        proteinring_center=pistack.proteinring.center,
-                                        ligandring_atoms=pistack.ligandring.atoms_orig_idx,
-                                        ligandring_center=pistack.ligandring.center,
-                                        type=pistack.type) for pistack in pli.pistacking]
-
-        # Pi-cation interactions
-        self.pication = [pication_info(ring_center=picat.ring.center,
-                                       charge_center=picat.charge.center,
-                                       ring_atoms=picat.ring.atoms_orig_idx,
-                                       charge_atoms=picat.charge.atoms_orig_idx,
-                                       protcharged=picat.protcharged)
-                         for picat in pli.pication_paro+pli.pication_laro]
-
-        # Salt Bridges
-        self.saltbridges = [sbridge_info(positive_atoms=sbridge.positive.atoms_orig_idx,
-                                         negative_atoms=sbridge.negative.atoms_orig_idx,
-                                         positive_center=sbridge.positive.center,
-                                         negative_center=sbridge.negative.center,
-                                         protispos=sbridge.protispos)
-                            for sbridge in pli.saltbridge_lneg+pli.saltbridge_pneg]
-
-        # Water Bridgese('wbridge_info', 'don_id acc_id water_id protisdon')
-        self.waterbridges = [wbridge_info(don_id=wbridge.d_orig_idx,
-                                          acc_id=wbridge.a_orig_idx,
-                                          water_id=wbridge.water_orig_idx,
-                                          protisdon=wbridge.protisdon) for wbridge in pli.water_bridges]
-
-        # Metal Complexes
-        self.metal_complexes = [metal_info(metal_id=metalc.metal_orig_idx,
-                                           target_id=metalc.target_orig_idx,
-                                           location=metalc.location) for metalc in pli.metal_complexes]
-
-
-def set_fancy_ray():
-    """Give the molecule a flat, modern look."""
-    cmd.set('light_count', 6)
-    cmd.set('spec_count', 1.5)
-    cmd.set('shininess', 4)
-    cmd.set('specular', 0.3)
-    cmd.set('reflect', 1.6)
-    cmd.set('ambient', 0)
-    cmd.set('direct', 0)
-    cmd.set('ray_shadow', 0)  # Gives the molecules a flat, modern look
-    cmd.set('ambient_occlusion_mode', 1)
-
-
-def png_workaround(filepath, width=1200, height=800):
-    """Workaround for (a) severe bug(s) in PyMOL preventing ray-traced images to be produced in command-line mode.
-    Use this function in case neither cmd.ray() or cmd.png() work.
-    """
-    sys.stdout = sys.__stdout__
-    cmd.feedback('disable', 'movie', 'everything')
-    cmd.viewport(width, height)
-    cmd.zoom('visible', 1.5)  # Adapt the zoom to the viewport
-    cmd.set('ray_trace_frames', 1)  # Frames are raytraced before saving an image.
-    cmd.mpng(filepath, 1, 1)  # Use batch png mode with 1 frame only
-    cmd.mplay()  # cmd.mpng needs the animation to 'run'
-    cmd.refresh()
-    originalfile = "".join([filepath, '0001.png'])
-    newfile = "".join([filepath, '.png'])
 
-    #################################################
-    # Wait for file for max. 1 second and rename it #
-    #################################################
+def visualize_in_pymol(plcomplex):
+    """Visualizes the protein-ligand pliprofiler at one site in PyMOL."""
 
-    attempts = 0
-    while not os.path.isfile(originalfile) and attempts <= 10:
-        sleep(0.1)
-        attempts += 1
-    if os.name == 'nt':  # In Windows, make sure there is no file of the same name, cannot be overwritten as in Unix
-        if os.path.isfile(newfile):
-            os.remove(newfile)
-    os.rename(originalfile, newfile)  # Remove frame number in filename
+    vis = PyMOLVisualizer(plcomplex)
 
-    #  Check if imagemagick is available and crop + resize the images
-    if cmd_exists('convert'):
-        attempts, ecode = 0, 1
-        # Check if file is truncated and wait if that's the case
-        while ecode != 0 and attempts <= 10:
-            ecode = subprocess.call(['convert', newfile, '/dev/null'], stdout=open('/dev/null', 'w'),
-                                    stderr=subprocess.STDOUT)
-            sleep(0.1)
-            attempts += 1
-        trim = 'convert -trim ' + newfile + ' -bordercolor White -border 20x20 ' + newfile + ';'  # Trim the image
-        os.system(trim)
-        getwidth = 'w=`convert ' + newfile + ' -ping -format "%w" info:`;'  # Get the width of the new image
-        getheight = 'h=`convert ' + newfile + ' -ping -format "%h" info:`;'  # Get the hight of the new image
-        newres = 'if [ "$w" -gt "$h" ]; then newr="${w%.*}x$w"; else newr="${h%.*}x$h"; fi;'  # Set quadratic ratio
-        quadratic = 'convert ' + newfile + ' -gravity center -extent "$newr" ' + newfile  # Fill with whitespace
-        os.system(getwidth + getheight + newres + quadratic)
-    else:
-        sys.stderr.write('Imagemagick not available. Images will not be resized or cropped.')
 
 
-def visualize_in_pymol(plcomplex):
-    """Visualizes the protein-ligand pliprofiler at one site in PyMOL."""
-
     #####################
     # Set everything up #
     #####################
@@ -206,20 +69,18 @@ def visualize_in_pymol(plcomplex):
     ########################
 
     start_pymol(run=True, options='-pcq', quiet=not config.DEBUG)
-    standard_settings()
-    cmd.set('dash_gap', 0)  # Show not dashes, but lines for the pliprofiler
-    cmd.set('ray_shadow', 0)  # Turn on ray shadows for clearer ray-traced images
-    cmd.set('cartoon_color', 'mylightblue')
+    vis.set_initial_representations()
+
     cmd.load(plcomplex.sourcefile)
     current_name = cmd.get_object_list(selection='(all)')[0]
-    debuglog('Setting current_name to "%s" and pdbid to "%s"' % (current_name, pdbid))
+    write_message('Setting current_name to "%s" and pdbid to "%s\n"' % (current_name, pdbid), mtype='debug')
     cmd.set_name(current_name, pdbid)
     cmd.hide('everything', 'all')
     cmd.select(ligname, 'resn %s and chain %s and resi %s*' % (ligname, chain, plcomplex.position))
 
     # Visualize and color metal ions if there are any
     if not len(metal_ids) == 0:
-        select_by_ids(ligname, metal_ids, selection_exists=True)
+        vis.select_by_ids(ligname, metal_ids, selection_exists=True)
         cmd.show('spheres', 'id %s and %s' % (metal_ids_str, pdbid))
 
     # Additionally, select all members of composite ligands
@@ -236,316 +97,27 @@ def visualize_in_pymol(plcomplex):
         cmd.set('sphere_scale', 0.3, ligname)
     cmd.deselect()
 
-    ###########################
-    # Create empty selections #
-    ###########################
-
-    for group in ['Hydrophobic-P', 'Hydrophobic-L', 'HBondDonor-P', 'HBondDonor-L', 'HBondAccept-P', 'HBondAccept-L',
-                  'HalogenAccept', 'HalogenDonor', 'Water', 'MetalIons', 'StackRings-P', 'PosCharge-P', 'PosCharge-L',
-                  'NegCharge-P', 'NegCharge-L', 'PiCatRing-P', 'StackRings-L', 'PiCatRing-L', 'Metal-M', 'Metal-P',
-                  'Metal-W', 'Metal-L', 'Unpaired-HBA', 'Unpaired-HBD', 'Unpaired-HAL', 'Unpaired-RINGS']:
-        cmd.select(group, 'None')
-
-    # #@todo Up here, define all names necessary for analysis
-
-    ######################################
-    # Visualize hydrophobic interactions #
-    ######################################
-
-    if not len(plcomplex.hydrophobic_contacts.bs_ids) == 0:
-        for h in [['Hydrophobic-P', plcomplex.hydrophobic_contacts.bs_ids],
-                  ['Hydrophobic-L', plcomplex.hydrophobic_contacts.lig_ids]]:
-            select_by_ids(h[0], h[1])
-        for i in plcomplex.hydrophobic_contacts.pairs_ids:
-            cmd.select('tmp_bs', 'id %i' % i[0])
-            cmd.select('tmp_lig', 'id %i' % i[1])
-            cmd.distance('Hydrophobic', 'tmp_bs', 'tmp_lig')
-        if object_exists('Hydrophobic'):
-            cmd.set('dash_gap', 0.5, 'Hydrophobic')
-            cmd.set('dash_color', 'grey50', 'Hydrophobic')
-    else:
-        cmd.select('Hydrophobic-P', 'None')
-
-    #####################
-    # Visualize H-Bonds #
-    #####################
-
-    for group in [['HBondDonor-L', plcomplex.hbonds.lig_don_id], ['HBondDonor-P', plcomplex.hbonds.prot_don_id],
-                  ['HBondAccept-L', plcomplex.hbonds.lig_acc_id], ['HBondAccept-P', plcomplex.hbonds.prot_acc_id]]:
-        if not len(group[1]) == 0:
-            select_by_ids(group[0], group[1])
-    for i in plcomplex.hbonds.ldon_id:
-        cmd.select('tmp_bs', 'id %i' % i[0])
-        cmd.select('tmp_lig', 'id %i' % i[1])
-        cmd.distance('HBonds', 'tmp_bs', 'tmp_lig')
-    for i in plcomplex.hbonds.pdon_id:
-        cmd.select('tmp_bs', 'id %i' % i[1])
-        cmd.select('tmp_lig', 'id %i' % i[0])
-        cmd.distance('HBonds', 'tmp_bs', 'tmp_lig')
-    if object_exists('HBonds'):
-        cmd.set('dash_color', 'blue', 'HBonds')
+    vis.make_initial_selections()
 
-    ###########################
-    # Visualize Halogen Bonds #
-    ###########################
+    vis.show_hydrophobic()  # Hydrophobic Contacts
+    vis.show_hbonds()  # Hydrogen Bonds
+    vis.show_halogen()  # Halogen Bonds
+    vis.show_stacking()  # pi-Stacking Interactions
+    vis.show_cationpi()  # pi-Cation Interactions
+    vis.show_sbridges()  # Salt Bridges
+    vis.show_wbridges()  # Water Bridges
+    vis.show_metal()  # Metal Coordination
 
-    halogen = plcomplex.halogen_bonds
-    all_don_x, all_acc_o = [], []
-    for h in halogen:
-        all_don_x.append(h.don_id)
-        all_acc_o.append(h.acc_id)
-        for group in [['tmp_bs', h.acc_id], ['tmp_lig', h.don_id]]:
-            cmd.select(group[0], 'id %i' % group[1])
-        cmd.distance('HalogenBonds', 'tmp_bs', 'tmp_lig')
-    if not len(all_acc_o) == 0:
-        select_by_ids('HalogenAccept', all_acc_o)
-        select_by_ids('HalogenDonor', all_don_x)
-        #cmd.select('HalogenDonor', 'id %s' % '+'.join(map(str, all_don_x)))
-    if object_exists('HalogenBonds'):
-        cmd.set('dash_color', 'greencyan', 'HalogenBonds')
+    vis.refinements()
 
-    #########################
-    # Visualize Pi-Stacking #
-    #########################
 
-    stacks = plcomplex.pistacking
-    for i, stack in enumerate(stacks):
-        pires_ids = '+'.join(map(str, stack.proteinring_atoms))
-        pilig_ids = '+'.join(map(str, stack.ligandring_atoms))
-        cmd.select('StackRings-P', 'StackRings-P or id %s' % pires_ids)
-        cmd.select('StackRings-L', 'StackRings-L or id %s' % pilig_ids)
-        cmd.select('StackRings-P', 'byres StackRings-P')
-        cmd.show('sticks', 'StackRings-P')
+    vis.zoom_to_ligand()
 
-        cmd.pseudoatom('ps-pistack-1-%i' % i, pos=stack.proteinring_center)
-        cmd.pseudoatom('ps-pistack-2-%i' % i, pos=stack.ligandring_center)
-        cmd.pseudoatom('Centroids-P', pos=stack.proteinring_center)
-        cmd.pseudoatom('Centroids-L', pos=stack.ligandring_center)
-
-        if stack.type == 'P':
-            cmd.distance('PiStackingP', 'ps-pistack-1-%i' % i, 'ps-pistack-2-%i' % i)
-        if stack.type == 'T':
-            cmd.distance('PiStackingT', 'ps-pistack-1-%i' % i, 'ps-pistack-2-%i' % i)
-    if object_exists('PiStackingP'):
-        cmd.set('dash_color', 'green', 'PiStackingP')
-        cmd.set('dash_gap', 0.3, 'PiStackingP')
-        cmd.set('dash_length', 0.6, 'PiStackingP')
-    if object_exists('PiStackingT'):
-        cmd.set('dash_color', 'smudge', 'PiStackingT')
-        cmd.set('dash_gap', 0.3, 'PiStackingT')
-        cmd.set('dash_length', 0.6, 'PiStackingT')
-
-    ####################################
-    # Visualize Cation-pi interactions #
-    ####################################
-
-    for i, p in enumerate(plcomplex.pication):
-        cmd.pseudoatom('ps-picat-1-%i' % i, pos=p.ring_center)
-        cmd.pseudoatom('ps-picat-2-%i' % i, pos=p.charge_center)
-        if p.protcharged:
-            cmd.pseudoatom('Chargecenter-P', pos=p.charge_center)
-            cmd.pseudoatom('Centroids-L', pos=p.ring_center)
-            pilig_ids = '+'.join(map(str, p.ring_atoms))
-            cmd.select('PiCatRing-L', 'PiCatRing-L or id %s' % pilig_ids)
-            for a in p.charge_atoms:
-                cmd.select('PosCharge-P', 'PosCharge-P or id %i' % a)
-        else:
-            cmd.pseudoatom('Chargecenter-L', pos=p.charge_center)
-            cmd.pseudoatom('Centroids-P', pos=p.ring_center)
-            pires_ids = '+'.join(map(str, p.ring_atoms))
-            cmd.select('PiCatRing-P', 'PiCatRing-P or id %s' % pires_ids)
-            for a in p.charge_atoms:
-                cmd.select('PosCharge-L', 'PosCharge-L or id %i' % a)
-        cmd.distance('PiCation', 'ps-picat-1-%i' % i, 'ps-picat-2-%i' % i)
-    if object_exists('PiCation'):
-        cmd.set('dash_color', 'orange', 'PiCation')
-        cmd.set('dash_gap', 0.3, 'PiCation')
-        cmd.set('dash_length', 0.6, 'PiCation')
-
-    ##########################
-    # Visualize salt bridges #
-    ##########################
-
-    for i, saltb in enumerate(plcomplex.saltbridges):
-        if saltb.protispos:
-            for patom in saltb.positive_atoms:
-                cmd.select('PosCharge-P', 'PosCharge-P or id %i' % patom)
-            for latom in saltb.negative_atoms:
-                cmd.select('NegCharge-L', 'NegCharge-L or id %i' % latom)
-            for sbgroup in [['ps-sbl-1-%i' % i, 'Chargecenter-P', saltb.positive_center],
-                            ['ps-sbl-2-%i' % i, 'Chargecenter-L', saltb.negative_center]]:
-                cmd.pseudoatom(sbgroup[0], pos=sbgroup[2])
-                cmd.pseudoatom(sbgroup[1], pos=sbgroup[2])
-            cmd.distance('Saltbridges', 'ps-sbl-1-%i' % i, 'ps-sbl-2-%i' % i)
-        else:
-            for patom in saltb.negative_atoms:
-                cmd.select('NegCharge-P', 'NegCharge-P or id %i' % patom)
-            for latom in saltb.positive_atoms:
-                cmd.select('PosCharge-L', 'PosCharge-L or id %i' % latom)
-            for sbgroup in [['ps-sbp-1-%i' % i, 'Chargecenter-P', saltb.negative_center],
-                            ['ps-sbp-2-%i' % i, 'Chargecenter-L', saltb.positive_center]]:
-                cmd.pseudoatom(sbgroup[0], pos=sbgroup[2])
-                cmd.pseudoatom(sbgroup[1], pos=sbgroup[2])
-            cmd.distance('Saltbridges', 'ps-sbp-1-%i' % i, 'ps-sbp-2-%i' % i)
-
-    if object_exists('Saltbridges'):
-        cmd.set('dash_color', 'yellow', 'Saltbridges')
-        cmd.set('dash_gap', 0.5, 'Saltbridges')
-
-    ########################################
-    # Water-bridged H-Bonds (first degree) #
-    ########################################
-
-    for bridge in plcomplex.waterbridges:
-        if bridge.protisdon:
-            cmd.select('HBondDonor-P', 'HBondDonor-P or id %i' % bridge.don_id)
-            cmd.select('HBondAccept-L', 'HBondAccept-L or id %i' % bridge.acc_id)
-        else:
-            cmd.select('HBondDonor-L', 'HBondDonor-L or id %i' % bridge.don_id)
-            cmd.select('HBondAccept-P', 'HBondAccept-P or id %i' % bridge.acc_id)
-        cmd.select('Water', 'Water or id %i' % bridge.water_id)
-        cmd.select('tmp_don', 'id %i' % bridge.don_id)
-        cmd.select('tmp_water', 'id %i' % bridge.water_id)
-        cmd.select('tmp_acc', 'id %i' % bridge.acc_id)
-        cmd.distance('WaterBridges', 'tmp_acc', 'tmp_water')
-        cmd.distance('WaterBridges', 'tmp_don', 'tmp_water')
-    if object_exists('WaterBridges'):
-        cmd.set('dash_color', 'lightblue', 'WaterBridges')
-    cmd.delete('tmp_water or tmp_acc or tmp_don')
-    cmd.color('lightblue', 'Water')
-    cmd.show('spheres', 'Water')
-
-    ###################
-    # Metal Complexes #
-    ###################
-
-    if not len(plcomplex.metal_complexes) == 0:
-        select_by_ids('Metal-M', metal_ids)
-        for metal_complex in plcomplex.metal_complexes:
-            cmd.select('tmp_m', 'id %i' % metal_complex.metal_id)
-            cmd.select('tmp_t', 'id %i' % metal_complex.target_id)
-            if metal_complex.location == 'water':
-                cmd.select('Metal-W', 'Metal-W or id %s' % metal_complex.target_id)
-            if metal_complex.location.startswith('protein'):
-                cmd.select('Metal-P', 'Metal-P or id %s' % metal_complex.target_id)
-            if metal_complex.location == 'ligand':
-                cmd.select('Metal-L', 'Metal-L or id %s' % metal_complex.target_id)
-            cmd.distance('MetalComplexes', 'tmp_m', 'tmp_t')
-            cmd.delete('tmp_m or tmp_t')
-    if object_exists('MetalComplexes'):
-        cmd.set('dash_color', 'violetpurple', 'MetalComplexes')
-        cmd.set('dash_gap', 0.5, 'MetalComplexes')
-        # Show water molecules for metal complexes
-        cmd.show('spheres', 'Metal-W')
-        cmd.color('lightblue', 'Metal-W')
-
-    ######################
-    # Visualize the rest #
-    ######################
-
-    # Show sticks for all residues interacing with the ligand
-    cmd.select('AllBSRes', 'byres (Hydrophobic-P or HBondDonor-P or HBondAccept-P or PosCharge-P or NegCharge-P or '
-                           'StackRings-P or PiCatRing-P or HalogenAcc or Metal-P)')
-    # #@todo Check, should be HalogenAccept here?
-    cmd.show('sticks', 'AllBSRes')
-    # Show spheres for the ring centroids
-    cmd.hide('everything', 'centroids*')
-    cmd.show('nb_spheres', 'centroids*')
-    # Show spheres for centers of charge
-    if object_exists('Chargecenter-P') or object_exists('Chargecenter-L'):
-        cmd.hide('nonbonded', 'chargecenter*')
-        cmd.show('spheres', 'chargecenter*')
-        cmd.set('sphere_scale', 0.4, 'chargecenter*')
-        cmd.color('yellow', 'chargecenter*')
-
-    ####################
-    # Last refinements #
-    ####################
-
-    cmd.set('valence', 1)  # Show bond valency (e.g. double bonds)
-    # Optional cartoon representation of the protein
-    cmd.copy('%sCartoon' % pdbid, pdbid)
-    cmd.show('cartoon', '%sCartoon' % pdbid)
-    cmd.show('sticks', '%sCartoon' % pdbid)
-    cmd.set('stick_transparency', 1, '%sCartoon' % pdbid)
-    # Set view. Zoom on the ligand (and its pliprofiler)
-
-    cmd.center(ligname)
-    cmd.orient(ligname)
-    cmd.turn('x', 110)  # If the ligand is aligned with the longest axis, aromatic rings are hidden
-    if 'AllBSRes' in cmd.get_names("selections"):
-        cmd.zoom('%s or AllBSRes' % ligname, 3)
-    else:
-        if object_exists(ligname):
-            cmd.zoom(ligname, 3)
-
-    # Resize water molecules. Sometimes they are not heteroatoms HOH, but part of the protein
-    cmd.set('sphere_scale', 0.2, 'resn HOH or Water')  # Needs to be done here because of the copy made
-    cmd.set('sphere_transparency', 0.4, '!(resn HOH or Water)')
-
-    cmd.origin(ligname)
-    if 'Centroids*' in cmd.get_names("selections"):
-        cmd.color('grey80', 'Centroids*')
-    cmd.hide('spheres', '%sCartoon' % pdbid)
-    cmd.hide('cartoon', '%sCartoon and resn DA+DG+DC+DU+DT+A+G+C+U+T' % pdbid)  # Hide DNA/RNA Cartoon
-    if ligname == 'SF4':  # Special case for iron-sulfur clusters, can't be visualized with sticks
-        cmd.show('spheres', '%s' % ligname)
-
-    ##################################
-    # Selections for unpaired groups #
-    ##################################
-    if not len(plcomplex.unpaired_hba_idx) == 0:
-        select_by_ids('Unpaired-HBA', plcomplex.unpaired_hba_idx, selection_exists=True)
-    if not len(plcomplex.unpaired_hbd_idx) == 0:
-        select_by_ids('Unpaired-HBD', plcomplex.unpaired_hbd_idx, selection_exists=True)
-    if not len(plcomplex.unpaired_hal_idx) == 0:
-        select_by_ids('Unpaired-HAL', plcomplex.unpaired_hal_idx, selection_exists=True)
-
-    ##############################
-    # Organization of selections #
-    ##############################
-
-    # Delete all empty and temporary selections
-    selections = cmd.get_names("selections")
-    for selection in selections:
-        if len(cmd.get_model(selection).atom) == 0:
-            cmd.delete(selection)
-    cmd.deselect()
-    cmd.delete('tmp*')
-    cmd.delete('ps-*')
-
-    # Group non-empty selections
-    cmd.group('Structures', '%s %s %sCartoon' % (pdbid, ligname, pdbid))
-    cmd.group('Interactions', 'Hydrophobic HBonds HalogenBonds WaterBridges PiCation PiStackingP PiStackingT '
-                              'Saltbridges MetalComplexes')
-    cmd.group('Atoms', '')
-    cmd.group('Atoms.Protein', 'Hydrophobic-P HBondAccept-P HBondDonor-P HalogenAccept Centroids-P PiCatRing-P '
-                               'StackRings-P PosCharge-P NegCharge-P AllBSRes Chargecenter-P  Metal-P')
-    cmd.group('Atoms.Ligand', 'Hydrophobic-L HBondAccept-L HBondDonor-L HalogenDonor Centroids-L NegCharge-L '
-                              'PosCharge-L NegCharge-L ChargeCenter-L StackRings-L PiCatRing-L Metal-L Metal-M '
-                              'Unpaired-HBA Unpaired-HBD Unpaired-HAL Unpaired-RINGS')
-    cmd.group('Atoms.Other', 'Water Metal-W')
-    cmd.order('*', 'y')
-
-    ###############################################
-    # Remove atoms with alternative conformations #
-    ###############################################
-
-    cmd.remove('not alt ""+A')
-
-    ########################################
-    # Clean up and save PyMOL session file #
-    ########################################
-
-    cmd.hide('labels', 'Interactions')
-    cmd.disable('%sCartoon' % pdbid)
-    cmd.hide('everything', 'hydrogens')
-
-    filename = '%s_%s' % (pdbid.upper(), "_".join([ligname, plcomplex.chain, plcomplex.position]))
+    vis.selections_cleanup()
+    vis.selections_group()
+    vis.additional_cleanup()
     if config.PYMOL:
-        cmd.save("/".join([config.OUTPATH, "%s.pse" % filename]))
-
-    # Create output pictures (experimental)
-    set_fancy_ray()
+        vis.save_session(config.OUTPATH)
     if config.PICS:
-        png_workaround("/".join([config.OUTPATH, filename]))
+        filename = '%s_%s' % (pdbid.upper(), "_".join([ligname, plcomplex.chain, plcomplex.position]))
+        vis.save_picture(config.OUTPATH, filename)
diff --git a/plip/plipcmd b/plip/plipcmd
index 9aa84de..576bac0 100755
--- a/plip/plipcmd
+++ b/plip/plipcmd
@@ -23,13 +23,15 @@ from __future__ import print_function
 # Own modules
 try:
     from plip.modules.preparation import *
-    from plip.modules.visualize import visualize_in_pymol, PyMOLComplex
+    from plip.modules.visualize import visualize_in_pymol
+    from plip.modules.plipremote import VisualizerData
     from plip.modules.report import TextOutput
     from plip.modules import config
     from plip.modules.mp import parallel_fn
 except ImportError:
     from modules.preparation import *
     from modules.visualize import visualize_in_pymol, PyMOLComplex
+    from modules.plipremote import VisualizerData
     from modules.report import TextOutput
     from modules import config
     from modules.mp import parallel_fn
@@ -41,11 +43,12 @@ from argparse import ArgumentParser
 import urllib2
 import time
 import multiprocessing
+import json
 
 # External libraries
 import lxml.etree as et
 
-__version__ = '1.3.0'
+__version__ = '1.3.1'
 descript = "Protein-Ligand Interaction Profiler (PLIP) v%s " \
            "is a command-line based tool to analyze interactions in a protein-ligand complex. " \
            "If you are using PLIP in your work, please cite: " \
@@ -78,30 +81,30 @@ def check_pdb_status(pdbid):
 def fetch_pdb(pdbid):
     """Get the newest entry from the RCSB server for the given PDB ID. Exits with '1' if PDB ID is invalid."""
     pdbid = pdbid.lower()
-    message('\nChecking status of PDB ID %s ... ' % pdbid)
+    write_message('\nChecking status of PDB ID %s ... ' % pdbid)
     state, current_entry = check_pdb_status(pdbid)  # Get state and current PDB ID
 
     if state == 'OBSOLETE':
-        message('entry is obsolete, getting %s instead.\n' % current_entry)
+        write_message('entry is obsolete, getting %s instead.\n' % current_entry)
     elif state == 'CURRENT':
-        message('entry is up to date.\n')
+        write_message('entry is up to date.\n')
     elif state == 'UNKNOWN':
-        sysexit(3, 'Invalid PDB ID (Entry does not exist on PDB server)')
-    message('Downloading file from PDB ... ')
+        sysexit(3, 'Invalid PDB ID (Entry does not exist on PDB server)\n')
+    write_message('Downloading file from PDB ... ')
     pdburl = 'http://www.rcsb.org/pdb/files/%s.pdb' % current_entry  # Get URL for current entry
     pdbfile = urllib2.urlopen(pdburl).read()
     # If no PDB file is available, a text is now shown with "We're sorry, but ..."
     # Could previously be distinguished by an HTTP error
     if 'sorry' in pdbfile:
-        sysexit(5, "Error: No file in PDB format available from wwPDB for the given PDB ID.\n")
+        sysexit(5, "No file in PDB format available from wwPDB for the given PDB ID.\n")
     return [pdbfile, current_entry]
 
 
 def process_pdb(pdbfile, outpath):
     """Analysis of a single PDB file. Can generate textual reports XML, PyMOL session files and images as output."""
     startmessage = '\nStarting analysis of %s\n' % pdbfile.split('/')[-1]
-    message(startmessage)
-    message('='*len(startmessage)+'\n')
+    write_message(startmessage)
+    write_message('='*len(startmessage)+'\n')
     mol = PDBComplex()
     mol.output_path = outpath
     mol.load_pdb(pdbfile)
@@ -139,15 +142,29 @@ def process_pdb(pdbfile, outpath):
 
     config.MAXTHREADS = min(config.MAXTHREADS, len(mol.interaction_sets))
 
+    ####
+    # Create JSON dump of PyMOL helper class
+    ###
+
+    if config.VISJSON:
+        complexes = [VisualizerData(mol, site) for site in sorted(mol.interaction_sets)
+        if not len(mol.interaction_sets[site].interacting_res) == 0]
+
+        with open('%s/plipvis.json' % outpath, 'w') as f:
+            cplx_dict = {}
+            for cplx in complexes:
+                cplx_dict[cplx.uid] = cplx.to_json()
+            json.dump(cplx_dict, f)
+
     ######################################
     # PyMOL Visualization (parallelized) #
     ######################################
 
     if config.PYMOL or config.PICS:
-        complexes = [PyMOLComplex(mol, site) for site in sorted(mol.interaction_sets)
+        complexes = [VisualizerData(mol, site) for site in sorted(mol.interaction_sets)
                      if not len(mol.interaction_sets[site].interacting_res) == 0]
         if config.MAXTHREADS > 1:
-            message('\nGenerating visualizations in parallel on %i cores ...' % config.MAXTHREADS)
+            write_message('\nGenerating visualizations in parallel on %i cores ...' % config.MAXTHREADS)
             parfn = parallel_fn(visualize_in_pymol)
             parfn(complexes, processes=config.MAXTHREADS)
         else:
@@ -185,17 +202,17 @@ def download_structure(inputpdbid):
     Returns the path of the downloaded file."""
     try:
         if len(inputpdbid) != 4 or extract_pdbid(inputpdbid.lower()) == 'UnknownProtein':
-            sysexit(3, 'Error: Invalid PDB ID (Wrong format)')
+            sysexit(3, 'Invalid PDB ID (Wrong format)\n')
         pdbfile, pdbid = fetch_pdb(inputpdbid.lower())
         pdbpath = tilde_expansion('%s/%s.pdb' % (config.BASEPATH.rstrip('/'), pdbid))
         create_folder_if_not_exists(config.BASEPATH)
         with open(pdbpath, 'w') as g:
             g.write(pdbfile)
-        message('file downloaded as %s\n\n' % pdbpath)
+        write_message('file downloaded as %s\n\n' % pdbpath)
         return pdbpath, pdbid
 
     except ValueError:  # Invalid PDB ID, cannot fetch from RCBS server
-        sysexit(3, 'Error: Invalid PDB ID (Entry does not exist)')
+        sysexit(3, 'Invalid PDB ID (Entry does not exist)\n')
 
 def remove_duplicates(slist):
     """Checks input lists for duplicates and returns
@@ -203,9 +220,9 @@ def remove_duplicates(slist):
     unique = list(set(slist))
     difference = len(slist) - len(unique)
     if difference == 1:
-        message("Removed one duplicate entry from input list.\n")
+        write_message("Removed one duplicate entry from input list.\n")
     if difference > 1:
-        message("Removed %i duplicate entries from input list.\n" % difference)
+        write_message("Removed %i duplicate entries from input list.\n" % difference)
     return unique
 
 
@@ -213,19 +230,18 @@ def main(inputstructs, inputpdbids):
     """Main function. Calls functions for processing, report generation and visualization."""
     pdbid, pdbpath = None, None
     # #@todo For multiprocessing, implement better stacktracing for errors
-
     # Print title and version
     title = "* Protein-Ligand Interaction Profiler v%s *" % __version__
-    message('\n' + '*' * len(title) + '\n')
-    message(title)
-    message('\n' + '*' * len(title) + '\n\n')
+    write_message('\n' + '*' * len(title) + '\n')
+    write_message(title)
+    write_message('\n' + '*' * len(title) + '\n\n')
 
     if inputstructs is not None:  # Process PDB file(s)
         num_structures = len(inputstructs)
         inputstructs = remove_duplicates(inputstructs)
         for inputstruct in inputstructs:
             if os.path.getsize(inputstruct) == 0:
-                sysexit(2, 'Error: Empty PDB file')  # Exit if input file is empty
+                sysexit(2, 'Empty PDB file\n')  # Exit if input file is empty
             if num_structures > 1:
                 basename = inputstruct.split('.')[0].split('/')[-1]
                 config.OUTPATH = '/'.join([config.BASEPATH, basename])
@@ -241,9 +257,9 @@ def main(inputstructs, inputpdbids):
 
     if (pdbid is not None or inputstructs is not None) and config.BASEPATH is not None:
         if config.BASEPATH in ['.', './']:
-            message('\nFinished analysis. Find the result files in the working directory.\n\n')
+            write_message('\nFinished analysis. Find the result files in the working directory.\n\n')
         else:
-            message('\nFinished analysis. Find the result files in %s\n\n' % config.BASEPATH)
+            write_message('\nFinished analysis. Find the result files in %s\n\n' % config.BASEPATH)
 
 if __name__ == '__main__':
 
@@ -278,6 +294,9 @@ if __name__ == '__main__':
     parser.add_argument("--debug", dest="debug", default=False,
                         help="Turn on DEBUG mode with extended log.",
                         action="store_true")
+    parser.add_argument("--nofix", dest="nofix", default=False,
+                        help="Turns off fixing of PDB files.",
+                        action="store_true")
     # Optional threshold arguments, not shown in help
     thr = namedtuple('threshold', 'name type')
     thresholds = [thr(name='aromatic_planarity', type='angle'),
@@ -298,13 +317,17 @@ if __name__ == '__main__':
     config.VERBOSE = True if (arguments.verbose or arguments.debug) else False
     config.DEBUG = True if arguments.debug else False
     config.MAXTHREADS = arguments.maxthreads
-    config.XML, config.TXT, config.PICS, config.PYMOL = arguments.xml, arguments.txt, arguments.pics, arguments.pymol
+    config.XML = arguments.xml
+    config.TXT = arguments.txt
+    config.PICS = arguments.pics
+    config.PYMOL = arguments.pymol
     config.OUTPATH = arguments.outpath
     config.OUTPATH = tilde_expansion("".join([config.OUTPATH, '/'])
                                      if not config.OUTPATH.endswith('/') else config.OUTPATH)
     config.BASEPATH = config.OUTPATH  # Used for batch processing
     config.BREAKCOMPOSITE = arguments.breakcomposite
     config.ALTLOC = arguments.altlocation
+    config.NOFIX = arguments.nofix
     # Assign values to global thresholds
     for t in thresholds:
         tvalue = getattr(arguments, t.name)
diff --git a/setup.py b/setup.py
index e57cc90..1fff2dc 100644
--- a/setup.py
+++ b/setup.py
@@ -19,7 +19,7 @@ limitations under the License.
 from setuptools import setup
 
 setup(name='plip',
-      version='1.3.0',
+      version='1.3.1',
       description='PLIP - Fully automated protein-ligand interaction profiler',
       classifiers=[
           'Development Status :: 5 - Production/Stable',

-- 
Alioth's /usr/local/bin/git-commit-notice on /srv/git.debian.org/git/debian-med/plip.git



More information about the debian-med-commit mailing list