diff --git a/modules/mol/alg/pymod/ligand_scoring_base.py b/modules/mol/alg/pymod/ligand_scoring_base.py
index c6e3d713fac3e8be389196cd4e88441312a6da2c..71223951e95d460b6802be630f5a989902121d27 100644
--- a/modules/mol/alg/pymod/ligand_scoring_base.py
+++ b/modules/mol/alg/pymod/ligand_scoring_base.py
@@ -222,6 +222,8 @@ class LigandScorer:
         # simple integers instead of enums - documentation of property describes
         # encoding
         self._state_matrix = None
+        self._model_ligand_states = None
+        self._target_ligand_states = None
 
         # score matrices
         self._score_matrix = None
@@ -243,6 +245,7 @@ class LigandScorer:
             iso = "subgraph isomorphism"
         else:
             iso = "full graph isomorphism"
+
         self.state_decoding = \
         {0: ("OK", "OK"),
          1: ("identity", f"Ligands could not be matched (by {iso})"),
@@ -251,6 +254,10 @@ class LigandScorer:
          3: ("no_iso", "No fully isomorphic match could be found - enabling "
              "substructure_match might allow a match"),
          4: ("disconnected", "Ligand graph is disconnected"),
+         5: ("stoichiometry", "Ligand was already assigned to another ligand "
+             "(different stoichiometry)"),
+         6: ("single_ligand_issue", "Cannot compute valid pairwise score as "
+             "either model or target ligand have non-zero state."),
          9: ("unknown", "An unknown error occured in LigandScorer")}
 
     @property
@@ -270,6 +277,32 @@ class LigandScorer:
             self._compute_scores()
         return self._state_matrix
 
+    @property
+    def model_ligand_states(self):
+        """ Encodes states of model ligands
+
+        Non-zero state in any of the model ligands invalidates the full
+        respective column in :attr:`~state_matrix`.
+
+        :rtype: :class:`~numpy.ndarray`
+        """
+        if self._model_ligand_states is None:
+            self._compute_scores()
+        return self._model_ligand_states
+
+    @property
+    def target_ligand_states(self):
+        """ Encodes states of target ligands
+
+        Non-zero state in any of the target ligands invalidates the full
+        respective row in :attr:`~state_matrix`.
+
+        :rtype: :class:`~numpy.ndarray`
+        """
+        if self._target_ligand_states is None:
+            self._compute_scores()
+        return self._target_ligand_states
+
     @property
     def score_matrix(self):
         """ Get the matrix of scores.
@@ -427,6 +460,7 @@ class LigandScorer:
 
         :rtype: :class:`list` of :class:`int`
         """
+        # compute on-the-fly, no need for caching
         assigned = set([x[0] for x in self.assignment])
         return [x for x in range(len(self.target_ligands)) if x not in assigned]
 
@@ -436,6 +470,7 @@ class LigandScorer:
 
         :rtype: :class:`list` of :class:`int`
         """
+        # compute on-the-fly, no need for caching
         assigned = set([x[1] for x in self.assignment])
         return [x for x in range(len(self.model_ligands)) if x not in assigned]
 
@@ -448,7 +483,8 @@ class LigandScorer:
                             generated
         :type trg_lig_idx: :class:`int`
         """
-        return self._get_report(self.state_matrix[trg_lig_idx,:])
+        return self._get_report(self.target_ligand_states[trg_lig_idx],
+                                self.state_matrix[trg_lig_idx,:])
 
     def get_model_ligand_state_report(self, mdl_lig_idx):
         """ Get summary of states observed with respect to all target ligands
@@ -459,20 +495,180 @@ class LigandScorer:
                             generated
         :type mdl_lig_idx: :class:`int`
         """
-        return self._get_report(self.state_matrix[:, mdl_lig_idx])
-
+        return self._get_report(self.model_ligand_states[mdl_lig_idx],
+                                self.state_matrix[:, mdl_lig_idx])
 
-    def _get_report(self, states):
+    def _get_report(self, ligand_state, pair_states):
         """ Helper
         """
-        report = list()
-        for s in np.unique(states):
+        pair_report = list()
+        for s in np.unique(pair_states):
             desc = self.state_decoding[s]
