From 7487169bae6d5ffceb8e7348c8936e82abc8f562 Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:37:29 +0000 Subject: [PATCH 1/3] Initial plan From 99961b37de2c605770a86c9320a46bb6e9d3ab3c Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 16:45:20 +0000 Subject: [PATCH 2/3] Refactor SmilesFormatter: remove all methods except isContainsCoordinates Co-authored-by: AlexeyGirin <26869421+AlexeyGirin@users.noreply.github.com> --- .../application/formatters/smilesFormatter.ts | 45 +------------------ 1 file changed, 1 insertion(+), 44 deletions(-) diff --git a/packages/ketcher-core/src/application/formatters/smilesFormatter.ts b/packages/ketcher-core/src/application/formatters/smilesFormatter.ts index edd3b8b5f31..68551838964 100644 --- a/packages/ketcher-core/src/application/formatters/smilesFormatter.ts +++ b/packages/ketcher-core/src/application/formatters/smilesFormatter.ts @@ -14,50 +14,7 @@ * limitations under the License. ***************************************************************************/ -import { KetSerializer, SmiSerializer } from 'domain/serializers'; -import { StructFormatter, SupportedFormat } from './structFormatter.types'; -import { StructService, StructServiceOptions } from 'domain/services'; - -import { ServerFormatter } from './serverFormatter'; -import { Struct } from 'domain/entities'; - -export class SmilesFormatter implements StructFormatter { - readonly #smiSerializer: SmiSerializer; - readonly #structService: StructService; - readonly #ketSerializer: KetSerializer; - readonly #format: SupportedFormat; - readonly #options?: StructServiceOptions; - - constructor( - smiSerializer: SmiSerializer, - structService: StructService, - ketSerializer: KetSerializer, - format: SupportedFormat, - options?: StructServiceOptions, - ) { - this.#smiSerializer = smiSerializer; - this.#ketSerializer = ketSerializer; - this.#structService = structService; - this.#format = format; - this.#options = options; - } - - async getStructureFromStructAsync(struct: Struct): Promise { - const stringifiedMolfile = this.#smiSerializer.serialize(struct); - return stringifiedMolfile; - } - - getStructureFromStringAsync(stringifiedStruct: string): Promise { - const serverFormatter = new ServerFormatter( - this.#structService, - this.#ketSerializer, - this.#format, - this.#options, - ); - - return serverFormatter.getStructureFromStringAsync(stringifiedStruct); - } - +export class SmilesFormatter { static isContainsCoordinates(smiles: string) { return /\([\d.,;-]+\)/.test(smiles); } From 72df6bda43df9c31a61ae00a6cebee5ac374fe4e Mon Sep 17 00:00:00 2001 From: "copilot-swe-agent[bot]" <198982749+Copilot@users.noreply.github.com> Date: Thu, 20 Nov 2025 18:11:10 +0000 Subject: [PATCH 3/3] Remove unused SmiSerializer and Smiles serialization code Co-authored-by: AlexeyGirin <26869421+AlexeyGirin@users.noreply.github.com> --- .../src/domain/serializers/index.ts | 1 - .../serializers/smi/__tests__/dfs.test.js | 61 -- .../serializers/smi/__tests__/smiles.test.js | 79 -- .../src/domain/serializers/smi/cis_trans.js | 219 ----- .../src/domain/serializers/smi/dfs.js | 187 ---- .../src/domain/serializers/smi/index.ts | 18 - .../src/domain/serializers/smi/smi.types.ts | 19 - .../domain/serializers/smi/smiSerializer.ts | 40 - .../src/domain/serializers/smi/smiles.js | 821 ------------------ .../domain/serializers/smi/stereocenters.js | 729 ---------------- 10 files changed, 2174 deletions(-) delete mode 100644 packages/ketcher-core/src/domain/serializers/smi/__tests__/dfs.test.js delete mode 100644 packages/ketcher-core/src/domain/serializers/smi/__tests__/smiles.test.js delete mode 100644 packages/ketcher-core/src/domain/serializers/smi/cis_trans.js delete mode 100644 packages/ketcher-core/src/domain/serializers/smi/dfs.js delete mode 100644 packages/ketcher-core/src/domain/serializers/smi/index.ts delete mode 100644 packages/ketcher-core/src/domain/serializers/smi/smi.types.ts delete mode 100644 packages/ketcher-core/src/domain/serializers/smi/smiSerializer.ts delete mode 100644 packages/ketcher-core/src/domain/serializers/smi/smiles.js delete mode 100644 packages/ketcher-core/src/domain/serializers/smi/stereocenters.js diff --git a/packages/ketcher-core/src/domain/serializers/index.ts b/packages/ketcher-core/src/domain/serializers/index.ts index 6ee58622559..598f5d936e1 100644 --- a/packages/ketcher-core/src/domain/serializers/index.ts +++ b/packages/ketcher-core/src/domain/serializers/index.ts @@ -17,5 +17,4 @@ export * from './serializers.types'; export * from './ket'; export * from './mol'; -export * from './smi'; export * from './sdf'; diff --git a/packages/ketcher-core/src/domain/serializers/smi/__tests__/dfs.test.js b/packages/ketcher-core/src/domain/serializers/smi/__tests__/dfs.test.js deleted file mode 100644 index 88ac2f14c49..00000000000 --- a/packages/ketcher-core/src/domain/serializers/smi/__tests__/dfs.test.js +++ /dev/null @@ -1,61 +0,0 @@ -import Dfs from '../dfs'; -import { Struct } from 'domain/entities'; - -const struct = new Struct(); -const atomData = new Array(struct.atoms.size); -const components = struct.getComponents(); -const componentsAll = components.reactants.concat(components.products); - -const dfs = new Dfs( - struct, - atomData, - componentsAll, - components.reactants.length, -); - -describe('Dfs', () => { - describe('edgeClosingCycle method', () => { - it('should return false if no cycles in structure', () => { - dfs.edges = [{ closing_cycle: 0, opening_cycles: 0 }]; - const eIdx = 0; - expect(dfs.edgeClosingCycle(eIdx)).toBe(false); - }); - - it('should return true if is cycles in structure', () => { - dfs.edges = [{ closing_cycle: 1, opening_cycles: 0 }]; - const eIdx = 0; - expect(dfs.edgeClosingCycle(eIdx)).toBe(true); - }); - }); - - describe('numBranches method', () => { - it('should return number of branches', () => { - dfs.vertices = [{ branches: 1 }]; - const vIdx = 0; - expect(dfs.numBranches(vIdx)).toBe(1); - }); - }); - - describe('toString method', () => { - it('should return * if no structure', () => { - const str = dfs.toString(); - expect(str).toBe('*'); - }); - - it('should return string', () => { - dfs.v_seq = [{ idx: 0 }, { idx: 1 }]; - const str = dfs.toString(); - expect(str).toBe('0 -> 1 -> *'); - }); - }); - - describe('numOpeningCycles method', () => { - it('should return number of opening cycles', () => { - let eIdx = 0; - expect(dfs.numOpeningCycles(eIdx)).toBe(0); - eIdx = 1; - dfs.edges = [{}, { closing_cycle: 0, opening_cycles: 3 }]; - expect(dfs.numOpeningCycles(eIdx)).toBe(3); - }); - }); -}); diff --git a/packages/ketcher-core/src/domain/serializers/smi/__tests__/smiles.test.js b/packages/ketcher-core/src/domain/serializers/smi/__tests__/smiles.test.js deleted file mode 100644 index 2ed239e8589..00000000000 --- a/packages/ketcher-core/src/domain/serializers/smi/__tests__/smiles.test.js +++ /dev/null @@ -1,79 +0,0 @@ -import { Smiles } from '../smiles'; -import { Struct } from 'domain/entities'; -import { Atom } from '../../../entities'; - -let smiles = new Smiles(); -const struct = new Struct(); -struct.atoms.add(new Atom({ label: 'A' })); -struct.atoms.add(new Atom({ label: 'R' })); -struct.atoms.add(new Atom({ label: 'D' })); -struct.atoms.add(new Atom({ label: 'Be', hCount: 0 })); -struct.atoms.add(new Atom({ label: 'V', isotope: 2 })); -struct.atoms.add(new Atom({ label: 'O', isotope: 2 })); -struct.atoms.add(new Atom({ label: 'H', charge: 1 })); -struct.atoms.add(new Atom({ label: 'H', charge: 2 })); -struct.atoms.add(new Atom({ label: 'H', charge: -1 })); -struct.atoms.add(new Atom({ label: 'H', charge: -2 })); -struct.atoms.add(new Atom({ label: 'C' })); - -describe('Smiles.writeAtom method', () => { - beforeEach(() => { - smiles = new Smiles(); - smiles.saveMolecule(struct, true); - smiles.smiles = ''; - }); - - it.each([ - [0, '*'], - [2, '*'], - ])( - 'should add * to smiles if label is A or other generic atom except R(#)', - (index, expected) => { - // const index = 0 - smiles.writeAtom(struct, index); - expect(smiles.smiles).toBe(expected); - }, - ); - it('should add [*] to smiles if label is R(#)', () => { - const index = 1; - smiles.writeAtom(struct, index); - expect(smiles.smiles).toBe('[*]'); - }); - it('should add atom symbol in square brackets to smiles if label is non-hydrogen atom', () => { - const index = 3; - smiles.writeAtom(struct, index); - expect(smiles.smiles).toBe('[Be]'); - }); - it('should add atom symbol without square brackets to smiles', () => { - const index = 10; - smiles.writeAtom(struct, index); - expect(smiles.smiles).toBe('C'); - }); - it('should add atom isotope to smiles', () => { - const index = 4; - smiles.writeAtom(struct, index); - expect(smiles.smiles).toBe('[2V]'); - }); - it('should add atom and H in square brackets to smiles if it has hydro', () => { - const index = 5; - smiles.writeAtom(struct, index); - expect(smiles.smiles).toBe('[2OH2]'); - }); - it.each([ - [6, '[H+]'], - [7, '[H+2]'], - [8, '[H-]'], - [9, '[H-2]'], - ])('should add atom charge to smiles', (index, expected) => { - smiles.writeAtom(struct, index); - expect(smiles.smiles).toBe(expected); - }); - it.each([ - [1, '[Be@]'], - [2, '[Be@@]'], - ])('should add chirality to smiles', (chirality, expected) => { - const index = 3; - smiles.writeAtom(struct, index, false, false, chirality); - expect(smiles.smiles).toBe(expected); - }); -}); diff --git a/packages/ketcher-core/src/domain/serializers/smi/cis_trans.js b/packages/ketcher-core/src/domain/serializers/smi/cis_trans.js deleted file mode 100644 index 8a03ce26550..00000000000 --- a/packages/ketcher-core/src/domain/serializers/smi/cis_trans.js +++ /dev/null @@ -1,219 +0,0 @@ -/**************************************************************************** - * Copyright 2021 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. - ***************************************************************************/ - -import { Bond, Pool, Vec2 } from 'domain/entities'; - -function CisTrans(mol, neighborsFunc, context) { - this.molecule = mol; - this.bonds = new Pool(); - this.getNeighbors = neighborsFunc; - this.context = context; -} - -CisTrans.PARITY = { - NONE: 0, - CIS: 1, - TRANS: 2, -}; - -CisTrans.prototype.each = function (func) { - this.bonds.forEach(func); -}; - -CisTrans.prototype.getParity = function (idx) { - return this.bonds.get(idx).parity; -}; - -CisTrans.prototype.getSubstituents = function (idx) { - return this.bonds.get(idx).substituents; -}; - -CisTrans.prototype.sameside = function (beg, end, neiBeg, neiEnd) { - const diff = Vec2.diff(beg, end); - const norm = new Vec2(-diff.y, diff.x); - - if (!norm.normalize()) return 0; - - const normBeg = Vec2.diff(neiBeg, beg); - const normEnd = Vec2.diff(neiEnd, end); - - if (!normBeg.normalize()) return 0; - if (!normEnd.normalize()) return 0; - - const prodBeg = Vec2.dot(normBeg, norm); - const prodEnd = Vec2.dot(normEnd, norm); - - if (Math.abs(prodBeg) < 0.001 || Math.abs(prodEnd) < 0.001) return 0; - - return prodBeg * prodEnd > 0 ? 1 : -1; -}; - -CisTrans.prototype.samesides = function (iBeg, iEnd, iNeiBeg, iNeiEnd) { - return this.sameside( - this.molecule.atoms.get(iBeg).pp, - this.molecule.atoms.get(iEnd).pp, - this.molecule.atoms.get(iNeiBeg).pp, - this.molecule.atoms.get(iNeiEnd).pp, - ); -}; - -CisTrans.prototype.sortSubstituents = function (substituents) { - // eslint-disable-line max-statements - const h0 = this.molecule.atoms.get(substituents[0]).pureHydrogen(); - const h1 = - substituents[1] < 0 || - this.molecule.atoms.get(substituents[1]).pureHydrogen(); - const h2 = this.molecule.atoms.get(substituents[2]).pureHydrogen(); - const h3 = - substituents[3] < 0 || - this.molecule.atoms.get(substituents[3]).pureHydrogen(); - - if (h0 && h1) return false; - if (h2 && h3) return false; - - if (h1) { - substituents[1] = -1; - } else if (h0) { - substituents[0] = substituents[1]; - substituents[1] = -1; - } else if (substituents[0] > substituents[1]) { - swap(substituents, 0, 1); - } - - if (h3) { - substituents[3] = -1; - } else if (h2) { - substituents[2] = substituents[3]; - substituents[3] = -1; - } else if (substituents[2] > substituents[3]) { - swap(substituents, 2, 3); - } - - return true; -}; - -CisTrans.prototype.isGeomStereoBond = function (bondIdx, substituents) { - // eslint-disable-line max-statements - // it must be [C,N,Si]=[C,N,Si] bond - const bond = this.molecule.bonds.get(bondIdx); - - if (bond.type !== Bond.PATTERN.TYPE.DOUBLE) return false; - - const label1 = this.molecule.atoms.get(bond.begin).label; - const label2 = this.molecule.atoms.get(bond.end).label; - - if (label1 !== 'C' && label1 !== 'N' && label1 !== 'Si' && label1 !== 'Ge') { - return false; - } - if (label2 !== 'C' && label2 !== 'N' && label2 !== 'Si' && label2 !== 'Ge') { - return false; - } - - // the atoms should have 1 or 2 single bonds - // (apart from the double bond under consideration) - const neiBegin = this.getNeighbors.call(this.context, bond.begin); - const neiEnd = this.getNeighbors.call(this.context, bond.end); - - if ( - neiBegin.length < 2 || - neiBegin.length > 3 || - neiEnd.length < 2 || - neiEnd.length > 3 - ) { - return false; - } - - substituents[0] = -1; - substituents[1] = -1; - substituents[2] = -1; - substituents[3] = -1; - - let i; - let nei; - - for (i = 0; i < neiBegin.length; i++) { - nei = neiBegin[i]; - - if (nei.bid === bondIdx) continue; // eslint-disable-line no-continue - - if (this.molecule.bonds.get(nei.bid).type !== Bond.PATTERN.TYPE.SINGLE) { - return false; - } - - if (substituents[0] === -1) substituents[0] = nei.aid; - // (substituents[1] === -1) - else substituents[1] = nei.aid; - } - - for (i = 0; i < neiEnd.length; i++) { - nei = neiEnd[i]; - - if (nei.bid === bondIdx) continue; // eslint-disable-line no-continue - - if (this.molecule.bonds.get(nei.bid).type !== Bond.PATTERN.TYPE.SINGLE) { - return false; - } - - if (substituents[2] === -1) substituents[2] = nei.aid; - // (substituents[3] == -1) - else substituents[3] = nei.aid; - } - - const hasSameSideAtBegin = - substituents[1] !== -1 && - this.samesides(bond.begin, bond.end, substituents[0], substituents[1]) !== - -1; - const hasSameSideAtEnd = - substituents[3] !== -1 && - this.samesides(bond.begin, bond.end, substituents[2], substituents[3]) !== - -1; - - return !(hasSameSideAtBegin || hasSameSideAtEnd); -}; - -CisTrans.prototype.build = function (excludeBonds) { - this.molecule.bonds.forEach((bond, bid) => { - const ct = { - parity: 0, - substituents: [], - }; - this.bonds.set(bid, ct); - - if (Array.isArray(excludeBonds) && excludeBonds[bid]) return; - - if (!this.isGeomStereoBond(bid, ct.substituents)) return; - - if (!this.sortSubstituents(ct.substituents)) return; - - const sign = this.samesides( - bond.begin, - bond.end, - ct.substituents[0], - ct.substituents[2], - ); - - if (sign === 1) ct.parity = CisTrans.PARITY.CIS; - else if (sign === -1) ct.parity = CisTrans.PARITY.TRANS; - }); -}; - -function swap(arr, i1, i2) { - const tmp = arr[i1]; - arr[i1] = arr[i2]; - arr[i2] = tmp; -} - -export default CisTrans; diff --git a/packages/ketcher-core/src/domain/serializers/smi/dfs.js b/packages/ketcher-core/src/domain/serializers/smi/dfs.js deleted file mode 100644 index 48a5e35c062..00000000000 --- a/packages/ketcher-core/src/domain/serializers/smi/dfs.js +++ /dev/null @@ -1,187 +0,0 @@ -/**************************************************************************** - * Copyright 2021 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. - ***************************************************************************/ - -function Dfs(mol, atomData, components, nReactants) { - this.molecule = mol; - this.atom_data = atomData; - this.components = components; - this.nComponentsInReactants = -1; - this.nReactants = nReactants; - - this.vertices = new Array(this.molecule.atoms.size); // Minimum size - this.molecule.atoms.forEach((atom, aid) => { - this.vertices[aid] = new Dfs.VertexDesc(); - }, this); - - this.edges = new Array(this.molecule.bonds.size); // Minimum size - this.molecule.bonds.forEach((bond, bid) => { - this.edges[bid] = new Dfs.EdgeDesc(); - }, this); - - this.v_seq = []; -} - -Dfs.VertexDesc = function () { - this.dfs_state = 0; // 0 -- not on stack - // 1 -- on stack - // 2 -- removed from stack - this.parent_vertex = 0; // parent vertex in DFS tree - this.parent_edge = 0; // edge to parent vertex - this.branches = 0; // how many DFS branches go out from this vertex} -}; - -Dfs.EdgeDesc = function () { - this.opening_cycles = 0; // how many cycles are - // (i) starting with this edge - // and (ii) ending in this edge's first vertex - this.closing_cycle = 0; // 1 if this edge closes a cycle -}; - -Dfs.SeqElem = function (vIdx, parVertex, parEdge) { - this.idx = vIdx; // index of vertex in _graph - this.parent_vertex = parVertex; // parent vertex in DFS tree - this.parent_edge = parEdge; // edge to parent vertex -}; - -Dfs.prototype.walk = function () { - // eslint-disable-line max-statements - const vStack = []; - let i, j; - let cid = 0; - let component = 0; - - while (true) { - // eslint-disable-line no-constant-condition - if (vStack.length < 1) { - let selected = -1; - - while (cid < this.components.length && selected === -1) { - selected = this.components[cid].find((aid) => { - if (this.vertices[aid].dfs_state === 0) { - selected = aid; - return true; - } - return false; - }); - if (selected === null) { - selected = -1; - cid++; - } - if (cid === this.nReactants) this.nComponentsInReactants = component; - } - if (selected < -1) { - this.molecule.atoms.find((aid) => { - if (this.vertices[aid].dfs_state === 0) { - selected = aid; - return true; - } - return false; - }); - } - if (selected === -1) break; - this.vertices[selected].parent_vertex = -1; - this.vertices[selected].parent_edge = -1; - vStack.push(selected); - component++; - } - - const vIdx = vStack.pop(); - const parentVertex = this.vertices[vIdx].parent_vertex; - - let seqElem = new Dfs.SeqElem( - vIdx, - parentVertex, - this.vertices[vIdx].parent_edge, - ); - this.v_seq.push(seqElem); - - this.vertices[vIdx].dfs_state = 2; - - const atomD = this.atom_data[vIdx]; - - for (i = 0; i < atomD.neighbours.length; i++) { - const neiIdx = atomD.neighbours[i].aid; - const edgeIdx = atomD.neighbours[i].bid; - - if (neiIdx === parentVertex) continue; // eslint-disable-line no-continue - - if (this.vertices[neiIdx].dfs_state === 2) { - this.edges[edgeIdx].closing_cycle = 1; - - j = vIdx; - - while (j !== -1 && this.vertices[j].parent_vertex !== neiIdx) { - j = this.vertices[j].parent_vertex; - } - - if (j === -1) throw new Error('cycle unwind error'); - - this.edges[this.vertices[j].parent_edge].opening_cycles++; - this.vertices[vIdx].branches++; - - seqElem = new Dfs.SeqElem(neiIdx, vIdx, edgeIdx); - this.v_seq.push(seqElem); - } else { - if (this.vertices[neiIdx].dfs_state === 1) { - j = vStack.indexOf(neiIdx); - - if (j === -1) { - // eslint-disable-line max-depth - throw new Error('internal: removing vertex from stack'); - } - - vStack.splice(j, 1); - - const parent = this.vertices[neiIdx].parent_vertex; - - if (parent >= 0) { - // eslint-disable-line max-depth - this.vertices[parent].branches--; - } - } - - this.vertices[vIdx].branches++; - this.vertices[neiIdx].parent_vertex = vIdx; - this.vertices[neiIdx].parent_edge = edgeIdx; - this.vertices[neiIdx].dfs_state = 1; - vStack.push(neiIdx); - } - } - } -}; - -Dfs.prototype.edgeClosingCycle = function (eIdx) { - return this.edges[eIdx].closing_cycle !== 0; -}; - -Dfs.prototype.numBranches = function (vIdx) { - return this.vertices[vIdx].branches; -}; - -Dfs.prototype.numOpeningCycles = function (eIdx) { - return this.edges[eIdx].opening_cycles; -}; - -Dfs.prototype.toString = function () { - let str = ''; - this.v_seq.forEach((seqElem) => { - str += seqElem.idx + ' -> '; - }); - str += '*'; - return str; -}; - -export default Dfs; diff --git a/packages/ketcher-core/src/domain/serializers/smi/index.ts b/packages/ketcher-core/src/domain/serializers/smi/index.ts deleted file mode 100644 index 4b4215424c4..00000000000 --- a/packages/ketcher-core/src/domain/serializers/smi/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/**************************************************************************** - * Copyright 2021 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. - ***************************************************************************/ - -export * from './smi.types'; -export * from './smiSerializer'; diff --git a/packages/ketcher-core/src/domain/serializers/smi/smi.types.ts b/packages/ketcher-core/src/domain/serializers/smi/smi.types.ts deleted file mode 100644 index a750d561514..00000000000 --- a/packages/ketcher-core/src/domain/serializers/smi/smi.types.ts +++ /dev/null @@ -1,19 +0,0 @@ -/**************************************************************************** - * Copyright 2021 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. - ***************************************************************************/ - -export interface SmiSerializerOptions { - ignoreErrors: boolean; -} diff --git a/packages/ketcher-core/src/domain/serializers/smi/smiSerializer.ts b/packages/ketcher-core/src/domain/serializers/smi/smiSerializer.ts deleted file mode 100644 index b928d8f046a..00000000000 --- a/packages/ketcher-core/src/domain/serializers/smi/smiSerializer.ts +++ /dev/null @@ -1,40 +0,0 @@ -/**************************************************************************** - * Copyright 2021 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. - ***************************************************************************/ - -import { Serializer } from '../serializers.types'; -import { SmiSerializerOptions } from './smi.types'; -import { Smiles } from './smiles'; -import { Struct } from 'domain/entities'; - -export class SmiSerializer implements Serializer { - static readonly DefaultOptions: SmiSerializerOptions = { - ignoreErrors: false, - }; - - private readonly options: SmiSerializerOptions; - - constructor(options?: Partial) { - this.options = { ...SmiSerializer.DefaultOptions, ...options }; - } - - deserialize(_content: string): Struct { - throw new Error('Not implemented yet.'); - } - - serialize(struct: Struct): string { - return new Smiles().saveMolecule(struct, this.options.ignoreErrors); - } -} diff --git a/packages/ketcher-core/src/domain/serializers/smi/smiles.js b/packages/ketcher-core/src/domain/serializers/smi/smiles.js deleted file mode 100644 index 6b4bdc21bb6..00000000000 --- a/packages/ketcher-core/src/domain/serializers/smi/smiles.js +++ /dev/null @@ -1,821 +0,0 @@ -/**************************************************************************** - * Copyright 2021 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. - ***************************************************************************/ - -import { Atom, Bond, Pile, SGroup } from 'domain/entities'; -import { KetcherLogger } from 'utilities'; - -import CisTrans from './cis_trans'; -import Dfs from './dfs'; -import Stereocenters from './stereocenters'; - -export function Smiles() { - this.smiles = ''; - this.writtenAtoms = []; - this.writtenComponents = 0; - - this.ignoreErrors = false; -} - -Smiles._Atom = function (hCount) { - // eslint-disable-line no-underscore-dangle - this.neighbours = []; // Array of integer pairs {a, b} - this.aromatic = false; // has aromatic bond - this.lowercase = false; // aromatic and has to be written lowercase - this.chirality = 0; // 0 means no chirality, 1 means CCW pyramid, 2 means CW pyramid - this.branch_cnt = 0; // runs from 0 to (branches - 1) - this.paren_written = false; - this.h_count = hCount; - this.parent = -1; -}; - -// NB: only loops of length up to 6 are included here -Smiles.prototype.isBondInRing = function (bid) { - return this.inLoop[bid]; -}; - -Smiles.prototype.saveMolecule = function (struct, ignoreErrors) { - // eslint-disable-line max-statements - let i, j, k; - - if (!ignoreErrors) this.ignoreErrors = ignoreErrors; - - // [RB]: KETCHER-498 (Incorrect smile-string for multiple Sgroup) - // TODO the fix is temporary, still need to implement error handling/reporting - // BEGIN - struct = struct.clone( - undefined, - undefined, - !struct.hasRxnArrow(), // make it drop multiple reactions - undefined, - undefined, - undefined, - ); - struct.initHalfBonds(); - struct.initNeighbors(); - struct.sortNeighbors(); - struct.setImplicitHydrogen(); - struct.sgroups.forEach((sg) => { - if (sg.type === 'MUL') { - try { - SGroup.prepareMulForSaving(sg, struct); - } catch (error) { - KetcherLogger.error('smiles.js::Smiles.prototype.saveMolecule', error); - throw Error('Bad s-group (' + error.message + ')'); - } - } - // 'SMILES data format doesn\'t support s-groups' - }); - // END - - this.atoms = new Array(struct.atoms.size); - - struct.atoms.forEach((atom, aid) => { - this.atoms[aid] = new Smiles._Atom(atom.implicitH); // eslint-disable-line no-underscore-dangle - }); - - // From the SMILES specification: - // Please note that only atoms on the following list - // can be considered aromatic: C, N, O, P, S, As, Se, and * (wildcard). - const allowedLowercase = ['B', 'C', 'N', 'O', 'P', 'S', 'Se', 'As']; - - // Detect atoms that have aromatic bonds and count neighbours - struct.bonds.forEach((bond, bid) => { - if (bond.type === Bond.PATTERN.TYPE.AROMATIC) { - this.atoms[bond.begin].aromatic = true; - if (allowedLowercase.indexOf(struct.atoms.get(bond.begin).label) !== -1) { - this.atoms[bond.begin].lowercase = true; - } - this.atoms[bond.end].aromatic = true; - if (allowedLowercase.indexOf(struct.atoms.get(bond.end).label) !== -1) { - this.atoms[bond.end].lowercase = true; - } - } - this.atoms[bond.begin].neighbours.push({ aid: bond.end, bid }); - this.atoms[bond.end].neighbours.push({ aid: bond.begin, bid }); - }); - - this.inLoop = (function () { - struct.prepareLoopStructure(); - let bondsInLoops = new Pile(); - struct.loops.forEach((loop) => { - if (loop.hbs.length <= 6) { - const hbids = loop.hbs.map((hbid) => struct.halfBonds.get(hbid).bid); - bondsInLoops = bondsInLoops.union(new Pile(hbids)); - } - }); - const inLoop = {}; - bondsInLoops.forEach((bid) => { - inLoop[bid] = 1; - }); - return inLoop; - })(); - - this.touchedCistransbonds = 0; - this.markCisTrans(struct); - - const components = struct.getComponents(); - const componentsAll = components.reactants.concat(components.products); - - const walk = new Dfs( - struct, - this.atoms, - componentsAll, - components.reactants.length, - ); - - walk.walk(); - this.atoms.forEach((atom) => { - atom.neighbours = []; - }); - - // fill up neighbor lists for the stereocenters calculation - for (i = 0; i < walk.v_seq.length; i++) { - const seqEl = walk.v_seq[i]; - const vIdx = seqEl.idx; - const eIdx = seqEl.parent_edge; - const vPrevIdx = seqEl.parent_vertex; - - if (eIdx >= 0) { - const atom = this.atoms[vIdx]; - - const openingCycles = walk.numOpeningCycles(eIdx); - - for (j = 0; j < openingCycles; j++) { - this.atoms[vPrevIdx].neighbours.push({ aid: -1, bid: -1 }); - } - - if (walk.edgeClosingCycle(eIdx)) { - for (k = 0; k < atom.neighbours.length; k++) { - if (atom.neighbours[k].aid === -1) { - // eslint-disable-line max-depth - atom.neighbours[k].aid = vPrevIdx; - atom.neighbours[k].bid = eIdx; - break; - } - } - if (k === atom.neighbours.length) { - throw new Error('internal: can not put closing bond to its place'); - } - } else { - atom.neighbours.push({ aid: vPrevIdx, bid: eIdx }); - atom.parent = vPrevIdx; - } - this.atoms[vPrevIdx].neighbours.push({ aid: vIdx, bid: eIdx }); - } - } - - try { - // detect chiral configurations - const stereocenters = new Stereocenters( - struct, - function (idx) { - return this.atoms[idx].neighbours; - }, - this, - ); - stereocenters.buildFromBonds(this.ignoreErrors); - - stereocenters.each((sc, atomIdx) => { - // eslint-disable-line max-statements - // if (sc.type < MoleculeStereocenters::ATOM_AND) - // continue; - - let implicitHIdx = -1; - - if (sc.pyramid[3] === -1) implicitHIdx = 3; - /* - else for (j = 0; j < 4; j++) - if (ignored_vertices[pyramid[j]]) - { - implicit_h_idx = j; - break; - } - */ - - const pyramidMapping = []; - let counter = 0; - - const atom = this.atoms[atomIdx]; - - if (atom.parent !== -1) { - for (k = 0; k < 4; k++) { - if (sc.pyramid[k] === atom.parent) { - pyramidMapping[counter++] = k; - break; - } - } - } - - if (implicitHIdx !== -1) pyramidMapping[counter++] = implicitHIdx; - - for (j = 0; j !== atom.neighbours.length; j++) { - if (atom.neighbours[j].aid === atom.parent) continue; // eslint-disable-line no-continue - - for (k = 0; k < 4; k++) { - if (atom.neighbours[j].aid === sc.pyramid[k]) { - if (counter >= 4) throw new Error('internal: pyramid overflow'); - pyramidMapping[counter++] = k; - break; - } - } - } - - if (counter === 4) { - // move the 'from' atom to the end - counter = pyramidMapping[0]; - pyramidMapping[0] = pyramidMapping[1]; - pyramidMapping[1] = pyramidMapping[2]; - pyramidMapping[2] = pyramidMapping[3]; - pyramidMapping[3] = counter; - } else if (counter !== 3) { - throw new Error('cannot calculate chirality'); - } - - if (Stereocenters.isPyramidMappingRigid(pyramidMapping)) { - this.atoms[atomIdx].chirality = 1; - } else this.atoms[atomIdx].chirality = 2; - }); - } catch (e) { - KetcherLogger.error('smiles.js::Smiles.prototype.saveMolecule', e); - // TODO: add error handler call - } - - // write the SMILES itself - - // cycle_numbers[i] == -1 means that the number is available - // cycle_numbers[i] == n means that the number is used by vertex n - const cycleNumbers = []; - - cycleNumbers.push(0); // never used - - let firstComponent = true; - - for (i = 0; i < walk.v_seq.length; i++) { - const seqEl = walk.v_seq[i]; - const vIdx = seqEl.idx; - const eIdx = seqEl.parent_edge; - const vPrevIdx = seqEl.parent_vertex; - let writeAtom = true; - - if (vPrevIdx >= 0) { - if (walk.numBranches(vPrevIdx) > 1) { - if ( - this.atoms[vPrevIdx].branch_cnt > 0 && - this.atoms[vPrevIdx].paren_written - ) { - this.smiles += ')'; - } - } - - const openingCycles = walk.numOpeningCycles(eIdx); - - for (j = 0; j < openingCycles; j++) { - for (k = 1; k < cycleNumbers.length; k++) { - if (cycleNumbers[k] === -1) { - // eslint-disable-line max-depth - break; - } - } - if (k === cycleNumbers.length) cycleNumbers.push(vPrevIdx); - else cycleNumbers[k] = vPrevIdx; - - this.writeCycleNumber(k); - } - - if (vPrevIdx >= 0) { - const branches = walk.numBranches(vPrevIdx); - - if (branches > 1 && this.atoms[vPrevIdx].branch_cnt < branches - 1) { - if (walk.edgeClosingCycle(eIdx)) { - // eslint-disable-line max-depth - this.atoms[vPrevIdx].paren_written = false; - } else { - this.smiles += '('; - this.atoms[vPrevIdx].paren_written = true; - } - } - - this.atoms[vPrevIdx].branch_cnt++; - - if (this.atoms[vPrevIdx].branch_cnt > branches) { - throw new Error('unexpected branch'); - } - } - - const bond = struct.bonds.get(eIdx); - - let dir = 0; - - if (bond.type === Bond.PATTERN.TYPE.SINGLE) { - dir = this.calcBondDirection(struct, eIdx, vPrevIdx); - } - - if ( - (dir === 1 && vIdx === bond.end) || - (dir === 2 && vIdx === bond.begin) - ) { - this.smiles += '/'; - } else if ( - (dir === 2 && vIdx === bond.end) || - (dir === 1 && vIdx === bond.begin) - ) { - this.smiles += '\\'; - } else if (bond.type === Bond.PATTERN.TYPE.ANY) { - this.smiles += '~'; - } else if (bond.type === Bond.PATTERN.TYPE.DOUBLE) { - this.smiles += '='; - } else if (bond.type === Bond.PATTERN.TYPE.TRIPLE) { - this.smiles += '#'; - } else if (bond.type === Bond.PATTERN.TYPE.SINGLE_OR_AROMATIC) { - this.smiles += '-,:'; - } else if (bond.type === Bond.PATTERN.TYPE.DOUBLE_OR_AROMATIC) { - this.smiles += '=,:'; - } else if (bond.type === Bond.PATTERN.TYPE.SINGLE_OR_DOUBLE) { - this.smiles += '-,='; - } else if ( - bond.type === Bond.PATTERN.TYPE.AROMATIC && - (!this.atoms[bond.begin].lowercase || - !this.atoms[bond.end].lowercase || - !this.isBondInRing(eIdx)) - ) { - this.smiles += ':'; - } - // TODO: Check if this : is needed - else if ( - bond.type === Bond.PATTERN.TYPE.SINGLE && - this.atoms[bond.begin].aromatic && - this.atoms[bond.end].aromatic - ) { - this.smiles += '-'; - } - - if (walk.edgeClosingCycle(eIdx)) { - for (j = 1; j < cycleNumbers.length; j++) { - if (cycleNumbers[j] === vIdx) break; - } - - if (j === cycleNumbers.length) - throw new Error('cycle number not found'); - - this.writeCycleNumber(j); - - cycleNumbers[j] = -1; - writeAtom = false; - } - } else { - if (!firstComponent) { - this.smiles += - this.writtenComponents === walk.nComponentsInReactants && - walk.nReactants !== 0 - ? '>>' - : '.'; // when walk.nReactants === 0 - not reaction - } - firstComponent = false; - this.writtenComponents++; - } - if (writeAtom) { - this.writeAtom( - struct, - vIdx, - this.atoms[vIdx].aromatic, - this.atoms[vIdx].lowercase, - this.atoms[vIdx].chirality, - ); - this.writtenAtoms.push(seqEl.idx); - } - } - - this.comma = false; - - this.writeRadicals(struct); - - if (this.comma) this.smiles += '|'; - - return this.smiles; -}; - -Smiles.prototype.writeCycleNumber = function (n) { - if (n > 0 && n < 10) this.smiles += n; - else if (n >= 10 && n < 100) this.smiles += '%' + n; - else if (n >= 100 && n < 1000) this.smiles += '%%' + n; - else throw new Error('bad cycle number: ' + n); -}; - -Smiles.prototype.writeAtom = function ( - mol, - idx, - aromatic, - lowercase, - chirality, -) { - // eslint-disable-line max-params, max-statements - const atom = mol.atoms.get(idx); - let needBrackets = false; - let hydro = -1; - let aam = 0; - - /* - if (mol.haveQueryAtoms()) - { - query_atom = &mol.getQueryAtom(idx); - - if (query_atom->type == QUERY_ATOM_RGROUP) - { - if (mol.getRGroups()->isRGroupAtom(idx)) - { - const Array &rg = mol.getRGroups()->getSiteRGroups(idx); - - if (rg.length != 1) - throw Error("rgroup count %d", rg.length); - - _output.printf("[&%d]", rg[0] + 1); - } - else - _output.printf("[&%d]", 1); - - return; - } - } - */ - - if (atom.label === 'A') { - this.smiles += '*'; - return; - } - - if (atom.label === 'R' || atom.label === 'R#') { - this.smiles += '[*]'; - return; - } - - // KETCHER-598 (Ketcher does not save AAM into reaction SMILES) - // BEGIN - // if (this.atom_atom_mapping) - // aam = atom_atom_mapping[idx]; - aam = atom.aam; - // END - - if ( - atom.label !== 'C' && - atom.label !== 'P' && - atom.label !== 'N' && - atom.label !== 'S' && - atom.label !== 'O' && - atom.label !== 'Cl' && - atom.label !== 'F' && - atom.label !== 'Br' && - atom.label !== 'B' && - atom.label !== 'I' - ) { - needBrackets = true; - } - - if ( - atom.explicitValence >= 0 || - atom.radical !== 0 || - chirality > 0 || - (aromatic && atom.label !== 'C' && atom.label !== 'O') || - (aromatic && - atom.label === 'C' && - this.atoms[idx].neighbours.length < 3 && - this.atoms[idx].h_count === 0) - ) { - hydro = this.atoms[idx].h_count; - } - - let label = atom.label; - if (atom.atomList && !atom.atomList.notList) { - label = atom.atomList.label(); - needBrackets = false; // atom list label already has brackets - } else if (atom.isPseudo() || atom.atomList?.notList) { - label = '*'; - needBrackets = false; - } else if ( - chirality || - (atom.charge !== 0 && atom.charge !== null) || - atom.isotope > 0 || - hydro >= 0 || - aam > 0 - ) { - needBrackets = true; - } - - if (needBrackets) { - if (hydro === -1) hydro = this.atoms[idx].h_count; - this.smiles += '['; - } - - if (atom.isotope > 0) this.smiles += atom.isotope; - - if (lowercase) this.smiles += label.toLowerCase(); - else this.smiles += label; - - if (chirality > 0) { - if (chirality === 1) this.smiles += '@'; - // chirality == 2 - else this.smiles += '@@'; - - if (atom.implicitH > 1) { - throw new Error(atom.implicitH + ' implicit H near stereocenter'); - } - } - - if (atom.label !== 'H') { - if (hydro > 1 || (hydro === 0 && !needBrackets)) this.smiles += 'H' + hydro; - else if (hydro === 1) this.smiles += 'H'; - } - - if (atom.charge > 1) this.smiles += '+' + atom.charge; - else if (atom.charge < -1) this.smiles += atom.charge; - else if (atom.charge === 1) this.smiles += '+'; - else if (atom.charge === -1) this.smiles += '-'; - - if (aam > 0) this.smiles += ':' + aam; - - if (needBrackets) this.smiles += ']'; - - /* - if (mol.getRGroupFragment() != 0) - { - for (i = 0; i < 2; i++) - { - int j; - - for (j = 0; mol.getRGroupFragment()->getAttachmentPoint(i, j) != -1; j++) - if (idx == mol.getRGroupFragment()->getAttachmentPoint(i, j)) - { - _output.printf("([*])"); - break; - } - - if (mol.getRGroupFragment()->getAttachmentPoint(i, j) != -1) - break; - } - } - */ -}; - -Smiles.prototype.markCisTrans = function (mol) { - this.cis_trans = new CisTrans( - mol, - function (idx) { - return this.atoms[idx].neighbours; - }, - this, - ); - this.cis_trans.build(); - this.dbonds = new Array(mol.bonds.size); - - mol.bonds.forEach((bond, bid) => { - this.dbonds[bid] = { - ctbond_beg: -1, - ctbond_end: -1, - saved: 0, - }; - }); - - this.cis_trans.each((ct, bid) => { - const bond = mol.bonds.get(bid); - - if (ct.parity !== 0 && !this.isBondInRing(bid)) { - const neiBeg = this.atoms[bond.begin].neighbours; - const neiEnd = this.atoms[bond.end].neighbours; - let aromFailBeg = true; - let aromFailEnd = true; - - neiBeg.forEach((nei) => { - if ( - nei.bid !== bid && - mol.bonds.get(nei.bid).type === Bond.PATTERN.TYPE.SINGLE - ) { - aromFailBeg = false; - } - }); - - neiEnd.forEach((nei) => { - if ( - nei.bid !== bid && - mol.bonds.get(nei.bid).type === Bond.PATTERN.TYPE.SINGLE - ) { - aromFailEnd = false; - } - }); - - if (aromFailBeg || aromFailEnd) return; - - neiBeg.forEach((nei) => { - if (nei.bid === bid) return; - if (mol.bonds.get(nei.bid).begin === bond.begin) { - this.dbonds[nei.bid].ctbond_beg = bid; - } else this.dbonds[nei.bid].ctbond_end = bid; - }); - - neiEnd.forEach((nei) => { - if (nei.bid === bid) return; - if (mol.bonds.get(nei.bid).begin === bond.end) { - this.dbonds[nei.bid].ctbond_beg = bid; - } else this.dbonds[nei.bid].ctbond_end = bid; - }); - } - }); -}; - -Smiles.prototype.updateSideBonds = function (mol, bondIdx) { - // eslint-disable-line max-statements - const bond = mol.bonds.get(bondIdx); - const subst = this.cis_trans.getSubstituents(bondIdx); - const parity = this.cis_trans.getParity(bondIdx); - - const sidebonds = [-1, -1, -1, -1]; - - sidebonds[0] = mol.findBondId(subst[0], bond.begin); - if (subst[1] !== -1) sidebonds[1] = mol.findBondId(subst[1], bond.begin); - - sidebonds[2] = mol.findBondId(subst[2], bond.end); - if (subst[3] !== -1) sidebonds[3] = mol.findBondId(subst[3], bond.end); - - let n1 = 0; - let n2 = 0; - let n3 = 0; - let n4 = 0; - - if (this.dbonds[sidebonds[0]].saved !== 0) { - if ( - (this.dbonds[sidebonds[0]].saved === 1 && - mol.bonds.get(sidebonds[0]).begin === bond.begin) || - (this.dbonds[sidebonds[0]].saved === 2 && - mol.bonds.get(sidebonds[0]).end === bond.begin) - ) { - n1++; - } else n2++; - } - if (sidebonds[1] !== -1 && this.dbonds[sidebonds[1]].saved !== 0) { - if ( - (this.dbonds[sidebonds[1]].saved === 2 && - mol.bonds.get(sidebonds[1]).begin === bond.begin) || - (this.dbonds[sidebonds[1]].saved === 1 && - mol.bonds.get(sidebonds[1]).end === bond.begin) - ) { - n1++; - } else n2++; - } - if (this.dbonds[sidebonds[2]].saved !== 0) { - if ( - (this.dbonds[sidebonds[2]].saved === 1 && - mol.bonds.get(sidebonds[2]).begin === bond.end) || - (this.dbonds[sidebonds[2]].saved === 2 && - mol.bonds.get(sidebonds[2]).end === bond.end) - ) { - n3++; - } else n4++; - } - if (sidebonds[3] !== -1 && this.dbonds[sidebonds[3]].saved !== 0) { - if ( - (this.dbonds[sidebonds[3]].saved === 2 && - mol.bonds.get(sidebonds[3]).begin === bond.end) || - (this.dbonds[sidebonds[3]].saved === 1 && - mol.bonds.get(sidebonds[3]).end === bond.end) - ) { - n3++; - } else n4++; - } - - if (parity === CisTrans.PARITY.CIS) { - n1 += n3; - n2 += n4; - } else { - n1 += n4; - n2 += n3; - } - - if (n1 > 0 && n2 > 0) throw new Error('incompatible cis-trans configuration'); - - if (n1 === 0 && n2 === 0) return false; - - if (n1 > 0) { - this.dbonds[sidebonds[0]].saved = - mol.bonds.get(sidebonds[0]).begin === bond.begin ? 1 : 2; - if (sidebonds[1] !== -1) { - this.dbonds[sidebonds[1]].saved = - mol.bonds.get(sidebonds[1]).begin === bond.begin ? 2 : 1; - } - - this.dbonds[sidebonds[2]].saved = - (mol.bonds.get(sidebonds[2]).begin === bond.end) === - (parity === CisTrans.PARITY.CIS) - ? 1 - : 2; - if (sidebonds[3] !== -1) { - this.dbonds[sidebonds[3]].saved = - (mol.bonds.get(sidebonds[3]).begin === bond.end) === - (parity === CisTrans.PARITY.CIS) - ? 2 - : 1; - } - } - if (n2 > 0) { - this.dbonds[sidebonds[0]].saved = - mol.bonds.get(sidebonds[0]).begin === bond.begin ? 2 : 1; - if (sidebonds[1] !== -1) { - this.dbonds[sidebonds[1]].saved = - mol.bonds.get(sidebonds[1]).begin === bond.begin ? 1 : 2; - } - - this.dbonds[sidebonds[2]].saved = - (mol.bonds.get(sidebonds[2]).begin === bond.end) === - (parity === CisTrans.PARITY.CIS) - ? 2 - : 1; - if (sidebonds[3] !== -1) { - this.dbonds[sidebonds[3]].saved = - (mol.bonds.get(sidebonds[3]).begin === bond.end) === - (parity === CisTrans.PARITY.CIS) - ? 1 - : 2; - } - } - - return true; -}; - -Smiles.prototype.calcBondDirection = function (mol, idx, vprev) { - let ntouched; - - if ( - this.dbonds[idx].ctbond_beg === -1 && - this.dbonds[idx].ctbond_end === -1 - ) { - return 0; - } - - if (mol.bonds.get(idx).type !== Bond.PATTERN.TYPE.SINGLE) { - throw new Error('internal: directed bond type ' + mol.bonds.get(idx).type); - } - - while (true) { - // eslint-disable-line no-constant-condition - ntouched = 0; - this.cis_trans.each((ct, bid) => { - if (ct.parity !== 0 && !this.isBondInRing(bid)) { - if (this.updateSideBonds(mol, bid)) ntouched++; - } - }); - if (ntouched === this.touchedCistransbonds) break; - this.touchedCistransbonds = ntouched; - } - - if (this.dbonds[idx].saved === 0) { - if (vprev === mol.bonds.get(idx).begin) this.dbonds[idx].saved = 1; - else this.dbonds[idx].saved = 2; - } - - return this.dbonds[idx].saved; -}; - -Smiles.prototype.writeRadicals = function (mol) { - // eslint-disable-line max-statements - const marked = new Array(this.writtenAtoms.length); - let i, j; - - for (i = 0; i < this.writtenAtoms.length; i++) { - if (marked[i]) continue; // eslint-disable-line no-continue - - const radical = mol.atoms.get(this.writtenAtoms[i]).radical; - - if (radical === 0) continue; // eslint-disable-line no-continue - - if (this.comma) { - this.smiles += ','; - } else { - this.smiles += ' |'; - this.comma = true; - } - - if (radical === Atom.PATTERN.RADICAL.SINGLET) this.smiles += '^3:'; - else if (radical === Atom.PATTERN.RADICAL.DOUPLET) this.smiles += '^1:'; - // RADICAL_TRIPLET - else this.smiles += '^4:'; - - this.smiles += i; - - for (j = i + 1; j < this.writtenAtoms.length; j++) { - if (mol.atoms.get(this.writtenAtoms[j]).radical === radical) { - marked[j] = true; - this.smiles += ',' + j; - } - } - } -}; diff --git a/packages/ketcher-core/src/domain/serializers/smi/stereocenters.js b/packages/ketcher-core/src/domain/serializers/smi/stereocenters.js deleted file mode 100644 index 7e96ebdff1f..00000000000 --- a/packages/ketcher-core/src/domain/serializers/smi/stereocenters.js +++ /dev/null @@ -1,729 +0,0 @@ -/**************************************************************************** - * Copyright 2021 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. - ***************************************************************************/ - -import { Bond, Pile, Pool, Vec2 } from 'domain/entities'; - -function Stereocenters(mol, neighborsFunc, context) { - this.molecule = mol; - this.atoms = new Pool(); - this.getNeighbors = neighborsFunc; - this.context = context; -} - -Stereocenters.prototype.each = function (func, context) { - this.atoms.forEach(func, context); -}; - -Stereocenters.prototype.buildFromBonds = function ( - /* const int *atom_types, const int *atom_groups, const int *bond_orientations, */ ignoreErrors, -) { - const atoms = this.molecule.atoms; - const 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 - */ - - const alleneMask = new Pile(); - atoms.forEach((atom, aid) => { - const neiList = this.getNeighbors.call(this.context, aid); - if (neiList.length !== 2) return false; - const nei1 = neiList[0]; - const nei2 = neiList[1]; - // check atom labels - if ( - [aid, nei1.aid, nei2.aid].findIndex( - (aid) => ['C', 'Si'].indexOf(atoms.get(aid).label) < 0, - this, - ) >= 0 - ) { - return false; - } - - // check adjacent bond types - if ( - [nei1.bid, nei2.bid].findIndex( - (bid) => bonds.get(bid).type !== Bond.PATTERN.TYPE.DOUBLE, - this, - ) >= 0 - ) { - return false; - } - - // get the other neighbors of the two adjacent atoms except for the central atom - const nei1nei = this.getNeighbors - .call(this.context, nei1.aid) - .filter((nei) => nei.aid !== aid); - const nei2nei = this.getNeighbors - .call(this.context, nei2.aid) - .filter((nei) => nei.aid !== aid); - if ( - nei1nei.length < 1 || - nei1nei.length > 2 || - nei2nei.length < 1 || - nei2nei.length > 2 - ) { - return false; - } - - if ( - nei1nei - .concat(nei2nei) - .findIndex( - (nei) => bonds.get(nei.bid).type !== Bond.PATTERN.TYPE.SINGLE, - this, - ) >= 0 - ) { - return false; - } - - if ( - nei1nei - .concat(nei2nei) - .findIndex( - (nei) => bonds.get(nei.bid).stereo === Bond.PATTERN.STEREO.EITHER, - this, - ) >= 0 - ) { - return false; - } - alleneMask.add(nei1.aid).add(nei2.aid); - return true; - }); - - if (alleneMask.size > 0) { - // TODO: add error handler call - // legacy message: This structure may contain allenes, which cannot be represented in the SMILES notation. Relevant stereo-information will be discarded. - - atoms.forEach((atom, aid) => { - if (alleneMask.has(aid)) return; - /* - if (atom_types[atom_idx] == 0) - continue; - */ - const neiList = this.getNeighbors.call(this.context, aid); - let stereocenter = false; - - neiList.find(function (nei) { - const bond = this.molecule.bonds.get(nei.bid); - - if (bond.type === Bond.PATTERN.TYPE.SINGLE && bond.begin === aid) { - if ( - bond.stereo === Bond.PATTERN.STEREO.UP || - bond.stereo === Bond.PATTERN.STEREO.DOWN - ) { - stereocenter = true; - return true; - } - } - return false; - }, this); - - if (!stereocenter) return; - - if (ignoreErrors) { - this.buildOneCenter( - aid /* , atom_groups[atom_idx], atom_types[atom_idx], bond_orientations */, - ); - } else { - this.buildOneCenter( - aid /* , atom_groups[atom_idx], atom_types[atom_idx], bond_orientations */, - ); - } - }); - } -}; - -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 */, -) { - const atom = this.molecule.atoms.get(atomIdx); - const { edgeIds, nPureHydrogens, nDoubleBonds } = collectEdgeData( - this, - atomIdx, - atom, - ); - const degree = edgeIds.length; - const implicitDegree = resolveImplicitDegree(atom, degree, nDoubleBonds); - - validateHydrogenCounts(degree, implicitDegree, nPureHydrogens); - - const stereocenter = createEmptyStereocenter(); - - if (degree === 4) { - buildTetrahedralPyramid(this, atomIdx, edgeIds, stereocenter); - } else if (degree === 3) { - buildTrigonalPyramid(this, atomIdx, edgeIds, stereocenter, implicitDegree); - } - - this.atoms.set(atomIdx, stereocenter); -}; - -function createEmptyStereocenter() { - return { - group: 0, - type: 0, - pyramid: [-1, -1, -1, -1], - }; -} - -function collectEdgeData(stereocenters, atomIdx, atom) { - const edgeIds = []; - let nPureHydrogens = 0; - let nDoubleBonds = 0; - - const neiList = stereocenters.getNeighbors.call( - stereocenters.context, - atomIdx, - ); - - if (neiList.length > 4) { - throw new Error( - 'stereocenter with %d bonds are not supported' + neiList.length, - ); - } - - neiList.forEach((nei) => { - const neiAtom = stereocenters.molecule.atoms.get(nei.aid); - const bond = stereocenters.molecule.bonds.get(nei.bid); - const edge = { - edge_idx: nei.bid, - nei_idx: nei.aid, - rank: nei.aid, - vec: Vec2.diff(neiAtom.pp, atom.pp).yComplement(), - }; - - if (neiAtom.pureHydrogen()) { - nPureHydrogens++; - edge.rank = 10000; - } else if (neiAtom.label === 'H') { - edge.rank = 5000; - } - - if (!edge.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'); - } - if (bond.type === Bond.PATTERN.TYPE.AROMATIC) { - throw new Error('aromatic bonds not allowed near stereocenter'); - } - if (bond.type === Bond.PATTERN.TYPE.DOUBLE) { - nDoubleBonds++; - } - - edgeIds.push(edge); - }); - - sortEdgeIds(edgeIds); - - return { edgeIds, nPureHydrogens, nDoubleBonds }; -} - -function resolveImplicitDegree(atom, degree, nDoubleBonds) { - const match = Stereocenters.allowed_stereocenters.find( - (as) => - as.elem === atom.label && - as.charge === atom.charge && - as.degree === degree && - as.n_double_bonds === nDoubleBonds, - ); - - if (match) { - return match.implicit_degree; - } - - throw new Error( - 'unknown stereocenter configuration: ' + - atom.label + - ', charge ' + - atom.charge + - ', ' + - degree + - ' bonds (' + - nDoubleBonds + - ' double)', - ); -} - -function validateHydrogenCounts(degree, implicitDegree, nPureHydrogens) { - 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', - ); - } -} - -function buildTetrahedralPyramid( - stereocenters, - atomIdx, - edgeIds, - stereocenter, -) { - sortEdgeIds(edgeIds); - - const { mainIdx, mainDir } = findPrimaryStereoBond( - stereocenters, - atomIdx, - edgeIds, - ); - const { main2, side1, side2 } = findOppositeBondIndices(edgeIds, mainIdx); - - validateOppositeBondStereo( - stereocenters, - atomIdx, - edgeIds, - mainDir, - main2, - side1, - side2, - ); - - const lastAtomDir = determineLastAtomDir(mainIdx, main2, mainDir); - - assignTetrahedralPyramid(stereocenter, edgeIds, lastAtomDir); -} - -function findPrimaryStereoBond(stereocenters, atomIdx, edgeIds) { - for (let index = 0; index < edgeIds.length; index++) { - const stereo = stereocenters.getBondStereo( - atomIdx, - edgeIds[index].edge_idx, - ); - - if ( - stereo === Bond.PATTERN.STEREO.UP || - stereo === Bond.PATTERN.STEREO.DOWN - ) { - return { mainIdx: index, mainDir: stereo }; - } - } - - throw new Error('none of 4 bonds going from stereocenter is stereobond'); -} - -function findOppositeBondIndices(edgeIds, mainIdx) { - const candidates = [ - createBondCandidate(mainIdx, 1, 2, 3), - createBondCandidate(mainIdx, 2, 1, 3), - createBondCandidate(mainIdx, 3, 2, 1), - ]; - - for (const candidate of candidates) { - if (isOppositePair(edgeIds, mainIdx, candidate)) { - return candidate; - } - } - - throw new Error('internal error: can not find opposite bond'); -} - -function createBondCandidate(mainIdx, mainOffset, sideOffset1, sideOffset2) { - return { - main2: (mainIdx + mainOffset) % 4, - side1: (mainIdx + sideOffset1) % 4, - side2: (mainIdx + sideOffset2) % 4, - }; -} - -function isOppositePair(edgeIds, mainIdx, candidate) { - const first = Stereocenters.xyzzy( - edgeIds[mainIdx].vec, - edgeIds[candidate.main2].vec, - edgeIds[candidate.side1].vec, - ); - const second = Stereocenters.xyzzy( - edgeIds[mainIdx].vec, - edgeIds[candidate.main2].vec, - edgeIds[candidate.side2].vec, - ); - - return first + second === 3 || first + second === 12; -} - -function validateOppositeBondStereo( - stereocenters, - atomIdx, - edgeIds, - mainDir, - main2, - side1, - side2, -) { - const oppositeDir = stereocenters.getBondStereo( - atomIdx, - edgeIds[main2].edge_idx, - ); - - if ( - (mainDir === Bond.PATTERN.STEREO.UP && - oppositeDir === Bond.PATTERN.STEREO.DOWN) || - (mainDir === Bond.PATTERN.STEREO.DOWN && - oppositeDir === Bond.PATTERN.STEREO.UP) - ) { - throw new Error('stereo types of the opposite bonds mismatch'); - } - - const sideDir1 = stereocenters.getBondStereo( - atomIdx, - edgeIds[side1].edge_idx, - ); - const sideDir2 = stereocenters.getBondStereo( - atomIdx, - edgeIds[side2].edge_idx, - ); - - if (mainDir === sideDir1 || mainDir === sideDir2) { - throw new Error('stereo types of non-opposite bonds match'); - } -} - -function determineLastAtomDir(mainIdx, main2, mainDir) { - if (mainIdx === 3 || main2 === 3) { - return mainDir; - } - - return mainDir === Bond.PATTERN.STEREO.UP - ? Bond.PATTERN.STEREO.DOWN - : Bond.PATTERN.STEREO.UP; -} - -function assignTetrahedralPyramid(stereocenter, edgeIds, lastAtomDir) { - 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; -} - -function buildTrigonalPyramid( - stereocenters, - atomIdx, - edgeIds, - stereocenter, - implicitDegree, -) { - sortEdgeIds(edgeIds); - const stereos = edgeIds.map((edge) => - stereocenters.getBondStereo(atomIdx, edge.edge_idx), - ); - const { nUp, nDown } = countStereoBonds(stereos); - - if (implicitDegree === 4) { - buildTrigonalWithImplicitHydrogen( - stereocenters, - atomIdx, - edgeIds, - stereocenter, - nUp, - nDown, - ); - return; - } - - buildTrigonalWithoutImplicitHydrogen( - stereocenters, - atomIdx, - edgeIds, - stereocenter, - nUp, - nDown, - ); -} - -function buildTrigonalWithImplicitHydrogen( - stereocenters, - atomIdx, - edgeIds, - stereocenter, - nUp, - nDown, -) { - 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'); - } - - let lastAtomDir; - - if (nUp === 2) { - lastAtomDir = Bond.PATTERN.STEREO.DOWN; - } else if (nDown === 2) { - lastAtomDir = Bond.PATTERN.STEREO.UP; - } else { - lastAtomDir = determineLastDirForSingleStereoBond( - stereocenters, - atomIdx, - edgeIds, - ); - } - - assignTrigonalPyramidByDirection(stereocenter, edgeIds, lastAtomDir); -} - -function determineLastDirForSingleStereoBond(stereocenters, atomIdx, edgeIds) { - for (let index = 0; index < edgeIds.length; index++) { - const dir = stereocenters.getBondStereo(atomIdx, edgeIds[index].edge_idx); - - if (dir === Bond.PATTERN.STEREO.UP || dir === Bond.PATTERN.STEREO.DOWN) { - const side1 = (index + 1) % edgeIds.length; - const side2 = (index + 2) % edgeIds.length; - const xyz = Stereocenters.xyzzy( - edgeIds[side1].vec, - edgeIds[side2].vec, - edgeIds[index].vec, - ); - - if (xyz === 3 || xyz === 4) { - throw new Error('degenerate case for 3 bonds near stereoatom'); - } - - if (xyz === 1) { - return dir; - } - - return dir === Bond.PATTERN.STEREO.UP - ? Bond.PATTERN.STEREO.DOWN - : Bond.PATTERN.STEREO.UP; - } - } - - throw new Error('internal error: can not find up or down bond'); -} - -function assignTrigonalPyramidByDirection(stereocenter, edgeIds, lastAtomDir) { - 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; -} - -function buildTrigonalWithoutImplicitHydrogen( - stereocenters, - atomIdx, - edgeIds, - stereocenter, - nUp, - nDown, -) { - if (nDown > 0 && nUp > 0) { - throw new Error('one bond up, one bond down -- indefinite case'); - } - if (nDown === 0 && nUp === 0) { - throw new Error('no up-down bonds attached to stereocenter'); - } - - let dir = nUp > 0 ? 1 : -1; - - if (edgesShareHalfPlane(edgeIds)) { - 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; -} - -function edgesShareHalfPlane(edgeIds) { - return ( - 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 - ); -} - -function countStereoBonds(stereos) { - let nUp = 0; - let nDown = 0; - - stereos.forEach((stereo) => { - if (stereo === Bond.PATTERN.STEREO.UP) { - nUp++; - } else if (stereo === Bond.PATTERN.STEREO.DOWN) { - nDown++; - } - }); - - return { nUp, nDown }; -} - -function sortEdgeIds(edgeIds) { - edgeIds.sort((a, b) => a.rank - b.rank); -} - -Stereocenters.prototype.getBondStereo = function (centerIdx, edgeIdx) { - const 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) { - const eps = 0.001; - - const sine1 = Vec2.cross(v1, v2); - const cosine1 = Vec2.dot(v1, v2); - - const sine2 = Vec2.cross(v1, u); - const 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) { - const res = (v1.x - v3.x) * (v2.y - v3.y) - (v1.y - v3.y) * (v2.x - v3.x); // eslint-disable-line no-mixed-operators - const eps = 0.001; - - if (res > eps) return 1; - if (res < -eps) return -1; - - throw new Error('degenerate triangle'); -}; - -Stereocenters.isPyramidMappingRigid = function (mapping) { - const arr = mapping.slice(); - let 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) { - const tmp = arr[i1]; - arr[i1] = arr[i2]; - arr[i2] = tmp; -} - -export default Stereocenters;