diff --git a/modules/mol/alg/doc/molalg.rst b/modules/mol/alg/doc/molalg.rst index 3f44eae5de575b507c08cc692be5d8bd77e48bf4..4eb0d9ea963021227b0583fabf152eac649edf2b 100644 --- a/modules/mol/alg/doc/molalg.rst +++ b/modules/mol/alg/doc/molalg.rst @@ -143,6 +143,10 @@ Local Distance Test scores (lDDT, DRMSD) .. autoclass:: ost.mol.alg.scoring.lDDTBSScorer :members: +.. autoclass:: ost.mol.alg.scoring.Scorer + :members: + :member-order: bysource + .. currentmodule:: ost.mol.alg diff --git a/modules/mol/alg/pymod/scoring.py b/modules/mol/alg/pymod/scoring.py index a012692667ab34881f472562be81d95841b2bf5c..11cdb06939c6f7c05b2a8e035e07aaa1f7c638a5 100644 --- a/modules/mol/alg/pymod/scoring.py +++ b/modules/mol/alg/pymod/scoring.py @@ -1,8 +1,18 @@ from ost import mol from ost import seq from ost import io +from ost import conop +from ost import settings +from ost import geom from ost.mol.alg import lddt +from ost.mol.alg import qsscore from ost.mol.alg import chain_mapping +from ost.mol.alg.lddt import lDDTScorer +from ost.mol.alg.qsscore import QSScorer +from ost.io import ReadStereoChemicalPropsFile +from ost.mol.alg import CheckStructure, Molck, MolckSettings +from ost.bindings import dockq +import numpy as np class lDDTBSScorer: """Scorer specific for a reference/model pair @@ -70,3 +80,756 @@ class lDDTBSScorer: return bs_repr[0] else: return None + + +class Scorer: + """ Helper class to access the various scores available from ost.mol.alg + + Deals with structure cleanup, chain mapping, interface identification etc. + Intermediate results are available as attributes. + + :param model: Model structure - a deep copy is available as :attr:`model`. + Additionally, :func:`ost.mol.alg.Molck` using *molck_settings* + is applied. + :type model: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView` + :param target: Target structure - a deep copy is available as :attr:`target`. + Additionally, :func:`ost.mol.alg.Molck` using *molck_settings* + is applied. + :type target: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView` + :param resnum_alignments: Whether alignments between chemically equivalent + chains in *model* and *target* can be computed + based on residue numbers. This can be assumed in + benchmarking setups such as CAMEO/CASP. + :type resnum_alignments: :class:`bool` + :param molck_settings: Settings used for Molck on *model* and *target*, if + set to None, a default object is constructed by + setting everything except rm_zero_occ_atoms and + colored to True in + :class:`ost.mol.alg.MolckSettings` constructor. + :type molck_settings: :class:`ost.mol.alg.MolckSettings` + :param naive_chain_mapping_thresh: Chain mappings for targets up to that + number of chains will be fully enumerated + to optimize for QS-score. Everything + above is treated with a heuristic. + :type naive_chain_mapping_thresh: :class:`int` + """ + def __init__(self, model, target, resnum_alignments=False, + molck_settings = None, naive_chain_mapping_thresh=12): + + if isinstance(model, mol.EntityView): + self._model = mol.CreateEntityFromView(model, False) + elif isinstance(model, mol.EntityHandle): + self._model = model.Copy() + else: + raise RuntimeError("model must be of type EntityView/EntityHandle") + + if isinstance(target, mol.EntityView): + self._target = mol.CreateEntityFromView(target, False) + elif isinstance(target, mol.EntityHandle): + self._target = target.Copy() + else: + raise RuntimeError("model must be of type EntityView/EntityHandle") + + # catch models which have empty chain names + for ch in self._model.chains: + if ch.GetName().strip() == "": + raise RuntimeError("Model chains must have valid chain names") + + # catch models with residue numbers that are not strictly increasing + # (requirement of ChainMapper) + for ch in self._model.chains: + nums = [r.GetNumber().GetNum() for r in ch.residues] + if not all(i < j for i, j in zip(nums, nums[1:])): + raise RuntimeError("Residue numbers in each model chain must " + "be strictly increasing") + + # catch targets which have empty chain names + for ch in self._target.chains: + if ch.GetName().strip() == "": + raise RuntimeError("Target chains must have valid chain names") + + # catch targets with residue numbers that are not strictly increasing + # (requirement of ChainMapper) + for ch in self._target.chains: + nums = [r.GetNumber().GetNum() for r in ch.residues] + if not all(i < j for i, j in zip(nums, nums[1:])): + raise RuntimeError("Residue numbers in each target chain must " + "be strictly increasing") + + if molck_settings is None: + molck_settings = MolckSettings(rm_unk_atoms=True, + rm_non_std=True, + rm_hyd_atoms=True, + rm_oxt_atoms=True, + rm_zero_occ_atoms=False, + colored=False, + map_nonstd_res=True, + assign_elem=True) + Molck(self._model, conop.GetDefaultLib(), molck_settings) + Molck(self._target, conop.GetDefaultLib(), molck_settings) + self.resnum_alignments = resnum_alignments + self.naive_chain_mapping_thresh = naive_chain_mapping_thresh + + # lazily evaluated attributes + self._stereochecked_model = None + self._stereochecked_target = None + self._chain_mapper = None + self._mapping = None + self._model_interface_residues = None + self._target_interface_residues = None + + # lazily constructed scorer objects + self._lddt_scorer = None + self._qs_scorer = None + + # lazily computed scores + self._lddt = None + self._local_lddt = None + + self._QS_global = None + self._QS_best = None + + self._dockq_interfaces = None + self._dockq_native_contacts = None + self._dockq_scores = None + self._dockq_nonmapped_interfaces = None + self._dockq_nonmapped_interfaces_counts = None + self._dockq_ave = None + self._dockq_wave = None + self._dockq_ave_full = None + self._dockq_wave_full = None + + self._mapped_target_pos = None + self._mapped_model_pos = None + self._transformed_mapped_model_pos = None + self._n_target_not_mapped = None + self._transform = None + self._gdtts = None + self._gdtha = None + self._rmsd = None + + @property + def model(self): + """ Model with Molck cleanup + + :type: :class:`ost.mol.EntityHandle` + """ + return self._model + + @property + def target(self): + """ Target with Molck cleanup + + :type: :class:`ost.mol.EntityHandle` + """ + return self._target + + @property + def stereochecked_model(self): + """ View of :attr:`~model` that has stereochemistry checks applied + + First, a selection for peptide/nucleotide residues is performed, + secondly peptide sidechains with stereochemical irregularities are + removed (full residue if backbone atoms are involved). Irregularities + are clashes or bond lengths/angles more than 12 standard deviations + from expected values. + + :type: :class:`ost.mol.EntityView` + """ + if self._stereochecked_model is None: + stereo_param = ReadStereoChemicalPropsFile() + self._stereochecked_model = \ + self.model.CreateFullView().Select("peptide=true or nucleotide=true") + CheckStructure(self._stereochecked_model, + stereo_param.bond_table, + stereo_param.angle_table, + stereo_param.nonbonded_table, + 12.0, 12.0) + return self._stereochecked_model + + @property + def stereochecked_target(self): + """ Same as :attr:`~stereochecked_model` for :attr:`~target` + + :type: :class:`ost.mol.EntityView` + """ + if self._stereochecked_target is None: + stereo_param = ReadStereoChemicalPropsFile() + self._stereochecked_target = \ + self.target.CreateFullView().Select("peptide=true or nucleotide=true") + CheckStructure(self._stereochecked_target, + stereo_param.bond_table, + stereo_param.angle_table, + stereo_param.nonbonded_table, + 12.0, 12.0) + return self._stereochecked_target + + @property + def chain_mapper(self): + """ Chain mapper object for given :attr:`target` + + :type: :class:`ost.mol.alg.chain_mapping.ChainMapper` + """ + if self._chain_mapper is None: + self._chain_mapper = chain_mapping.ChainMapper(self.target, + n_max_naive=1e9, + resnum_alignments=self.resnum_alignments) + return self._chain_mapper + + @property + def mapping(self): + """ Full chain mapping result for :attr:`target`/:attr:`model` + + :type: :class:`ost.mol.alg.chain_mapping.MappingResult` + """ + if self._mapping is None: + n_trg_chains = len(self.chain_mapper.target.chains) + if n_trg_chains <= self.naive_chain_mapping_thresh: + m = self.chain_mapper.GetQSScoreMapping(self.model, + strategy="naive") + else: + m = self.chain_mapper.GetQSScoreMapping(self.model, + strategy="greedy_block", + steep_opt_rate=3, + block_seed_size=5, + block_blocks_per_chem_group=6) + self._mapping = m + return self._mapping + + @property + def model_interface_residues(self): + """ Interface residues in :attr:`~model` + + Thats all residues having a contact with at least one residue from + another chain (CB-CB distance <= 8A, CA in case of Glycine) + + :type: :class:`dict` with chain names as key and and :class:`list` + with residue numbers of the respective interface residues. + """ + if self._model_interface_residues is None: + self._model_interface_residues = \ + self._get_interface_residues(self.model) + return self._model_interface_residues + + @property + def target_interface_residues(self): + """ Same as :attr:`~model_interface_residues` for :attr:`~target` + + :type: :class:`dict` with chain names as key and and :class:`list` + with residue numbers of the respective interface residues. + """ + if self._target_interface_residues is None: + self._target_interface_residues = \ + self._get_interface_residues(self.target) + return self._target_interface_residues + + @property + def lddt_scorer(self): + """ lDDT scorer for :attr:`~stereochecked_target` (default parameters) + + :type: :class:`ost.mol.alg.lddt.lDDTScorer` + """ + if self._lddt_scorer is None: + self._lddt_scorer = lDDTScorer(self.stereochecked_target) + return self._lddt_scorer + + @property + def qs_scorer(self): + """ QS scorer constructed from :attr:`~mapping` + + The scorer object is constructed with default parameters and relates to + :attr:`~model` and :attr:`~target` (no stereochecks). + + :type: :class:`ost.mol.alg.qsscore.QSScorer` + """ + if self._qs_scorer is None: + self._qs_scorer = QSScorer.FromMappingResult(self.mapping) + return self._qs_scorer + + @property + def lddt(self): + """ Global lDDT score in range [0.0, 1.0] + + Computed based on :attr:`~stereochecked_model`. In case of oligomers, + :attr:`~mapping` is used. + + :type: :class:`float` + """ + if self._lddt is None: + self._compute_lddt() + return self._lddt + + @property + def local_lddt(self): + """ Per residue lDDT scores in range [0.0, 1.0] + + Computed based on :attr:`~stereochecked_model` but scores for all + residues in :attr:`~model` are reported. If a residue has been removed + by stereochemistry checks, the respective score is set to 0.0. If a + residue is not covered by the target or is in a chain skipped by the + chain mapping procedure (happens for super short chains), the respective + score is set to None. In case of oligomers, :attr:`~mapping` is used. + + :type: :class:`float` + """ + if self._local_lddt is None: + self._compute_lddt() + return self._local_lddt + + @property + def QS_global(self): + """ Global QS-score + + Computed based on :attr:`model` using :attr:`mapping` + + :type: :class:`float` + """ + if self._QS_global is None: + self._compute_qs() + return self._QS_global + + @property + def QS_best(self): + """ Global QS-score - only computed on aligned residues + + Computed based on :attr:`model` using :attr:`mapping`. The QS-score + computation only considers contacts between residues with a mapping + between target and model. As a result, the score won't be lowered in + case of additional chains/residues in any of the structures. + + :type: :class:`float` + """ + if self._QS_best is None: + self._compute_qs() + return self._QS_best + + @property + def dockq_interfaces(self): + """ Interfaces considered in DockQ (i.e. nonzero native contacts) + + DockQ is computed on interfaces as defined by :attr:`~mapping`. + + :type: :class:`list` of :class:`tuple` with 4 elements each: + (trg_ch1, trg_ch2, mdl_ch1, mdl_ch2) + """ + if self._dockq_interfaces is None: + self._compute_dockq() + return self._dockq_interfaces + + @property + def dockq_native_contacts(self): + """ N native contacts for interfaces in :attr:`~dockq_interfaces` + + :type: :class:`list` of :class:`int` + """ + if self._dockq_native_contacts is None: + self._compute_dockq() + return self._dockq_native_contacts + + @property + def dockq_scores(self): + """ DockQ scores for interfaces in :attr:`~dockq_interfaces` + + :class:`list` of :class:`float` + """ + if self._dockq_scores is None: + self._compute_dockq() + return self._dockq_scores + + @property + def dockq_nonmapped_interfaces(self): + """ Interfaces present in target that are not mapped + + At least one of the chains is not present in target + + :type: :class:`list` of :class:`tuple` with two elements each: + (trg_ch1, trg_ch2) + """ + if self._dockq_nonmapped_interfaces is None: + self._compute_dockq() + return self._dockq_nonmapped_interfaces + + @property + def dockq_nonmapped_interfaces_counts(self): + """ Number of native contacts in :attr:`~dockq_nonmapped_interfaces` + + :type: :class:`list` of :class:`int` + """ + if self._dockq_nonmapped_interfaces_counts is None: + self._compute_dockq() + return self._dockq_nonmapped_interfaces_counts + + @property + def dockq_ave(self): + """ Average of DockQ scores in :attr:`dockq_scores` + + In its original implementation, DockQ only operates on single + interfaces. Thus the requirement to combine scores for higher order + oligomers. + + :type: :class:`float` + """ + if self._dockq_ave is None: + self._compute_dockq() + return self._dockq_ave + + @property + def dockq_wave(self): + """ Same as :attr:`dockq_ave`, weighted by :attr:`dockq_native_contacts` + + :type: :class:`float` + """ + if self._dockq_wave is None: + self._compute_dockq() + return self._dockq_wave + + @property + def dockq_ave_full(self): + """ Same as :attr:`~dockq_ave` but penalizing for missing interfaces + + Interfaces in :attr:`dockq_nonmapped_interfaces` are added as 0.0 + in average computation. + + :type: :class:`float` + """ + if self._dockq_ave_full is None: + self._compute_dockq() + return self._dockq_ave_full + + @property + def dockq_wave_full(self): + """ Same as :attr:`~dockq_ave_full`, but weighted + + Interfaces in :attr:`dockq_nonmapped_interfaces` are added as 0.0 in + average computations and the respective weights are derived from + :attr:`~dockq_nonmapped_interfaces_counts` + """ + if self._dockq_wave_full is None: + self._compute_dockq() + return self._dockq_wave_full + + @property + def mapped_target_pos(self): + """ Mapped representative positions in target + + Thats CA positions for peptide residues and C3' positions for + nucleotides. Has same length as :attr:`~mapped_model_pos` and mapping + is based on :attr:`~mapping`. + + :type: :class:`ost.geom.Vec3List` + """ + if self._mapped_target_pos is None: + self._extract_mapped_pos() + return self._mapped_target_pos + + @property + def mapped_model_pos(self): + """ Mapped representative positions in model + + Thats CA positions for peptide residues and C3' positions for + nucleotides. Has same length as :attr:`~mapped_target_pos` and mapping + is based on :attr:`~mapping`. + + :type: :class:`ost.geom.Vec3List` + """ + if self._mapped_model_pos is None: + self._extract_mapped_pos() + return self._mapped_model_pos + + @property + def transformed_mapped_model_pos(self): + """ :attr:`~mapped_model_pos` with :attr:`~transform` applied + + :type: :class:`ost.geom.Vec3List` + """ + if self._transformed_mapped_model_pos is None: + self._transformed_mapped_model_pos = \ + geom.Vec3List(self.mapped_model_pos) + self._transformed_mapped_model_pos.ApplyTransform(self.transform) + return self._transformed_mapped_model_pos + + @property + def n_target_not_mapped(self): + """ Number of target residues which have no mapping to model + + :type: :class:`int` + """ + if self._n_target_not_mapped is None: + self._extract_mapped_pos() + return self._n_target_not_mapped + + @property + def transform(self): + """ Transform: :attr:`~mapped_model_pos` onto :attr:`~mapped_target_pos` + + Computed using Kabsch minimal rmsd algorithm + + :type: :class:`ost.geom.Mat4` + """ + if self._transform is None: + try: + res = mol.alg.SuperposeSVD(self.mapped_model_pos, + self.mapped_target_pos) + self._transform = res.transformation + except: + self._transform = geom.Mat4() + return self._transform + + @property + def gdtts(self): + """ GDT with thresholds: 8.0A, 4.0A, 2.0A and 1.0A + + Computed on :attr:`~transformed_mapped_model_pos` and + :attr:`mapped_target_pos` + + :type: :class:`float` + """ + if self._gdtts is None: + n = \ + self.mapped_target_pos.GetGDTTS(self.transformed_mapped_model_pos, + norm=False) + n_full = 4*len(self.mapped_target_pos) + 4*self.n_target_not_mapped + if n_full > 0: + self._gdtts = float(n) / n_full + else: + self._gdtts = 0.0 + return self._gdtts + + @property + def gdtha(self): + """ GDT with thresholds: 4.0A, 2.0A, 1.0A and 0.5A + + Computed on :attr:`~transformed_mapped_model_pos` and + :attr:`mapped_target_pos` + + :type: :class:`float` + """ + if self._gdtha is None: + n = \ + self.mapped_target_pos.GetGDTHA(self.transformed_mapped_model_pos, + norm=False) + n_full = 4*len(self.mapped_target_pos) + 4*self.n_target_not_mapped + if n_full > 0: + self._gdtha = float(n) / n_full + else: + self._gdtha = 0.0 + return self._gdtha + + @property + def rmsd(self): + """ RMSD + + Computed on :attr:`~transformed_mapped_model_pos` and + :attr:`mapped_target_pos` + + :type: :class:`float` + """ + if self._rmsd is None: + self._rmsd = \ + self.mapped_target_pos.GetRMSD(self.transformed_mapped_model_pos) + return self._rmsd + + def _compute_lddt(self): + # lDDT requires a flat mapping with mdl_ch as key and trg_ch as value + flat_mapping = self.mapping.GetFlatMapping(mdl_as_key=True) + + # perform required alignments - cannot take the alignments from the + # mapping results as we potentially remove stuff in the stereocheck + # process. + trg_seqs = dict() + for ch in self.stereochecked_target.chains: + cname = ch.GetName() + s = ''.join([r.one_letter_code for r in ch.residues]) + s = seq.CreateSequence(ch.GetName(), s) + s.AttachView(self.stereochecked_target.Select(f"cname={cname}")) + trg_seqs[ch.GetName()] = s + mdl_seqs = dict() + for ch in self.stereochecked_model.chains: + cname = ch.GetName() + s = ''.join([r.one_letter_code for r in ch.residues]) + s = seq.CreateSequence(cname, s) + s.AttachView(self.stereochecked_model.Select(f"cname={cname}")) + mdl_seqs[ch.GetName()] = s + + trg_pep_chains = [s.GetName() for s in self.chain_mapper.polypep_seqs] + trg_nuc_chains = [s.GetName() for s in self.chain_mapper.polynuc_seqs] + trg_pep_chains = set(trg_pep_chains) + trg_nuc_chains = set(trg_nuc_chains) + alns = dict() + for mdl_ch, trg_ch in flat_mapping.items(): + if trg_ch in trg_pep_chains: + stype = mol.ChemType.AMINOACIDS + elif trg_ch in trg_nuc_chains: + stype = mol.ChemType.NUCLEOTIDES + else: + raise RuntimeError("Chain name inconsistency... ask Gabriel") + alns[mdl_ch] = self.chain_mapper.Align(trg_seqs[trg_ch], + mdl_seqs[mdl_ch], + stype) + + lddt_score = self.lddt_scorer.lDDT(self.stereochecked_model, + chain_mapping = flat_mapping, + residue_mapping = alns, + check_resnames=False, + local_lddt_prop="lddt")[0] + local_lddt = dict() + for r in self.model.residues: + cname = r.GetChain().GetName() + if cname not in local_lddt: + local_lddt[cname] = dict() + if r.HasProp("lddt"): + score = round(r.GetFloatProp("lddt"), 3) + local_lddt[cname][r.GetNumber().GetNum()] = score + else: + # rsc => residue stereo checked... + rsc = self.stereochecked_model.FindResidue(cname, r.GetNumber()) + if not rsc.IsValid(): + # has been removed by stereochecks => assign 0.0 + local_lddt[cname][r.GetNumber().GetNum()] = 0.0 + else: + # not covered by trg or skipped in chain mapping procedure + # the latter happens if its part of a super short chain + local_lddt[cname][r.GetNumber().GetNum()] = None + self._lddt = lddt_score + self._local_lddt = local_lddt + + def _compute_qs(self): + qs_score_result = self.qs_scorer.Score(self.mapping.mapping) + self._QS_global = qs_score_result.QS_global + self._QS_best = qs_score_result.QS_best + + def _compute_dockq(self): + try: + dockq_exec = settings.Locate("DockQ.py") + except: + raise RuntimeError("DockQ.py must be in PATH for DockQ scoring") + + flat_mapping = self.mapping.GetFlatMapping() + # list of [trg_ch1, trg_ch2, mdl_ch1, mdl_ch2] + self._dockq_interfaces = list() + # lists with respective values for these interfaces + self._dockq_native_contacts = list() + self._dockq_scores = list() + + # list of interfaces which are present in target but not mapped, i.e. + # not present in mdl + self._dockq_nonmapped_interfaces = list() + self._dockq_nonmapped_interfaces_counts = list() + + nonmapped_interface_counts = list() + + flat_mapping = self.mapping.GetFlatMapping() + for trg_int in self.qs_scorer.qsent1.interacting_chains: + trg_ch1 = trg_int[0] + trg_ch2 = trg_int[1] + if trg_ch1 in flat_mapping and trg_ch2 in flat_mapping: + mdl_ch1 = flat_mapping[trg_ch1] + mdl_ch2 = flat_mapping[trg_ch2] + try: + res = dockq.DockQ(dockq_exec, self.model, self.target, + mdl_ch1, mdl_ch2, trg_ch1, trg_ch2) + except Exception as e: + if "AssertionError: length of native is zero" in str(e): + # happens if target interface has no native contacts + continue + else: + raise + + if res.native_contacts > 0: + self._dockq_interfaces.append((trg_ch1, trg_ch2, + mdl_ch1, mdl_ch2)) + self._dockq_native_contacts.append(res.native_contacts) + self._dockq_scores.append(res.DockQ) + else: + # interface which is not covered by mdl... let's run DockQ with + # trg as trg/mdl in order to get the native contacts out + try: + res = dockq.DockQ(dockq_exec, self.target, self.target, + trg_ch1, trg_ch2, trg_ch1, trg_ch2) + counts = res.native_contacts + if counts > 0: + self._dockq_nonmapped_interfaces.append((trg_ch1, + trg_ch2)) + self._dockq_nonmapped_interfaces_counts.append(counts) + except Exception as e: + if "AssertionError: length of native is zero" in str(e): + # happens if target interface has no native contacts + continue + else: + raise + + # there are 4 types of combined scores + # - simple average + # - average weighted by native_contacts + # - the two above including nonmapped_interfaces => set DockQ to 0.0 + scores = np.array(self._dockq_scores) + weights = np.array(self._dockq_native_contacts) + self._dockq_ave = np.mean(scores) + self._dockq_wave = np.sum(np.multiply(weights/np.sum(weights), scores)) + scores = np.append(scores, [0.0]*len(self._dockq_nonmapped_interfaces)) + weights = np.append(weights, self._dockq_nonmapped_interfaces_counts) + self._dockq_ave_full = np.mean(scores) + self._dockq_wave_full = np.sum(np.multiply(weights/np.sum(weights), + scores)) + + def _extract_mapped_pos(self): + self._mapped_target_pos = geom.Vec3List() + self._mapped_model_pos = geom.Vec3List() + self._n_target_not_mapped = 0 + processed_trg_chains = set() + for trg_ch, mdl_ch in self.mapping.GetFlatMapping().items(): + processed_trg_chains.add(trg_ch) + aln = self.mapping.alns[(trg_ch, mdl_ch)] + for col in aln: + if col[0] != '-' and col[1] != '-': + trg_res = col.GetResidue(0) + mdl_res = col.GetResidue(1) + trg_at = trg_res.FindAtom("CA") + mdl_at = mdl_res.FindAtom("CA") + if not trg_at.IsValid(): + trg_at = trg_res.FindAtom("C3'") + if not mdl_at.IsValid(): + mdl_at = mdl_res.FindAtom("C3'") + self._mapped_target_pos.append(trg_at.GetPos()) + self._mapped_model_pos.append(mdl_at.GetPos()) + elif col[0] != '-': + self._n_target_not_mapped += 1 + # count number of trg residues from non-mapped chains + for ch in self.mapping.target.chains: + if ch.GetName() not in processed_trg_chains: + self._n_target_not_mapped += len(ch.residues) + + def _get_repr_view(self, ent): + """ Returns view with representative atoms => CB, CA for GLY + + Ensures that each residue has exactly one atom with assertions + + :param ent: Entity for which you want the representative view + :param ent: :class:`ost.mol.EntityHandle`/:class:`ost.mol.EntityView` + :returns: :class:`ost.mol.EntityView` derived from ent + """ + repr_ent = ent.Select("(aname=\"CB\" or (rname=\"GLY\" and aname=\"CA\"))") + for r in repr_ent.residues: + assert(len(r.atoms) == 1) + return repr_ent + + def _get_interface_residues(self, ent): + """ Get interface residues + + Thats all residues having a contact with at least one residue from another + chain (CB-CB distance <= 8A, CA in case of Glycine) + + :param ent: Model for which to extract interface residues + :type ent: :class:`ost.mol.EntityView` + :returns: :class:`dict` with chain names as key and and :class:`list` + with residue numbers of the respective interface residues. + """ + # select for representative positions => CB, CA for GLY + repr_ent = self._get_repr_view(ent) + result = {ch.GetName(): list() for ch in ent.chains} + for ch in ent.chains: + cname = ch.GetName() + sel = repr_ent.Select(f"(cname={cname} and 8 <> [cname!={cname}])") + result[cname] = [r.GetNumber().GetNum() for r in sel.residues] + return result + \ No newline at end of file