in packages/ketcher-core/src/domain/serializers/smi/stereocenters.js [168:552]
Stereocenters.prototype.buildOneCenter = function (
atomIdx /* , int group, int type, const int *bond_orientations */,
) {
// eslint-disable-line max-statements
const atom = this.molecule.atoms.get(atomIdx);
const neiList = this.getNeighbors.call(this.context, atomIdx);
const degree = neiList.length;
let implicitDegree = -1;
const stereocenter = {
group: 0, // = group;
type: 0, // = type;
pyramid: [],
};
const edgeIds = [];
let lastAtomDir = 0;
let nDoubleBonds = 0;
stereocenter.pyramid[0] = -1;
stereocenter.pyramid[1] = -1;
stereocenter.pyramid[2] = -1;
stereocenter.pyramid[3] = -1;
let nPureHydrogens = 0;
if (degree > 4) {
throw new Error('stereocenter with %d bonds are not supported' + degree);
}
neiList.forEach((nei, neiIdx) => {
const neiAtom = this.molecule.atoms.get(nei.aid);
const 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 === Bond.PATTERN.TYPE.TRIPLE) {
throw new Error('non-single bonds not allowed near stereocenter');
} else if (bond.type === Bond.PATTERN.TYPE.AROMATIC) {
throw new Error('aromatic bonds not allowed near stereocenter');
} else if (bond.type === Bond.PATTERN.TYPE.DOUBLE) nDoubleBonds++;
});
Stereocenters.allowed_stereocenters.find((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;
});
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 (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;
let main2 = -1;
var side1 = -1;
var side2 = -1;
var mainDir = 0;
for (var neiIdx = 0; neiIdx < 4; neiIdx++) {
const stereo = this.getBondStereo(atomIdx, edgeIds[neiIdx].edge_idx);
if (
stereo === Bond.PATTERN.STEREO.UP ||
stereo === Bond.PATTERN.STEREO.DOWN
) {
main1 = neiIdx;
mainDir = stereo;
break;
}
}
if (main1 === -1) {
throw new Error('none of 4 bonds going from stereocenter is stereobond');
}
let 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 === Bond.PATTERN.STEREO.UP &&
this.getBondStereo(atomIdx, edgeIds[main2].edge_idx) ===
Bond.PATTERN.STEREO.DOWN
) {
throw new Error('stereo types of the opposite bonds mismatch');
}
if (
mainDir === Bond.PATTERN.STEREO.DOWN &&
this.getBondStereo(atomIdx, edgeIds[main2].edge_idx) ===
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 === Bond.PATTERN.STEREO.UP
? Bond.PATTERN.STEREO.DOWN
: Bond.PATTERN.STEREO.UP;
}
const sign = Stereocenters.sign(
edgeIds[0].vec,
edgeIds[1].vec,
edgeIds[2].vec,
);
if (
(lastAtomDir === Bond.PATTERN.STEREO.UP && sign > 0) ||
(lastAtomDir === 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);
const stereo0 = this.getBondStereo(atomIdx, edgeIds[0].edge_idx);
const stereo1 = this.getBondStereo(atomIdx, edgeIds[1].edge_idx);
const stereo2 = this.getBondStereo(atomIdx, edgeIds[2].edge_idx);
let nUp = 0;
let nDown = 0;
nUp += stereo0 === Bond.PATTERN.STEREO.UP ? 1 : 0;
nUp += stereo1 === Bond.PATTERN.STEREO.UP ? 1 : 0;
nUp += stereo2 === Bond.PATTERN.STEREO.UP ? 1 : 0;
nDown += stereo0 === Bond.PATTERN.STEREO.DOWN ? 1 : 0;
nDown += stereo1 === Bond.PATTERN.STEREO.DOWN ? 1 : 0;
nDown += stereo2 === 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 = Bond.PATTERN.STEREO.DOWN;
} else if (nDown === 2) {
lastAtomDir = Bond.PATTERN.STEREO.UP;
} else {
main1 = -1;
side1 = -1;
side2 = -1;
for (neiIdx = 0; neiIdx < 3; neiIdx++) {
const dir = this.getBondStereo(atomIdx, edgeIds[neiIdx].edge_idx);
if (
dir === Bond.PATTERN.STEREO.UP ||
dir === 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');
}
const 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 === Bond.PATTERN.STEREO.UP
? Bond.PATTERN.STEREO.DOWN
: Bond.PATTERN.STEREO.UP;
}
}
const sign = Stereocenters.sign(
edgeIds[0].vec,
edgeIds[1].vec,
edgeIds[2].vec,
);
if (
(lastAtomDir === Bond.PATTERN.STEREO.UP && sign > 0) ||
(lastAtomDir === 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
let 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;
}
const 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);
};