packages/miew/src/chem/SecondaryStructureMap.js (411 lines of code) (raw):
import HBondInfo from './HBondInfo';
import ResidueType from './ResidueType';
const BridgeType = Object.freeze({
NO_BRIDGE: 0,
PARALLEL: 1,
ANTI_PARALLEL: 2,
});
const HelixFlag = Object.freeze({
START: 1,
MIDDLE: 2,
END: 3,
START_AND_END: 4,
});
const StructureType = Object.freeze({
STRAND: 'E',
BRIDGE: 'B',
HELIX_310: 'G',
HELIX_ALPHA: 'H',
HELIX_PI: 'I',
TURN: 'T',
BEND: 'S',
LOOP: ' ',
});
export default class SecondaryStructureMap {
constructor(complex) {
this._complex = complex;
this._build();
}
_build() {
const self = this;
this._hbonds = new HBondInfo(this._complex);
this._ss = []; // DSSP map by residue
// auxilliary data
this._sheet = [];
this._betaPartners = [];
this._bend = [];
for (let i = 0; i < this._complex.getResidues().length; ++i) {
this._betaPartners[i] = [];
}
this._helixFlags = [];
this._helixFlags[3] = [];
this._helixFlags[4] = [];
this._helixFlags[5] = [];
// calculate peptide chain lengths
this._chainLengths = [];
for (let i = 0; i < this._complex._chains.length; ++i) {
const chain = this._complex._chains[i].getResidues();
let len = 0;
for (; len < chain.length; ++len) {
if ((chain[len].getType().flags & ResidueType.Flags.PROTEIN) === 0) {
break;
}
}
this._chainLengths[i] = len;
}
this._buildBetaSheets();
for (let i = 0; i < this._complex._chains.length; ++i) {
self._buildAlphaHelices(this._complex._chains[i].getResidues(), this._chainLengths[i], false);
}
}
_buildAlphaHelices(inResidues, chainLength, inPreferPiHelices) {
// Helix and Turn
for (let stride = 3; stride <= 5; ++stride) {
if (inResidues.length < stride) {
break;
}
for (let i = 0; i + stride < chainLength; ++i) {
if (this._hbonds.isBond(inResidues[i + stride]._index, inResidues[i]._index)
/* && NoChainBreak(res[i], res[i + stride]) */) {
this._helixFlags[stride][inResidues[i + stride]._index] = HelixFlag.END;
for (let j = i + 1; j < i + stride; ++j) {
if (typeof this._helixFlags[stride][inResidues[j]._index] === 'undefined') {
this._helixFlags[stride][inResidues[j]._index] = HelixFlag.MIDDLE;
}
}
if (this._helixFlags[stride][inResidues[i]._index] === HelixFlag.END) {
this._helixFlags[stride][inResidues[i]._index] = HelixFlag.START_AND_END;
} else {
this._helixFlags[stride][inResidues[i]._index] = HelixFlag.START;
}
}
}
}
for (let i = 2; i < chainLength - 2; ++i) {
const kappa = this._kappa(inResidues[i - 2], inResidues[i], inResidues[i + 2]);
this._bend[inResidues[i]._index] = (kappa !== 360 && kappa > 70);
}
for (let i = 1; i + 4 < chainLength; ++i) {
if (this._isHelixStart(inResidues[i]._index, 4) && this._isHelixStart(inResidues[i - 1]._index, 4)) {
for (let j = i; j <= i + 3; ++j) {
this._ss[inResidues[j]._index] = StructureType.HELIX_ALPHA;
}
}
}
for (let i = 1; i + 3 < chainLength; ++i) {
if (this._isHelixStart(inResidues[i]._index, 3) && this._isHelixStart(inResidues[i - 1]._index, 3)) {
let empty = true;
for (let j = i; empty && j <= i + 2; ++j) {
empty = typeof this._ss[inResidues[j]._index] === 'undefined'
|| this._ss[inResidues[j]._index] === StructureType.HELIX_310;
}
if (empty) {
for (let j = i; j <= i + 2; ++j) {
this._ss[inResidues[j]._index] = StructureType.HELIX_310;
}
}
}
}
for (let i = 1; i + 5 < chainLength; ++i) {
if (this._isHelixStart(inResidues[i]._index, 5) && this._isHelixStart(inResidues[i - 1]._index, 5)) {
let empty = true;
for (let j = i; empty && j <= i + 4; ++j) {
empty = typeof this._ss[inResidues[j]._index] === 'undefined'
|| this._ss[inResidues[j]._index] === StructureType.HELIX_PI
|| (inPreferPiHelices && this._ss[inResidues[j]._index] === StructureType.HELIX_ALPHA);
}
if (empty) {
for (let j = i; j <= i + 4; ++j) {
this._ss[inResidues[j]._index] = StructureType.HELIX_PI;
}
}
}
}
for (let i = 1; i + 1 < chainLength; ++i) {
if (typeof this._ss[inResidues[i]._index] === 'undefined') {
let isTurn = false;
for (let stride = 3; stride <= 5 && !isTurn; ++stride) {
for (let k = 1; k < stride && !isTurn; ++k) {
isTurn = (i >= k) && this._isHelixStart(inResidues[i - k]._index, stride);
}
}
if (isTurn) {
this._ss[inResidues[i]._index] = StructureType.TURN;
} else if (this._bend[inResidues[i]._index]) {
this._ss[inResidues[i]._index] = StructureType.BEND;
}
}
}
}
_residueGetCAlpha(res) {
for (let i = 0; i < res._atoms.length; ++i) {
const { name } = res._atoms[i];
if (name === 'CA'
|| name === 'C1') {
return res._atoms[i].position;
}
}
return null;
}
_cosinusAngle(p1, p2, p3, p4) {
const v12 = p1.clone().sub(p2);
const v34 = p3.clone().sub(p4);
let result = 0;
const x = v12.dot(v12) * v34.dot(v34);
if (x > 0) {
result = v12.dot(v34) / Math.sqrt(x);
}
return result;
}
_kappa(prevPrev, res, nextNext) {
const curCA = this._residueGetCAlpha(res);
const ppCA = this._residueGetCAlpha(prevPrev);
const nnCA = this._residueGetCAlpha(nextNext);
if (curCA === null || ppCA === null || nnCA === null) {
return 180;
}
const ckap = this._cosinusAngle(curCA, ppCA, nnCA, curCA);
const skap = Math.sqrt(1 - ckap * ckap);
return Math.atan2(skap, ckap) * 180 / Math.PI;
}
_isHelixStart(res, stride) {
return (this._helixFlags[stride][res] === HelixFlag.START
|| this._helixFlags[stride][res] === HelixFlag.START_AND_END);
}
_buildBetaSheets() {
// find bridges
// check each chain against each other chain, and against itself
const bridges = [];
for (let a = 0; a < this._complex._chains.length; ++a) {
const lenA = this._chainLengths[a];
if (lenA <= 4) {
continue;
}
const chainA = this._complex._chains[a].getResidues();
for (let b = a; b < this._complex._chains.length; ++b) {
const lenB = this._chainLengths[b];
if (lenB <= 4) {
continue;
}
const chainB = this._complex._chains[b].getResidues();
for (let i = 1; i + 1 < lenA; ++i) {
const ri = chainA[i];
let j = 1;
if (b === a) {
j = i + 3; // check for self-bridges forward down the chain
}
for (; j + 1 < lenB; ++j) {
const rj = chainB[j];
const type = this._testBridge(chainA, i, chainB, j);
if (type === BridgeType.NO_BRIDGE) {
continue;
}
// there is a bridge, try to attach it to previously found sequence
let found = false;
for (const bridge of bridges) {
if (type !== bridge.type || ri._index !== bridge.i[bridge.i.length - 1] + 1) {
continue;
}
if (type === BridgeType.PARALLEL && bridge.j[bridge.j.length - 1] + 1 === rj._index) {
bridge.i.push(ri._index);
bridge.j.push(rj._index);
found = true;
break;
}
if (type === BridgeType.ANTI_PARALLEL && bridge.j[0] - 1 === rj._index) {
bridge.i.push(ri._index);
bridge.j.unshift(rj._index);
found = true;
break;
}
}
// this bridge cannot be attached anywhere, start a new sequence
if (!found) {
bridges.push({
type,
i: [ri._index],
chainI: ri.getChain()._index,
j: [rj._index],
chainJ: rj.getChain()._index,
});
}
}
}
}
}
// extend ladders
bridges.sort((a, b) => {
if (a.chainI < b.chainI || (a.chainI === b.chainI && a.i[0] < b.i[0])) {
return -1;
}
return 1;
});
for (let i = 0; i < bridges.length; ++i) {
for (let j = i + 1; j < bridges.length; ++j) {
const ibi = bridges[i].i[0];
const iei = bridges[i].i[bridges[i].i.length - 1];
const jbi = bridges[i].j[0];
const jei = bridges[i].j[bridges[i].j.length - 1];
const ibj = bridges[j].i[0];
const iej = bridges[j].i[bridges[j].i.length - 1];
const jbj = bridges[j].j[0];
const jej = bridges[j].j[bridges[j].j.length - 1];
if (bridges[i].type !== bridges[j].type
|| this._hasChainBreak(Math.min(ibi, ibj), Math.max(iei, iej))
|| this._hasChainBreak(Math.min(jbi, jbj), Math.max(jei, jej))
|| ibj - iei >= 6 || (iei >= ibj && ibi <= iej)) {
continue;
}
let bulge = false;
if (bridges[i].type === BridgeType.PARALLEL) {
bulge = ((jbj - jei < 6 && ibj - iei < 3) || (jbj - jei < 3));
} else {
bulge = ((jbi - jej < 6 && ibj - iei < 3) || (jbi - jej < 3));
}
if (bulge) {
bridges[i].i = bridges[i].i.concat(bridges[j].i);
if (bridges[i].type === BridgeType.PARALLEL) {
bridges[i].j = bridges[i].j.concat(bridges[j].j);
} else {
bridges[i].j = bridges[j].j.concat(bridges[i].j);
}
bridges.splice(j--, 1);
}
}
}
// Sheet
const ladderset = new Set();
for (let i = 0; i < bridges.length; ++i) {
ladderset.add(bridges[i]);
}
let sheet = 1;
let ladder = 0;
while (ladderset.size > 0) {
let bridge = ladderset.values().next().value;
ladderset.delete(bridge);
const sheetset = new Set();
sheetset.add(bridge);
let toMove;
do {
toMove = new Set();
for (const a of sheetset.values()) {
for (const b of ladderset.values()) {
if (this._areBridgesLinked(a, b)) {
toMove.add(b);
}
}
}
for (bridge of toMove.values()) {
sheetset.add(bridge);
ladderset.delete(bridge);
}
} while (toMove.size > 0);
for (bridge of sheetset.values()) {
bridge.ladder = ladder;
bridge.sheet = sheet;
bridge.link = sheetset;
++ladder;
}
++sheet;
}
for (let i = 0; i < bridges.length; ++i) {
const bridge = bridges[i];
// find out if any of the i and j set members already have
// a bridge assigned, if so, we're assigning bridge 2
let betai = 0;
let betaj = 0;
for (let l = 0; l < bridge.i.length; ++l) {
if (this._betaPartners[bridge.i[l]][0]) {
betai = 1;
break;
}
}
for (let l = 0; l < bridge.j.length; ++l) {
if (this._betaPartners[bridge.j[l]][0]) {
betaj = 1;
break;
}
}
let ss = StructureType.BRIDGE;
if (bridge.i.length > 1) {
ss = StructureType.STRAND;
}
if (bridge.type === BridgeType.PARALLEL) {
let j = 0;
for (let k = 0; k < bridge.i.length; ++k) {
this._betaPartners[bridge.i[k]][betai] = {
residue: bridge.j[j++],
ladder: bridge.ladder,
parallel: true,
};
}
j = 0;
for (let k = 0; k < bridge.j.length; ++k) {
this._betaPartners[bridge.j[k]][betaj] = {
residue: bridge.i[j++],
ladder: bridge.ladder,
parallel: true,
};
}
} else {
let j = bridge.j.length - 1;
for (let k = 0; k < bridge.i.length; ++k) {
this._betaPartners[bridge.i[k]][betai] = {
residue: bridge.j[j--],
ladder: bridge.ladder,
parallel: false,
};
}
j = bridge.i.length - 1;
for (let k = 0; k < bridge.j.length; ++k) {
this._betaPartners[bridge.j[k]][betaj] = {
residue: bridge.i[j--],
ladder: bridge.ladder,
parallel: false,
};
}
}
for (let k = bridge.i[0]; k <= bridge.i[bridge.i.length - 1]; ++k) {
if (this._ss[k] !== StructureType.STRAND) {
this._ss[k] = ss;
this._sheet[k] = bridge.sheet;
}
}
for (let k = bridge.j[0]; k <= bridge.j[bridge.j.length - 1]; ++k) {
if (this._ss[k] !== StructureType.STRAND) {
this._ss[k] = ss;
this._sheet[k] = bridge.sheet;
}
}
}
}
_testBridge(chainA, from, chainB, to) {
let result = BridgeType.NO_BRIDGE;
const a = chainA[from - 1]._index;
const b = chainA[from]._index;
const c = chainA[from + 1]._index;
const d = chainB[to - 1]._index;
const e = chainB[to]._index;
const f = chainB[to + 1]._index;
const isBond = this._hbonds.isBond.bind(this._hbonds);
if ((isBond(c, e) && isBond(e, a)) || (isBond(f, b) && isBond(b, d))) {
result = BridgeType.PARALLEL;
} else if ((isBond(c, d) && isBond(f, a)) || (isBond(e, b) && isBond(b, e))) {
result = BridgeType.ANTI_PARALLEL;
}
return result;
}
// return true if any of the residues in bridge a is identical to any of the residues in bridge b
_areBridgesLinked(a, b) {
const ai = new Set(a.i);
const aj = new Set(a.j);
for (const i of b.i) {
if (ai.has(i) || aj.has(i)) {
return true;
}
}
for (const i of b.j) {
if (ai.has(i) || aj.has(i)) {
return true;
}
}
return false;
}
_hasChainBreak(from, to) {
for (let i = from + 1; i <= to; ++i) {
if (this._complex._residues[i]._sequence !== this._complex._residues[i - 1]._sequence + 1) {
return true;
}
}
return false;
}
}
SecondaryStructureMap.StructureType = StructureType;