-            report.append({"state": s,
-                           "short desc": desc[0],
-                           "desc": desc[1],
-                           "indices": np.flatnonzero(states == s).tolist()})
-        return report
+            indices = np.flatnonzero(pair_states == s).tolist()
+            pair_report.append({"state": s,
+                                "short desc": desc[0],
+                                "desc": desc[1],
+                                "indices": indices})
+
+        desc = self.state_decoding[ligand_state]
+        ligand_report = {"state": ligand_state,
+                         "short desc": desc[0],
+                         "desc": desc[1]}
+
+        return (ligand_report, pair_report)
+
+    def guess_target_ligand_unassigned_reason(self, trg_lig_idx):
+        """ Makes an educated guess why target ligand is not assigned
+
+        This either returns actual error states or custom states that are
+        derived from them.
+
+        :param trg_lig_idx: Index of target ligand
+        :type trg_lig_idx: :class:`int`
+        :returns: :class:`tuple` with two elements: 1) keyword 2) human readable
+                  sentence describing the issue, (\"unknown\",\"unknown\") if
+                  nothing obvious can be found.
+        :raises: :class:`RuntimeError` if specified target ligand is assigned
+        """
+        if trg_lig_idx not in self.unassigned_target_ligands:
+            raise RuntimeError("Specified target ligand is not unassigned")
+
+        # hardcoded tuple if there is simply nothing we can assign it to
+        if len(self.model_ligands) == 0:
+            return ("no_ligand", "No ligand in the model")
+
+        # if something with the ligand itself is wrong, we can be pretty sure
+        # thats why the ligand is unassigned
+        if self.target_ligand_states[trg_lig_idx] != 0:
+            return self.state_decoding[self.target_ligand_states[trg_lig_idx]]
+
+        # The next best guess comes from looking at pair states
+        tmp = np.unique(self.state_matrix[trg_lig_idx,:])
+
+        # In case of any 0, it could have been assigned so it's probably 
+        # just not selected due to different stoichiometry - this is no
+        # defined state, we just return a hardcoded tuple in this case
+        if 0 in tmp:
+            return ("stoichiometry",
+                    "Ligand was already assigned to an other "
+                    "model ligand (different stoichiometry)")
+
+        # maybe its a symmetry issue?
+        if 2 in tmp:
+            return self.state_decoding[2]
+
+        # if the state is 6 (single_ligand_issue), there is an issue with its
+        # target counterpart.
+        if 6 in tmp:
+            mdl_idx = np.where(self.state_matrix[trg_lig_idx,:]==6)[0]
+            # we're reporting everything except disconnected error...
+            # don't ask...
+            for i in mdl_idx:
+                if self.model_ligand_states[i] == 0:
+                    raise RuntimeError("This should never happen")
+                if self.model_ligand_states[i] != 4:
+                    return self.state_decoding[self.model_ligand_states[i]]
+
+        # get rid of remaining single ligand issues (only disconnected error)
+        if 6 in tmp and len(tmp) > 1:
+            tmp = tmp[tmp!=6]
+
+        # prefer everything over identity state
+        if 1 in tmp and len(tmp) > 1:
+            tmp = tmp[tmp!=1]
+
+        # just return whatever is left
+        return self.state_decoding[tmp[0]]
+
+
+    def guess_model_ligand_unassigned_reason(self, mdl_lig_idx):
+        """ Makes an educated guess why model ligand is not assigned
+
+        This either returns actual error states or custom states that are
+        derived from them.
+
+        :param mdl_lig_idx: Index of model ligand
+        :type mdl_lig_idx: :class:`int`
+        :returns: :class:`tuple` with two elements: 1) keyword 2) human readable
+                  sentence describing the issue, (\"unknown\",\"unknown\") if
+                  nothing obvious can be found.
+        :raises: :class:`RuntimeError` if specified model ligand is assigned
+        """
+        if mdl_lig_idx not in self.unassigned_model_ligands:
+            raise RuntimeError("Specified model ligand is not unassigned")
+
+        # hardcoded tuple if there is simply nothing we can assign it to
+        if len(self.target_ligands) == 0:
+            return ("no_ligand", "No ligand in the target")
+
+        # if something with the ligand itself is wrong, we can be pretty sure
+        # thats why the ligand is unassigned
+        if self.model_ligand_states[mdl_lig_idx] != 0:
+            return self.state_decoding[self.model_ligand_states[mdl_lig_idx]]
+
+        # The next best guess comes from looking at pair states
+        tmp = np.unique(self.state_matrix[:,mdl_lig_idx])
+
+        # In case of any 0, it could have been assigned so it's probably 
+        # just not selected due to different stoichiometry - this is no
+        # defined state, we just return a hardcoded tuple in this case
+        if 0 in tmp:
+            return ("stoichiometry",
+                    "Ligand was already assigned to an other "
+                    "model ligand (different stoichiometry)")
+
+        # maybe its a symmetry issue?
+        if 2 in tmp:
+            return self.state_decoding[2]
+
+        # if the state is 6 (single_ligand_issue), there is an issue with its
+        # target counterpart.
+        if 6 in tmp:
+            trg_idx = np.where(self.state_matrix[:,mdl_lig_idx]==6)[0]
+            # we're reporting everything except disconnected error...
+            # don't ask...
+            for i in trg_idx:
+                if self.target_ligand_states[i] == 0:
+                    raise RuntimeError("This should never happen")
+                if self.target_ligand_states[i] != 4:
+                    return self.state_decoding[self.target_ligand_states[i]]
+
+        # get rid of remaining single ligand issues (only disconnected error)
+        if 6 in tmp and len(tmp) > 1:
+            tmp = tmp[tmp!=6]
+
+        # prefer everything over identity state
+        if 1 in tmp and len(tmp) > 1:
+            tmp = tmp[tmp!=1]
+
+        # just return whatever is left
+        return self.state_decoding[tmp[0]]
+
+    @property
+    def unassigned_model_ligands_reasons(self):
+        return_dict = dict()
+        for i in self.unassigned_model_ligands:
+            lig = self.model_ligands[i]
+            cname = lig.GetChain().GetName()
+            rnum = lig.GetNumber()
+            if cname not in return_dict:
+                return_dict[cname] = dict()
+            return_dict[cname][rnum] = \
+            self.guess_model_ligand_unassigned_reason(i)[0]
+        return return_dict
+    
+    @property
+    def unassigned_target_ligands_reasons(self):
+        return_dict = dict()
+        for i in self.unassigned_target_ligands:
+            lig = self.target_ligands[i]
+            cname = lig.GetChain().GetName()
+            rnum = lig.GetNumber()
+            if cname not in return_dict:
+                return_dict[cname] = dict()
+            return_dict[cname][rnum] = \
+            self.guess_target_ligand_unassigned_reason(i)[0]
+        return return_dict
 
     @property
     def _chain_mapper(self):
