@datagrok/sequence-translator 1.9.11 → 1.9.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -3,26 +3,71 @@
3
3
  "heterodimerCode": "$2",
4
4
  "linkRules": [
5
5
  {
6
+ "code": 2,
6
7
  "firstMonomers": [
7
8
  "D"
8
9
  ],
9
10
  "secondMonomers": [
10
11
  "NH2"
11
12
  ],
12
- "firstLinkingGroup": "3",
13
- "secondLinkingGroup": "2",
14
- "code": "2"
13
+ "firstLinkingGroup": 3,
14
+ "secondLinkingGroup": 2
15
15
  },
16
16
  {
17
+ "code": 1,
17
18
  "firstMonomers": [
18
- "C"
19
+ "C",
20
+ "dC"
19
21
  ],
20
22
  "secondMonomers": [
21
- "C"
23
+ "C",
24
+ "dC"
22
25
  ],
23
- "firstLinkingGroup": "3",
24
- "secondLinkingGroup": "3",
25
- "code": "1"
26
+ "firstLinkingGroup": 3,
27
+ "secondLinkingGroup": 3
28
+ },
29
+ {
30
+ "code": 10,
31
+ "firstMonomers": [
32
+ "C",
33
+ "D",
34
+ "E",
35
+ "K"
36
+ ],
37
+ "secondMonomers": [
38
+ "C",
39
+ "D",
40
+ "E",
41
+ "K"
42
+ ],
43
+ "firstLinkingGroup": 3,
44
+ "secondLinkingGroup": 2
45
+ },
46
+ {
47
+ "code": 11,
48
+ "firstMonomers": [
49
+ "C",
50
+ "E",
51
+ "K"
52
+ ],
53
+ "secondMonomers": [
54
+ "NH2"
55
+ ],
56
+ "firstLinkingGroup": 3,
57
+ "secondLinkingGroup": 2
58
+ },
59
+ {
60
+ "code": 1,
61
+ "firstMonomers": [
62
+ "C",
63
+ "dC"
64
+ ],
65
+ "secondMonomers": [
66
+ "THA",
67
+ "MG3"
68
+ ],
69
+ "firstLinkingGroup": 3,
70
+ "secondLinkingGroup": 3
26
71
  }
27
72
  ],
28
73
  "reactionRules": [
@@ -36,6 +81,50 @@
36
81
  ],
37
82
  "reaction": "[C:1]N=[N+]=[N-].[C:2]C#C>>[C:1]N1-N=NC=C1[C:2]",
38
83
  "name": "GGaz"
84
+ },
85
+ {
86
+ "code": 8,
87
+ "firstMonomers": [
88
+ "DRR1"
89
+ ],
90
+ "secondMonomers": [
91
+ "DRR2"
92
+ ],
93
+ "reaction": "C=C[C:1].C=C/C=C/[C:2]>>C1=CC([C:2])C([C:1])CC1",
94
+ "name": "DARR"
95
+ },
96
+ {
97
+ "code": 8,
98
+ "firstMonomers": [
99
+ "ODAR1"
100
+ ],
101
+ "secondMonomers": [
102
+ "ODAR2"
103
+ ],
104
+ "reaction": "C=CC(=O)[C:1].C=C[C:2]>>C1CC([C:1])OC([C:2])C1",
105
+ "name": "ODARR"
106
+ },
107
+ {
108
+ "code": 7,
109
+ "firstMonomers": [
110
+ "PHAR1"
111
+ ],
112
+ "secondMonomers": [
113
+ "PHAR2"
114
+ ],
115
+ "reaction": "C=C[C:1].C=C[C:2]>>C1CC([C:2])C1[C:1]",
116
+ "name": "PHAR_CIS"
117
+ },
118
+ {
119
+ "code": 6,
120
+ "firstMonomers": [
121
+ "PHAR1"
122
+ ],
123
+ "secondMonomers": [
124
+ "PHAR2"
125
+ ],
126
+ "reaction": "C=C[C:1].C=C[C:2]>>C1C([C:1])CC1[C:2]",
127
+ "name": "PHAR_TRANS"
39
128
  }
