@datagrok/sequence-translator 1.10.7 → 1.10.9

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.7",
4
+ "version": "1.10.9",
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.61.6",
25
+ "@datagrok-libraries/bio": "^5.62.0",
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",
@@ -2,7 +2,6 @@ import * as grok from 'datagrok-api/grok';
2
2
  import * as ui from 'datagrok-api/ui';
3
3
  import * as DG from 'datagrok-api/dg';
4
4
 
5
- import {delay} from '@datagrok-libraries/test/src/test';
6
5
  import {PackageFunctions} from '../package';
7
6
  import {tryCatch} from '../apps/common/model/helpers';
8
7
 
@@ -23,7 +22,7 @@ export async function demoOligoPatternUI() {
23
22
  export async function demoOligoStructureUI() {
24
23
  await tryCatch(async () => {
25
24
  async function setInputValue(idx: number, sequence: string): Promise<void> {
26
- await delay(500);
25
+ await DG.delay(500);
27
26
  const textInputs: NodeListOf<HTMLTextAreaElement> = document.querySelectorAll('.st-colored-text-input > textarea');
28
27
  const textarea = textInputs[idx];
29
28
  textarea.value = sequence;
@@ -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
  }
@@ -40,3 +40,9 @@ 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
+ };
@@ -22,13 +22,12 @@ import {NOTATION} from '@datagrok-libraries/bio/src/utils/macromolecule';
22
22
  import {SeqTemps} from '@datagrok-libraries/bio/src/utils/macromolecule/seq-handler';
23
23
 
24
24
  import {PolyToolEnumeratorParams, PolyToolEnumeratorType, PolyToolEnumeratorTypes} from './types';
25
- import {getLibrariesList, LIB_PATH} from './utils';
26
25
  import {doPolyToolEnumerateHelm, PT_HELM_EXAMPLE} from './pt-enumeration-helm';
27
26
  import {PolyToolPlaceholdersInput} from './pt-placeholders-input';
28
27
  import {showMonomerSelectionDialog} from './pt-monomer-selection-dialog';
29
28
  import {defaultErrorHandler} from '../utils/err-info';
30
29
  import {PolyToolPlaceholdersBreadthInput} from './pt-placeholders-breadth-input';
31
- import {PT_UI_DIALOG_ENUMERATION, PT_UI_GET_HELM, PT_UI_HIGHLIGHT_MONOMERS, PT_UI_RULES_USED, PT_UI_USE_CHIRALITY} from './const';
30
+ 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';
32
31
  import {PolyToolDataRole, PolyToolTags} from '../consts';
33
32
  import {RuleInputs, RULES_PATH, RULES_STORAGE_NAME} from './conversion/pt-rules';
34
33
  import {Chain} from './conversion/pt-chain';
@@ -39,8 +38,22 @@ import {buildMonomerHoverLink} from '@datagrok-libraries/bio/src/monomer-works/m
39
38
  import {getRdKitModule} from '@datagrok-libraries/bio/src/chem/rdkit-module';
40
39
 
41
40
  import {PolymerTypes} from '@datagrok-libraries/js-draw-lite/src/types/org';
42
- import { CyclizedNotationProvider } from '../utils/cyclized';
43
- import { INotationProvider } from '@datagrok-libraries/bio/src/utils/macromolecule/types';
41
+ import {CyclizedNotationProvider} from '../utils/cyclized';
42
+ import {INotationProvider, NotationProviderBase} from '@datagrok-libraries/bio/src/utils/macromolecule/types';
43
+
44
+ /**
45
+ * PolyTool Enumeration Dialog
46
+ *
47
+ * Provides the UI for enumerating macromolecule variants. The user selects positions
48
+ * on a HELM molecule and specifies substitute monomers at each position. Supports
49
+ * multiple enumeration strategies (Single, Parallel, Matrix) and optional
50
+ * downstream processing (atomic-level conversion, chirality, rules).
51
+ *
52
+ * Architecture:
53
+ * polyToolEnumerateHelmUI() - Entry point: creates, sizes, and shows the dialog
54
+ * getPolyToolEnumerateDialog() - Builds all inputs, validators, event wiring, and the dialog
55
+ * polyToolEnumerateSeq() - Executes enumeration and post-processes results into a DataFrame
56
+ */
44
57
 
45
58
  type PolyToolEnumerateInputs = {
46
59
  macromolecule: HelmInputBase;
@@ -53,7 +66,6 @@ type PolyToolEnumerateInputs = {
53
66
  generateHelm: DG.InputBase<boolean>;
54
67
  chiralityEngine: DG.InputBase<boolean>;
55
68
  highlightMonomers: DG.InputBase<boolean>;
56
- library: DG.InputBase<string>;
57
69
  rules: { header: HTMLElement, form: HTMLDivElement },
58
70
  };
59
71
 
@@ -70,18 +82,23 @@ type PolyToolEnumerateHelmSerialized = {
70
82
  chiralityEngine: boolean;
71
83
  highlightMonomers: boolean;
72
84
  rules: string[],
73
- library: string;
74
85
  };
75
86
 
87
+ /** Entry point: creates, sizes, and shows the enumeration dialog. */
76
88
  export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
77
89
  await _package.initPromise;
78
90
 
91
+ // Capture viewport dimensions for dialog sizing
79
92
  const maxWidth = window.innerWidth;
80
93
  const maxHeight = window.innerHeight;
81
94
 
82
95
  try {
83
96
  // eslint-disable-next-line prefer-const
84
97
  let dialog: DG.Dialog;
98
+
99
+ // Dynamically allocates remaining vertical space to flex-fit inputs (e.g. the macromolecule editor)
100
+ // after fixed-height inputs are laid out. fitInputs maps child element indices to proportional
101
+ // height weights; elements not in fitInputs get their natural height.
85
102
  function resizeInputs() {
86
103
  if (dialog == null) return;
87
104
 
@@ -107,18 +124,13 @@ export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
107
124
  };
108
125
  dialog = await getPolyToolEnumerateDialog(cell, resizeInputs);
109
126
 
127
+ // On first show, center the dialog at 70% of viewport; on subsequent resizes, just reflow inputs
110
128
  let isFirstShow = true;
111
129
  ui.onSizeChanged(dialog.root).subscribe(() => {
112
130
  if (isFirstShow) {
113
- // const dialogRootCash = $(dialog.root);
114
- // const contentMaxHeight = maxHeight -
115
- // dialogRootCash.find('div.d4-dialog-header').get(0)!.offsetHeight -
116
- // dialogRootCash.find('div.d4-dialog-footer').get(0)!.offsetHeight;
117
-
118
131
  const dialogWidth = maxWidth * 0.7;
119
132
  const dialogHeight = maxHeight * 0.7;
120
133
 
121
- // Centered, but resizable dialog
122
134
  dialog.root.style.width = `${Math.min(maxWidth, dialogWidth)}px`;
123
135
  dialog.root.style.height = `${Math.min(maxHeight, dialogHeight)}px`;
124
136
  dialog.root.style.left = `${Math.floor((maxWidth - dialog.root.offsetWidth) / 2)}px`;
@@ -141,27 +153,30 @@ export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
141
153
  }
142
154
  }
143
155
 
156
+ /** Builds and configures the enumeration dialog with all inputs, validators, and event handlers. */
144
157
  async function getPolyToolEnumerateDialog(
145
158
  cell?: DG.Cell, resizeInputs?: () => void
146
159
  ): Promise<DG.Dialog> {
147
160
  const logPrefix = `ST: PT: HelmDialog()`;
148
161
  let inputs: PolyToolEnumerateInputs;
149
162
  const subs: Unsubscribable[] = [];
150
- // will store previous enumeration type to support logic of cleanups
151
163
  let prevEnumerationType: PolyToolEnumeratorType = PolyToolEnumeratorTypes.Single;
152
164
  const destroy = () => {
153
165
  for (const sub of subs) sub.unsubscribe();
154
166
  inputs.placeholders.detach();
155
167
  };
156
168
  try {
169
+ // --- Initialize helpers (monomer lib, sequence helper, HELM helper) ---
157
170
  const libHelper = await getMonomerLibHelper();
158
171
  const monomerLib = libHelper.getMonomerLib();
159
172
  const seqHelper = await getSeqHelper();
160
173
  const emptyDf: DG.DataFrame = DG.DataFrame.fromColumns([]);
161
174
 
162
- const [_libList, helmHelper] = await Promise.all([getLibrariesList(), getHelmHelper()]);
175
+ const helmHelper = await getHelmHelper();
163
176
  const monomerLibFuncs = helmHelper.buildMonomersFuncsFromLib(monomerLib);
164
177
 
178
+ // Resolves the source macromolecule from the given cell or falls back to a default example.
179
+ // Returns the sequence value and its data role (macromolecule vs template).
165
180
  const getValue = (cell?: DG.Cell): [SeqValueBase, PolyToolDataRole] => {
166
181
  let resSeqValue: SeqValueBase;
167
182
  let resDataRole: PolyToolDataRole;
@@ -197,12 +212,14 @@ async function getPolyToolEnumerateDialog(
197
212
 
198
213
  let [seqValue, dataRole]: [SeqValueBase, PolyToolDataRole] = getValue(cell);
199
214
 
215
+ // --- UI state ---
200
216
  let srcId: { value: string, colName: string } | null = null;
201
217
  let ruleFileList: string[];
202
218
  let ruleInputs: RuleInputs;
203
219
  const trivialNameSampleDiv = ui.divText('', {style: {marginLeft: '8px', marginTop: '2px'}});
204
220
  const warningsTextDiv = ui.divText('', {style: {color: 'red'}});
205
- // #### Inputs
221
+
222
+ // === INPUT DEFINITIONS ===
206
223
  inputs = {
207
224
  macromolecule: helmHelper.createHelmInput(
208
225
  'Macromolecule', {
@@ -289,10 +306,16 @@ async function getPolyToolEnumerateDialog(
289
306
  },
290
307
  nullable: true,
291
308
  }),
292
- library: ui.input.choice('Monomer Library', {items: _libList, value: _libList[0], nullable: true}) as DG.InputBase<string>,
293
309
  };
294
310
  // #### Inputs END
295
- inputs.library.root.style.setProperty('display', 'none');
311
+
312
+ // Bind tooltip to enumerator type choice, updating on each change
313
+ const updateEnumTypeTooltip = () => {
314
+ const tooltipText = PT_ENUM_TYPE_TOOLTIPS[inputs.enumeratorType.value] ?? '';
315
+ ui.tooltip.bind(inputs.enumeratorType.input, tooltipText);
316
+ };
317
+ updateEnumTypeTooltip();
318
+
296
319
  inputs.trivialNameCol.addOptions(trivialNameSampleDiv);
297
320
 
298
321
  // Wire up monomer cell double-click to open selection dialog
@@ -303,7 +326,7 @@ async function getPolyToolEnumerateDialog(
303
326
  const atom = mol.atoms[position];
304
327
  const helmType: HelmType = atom.biotype()!;
305
328
  const polymerType = helmTypeToPolymerType(helmType);
306
- return showMonomerSelectionDialog(monomerLib, polymerType, currentMonomers);
329
+ return showMonomerSelectionDialog(monomerLib, polymerType, currentMonomers, libHelper);
307
330
  };
308
331
 
309
332
  // Wire up breadth monomer cell double-click to open selection dialog
@@ -316,26 +339,30 @@ async function getPolyToolEnumerateDialog(
316
339
  const atom = mol.atoms[start];
317
340
  const helmType: HelmType = atom.biotype()!;
318
341
  const polymerType = helmTypeToPolymerType(helmType);
319
- return showMonomerSelectionDialog(monomerLib, polymerType, currentMonomers);
342
+ return showMonomerSelectionDialog(monomerLib, polymerType, currentMonomers, libHelper);
320
343
  };
321
344
 
345
+ // === VALIDATORS ===
346
+ // Validates placeholder positions and monomers based on the current enumeration mode.
322
347
  let placeholdersValidity: string | null = null;
323
348
  inputs.placeholders.addValidator((value: string): string | null => {
349
+ placeholdersValidity = null;
324
350
  const errors: string[] = [];
351
+ setTimeout(() => { updateWarnings(); }, 100);
325
352
  try {
326
- // for library, ony one placeholder is allowed
327
- if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Library) {
328
- setTimeout(() => { updateWarnings(); }, 10);
353
+ // Parallel mode: all placeholders must have the same monomer count
354
+ if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Parallel) {
329
355
  const pcs = inputs.placeholders.placeholdersValue;
330
- if (pcs.length > 1)
331
- return 'Only one placeholder is allowed for Library mode';
332
- if (pcs.length === 1) {
333
- if (pcs[0].position == null)
334
- return 'Position is required for Library mode';
335
- if (pcs[0].position > inputs.macromolecule.molValue.atoms.length)
336
- return `There is no monomer at position ${pcs[0].position + 1}.`;
356
+ const nonEmpty = pcs.filter((ph) => ph.monomers.length > 0);
357
+ if (nonEmpty.length > 1) {
358
+ const firstCount = nonEmpty[0].monomers.length;
359
+ const mismatch = nonEmpty.find((ph) => ph.monomers.length !== firstCount);
360
+ if (mismatch) {
361
+ placeholdersValidity = `Parallel mode requires all positions to have the same number of monomers. ` +
362
+ `Position ${nonEmpty[0].position + 1} has ${firstCount}, ` +
363
+ `but position ${mismatch.position + 1} has ${mismatch.monomers.length}.`;
364
+ }
337
365
  }
338
- return null;
339
366
  }
340
367
 
341
368
  if (dataRole !== PolyToolDataRole.macromolecule)
@@ -373,7 +400,7 @@ async function getPolyToolEnumerateDialog(
373
400
  .join('\n');
374
401
  if (Object.keys(byTypeStr).length > 0)
375
402
  errors.push(`Placeholders contain missed monomers: ${byTypeStr}`);
376
- placeholdersValidity = errors.length > 0 ? errors.join('\n') : null;
403
+ placeholdersValidity = errors.length > 0 ? errors.join('\n') : placeholdersValidity;
377
404
  } catch (err: any) {
378
405
  const [errMsg, errStack] = defaultErrorHandler(err, false);
379
406
  placeholdersValidity = errMsg;
@@ -433,35 +460,16 @@ async function getPolyToolEnumerateDialog(
433
460
  // return placeholdersValidity;
434
461
  // });
435
462
 
436
- inputs.library.addValidator((_value) => {
437
- if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Library && !inputs.library.value)
438
- return 'Monomer Library is required for this enumerator type';
439
- return null;
440
- });
441
-
442
- inputs.library.onChanged.subscribe(() => {
443
- if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Library)
444
- inputs.placeholders.setMonomersValue(0, inputs.library.value ?? '');
445
- });
446
-
447
463
  inputs.enumeratorType.onChanged.subscribe((_) => {
448
- const isLibraryModeChosen = inputs.enumeratorType.value === PolyToolEnumeratorTypes.Library;
449
- inputs.library.root.style.setProperty('display', isLibraryModeChosen ? 'flex' : 'none');
450
- try {
451
- if ((prevEnumerationType !== PolyToolEnumeratorTypes.Library && isLibraryModeChosen) || (prevEnumerationType === PolyToolEnumeratorTypes.Library && !isLibraryModeChosen)) {
452
- const prevSinglePosition = inputs.placeholders.placeholdersValue.length === 1 ? inputs.placeholders.placeholdersValue[0].position : null;
453
- // clear the placeHolders input and helm inputs if switching happened to/from Library mode
454
- inputs.placeholders.clearInput();
455
- inputs.placeholdersBreadth.clearInput();
456
- if (isLibraryModeChosen && prevSinglePosition != null)
457
- inputs.placeholders.addPosition(prevSinglePosition, inputs.library.value ?? '');
458
- }
459
- } catch (err: any) {
460
- defaultErrorHandler(err, false);
461
- }
462
464
  prevEnumerationType = inputs.enumeratorType.value;
465
+ updateEnumTypeTooltip();
466
+ // Re-validate placeholders (Parallel mode has different constraints than Single/Matrix)
467
+ inputs.placeholders.fireChanged();
463
468
  });
464
469
 
470
+ // === MOLECULE INTERACTION EVENT HANDLERS ===
471
+
472
+ // Mouse move: show tooltip of substitute monomers at hovered position
465
473
  subs.push(inputs.macromolecule.onMouseMove.subscribe((e: MouseEvent) => {
466
474
  try {
467
475
  _package.logger.debug(`${logPrefix}, placeholdersInput.onMouseMove()`);
@@ -486,6 +494,7 @@ async function getPolyToolEnumerateDialog(
486
494
  defaultErrorHandler(err, false);
487
495
  }
488
496
  }));
497
+ // Click on molecule: add clicked atom position to the placeholders grid
489
498
  subs.push(inputs.macromolecule.onClick.subscribe((e: MouseEvent) => {
490
499
  try {
491
500
  _package.logger.debug(`${logPrefix}, placeholdersInput.onClick()`);
@@ -496,13 +505,7 @@ async function getPolyToolEnumerateDialog(
496
505
  const clickedAtom = helmHelper.getHoveredAtom(argsX, argsY, mol, inputs.macromolecule.root.clientHeight);
497
506
  if (clickedAtom) {
498
507
  const clickedAtomContIdx = clickedAtom._parent.atoms.indexOf(clickedAtom);
499
- const clickedAtomContIdxStr = String(clickedAtomContIdx + 1);
500
- let monomerValue = '';
501
- if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Library) {
502
- inputs.placeholders.clearInput();
503
- monomerValue = inputs.library.value ?? '';
504
- }
505
- inputs.placeholders.addPosition(clickedAtomContIdx, monomerValue);
508
+ inputs.placeholders.addPosition(clickedAtomContIdx, '');
506
509
  }
507
510
  } catch (err: any) {
508
511
  defaultErrorHandler(err);
@@ -528,6 +531,9 @@ async function getPolyToolEnumerateDialog(
528
531
  inputs.macromolecule.root.style.setProperty('min-width', '250px', 'important');
529
532
  // inputs.macromolecule.root.style.setProperty('max-height', '300px', 'important');
530
533
 
534
+ // === VIEW UPDATE HELPERS ===
535
+
536
+ // Highlights atoms in the molecule editor that have placeholders defined
531
537
  const updateViewMol = () => {
532
538
  const phPosSet = new Set<number>(inputs.placeholders.placeholdersValue.map((ph) => ph.position));
533
539
  const mol = inputs.macromolecule.molValue;
@@ -538,6 +544,7 @@ async function getPolyToolEnumerateDialog(
538
544
  inputs.macromolecule.redraw();
539
545
  };
540
546
 
547
+ // Shows/hides conversion rules UI based on toAtomicLevel setting and data role
541
548
  const updateViewRules = () => {
542
549
  if (inputs.toAtomicLevel.value && dataRole === PolyToolDataRole.template) {
543
550
  inputs.generateHelm.root.style.removeProperty('display');
@@ -553,7 +560,7 @@ async function getPolyToolEnumerateDialog(
553
560
  if (resizeInputs)
554
561
  resizeInputs();
555
562
  };
556
-
563
+ let dialogOKButton: HTMLButtonElement | null | undefined = null;
557
564
  const updateWarnings = () => {
558
565
  const warnings = placeholdersValidity;
559
566
  // const iw = inputs.warnings;
@@ -565,6 +572,8 @@ async function getPolyToolEnumerateDialog(
565
572
 
566
573
  w.innerText = warnings;
567
574
  w.style.removeProperty('display');
575
+ if (dialogOKButton)
576
+ dialogOKButton.disabled = true;
568
577
  } else {
569
578
  // iw.value = ''; // <- breaks dialog resize
570
579
  // iw.enabled = false;
@@ -572,6 +581,8 @@ async function getPolyToolEnumerateDialog(
572
581
 
573
582
  w.innerText = '';
574
583
  w.style.setProperty('display', 'none');
584
+ if (dialogOKButton)
585
+ dialogOKButton.disabled = false;
575
586
  }
576
587
  //resizeInputs();
577
588
  };
@@ -597,22 +608,11 @@ async function getPolyToolEnumerateDialog(
597
608
  fillForCurrentCell(seqValue, dataRole, cell);
598
609
  updateViewRules();
599
610
 
611
+ // === EXECUTION (OK button handler) ===
612
+ // Pre-flight validates inputs, builds params, and runs enumeration.
600
613
  const exec = async (): Promise<void> => {
601
614
  try {
602
615
  const srcHelm = inputs.macromolecule.stringValue;
603
- const helmSelections = wu.enumerate<HelmAtom>(inputs.macromolecule.molValue.atoms)
604
- .filter(([a, aI]) => a.highlighted)
605
- .map(([a, aI]) => a).toArray();
606
- if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Library) {
607
- if (helmSelections.length === 0) {
608
- grok.shell.warning('PolyTool: position for enumeration was not selected');
609
- return;
610
- }
611
- if (!inputs.library.value) {
612
- grok.shell.warning('PolyTool: No monomer library was selected');
613
- return;
614
- }
615
- }
616
616
  if (srcHelm === undefined || srcHelm === '') {
617
617
  grok.shell.warning('PolyTool: no molecule was provided');
618
618
  } else {
@@ -625,26 +625,17 @@ async function getPolyToolEnumerateDialog(
625
625
  await getHelmHelper(); // initializes JSDraw and org
626
626
 
627
627
  const placeHoldersValue = inputs.placeholders.placeholdersValue;
628
- let enumerationType = inputs.enumeratorType.value;
629
- if (enumerationType === PolyToolEnumeratorTypes.Library) {
630
- if (placeHoldersValue.length !== 1) {
631
- grok.shell.warning('Only one placeholder is allowed for Library mode');
632
- return;
633
- }
634
- if (!placeHoldersValue[0].monomers || !_libList.includes(placeHoldersValue[0].monomers[0])) {
635
- grok.shell.warning('Valid Monomer Library is required for this enumerator type');
636
- return;
628
+ const enumerationType = inputs.enumeratorType.value;
629
+
630
+ if (enumerationType === PolyToolEnumeratorTypes.Parallel) {
631
+ const nonEmpty = placeHoldersValue.filter((ph) => ph.monomers.length > 0);
632
+ if (nonEmpty.length > 1) {
633
+ const firstCount = nonEmpty[0].monomers.length;
634
+ if (nonEmpty.some((ph) => ph.monomers.length !== firstCount)) {
635
+ grok.shell.warning('Parallel mode requires all positions to have the same number of monomers');
636
+ return;
637
+ }
637
638
  }
638
- const monLibName = placeHoldersValue[0].monomers[0];
639
- const monLib = await libHelper.readSingleLibraryByName(monLibName);
640
- if (!monLib) {
641
- grok.shell.warning(`Monomer Library '${monLibName}' was not found`);
642
- return;
643
- }
644
- const polymerType = helmTypeToPolymerType(helmSelections[0].biotype() ?? 'HELM_AA');
645
- const peptideMonomers = monLib.getMonomerSymbolsByType(polymerType);
646
- placeHoldersValue[0].monomers = peptideMonomers;
647
- enumerationType = PolyToolEnumeratorTypes.Single;
648
639
  }
649
640
 
650
641
  const params: PolyToolEnumeratorParams = {
@@ -653,6 +644,16 @@ async function getPolyToolEnumerateDialog(
653
644
  breadthPlaceholders: inputs.placeholdersBreadth.placeholdersBreadthValue,
654
645
  keepOriginal: inputs.keepOriginal.value,
655
646
  };
647
+
648
+ if (cell?.column?.temp?.[SeqTemps.notationProvider] &&
649
+ !(cell.column.temp[SeqTemps.notationProvider] instanceof CyclizedNotationProvider)) {
650
+ const notationProvider = cell.column.temp[SeqTemps.notationProvider] as INotationProvider;
651
+ if ((notationProvider.constructor as typeof NotationProviderBase).implementsFromHelm) {
652
+ const cons = notationProvider.constructor as typeof NotationProviderBase;
653
+ params.fromHelmNotation = {notationName: cons.notationName, convert: (helm) => cons.convertFromHelm(helm, {})};
654
+ }
655
+ }
656
+
656
657
  const toAtomicLevelV: boolean = inputs.toAtomicLevel.value;
657
658
  const enumeratorResDf = await polyToolEnumerateSeq(srcHelm, dataRole, srcId, params,
658
659
  toAtomicLevelV ? {
@@ -669,12 +670,14 @@ async function getPolyToolEnumerateDialog(
669
670
  }
670
671
  };
671
672
 
673
+ // === DIALOG CONSTRUCTION AND LAYOUT ===
674
+ // Layout: macromolecule editor on top, two-column (placeholders | breadth), two-column (options | rules)
672
675
  const dialog = ui.dialog({title: PT_UI_DIALOG_ENUMERATION, showFooter: true})
673
676
  .add(inputs.macromolecule.root)
674
677
  .add(ui.divH([
675
678
  ui.divV([
676
679
  inputs.placeholders.root,
677
- inputs.enumeratorType.root, inputs.library.root],
680
+ inputs.enumeratorType.root],
678
681
  {style: {width: '50%'}}
679
682
  ),
680
683
  ui.divV([
@@ -693,14 +696,20 @@ async function getPolyToolEnumerateDialog(
693
696
  {style: {width: '50%'}})],
694
697
  {style: {width: '100%'}}))
695
698
  .add(warningsTextDiv)
696
- // .addButton('Enumerate', () => {
697
- // execDialog()
698
- // .then(() => {});
699
- // }, 0, 'Keeps the dialog open')
700
- .onOK(() => { exec(); });
699
+ .onOK(() => {
700
+ if (placeholdersValidity) {
701
+ updateWarnings();
702
+ grok.shell.warning('Please fix validation errors before running enumeration');
703
+ return;
704
+ }
705
+ exec();
706
+ });
707
+ // .onOK(() => { exec(); });
708
+ dialogOKButton = dialog.getButton('OK') as HTMLButtonElement;
701
709
  subs.push(dialog.onClose.subscribe(() => {
702
710
  destroy();
703
711
  }));
712
+ // Dialog history: serialization/deserialization for persisting dialog state across sessions
704
713
  dialog.history(
705
714
  /* getInput */ (): PolyToolEnumerateHelmSerialized => {
706
715
  return {
@@ -716,7 +725,6 @@ async function getPolyToolEnumerateDialog(
716
725
  chiralityEngine: inputs.chiralityEngine.value,
717
726
  highlightMonomers: inputs.highlightMonomers.value,
718
727
  rules: ruleFileList,
719
- library: inputs.library.value,
720
728
  };
721
729
  },
722
730
  /* applyInput */ (x: PolyToolEnumerateHelmSerialized): void => {
@@ -731,7 +739,10 @@ async function getPolyToolEnumerateDialog(
731
739
  inputs.chiralityEngine.value = x.chiralityEngine ?? false;
732
740
  inputs.highlightMonomers.value = x.highlightMonomers ?? false;
733
741
  ruleInputs.setActive(x.rules);
734
- inputs.library.value = x.library;
742
+ setTimeout(() => {
743
+ inputs.placeholders.invalidateGrid();
744
+ inputs.placeholdersBreadth.invalidateGrid();
745
+ }, 100);
735
746
  });
736
747
  return dialog;
737
748
  } catch (err: any) {
@@ -740,10 +751,16 @@ async function getPolyToolEnumerateDialog(
740
751
  }
741
752
  }
742
753
 
743
- /**
744
- * @param {DG.SemanticValue} srcValue Source value to enumerate, either of data role
745
- * {@link PolyToolDataRole.template} or {@link PolyToolDataRole.macromolecule}
746
- * */
754
+ /** Executes enumeration and post-processes results into a DataFrame.
755
+ * Handles both macromolecule and template data roles, with optional
756
+ * atomic-level conversion (chirality, highlighting, rules).
757
+ *
758
+ * @param srcHelm Source HELM string to enumerate
759
+ * @param dataRole Whether the source is a {@link PolyToolDataRole.template} or {@link PolyToolDataRole.macromolecule}
760
+ * @param srcId Optional trivial name column info for generating IDs
761
+ * @param params Enumeration parameters (type, placeholders, keepOriginal)
762
+ * @param toAtomicLevel Post-processing options, or false to skip
763
+ */
747
764
  export async function polyToolEnumerateSeq(
748
765
  srcHelm: string, dataRole: PolyToolDataRole, srcId: { value: string, colName: string } | null,
749
766
  params: PolyToolEnumeratorParams,
@@ -756,7 +773,10 @@ export async function polyToolEnumerateSeq(
756
773
  const rdKitModule = await getRdKitModule();
757
774
  const monomerLib = libHelper.getMonomerLib(); // TODO: Get monomer lib from src SeqValueBase
758
775
 
776
+ // Core enumeration: produces [helm, id] pairs
759
777
  const resList = doPolyToolEnumerateHelm(srcHelm, srcId?.value ?? '', params);
778
+
779
+ // Create result column based on data role (macromolecule uses HELM directly, template converts via Chain)
760
780
  let enumCol: DG.Column<string>;
761
781
  switch (dataRole) {
762
782
  case PolyToolDataRole.macromolecule: {
@@ -779,11 +799,19 @@ export async function polyToolEnumerateSeq(
779
799
  }
780
800
  }
781
801
  const enumeratorResDf = DG.DataFrame.fromColumns([enumCol]);
802
+
803
+ if (dataRole === PolyToolDataRole.macromolecule && params.fromHelmNotation) {
804
+ const c = DG.Column.fromType(DG.COLUMN_TYPE.STRING, `${params.fromHelmNotation.notationName}(${enumCol.name})`, enumeratorResDf.rowCount)
805
+ .init((rowIdx: number) => enumCol.isNone(rowIdx) ? null : params.fromHelmNotation!.convert(enumCol.get(rowIdx)!));
806
+ enumeratorResDf.columns.add(c);
807
+ }
808
+
782
809
  await grok.data.detectSemanticTypes(enumeratorResDf);
783
810
  if (dataRole == PolyToolDataRole.template)
784
811
  PackageFunctions.applyNotationProviderForCyclized(enumCol, '-');
785
812
 
786
813
 
814
+ // Optional post-processing: convert to atomic level with chirality/highlighting
787
815
  if (toAtomicLevel) {
788
816
  let resHelmCol: DG.Column<string>;
789
817
  if (dataRole === PolyToolDataRole.macromolecule) {
@@ -800,6 +828,7 @@ export async function polyToolEnumerateSeq(
800
828
  }
801
829
  }
802
830
 
831
+ // Add trivial name ID column if the source had one
803
832
  if (srcId) {
804
833
  const enumIdCol = DG.Column.fromType(DG.COLUMN_TYPE.STRING, srcId.colName, resList.length)
805
834
  .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;