@@ -629,23 +825,58 @@ class LigandScorer:
         shape = (len(self.target_ligands), len(self.model_ligands))
         self._score_matrix = np.full(shape, np.nan, dtype=np.float32)
         self._coverage_matrix = np.full(shape, np.nan, dtype=np.float32)
-        self._state_matrix = np.full(shape, -1, dtype=np.int32)
+        self._state_matrix = np.full(shape, 0, dtype=np.int32)
+        self._model_ligand_states = np.zeros(len(self.model_ligands))
+        self._target_ligand_states = np.zeros(len(self.target_ligands))
         self._aux_matrix = np.empty(shape, dtype=dict)
 
+        # precompute ligand graphs
+        target_graphs = [_ResidueToGraph(l, by_atom_index=True) for l in self.target_ligands]
+        model_graphs = [_ResidueToGraph(l, by_atom_index=True) for l in self.model_ligands]
+
+        for g_idx, g in enumerate(target_graphs):
+            if not networkx.is_connected(g):
+                self._target_ligand_states[g_idx] = 4
+                self._state_matrix[g_idx,:] = 6
+                LogVerbose("Disconnected graph observed for target ligand %s" % (
+                        str(self.target_ligands[g_idx])))
+
+        for g_idx, g in enumerate(model_graphs):
+            if not networkx.is_connected(g):
+                self._model_ligand_states[g_idx] = 4
+                self._state_matrix[:,g_idx] = 6
+                LogVerbose("Disconnected graph observed for model ligand %s" % (
+                        str(self.model_ligands[g_idx])))
+
+
         for target_id, target_ligand in enumerate(self.target_ligands):
             LogVerbose("Analyzing target ligand %s" % target_ligand)
+
+            if self._target_ligand_states[target_id] == 4:
+                # Disconnected graph - already updated states and reported
+                # to LogVerbose
+                continue 
+
             for model_id, model_ligand in enumerate(self.model_ligands):
                 LogVerbose("Compare to model ligand %s" % model_ligand)
 
                 #########################################################
                 # Compute symmetries for given target/model ligand pair #
                 #########################################################
+
+                if self._model_ligand_states[model_id] == 4:
+                    # Disconnected graph - already updated states and reported
+                    # to LogVerbose
+                    continue 
+
                 try:
                     symmetries = ComputeSymmetries(
                         model_ligand, target_ligand,
                         substructure_match=self.substructure_match,
                         by_atom_index=True,
-                        max_symmetries=self.max_symmetries)
+                        max_symmetries=self.max_symmetries,
+                        model_graph = model_graphs[model_id],
+                        target_graph = target_graphs[target_id])
                     LogVerbose("Ligands %s and %s symmetry match" % (
                         str(model_ligand), str(target_ligand)))
                 except NoSymmetryError:
@@ -667,38 +898,57 @@ class LigandScorer:
                     self._state_matrix[target_id, model_id] = 3
                     continue
                 except DisconnectedGraphError:
+                    # this should never happen as we guard against
+                    # DisconnectedGraphError when precomputing the graph
                     LogVerbose("Disconnected graph observed for %s and %s" % (
                         str(model_ligand), str(target_ligand)))
