@datagrok/sequence-translator 1.10.6 → 1.10.8

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.
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@datagrok/sequence-translator",
3
3
  "friendlyName": "Sequence Translator",
4
- "version": "1.10.6",
4
+ "version": "1.10.8",
5
5
  "author": {
6
6
  "name": "Davit Rizhinashvili",
7
7
  "email": "drizhinashvili@datagrok.ai"
@@ -22,7 +22,7 @@
22
22
  }
23
23
  ],
24
24
  "dependencies": {
25
- "@datagrok-libraries/bio": "^5.60.2",
25
+ "@datagrok-libraries/bio": "^5.61.6",
26
26
  "@datagrok-libraries/chem-meta": "^1.2.8",
27
27
  "@datagrok-libraries/tutorials": "^1.6.1",
28
28
  "@datagrok-libraries/utils": "^4.6.5",
@@ -55,7 +55,6 @@
55
55
  "@typescript-eslint/eslint-plugin": "^7.2.0",
56
56
  "@typescript-eslint/parser": "^7.2.0",
57
57
  "css-loader": "^6.7.3",
58
- "datagrok-tools": "^5.0.0",
59
58
  "eslint": "^8.57.0",
60
59
  "eslint-config-google": "^0.14.0",
61
60
  "style-loader": "^3.3.1",
@@ -147,4 +147,8 @@ export namespace funcs {
147
147
  export async function applyNotationProviderForCyclized(col: DG.Column , separator: string ): Promise<void> {
148
148
  return await grok.functions.call('SequenceTranslator:ApplyNotationProviderForCyclized', { col, separator });
149
149
  }
150
+
151
+ export async function harmonizedSequenceNotationProviderConstructor(): Promise<any> {
152
+ return await grok.functions.call('SequenceTranslator:HarmonizedSequenceNotationProviderConstructor', {});
153
+ }
150
154
  }
package/src/package.g.ts CHANGED
@@ -221,3 +221,9 @@ export async function getPolyToolCombineDialog() : Promise<void> {
221
221
  export function applyNotationProviderForCyclized(col: DG.Column<any>, separator: string) : void {
222
222
  PackageFunctions.applyNotationProviderForCyclized(col, separator);
223
223
  }
224
+
225
+ //output: dynamic result
226
+ //meta.role: notationProviderConstructor
227
+ export async function harmonizedSequenceNotationProviderConstructor() : Promise<any> {
228
+ return await PackageFunctions.harmonizedSequenceNotationProviderConstructor();
229
+ }
package/src/package.ts CHANGED
@@ -3,7 +3,7 @@ import * as grok from 'datagrok-api/grok';
3
3
  import * as ui from 'datagrok-api/ui';
4
4
  import * as DG from 'datagrok-api/dg';
5
5
 
6
- import {NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule/consts';
6
+ import {NOTATION, NOTATION_PROVIDER_CONSTRUCTOR_ROLE} from '@datagrok-libraries/bio/src/utils/macromolecule/consts';
7
7
  import {SeqTemps} from '@datagrok-libraries/bio/src/utils/macromolecule/seq-handler';
8
8
 
9
9
  import {OligoToolkitPackage} from './apps/common/model/oligo-toolkit-package';
@@ -400,6 +400,14 @@ export class PackageFunctions {
400
400
  col.tags[PolyToolTags.dataRole] = 'template';
401
401
  col.temp[SeqTemps.notationProvider] = new CyclizedNotationProvider(separator, _package.helmHelper);
402
402
  }
403
+
404
+ @grok.decorators.func({
405
+ name: 'harmonizedSequenceNotationProviderConstructor',
406
+ meta: {role: 'notationProviderConstructor'}
407
+ })
408
+ static async harmonizedSequenceNotationProviderConstructor(): Promise<typeof CyclizedNotationProvider> {
409
+ return CyclizedNotationProvider;
410
+ }
403
411
  }
404
412
 
405
413
  //name: getSpecifiedAppView
@@ -40,3 +40,10 @@ export const PT_UI_DIALOG_CONVERSION = 'Poly Tool Conversion';
40
40
  export const PT_UI_DIALOG_UNRULE = 'Poly Tool Unrule';
41
41
  export const PT_UI_DIALOG_ENUMERATION = 'Poly Tool Enumeration';
42
42
  export const PT_UI_RULES_USED = 'Rules used';
43
+
44
+ export const PT_ENUM_TYPE_TOOLTIPS: Record<string, string> = {
45
+ 'single': 'Each position is enumerated independently. Total results = sum of monomers across all positions.',
46
+ 'parallel': 'The i-th result uses the i-th monomer from every position (zip). All positions must have the same number of monomers. Total results = number of monomers per position.',
47
+ 'matrix': 'Cartesian product of all positions. Total results = product of monomer counts across all positions.',
48
+ 'library': 'Substitutes all monomers from a selected library at a single position.',
49
+ };
@@ -25,9 +25,10 @@ import {PolyToolEnumeratorParams, PolyToolEnumeratorType, PolyToolEnumeratorType
25
25
  import {getLibrariesList, LIB_PATH} from './utils';
26
26
  import {doPolyToolEnumerateHelm, PT_HELM_EXAMPLE} from './pt-enumeration-helm';
27
27
  import {PolyToolPlaceholdersInput} from './pt-placeholders-input';
28
+ import {showMonomerSelectionDialog} from './pt-monomer-selection-dialog';
28
29
  import {defaultErrorHandler} from '../utils/err-info';
29
30
  import {PolyToolPlaceholdersBreadthInput} from './pt-placeholders-breadth-input';
30
- import {PT_UI_DIALOG_ENUMERATION, PT_UI_GET_HELM, PT_UI_HIGHLIGHT_MONOMERS, PT_UI_RULES_USED, PT_UI_USE_CHIRALITY} from './const';
31
+ import {PT_ENUM_TYPE_TOOLTIPS, PT_UI_DIALOG_ENUMERATION, PT_UI_GET_HELM, PT_UI_HIGHLIGHT_MONOMERS, PT_UI_RULES_USED, PT_UI_USE_CHIRALITY} from './const';
31
32
  import {PolyToolDataRole, PolyToolTags} from '../consts';
32
33
  import {RuleInputs, RULES_PATH, RULES_STORAGE_NAME} from './conversion/pt-rules';
33
34
  import {Chain} from './conversion/pt-chain';
@@ -38,8 +39,22 @@ import {buildMonomerHoverLink} from '@datagrok-libraries/bio/src/monomer-works/m
38
39
  import {getRdKitModule} from '@datagrok-libraries/bio/src/chem/rdkit-module';
39
40
 
40
41
  import {PolymerTypes} from '@datagrok-libraries/js-draw-lite/src/types/org';
41
- import { CyclizedNotationProvider } from '../utils/cyclized';
42
- import { INotationProvider } from '@datagrok-libraries/bio/src/utils/macromolecule/types';
42
+ import {CyclizedNotationProvider} from '../utils/cyclized';
43
+ import {INotationProvider, NotationProviderBase} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
44
+
45
+ /**
46
+ * PolyTool Enumeration Dialog
47
+ *
48
+ * Provides the UI for enumerating macromolecule variants. The user selects positions
49
+ * on a HELM molecule and specifies substitute monomers at each position. Supports
50
+ * multiple enumeration strategies (Single, Parallel, Matrix, Library) and optional
51
+ * downstream processing (atomic-level conversion, chirality, rules).
52
+ *
53
+ * Architecture:
54
+ * polyToolEnumerateHelmUI() - Entry point: creates, sizes, and shows the dialog
55
+ * getPolyToolEnumerateDialog() - Builds all inputs, validators, event wiring, and the dialog
56
+ * polyToolEnumerateSeq() - Executes enumeration and post-processes results into a DataFrame
57
+ */
43
58
 
44
59
  type PolyToolEnumerateInputs = {
45
60
  macromolecule: HelmInputBase;
@@ -72,15 +87,21 @@ type PolyToolEnumerateHelmSerialized = {
72
87
  library: string;
73
88
  };
74
89
 
90
+ /** Entry point: creates, sizes, and shows the enumeration dialog. */
75
91
  export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
76
92
  await _package.initPromise;
77
93
 
94
+ // Capture viewport dimensions for dialog sizing
78
95
  const maxWidth = window.innerWidth;
79
96
  const maxHeight = window.innerHeight;
80
97
 
81
98
  try {
82
99
  // eslint-disable-next-line prefer-const
83
100
  let dialog: DG.Dialog;
101
+
102
+ // Dynamically allocates remaining vertical space to flex-fit inputs (e.g. the macromolecule editor)
103
+ // after fixed-height inputs are laid out. fitInputs maps child element indices to proportional
104
+ // height weights; elements not in fitInputs get their natural height.
84
105
  function resizeInputs() {
85
106
  if (dialog == null) return;
86
107
 
@@ -106,18 +127,13 @@ export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
106
127
  };
107
128
  dialog = await getPolyToolEnumerateDialog(cell, resizeInputs);
108
129
 
130
+ // On first show, center the dialog at 70% of viewport; on subsequent resizes, just reflow inputs
109
131
  let isFirstShow = true;
110
132
  ui.onSizeChanged(dialog.root).subscribe(() => {
111
133
  if (isFirstShow) {
112
- // const dialogRootCash = $(dialog.root);
113
- // const contentMaxHeight = maxHeight -
114
- // dialogRootCash.find('div.d4-dialog-header').get(0)!.offsetHeight -
115
- // dialogRootCash.find('div.d4-dialog-footer').get(0)!.offsetHeight;
116
-
117
134
  const dialogWidth = maxWidth * 0.7;
118
135
  const dialogHeight = maxHeight * 0.7;
119
136
 
120
- // Centered, but resizable dialog
121
137
  dialog.root.style.width = `${Math.min(maxWidth, dialogWidth)}px`;
122
138
  dialog.root.style.height = `${Math.min(maxHeight, dialogHeight)}px`;
123
139
  dialog.root.style.left = `${Math.floor((maxWidth - dialog.root.offsetWidth) / 2)}px`;
@@ -140,19 +156,21 @@ export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
140
156
  }
141
157
  }
142
158
 
159
+ /** Builds and configures the enumeration dialog with all inputs, validators, and event handlers. */
143
160
  async function getPolyToolEnumerateDialog(
144
161
  cell?: DG.Cell, resizeInputs?: () => void
145
162
  ): Promise<DG.Dialog> {
146
163
  const logPrefix = `ST: PT: HelmDialog()`;
147
164
  let inputs: PolyToolEnumerateInputs;
148
165
  const subs: Unsubscribable[] = [];
149
- // will store previous enumeration type to support logic of cleanups
166
+ // Track previous enumeration type to clean up state when switching to/from Library mode
150
167
  let prevEnumerationType: PolyToolEnumeratorType = PolyToolEnumeratorTypes.Single;
151
168
  const destroy = () => {
152
169
  for (const sub of subs) sub.unsubscribe();
153
170
  inputs.placeholders.detach();
154
171
  };
155
172
  try {
173
+ // --- Initialize helpers (monomer lib, sequence helper, HELM helper) ---
156
174
  const libHelper = await getMonomerLibHelper();
157
175
  const monomerLib = libHelper.getMonomerLib();
158
176
  const seqHelper = await getSeqHelper();
@@ -161,6 +179,8 @@ async function getPolyToolEnumerateDialog(
161
179
  const [_libList, helmHelper] = await Promise.all([getLibrariesList(), getHelmHelper()]);
162
180
  const monomerLibFuncs = helmHelper.buildMonomersFuncsFromLib(monomerLib);
163
181
 
182
+ // Resolves the source macromolecule from the given cell or falls back to a default example.
183
+ // Returns the sequence value and its data role (macromolecule vs template).
164
184
  const getValue = (cell?: DG.Cell): [SeqValueBase, PolyToolDataRole] => {
165
185
  let resSeqValue: SeqValueBase;
166
186
  let resDataRole: PolyToolDataRole;
@@ -196,12 +216,14 @@ async function getPolyToolEnumerateDialog(
196
216
 
197
217
  let [seqValue, dataRole]: [SeqValueBase, PolyToolDataRole] = getValue(cell);
198
218
 
219
+ // --- UI state ---
199
220
  let srcId: { value: string, colName: string } | null = null;
200
221
  let ruleFileList: string[];
201
222
  let ruleInputs: RuleInputs;
202
223
  const trivialNameSampleDiv = ui.divText('', {style: {marginLeft: '8px', marginTop: '2px'}});
203
224
  const warningsTextDiv = ui.divText('', {style: {color: 'red'}});
204
- // #### Inputs
225
+
226
+ // === INPUT DEFINITIONS ===
205
227
  inputs = {
206
228
  macromolecule: helmHelper.createHelmInput(
207
229
  'Macromolecule', {
@@ -291,26 +313,76 @@ async function getPolyToolEnumerateDialog(
291
313
  library: ui.input.choice('Monomer Library', {items: _libList, value: _libList[0], nullable: true}) as DG.InputBase<string>,
292
314
  };
293
315
  // #### Inputs END
316
+
317
+ // Bind tooltip to enumerator type choice, updating on each change
318
+ const updateEnumTypeTooltip = () => {
319
+ const tooltipText = PT_ENUM_TYPE_TOOLTIPS[inputs.enumeratorType.value] ?? '';
320
+ ui.tooltip.bind(inputs.enumeratorType.input, tooltipText);
321
+ };
322
+ updateEnumTypeTooltip();
323
+
294
324
  inputs.library.root.style.setProperty('display', 'none');
295
325
  inputs.trivialNameCol.addOptions(trivialNameSampleDiv);
296
326
 
327
+ // Wire up monomer cell double-click to open selection dialog
328
+ inputs.placeholders.onMonomerCellEdit = async (position: number, currentMonomers: string[]) => {
329
+ const mol = inputs.macromolecule.molValue;
330
+ if (position < 0 || position >= mol.atoms.length)
331
+ return null;
332
+ const atom = mol.atoms[position];
333
+ const helmType: HelmType = atom.biotype()!;
334
+ const polymerType = helmTypeToPolymerType(helmType);
335
+ return showMonomerSelectionDialog(monomerLib, polymerType, currentMonomers);
336
+ };
337
+
338
+ // Wire up breadth monomer cell double-click to open selection dialog
339
+ inputs.placeholdersBreadth.onMonomerCellEdit = async (
340
+ start: number, _end: number, currentMonomers: string[],
341
+ ) => {
342
+ const mol = inputs.macromolecule.molValue;
343
+ if (start < 0 || start >= mol.atoms.length)
344
+ return null;
345
+ const atom = mol.atoms[start];
346
+ const helmType: HelmType = atom.biotype()!;
347
+ const polymerType = helmTypeToPolymerType(helmType);
348
+ return showMonomerSelectionDialog(monomerLib, polymerType, currentMonomers);
349
+ };
350
+
351
+ // === VALIDATORS ===
352
+ // Validates placeholder positions and monomers based on the current enumeration mode.
297
353
  let placeholdersValidity: string | null = null;
298
354
  inputs.placeholders.addValidator((value: string): string | null => {
355
+ placeholdersValidity = null;
299
356
  const errors: string[] = [];
357
+ setTimeout(() => { updateWarnings(); }, 100);
300
358
  try {
301
359
  // for library, ony one placeholder is allowed
302
360
  if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Library) {
303
- setTimeout(() => { updateWarnings(); }, 10);
304
361
  const pcs = inputs.placeholders.placeholdersValue;
305
362
  if (pcs.length > 1)
306
- return 'Only one placeholder is allowed for Library mode';
363
+ placeholdersValidity = 'Only one placeholder is allowed for Library mode';
307
364
  if (pcs.length === 1) {
308
365
  if (pcs[0].position == null)
309
- return 'Position is required for Library mode';
366
+ placeholdersValidity = 'Position is required for Library mode';
310
367
  if (pcs[0].position > inputs.macromolecule.molValue.atoms.length)
311
- return `There is no monomer at position ${pcs[0].position + 1}.`;
368
+ placeholdersValidity = `There is no monomer at position ${pcs[0].position + 1}.`;
369
+ }
370
+ return placeholdersValidity;
371
+ }
372
+
373
+ // Parallel mode: all placeholders must have the same monomer count
374
+ if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Parallel) {
375
+ const pcs = inputs.placeholders.placeholdersValue;
376
+ const nonEmpty = pcs.filter((ph) => ph.monomers.length > 0);
377
+ if (nonEmpty.length > 1) {
378
+ const firstCount = nonEmpty[0].monomers.length;
379
+ const mismatch = nonEmpty.find((ph) => ph.monomers.length !== firstCount);
380
+ if (mismatch) {
381
+ placeholdersValidity = `Parallel mode requires all positions to have the same number of monomers. ` +
382
+ `Position ${nonEmpty[0].position + 1} has ${firstCount}, ` +
383
+ `but position ${mismatch.position + 1} has ${mismatch.monomers.length}.`;
384
+ }
312
385
  }
313
- return null;
314
386
  }
315
387
 
316
388
  if (dataRole !== PolyToolDataRole.macromolecule)
@@ -348,7 +420,7 @@ async function getPolyToolEnumerateDialog(
348
420
  .join('\n');
349
421
  if (Object.keys(byTypeStr).length > 0)
350
422
  errors.push(`Placeholders contain missed monomers: ${byTypeStr}`);
351
- placeholdersValidity = errors.length > 0 ? errors.join('\n') : null;
423
+ placeholdersValidity = errors.length > 0 ? errors.join('\n') : placeholdersValidity;
352
424
  } catch (err: any) {
353
425
  const [errMsg, errStack] = defaultErrorHandler(err, false);
354
426
  placeholdersValidity = errMsg;
@@ -435,8 +507,14 @@ async function getPolyToolEnumerateDialog(
435
507
  defaultErrorHandler(err, false);
436
508
  }
437
509
  prevEnumerationType = inputs.enumeratorType.value;
510
+ updateEnumTypeTooltip();
511
+ // Re-validate placeholders (Parallel mode has different constraints than Single/Matrix)
512
+ inputs.placeholders.fireChanged();
438
513
  });
439
514
 
515
+ // === MOLECULE INTERACTION EVENT HANDLERS ===
516
+
517
+ // Mouse move: show tooltip of substitute monomers at hovered position
440
518
  subs.push(inputs.macromolecule.onMouseMove.subscribe((e: MouseEvent) => {
441
519
  try {
442
520
  _package.logger.debug(`${logPrefix}, placeholdersInput.onMouseMove()`);
@@ -461,6 +539,7 @@ async function getPolyToolEnumerateDialog(
461
539
  defaultErrorHandler(err, false);
462
540
  }
463
541
  }));
542
+ // Click on molecule: add clicked atom position to the placeholders grid
464
543
  subs.push(inputs.macromolecule.onClick.subscribe((e: MouseEvent) => {
465
544
  try {
466
545
  _package.logger.debug(`${logPrefix}, placeholdersInput.onClick()`);
@@ -503,6 +582,9 @@ async function getPolyToolEnumerateDialog(
503
582
  inputs.macromolecule.root.style.setProperty('min-width', '250px', 'important');
504
583
  // inputs.macromolecule.root.style.setProperty('max-height', '300px', 'important');
505
584
 
585
+ // === VIEW UPDATE HELPERS ===
586
+
587
+ // Highlights atoms in the molecule editor that have placeholders defined
506
588
  const updateViewMol = () => {
507
589
  const phPosSet = new Set<number>(inputs.placeholders.placeholdersValue.map((ph) => ph.position));
508
590
  const mol = inputs.macromolecule.molValue;
@@ -513,6 +595,7 @@ async function getPolyToolEnumerateDialog(
513
595
  inputs.macromolecule.redraw();
514
596
  };
515
597
 
598
+ // Shows/hides conversion rules UI based on toAtomicLevel setting and data role
516
599
  const updateViewRules = () => {
517
600
  if (inputs.toAtomicLevel.value && dataRole === PolyToolDataRole.template) {
518
601
  inputs.generateHelm.root.style.removeProperty('display');
@@ -528,7 +611,7 @@ async function getPolyToolEnumerateDialog(
528
611
  if (resizeInputs)
529
612
  resizeInputs();
530
613
  };
531
-
614
+ let dialogOKButton: HTMLButtonElement | null | undefined = null;
532
615
  const updateWarnings = () => {
533
616
  const warnings = placeholdersValidity;
534
617
  // const iw = inputs.warnings;
@@ -540,6 +623,8 @@ async function getPolyToolEnumerateDialog(
540
623
 
541
624
  w.innerText = warnings;
542
625
  w.style.removeProperty('display');
626
+ if (dialogOKButton)
627
+ dialogOKButton.disabled = true;
543
628
  } else {
544
629
  // iw.value = ''; // <- breaks dialog resize
545
630
  // iw.enabled = false;
@@ -547,6 +632,8 @@ async function getPolyToolEnumerateDialog(
547
632
 
548
633
  w.innerText = '';
549
634
  w.style.setProperty('display', 'none');
635
+ if (dialogOKButton)
636
+ dialogOKButton.disabled = false;
550
637
  }
551
638
  //resizeInputs();
552
639
  };
@@ -572,6 +659,8 @@ async function getPolyToolEnumerateDialog(
572
659
  fillForCurrentCell(seqValue, dataRole, cell);
573
660
  updateViewRules();
574
661
 
662
+ // === EXECUTION (OK button handler) ===
663
+ // Pre-flight validates inputs, resolves Library mode, builds params, and runs enumeration.
575
664
  const exec = async (): Promise<void> => {
576
665
  try {
577
666
  const srcHelm = inputs.macromolecule.stringValue;
@@ -600,6 +689,7 @@ async function getPolyToolEnumerateDialog(
600
689
  await getHelmHelper(); // initializes JSDraw and org
601
690
 
602
691
  const placeHoldersValue = inputs.placeholders.placeholdersValue;
692
+ // Library mode: load all monomers from the selected library and convert to Single mode
603
693
  let enumerationType = inputs.enumeratorType.value;
604
694
  if (enumerationType === PolyToolEnumeratorTypes.Library) {
605
695
  if (placeHoldersValue.length !== 1) {
@@ -622,12 +712,33 @@ async function getPolyToolEnumerateDialog(
622
712
  enumerationType = PolyToolEnumeratorTypes.Single;
623
713
  }
624
714
 
715
+ if (enumerationType === PolyToolEnumeratorTypes.Parallel) {
716
+ const nonEmpty = placeHoldersValue.filter((ph) => ph.monomers.length > 0);
717
+ if (nonEmpty.length > 1) {
718
+ const firstCount = nonEmpty[0].monomers.length;
719
+ if (nonEmpty.some((ph) => ph.monomers.length !== firstCount)) {
720
+ grok.shell.warning('Parallel mode requires all positions to have the same number of monomers');
721
+ return;
722
+ }
723
+ }
724
+ }
725
+
625
726
  const params: PolyToolEnumeratorParams = {
626
727
  placeholders: placeHoldersValue,
627
728
  type: enumerationType,
628
729
  breadthPlaceholders: inputs.placeholdersBreadth.placeholdersBreadthValue,
629
730
  keepOriginal: inputs.keepOriginal.value,
630
731
  };
732
+
733
+ if (cell?.column?.temp?.[SeqTemps.notationProvider] &&
734
+ !(cell.column.temp[SeqTemps.notationProvider] instanceof CyclizedNotationProvider)) {
735
+ const notationProvider = cell.column.temp[SeqTemps.notationProvider] as INotationProvider;
736
+ if ((notationProvider.constructor as typeof NotationProviderBase).implementsFromHelm) {
737
+ const cons = notationProvider.constructor as typeof NotationProviderBase;
738
+ params.fromHelmNotation = {notationName: cons.notationName, convert: (helm) => cons.convertFromHelm(helm, {})};
739
+ }
740
+ }
741
+
631
742
  const toAtomicLevelV: boolean = inputs.toAtomicLevel.value;
632
743
  const enumeratorResDf = await polyToolEnumerateSeq(srcHelm, dataRole, srcId, params,
633
744
  toAtomicLevelV ? {
@@ -644,6 +755,8 @@ async function getPolyToolEnumerateDialog(
644
755
  }
645
756
  };
646
757
 
758
+ // === DIALOG CONSTRUCTION AND LAYOUT ===
759
+ // Layout: macromolecule editor on top, two-column (placeholders | breadth), two-column (options | rules)
647
760
  const dialog = ui.dialog({title: PT_UI_DIALOG_ENUMERATION, showFooter: true})
648
761
  .add(inputs.macromolecule.root)
649
762
  .add(ui.divH([
@@ -668,14 +781,20 @@ async function getPolyToolEnumerateDialog(
668
781
  {style: {width: '50%'}})],
669
782
  {style: {width: '100%'}}))
670
783
  .add(warningsTextDiv)
671
- // .addButton('Enumerate', () => {
672
- // execDialog()
673
- // .then(() => {});
674
- // }, 0, 'Keeps the dialog open')
675
- .onOK(() => { exec(); });
784
+ .onOK(() => {
785
+ if (placeholdersValidity) {
786
+ updateWarnings();
787
+ grok.shell.warning('Please fix validation errors before running enumeration');
788
+ return;
789
+ }
790
+ exec();
791
+ });
792
+ // .onOK(() => { exec(); });
793
+ dialogOKButton = dialog.getButton('OK') as HTMLButtonElement;
676
794
  subs.push(dialog.onClose.subscribe(() => {
677
795
  destroy();
678
796
  }));
797
+ // Dialog history: serialization/deserialization for persisting dialog state across sessions
679
798
  dialog.history(
680
799
  /* getInput */ (): PolyToolEnumerateHelmSerialized => {
681
800
  return {
@@ -707,6 +826,10 @@ async function getPolyToolEnumerateDialog(
707
826
  inputs.highlightMonomers.value = x.highlightMonomers ?? false;
708
827
  ruleInputs.setActive(x.rules);
709
828
  inputs.library.value = x.library;
829
+ setTimeout(() => {
830
+ inputs.placeholders.invalidateGrid();
831
+ inputs.placeholdersBreadth.invalidateGrid();
832
+ }, 100);
710
833
  });
711
834
  return dialog;
712
835
  } catch (err: any) {
@@ -715,10 +838,16 @@ async function getPolyToolEnumerateDialog(
715
838
  }
716
839
  }
717
840
 
718
- /**
719
- * @param {DG.SemanticValue} srcValue Source value to enumerate, either of data role
720
- * {@link PolyToolDataRole.template} or {@link PolyToolDataRole.macromolecule}
721
- * */
841
+ /** Executes enumeration and post-processes results into a DataFrame.
842
+ * Handles both macromolecule and template data roles, with optional
843
+ * atomic-level conversion (chirality, highlighting, rules).
844
+ *
845
+ * @param srcHelm Source HELM string to enumerate
846
+ * @param dataRole Whether the source is a {@link PolyToolDataRole.template} or {@link PolyToolDataRole.macromolecule}
847
+ * @param srcId Optional trivial name column info for generating IDs
848
+ * @param params Enumeration parameters (type, placeholders, keepOriginal)
849
+ * @param toAtomicLevel Post-processing options, or false to skip
850
+ */
722
851
  export async function polyToolEnumerateSeq(
723
852
  srcHelm: string, dataRole: PolyToolDataRole, srcId: { value: string, colName: string } | null,
724
853
  params: PolyToolEnumeratorParams,
@@ -731,7 +860,10 @@ export async function polyToolEnumerateSeq(
731
860
  const rdKitModule = await getRdKitModule();
732
861
  const monomerLib = libHelper.getMonomerLib(); // TODO: Get monomer lib from src SeqValueBase
733
862
 
863
+ // Core enumeration: produces [helm, id] pairs
734
864
  const resList = doPolyToolEnumerateHelm(srcHelm, srcId?.value ?? '', params);
865
+
866
+ // Create result column based on data role (macromolecule uses HELM directly, template converts via Chain)
735
867
  let enumCol: DG.Column<string>;
736
868
  switch (dataRole) {
737
869
  case PolyToolDataRole.macromolecule: {
@@ -754,11 +886,19 @@ export async function polyToolEnumerateSeq(
754
886
  }
755
887
  }
756
888
  const enumeratorResDf = DG.DataFrame.fromColumns([enumCol]);
889
+
890
+ if (dataRole === PolyToolDataRole.macromolecule && params.fromHelmNotation) {
891
+ const c = DG.Column.fromType(DG.COLUMN_TYPE.STRING, `${params.fromHelmNotation.notationName}(${enumCol.name})`, enumeratorResDf.rowCount)
892
+ .init((rowIdx: number) => enumCol.isNone(rowIdx) ? null : params.fromHelmNotation!.convert(enumCol.get(rowIdx)!));
893
+ enumeratorResDf.columns.add(c);
894
+ }
895
+
757
896
  await grok.data.detectSemanticTypes(enumeratorResDf);
758
897
  if (dataRole == PolyToolDataRole.template)
759
898
  PackageFunctions.applyNotationProviderForCyclized(enumCol, '-');
760
899
 
761
900
 
901
+ // Optional post-processing: convert to atomic level with chirality/highlighting
762
902
  if (toAtomicLevel) {
763
903
  let resHelmCol: DG.Column<string>;
764
904
  if (dataRole === PolyToolDataRole.macromolecule) {
@@ -775,6 +915,7 @@ export async function polyToolEnumerateSeq(
775
915
  }
776
916
  }
777
917
 
918
+ // Add trivial name ID column if the source had one
778
919
  if (srcId) {
779
920
  const enumIdCol = DG.Column.fromType(DG.COLUMN_TYPE.STRING, srcId.colName, resList.length)
780
921
  .init((rowIdx: number) => resList[rowIdx][1]);
@@ -61,6 +61,39 @@ function getPtEnumeratorMatrix(m: HelmMol, placeholders: PolyToolPlaceholder[]):
61
61
  return resMolList;
62
62
  }
63
63
 
64
+ /** Parallel (zip) enumeration: the i-th result takes the i-th monomer from each placeholder position.
65
+ * All placeholders must have the same number of monomers (validated upstream).
66
+ * With K positions and N monomers each, produces exactly N results. */
67
+ function getPtEnumeratorParallel(m: HelmMol, placeholders: PolyToolPlaceholder[]): HelmMol[] {
68
+ if (placeholders.length === 0)
69
+ return [];
70
+
71
+ const monomerCount = placeholders[0].monomers.length;
72
+ for (const ph of placeholders) {
73
+ if (ph.monomers.length !== monomerCount)
74
+ throw new Error(`Parallel enumeration requires all positions to have the same number of monomers`);
75
+ }
76
+
77
+ const resMolList: HelmMol[] = new Array<HelmMol>(monomerCount);
78
+ for (let i = 0; i < monomerCount; i++) {
79
+ const resM = m.clone() as HelmMol;
80
+ const nameParts: string[] = [];
81
+ for (const ph of placeholders) {
82
+ const pos = ph.position;
83
+ const newSymbol = ph.monomers[i];
84
+ const oldSymbol = resM.atoms[pos].elem;
85
+ resM.atoms[pos].elem = newSymbol;
86
+
87
+ const idOldSymbol = oldSymbol?.length > 1 ? `[${oldSymbol}]` : oldSymbol;
88
+ const idNewSymbol = newSymbol?.length > 1 ? `[${newSymbol}]` : newSymbol;
89
+ nameParts.push(`${idOldSymbol}${pos + 1}${idNewSymbol}`);
90
+ }
91
+ resM.name = `${m.name}-${nameParts.join('-')}`;
92
+ resMolList[i] = resM;
93
+ }
94
+ return resMolList;
95
+ }
96
+
64
97
  function getPtEnumeratorBreadth(m: HelmMol, placeholdersBreadth: PolyToolBreadthPlaceholder[]): HelmMol[] {
65
98
  if (placeholdersBreadth.length == 0)
66
99
  return [];
@@ -90,6 +123,10 @@ export function doPolyToolEnumerateHelm(
90
123
  resMolList = getPtEnumeratorSingle(molHandler.m, params.placeholders);
91
124
  break;
92
125
  }
126
+ case PolyToolEnumeratorTypes.Parallel: {
127
+ resMolList = getPtEnumeratorParallel(molHandler.m, params.placeholders);
128
+ break;
129
+ }
93
130
  case PolyToolEnumeratorTypes.Matrix: {
94
131
  resMolList = getPtEnumeratorMatrix(molHandler.m, params.placeholders);
95
132
  break;