40
129
  ]
41
- }
130
+ }
@@ -7,3 +7,9 @@ n,seqs
7
7
  6,D(2)-T-G-H-F-Y-P-NH2(2)
8
8
  7,R-F-azG(4)-T-G-H-F-Y-P-aG(4)-meI
9
9
  8,R-F-aG(4)-T-G-H-F-Y-P-azG(4)-meI
10
+ 9,R-F-R-I-F-D-C(1)-T-G-H-F-Y-G-H-F-Y-G-H-F-Y-P-THA(1)-meI-G-T
11
+ 10,R-F-R-I-F-D-C(1)-T-G-H-F-Y-G-H-F-Y-G-H-F-Y-P-MG3(1)-meI-G-T
12
+ 11,R-F-DRR1(8)-T-G-H-F-Y-P-DRR2(8)-C-C
13
+ 12,R-F-ODAR1(8)-T-G-H-F-Y-P-ODAR2(8)-meI-R-P-NH2
14
+ 13,R-F-PHAR1(7)-T-G-H-F-Y-P-PHAR2(7)-meI-F-K-NH2
15
+ 14,R-F-PHAR1(6)-T-G-H-F-Y-P-PHAR2(6)-C-C
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datagrok/sequence-translator",
3
3
  "friendlyName": "Sequence Translator",