-                    self._state_matrix[target_id, model_id] = 4
+                    # just set both ligand states to 4
+                    self._model_ligand_states[model_id] = 4
+                    self._model_ligand_states[target_id] = 4
+                    self._state_matrix[target_id, model_id] = 6
                     continue
 
                 #####################################################
                 # Compute score by calling the child class _compute #
                 #####################################################
-                score, state, aux = self._compute(symmetries, target_ligand,
-                                                  model_ligand)
+                score, pair_state, target_ligand_state, model_ligand_state, aux\
+                 = self._compute(symmetries, target_ligand, model_ligand)
 
                 ############
                 # Finalize #
                 ############
-                if state != 0:
-                    # non-zero error states up to 9 are reserved for base class
-                    if state <= 9:
-                        raise RuntimeError("Child returned reserved err. state")
-
-                    # Ensure that returned state is associated with a
-                    # description. This is a requirement when subclassing
-                    # LigandScorer => state_decoding dict from base class must
-                    # be modified in subclass constructor
-                    if state not in self.state_decoding:
-                        raise RuntimeError(f"Subclass returned state "
-                                           f"\"{state}\" for which no "
-                                           f"description is available. Point "
-                                           f"the developer of the used scorer "
-                                           f"to this error message.")
-
-                self._state_matrix[target_id, model_id] = state
-                if state == 0:
+
+                # Ensure that returned states are associated with a
+                # description. This is a requirement when subclassing
+                # LigandScorer => state_decoding dict from base class must
+                # be modified in subclass constructor
+                if pair_state not in self.state_decoding or \
+                   target_ligand_state not in self.state_decoding or \
+                   model_ligand_state not in self.state_decoding:
+                    raise RuntimeError(f"Subclass returned state "
+                                       f"\"{state}\" for which no "
+                                       f"description is available. Point "
+                                       f"the developer of the used scorer "
+                                       f"to this error message.")
+
+                # if any of the ligand states is non-zero, this must be marked
+                # by the scorer subclass by specifying a specific pair state
+                if target_ligand_state != 0 and pair_state != 6:
+                    raise RuntimeError("Observed non-zero target-ligand "
+                                       "state which must trigger certain "
+                                       "pair state. Point the developer of "
+                                       "the used scorer to this error message")
+
+                if model_ligand_state != 0 and pair_state != 6:
+                    raise RuntimeError("Observed non-zero model-ligand "
+                                       "state which must trigger certain "
+                                       "pair state. Point the developer of "
+                                       "the used scorer to this error message")
+
+                self._state_matrix[target_id, model_id] = pair_state
+                self._target_ligand_states[target_id] = target_ligand_state
+                self._model_ligand_states[model_id] = model_ligand_state
+                if pair_state == 0:
                     if score is None or np.isnan(score):
                         raise RuntimeError("LigandScorer returned invalid "
                                            "score despite 0 error state")
@@ -784,7 +1034,8 @@ def _ResidueToGraph(residue, by_atom_index=False):
 
 def ComputeSymmetries(model_ligand, target_ligand, substructure_match=False,
                       by_atom_index=False, return_symmetries=True,
-                      max_symmetries=1e6):
+                      max_symmetries=1e6, model_graph = None,
+                      target_graph = None):
     """Return a list of symmetries (isomorphisms) of the model onto the target
     residues.
 
@@ -820,11 +1071,18 @@ def ComputeSymmetries(model_ligand, target_ligand, substructure_match=False,
     """
 
     # Get the Graphs of the ligands
-    model_graph = _ResidueToGraph(model_ligand, by_atom_index=by_atom_index)
-    target_graph = _ResidueToGraph(target_ligand, by_atom_index=by_atom_index)
+    if model_graph is None:
+        model_graph = _ResidueToGraph(model_ligand,
+                                      by_atom_index=by_atom_index)
 
     if not networkx.is_connected(model_graph):
         raise DisconnectedGraphError("Disconnected graph for model ligand %s" % model_ligand)
+
+
+    if target_graph is None:
+        target_graph = _ResidueToGraph(target_ligand,
+                                       by_atom_index=by_atom_index)
+
     if not networkx.is_connected(target_graph):
         raise DisconnectedGraphError("Disconnected graph for target ligand %s" % target_ligand)
 
diff --git a/modules/mol/alg/pymod/ligand_scoring_lddtpli.py b/modules/mol/alg/pymod/ligand_scoring_lddtpli.py
index 7ddd8f5eea7676a2ca4706b86f473f51764cfd63..5566253611d860f3adccf2cf3b05cb5ac0f3b9ba 100644
--- a/modules/mol/alg/pymod/ligand_scoring_lddtpli.py
+++ b/modules/mol/alg/pymod/ligand_scoring_lddtpli.py
@@ -153,18 +153,23 @@ class LDDTPLIScorer(ligand_scoring_base.LigandScorer):
                                                     target_ligand,
                                                     model_ligand)
 
