From 11ed34ed011f160aeeeae2b8b29b34def07f53db Mon Sep 17 00:00:00 2001 From: David Meijer Date: Mon, 10 Aug 2026 17:49:15 +0200 Subject: [PATCH 1/5] UPD: make display isotope tags optional --- .../client/src/components/MotifHoverCard.tsx | 2 +- .../src/components/SmilesDrawerContainer.js | 19 +++++++++++++------ .../components/workspace/WorkspaceRules.tsx | 1 + 3 files changed, 15 insertions(+), 7 deletions(-) diff --git a/gui/src/client/src/components/MotifHoverCard.tsx b/gui/src/client/src/components/MotifHoverCard.tsx index 7c63b0d..17bb851 100644 --- a/gui/src/client/src/components/MotifHoverCard.tsx +++ b/gui/src/client/src/components/MotifHoverCard.tsx @@ -40,7 +40,7 @@ function MotifHoverContent({ name, hint }: { name: string; hint?: string }) { {smiles ? ( <> - + ) : ( diff --git a/gui/src/client/src/components/SmilesDrawerContainer.js b/gui/src/client/src/components/SmilesDrawerContainer.js index dc2bc23..251a72f 100644 --- a/gui/src/client/src/components/SmilesDrawerContainer.js +++ b/gui/src/client/src/components/SmilesDrawerContainer.js @@ -5,8 +5,9 @@ import { useColorScheme } from '@mui/material/styles'; class CustomSvgDrawer extends SmilesDrawer.SvgDrawer { - constructor(options) { + constructor(options, showIsotopes = false) { super(options); + this.showIsotopes = showIsotopes; const themeOverrides = { light: { @@ -126,11 +127,15 @@ class CustomSvgDrawer extends SmilesDrawer.SvgDrawer { }; }; - // loop over all atoms and set atom.bracket to null + // loop over all atoms and, unless isotopes were asked to be kept (used to show + // R-group numbers, e.g. [1*]/[2*], in the rules browser and motif hover card), + // clear atom.bracket so isotope/charge annotations don't clutter the drawing. for (let i = 0; i < graph.vertices.length; i++) { let vertex = graph.vertices[i]; let atom = vertex.value; - atom.bracket = null; + if (!this.showIsotopes) { + atom.bracket = null; + } // make sure COOH is drawn fully instead of displayed with text if (atom.element === 'C') { @@ -170,12 +175,14 @@ class CustomSvgDrawer extends SmilesDrawer.SvgDrawer { * @property {number} size width & height of the drawing * @property {HighlightAtom[]} [highlightAtoms] array of `[atomNumber, color]` * @property {string} [themeOverride] force “light” or “dark” drawing theme + * @property {boolean} [showIsotopes] draw isotope numbers (e.g. R-group + * tags like [1*]/[2*]) instead of hiding them */ /** * @param {Props} props */ -const SmilesDrawerContainer = ({ identifier, smiles, size, highlightAtoms = [], themeOverride = '' }) => { +const SmilesDrawerContainer = ({ identifier, smiles, size, highlightAtoms = [], themeOverride = '', showIsotopes = false }) => { const { mode, systemMode } = useColorScheme(); const [error, setError] = useState(null); @@ -191,7 +198,7 @@ const SmilesDrawerContainer = ({ identifier, smiles, size, highlightAtoms = [], // A fresh drawer instance per draw call, since it isn't safe to reuse // once it holds a graph for a previous (possibly differently-sized) SMILES. - let drawer = new CustomSvgDrawer({ width: size, height: size }); + let drawer = new CustomSvgDrawer({ width: size, height: size }, showIsotopes); try { SmilesDrawer.parse( @@ -214,7 +221,7 @@ const SmilesDrawerContainer = ({ identifier, smiles, size, highlightAtoms = [], console.error('SmilesDrawerContainer: unexpected error', err); setError('Could not render this structure.'); } - }, [identifier, smiles, highlightAtoms, size, themeOverride, mode, systemMode]); + }, [identifier, smiles, highlightAtoms, size, themeOverride, showIsotopes, mode, systemMode]); if (error) { return ( diff --git a/gui/src/client/src/components/workspace/WorkspaceRules.tsx b/gui/src/client/src/components/workspace/WorkspaceRules.tsx index 4e281af..84e7e2a 100644 --- a/gui/src/client/src/components/workspace/WorkspaceRules.tsx +++ b/gui/src/client/src/components/workspace/WorkspaceRules.tsx @@ -101,6 +101,7 @@ function MatchingRuleRow({ rule }: { rule: MatchingRule }) { identifier={`matching-rule-${rule.id}`} smiles={rule.displaySmiles || rule.smiles} size={STRUCTURE_SIZE} + showIsotopes /> From 7d7e80d059e650cb9c0ddb2f1a24edabd5c79214 Mon Sep 17 00:00:00 2001 From: David Meijer Date: Mon, 10 Aug 2026 17:49:27 +0200 Subject: [PATCH 2/5] WIP: adding display SMILES for polyketide motifs --- src/retromol/data/mxn.yml | 66 +++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/retromol/data/mxn.yml b/src/retromol/data/mxn.yml index fa0b98c..f2a24d0 100644 --- a/src/retromol/data/mxn.yml +++ b/src/retromol/data/mxn.yml @@ -1,260 +1,326 @@ - name: A1 smiles: "O=C(O)CC(=O)SO" + display_smiles: "O=C(CC([1*])=O)S[2*]" pseudonyms: ["PK_A", "PK"] - name: A1 smiles: "O=C(O)C=C(O)SO" + display_smiles: "O=C(CC([1*])=O)S[2*]" pseudonyms: ["PK_A", "PK"] - name: A2^R smiles: "C[C@@H](C(SO)=O)C(O)=O" + display_smiles: "C[C@@H](C(S[2*])=O)C([1*])=O" stereochemistry: true pseudonyms: ["A2", "PK_A", "PK"] - name: A2^S smiles: "C[C@H](C(SO)=O)C(O)=O" + display_smiles: "C[C@H](C(S[2*])=O)C([1*])=O" stereochemistry: true pseudonyms: ["A2", "PK_A", "PK"] - name: A2 smiles: "CC(C(=O)O)C(=O)SO" + display_smiles: "O=C(C(C)C([1*])=O)S[2*]" pseudonyms: ["PK_A", "PK"] - name: A2 smiles: "CC(C(=O)O)=C(O)SO" + display_smiles: "O=C(C(C)C([1*])=O)S[2*]" pseudonyms: ["PK_A", "PK"] - name: A3 smiles: "CC(C)(C(=O)O)C(=O)SO" + display_smiles: "CC(C)(C(=O)[1*])C(=O)S[2*]" pseudonyms: ["PK_A", "PK"] - name: A4^R smiles: "CC[C@@H](C(SO)=O)C(O)=O" + display_smiles: "CC[C@@H](C(S[2*])=O)C([1*])=O" stereochemistry: true pseudonyms: ["A4", "PK_A", "PK"] - name: A4^S smiles: "CC[C@H](C(SO)=O)C(O)=O" + display_smiles: "CC[C@H](C(S[2*])=O)C([1*])=O" stereochemistry: true pseudonyms: ["A4", "PK_A", "PK"] - name: A4 smiles: "CCC(C(=O)O)C(=O)SO" + display_smiles: "CCC(C(=O)[1*])C(=O)S[2*]" pseudonyms: ["PK_A", "PK"] - name: A5^R smiles: "O=C([C@H](C(SO)=O)O)O" + display_smiles: "O=C([C@H](C(S[2*])=O)O)[1*]" stereochemistry: true pseudonyms: ["A5", "PK_A", "PK"] - name: A5^S smiles: "O=C([C@@H](C(SO)=O)O)O" + display_smiles: "O=C([C@@H](C(S[2*])=O)O)[1*]" stereochemistry: true pseudonyms: ["A5", "PK_A", "PK"] - name: A5 smiles: "O=C(O)C(O)C(=O)SO" + display_smiles: "O=C(C(C(S[2*])=O)O)[1*]" pseudonyms: ["PK_A", "PK"] - name: A5 smiles: "O=C(C(O)=C(SO)O)O" + display_smiles: "O=C(C(C(S[2*])=O)O)[1*]" pseudonyms: ["PK_A", "PK"] - name: A6^R smiles: "C[C@@](C(O)=O)(C(SO)=O)O" + display_smiles: "C[C@@](C([1*])=O)(C(S[2*])=O)O" stereochemistry: true pseudonyms: ["A6", "PK_A", "PK"] - name: A6^S smiles: "C[C@](C(O)=O)(C(SO)=O)O" + display_smiles: "C[C@](C([1*])=O)(C(S[2*])=O)O" stereochemistry: true pseudonyms: ["A6", "PK_A", "PK"] - name: A6 smiles: "CC(O)(C(=O)O)C(=O)SO" + display_smiles: "CC(C([1*])=O)(C(S[2*])=O)O" pseudonyms: ["PK_A", "PK"] - name: A7^R smiles: "O=C([C@H](C(SO)=O)CO)O" + display_smiles: "O=C([C@H](C(S[2*])=O)CO)[1*]" stereochemistry: true pseudonyms: ["A7", "PK_A", "PK"] - name: A7^S smiles: "O=C([C@@H](C(SO)=O)CO)O" + display_smiles: "O=C([C@@H](C(S[2*])=O)CO)[1*]" stereochemistry: true pseudonyms: ["A7", "PK_A", "PK"] - name: A7 smiles: "O=C(O)C(CO)C(=O)SO" + display_smiles: "O=C(C(C(S[2*])=O)CO)[1*]" pseudonyms: ["PK_A", "PK"] - name: A7 smiles: "O=C(O)C(CO)=C(O)SO" + display_smiles: "O=C(C(C(S[2*])=O)CO)[1*]" pseudonyms: ["PK_A", "PK"] - name: A8 smiles: "C=C(CC(=O)O)SO" + display_smiles: "C=C(CC(=O)[1*])S[2*]" pseudonyms: ["PK_A", "PK"] - name: A8 smiles: "CC(=CC(=O)O)SO" + display_smiles: "C=C(CC(=O)[1*])S[2*]" pseudonyms: ["PK_A", "PK"] - name: A9^R smiles: "C=C([C@@H](C(O)=O)C)SO" + display_smiles: "C=C([C@@H](C([1*])=O)C)S[2*]" stereochemistry: true pseudonyms: ["A9", "PK_A", "PK"] - name: A9^S smiles: "C=C([C@H](C(O)=O)C)SO" + display_smiles: "C=C([C@H](C([1*])=O)C)S[2*]" stereochemistry: true pseudonyms: ["A9", "PK_A", "PK"] - name: A9 smiles: "C=C(SO)C(C)C(=O)O" + display_smiles: "C=C(C(C([1*])=O)C)S[2*]" pseudonyms: ["PK_A", "PK"] - name: A10^R smiles: "C=C([C@@H](C(O)=O)O)SO" + display_smiles: "C=C([C@@H](C([1*])=O)O)S[2*]" stereochemistry: true pseudonyms: ["A10", "PK_A", "PK"] - name: A10^S smiles: "C=C([C@H](C(O)=O)O)SO" + display_smiles: "C=C([C@H](C([1*])=O)O)S[2*]" stereochemistry: true pseudonyms: ["A10", "PK_A", "PK"] - name: A10 smiles: "C=C(SO)C(O)C(=O)O" + display_smiles: "C=C(C(C([1*])=O)O)S[2*]" pseudonyms: ["PK_A", "PK"] - name: A11 smiles: "C=C(SO)C(=O)C(=O)O" + display_smiles: "C=C(S[2*])C(=O)C(=O)[1*]" pseudonyms: ["PK_A", "PK"] - name: B^R1 smiles: "O=C(C[C@@H](SO)O)O" + display_smiles: "O=C(C[C@@H](S[2*])O)[1*]" stereochemistry: true pseudonyms: ["B1", "PK_B", "PK"] - name: B^S1 smiles: "O=C(C[C@H](SO)O)O" + display_smiles: "O=C(C[C@H](S[2*])O)[1*]" stereochemistry: true pseudonyms: ["B1", "PK_B", "PK"] - name: B1 smiles: "O=C(O)CC(O)SO" + display_smiles: "O=C(CC(S[2*])O)[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R2^R smiles: "C[C@@H]([C@@H](SO)O)C(O)=O" + display_smiles: "C[C@@H]([C@@H](S[2*])O)C([1*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B^R2^S smiles: "C[C@H]([C@@H](SO)O)C(O)=O" + display_smiles: "C[C@H]([C@@H](S[2*])O)C([1*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B^R2 smiles: "CC([C@@H](SO)O)C(O)=O" + display_smiles: "CC([C@@H](S[2*])O)C([1*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B^S2^R smiles: "C[C@@H]([C@H](SO)O)C(O)=O" + display_smiles: "C[C@@H]([C@H](S[2*])O)C([1*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B^S2^S smiles: "C[C@H]([C@H](SO)O)C(O)=O" + display_smiles: "C[C@H]([C@H](S[2*])O)C([1*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B^S2 smiles: "CC([C@H](SO)O)C(O)=O" + display_smiles: "CC([C@H](S[2*])O)C([1*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B2 smiles: "CC(C(=O)O)C(O)SO" + display_smiles: "CC(C(=O)[1*])C(O)S[2*]" pseudonyms: ["PK_B", "PK"] - name: B^R3 smiles: "CC(C(O)=O)([C@@H](SO)O)C" + display_smiles: "CC(C([1*])=O)([C@@H](S[2*])O)C" stereochemistry: true pseudonyms: ["B3", "PK_B", "PK"] - name: B^S3 smiles: "CC(C(O)=O)([C@H](SO)O)C" + display_smiles: "CC(C([1*])=O)([C@H](S[2*])O)C" stereochemistry: true pseudonyms: ["B3", "PK_B", "PK"] - name: B3 smiles: "CC(C)(C(=O)O)C(O)SO" + display_smiles: "CC(C)(C(=O)[1*])C(O)S[2*]" pseudonyms: ["PK_B", "PK"] - name: B^R4^R smiles: "CC[C@@H]([C@@H](SO)O)C(O)=O" + display_smiles: "CC[C@@H]([C@@H](S[2*])O)C([1*])=O" stereochemistry: true pseudonyms: ["B4", "PK_B", "PK"] - name: B^R4^S smiles: "CC[C@H]([C@@H](SO)O)C(O)=O" + display_smiles: "CC[C@H]([C@@H](S[2*])O)C([1*])=O" stereochemistry: true pseudonyms: ["B4", "PK_B", "PK"] - name: B^S4^R smiles: "CC[C@@H]([C@H](SO)O)C(O)=O" + display_smiles: "CC[C@@H]([C@H](S[2*])O)C([1*])=O" stereochemistry: true pseudonyms: ["B4", "PK_B", "PK"] - name: B^S4^S smiles: "CC[C@H]([C@H](SO)O)C(O)=O" + display_smiles: "CC[C@H]([C@H](S[2*])O)C([1*])=O" stereochemistry: true pseudonyms: ["B4", "PK_B", "PK"] - name: B4 smiles: "CCC(C(=O)O)C(O)SO" + display_smiles: "CCC(C(=O)[1*])C(O)S[2*]" pseudonyms: ["PK_B", "PK"] - name: B^R5^R smiles: "O=C([C@H]([C@@H](SO)O)O)O" + display_smiles: "O=C([C@H]([C@@H](S[2*])O)O)[1*]" stereochemistry: true pseudonyms: ["B5", "PK_B", "PK"] - name: B^R5^S smiles: "O=C([C@@H]([C@@H](SO)O)O)O" + display_smiles: "O=C([C@@H]([C@@H](S[2*])O)O)[1*]" stereochemistry: true pseudonyms: ["B5", "PK_B", "PK"] - name: B^S5^R smiles: "O=C([C@H]([C@H](SO)O)O)O" + display_smiles: "O=C([C@H]([C@H](S[2*])O)O)[1*]" stereochemistry: true pseudonyms: ["B5", "PK_B", "PK"] - name: B^S5^S smiles: "O=C([C@@H]([C@H](SO)O)O)O" + display_smiles: "O=C([C@@H]([C@H](S[2*])O)O)[1*]" stereochemistry: true pseudonyms: ["B5", "PK_B", "PK"] - name: B5 smiles: "O=C(O)C(O)C(O)SO" + display_smiles: "O=C([1*])C(O)C(O)S[2*]" pseudonyms: ["PK_B", "PK"] - name: B^R6^R smiles: "C[C@@](C(O)=O)([C@@H](SO)O)O" + display_smiles: "C[C@@](C([1*])=O)([C@@H](S[2*])O)O" stereochemistry: true pseudonyms: ["B6", "PK_B", "PK"] - name: B^R6^S smiles: "C[C@](C(O)=O)([C@@H](SO)O)O" + display_smiles: "C[C@](C([1*])=O)([C@@H](S[2*])O)O" stereochemistry: true pseudonyms: ["B6", "PK_B", "PK"] - name: B^S6^R smiles: "C[C@@](C(O)=O)([C@H](SO)O)O" + display_smiles: "C[C@@](C([1*])=O)([C@H](S[2*])O)O" stereochemistry: true pseudonyms: ["B6", "PK_B", "PK"] - name: B^S6^S smiles: "C[C@](C(O)=O)([C@H](SO)O)O" + display_smiles: "C[C@](C([1*])=O)([C@H](S[2*])O)O" stereochemistry: true pseudonyms: ["B6", "PK_B", "PK"] - name: B6 smiles: "CC(O)(C(=O)O)C(O)SO" + display_smiles: "CC(O)(C(=O)[1*])C(O)S[2*]" pseudonyms: ["PK_B", "PK"] - name: B^R7^R smiles: "O=C([C@H]([C@@H](SO)O)CO)O" + display_smiles: "O=C([C@H]([C@@H](S[2*])O)CO)[1*]" stereochemistry: true pseudonyms: ["B7", "PK_B", "PK"] - name: B^R7^S smiles: "O=C([C@@H]([C@@H](SO)O)CO)O" + display_smiles: "O=C([C@@H]([C@@H](S[2*])O)CO)[1*]" stereochemistry: true pseudonyms: ["B7", "PK_B", "PK"] - name: B^S7^R smiles: "O=C([C@H]([C@H](SO)O)CO)O" + display_smiles: "O=C([C@H]([C@H](S[2*])O)CO)[1*]" stereochemistry: true pseudonyms: ["B7", "PK_B", "PK"] - name: B^S7^S smiles: "O=C([C@@H]([C@H](SO)O)CO)O" + display_smiles: "O=C([C@@H]([C@H](S[2*])O)CO)[1*]" stereochemistry: true pseudonyms: ["B7", "PK_B", "PK"] - name: B7 smiles: "O=C(O)C(CO)C(O)SO" + display_smiles: "O=C([1*])C(CO)C(O)S[2*]" pseudonyms: ["PK_B", "PK"] - name: B^R8 smiles: "C[C@](CC(O)=O)(SO)O" + display_smiles: "C[C@](CC([1*])=O)(S[2*])O" stereochemistry: true pseudonyms: ["B8", "PK_B", "PK"] - name: B^S8 smiles: "C[C@@](CC(O)=O)(SO)O" + display_smiles: "C[C@@](CC([1*])=O)(S[2*])O" stereochemistry: true pseudonyms: ["B8", "PK_B", "PK"] - name: B8 smiles: "CC(O)(CC(=O)O)SO" + display_smiles: "CC(O)(CC(=O)[1*])S[2*]" pseudonyms: ["PK_B", "PK"] - name: B^R9^R From 5d0b7fbe930e5cee42fddabf0381d395f84a6782 Mon Sep 17 00:00:00 2001 From: David Meijer Date: Mon, 10 Aug 2026 19:16:36 +0200 Subject: [PATCH 3/5] ENH: adding display SMILES for polyketide motifs --- src/retromol/data/mxn.yml | 158 +++++++++++++++++++++++++++----------- 1 file changed, 113 insertions(+), 45 deletions(-) diff --git a/src/retromol/data/mxn.yml b/src/retromol/data/mxn.yml index f2a24d0..ac977da 100644 --- a/src/retromol/data/mxn.yml +++ b/src/retromol/data/mxn.yml @@ -101,496 +101,564 @@ - name: A8 smiles: "C=C(CC(=O)O)SO" - display_smiles: "C=C(CC(=O)[1*])S[2*]" + display_smiles: "C=C(CC(=O)S[2*])[1*]" pseudonyms: ["PK_A", "PK"] - name: A8 smiles: "CC(=CC(=O)O)SO" - display_smiles: "C=C(CC(=O)[1*])S[2*]" + display_smiles: "C=C(CC(=O)S[2*])[1*]" pseudonyms: ["PK_A", "PK"] - name: A9^R smiles: "C=C([C@@H](C(O)=O)C)SO" - display_smiles: "C=C([C@@H](C([1*])=O)C)S[2*]" + display_smiles: "C=C([C@@H](C(S[2*])=O)C)[1*]" stereochemistry: true pseudonyms: ["A9", "PK_A", "PK"] - name: A9^S smiles: "C=C([C@H](C(O)=O)C)SO" - display_smiles: "C=C([C@H](C([1*])=O)C)S[2*]" + display_smiles: "C=C([C@H](C(S[2*])=O)C)[1*]" stereochemistry: true pseudonyms: ["A9", "PK_A", "PK"] - name: A9 smiles: "C=C(SO)C(C)C(=O)O" - display_smiles: "C=C(C(C([1*])=O)C)S[2*]" + display_smiles: "C=C(C(C(S[2*])=O)C)[1*]" pseudonyms: ["PK_A", "PK"] - name: A10^R smiles: "C=C([C@@H](C(O)=O)O)SO" - display_smiles: "C=C([C@@H](C([1*])=O)O)S[2*]" + display_smiles: "C=C([C@@H](C(S[2*])=O)O)[1*]" stereochemistry: true pseudonyms: ["A10", "PK_A", "PK"] - name: A10^S smiles: "C=C([C@H](C(O)=O)O)SO" - display_smiles: "C=C([C@H](C([1*])=O)O)S[2*]" + display_smiles: "C=C([C@H](C(S[2*])=O)O)[1*]" stereochemistry: true pseudonyms: ["A10", "PK_A", "PK"] - name: A10 smiles: "C=C(SO)C(O)C(=O)O" - display_smiles: "C=C(C(C([1*])=O)O)S[2*]" + display_smiles: "C=C(C(C(S[2*])=O)O)[1*]" pseudonyms: ["PK_A", "PK"] - name: A11 smiles: "C=C(SO)C(=O)C(=O)O" - display_smiles: "C=C(S[2*])C(=O)C(=O)[1*]" + display_smiles: "C=C([1*])C(=O)C(=O)S[2*]" pseudonyms: ["PK_A", "PK"] - name: B^R1 smiles: "O=C(C[C@@H](SO)O)O" - display_smiles: "O=C(C[C@@H](S[2*])O)[1*]" + display_smiles: "O=C(C[C@@H]([1*])O)S[2*]" stereochemistry: true pseudonyms: ["B1", "PK_B", "PK"] - name: B^S1 smiles: "O=C(C[C@H](SO)O)O" - display_smiles: "O=C(C[C@H](S[2*])O)[1*]" + display_smiles: "O=C(C[C@H]([1*])O)S[2*]" stereochemistry: true pseudonyms: ["B1", "PK_B", "PK"] - name: B1 smiles: "O=C(O)CC(O)SO" - display_smiles: "O=C(CC(S[2*])O)[1*]" + display_smiles: "O=C(CC([1*])O)S[2*]" pseudonyms: ["PK_B", "PK"] - name: B^R2^R smiles: "C[C@@H]([C@@H](SO)O)C(O)=O" - display_smiles: "C[C@@H]([C@@H](S[2*])O)C([1*])=O" + display_smiles: "C[C@@H]([C@@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B^R2^S smiles: "C[C@H]([C@@H](SO)O)C(O)=O" - display_smiles: "C[C@H]([C@@H](S[2*])O)C([1*])=O" + display_smiles: "C[C@H]([C@@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B^R2 smiles: "CC([C@@H](SO)O)C(O)=O" - display_smiles: "CC([C@@H](S[2*])O)C([1*])=O" + display_smiles: "CC([C@@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B^S2^R smiles: "C[C@@H]([C@H](SO)O)C(O)=O" - display_smiles: "C[C@@H]([C@H](S[2*])O)C([1*])=O" + display_smiles: "C[C@@H]([C@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B^S2^S smiles: "C[C@H]([C@H](SO)O)C(O)=O" - display_smiles: "C[C@H]([C@H](S[2*])O)C([1*])=O" + display_smiles: "C[C@H]([C@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B^S2 smiles: "CC([C@H](SO)O)C(O)=O" - display_smiles: "CC([C@H](S[2*])O)C([1*])=O" + display_smiles: "CC([C@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B2", "PK_B", "PK"] - name: B2 smiles: "CC(C(=O)O)C(O)SO" - display_smiles: "CC(C(=O)[1*])C(O)S[2*]" + display_smiles: "CC(C(=O)S[2*])C(O)[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R3 smiles: "CC(C(O)=O)([C@@H](SO)O)C" - display_smiles: "CC(C([1*])=O)([C@@H](S[2*])O)C" + display_smiles: "CC(C(S[2*])=O)([C@@H]([1*])O)C" stereochemistry: true pseudonyms: ["B3", "PK_B", "PK"] - name: B^S3 smiles: "CC(C(O)=O)([C@H](SO)O)C" - display_smiles: "CC(C([1*])=O)([C@H](S[2*])O)C" + display_smiles: "CC(C(S[2*])=O)([C@H]([1*])O)C" stereochemistry: true pseudonyms: ["B3", "PK_B", "PK"] - name: B3 smiles: "CC(C)(C(=O)O)C(O)SO" - display_smiles: "CC(C)(C(=O)[1*])C(O)S[2*]" + display_smiles: "CC(C)(C(=O)S[2*])C(O)[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R4^R smiles: "CC[C@@H]([C@@H](SO)O)C(O)=O" - display_smiles: "CC[C@@H]([C@@H](S[2*])O)C([1*])=O" + display_smiles: "CC[C@@H]([C@@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B4", "PK_B", "PK"] - name: B^R4^S smiles: "CC[C@H]([C@@H](SO)O)C(O)=O" - display_smiles: "CC[C@H]([C@@H](S[2*])O)C([1*])=O" + display_smiles: "CC[C@H]([C@@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B4", "PK_B", "PK"] - name: B^S4^R smiles: "CC[C@@H]([C@H](SO)O)C(O)=O" - display_smiles: "CC[C@@H]([C@H](S[2*])O)C([1*])=O" + display_smiles: "CC[C@@H]([C@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B4", "PK_B", "PK"] - name: B^S4^S smiles: "CC[C@H]([C@H](SO)O)C(O)=O" - display_smiles: "CC[C@H]([C@H](S[2*])O)C([1*])=O" + display_smiles: "CC[C@H]([C@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B4", "PK_B", "PK"] - name: B4 smiles: "CCC(C(=O)O)C(O)SO" - display_smiles: "CCC(C(=O)[1*])C(O)S[2*]" + display_smiles: "CCC(C(=O)S[2*])C(O)[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R5^R smiles: "O=C([C@H]([C@@H](SO)O)O)O" - display_smiles: "O=C([C@H]([C@@H](S[2*])O)O)[1*]" + display_smiles: "O=C([C@H]([C@@H]([1*])O)O)S[2*]" stereochemistry: true pseudonyms: ["B5", "PK_B", "PK"] - name: B^R5^S smiles: "O=C([C@@H]([C@@H](SO)O)O)O" - display_smiles: "O=C([C@@H]([C@@H](S[2*])O)O)[1*]" + display_smiles: "O=C([C@@H]([C@@H]([1*])O)O)S[2*]" stereochemistry: true pseudonyms: ["B5", "PK_B", "PK"] - name: B^S5^R smiles: "O=C([C@H]([C@H](SO)O)O)O" - display_smiles: "O=C([C@H]([C@H](S[2*])O)O)[1*]" + display_smiles: "O=C([C@H]([C@H]([1*])O)O)S[2*]" stereochemistry: true pseudonyms: ["B5", "PK_B", "PK"] - name: B^S5^S smiles: "O=C([C@@H]([C@H](SO)O)O)O" - display_smiles: "O=C([C@@H]([C@H](S[2*])O)O)[1*]" + display_smiles: "O=C([C@@H]([C@H]([1*])O)O)S[2*]" stereochemistry: true pseudonyms: ["B5", "PK_B", "PK"] - name: B5 smiles: "O=C(O)C(O)C(O)SO" - display_smiles: "O=C([1*])C(O)C(O)S[2*]" + display_smiles: "O=C(S[2*])C(O)C(O)[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R6^R smiles: "C[C@@](C(O)=O)([C@@H](SO)O)O" - display_smiles: "C[C@@](C([1*])=O)([C@@H](S[2*])O)O" + display_smiles: "C[C@@](C(S[2*])=O)([C@@H]([1*])O)O" stereochemistry: true pseudonyms: ["B6", "PK_B", "PK"] - name: B^R6^S smiles: "C[C@](C(O)=O)([C@@H](SO)O)O" - display_smiles: "C[C@](C([1*])=O)([C@@H](S[2*])O)O" + display_smiles: "C[C@](C(S[2*])=O)([C@@H]([1*])O)O" stereochemistry: true pseudonyms: ["B6", "PK_B", "PK"] - name: B^S6^R smiles: "C[C@@](C(O)=O)([C@H](SO)O)O" - display_smiles: "C[C@@](C([1*])=O)([C@H](S[2*])O)O" + display_smiles: "C[C@@](C(S[2*])=O)([C@H]([1*])O)O" stereochemistry: true pseudonyms: ["B6", "PK_B", "PK"] - name: B^S6^S smiles: "C[C@](C(O)=O)([C@H](SO)O)O" - display_smiles: "C[C@](C([1*])=O)([C@H](S[2*])O)O" + display_smiles: "C[C@](C(S[2*])=O)([C@H]([1*])O)O" stereochemistry: true pseudonyms: ["B6", "PK_B", "PK"] - name: B6 smiles: "CC(O)(C(=O)O)C(O)SO" - display_smiles: "CC(O)(C(=O)[1*])C(O)S[2*]" + display_smiles: "CC(O)(C(=O)S[2*])C(O)[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R7^R smiles: "O=C([C@H]([C@@H](SO)O)CO)O" - display_smiles: "O=C([C@H]([C@@H](S[2*])O)CO)[1*]" + display_smiles: "O=C([C@H]([C@@H]([1*])O)CO)S[2*]" stereochemistry: true pseudonyms: ["B7", "PK_B", "PK"] - name: B^R7^S smiles: "O=C([C@@H]([C@@H](SO)O)CO)O" - display_smiles: "O=C([C@@H]([C@@H](S[2*])O)CO)[1*]" + display_smiles: "O=C([C@@H]([C@@H]([1*])O)CO)S[2*]" stereochemistry: true pseudonyms: ["B7", "PK_B", "PK"] - name: B^S7^R smiles: "O=C([C@H]([C@H](SO)O)CO)O" - display_smiles: "O=C([C@H]([C@H](S[2*])O)CO)[1*]" + display_smiles: "O=C([C@H]([C@H]([1*])O)CO)S[2*]" stereochemistry: true pseudonyms: ["B7", "PK_B", "PK"] - name: B^S7^S smiles: "O=C([C@@H]([C@H](SO)O)CO)O" - display_smiles: "O=C([C@@H]([C@H](S[2*])O)CO)[1*]" + display_smiles: "O=C([C@@H]([C@H]([1*])O)CO)S[2*]" stereochemistry: true pseudonyms: ["B7", "PK_B", "PK"] - name: B7 smiles: "O=C(O)C(CO)C(O)SO" - display_smiles: "O=C([1*])C(CO)C(O)S[2*]" + display_smiles: "O=C(S[2*])C(CO)C(O)[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R8 smiles: "C[C@](CC(O)=O)(SO)O" - display_smiles: "C[C@](CC([1*])=O)(S[2*])O" + display_smiles: "C[C@](CC(S[2*])=O)([1*])O" stereochemistry: true pseudonyms: ["B8", "PK_B", "PK"] - name: B^S8 smiles: "C[C@@](CC(O)=O)(SO)O" - display_smiles: "C[C@@](CC([1*])=O)(S[2*])O" + display_smiles: "C[C@@](CC(S[2*])=O)([1*])O" stereochemistry: true pseudonyms: ["B8", "PK_B", "PK"] - name: B8 smiles: "CC(O)(CC(=O)O)SO" - display_smiles: "CC(O)(CC(=O)[1*])S[2*]" + display_smiles: "CC(O)(CC(=O)S[2*])[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R9^R smiles: "C[C@@H]([C@](O)(SO)C)C(O)=O" + display_smiles: "C[C@@H]([C@](O)([1*])C)C(S[2*])=O" stereochemistry: true pseudonyms: ["B9", "PK_B", "PK"] - name: B^R9^S smiles: "C[C@H]([C@](O)(SO)C)C(O)=O" + display_smiles: "C[C@H]([C@](O)([1*])C)C(S[2*])=O" stereochemistry: true pseudonyms: ["B9", "PK_B", "PK"] - name: B^S9^R smiles: "C[C@@H]([C@@](O)(SO)C)C(O)=O" + display_smiles: "C[C@@H]([C@@](O)([1*])C)C(S[2*])=O" stereochemistry: true pseudonyms: ["B9", "PK_B", "PK"] - name: B^S9^S smiles: "C[C@H]([C@@](O)(SO)C)C(O)=O" + display_smiles: "C[C@H]([C@@](O)([1*])C)C(S[2*])=O" stereochemistry: true pseudonyms: ["B9", "PK_B", "PK"] - name: B9 smiles: "CC(C(=O)O)C(C)(O)SO" + display_smiles: "CC(C(=O)S[2*])C(C)(O)[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R10^R smiles: "C[C@@](SO)([C@@H](C(O)=O)O)O" + display_smiles: "C[C@@]([1*])([C@@H](C(S[2*])=O)O)O" stereochemistry: true pseudonyms: ["B10", "PK_B", "PK"] - name: B^R10^S smiles: "C[C@@](SO)([C@H](C(O)=O)O)O" + display_smiles: "C[C@@]([1*])([C@H](C(S[2*])=O)O)O" stereochemistry: true pseudonyms: ["B10", "PK_B", "PK"] - name: B^S10^R smiles: "C[C@](SO)([C@@H](C(O)=O)O)O" + display_smiles: "C[C@]([1*])([C@@H](C(S[2*])=O)O)O" stereochemistry: true pseudonyms: ["B10", "PK_B", "PK"] - name: B^S10^S smiles: "C[C@](SO)([C@H](C(O)=O)O)O" + display_smiles: "C[C@]([1*])([C@H](C(S[2*])=O)O)O" stereochemistry: true pseudonyms: ["B10", "PK_B", "PK"] - name: B10 smiles: "CC(O)(SO)C(O)C(=O)O" + display_smiles: "CC(O)([1*])C(O)C(=O)S[2*]" pseudonyms: ["PK_B", "PK"] - name: B11 smiles: "O=C(O)C(=O)C(O)SO" + display_smiles: "O=C(S[2*])C(=O)C(O)[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R12^R smiles: "N[C@@H]([C@@H](SO)O)C(O)=O" + display_smiles: "N[C@@H]([C@@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B12", "PK_B", "PK"] - name: B^R12^S smiles: "N[C@H]([C@@H](SO)O)C(O)=O" + display_smiles: "N[C@H]([C@@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B12", "PK_B", "PK"] - name: B^S12^R smiles: "N[C@@H]([C@H](SO)O)C(O)=O" + display_smiles: "N[C@@H]([C@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B12", "PK_B", "PK"] - name: B^S12^S smiles: "N[C@H]([C@H](SO)O)C(O)=O" + display_smiles: "N[C@H]([C@H]([1*])O)C(S[2*])=O" stereochemistry: true pseudonyms: ["B12", "PK_B", "PK"] - name: B12 smiles: "NC(C(=O)O)C(O)SO" + display_smiles: "NC(C(=O)S[2*])C(O)[1*]" pseudonyms: ["PK_B", "PK"] - name: B^R13 smiles: "OS[C@H](C(C(O)=O)C(O)=O)O" + display_smiles: "[1*][C@H](C(C(S[2*])=O)C(O)=O)O" stereochemistry: true pseudonyms: ["B13", "PK_B", "PK"] - name: B^S13 smiles: "OS[C@@H](C(C(O)=O)C(O)=O)O" + display_smiles: "[1*][C@@H](C(C(O)=O)C(S[2*])=O)O" stereochemistry: true pseudonyms: ["B13", "PK_B", "PK"] - name: B13 smiles: "OSC(O)C(C(=O)O)C(=O)O" + display_smiles: "[1*]C(O)C(C(=O)O)C(=O)S[2*]" pseudonyms: ["PK_B", "PK"] - name: C1 smiles: "O=C(O)C=CSO" + display_smiles: "O=C(S[2*])C=C[1*]" pseudonyms: ["PK_C", "PK"] - name: C2 smiles: "CC(=CSO)C(=O)O" + display_smiles: "CC(=C[1*])C(=O)S[2*]" pseudonyms: ["PK_C", "PK"] - name: C4 smiles: "CCC(=CSO)C(=O)O" + display_smiles: "CCC(=C[1*])C(=O)S[2*]" pseudonyms: ["PK_C", "PK"] - name: C7 smiles: "O=C(O)C(=CSO)CO" + display_smiles: "O=C(S[2*])C(=C[1*])CO" pseudonyms: ["PK_C", "PK"] - name: C13 smiles: "O=C(C(C(O)=O)=CSO)O" + display_smiles: "O=C(C(C(O)=O)=C[1*])S[2*]" pseudonyms: ["PK_C", "PK"] - name: D1 smiles: "O=C(O)CCSO" + display_smiles: "O=C(S[2*])CC[1*]" pseudonyms: ["PK_D", "PK"] - name: D2^R smiles: "C[C@H](C(O)=O)CSO" + display_smiles: "C[C@H](C(S[2*])=O)C[1*]" stereochemistry: true pseudonyms: ["D2", "PK_D", "PK"] - name: D2^S smiles: "C[C@@H](C(O)=O)CSO" + display_smiles: "C[C@@H](C(S[2*])=O)C[1*]" stereochemistry: true pseudonyms: ["D2", "PK_D", "PK"] - name: D2 smiles: "CC(CSO)C(=O)O" + display_smiles: "CC(C[1*])C(=O)S[2*]" pseudonyms: ["PK_D", "PK"] - name: D3 smiles: "CC(C)(CSO)C(=O)O" + display_smiles: "CC(C)(C[1*])C(=O)S[2*]" pseudonyms: ["PK_D", "PK"] - name: D4^R smiles: "CC[C@H](C(O)=O)CSO" + display_smiles: "CC[C@H](C(S[2*])=O)C[1*]" stereochemistry: true pseudonyms: ["D4", "PK_D", "PK"] - name: D4^S smiles: "CC[C@@H](C(O)=O)CSO" + display_smiles: "CC[C@@H](C(S[2*])=O)C[1*]" stereochemistry: true pseudonyms: ["D4", "PK_D", "PK"] - name: D4 smiles: "CCC(CSO)C(=O)O" + display_smiles: "CCC(C[1*])C(=O)S[2*]" pseudonyms: ["PK_D", "PK"] - name: D5^R smiles: "O=C([C@H](CSO)O)O" + display_smiles: "O=C([C@H](C[1*])O)S[2*]" pseudonyms: ["D5", "PK_D", "PK"] stereochemistry: true - name: D5^S smiles: "O=C([C@@H](CSO)O)O" + display_smiles: "O=C([C@@H](C[1*])O)S[2*]" stereochemistry: true pseudonyms: ["D5", "PK_D", "PK"] - name: D5 smiles: "O=C(O)C(O)CSO" + display_smiles: "O=C(S[2*])C(O)C[1*]" pseudonyms: ["PK_D", "PK"] - name: D6^R smiles: "C[C@](CSO)(C(O)=O)O" + display_smiles: "C[C@](C[1*])(C(S[2*])=O)O" stereochemistry: true pseudonyms: ["D6", "PK_D", "PK"] - name: D6^S smiles: "C[C@@](CSO)(C(O)=O)O" + display_smiles: "C[C@@](C[1*])(C(S[2*])=O)O" stereochemistry: true pseudonyms: ["D6", "PK_D", "PK"] - name: D6 smiles: "CC(O)(CSO)C(=O)O" + display_smiles: "CC(O)(C[1*])C(=O)S[2*]" pseudonyms: ["PK_D", "PK"] - name: D7^R smiles: "O=C([C@H](CSO)CO)O" + display_smiles: "O=C([C@H](C[1*])CO)S[2*]" stereochemistry: true pseudonyms: ["D7", "PK_D", "PK"] - name: D7^S smiles: "O=C([C@@H](CSO)CO)O" + display_smiles: "O=C([C@@H](C[1*])CO)S[2*]" stereochemistry: true pseudonyms: ["D7", "PK_D", "PK"] - name: D7 smiles: "O=C(O)C(CO)CSO" + display_smiles: "O=C(S[2*])C(CO)C[1*]" pseudonyms: ["PK_D", "PK"] - name: D^R8 smiles: "C[C@@H](SO)CC(O)=O" + display_smiles: "C[C@@H]([1*])CC(S[2*])=O" stereochemistry: true pseudonyms: ["D8", "PK_D", "PK"] - name: D^S8 smiles: "C[C@H](SO)CC(O)=O" + display_smiles: "C[C@H]([1*])CC(S[2*])=O" stereochemistry: true pseudonyms: ["D8", "PK_D", "PK"] - name: D8 smiles: "CC(CC(=O)O)SO" + display_smiles: "CC(CC(=O)S[2*])[1*]" pseudonyms: ["PK_D", "PK"] - name: D^R10^R smiles: "C[C@H]([C@@H](C(O)=O)O)SO" + display_smiles: "C[C@H]([C@@H](C(S[2*])=O)O)[1*]" stereochemistry: true pseudonyms: ["D10", "PK_D", "PK"] - name: D^R10^S smiles: "C[C@H]([C@H](C(O)=O)O)SO" + display_smiles: "C[C@H]([C@H](C(S[2*])=O)O)[1*]" stereochemistry: true pseudonyms: ["D10", "PK_D", "PK"] - name: D^S10^R smiles: "C[C@@H]([C@@H](C(O)=O)O)SO" + display_smiles: "C[C@@H]([C@@H](C(S[2*])=O)O)[1*]" stereochemistry: true pseudonyms: ["D10", "PK_D", "PK"] - name: D^S10^S smiles: "C[C@@H]([C@H](C(O)=O)O)SO" + display_smiles: "C[C@@H]([C@H](C(S[2*])=O)O)[1*]" stereochemistry: true pseudonyms: ["D10", "PK_D", "PK"] - name: D10 smiles: "CC(SO)C(O)C(=O)O" + display_smiles: "CC([1*])C(O)C(=O)S[2*]" pseudonyms: ["PK_D", "PK"] - name: D11 smiles: "O=C(O)C(=O)CSO" + display_smiles: "O=C(S[2*])C(=O)C[1*]" pseudonyms: ["PK_D", "PK"] - name: D11 smiles: "O=C(O)C(O)=CSO" + display_smiles: "O=C(S[2*])C(O)=C[1*]" pseudonyms: ["PK_D", "PK"] - name: D14 smiles: "CC(O)C(CSO)C(=O)O" + display_smiles: "CC(O)C(C[1*])C(=O)S[2*]" pseudonyms: ["PK_D", "PK"] - name: D15 smiles: "C=C(C(O)=O)CSO" + display_smiles: "C=C(C(S[2*])=O)C[1*]" pseudonyms: ["PK_D", "PK"] - name: D16^R smiles: "OSC[C@@H](C(O)=O)CC=O" + display_smiles: "[1*]C[C@@H](C(S[2*])=O)CC=O" stereochemistry: true pseudonyms: ["D16", "PK_D", "PK"] - name: D16^S smiles: "OSC[C@H](C(O)=O)CC=O" + display_smiles: "[1*]C[C@H](C(S[2*])=O)CC=O" stereochemistry: true pseudonyms: ["D16", "PK_D", "PK"] - name: D16 smiles: "OSCC(CC=O)C(=O)O" + display_smiles: "[1*]CC(CC=O)C(=O)S[2*]" pseudonyms: ["PK_D", "PK"] - name: D^R17 smiles: "O=C(O)C([C@H](SO)C)=O" + display_smiles: "O=C(S[2*])C([C@H]([1*])C)=O" stereochemistry: true pseudonyms: ["D17", "PK_D", "PK"] - name: D^S17 smiles: "O=C(O)C([C@@H](SO)C)=O" + display_smiles: "O=C(S[2*])C([C@@H]([1*])C)=O" stereochemistry: true pseudonyms: ["D17", "PK_D", "PK"] - name: D17 smiles: "O=C(C(C(C)SO)=O)O" + display_smiles: "O=C(C(C(C)[1*])=O)S[2*]" pseudonyms: ["PK_D", "PK"] - name: D17 smiles: "O=C(C(O)=C(C)SO)O" + display_smiles: "O=C(C(C(C)[1*])=O)S[2*]" pseudonyms: ["PK_D", "PK"] - name: A smiles: "OSC(N)CC(=O)O" + display_smiles: "[1*]C(N)CC(=O)S[2*]" pseudonyms: ["PK"] - name: A smiles: "OSC(N)C(C)C(=O)O" + display_smiles: "[1*]C(N)C(C)C(=O)S[2*]" pseudonyms: ["PK"] - name: A smiles: "OSC(N)C(C)(C)C(=O)O" + display_smiles: "[1*]C(N)C(C)(C)C(=O)S[2*]" pseudonyms: ["PK"] - name: A smiles: "OSC(N)C(CC)C(=O)O" + display_smiles: "[1*]C(N)C(CC)C(=O)S[2*]" pseudonyms: ["PK"] - name: A smiles: "OSC(N)C(C)(O)C(=O)O" + display_smiles: "[1*]C(N)C(C)(O)C(=O)S[2*]" pseudonyms: ["PK"] - name: A smiles: "NC(C(O)=O)CSO" + display_smiles: "NC(C(S[2*])=O)C[1*]" pseudonyms: ["PK"] - name: A smiles: "NC(C(C(O)=O)O)SO" + display_smiles: "NC(C(C(S[2*])=O)O)[1*]" pseudonyms: ["PK"] - name: A smiles: "O=C(C(C(C(O)=O)SO)O)O" + display_smiles: "O=C(C(C(C(O)=O)[1*])O)S[2*]" pseudonyms: ["PK"] - name: methylation From 7a2d27577b417c06bdd9961f1518be36441185e7 Mon Sep 17 00:00:00 2001 From: David Meijer Date: Mon, 10 Aug 2026 23:39:44 +0200 Subject: [PATCH 4/5] ADD: E/Z matching rules for C motifs --- src/retromol/data/mxn.yml | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/src/retromol/data/mxn.yml b/src/retromol/data/mxn.yml index ac977da..702edef 100644 --- a/src/retromol/data/mxn.yml +++ b/src/retromol/data/mxn.yml @@ -418,21 +418,59 @@ display_smiles: "[1*]C(O)C(C(=O)O)C(=O)S[2*]" pseudonyms: ["PK_B", "PK"] +- name: C^E1 + smiles: 'O=C(/C=C/SO)O' + display_smiles: 'O=C(/C=C/[1*])S[2*]' + stereochemistry: true + pseudonyms: ["PK_C", "PK"] +- name: C^Z1 + smiles: 'O=C(/C=C\SO)O' + display_smiles: 'O=C(/C=C\[1*)S[2*]' + stereochemistry: true + pseudonyms: ["PK_C", "PK"] - name: C1 smiles: "O=C(O)C=CSO" display_smiles: "O=C(S[2*])C=C[1*]" pseudonyms: ["PK_C", "PK"] +- name: C^E2 + smiles: 'C/C(C(O)=O)=C\SO' + display_smiles: 'C/C(C(S[2*])=O)=C\[1*]' + stereochemistry: true + pseudonyms: ["PK_C", "PK"] +- name: C^Z2 + smiles: 'C/C(C(O)=O)=C/SO' + display_smiles: 'C/C(C(S[2*])=O)=C/[1*]' + stereochemistry: true + pseudonyms: ["PK_C", "PK"] - name: C2 smiles: "CC(=CSO)C(=O)O" display_smiles: "CC(=C[1*])C(=O)S[2*]" pseudonyms: ["PK_C", "PK"] +- name: C^E4 + smiles: 'CC/C(C(O)=O)=C\SO' + display_smiles: 'CC/C(C(S[2*])=O)=C\[1*]' + stereochemistry: true + pseudonyms: ["PK_C", "PK"] +- name: C^Z4 + smiles: 'CC/C(C(O)=O)=C/SO' + display_smiles: 'CC/C(C(S[2*])=O)=C/[1*]' + stereochemistry: true + pseudonyms: ["PK_C", "PK"] - name: C4 smiles: "CCC(=CSO)C(=O)O" display_smiles: "CCC(=C[1*])C(=O)S[2*]" pseudonyms: ["PK_C", "PK"] +- name: C^E7 + smiles: 'O=C(/C(CO)=C/SO)O' + display_smiles: 'O=C(/C(CO)=C/[1*])S[2*]' + pseudonyms: ["PK_C", "PK"] +- name: C^Z7 + smiles: 'O=C(/C(CO)=C\SO)O' + display_smiles: 'O=C(/C(CO)=C\[1*])S[2*]' + pseudonyms: ["PK_C", "PK"] - name: C7 smiles: "O=C(O)C(=CSO)CO" display_smiles: "O=C(S[2*])C(=C[1*])CO" From aeac8d88c5e69505bd4dfe90a7c008c68c145038 Mon Sep 17 00:00:00 2001 From: David Meijer Date: Tue, 11 Aug 2026 00:37:15 +0200 Subject: [PATCH 5/5] FEA: parse and identify E/Z stereochemistry --- gui/src/client/src/components/MotifName.tsx | 6 +- .../workspace/alignmentSvgExport.ts | 10 +- src/retromol/chem/stereo.py | 174 ++++++++++++++++++ src/retromol/data/mxn.yml | 2 + src/retromol/model/rules.py | 23 ++- src/retromol/model/submission.py | 7 + src/retromol/pipelines/parsing.py | 5 +- 7 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 src/retromol/chem/stereo.py diff --git a/gui/src/client/src/components/MotifName.tsx b/gui/src/client/src/components/MotifName.tsx index de7ff14..03562e8 100644 --- a/gui/src/client/src/components/MotifName.tsx +++ b/gui/src/client/src/components/MotifName.tsx @@ -1,14 +1,14 @@ import React from "react"; import Box from "@mui/material/Box"; -// Renders a monomer name like "A2^R", superscripting the trailing stereo marker. +// Renders a monomer name like "A2^R" or "C^E2", superscripting the trailing stereo marker. export function MotifName({ name }: { name: string }) { - const parts = name.split(/(\^[SR])/g); + const parts = name.split(/(\^[SREZ])/g); return ( <> {parts.map((part, i) => { - if (part === "^S" || part === "^R") { + if (part === "^S" || part === "^R" || part === "^E" || part === "^Z") { return ( part !== "") - .map((part) => (part === "^S" || part === "^R" ? { text: part.slice(1), sup: true } : { text: part, sup: false })); + .map((part) => + part === "^S" || part === "^R" || part === "^E" || part === "^Z" + ? { text: part.slice(1), sup: true } + : { text: part, sup: false } + ); } function measureName(ctx: CanvasRenderingContext2D, name: string, fontSize: number): number { diff --git a/src/retromol/chem/stereo.py b/src/retromol/chem/stereo.py new file mode 100644 index 0000000..b40b5ce --- /dev/null +++ b/src/retromol/chem/stereo.py @@ -0,0 +1,174 @@ +"""Module for restoring double bond (E/Z) stereochemistry that reaction rules discard. + +Reaction SMARTS that replace one of a stereo double bond's substituents (e.g. cleaving +a chain carbon off an alkene to attach a thiol leaving-group placeholder) leave RDKit +with no information to derive the product bond's stereo from -- the substituent that +defined the geometry is gone, and reaction product templates in `rxn.yml` don't encode +`/`/`\\` themselves. Since the atoms of the double bond usually keep their original +isotope tags across a reaction, we can look up what stereo *that exact bond* (identified +by its two atom tags) had in the original input molecule and, when the replacement +substituent occupies the same graph position 1:1, carry the geometry forward. + +This is a no-op for bonds that never existed as a stereo-defined double bond in the +original input (e.g. a bond created by "shifted" reversed-polyketide-synthesis rules, +where the product's double bond sits at a different atom pair than any double bond in +the input) -- there is nothing to look up for those, and none is invented. +""" + +from dataclasses import dataclass + +from rdkit.Chem.rdchem import Atom, Bond, BondStereo, BondType, Mol +from rdkit.Chem.rdmolops import AssignStereochemistry, SetDoubleBondNeighborDirections + +_STEREO_TYPES = (BondStereo.STEREOE, BondStereo.STEREOZ, BondStereo.STEREOCIS, BondStereo.STEREOTRANS) +_CIS_TYPES = (BondStereo.STEREOZ, BondStereo.STEREOCIS) + + +@dataclass(frozen=True) +class BondStereoRecord: + """ + Recorded geometry of one stereo-defined double bond, keyed elsewhere by the isotope + tags of its two atoms. + + :cvar ref_tags: Maps each of the bond's two atom tags to the isotope tag of the + neighbor atom that was used as its stereo reference substituent. + :cvar cis: Whether the two reference substituents are cis (True) or trans (False) + to each other. + """ + + ref_tags: dict[int, int] + cis: bool + + +def capture_double_bond_stereo(mol: Mol) -> dict[frozenset[int], BondStereoRecord]: + """ + Record the geometry of every stereo-defined double bond in `mol`, keyed by the + isotope tags of its two atoms. + + :param mol: Molecule to inspect. Every atom is expected to already carry a unique, + nonzero isotope tag (see `tag_mol`). + :return: Registry of double bond stereo records, keyed by the frozenset of the + bond's two atom tags. + """ + registry: dict[frozenset[int], BondStereoRecord] = {} + + for bond in mol.GetBonds(): + if bond.GetBondType() != BondType.DOUBLE: + continue + + stereo = bond.GetStereo() + if stereo not in _STEREO_TYPES: + continue + + stereo_atom_idxs = bond.GetStereoAtoms() + if len(stereo_atom_idxs) != 2: + continue + + a, b = bond.GetBeginAtom(), bond.GetEndAtom() + tag_a, tag_b = a.GetIsotope(), b.GetIsotope() + if tag_a == 0 or tag_b == 0 or tag_a == tag_b: + continue + + ref_0 = mol.GetAtomWithIdx(stereo_atom_idxs[0]) + ref_1 = mol.GetAtomWithIdx(stereo_atom_idxs[1]) + a_neighbor_idxs = {n.GetIdx() for n in a.GetNeighbors()} + b_neighbor_idxs = {n.GetIdx() for n in b.GetNeighbors()} + + if ref_0.GetIdx() in a_neighbor_idxs and ref_1.GetIdx() in b_neighbor_idxs: + ref_tag_a, ref_tag_b = ref_0.GetIsotope(), ref_1.GetIsotope() + elif ref_0.GetIdx() in b_neighbor_idxs and ref_1.GetIdx() in a_neighbor_idxs: + ref_tag_a, ref_tag_b = ref_1.GetIsotope(), ref_0.GetIsotope() + else: + continue + + if ref_tag_a == 0 or ref_tag_b == 0: + continue + + registry[frozenset((tag_a, tag_b))] = BondStereoRecord( + ref_tags={tag_a: ref_tag_a, tag_b: ref_tag_b}, + cis=stereo in _CIS_TYPES, + ) + + return registry + + +def _resolve_stereo_reference(atom: Atom, double_bond_partner: Atom, original_ref_tag: int) -> int | None: + """ + Find the atom index that should now act as the stereo reference substituent on `atom`. + + :param atom: One of the two double bond atoms. + :param double_bond_partner: The atom on the other end of the double bond. + :param original_ref_tag: Isotope tag of the substituent that used to define this + side's geometry, before the reaction ran. + :return: Atom index of the resolved reference substituent, or None if it can't be + determined unambiguously (in which case the caller should leave stereo unset). + """ + substituents = [n for n in atom.GetNeighbors() if n.GetIdx() != double_bond_partner.GetIdx()] + if not substituents: + return None + + # The original reference substituent is still directly attached: reuse it as-is. + for n in substituents: + if n.GetIsotope() == original_ref_tag: + return n.GetIdx() + + # It was replaced (e.g. by a new leaving-group atom introduced by the reaction). Any + # other substituent still carrying its own original tag (this matters for + # trisubstituted alkenes, which have such a second, untouched substituent alongside + # the reference one) is not a candidate -- it was never the reference. Only a + # substituent freshly introduced by this same reaction (no tag yet) can be the + # replacement; if there's exactly one, the reaction swapped it in 1:1 at the same + # graph position, so the geometric relationship still holds through it. + new_substituents = [n for n in substituents if n.GetAtomicNum() > 1 and n.GetIsotope() == 0] + if len(new_substituents) == 1: + return new_substituents[0].GetIdx() + + return None + + +def restore_double_bond_stereo(mol: Mol, registry: dict[frozenset[int], BondStereoRecord]) -> Mol: + """ + Restore double bond stereo on `mol` for bonds that lost it during a reaction but + whose two atoms match a stereo-defined bond recorded in `registry`. + + :param mol: Product molecule to restore stereo on, mutated in place. + :param registry: Registry produced by `capture_double_bond_stereo` on the original + input molecule. + :return: The same molecule, for convenient chaining. + .. note:: This function mutates the input molecule in place. + """ + if not registry: + return mol + + restored_any = False + + for bond in mol.GetBonds(): + if bond.GetBondType() != BondType.DOUBLE or bond.GetStereo() != BondStereo.STEREONONE: + continue + + a, b = bond.GetBeginAtom(), bond.GetEndAtom() + tag_a, tag_b = a.GetIsotope(), b.GetIsotope() + if tag_a == 0 or tag_b == 0: + continue + + record = registry.get(frozenset((tag_a, tag_b))) + if record is None: + continue + + ref_a_idx = _resolve_stereo_reference(a, b, record.ref_tags[tag_a]) + ref_b_idx = _resolve_stereo_reference(b, a, record.ref_tags[tag_b]) + if ref_a_idx is None or ref_b_idx is None: + continue + + bond.SetStereoAtoms(ref_a_idx, ref_b_idx) + bond.SetStereo(BondStereo.STEREOCIS if record.cis else BondStereo.STEREOTRANS) + restored_any = True + + if restored_any: + # `/`/`\` SMILES output is driven by BondDir on the neighboring single bonds, + # not directly by the double bond's Stereo/StereoAtoms -- derive it from what + # we just set, then re-perceive so the assignment is fully consistent. + SetDoubleBondNeighborDirections(mol) + AssignStereochemistry(mol, cleanIt=True, force=True) + + return mol diff --git a/src/retromol/data/mxn.yml b/src/retromol/data/mxn.yml index 702edef..58abc23 100644 --- a/src/retromol/data/mxn.yml +++ b/src/retromol/data/mxn.yml @@ -466,10 +466,12 @@ - name: C^E7 smiles: 'O=C(/C(CO)=C/SO)O' display_smiles: 'O=C(/C(CO)=C/[1*])S[2*]' + stereochemistry: true pseudonyms: ["PK_C", "PK"] - name: C^Z7 smiles: 'O=C(/C(CO)=C\SO)O' display_smiles: 'O=C(/C(CO)=C\[1*])S[2*]' + stereochemistry: true pseudonyms: ["PK_C", "PK"] - name: C7 smiles: "O=C(O)C(=CSO)CO" diff --git a/src/retromol/model/rules.py b/src/retromol/model/rules.py index 2514fed..24f0700 100644 --- a/src/retromol/model/rules.py +++ b/src/retromol/model/rules.py @@ -22,6 +22,7 @@ reassign_stereochemistry, ) from retromol.chem.reaction import smarts_to_reaction, reactive_template_atoms +from retromol.chem.stereo import BondStereoRecord, restore_double_bond_stereo from retromol.chem.tagging import get_tags_mol from retromol.chem.masking import is_masked_preserved @@ -79,14 +80,22 @@ def from_dict(cls, data: dict[str, Any]) -> "ReactionRule": ) return reaction_rule - def apply(self, reactant: Mol, mask_tags: set[int] | None = None) -> list[list[Mol]]: + def apply( + self, + reactant: Mol, + mask_tags: set[int] | None = None, + stereo_registry: dict[frozenset[int], BondStereoRecord] | None = None, + ) -> list[list[Mol]]: """ Apply the reaction to the given reactant molecule, optionally enforcing a mask on atom tags. :param reactant: The reactant molecule. :param mask_tags: Set of atom tags (isotope-based tags) that are allowed to change. + :param stereo_registry: Registry of double bond stereo recorded from the original + input molecule (see `capture_double_bond_stereo`), used to restore E/Z stereo + on products that lost it during this reaction. Skipped when None/empty. :return: List of unique product tuples (each tuple as a list[Mol]). - """ + """ log.debug(f"Applying reaction rule '{self.name}'") results = self.rxn.RunReactants([reactant]) @@ -120,6 +129,11 @@ def apply(self, reactant: Mol, mask_tags: set[int] | None = None) -> list[list[M # Reassign stereo on the sanitized product prod = reassign_stereochemistry(prod) + # Restore E/Z stereo the reaction couldn't reconstruct itself, where the + # original input molecule had it defined for this same bond + if stereo_registry: + prod = restore_double_bond_stereo(prod, stereo_registry) + products.append(prod) atom_tag_sets.append(get_tags_mol(prod)) @@ -223,6 +237,7 @@ def apply_uncontested( parent: Mol, uncontested: list[tuple[ReactionRule, set[int]]], original_taken_tags: set[int], + stereo_registry: dict[frozenset[int], BondStereoRecord] | None = None, ) -> tuple[list[Mol], list[tuple[ReactionRule, set[int]]], set[tuple[int, frozenset[int]]]]: """ Apply uncontested reactions in bulk. @@ -230,6 +245,8 @@ def apply_uncontested( :param parent: RDKit molecule. :param uncontested: List of uncontested reactions. :param original_taken_tags: List of atom tags from original reactant. + :param stereo_registry: Registry of double bond stereo recorded from the original + input molecule, forwarded to `ReactionRule.apply` for stereo restoration. :return: List of true products, a list of applied ReactionRules with their masks, and a set of failed combinations. """ applied_reactions: list[tuple[ReactionRule, set[int]]] = [] @@ -294,7 +311,7 @@ def apply_uncontested( temp_taken_tags_uncontested.add(tag) unmasked_parent = Mol(parent) # keep original parent for later - results = rl.apply(parent, msk) # apply reaction rule + results = rl.apply(parent, msk, stereo_registry) # apply reaction rule try: if len(results) == 0: diff --git a/src/retromol/model/submission.py b/src/retromol/model/submission.py index 26c1eb4..5bdc1f8 100644 --- a/src/retromol/model/submission.py +++ b/src/retromol/model/submission.py @@ -6,6 +6,7 @@ from rdkit.Chem.rdchem import Mol from retromol.chem.mol import standardize_from_smiles, mol_to_inchikey +from retromol.chem.stereo import BondStereoRecord, capture_double_bond_stereo from retromol.chem.tagging import tag_mol @@ -32,6 +33,7 @@ class Submission: mol: Mol = field(init=False, repr=False) inchikey: str = field(init=False, repr=False) + stereo_registry: dict[frozenset[int], BondStereoRecord] = field(init=False, repr=False) def __post_init__(self) -> None: """ @@ -54,9 +56,14 @@ def __post_init__(self) -> None: # Tag molecule tag_mol(mol) + # Record original double bond stereo (by atom tag) so it can be restored on + # products of reaction rules that otherwise can't reconstruct it themselves + stereo_registry = capture_double_bond_stereo(mol) + object.__setattr__(self, "smiles", smiles) object.__setattr__(self, "mol", mol) object.__setattr__(self, "inchikey", inchikey) + object.__setattr__(self, "stereo_registry", stereo_registry) def __str__(self) -> str: """ diff --git a/src/retromol/pipelines/parsing.py b/src/retromol/pipelines/parsing.py index 0abfbd1..faa185e 100644 --- a/src/retromol/pipelines/parsing.py +++ b/src/retromol/pipelines/parsing.py @@ -34,6 +34,7 @@ def process_mol(submission: Submission, ruleset: RuleSet) -> ReactionGraph: g = ReactionGraph() original_taken_tags = get_tags_mol(submission.mol) + stereo_registry = submission.stereo_registry failed_combos: set[tuple[int, frozenset[int]]] = set() # Track queue/expansion status by encoding to avoid duplicate work @@ -68,7 +69,7 @@ def process_mol(submission: Submission, ruleset: RuleSet) -> ReactionGraph: if uncontested: log.debug(f"Applying {len(uncontested)} uncontested rule(s) in bulk") - products, applied_in_bulk, new_failed = apply_uncontested(parent, uncontested, original_taken_tags) + products, applied_in_bulk, new_failed = apply_uncontested(parent, uncontested, original_taken_tags, stereo_registry) failed_combos.update(new_failed) # If uncontested existed but none succeed, fall through to contested @@ -92,7 +93,7 @@ def process_mol(submission: Submission, ruleset: RuleSet) -> ReactionGraph: # Contested exhaustive for rl in reaction_rules: - results = rl.apply(parent, None) + results = rl.apply(parent, None, stereo_registry) if not results: continue