diff --git a/actions/ost-compare-structures b/actions/ost-compare-structures index 1afa5198a6182c0c2a0484eb5b55f6de7953e9b3..57ed24b902052b97f1e337baf91cbc4d6d467c35 100644 --- a/actions/ost-compare-structures +++ b/actions/ost-compare-structures @@ -433,6 +433,22 @@ def _ParseArgs(): "available as keys \"per_interface_ics_precision\", " "\"per_interface_ics_recall\" and \"per_interface_ics\".")) + parser.add_argument( + "--ics-trimmed", + dest="ics_trimmed", + default=False, + action="store_true", + help=("Computes interface contact similarity (ICS) related scores but " + "on a trimmed model. That means that a mapping between model and " + "reference is performed and all model residues without reference " + "counterpart are removed. As a consequence, model contacts for " + "which we have no experimental evidence do not affect the score. " + "The effect of these added model contacts without mapping to " + "target are decreased precision and thus lower ics. Recall is " + "not affected. Enabling this flag adds the following keys: " + "\"ics_trimmed\", \"ics_precision_trimmed\", " + "\"ics_recall_trimmed\" and \"model_contacts_trimmed\"")) + parser.add_argument( "--ips", dest="ips", @@ -460,6 +476,12 @@ def _ParseArgs(): "available as keys \"per_interface_ips_precision\", " "\"per_interface_ips_recall\" and \"per_interface_ips\".")) + parser.add_argument( + "--ips-trimmed", + dest="ips_trimmed", + default=False, + action="store_true", + help=("The IPS equivalent of ICS on trimmed models.")) parser.add_argument( "--rigid-scores", @@ -910,6 +932,19 @@ def _Process(model, reference, args, model_format, reference_format): out["per_interface_ips"] = \ [_RoundOrNone(x) for x in scorer.per_interface_ips] + if args.ics_trimmed or args.ips_trimmed: + out["model_contacts_trimmed"] = scorer.trimmed_model_contacts + + if args.ics_trimmed: + out["ics_trimmed"] = _RoundOrNone(scorer.ics_trimmed) + out["ics_precision_trimmed"] = _RoundOrNone(scorer.ics_precision_trimmed) + out["ics_recall_trimmed"] = _RoundOrNone(scorer.ics_recall_trimmed) + + if args.ips_trimmed: + out["ips_trimmed"] = _RoundOrNone(scorer.ips_trimmed) + out["ips_precision_trimmed"] = _RoundOrNone(scorer.ips_precision_trimmed) + out["ips_recall_trimmed"] = _RoundOrNone(scorer.ips_recall_trimmed) + if args.dockq: out["dockq_reference_interfaces"] = scorer.dockq_target_interfaces out["dockq_interfaces"] = scorer.dockq_interfaces diff --git a/modules/mol/alg/pymod/scoring.py b/modules/mol/alg/pymod/scoring.py index 7445c1f2b1d2a1a765079d56b105a067e08d8b52..d1ec0705db3b7599c2eacfaa2e78b298ed95f84f 100644 --- a/modules/mol/alg/pymod/scoring.py +++ b/modules/mol/alg/pymod/scoring.py @@ -326,6 +326,7 @@ class Scorer: self._target_clashes = None self._target_bad_bonds = None self._target_bad_angles = None + self._trimmed_model = None self._chain_mapper = None self._mapping = None self._rigid_mapping = None @@ -334,12 +335,14 @@ class Scorer: self._aln = None self._stereochecked_aln = None self._pepnuc_aln = None + self._trimmed_aln = None # lazily constructed scorer objects self._lddt_scorer = None self._bb_lddt_scorer = None self._qs_scorer = None self._contact_scorer = None + self._trimmed_contact_scorer = None # lazily computed scores self._lddt = None @@ -361,6 +364,7 @@ class Scorer: self._contact_model_interfaces = None self._native_contacts = None self._model_contacts = None + self._trimmed_model_contacts = None self._ics_precision = None self._ics_recall = None self._ics = None @@ -374,6 +378,16 @@ class Scorer: self._per_interface_ics_recall = None self._per_interface_ics = None + # subset of contact scores that operate on trimmed model + # i.e. no contacts from model residues that are not present in + # target + self._ics_trimmed = None + self._ics_precision_trimmed = None + self._ics_recall_trimmed = None + self._ips_trimmed = None + self._ips_precision_trimmed = None + self._ips_recall_trimmed = None + self._dockq_target_interfaces = None self._dockq_interfaces = None self._fnat = None @@ -502,6 +516,19 @@ class Scorer: self._compute_pepnuc_aln() return self._pepnuc_aln + @property + def trimmed_aln(self): + """ Alignments of :attr:`trimmed_model`/:attr:`target` chains + + Alignments for each pair of chains mapped in :attr:`mapping`. + First sequence is target sequence, second sequence the model sequence. + + :type: :class:`list` of :class:`ost.seq.AlignmentHandle` + """ + if self._trimmed_aln is None: + self._trim_model() + return self._trimmed_aln + @property def stereochecked_model(self): """ View of :attr:`~model` that has stereochemistry checks applied @@ -592,6 +619,20 @@ class Scorer: self._do_stereochecks() return self._target_bad_angles + @property + def trimmed_model(self): + """ :attr:`model` trimmed to target + + Removes residues that are not covered by :class:`target` given + :attr:`mapping`. In other words: no model residues without experimental + evidence from :class:`target`. + + :type: :class:`ost.mol.EntityView` + """ + if self._trimmed_model is None: + self._trim_model() + return self._trimmed_model + @property def chain_mapper(self): """ Chain mapper object for given :attr:`target` @@ -713,6 +754,14 @@ class Scorer: self._contact_scorer = ContactScorer.FromMappingResult(self.mapping) return self._contact_scorer + @property + def trimmed_contact_scorer(self): + if self._trimmed_contact_scorer is None: + self._trimmed_contact_scorer = ContactScorer(self.mapping.target, + self.mapping.chem_groups, + self.trimmed_model, + self.trimmed_aln) + return self._trimmed_contact_scorer @property def lddt(self): @@ -928,12 +977,20 @@ class Scorer: @property def model_contacts(self): - """ Same for model + """ Same for :attr:`model` """ if self._model_contacts is None: self._model_contacts = self.contact_scorer.cent2.hr_contacts return self._model_contacts + @property + def trimmed_model_contacts(self): + """ Same for :attr:`trimmed_model` + """ + if self._trimmed_model_contacts is None: + self._trimmed_model_contacts = self.trimmed_contact_scorer.cent2.hr_contacts + return self._trimmed_model_contacts + @property def contact_target_interfaces(self): """ Interfaces in :class:`target` which have at least one contact @@ -1076,6 +1133,90 @@ class Scorer: self._compute_ips_scores() return self._ips + @property + def ics_trimmed(self): + """ Same as :attribute:`ics` but with trimmed model + + Model is trimmed to residues which can me mapped to target in order + to not penalize contacts in the model for which we have no experimental + evidence. + + :type: :class:`float` + """ + if self._ics_trimmed is None: + self._compute_ics_scores_trimmed() + return self._ics_trimmed + + @property + def ics_precision_trimmed(self): + """ Same as :attribute:`ics_precision` but with trimmed model + + Model is trimmed to residues which can me mapped to target in order + to not penalize contacts in the model for which we have no experimental + evidence. + + :type: :class:`float` + """ + if self._ics_precision_trimmed is None: + self._compute_ics_scores_trimmed() + return self._ics_precision_trimmed + + @property + def ics_recall_trimmed(self): + """ Same as :attribute:`ics_recall` but with trimmed model + + Model is trimmed to residues which can me mapped to target in order + to not penalize contacts in the model for which we have no experimental + evidence. + + :type: :class:`float` + """ + if self._ics_recall_trimmed is None: + self._compute_ics_scores_trimmed() + return self._ics_recall_trimmed + + @property + def ips_trimmed(self): + """ Same as :attribute:`ips` but with trimmed model + + Model is trimmed to residues which can me mapped to target in order + to not penalize contacts in the model for which we have no experimental + evidence. + + :type: :class:`float` + """ + if self._ips_trimmed is None: + self._compute_ips_scores_trimmed() + return self._ips_trimmed + + @property + def ips_precision_trimmed(self): + """ Same as :attribute:`ips_precision` but with trimmed model + + Model is trimmed to residues which can me mapped to target in order + to not penalize contacts in the model for which we have no experimental + evidence. + + :type: :class:`float` + """ + if self._ips_precision_trimmed is None: + self._compute_ips_scores_trimmed() + return self._ips_precision_trimmed + + @property + def ips_recall_trimmed(self): + """ Same as :attribute:`ips_recall` but with trimmed model + + Model is trimmed to residues which can me mapped to target in order + to not penalize contacts in the model for which we have no experimental + evidence. + + :type: :class:`float` + """ + if self._ips_recall_trimmed is None: + self._compute_ips_scores_trimmed() + return self._ips_recall_trimmed + @property def per_interface_ips_precision(self): """ Per-interface IPS precision @@ -1089,7 +1230,6 @@ class Scorer: self._compute_ips_scores() return self._per_interface_ips_precision - @property def per_interface_ips_recall(self): """ Per-interface IPS recall @@ -2081,6 +2221,51 @@ class Scorer: self._per_interface_ics_recall.append(None) self._per_interface_ics.append(None) + def _trim_model(self): + trimmed_mdl = mol.CreateEntityFromView(self.mapping.model, True) + trimmed_aln = dict() + + for trg_cname, mdl_cname in self.mapping.GetFlatMapping().items(): + mdl_ch = trimmed_mdl.FindChain(mdl_cname) + aln = self.mapping.alns[(trg_cname, mdl_cname)] + + # some limited test that stuff matches + assert(mdl_ch.GetResidueCount() == \ + len(aln.GetSequence(1).GetGaplessString())) + + mdl_residues = mdl_ch.residues + mdl_res_idx = 0 + aligned_mdl_seq = ['-'] * aln.GetLength() + for col_idx, col in enumerate(aln): + if col[0] != '-' and col[1] != '-': + mdl_res = mdl_residues[mdl_res_idx] + mdl_res.SetIntProp("aligned", 1) + aligned_mdl_seq[col_idx] = col[1] + if col[1] != '-': + mdl_res_idx += 1 + aligned_mdl_seq = ''.join(aligned_mdl_seq) + aligned_mdl_seq = seq.CreateSequence(mdl_cname, aligned_mdl_seq) + + new_aln = seq.CreateAlignment() + new_aln.AddSequence(aln.GetSequence(0)) + new_aln.AddSequence(aligned_mdl_seq) + trimmed_aln[(trg_cname, mdl_cname)] = new_aln + + self._trimmed_model = trimmed_mdl.Select("graligned:0=1") + self._trimmed_aln = trimmed_aln + + def _compute_ics_scores_trimmed(self): + LogScript("Computing ICS scores trimmed") + + # this is an ugly hack without any efficiency in mind + # we're simply taking the entities from mapper and construct + # a new contact scorer from scratch + + contact_scorer_res = self.trimmed_contact_scorer.ScoreICS(self.mapping.mapping) + self._ics_trimmed = contact_scorer_res.ics + self._ics_precision_trimmed = contact_scorer_res.precision + self._ics_recall_trimmed = contact_scorer_res.recall + def _compute_ips_scores(self): LogScript("Computing IPS scores") contact_scorer_res = self.contact_scorer.ScoreIPS(self.mapping.mapping) @@ -2108,6 +2293,17 @@ class Scorer: self._per_interface_ips_recall.append(None) self._per_interface_ips.append(None) + def _compute_ips_scores_trimmed(self): + LogScript("Computing IPS scores trimmed") + + # this is an ugly hack without any efficiency in mind + # we're simply taking the entities from mapper and construct + # a new contact scorer from scratch + contact_scorer_res = self.trimmed_contact_scorer.ScoreIPS(self.mapping.mapping) + self._ips_precision_trimmed = contact_scorer_res.precision + self._ips_recall_trimmed = contact_scorer_res.recall + self._ips_trimmed = contact_scorer_res.ips + def _compute_dockq_scores(self): LogScript("Computing DockQ")