-        state = 0
+        pair_state = 0
         score = result["lddt_pli"]
 
         if score is None or np.isnan(score):
             if result["lddt_pli_n_contacts"] == 0:
             	# it's a space ship!
-                state = 10
+                pair_state = 10
             else:
             	# unknwon error state
-                state = 20
+                pair_state = 20
 
-        return (score, state, result)
+        # the ligands get a zero-state...
+        target_ligand_state = 0
+        model_ligand_state = 0
+
+        return (score, pair_state, target_ligand_state, model_ligand_state,
+                result)
 
     def _score_dir(self):
         """ Implements interface from parent
diff --git a/modules/mol/alg/pymod/ligand_scoring_scrmsd.py b/modules/mol/alg/pymod/ligand_scoring_scrmsd.py
index e300301e368ad4144ef44f91db0bb9678dce9ab4..19c51a5da134fe9ea03a8ae0f769f0fda73f5b03 100644
--- a/modules/mol/alg/pymod/ligand_scoring_scrmsd.py
+++ b/modules/mol/alg/pymod/ligand_scoring_scrmsd.py
@@ -193,19 +193,23 @@ class SCRMSDScorer(ligand_scoring_base.LigandScorer):
                                     "transform": r.transform,
                                     "inconsistent_residues": r.inconsistent_residues}
 
+        target_ligand_state = 0
+        model_ligand_state = 0
+        pair_state = 0
+
         if best_rmsd_result["rmsd"] is not None:
         	best_rmsd = best_rmsd_result["rmsd"]
-        	error_state = 0
         else:
             # try to identify error states
             best_rmsd = np.nan
             error_state = 20 # unknown error
             if self._get_target_binding_site(target_ligand).GetResidueCount() == 0:
-                error_state = 10 # binding_site
+                pair_state = 6 # binding_site
+                target_ligand_state = 10
             elif len(representations) == 0:
-                error_state = 11 # model_representation
+                pair_state = 11
 
-        return (best_rmsd, error_state, best_rmsd_result)
+        return (best_rmsd, pair_state, target_ligand_state, model_ligand_state, best_rmsd_result)
 
     def _score_dir(self):
         """ Implements interface from parent
diff --git a/modules/mol/alg/tests/test_ligand_scoring.py b/modules/mol/alg/tests/test_ligand_scoring.py
index 1bd7971c9237fa3264b38a9c03fd3bbd6b38cb84..55d1cb2665638b7c71dadc210cca9abd6913f97d 100644
--- a/modules/mol/alg/tests/test_ligand_scoring.py
+++ b/modules/mol/alg/tests/test_ligand_scoring.py
@@ -507,10 +507,11 @@ class TestLigandScoring(unittest.TestCase):
         mdl_ed.UpdateICS()
         trg_ed.UpdateICS()
 
+        print("1")
         sc = LigandScorer(mdl, trg, None, None, unassigned=True,
                           full_bs_search=True,
                           add_mdl_contacts=False)
-
+        print("done")
         # Check unassigned targets
         # NA: not in contact with target
         trg_na = sc.target.FindResidue("L_NA", 1)
diff --git a/modules/mol/alg/tests/test_ligand_scoring_fancy.py b/modules/mol/alg/tests/test_ligand_scoring_fancy.py
index 15000f5188772a96f7d8abd89c6a944481621f6e..934ef783407872ce57f5e4785505367a3d062f11 100644
--- a/modules/mol/alg/tests/test_ligand_scoring_fancy.py
+++ b/modules/mol/alg/tests/test_ligand_scoring_fancy.py
@@ -629,13 +629,18 @@ class TestLigandScoringFancy(unittest.TestCase):
         self.assertEqual(sc.unassigned_target_ligands, [0])
         self.assertEqual(sc.unassigned_model_ligands, [0])
 
-        trg_report = sc.get_target_ligand_state_report(0)
-        mdl_report = sc.get_model_ligand_state_report(0)
+        trg_report, trg_pair_report = sc.get_target_ligand_state_report(0)
+        mdl_report, mdl_pair_report = sc.get_model_ligand_state_report(0)
 