4
- "version": "1.9.11",
4
+ "version": "1.9.13",
5
5
  "author": {
6
6
  "name": "Davit Rizhinashvili",
7
7
  "email": "drizhinashvili@datagrok.ai"
@@ -7,8 +7,10 @@ import {RulesManager} from './rule-manager';
7
7
  import {RuleCards} from './pt-rule-cards';
8
8
  import {getMonomerLibHelper} from '@datagrok-libraries/bio/src/monomer-works/monomer-utils';
9
9
  import {
10
+ _package,
10
11
  PackageFunctions} from '../../package';
11
12
  import {MmcrTemps} from '@datagrok-libraries/bio/src/utils/cell-renderer-consts';
13
+ import {ReactionEditorProps} from './rule-reaction-editor';
12
14
 
13
15
  export const RULES_PATH = 'System:AppData/SequenceTranslator/polytool-rules/';
14
16
  export const RULES_STORAGE_NAME = 'Polytool';
@@ -25,6 +27,7 @@ const NAME_FIRST_LINK = 'firstLinkingGroup';
25
27
  const NAME_SECOND_LINK = 'secondLinkingGroup';
26
28
 
27
29
  export class RuleInputs extends ActiveFiles {
30
+ static ruleDescriptions: Record<string, Promise<HTMLElement>> = {};
28
31
  constructor(
29
32
  path: string, userStorageName: string, ext: string,
30
33
  options?: { onValueChanged: (value: string[]) => void }
@@ -47,8 +50,70 @@ export class RuleInputs extends ActiveFiles {
47
50
 
48
51
  return res;
49
52
  }
53
+
54
+ override async getForm(): Promise<HTMLDivElement> {
55
+ const form = await super.getForm();
56
+ this.processRulesForm(form);
57
+ return form;
58
+ }
59
+
60
+ private async processRulesForm(rulesForm: HTMLElement) {
61
+ const ruleNameLabels = Array.from(rulesForm.getElementsByTagName('label') ?? [])
62
+ .filter((l) => l.textContent?.endsWith('.json') && l.parentElement != null);
63
+ for (const label of ruleNameLabels) {
64
+ const ruleName = label.textContent!.trim();
65
+ const inputRoot = label.parentElement!.getElementsByTagName('input')[0]!;
66
+ ui.tooltip.bind(inputRoot, ui.wait(async () => {
67
+ RuleInputs.ruleDescriptions[ruleName] ??= (async () => {
68
+ try {
69
+ const rule = JSON.parse(await grok.dapi.files.readAsText(`${RULES_PATH}/${ruleName}`));
70
+ const descDiv = ui.divV([ui.h1(ruleName)]);
71
+ const linkRules: RuleLink[] = rule.linkRules ?? [];
72
+ // link rules
73
+ if (linkRules.length > 0) {
74
+ descDiv.appendChild(ui.h3('Linkage Rules'));
75
+ const table = ui.table(linkRules, (item) => [
76
+ item.code?.toString() ?? '',
77
+ shortenToMaxLen((item.firstMonomers ?? []).join(', ')),
78
+ shortenToMaxLen((item.secondMonomers ?? []).join(', ')),
79
+ item.firstLinkingGroup?.toString() ?? '',
80
+ item.secondLinkingGroup?.toString() ?? ''
81
+ ], ['Code', 'M1', 'M2', 'L1', 'L2']);
82
+ descDiv.appendChild(table);
83
+ }
84
+ // reaction rules
85
+ const reactionRules: RuleReaction[] = rule.reactionRules ?? [];
86
+ if (reactionRules.length > 0) {
87
+ descDiv.appendChild(ui.h3('Synthesis Rules'));
88
+ const table = ui.table(reactionRules, (item) => [
89
+ item.code?.toString() ?? '',
90
+ shortenToMaxLen((item.firstMonomers ?? []).join(', ')),
91
+ shortenToMaxLen((item.secondMonomers ?? []).join(', ')),
92
+ shortenToMaxLen(item.name ?? ''),
93
+ grok.chem.drawMolecule(item.reaction?.split('>>')[1] ?? '', 70, 70)
94
+ ], ['Code', 'M1', 'M2', 'Name', 'Product']);
95
+ descDiv.appendChild(table);
96
+ }
97
+ return descDiv;
98
+ } catch (e: any) {
99
+ _package.logger.error(`Failed to load rule ${ruleName}: ${e?.toString?.()}`);
100
+ console.error(e);
101
+ return ui.divText(`Failed to load rule ${ruleName}`);
102
+ }
103
+ })();
104
+ return await RuleInputs.ruleDescriptions[ruleName];
105
+ }));
106
+ }
107
+ }
50
108
  }
51
109
 
110
+
111
+ function shortenToMaxLen(s: string, maxLen: number = 30): string {
112
+ if (s.length <= maxLen) return s;
113
+ return `${s.substring(0, maxLen)}...`;
114
+ }
115
+
116
+
52
117
  export type RuleLink = {
53
118
  code: number,
54
119
  firstMonomers: string[],
@@ -170,7 +235,7 @@ export class Rules {
170
235
  return cards;
171
236
  }
172
237
 
173
- getSynthesisRulesDf(): DG.DataFrame {
238
+ getSynthesisRulesDf() {
174
239
  const length = this.reactionRules.length;
175
240
  const codeCol = DG.Column.int(NAME_CODE, length);
176
241
  codeCol.setTag('friendlyName', 'Code');
@@ -178,6 +243,15 @@ export class Rules {
178
243
  firstMonomerCol.setTag('friendlyName', 'First monomers');
179
244
  const secondMonomerCol = DG.Column.string(NAME_SECOND_MONOMERS, length);
180
245
  secondMonomerCol.setTag('friendlyName', 'Second monomers');
246
+ // set these columns as macromolecule for better visualization
247
+ firstMonomerCol.semType = DG.SEMTYPE.MACROMOLECULE;
248
+ firstMonomerCol.temp[MmcrTemps.fontSize] = 16;
249
+ PackageFunctions.applyNotationProviderForCyclized(firstMonomerCol, ',');
250
+ secondMonomerCol.semType = DG.SEMTYPE.MACROMOLECULE;
251
+ secondMonomerCol.temp[MmcrTemps.fontSize] = 16;
252
+ PackageFunctions.applyNotationProviderForCyclized(secondMonomerCol, ',');
253
+ secondMonomerCol.setTag(DG.TAGS.CELL_RENDERER, 'Sequence');
254
+ firstMonomerCol.setTag(DG.TAGS.CELL_RENDERER, 'Sequence');
181
255
  const name = DG.Column.string(NAME_REACTION_NAME, length);
182
256
  name.setTag('friendlyName', 'Name');
183
257
  const firstReactant = DG.Column.string('firstReactant', length);
@@ -205,9 +279,27 @@ export class Rules {
205
279
  product.semType = DG.SEMTYPE.MOLECULE;
206
280
 
207
281
 
208
- return DG.DataFrame.fromColumns([
282
+ const df = DG.DataFrame.fromColumns([
209
283
  name, firstReactant, secondReactant, product, codeCol, firstMonomerCol, secondMonomerCol
210
284
  ]);
285
+
286
+ const addNewRow = (rowObj: ReactionEditorProps) => {
287
+ if (rowObj.rowIndex != undefined && rowObj.rowIndex < df.rowCount && rowObj.rowIndex >= 0) {
288
+ name.set(rowObj.rowIndex, rowObj.resultMonomerName);
289
+ codeCol.set(rowObj.rowIndex, rowObj.code);
290
+ firstMonomerCol.set(rowObj.rowIndex, rowObj.firstMonomers.join(','));
291
+ secondMonomerCol.set(rowObj.rowIndex, rowObj.secondMonomers.join(','));
292
+ firstReactant.set(rowObj.rowIndex, rowObj.firstReactantSmiles);
293
+ secondReactant.set(rowObj.rowIndex, rowObj.secondReactantSmiles);
294
+ product.set(rowObj.rowIndex, rowObj.productSmiles);
295
+ } else {
296
+ const {resultMonomerName, code, firstMonomers, secondMonomers,
297
+ firstReactantSmiles, secondReactantSmiles, productSmiles} = rowObj;
298
+ df.rows.addNew([resultMonomerName, firstReactantSmiles, secondReactantSmiles,
299
+ productSmiles, code, firstMonomers.join(','), secondMonomers.join(',')]);
300
+ }
301
+ };
302
+ return {df, addNewRow};
211
303
  }
212
304
 
213
305
  setLinkRules(df: DG.DataFrame) : void {
@@ -8,6 +8,12 @@ import {getHelmHelper} from '@datagrok-libraries/bio/src/helm/helm-helper';
8
8
  import {doPolyToolConvert} from './pt-conversion';
9
9
  import {NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule/consts';
10
10
  import {RuleCards} from './pt-rule-cards';
11
+ import {getOverriddenLibrary} from './pt-synthetic';
12
+ import {MmcrTemps} from '@datagrok-libraries/bio/src/utils/cell-renderer-consts';
13
+ import {helmToMol} from './pt-atomic';
14
+ import {getRdKitModule} from '@datagrok-libraries/bio/src/chem/rdkit-module';
15
+ import {getSeqHelper} from '@datagrok-libraries/bio/src/utils/seq-helper';
16
+ import {getReactionEditor, ReactionEditorProps} from './rule-reaction-editor';
11
17
 
12
18
 
13
19
  const TAB_LINKS = 'Links';
@@ -26,6 +32,7 @@ export class RulesManager {
26
32
 
27
33
  linkCards: RuleCards[];
28
34
  addLinkRulesFunc: (rowObj: LinkRuleRowArgs) => void;
35
+ addSynthRulesFunc: (rowObj: ReactionEditorProps) => void;
29
36
 
30
37
  // every rule set will have its editor instance
31
38
  private static instances: Record<string, RulesManager> = {};
@@ -36,7 +43,15 @@ export class RulesManager {
36
43
  this.linkRuleDataFrame = linksRes.res;
37
44
  this.addLinkRulesFunc = linksRes.addNewRow;
38
45
 
39
- this.synthRuleDataFrame = this.rules.getSynthesisRulesDf();
46
+ const synthRes = this.rules.getSynthesisRulesDf();
47
+ this.addSynthRulesFunc = (r) => {
48
+ synthRes.addNewRow(r);
49
+ this.rules.setSynthesisRules(synthRes.df);
50
+ this.synthRuleDataFrame = synthRes.df;
51
+ this.substituteReactionGridDataFrame?.();
52
+ this.save();
53
+ };
54
+ this.synthRuleDataFrame = synthRes.df;
40
55
  this.fileName = fileName;
41
56
 
42
57
  const homoValue = this.rules.homodimerCode ? this.rules.homodimerCode : '';
@@ -170,18 +185,36 @@ export class RulesManager {
170
185
 
171
186
  const initCol = DG.Column.fromStrings('Monomers', seqs);
172
187
  const helmCol = DG.Column.fromStrings('Helm', helms);
173
-
188
+ const df = DG.DataFrame.fromColumns([
189
+ initCol, helmCol
190
+ ]);
174
191
  initCol.semType = DG.SEMTYPE.MACROMOLECULE;
192
+ const rdKitModule = await getRdKitModule();
193
+ const seqHelper = await getSeqHelper();
194
+ // initialize seqHandler for this column
175
195
  PackageFunctions.applyNotationProviderForCyclized(initCol, '-');
196
+ initCol.tags[DG.TAGS.CELL_RENDERER] = 'Sequence';
176
197
 
177
198
  helmCol.semType = DG.SEMTYPE.MACROMOLECULE;
178
199
  helmCol.meta.units = NOTATION.HELM;
179
200
  helmCol.setTag(DG.TAGS.CELL_RENDERER, 'helm');
180
- return DG.DataFrame.fromColumns([
181
- initCol, helmCol
182
- ]).plot.grid();
201
+
202
+ const lib = await getOverriddenLibrary(this.rules);
203
+ const resHelmColTemp = helmCol.temp;
204
+ resHelmColTemp[MmcrTemps.overriddenLibrary] = lib;
205
+ helmCol.temp = resHelmColTemp;
206
+
207
+ const resMolCol = await helmToMol(helmCol, seqs,
208
+ isLinear, true, false, false, lib, rdKitModule, seqHelper);
209
+ resMolCol.name = `molfile(sequence)`;
210
+ resMolCol.semType = DG.SEMTYPE.MOLECULE;
211
+ df.columns.add(resMolCol);
212
+
213
+ return df.plot.grid();
183
214
  }
184
215
 
216
+ private substituteReactionGridDataFrame: (() => void) | null = null;
217
+
185
218
  async getForm() {
186
219
  const gridOptions: Partial<DG.IGridSettings> = {showAddNewRowIcon: false, allowEdit: false, rowHeight: 60};
187
220
 
@@ -240,10 +273,31 @@ export class RulesManager {
240
273
  });
241
274
 
242
275
  const links = ui.splitH([linksGridDiv, gridDiv], null, true);
276
+ const synthesisGrid = this.synthRuleDataFrame.plot.grid({showAddNewRowIcon: false, allowEdit: false, rowHeight: 130});
277
+ synthesisGrid.onCellDoubleClick.subscribe(() => {
278
+ if (!synthesisGrid.dataFrame || synthesisGrid.dataFrame.currentRowIdx == -1 || synthesisGrid.dataFrame.currentRowIdx == undefined)
279
+ return;
280
+ const idx = synthesisGrid.dataFrame.currentRowIdx;
281
+ const editArgs: ReactionEditorProps = {
282
+ rowIndex: idx,
283
+ code: this.synthRuleDataFrame.get('code', idx),
284
+ firstMonomers: this.synthRuleDataFrame.get('firstMonomers', idx).split(',').map((s: string) => s.trim()).filter((s: string) => s),
285
+ secondMonomers: this.synthRuleDataFrame.get('secondMonomers', idx).split(',').map((s: string) => s.trim()).filter((s: string) => s),
286
+ resultMonomerName: this.synthRuleDataFrame.get('name', idx),
287
+ firstReactantSmiles: this.synthRuleDataFrame.get('firstReactant', idx),
288
+ secondReactantSmiles: this.synthRuleDataFrame.get('secondReactant', idx),
289
+ productSmiles: this.synthRuleDataFrame.get('product', idx),
290
+ };
291
+ getReactionEditor((props: ReactionEditorProps) => this.addSynthRulesFunc(props), editArgs);
292
+ });
293
+ const reactionsGridDiv = this.createGridDiv('Rules', synthesisGrid);
243
294
 
244
- const reactionsGridDiv = this.createGridDiv('Rules',
245
- this.synthRuleDataFrame.plot.grid({showAddNewRowIcon: true}));
246
- const reactionExamples = this.createGridDiv('Examples', await this.getReactionExamplesGrid());
295
+ const reactionExamplesGrid = await this.getReactionExamplesGrid();
296
+ const reactionExamples = this.createGridDiv('Examples', reactionExamplesGrid);
297
+ this.substituteReactionGridDataFrame = async () => {
298
+ const newGrid = await this.getReactionExamplesGrid();
299
+ reactionExamplesGrid.dataFrame = newGrid.dataFrame;
300
+ };
247
301
  reactionsGridDiv.style.width = '50%';
248
302
  reactionExamples.style.width = '50%';
249
303
  const reactions = ui.divH([reactionsGridDiv, reactionExamples]);
@@ -281,9 +335,37 @@ export class RulesManager {
281
335
  if (currentTab == TAB_LINKS)
282
336
  this.getAddNewLinkRuleDialog();
283
337
  else if (currentTab == TAB_REACTIONS)
284
- this.synthRuleDataFrame.rows.addNew();
338
+ getReactionEditor((props: ReactionEditorProps) => this.addSynthRulesFunc(props));
285
339
  });
286
- const topPanel = [saveButton, addButton];
340
+ const removeButton = ui.button('Remove rule', () => {
341
+ const currentTab = inputsTabControl.currentPane.name;
342
+ if (currentTab == TAB_LINKS) {
343
+ if (this.linkRuleDataFrame == null || this.linkRuleDataFrame.currentRowIdx == -1 || this.linkRuleDataFrame.currentRowIdx == undefined)
344
+ return;
345
+ const idx = linksGrid.dataFrame.currentRowIdx;
346
+ ui.dialog('Are you sure you want to remove the rule?')
347
+ .add(ui.divText('This action is irreversible!'))
348
+ .onOK(() => {
349
+ this.linkRuleDataFrame.rows.removeAt(idx);
350
+ this.rules.setLinkRules(this.linkRuleDataFrame);
351
+ this.save();
352
+ }).show();
353
+ } else if (currentTab == TAB_REACTIONS) {
354
+ if (this.synthRuleDataFrame == null || this.synthRuleDataFrame.currentRowIdx == -1 || this.synthRuleDataFrame.currentRowIdx == undefined)
355
+ return;
356
+ const idx = synthesisGrid.dataFrame.currentRowIdx;
357
+ ui.dialog('Are you sure you want to remove the rule?')
358
+ .add(ui.divText('This action is irreversible!'))
359
+ .onOK(() => {
360
+ this.synthRuleDataFrame.rows.removeAt(idx);
361
+ this.rules.setSynthesisRules(this.synthRuleDataFrame);
362
+ this.substituteReactionGridDataFrame?.();
363
+ this.save();
364
+ }).show();
365
+ }
366
+ });
367
+
368
+ const topPanel = [saveButton, addButton, removeButton];
287
369
  this.v!.setRibbonPanels([topPanel]);
288
370
 
289
371
  panel.style.height = '100%';
@@ -0,0 +1,125 @@
1
+ /* eslint-disable max-len */
2
+ import * as DG from 'datagrok-api/dg';
3
+ import * as grok from 'datagrok-api/grok';
4
+ import * as ui from 'datagrok-api/ui';
5
+
6
+ function correctRGroups(smiles: string): string {
7
+ const elementRGroupRegex = /\[R[1-9]\]/g;
8
+ // replace all [R1] with [*:1]
9
+ let correctedSmiles = smiles.replaceAll(elementRGroupRegex, (match) => {
10
+ const rGroupNum = match[2];
11
+ return `[*:${rGroupNum}]`;
12
+ });
13
+
14
+ // in some scenarios, rgroups can be written as [2*]
15
+ const elementRGroupRegex2 = /\[\d\*\]/g;
16
+ correctedSmiles = correctedSmiles.replaceAll(elementRGroupRegex2, (match) => {
17
+ const rGroupNum = match[1];
18
+ return `[*:${rGroupNum}]`;
19
+ });
20
+
21
+ // in some other scenarios, rgroups can be written as [1*:1] or [1*:0]
22
+ const elementRGroupRegex3 = /\[\d\*\:\d\]/g;
23
+ return correctedSmiles.replaceAll(elementRGroupRegex3, (match) => {
24
+ const rGroupNum = match[1];
25
+ return `[*:${rGroupNum}]`;
26
+ });
27
+ }
28
+
29
+ function rgroupToRadicalForm(smiles: string): string {
30
+ // here we are sure that all rgroups are in the form [*:1], [*:2], ..., [*:9]
31
+ // replace all [*:1] with [C:1] to mark carbons
32
+ return smiles.replaceAll(/\[\*\:\d\]/g, (match) => {
33
+ const rGroupNum = match[3];
34
+ return `[C:${rGroupNum}]`;
35
+ });
36
+ }
37
+
38
+ function radicalToRgroupForm(smiles: string): string {
39
+ // here we are sure that all marked carbons are in the form [C:1], [C:2], ..., [C:9]
40
+ // replace all [C:1] with [*:1]
41
+ return smiles.replaceAll(/\[C\:\d\]/g, (match) => {
42
+ const rGroupNum = match[3];
43
+ return `[*:${rGroupNum}]`;
44
+ });
45
+ }
46
+
47
+ export type ReactionEditorProps = {
48
+ resultMonomerName: string,
49
+ firstReactantSmiles: string,
50
+ secondReactantSmiles: string,
51
+ productSmiles: string,
52
+ firstMonomers: string[],
53
+ secondMonomers: string[],
54
+ code: number,
55
+ rowIndex?: number,
56
+ }
57
+
58
+ export function getReactionEditor(onOk: (props: ReactionEditorProps) => void, preset?: ReactionEditorProps) {
59
+ // preset smiles are in radical form, need to convert to rgroup form
60
+ const firstReactant = preset?.firstReactantSmiles ? radicalToRgroupForm(preset.firstReactantSmiles) : '[*:1]C';
61
+ const secondReactant = preset?.secondReactantSmiles ? radicalToRgroupForm(preset.secondReactantSmiles) : '[*:2]C';
62
+ const product = preset?.productSmiles ? radicalToRgroupForm(preset.productSmiles) : '[*:1]CC[*:2]';
63
+ const code = preset?.code;
64
+ const firstMonomers = preset?.firstMonomers ?? [];
65
+ const secondMonomers = preset?.secondMonomers ?? [];
66
+ const resultMonomerName = preset?.resultMonomerName ?? '';
67
+
68
+ const codeInput = ui.input.int('Code', {value: code, nullable: false, showPlusMinus: false, showSlider: false, tooltipText: 'Reaction code'});
69
+ const firstMonomersInput = ui.input.string('First monomers', {value: firstMonomers.join(','), nullable: false, tooltipText: 'Comma-separated list of monomers for the first reactant'});
70
+ const secondMonomersInput = ui.input.string('Second monomers', {value: secondMonomers.join(','), nullable: false, tooltipText: 'Comma-separated list of monomers for the second reactant'});
71
+ const resultMonomerNameInput = ui.input.string('Result monomer name', {value: resultMonomerName, nullable: false, tooltipText: 'Name of the resulting pseudo-monomer generated by the reaction of the two reactants'});
72
+
73
+ const firstReactantInput = ui.input.molecule('First reactant', {value: firstReactant, nullable: false, tooltipText: 'Reactant fragment of first monomer(s). Use numbered R groups in correspondence with the rest of the molecule.'});
74
+ const secondReactantInput = ui.input.molecule('Second reactant', {value: secondReactant, nullable: false, tooltipText: 'Reactant fragment of second monomer(s). Use numbered R groups in correspondence with the rest of the molecule.'});
75
+ const productInput = ui.input.molecule('Product', {value: product, nullable: false, tooltipText: 'Product fragment of the resulting monomer from the reaction of the two reactants. Use numbered R groups in correspondence with the reactants.'});
76
+
77
+ const validate = () => {
78
+ if (!firstReactantInput || !secondReactantInput || !productInput)
79
+ return 'Inputs not initialized yet';
80
+ const firstSmiles = rgroupToRadicalForm(correctRGroups(grok.chem.convert(firstReactantInput.value ?? '', grok.chem.Notation.Unknown, grok.chem.Notation.Smiles)));
81
+ const secondSmiles = rgroupToRadicalForm(correctRGroups(grok.chem.convert(secondReactantInput.value ?? '', grok.chem.Notation.Unknown, grok.chem.Notation.Smiles)));
82
+ const productSmiles = rgroupToRadicalForm(correctRGroups(grok.chem.convert(productInput.value ?? '', grok.chem.Notation.Unknown, grok.chem.Notation.Smiles)));
83
+ if (!firstSmiles || !secondSmiles || !productSmiles)
84
+ return 'First/Second Reactants and Product must all be set';
85
+ if (firstSmiles.indexOf('[C:1]') == -1)
86
+ return 'First Reactant must contain R1-group';
87
+ if (secondSmiles.indexOf('[C:2]') == -1)
88
+ return 'Second Reactant must contain R2-group';
89
+ if (productSmiles.indexOf('[C:1]') == -1 || productSmiles.indexOf('[C:2]') == -1)
90
+ return 'Product must contain R1- and R2-groups';
91
+ return null;
92
+ };
93
+ firstReactantInput.addValidator(validate);
94
+ secondReactantInput.addValidator(validate);
95
+ productInput.addValidator(validate);
96
+ const dialog = ui.dialog(preset ? 'Edit Reaction Rule' : 'Add Reaction Rule')
97
+ .add(codeInput)
98
+ .add(resultMonomerNameInput)
99
+ .add(firstMonomersInput)
100
+ .add(secondMonomersInput)
101
+ .add(firstReactantInput)
102
+ .add(secondReactantInput)
103
+ .add(productInput);
104
+ dialog.addButton('Save', () => {
105
+ const validationError = validate();
106
+ if (validationError != null) {
107
+ grok.shell.warning(validationError);
108
+ return;
109
+ }
110
+ const firstSmiles = rgroupToRadicalForm(correctRGroups(grok.chem.convert(firstReactantInput.value ?? '', grok.chem.Notation.Unknown, grok.chem.Notation.Smiles)));
111
+ const secondSmiles = rgroupToRadicalForm(correctRGroups(grok.chem.convert(secondReactantInput.value ?? '', grok.chem.Notation.Unknown, grok.chem.Notation.Smiles)));
112
+ const productSmiles = rgroupToRadicalForm(correctRGroups(grok.chem.convert(productInput.value ?? '', grok.chem.Notation.Unknown, grok.chem.Notation.Smiles)));
113
+ onOk({
114
+ code: codeInput.value!,
115
+ resultMonomerName: resultMonomerNameInput.value,
116
+ firstMonomers: firstMonomersInput.value.split(',').map((s) => s.trim()).filter((s) => s.length > 0),
117
+ secondMonomers: secondMonomersInput.value.split(',').map((s) => s.trim()).filter((s) => s.length > 0),
118
+ firstReactantSmiles: firstSmiles,
119
+ secondReactantSmiles: secondSmiles,
120
+ productSmiles: productSmiles,
121
+ rowIndex: preset?.rowIndex});
122
+ dialog.close();
123
+ });
124
+ dialog.show();
125
+ }
@@ -35,7 +35,7 @@ export class PolyToolConvertFuncEditor {
35
35
 
36
36
  private async initInputs(): Promise<void> {
37
37
  const getParam = (pName: string) => this.call.inputParams[pName];
38
-
38
+ const rulesForm = await this.ruleInputs.getForm();
39
39
  this.inputs = {
40
40
  table: (() => {
41
41
  const p = getParam(P.table);
@@ -49,7 +49,7 @@ export class PolyToolConvertFuncEditor {
49
49
  chiralityEngine: ui.input.forProperty(getParam(P.chiralityEngine).property),
50
50
  rules: {
51
51
  header: ui.inlineText([PT_UI_RULES_USED]),
52
- form: await this.ruleInputs.getForm(),
52
+ form: rulesForm,
53
53
  }
54
54
  };
55
55
  }