Files
enviPy-bayer/static/js/ketcher2/script/chem/smiles/stereocenters.js
2025-06-23 20:13:54 +02:00

520 lines
17 KiB
JavaScript

/****************************************************************************
* Copyright 2017 EPAM Systems
*
* 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.
***************************************************************************/
var Map = require('../../util/map');
var Set = require('../../util/set');
var Vec2 = require('../../util/vec2');
var Struct = require('../struct');
function Stereocenters(mol, neighborsFunc, context) {
this.molecule = mol;
this.atoms = new Map();
this.getNeighbors = neighborsFunc;
this.context = context;
}
Stereocenters.prototype.each = function (func, context) {
this.atoms.each(func, context);
};
Stereocenters.prototype.buildFromBonds = function (/* const int *atom_types, const int *atom_groups, const int *bond_orientations, */ignoreErrors) {
var atoms = this.molecule.atoms;
var bonds = this.molecule.bonds;
// this is a set of atoms that are likely to belong to allene structures and
// therefore should not be considered as potential stereocenters in the code below,
// as allenes cannot be encoded in the SMILES notation
var alleneMask = Set.empty();
atoms.each(function (aid) {
var neiList = this.getNeighbors.call(this.context, aid);
if (neiList.length != 2)
return false;
var nei1 = neiList[0];
var nei2 = neiList[1];
// check atom labels
if ([aid, nei1.aid, nei2.aid].findIndex(function (aid) {
return ['C', 'Si'].indexOf(atoms.get(aid).label) < 0;
}, this) >= 0)
return false;
// check adjacent bond types
if ([nei1.bid, nei2.bid].findIndex(function (bid) {
return bonds.get(bid).type != Struct.Bond.PATTERN.TYPE.DOUBLE;
}, this) >= 0)
return false;
// get the other neighbors of the two adjacent atoms except for the central atom
var nei1nei = this.getNeighbors.call(this.context, nei1.aid).filter(function (nei) {
return nei.aid != aid;
});
var nei2nei = this.getNeighbors.call(this.context, nei2.aid).filter(function (nei) {
return nei.aid != aid;
});
if (nei1nei.length < 1 || nei1nei.length > 2 || nei2nei.length < 1 || nei2nei.length > 2)
return false;
if (nei1nei.concat(nei2nei).findIndex(function (nei) {
return bonds.get(nei.bid).type != Struct.Bond.PATTERN.TYPE.SINGLE;
}, this) >= 0)
return false;
if (nei1nei.concat(nei2nei).findIndex(function (nei) {
return bonds.get(nei.bid).stereo == Struct.Bond.PATTERN.STEREO.EITHER;
}, this) >= 0)
return false;
Set.add(alleneMask, nei1.aid);
Set.add(alleneMask, nei2.aid);
}, this);
if (Set.size(alleneMask) > 0)
alert('This structure may contain allenes, which cannot be represented in the SMILES notation. Relevant stereo-information will be discarded.');
atoms.each(function (aid) {
if (Set.contains(alleneMask, aid))
return;
/*
if (atom_types[atom_idx] == 0)
continue;
*/
var neiList = this.getNeighbors.call(this.context, aid);
var stereocenter = false;
neiList.find(function (nei) {
var bond = this.molecule.bonds.get(nei.bid);
if (bond.type == Struct.Bond.PATTERN.TYPE.SINGLE && bond.begin == aid) {
if (bond.stereo == Struct.Bond.PATTERN.STEREO.UP || bond.stereo == Struct.Bond.PATTERN.STEREO.DOWN) {
stereocenter = true;
return true;
}
}
return false;
}, this);
if (!stereocenter)
return;
if (ignoreErrors)
// try
// {
this.buildOneCenter(aid/* , atom_groups[atom_idx], atom_types[atom_idx], bond_orientations*/);
// }
// catch (er)
// {
// }
else
this.buildOneCenter(aid/* , atom_groups[atom_idx], atom_types[atom_idx], bond_orientations*/);
}, this);
};
Stereocenters.allowed_stereocenters =
[
{ elem: 'C', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'C', charge: 0, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'Si', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'Si', charge: 0, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'N', charge: 1, degree: 3, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'N', charge: 1, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'N', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 3 },
{ elem: 'S', charge: 0, degree: 4, n_double_bonds: 2, implicit_degree: 4 },
{ elem: 'S', charge: 1, degree: 3, n_double_bonds: 0, implicit_degree: 3 },
{ elem: 'S', charge: 0, degree: 3, n_double_bonds: 1, implicit_degree: 3 },
{ elem: 'P', charge: 0, degree: 3, n_double_bonds: 0, implicit_degree: 3 },
{ elem: 'P', charge: 1, degree: 4, n_double_bonds: 0, implicit_degree: 4 },
{ elem: 'P', charge: 0, degree: 4, n_double_bonds: 1, implicit_degree: 4 }
];
Stereocenters.prototype.buildOneCenter = function (atomIdx/* , int group, int type, const int *bond_orientations*/) { // eslint-disable-line max-statements
var atom = this.molecule.atoms.get(atomIdx);
var neiList = this.getNeighbors.call(this.context, atomIdx);
var degree = neiList.length;
var implicitDegree = -1;
var stereocenter = {
group: 0, // = group;
type: 0, // = type;
pyramid: []
};
var edgeIds = [];
var lastAtomDir = 0;
var nDoubleBonds = 0;
stereocenter.pyramid[0] = -1;
stereocenter.pyramid[1] = -1;
stereocenter.pyramid[2] = -1;
stereocenter.pyramid[3] = -1;
var nPureHydrogens = 0;
if (degree > 4)
throw new Error('stereocenter with %d bonds are not supported' + degree);
neiList.forEach(function (nei, neiIdx) {
var neiAtom = this.molecule.atoms.get(nei.aid);
var bond = this.molecule.bonds.get(nei.bid);
edgeIds[neiIdx] = {
edge_idx: nei.bid,
nei_idx: nei.aid,
rank: nei.aid,
vec: Vec2.diff(neiAtom.pp, atom.pp).yComplement()
};
if (neiAtom.pureHydrogen()) {
nPureHydrogens++;
edgeIds[neiIdx].rank = 10000;
} else if (neiAtom.label === 'H') {
edgeIds[neiIdx].rank = 5000;
}
if (!edgeIds[neiIdx].vec.normalize())
throw new Error('zero bond length');
if (bond.type === Struct.Bond.PATTERN.TYPE.TRIPLE)
throw new Error('non-single bonds not allowed near stereocenter');
else if (bond.type === Struct.Bond.PATTERN.TYPE.AROMATIC)
throw new Error('aromatic bonds not allowed near stereocenter');
else if (bond.type === Struct.Bond.PATTERN.TYPE.DOUBLE)
nDoubleBonds++;
}, this);
Stereocenters.allowed_stereocenters.find(function (as) {
if (as.elem == atom.label && as.charge == atom.charge &&
as.degree == degree && as.n_double_bonds == nDoubleBonds) {
implicitDegree = as.implicit_degree;
return true;
}
return false;
}, this);
if (implicitDegree === -1)
throw new Error('unknown stereocenter configuration: ' + atom.label + ', charge ' + atom.charge + ', ' + degree + ' bonds (' + nDoubleBonds + ' double)');
if (degree === 4 && nPureHydrogens > 1)
throw new Error(nPureHydrogens + ' hydrogens near stereocenter');
if (degree === 3 && implicitDegree == 4 && nPureHydrogens > 0)
throw new Error('have hydrogen(s) besides implicit hydrogen near stereocenter');
/*
if (stereocenter.type == ATOM_ANY)
{
_stereocenters.insert(atom_idx, stereocenter);
return;
}
*/
if (degree === 4) {
// sort by neighbor atom index (ascending)
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
if (edgeIds[2].rank > edgeIds[3].rank)
swap(edgeIds, 2, 3);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
var main1 = -1;
var main2 = -1;
var side1 = -1;
var side2 = -1;
var mainDir = 0;
for (var neiIdx = 0; neiIdx < 4; neiIdx++) {
var stereo = this.getBondStereo(atomIdx, edgeIds[neiIdx].edge_idx);
if (stereo == Struct.Bond.PATTERN.STEREO.UP || stereo == Struct.Bond.PATTERN.STEREO.DOWN) {
main1 = neiIdx;
mainDir = stereo;
break;
}
}
if (main1 == -1)
throw new Error('none of 4 bonds going from stereocenter is stereobond');
var xyz1, xyz2;
// find main2 as opposite to main1
if (main2 == -1) {
xyz1 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 1) % 4].vec, edgeIds[(main1 + 2) % 4].vec);
xyz2 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 1) % 4].vec, edgeIds[(main1 + 3) % 4].vec);
if (xyz1 + xyz2 == 3 || xyz1 + xyz2 == 12) {
main2 = (main1 + 1) % 4;
side1 = (main1 + 2) % 4;
side2 = (main1 + 3) % 4;
}
}
if (main2 == -1) {
xyz1 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 2) % 4].vec, edgeIds[(main1 + 1) % 4].vec);
xyz2 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 2) % 4].vec, edgeIds[(main1 + 3) % 4].vec);
if (xyz1 + xyz2 == 3 || xyz1 + xyz2 == 12) {
main2 = (main1 + 2) % 4;
side1 = (main1 + 1) % 4;
side2 = (main1 + 3) % 4;
}
}
if (main2 == -1) {
xyz1 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 3) % 4].vec, edgeIds[(main1 + 1) % 4].vec);
xyz2 = Stereocenters.xyzzy(edgeIds[main1].vec, edgeIds[(main1 + 3) % 4].vec, edgeIds[(main1 + 2) % 4].vec);
if (xyz1 + xyz2 == 3 || xyz1 + xyz2 == 12) {
main2 = (main1 + 3) % 4;
side1 = (main1 + 2) % 4;
side2 = (main1 + 1) % 4;
}
}
if (main2 == -1)
throw new Error('internal error: can not find opposite bond');
if (mainDir == Struct.Bond.PATTERN.STEREO.UP && this.getBondStereo(atomIdx, edgeIds[main2].edge_idx) == Struct.Bond.PATTERN.STEREO.DOWN)
throw new Error('stereo types of the opposite bonds mismatch');
if (mainDir == Struct.Bond.PATTERN.STEREO.DOWN && this.getBondStereo(atomIdx, edgeIds[main2].edge_idx) == Struct.Bond.PATTERN.STEREO.UP)
throw new Error('stereo types of the opposite bonds mismatch');
if (mainDir == this.getBondStereo(atomIdx, edgeIds[side1].edge_idx))
throw new Error('stereo types of non-opposite bonds match');
if (mainDir == this.getBondStereo(atomIdx, edgeIds[side2].edge_idx))
throw new Error('stereo types of non-opposite bonds match');
if (main1 == 3 || main2 == 3)
lastAtomDir = mainDir;
else
lastAtomDir = (mainDir == Struct.Bond.PATTERN.STEREO.UP ? Struct.Bond.PATTERN.STEREO.DOWN : Struct.Bond.PATTERN.STEREO.UP);
sign = Stereocenters.sign(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec);
if ((lastAtomDir == Struct.Bond.PATTERN.STEREO.UP && sign > 0) ||
(lastAtomDir == Struct.Bond.PATTERN.STEREO.DOWN && sign < 0)) {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[1].nei_idx;
stereocenter.pyramid[2] = edgeIds[2].nei_idx;
} else {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[2].nei_idx;
stereocenter.pyramid[2] = edgeIds[1].nei_idx;
}
stereocenter.pyramid[3] = edgeIds[3].nei_idx;
} else if (degree === 3) {
// sort by neighbor atom index (ascending)
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
if (edgeIds[1].rank > edgeIds[2].rank)
swap(edgeIds, 1, 2);
if (edgeIds[0].rank > edgeIds[1].rank)
swap(edgeIds, 0, 1);
var stereo0 = this.getBondStereo(atomIdx, edgeIds[0].edge_idx);
var stereo1 = this.getBondStereo(atomIdx, edgeIds[1].edge_idx);
var stereo2 = this.getBondStereo(atomIdx, edgeIds[2].edge_idx);
var nUp = 0;
var nDown = 0;
nUp += (stereo0 === Struct.Bond.PATTERN.STEREO.UP) ? 1 : 0;
nUp += (stereo1 === Struct.Bond.PATTERN.STEREO.UP) ? 1 : 0;
nUp += (stereo2 === Struct.Bond.PATTERN.STEREO.UP) ? 1 : 0;
nDown += (stereo0 === Struct.Bond.PATTERN.STEREO.DOWN) ? 1 : 0;
nDown += (stereo1 === Struct.Bond.PATTERN.STEREO.DOWN) ? 1 : 0;
nDown += (stereo2 === Struct.Bond.PATTERN.STEREO.DOWN) ? 1 : 0;
if (implicitDegree == 4) { // have implicit hydrogen
if (nUp == 3)
throw new Error('all 3 bonds up near stereoatom');
if (nDown == 3)
throw new Error('all 3 bonds down near stereoatom');
if (nUp == 0 && nDown == 0)
throw new Error('no up/down bonds near stereoatom -- indefinite case');
if (nUp == 1 && nDown == 1)
throw new Error('one bond up, one bond down -- indefinite case');
mainDir = 0;
if (nUp == 2) {
lastAtomDir = Struct.Bond.PATTERN.STEREO.DOWN;
} else if (nDown == 2) {
lastAtomDir = Struct.Bond.PATTERN.STEREO.UP;
} else {
main1 = -1;
side1 = -1;
side2 = -1;
for (neiIdx = 0; neiIdx < 3; neiIdx++) {
dir = this.getBondStereo(atomIdx, edgeIds[neiIdx].edge_idx);
if (dir == Struct.Bond.PATTERN.STEREO.UP || dir == Struct.Bond.PATTERN.STEREO.DOWN) { // eslint-disable-line max-depth
main1 = neiIdx;
mainDir = dir;
side1 = (neiIdx + 1) % 3;
side2 = (neiIdx + 2) % 3;
break;
}
}
if (main1 == -1)
throw new Error('internal error: can not find up or down bond');
var xyz = Stereocenters.xyzzy(edgeIds[side1].vec, edgeIds[side2].vec, edgeIds[main1].vec);
if (xyz == 3 || xyz == 4)
throw new Error('degenerate case for 3 bonds near stereoatom');
if (xyz == 1)
lastAtomDir = mainDir;
else
lastAtomDir = (mainDir == Struct.Bond.PATTERN.STEREO.UP ? Struct.Bond.PATTERN.STEREO.DOWN : Struct.Bond.PATTERN.STEREO.UP);
}
var sign = Stereocenters.sign(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec);
if ((lastAtomDir == Struct.Bond.PATTERN.STEREO.UP && sign > 0) ||
(lastAtomDir == Struct.Bond.PATTERN.STEREO.DOWN && sign < 0)) {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[1].nei_idx;
stereocenter.pyramid[2] = edgeIds[2].nei_idx;
} else {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[2].nei_idx;
stereocenter.pyramid[2] = edgeIds[1].nei_idx;
}
stereocenter.pyramid[3] = -1;
} else { // 3-connected P, N or S; no implicit hydrogens
var dir;
if (nDown > 0 && nUp > 0)
throw new Error('one bond up, one bond down -- indefinite case');
else if (nDown == 0 && nUp == 0)
throw new Error('no up-down bonds attached to stereocenter');
else if (nUp > 0)
dir = 1;
else
dir = -1;
if (Stereocenters.xyzzy(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec) === 1 ||
Stereocenters.xyzzy(edgeIds[0].vec, edgeIds[2].vec, edgeIds[1].vec) === 1 ||
Stereocenters.xyzzy(edgeIds[2].vec, edgeIds[1].vec, edgeIds[0].vec) === 1)
// all bonds belong to the same half-plane
dir = -dir;
sign = Stereocenters.sign(edgeIds[0].vec, edgeIds[1].vec, edgeIds[2].vec);
if (sign == dir) {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[2].nei_idx;
stereocenter.pyramid[2] = edgeIds[1].nei_idx;
} else {
stereocenter.pyramid[0] = edgeIds[0].nei_idx;
stereocenter.pyramid[1] = edgeIds[1].nei_idx;
stereocenter.pyramid[2] = edgeIds[2].nei_idx;
}
stereocenter.pyramid[3] = -1;
}
}
this.atoms.set(atomIdx, stereocenter);
};
Stereocenters.prototype.getBondStereo = function (centerIdx, edgeIdx) {
var bond = this.molecule.bonds.get(edgeIdx);
if (centerIdx != bond.begin) // TODO: check this
return 0;
return bond.stereo;
};
// 1 -- in the smaller angle, 2 -- in the bigger angle,
// 4 -- in the 'positive' straight angle, 8 -- in the 'negative' straight angle
Stereocenters.xyzzy = function (v1, v2, u) {
var eps = 0.001;
var sine1 = Vec2.cross(v1, v2);
var cosine1 = Vec2.dot(v1, v2);
var sine2 = Vec2.cross(v1, u);
var cosine2 = Vec2.dot(v1, u);
if (Math.abs(sine1) < eps) {
if (Math.abs(sine2) < eps)
throw new Error('degenerate case -- bonds overlap');
return (sine2 > 0) ? 4 : 8;
}
if (sine1 * sine2 < -eps * eps)
return 2;
if (cosine2 < cosine1)
return 2;
return 1;
};
Stereocenters.sign = function (v1, v2, v3) {
var res = (v1.x - v3.x) * (v2.y - v3.y) - (v1.y - v3.y) * (v2.x - v3.x); // eslint-disable-line no-mixed-operators
var eps = 0.001;
if (res > eps)
return 1;
if (res < -eps)
return -1;
throw new Error('degenerate triangle');
};
Stereocenters.isPyramidMappingRigid = function (mapping) {
var arr = mapping.slice();
var rigid = true;
if (arr[0] > arr[1])
swap(arr, 0, 1), rigid = !rigid;
if (arr[1] > arr[2])
swap(arr, 1, 2), rigid = !rigid;
if (arr[2] > arr[3])
swap(arr, 2, 3), rigid = !rigid;
if (arr[1] > arr[2])
swap(arr, 1, 2), rigid = !rigid;
if (arr[0] > arr[1])
swap(arr, 0, 1), rigid = !rigid;
if (arr[1] > arr[2])
swap(arr, 1, 2), rigid = !rigid;
return rigid;
};
function swap(arr, i1, i2) {
var tmp = arr[i1];
arr[i1] = arr[i2];
arr[i2] = tmp;
}
module.exports = Stereocenters;