-        self.assertEqual(len(trg_report), 1)
-        self.assertEqual(len(mdl_report), 1)
-        self.assertEqual(trg_report[0]["short desc"], "symmetries")
-        self.assertEqual(mdl_report[0]["short desc"], "symmetries")
+        # the individual ligands are OK
+        self.assertEqual(trg_report["short desc"], "OK")
+        self.assertEqual(mdl_report["short desc"], "OK")
+
+        # but there are too many symmetries
+        self.assertEqual(len(trg_pair_report), 1)
+        self.assertEqual(len(mdl_pair_report), 1)
+        self.assertEqual(trg_pair_report[0]["short desc"], "symmetries")
+        self.assertEqual(mdl_pair_report[0]["short desc"], "symmetries")
 
     def test_no_binding_site(self):
         """
@@ -665,18 +670,24 @@ class TestLigandScoringFancy(unittest.TestCase):
 
         self.assertTrue(np.isnan(sc.score_matrix[0, 3]))
 
-        trg_report = sc.get_target_ligand_state_report(0)
+        trg_report, trg_pair_report = sc.get_target_ligand_state_report(0)
+
+        exp_lig_report = {'state': 10.0,
+                          'short desc': 'binding_site',
+                          'desc': 'No residues were in proximity of the target ligand.'}
 
-        expected_report = [{'state': 1, 'short desc': 'identity',
+        exp_pair_report = [{'state': 1, 'short desc': 'identity',
                             'desc': 'Ligands could not be matched (by subgraph isomorphism)',
                             'indices': [0, 1, 2, 4, 5, 6]},
-                           {'state': 10, 'short desc': 'binding_site',
-                            'desc': 'No residues were in proximity of the target ligand.',
+                           {'state': 6, 'short desc': 'single_ligand_issue',
+                            'desc': 'Cannot compute valid pairwise score as either model or target ligand have non-zero state.',
                             'indices': [3]}]
 
-        # order of report is fix 
-        self.assertDictEqual(trg_report[0], expected_report[0])
-        self.assertDictEqual(trg_report[1], expected_report[1])
+        # order of report is fix
+        self.assertDictEqual(trg_report, exp_lig_report)
+        self.assertDictEqual(trg_pair_report[0], exp_pair_report[0])
+        self.assertDictEqual(trg_pair_report[1], exp_pair_report[1])
+
 
     def test_no_lddt_pli_contact(self):
         """
@@ -717,12 +728,12 @@ class TestLigandScoringFancy(unittest.TestCase):
                             'indices': [0, 1]}]
 
         # both target ligands are expected to have the same report => no_contact
-        report_1 = sc.get_target_ligand_state_report(0)
-        report_2 = sc.get_target_ligand_state_report(1)
-        self.assertEqual(len(report_1), 1)
-        self.assertEqual(len(report_2), 1)
-        self.assertDictEqual(report_1[0], expected_report[0])
-        self.assertDictEqual(report_2[0], expected_report[0])
+        report_1, report_pair_1 = sc.get_target_ligand_state_report(0)
+        report_2, report_pair_2 = sc.get_target_ligand_state_report(1)
+        self.assertEqual(len(report_pair_1), 1)
+        self.assertEqual(len(report_pair_2), 1)
+        self.assertDictEqual(report_pair_1[0], expected_report[0])
+        self.assertDictEqual(report_pair_2[0], expected_report[0])
 
 
         # However RMSD should be assigned
@@ -742,14 +753,190 @@ class TestLigandScoringFancy(unittest.TestCase):
                             'indices': [0, 1]}]
 
         # both target ligands are expected to have the same report => no_contact
-        report_1 = sc.get_target_ligand_state_report(0)
-        report_2 = sc.get_target_ligand_state_report(1)
-        self.assertEqual(len(report_1), 1)
-        self.assertEqual(len(report_2), 1)
-        self.assertDictEqual(report_1[0], expected_report[0])
-        self.assertDictEqual(report_2[0], expected_report[0])
-
+        report_1, report_pair_1 = sc.get_target_ligand_state_report(0)
+        report_2, report_pair_2 = sc.get_target_ligand_state_report(1)
+        self.assertEqual(len(report_pair_1), 1)
+        self.assertEqual(len(report_pair_2), 1)
+        self.assertDictEqual(report_pair_1[0], expected_report[0])
+        self.assertDictEqual(report_pair_2[0], expected_report[0])
+
+    def test_unassigned_reasons(self):
+        """Test reasons for being unassigned."""
+        trg = _LoadMMCIF("1r8q.cif.gz")
+        mdl = _LoadMMCIF("P84080_model_02.cif.gz")
 
+        def _AppendResidueWithBonds(ed, chain, old_res):
+            new_res = ed.AppendResidue(chain, old_res.name)
+            for old_atom in old_res.atoms:
+                ed.InsertAtom(new_res, old_atom.name, old_atom.pos, old_atom.element,
+                              old_atom.occupancy, old_atom.b_factor, old_atom.is_hetatom)
+            for old_bond in old_atom.bonds:
+                new_first = new_res.FindAtom(old_bond.first.name)
+                new_second = new_res.FindAtom(old_bond.second.name)
+                ed.Connect(new_first, new_second)
+            return new_res
+
+        # Add interesting ligands to model and target
+        mdl_ed = mdl.EditXCS()
+        trg_ed = trg.EditXCS()
+
+        # Add ZN: representation in the model (chain missing in model)
+        new_chain = mdl_ed.InsertChain("L_ZN")
+        mdl_ed.SetChainType(new_chain, mol.ChainType.CHAINTYPE_NON_POLY)
+        new_res = _AppendResidueWithBonds(mdl_ed, new_chain, trg.Select("rname=ZN").residues[0].handle)
+        new_res.is_ligand = True
+
+        # Add NA: not in contact with target
+        new_chain = trg_ed.InsertChain("L_NA")
+        trg_ed.SetChainType(new_chain, mol.ChainType.CHAINTYPE_NON_POLY)
+        new_res = trg_ed.AppendResidue(new_chain, "NA")
+        new_atom = trg_ed.InsertAtom(new_res, "NA", geom.Vec3(100, 100, 100), "NA")
+        new_res.is_ligand = True
+        new_chain = mdl_ed.InsertChain("L_NA")
+        mdl_ed.SetChainType(new_chain, mol.ChainType.CHAINTYPE_NON_POLY)
+        new_res = mdl_ed.AppendResidue(new_chain, "NA")
+        new_atom = mdl_ed.InsertAtom(new_res, "NA", geom.Vec3(100, 100, 100), "NA")
+        new_res.is_ligand = True
+
+        # Add OXY: no symmetry/ not identical -
+        new_chain = mdl_ed.InsertChain("L_OXY")
+        mdl_ed.SetChainType(new_chain, mol.ChainType.CHAINTYPE_NON_POLY)
+        new_res = mdl_ed.AppendResidue(new_chain, "OXY")
+        new_atom1 = mdl_ed.InsertAtom(new_res, "O1", geom.Vec3(0, 0, 0), "O")
+        new_atom2 = mdl_ed.InsertAtom(new_res, "O2", geom.Vec3(1, 1, 1), "O")
+        mdl_ed.Connect(new_atom1, new_atom2)
+        new_res.is_ligand = True
+
+        # Add CMO: disconnected
+        new_chain = mdl_ed.InsertChain("L_CMO")
+        mdl_ed.SetChainType(new_chain, mol.ChainType.CHAINTYPE_NON_POLY)
+        new_res = mdl_ed.AppendResidue(new_chain, "CMO")
+        new_atom1 = mdl_ed.InsertAtom(new_res, "O", geom.Vec3(0, 0, 0), "O")
+        new_atom2 = mdl_ed.InsertAtom(new_res, "C", geom.Vec3(1, 1, 1), "O")
+        new_res.is_ligand = True
+        new_chain = trg_ed.InsertChain("L_CMO")
+        trg_ed.SetChainType(new_chain, mol.ChainType.CHAINTYPE_NON_POLY)
+        new_res = trg_ed.AppendResidue(new_chain, "CMO")
+        new_atom1 = trg_ed.InsertAtom(new_res, "O", geom.Vec3(0, 0, 0), "O")
+        new_atom2 = trg_ed.InsertAtom(new_res, "C", geom.Vec3(1, 1, 1), "O")
+        new_res.is_ligand = True
+
+        # Add 3 MG in model: assignment/stoichiometry
+        mg_pos = [
+            geom.Vec3(3.871, 12.343, 44.485),
+            geom.Vec3(3.871, 12.343, 44.485) + 1,
+            geom.Vec3(3.871, 12.343, 44.485) + 100
+        ]
+        for i in range(3):
+            new_chain = mdl_ed.InsertChain("L_MG_%d" % i)
+            mdl_ed.SetChainType(new_chain, mol.ChainType.CHAINTYPE_NON_POLY)
+            new_res = mdl_ed.AppendResidue(new_chain, "MG")
+            new_atom = mdl_ed.InsertAtom(new_res, "MG", mg_pos[i], "MG")
+            new_res.is_ligand = True
+
+        mdl_ed.UpdateICS()
+        trg_ed.UpdateICS()
+
+        sc = ligand_scoring_lddtpli.LDDTPLIScorer(mdl, trg, None, None)
+
+        # Check unassigned targets
+        # NA: not in contact with target
+        trg_na = sc.target.FindResidue("L_NA", 1)
+        self.assertEqual(sc.unassigned_target_ligands_reasons["L_NA"][1], "no_contact")
+        # AFB: not identical to anything in the model
+        trg_afb = sc.target.FindResidue("G", 1)
+        self.assertEqual(sc.unassigned_target_ligands_reasons["G"][1], "identity")
+        # F.G3D1: J.G3D1 assigned instead
+        trg_fg3d1 = sc.target.FindResidue("F", 1)
+        self.assertEqual(sc.unassigned_target_ligands_reasons["F"][1], "stoichiometry")
+        # CMO: disconnected
+        trg_cmo1 = sc.target.FindResidue("L_CMO", 1)
+        self.assertEqual(sc.unassigned_target_ligands_reasons["L_CMO"][1], "disconnected")
+        # J.G3D1: assigned to L_2.G3D1 => check if it is assigned
+        self.assertTrue(5 not in sc.unassigned_target_ligands)
+        self.assertNotIn("J", sc.unassigned_target_ligands_reasons)
+
+        # Check unassigned models
+        # OXY: not identical to anything in the model
+        mdl_oxy = sc.model.FindResidue("L_OXY", 1)
+        self.assertEqual(sc.unassigned_model_ligands_reasons["L_OXY"][1], "identity")
+        self.assertTrue("L_OXY" not in sc.score)
+        # NA: not in contact with target
+        mdl_na = sc.model.FindResidue("L_NA", 1)
+        self.assertEqual(sc.unassigned_model_ligands_reasons["L_NA"][1], "no_contact")
+        self.assertTrue("L_NA" not in sc.score)
+ 
+        # MG in L_MG_2 has stupid coordinates and is not assigned
+        mdl_mg_2 = sc.model.FindResidue("L_MG_2", 1)
+        self.assertEqual(sc.unassigned_model_ligands_reasons["L_MG_2"][1], "stoichiometry")
+        self.assertTrue("L_MG_2" not in sc.score)
+
+        self.assertNotIn("L_MG_0", sc.unassigned_model_ligands_reasons)
+        # CMO: disconnected
+        mdl_cmo1 = sc.model.FindResidue("L_CMO", 1)
+        self.assertEqual(sc.unassigned_model_ligands_reasons["L_CMO"][1], "disconnected")
+
+        # Should work with rmsd_assignment too
+        sc = ligand_scoring_scrmsd.SCRMSDScorer(mdl, trg, None, None,
+                                                full_bs_search=True)
+
+
+
+        self.assertDictEqual(sc.unassigned_model_ligands_reasons, {
+            'L_ZN': {1: 'model_representation'},
+            'L_NA': {1: 'binding_site'},
+            'L_OXY': {1: 'identity'},
+            'L_MG_2': {1: 'stoichiometry'},
+            "L_CMO": {1: 'disconnected'}
+        })
+        self.assertDictEqual(sc.unassigned_target_ligands_reasons, {
+            'G': {1: 'identity'},
+            'H': {1: 'model_representation'},
+            'J': {1: 'stoichiometry'},
+            'K': {1: 'identity'},
+            'L_NA': {1: 'binding_site'},
+            "L_CMO": {1: 'disconnected'}
+        })
+        self.assertTrue("L_OXY" not in sc.score)
+
+        # With missing ligands
+        sc = ligand_scoring_lddtpli.LDDTPLIScorer(mdl.Select("cname=A"), trg, None, None)
+        self.assertEqual(sc.unassigned_target_ligands_reasons["E"][1], 'no_ligand')
+
+        sc = ligand_scoring_lddtpli.LDDTPLIScorer(mdl, trg.Select("cname=A"), None, None)
+        self.assertEqual(sc.unassigned_model_ligands_reasons["L_2"][1], 'no_ligand')
+
+        sc = ligand_scoring_scrmsd.SCRMSDScorer(mdl.Select("cname=A"), trg, None, None)
+        self.assertEqual(sc.unassigned_target_ligands_reasons["E"][1], 'no_ligand')
+
+        sc = ligand_scoring_scrmsd.SCRMSDScorer(mdl, trg.Select("cname=A"), None, None)
+        self.assertEqual(sc.unassigned_model_ligands_reasons["L_2"][1], 'no_ligand')
+
+        # However not everything must be missing
+        with self.assertRaises(ValueError):
+            sc = LigandScorer(mdl.Select("cname=A"), trg.Select("cname=A"), None, None)
+
+        # Test with partial bs search (full_bs_search=True)
+        # Here we expect L_MG_2 to be unassigned because of stoichiometry
+        # rather than model_representation, as it no longer matters so far from
+        # the binding site.
+        sc = ligand_scoring_scrmsd.SCRMSDScorer(mdl, trg, None, None,
+                                               full_bs_search=True)
+        self.assertEqual(sc.unassigned_model_ligands_reasons, {
+            'L_ZN': {1: 'model_representation'},
+            'L_NA': {1: 'binding_site'},
+            'L_OXY': {1: 'identity'},
+            'L_MG_2': {1: 'stoichiometry'},
+            "L_CMO": {1: 'disconnected'}
+        })
+        self.assertEqual(sc.unassigned_target_ligands_reasons, {
+            'G': {1: 'identity'},
+            'H': {1: 'model_representation'},
+            'J': {1: 'stoichiometry'},
+            'K': {1: 'identity'},
+            'L_NA': {1: 'binding_site'},
+            "L_CMO": {1: 'disconnected'}
+        })
 
 if __name__ == "__main__":
     from ost import testutils