@datagrok/sequence-translator 1.10.7 → 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/dist/package-test.js +1 -1
- package/dist/package-test.js.map +1 -1
- package/dist/package.js +1 -1
- package/dist/package.js.map +1 -1
- package/package.json +1 -1
- package/src/package-api.ts +4 -0
- package/src/polytool/const.ts +7 -0
- package/src/polytool/pt-enumerate-seq-dialog.ts +143 -27
- package/src/polytool/pt-enumeration-helm.ts +37 -0
- package/src/polytool/pt-monomer-selection-dialog.ts +37 -0
- package/src/polytool/pt-placeholders-breadth-input.ts +13 -0
- package/src/polytool/pt-placeholders-input.ts +13 -0
- package/src/polytool/types.ts +2 -0
- package/src/tests/polytool-enumerate-tests.ts +60 -0
- package/test-console-output-1.log +94 -82
- package/test-record-1.mp4 +0 -0
package/package.json
CHANGED
package/src/package-api.ts
CHANGED
|
@@ -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/polytool/const.ts
CHANGED
|
@@ -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
|
+
};
|
|
@@ -28,7 +28,7 @@ import {PolyToolPlaceholdersInput} from './pt-placeholders-input';
|
|
|
28
28
|
import {showMonomerSelectionDialog} from './pt-monomer-selection-dialog';
|
|
29
29
|
import {defaultErrorHandler} from '../utils/err-info';
|
|
30
30
|
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';
|
|
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';
|
|
32
32
|
import {PolyToolDataRole, PolyToolTags} from '../consts';
|
|
33
33
|
import {RuleInputs, RULES_PATH, RULES_STORAGE_NAME} from './conversion/pt-rules';
|
|
34
34
|
import {Chain} from './conversion/pt-chain';
|
|
@@ -39,8 +39,22 @@ import {buildMonomerHoverLink} from '@datagrok-libraries/bio/src/monomer-works/m
|
|
|
39
39
|
import {getRdKitModule} from '@datagrok-libraries/bio/src/chem/rdkit-module';
|
|
40
40
|
|
|
41
41
|
import {PolymerTypes} from '@datagrok-libraries/js-draw-lite/src/types/org';
|
|
42
|
-
import {
|
|
43
|
-
import {
|
|
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
|
+
*/
|
|
44
58
|
|
|
45
59
|
type PolyToolEnumerateInputs = {
|
|
46
60
|
macromolecule: HelmInputBase;
|
|
@@ -73,15 +87,21 @@ type PolyToolEnumerateHelmSerialized = {
|
|
|
73
87
|
library: string;
|
|
74
88
|
};
|
|
75
89
|
|
|
90
|
+
/** Entry point: creates, sizes, and shows the enumeration dialog. */
|
|
76
91
|
export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
|
|
77
92
|
await _package.initPromise;
|
|
78
93
|
|
|
94
|
+
// Capture viewport dimensions for dialog sizing
|
|
79
95
|
const maxWidth = window.innerWidth;
|
|
80
96
|
const maxHeight = window.innerHeight;
|
|
81
97
|
|
|
82
98
|
try {
|
|
83
99
|
// eslint-disable-next-line prefer-const
|
|
84
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.
|
|
85
105
|
function resizeInputs() {
|
|
86
106
|
if (dialog == null) return;
|
|
87
107
|
|
|
@@ -107,18 +127,13 @@ export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
|
|
|
107
127
|
};
|
|
108
128
|
dialog = await getPolyToolEnumerateDialog(cell, resizeInputs);
|
|
109
129
|
|
|
130
|
+
// On first show, center the dialog at 70% of viewport; on subsequent resizes, just reflow inputs
|
|
110
131
|
let isFirstShow = true;
|
|
111
132
|
ui.onSizeChanged(dialog.root).subscribe(() => {
|
|
112
133
|
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
134
|
const dialogWidth = maxWidth * 0.7;
|
|
119
135
|
const dialogHeight = maxHeight * 0.7;
|
|
120
136
|
|
|
121
|
-
// Centered, but resizable dialog
|
|
122
137
|
dialog.root.style.width = `${Math.min(maxWidth, dialogWidth)}px`;
|
|
123
138
|
dialog.root.style.height = `${Math.min(maxHeight, dialogHeight)}px`;
|
|
124
139
|
dialog.root.style.left = `${Math.floor((maxWidth - dialog.root.offsetWidth) / 2)}px`;
|
|
@@ -141,19 +156,21 @@ export async function polyToolEnumerateHelmUI(cell?: DG.Cell): Promise<void> {
|
|
|
141
156
|
}
|
|
142
157
|
}
|
|
143
158
|
|
|
159
|
+
/** Builds and configures the enumeration dialog with all inputs, validators, and event handlers. */
|
|
144
160
|
async function getPolyToolEnumerateDialog(
|
|
145
161
|
cell?: DG.Cell, resizeInputs?: () => void
|
|
146
162
|
): Promise<DG.Dialog> {
|
|
147
163
|
const logPrefix = `ST: PT: HelmDialog()`;
|
|
148
164
|
let inputs: PolyToolEnumerateInputs;
|
|
149
165
|
const subs: Unsubscribable[] = [];
|
|
150
|
-
//
|
|
166
|
+
// Track previous enumeration type to clean up state when switching to/from Library mode
|
|
151
167
|
let prevEnumerationType: PolyToolEnumeratorType = PolyToolEnumeratorTypes.Single;
|
|
152
168
|
const destroy = () => {
|
|
153
169
|
for (const sub of subs) sub.unsubscribe();
|
|
154
170
|
inputs.placeholders.detach();
|
|
155
171
|
};
|
|
156
172
|
try {
|
|
173
|
+
// --- Initialize helpers (monomer lib, sequence helper, HELM helper) ---
|
|
157
174
|
const libHelper = await getMonomerLibHelper();
|
|
158
175
|
const monomerLib = libHelper.getMonomerLib();
|
|
159
176
|
const seqHelper = await getSeqHelper();
|
|
@@ -162,6 +179,8 @@ async function getPolyToolEnumerateDialog(
|
|
|
162
179
|
const [_libList, helmHelper] = await Promise.all([getLibrariesList(), getHelmHelper()]);
|
|
163
180
|
const monomerLibFuncs = helmHelper.buildMonomersFuncsFromLib(monomerLib);
|
|
164
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).
|
|
165
184
|
const getValue = (cell?: DG.Cell): [SeqValueBase, PolyToolDataRole] => {
|
|
166
185
|
let resSeqValue: SeqValueBase;
|
|
167
186
|
let resDataRole: PolyToolDataRole;
|
|
@@ -197,12 +216,14 @@ async function getPolyToolEnumerateDialog(
|
|
|
197
216
|
|
|
198
217
|
let [seqValue, dataRole]: [SeqValueBase, PolyToolDataRole] = getValue(cell);
|
|
199
218
|
|
|
219
|
+
// --- UI state ---
|
|
200
220
|
let srcId: { value: string, colName: string } | null = null;
|
|
201
221
|
let ruleFileList: string[];
|
|
202
222
|
let ruleInputs: RuleInputs;
|
|
203
223
|
const trivialNameSampleDiv = ui.divText('', {style: {marginLeft: '8px', marginTop: '2px'}});
|
|
204
224
|
const warningsTextDiv = ui.divText('', {style: {color: 'red'}});
|
|
205
|
-
|
|
225
|
+
|
|
226
|
+
// === INPUT DEFINITIONS ===
|
|
206
227
|
inputs = {
|
|
207
228
|
macromolecule: helmHelper.createHelmInput(
|
|
208
229
|
'Macromolecule', {
|
|
@@ -292,6 +313,14 @@ async function getPolyToolEnumerateDialog(
|
|
|
292
313
|
library: ui.input.choice('Monomer Library', {items: _libList, value: _libList[0], nullable: true}) as DG.InputBase<string>,
|
|
293
314
|
};
|
|
294
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
|
+
|
|
295
324
|
inputs.library.root.style.setProperty('display', 'none');
|
|
296
325
|
inputs.trivialNameCol.addOptions(trivialNameSampleDiv);
|
|
297
326
|
|
|
@@ -319,23 +348,41 @@ async function getPolyToolEnumerateDialog(
|
|
|
319
348
|
return showMonomerSelectionDialog(monomerLib, polymerType, currentMonomers);
|
|
320
349
|
};
|
|
321
350
|
|
|
351
|
+
// === VALIDATORS ===
|
|
352
|
+
// Validates placeholder positions and monomers based on the current enumeration mode.
|
|
322
353
|
let placeholdersValidity: string | null = null;
|
|
323
354
|
inputs.placeholders.addValidator((value: string): string | null => {
|
|
355
|
+
placeholdersValidity = null;
|
|
324
356
|
const errors: string[] = [];
|
|
357
|
+
setTimeout(() => { updateWarnings(); }, 100);
|
|
325
358
|
try {
|
|
326
359
|
// for library, ony one placeholder is allowed
|
|
327
360
|
if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.Library) {
|
|
328
|
-
setTimeout(() => { updateWarnings(); }, 10);
|
|
329
361
|
const pcs = inputs.placeholders.placeholdersValue;
|
|
330
362
|
if (pcs.length > 1)
|
|
331
|
-
|
|
363
|
+
placeholdersValidity = 'Only one placeholder is allowed for Library mode';
|
|
332
364
|
if (pcs.length === 1) {
|
|
333
365
|
if (pcs[0].position == null)
|
|
334
|
-
|
|
366
|
+
placeholdersValidity = 'Position is required for Library mode';
|
|
335
367
|
if (pcs[0].position > inputs.macromolecule.molValue.atoms.length)
|
|
336
|
-
|
|
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
|
+
}
|
|
337
385
|
}
|
|
338
|
-
return null;
|
|
339
386
|
}
|
|
340
387
|
|
|
341
388
|
if (dataRole !== PolyToolDataRole.macromolecule)
|
|
@@ -373,7 +420,7 @@ async function getPolyToolEnumerateDialog(
|
|
|
373
420
|
.join('\n');
|
|
374
421
|
if (Object.keys(byTypeStr).length > 0)
|
|
375
422
|
errors.push(`Placeholders contain missed monomers: ${byTypeStr}`);
|
|
376
|
-
placeholdersValidity = errors.length > 0 ? errors.join('\n') :
|
|
423
|
+
placeholdersValidity = errors.length > 0 ? errors.join('\n') : placeholdersValidity;
|
|
377
424
|
} catch (err: any) {
|
|
378
425
|
const [errMsg, errStack] = defaultErrorHandler(err, false);
|
|
379
426
|
placeholdersValidity = errMsg;
|
|
@@ -460,8 +507,14 @@ async function getPolyToolEnumerateDialog(
|
|
|
460
507
|
defaultErrorHandler(err, false);
|
|
461
508
|
}
|
|
462
509
|
prevEnumerationType = inputs.enumeratorType.value;
|
|
510
|
+
updateEnumTypeTooltip();
|
|
511
|
+
// Re-validate placeholders (Parallel mode has different constraints than Single/Matrix)
|
|
512
|
+
inputs.placeholders.fireChanged();
|
|
463
513
|
});
|
|
464
514
|
|
|
515
|
+
// === MOLECULE INTERACTION EVENT HANDLERS ===
|
|
516
|
+
|
|
517
|
+
// Mouse move: show tooltip of substitute monomers at hovered position
|
|
465
518
|
subs.push(inputs.macromolecule.onMouseMove.subscribe((e: MouseEvent) => {
|
|
466
519
|
try {
|
|
467
520
|
_package.logger.debug(`${logPrefix}, placeholdersInput.onMouseMove()`);
|
|
@@ -486,6 +539,7 @@ async function getPolyToolEnumerateDialog(
|
|
|
486
539
|
defaultErrorHandler(err, false);
|
|
487
540
|
}
|
|
488
541
|
}));
|
|
542
|
+
// Click on molecule: add clicked atom position to the placeholders grid
|
|
489
543
|
subs.push(inputs.macromolecule.onClick.subscribe((e: MouseEvent) => {
|
|
490
544
|
try {
|
|
491
545
|
_package.logger.debug(`${logPrefix}, placeholdersInput.onClick()`);
|
|
@@ -528,6 +582,9 @@ async function getPolyToolEnumerateDialog(
|
|
|
528
582
|
inputs.macromolecule.root.style.setProperty('min-width', '250px', 'important');
|
|
529
583
|
// inputs.macromolecule.root.style.setProperty('max-height', '300px', 'important');
|
|
530
584
|
|
|
585
|
+
// === VIEW UPDATE HELPERS ===
|
|
586
|
+
|
|
587
|
+
// Highlights atoms in the molecule editor that have placeholders defined
|
|
531
588
|
const updateViewMol = () => {
|
|
532
589
|
const phPosSet = new Set<number>(inputs.placeholders.placeholdersValue.map((ph) => ph.position));
|
|
533
590
|
const mol = inputs.macromolecule.molValue;
|
|
@@ -538,6 +595,7 @@ async function getPolyToolEnumerateDialog(
|
|
|
538
595
|
inputs.macromolecule.redraw();
|
|
539
596
|
};
|
|
540
597
|
|
|
598
|
+
// Shows/hides conversion rules UI based on toAtomicLevel setting and data role
|
|
541
599
|
const updateViewRules = () => {
|
|
542
600
|
if (inputs.toAtomicLevel.value && dataRole === PolyToolDataRole.template) {
|
|
543
601
|
inputs.generateHelm.root.style.removeProperty('display');
|
|
@@ -553,7 +611,7 @@ async function getPolyToolEnumerateDialog(
|
|
|
553
611
|
if (resizeInputs)
|
|
554
612
|
resizeInputs();
|
|
555
613
|
};
|
|
556
|
-
|
|
614
|
+
let dialogOKButton: HTMLButtonElement | null | undefined = null;
|
|
557
615
|
const updateWarnings = () => {
|
|
558
616
|
const warnings = placeholdersValidity;
|
|
559
617
|
// const iw = inputs.warnings;
|
|
@@ -565,6 +623,8 @@ async function getPolyToolEnumerateDialog(
|
|
|
565
623
|
|
|
566
624
|
w.innerText = warnings;
|
|
567
625
|
w.style.removeProperty('display');
|
|
626
|
+
if (dialogOKButton)
|
|
627
|
+
dialogOKButton.disabled = true;
|
|
568
628
|
} else {
|
|
569
629
|
// iw.value = ''; // <- breaks dialog resize
|
|
570
630
|
// iw.enabled = false;
|
|
@@ -572,6 +632,8 @@ async function getPolyToolEnumerateDialog(
|
|
|
572
632
|
|
|
573
633
|
w.innerText = '';
|
|
574
634
|
w.style.setProperty('display', 'none');
|
|
635
|
+
if (dialogOKButton)
|
|
636
|
+
dialogOKButton.disabled = false;
|
|
575
637
|
}
|
|
576
638
|
//resizeInputs();
|
|
577
639
|
};
|
|
@@ -597,6 +659,8 @@ async function getPolyToolEnumerateDialog(
|
|
|
597
659
|
fillForCurrentCell(seqValue, dataRole, cell);
|
|
598
660
|
updateViewRules();
|
|
599
661
|
|
|
662
|
+
// === EXECUTION (OK button handler) ===
|
|
663
|
+
// Pre-flight validates inputs, resolves Library mode, builds params, and runs enumeration.
|
|
600
664
|
const exec = async (): Promise<void> => {
|
|
601
665
|
try {
|
|
602
666
|
const srcHelm = inputs.macromolecule.stringValue;
|
|
@@ -625,6 +689,7 @@ async function getPolyToolEnumerateDialog(
|
|
|
625
689
|
await getHelmHelper(); // initializes JSDraw and org
|
|
626
690
|
|
|
627
691
|
const placeHoldersValue = inputs.placeholders.placeholdersValue;
|
|
692
|
+
// Library mode: load all monomers from the selected library and convert to Single mode
|
|
628
693
|
let enumerationType = inputs.enumeratorType.value;
|
|
629
694
|
if (enumerationType === PolyToolEnumeratorTypes.Library) {
|
|
630
695
|
if (placeHoldersValue.length !== 1) {
|
|
@@ -647,12 +712,33 @@ async function getPolyToolEnumerateDialog(
|
|
|
647
712
|
enumerationType = PolyToolEnumeratorTypes.Single;
|
|
648
713
|
}
|
|
649
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
|
+
|
|
650
726
|
const params: PolyToolEnumeratorParams = {
|
|
651
727
|
placeholders: placeHoldersValue,
|
|
652
728
|
type: enumerationType,
|
|
653
729
|
breadthPlaceholders: inputs.placeholdersBreadth.placeholdersBreadthValue,
|
|
654
730
|
keepOriginal: inputs.keepOriginal.value,
|
|
655
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
|
+
|
|
656
742
|
const toAtomicLevelV: boolean = inputs.toAtomicLevel.value;
|
|
657
743
|
const enumeratorResDf = await polyToolEnumerateSeq(srcHelm, dataRole, srcId, params,
|
|
658
744
|
toAtomicLevelV ? {
|
|
@@ -669,6 +755,8 @@ async function getPolyToolEnumerateDialog(
|
|
|
669
755
|
}
|
|
670
756
|
};
|
|
671
757
|
|
|
758
|
+
// === DIALOG CONSTRUCTION AND LAYOUT ===
|
|
759
|
+
// Layout: macromolecule editor on top, two-column (placeholders | breadth), two-column (options | rules)
|
|
672
760
|
const dialog = ui.dialog({title: PT_UI_DIALOG_ENUMERATION, showFooter: true})
|
|
673
761
|
.add(inputs.macromolecule.root)
|
|
674
762
|
.add(ui.divH([
|
|
@@ -693,14 +781,20 @@ async function getPolyToolEnumerateDialog(
|
|
|
693
781
|
{style: {width: '50%'}})],
|
|
694
782
|
{style: {width: '100%'}}))
|
|
695
783
|
.add(warningsTextDiv)
|
|
696
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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;
|
|
701
794
|
subs.push(dialog.onClose.subscribe(() => {
|
|
702
795
|
destroy();
|
|
703
796
|
}));
|
|
797
|
+
// Dialog history: serialization/deserialization for persisting dialog state across sessions
|
|
704
798
|
dialog.history(
|
|
705
799
|
/* getInput */ (): PolyToolEnumerateHelmSerialized => {
|
|
706
800
|
return {
|
|
@@ -732,6 +826,10 @@ async function getPolyToolEnumerateDialog(
|
|
|
732
826
|
inputs.highlightMonomers.value = x.highlightMonomers ?? false;
|
|
733
827
|
ruleInputs.setActive(x.rules);
|
|
734
828
|
inputs.library.value = x.library;
|
|
829
|
+
setTimeout(() => {
|
|
830
|
+
inputs.placeholders.invalidateGrid();
|
|
831
|
+
inputs.placeholdersBreadth.invalidateGrid();
|
|
832
|
+
}, 100);
|
|
735
833
|
});
|
|
736
834
|
return dialog;
|
|
737
835
|
} catch (err: any) {
|
|
@@ -740,10 +838,16 @@ async function getPolyToolEnumerateDialog(
|
|
|
740
838
|
}
|
|
741
839
|
}
|
|
742
840
|
|
|
743
|
-
/**
|
|
744
|
-
*
|
|
745
|
-
*
|
|
746
|
-
*
|
|
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
|
+
*/
|
|
747
851
|
export async function polyToolEnumerateSeq(
|
|
748
852
|
srcHelm: string, dataRole: PolyToolDataRole, srcId: { value: string, colName: string } | null,
|
|
749
853
|
params: PolyToolEnumeratorParams,
|
|
@@ -756,7 +860,10 @@ export async function polyToolEnumerateSeq(
|
|
|
756
860
|
const rdKitModule = await getRdKitModule();
|
|
757
861
|
const monomerLib = libHelper.getMonomerLib(); // TODO: Get monomer lib from src SeqValueBase
|
|
758
862
|
|
|
863
|
+
// Core enumeration: produces [helm, id] pairs
|
|
759
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)
|
|
760
867
|
let enumCol: DG.Column<string>;
|
|
761
868
|
switch (dataRole) {
|
|
762
869
|
case PolyToolDataRole.macromolecule: {
|
|
@@ -779,11 +886,19 @@ export async function polyToolEnumerateSeq(
|
|
|
779
886
|
}
|
|
780
887
|
}
|
|
781
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
|
+
|
|
782
896
|
await grok.data.detectSemanticTypes(enumeratorResDf);
|
|
783
897
|
if (dataRole == PolyToolDataRole.template)
|
|
784
898
|
PackageFunctions.applyNotationProviderForCyclized(enumCol, '-');
|
|
785
899
|
|
|
786
900
|
|
|
901
|
+
// Optional post-processing: convert to atomic level with chirality/highlighting
|
|
787
902
|
if (toAtomicLevel) {
|
|
788
903
|
let resHelmCol: DG.Column<string>;
|
|
789
904
|
if (dataRole === PolyToolDataRole.macromolecule) {
|
|
@@ -800,6 +915,7 @@ export async function polyToolEnumerateSeq(
|
|
|
800
915
|
}
|
|
801
916
|
}
|
|
802
917
|
|
|
918
|
+
// Add trivial name ID column if the source had one
|
|
803
919
|
if (srcId) {
|
|
804
920
|
const enumIdCol = DG.Column.fromType(DG.COLUMN_TYPE.STRING, srcId.colName, resList.length)
|
|
805
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;
|
|
@@ -6,6 +6,8 @@ import {HelmType, PolymerType} from '@datagrok-libraries/bio/src/helm/types';
|
|
|
6
6
|
import {IMonomerLib, Monomer} from '@datagrok-libraries/bio/src/types/monomer-library';
|
|
7
7
|
import {polymerTypeToHelmType} from '@datagrok-libraries/bio/src/utils/macromolecule/utils';
|
|
8
8
|
|
|
9
|
+
import {parseMonomerSymbolList} from './pt-placeholders-input';
|
|
10
|
+
|
|
9
11
|
const MAX_SUGGESTIONS = 20;
|
|
10
12
|
|
|
11
13
|
/** Shows a dialog for selecting monomers with autocomplete and tag-based display.
|
|
@@ -156,6 +158,41 @@ export async function showMonomerSelectionDialog(
|
|
|
156
158
|
|
|
157
159
|
inputEl.addEventListener('input', () => { showSuggestions(); });
|
|
158
160
|
|
|
161
|
+
// Handle pasting multiple monomers (space / comma / newline separated)
|
|
162
|
+
inputEl.addEventListener('paste', (e: ClipboardEvent) => {
|
|
163
|
+
const pastedText = e.clipboardData?.getData('text');
|
|
164
|
+
if (!pastedText)
|
|
165
|
+
return;
|
|
166
|
+
|
|
167
|
+
// Split on newlines first, then parse each line (handles parenthesized symbols like hArg(Et,Et))
|
|
168
|
+
const lines = pastedText.split(/[\n\r]+/).map((l) => l.trim()).filter((l) => l.length > 0);
|
|
169
|
+
const candidates: string[] = [];
|
|
170
|
+
for (const line of lines) {
|
|
171
|
+
const parsed = parseMonomerSymbolList(line);
|
|
172
|
+
// If parseMonomerSymbolList returned a single token but the line has spaces and no commas,
|
|
173
|
+
// split on spaces as well (e.g. "K P F" -> ["K", "P", "F"])
|
|
174
|
+
if (parsed.length === 1 && line.includes(' ') && !line.includes(','))
|
|
175
|
+
candidates.push(...line.split(/\s+/).map((s) => s.trim()).filter((s) => s.length > 0));
|
|
176
|
+
else
|
|
177
|
+
candidates.push(...parsed);
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
if (candidates.length <= 1)
|
|
181
|
+
return; // Single symbol: let default paste + autocomplete handle it
|
|
182
|
+
|
|
183
|
+
e.preventDefault();
|
|
184
|
+
|
|
185
|
+
for (const candidate of candidates) {
|
|
186
|
+
if (allSymbols.includes(candidate) && !selectedMonomers.includes(candidate))
|
|
187
|
+
selectedMonomers.push(candidate);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
renderTags();
|
|
191
|
+
inputEl.value = '';
|
|
192
|
+
hideMenu();
|
|
193
|
+
inputEl.focus();
|
|
194
|
+
});
|
|
195
|
+
|
|
159
196
|
inputEl.addEventListener('keydown', (e: KeyboardEvent) => {
|
|
160
197
|
if (e.key === 'ArrowDown') {
|
|
161
198
|
e.preventDefault();
|
|
@@ -29,6 +29,19 @@ export class PolyToolPlaceholdersBreadthInput extends DG.JsInputBase<DG.DataFram
|
|
|
29
29
|
this.setDataFrame(value);
|
|
30
30
|
}
|
|
31
31
|
|
|
32
|
+
public invalidateGrid(): void {
|
|
33
|
+
if (this.grid) {
|
|
34
|
+
const oldW = this.grid.root.style.width;
|
|
35
|
+
this.grid.root.style.width = '99.9%';
|
|
36
|
+
setTimeout(() => {
|
|
37
|
+
if (oldW)
|
|
38
|
+
this.grid.root.style.width = oldW;
|
|
39
|
+
else
|
|
40
|
+
this.grid.root.style.removeProperty('width');
|
|
41
|
+
}, 100);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
32
45
|
getStringValue(): string { return this.grid.dataFrame.toCsv(); }
|
|
33
46
|
|
|
34
47
|
setStringValue(str: string): void {
|
|
@@ -52,6 +52,19 @@ export class PolyToolPlaceholdersInput extends DG.JsInputBase<DG.DataFrame> {
|
|
|
52
52
|
|
|
53
53
|
private subs: Unsubscribable[] = [];
|
|
54
54
|
|
|
55
|
+
public invalidateGrid(): void {
|
|
56
|
+
if (this.grid) {
|
|
57
|
+
const oldW = this.grid.root.style.width;
|
|
58
|
+
this.grid.root.style.width = '99.9%';
|
|
59
|
+
setTimeout(() => {
|
|
60
|
+
if (oldW)
|
|
61
|
+
this.grid.root.style.width = oldW;
|
|
62
|
+
else
|
|
63
|
+
this.grid.root.style.removeProperty('width');
|
|
64
|
+
}, 100);
|
|
65
|
+
}
|
|
66
|
+
}
|
|
67
|
+
|
|
55
68
|
protected constructor(
|
|
56
69
|
private readonly name: string | undefined,
|
|
57
70
|
heightRowCount?: number, options?: {},
|
package/src/polytool/types.ts
CHANGED
|
@@ -6,6 +6,7 @@ import {PolymerType} from '@datagrok-libraries/bio/src/helm/types';
|
|
|
6
6
|
|
|
7
7
|
export enum PolyToolEnumeratorTypes {
|
|
8
8
|
Single = 'single',
|
|
9
|
+
Parallel = 'parallel',
|
|
9
10
|
Matrix = 'matrix',
|
|
10
11
|
Library = 'library',
|
|
11
12
|
}
|
|
@@ -27,6 +28,7 @@ export type PolyToolEnumeratorParams = {
|
|
|
27
28
|
breadthPlaceholders?: PolyToolBreadthPlaceholder[];
|
|
28
29
|
keepOriginal?: boolean;
|
|
29
30
|
trivialName?: boolean;
|
|
31
|
+
fromHelmNotation?: {convert:(helm: string) => string; notationName: string};
|
|
30
32
|
}
|
|
31
33
|
|
|
32
34
|
export class MonomerNotFoundError extends Error {
|
|
@@ -73,6 +73,66 @@ category('PolyTool: Enumerate', () => {
|
|
|
73
73
|
{seq: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.T.[C(1)].G.[NH2]}$$$$V2.0', name: '-[Tic]7T'},
|
|
74
74
|
]
|
|
75
75
|
},
|
|
76
|
+
'parallel1': {
|
|
77
|
+
src: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0',
|
|
78
|
+
params: {
|
|
79
|
+
type: PolyToolEnumeratorTypes.Parallel,
|
|
80
|
+
placeholders: [
|
|
81
|
+
{position: 1, monomers: ['D', 'L']},
|
|
82
|
+
{position: 6, monomers: ['Y', 'T']},
|
|
83
|
+
],
|
|
84
|
+
},
|
|
85
|
+
tgt: [
|
|
86
|
+
{seq: 'PEPTIDE1{[Ac(1)].D.W.G.P.L.Y.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2D-[Tic]7Y'},
|
|
87
|
+
{seq: 'PEPTIDE1{[Ac(1)].L.W.G.P.L.T.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2L-[Tic]7T'},
|
|
88
|
+
],
|
|
89
|
+
},
|
|
90
|
+
'parallel-three-positions': {
|
|
91
|
+
src: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0',
|
|
92
|
+
params: {
|
|
93
|
+
type: PolyToolEnumeratorTypes.Parallel,
|
|
94
|
+
placeholders: [
|
|
95
|
+
{position: 1, monomers: ['D', 'L', 'K']},
|
|
96
|
+
{position: 4, monomers: ['K', 'P', 'F4COO']},
|
|
97
|
+
{position: 6, monomers: ['Y', 'T', 'A']},
|
|
98
|
+
],
|
|
99
|
+
},
|
|
100
|
+
tgt: [
|
|
101
|
+
{seq: 'PEPTIDE1{[Ac(1)].D.W.G.K.L.Y.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2D-P5K-[Tic]7Y'},
|
|
102
|
+
{seq: 'PEPTIDE1{[Ac(1)].L.W.G.P.L.T.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2L-P5P-[Tic]7T'},
|
|
103
|
+
{seq: 'PEPTIDE1{[Ac(1)].K.W.G.[F4COO].L.A.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2K-P5[F4COO]-[Tic]7A'},
|
|
104
|
+
],
|
|
105
|
+
},
|
|
106
|
+
'parallel-with-original': {
|
|
107
|
+
src: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0',
|
|
108
|
+
params: {
|
|
109
|
+
type: PolyToolEnumeratorTypes.Parallel,
|
|
110
|
+
placeholders: [
|
|
111
|
+
{position: 1, monomers: ['D', 'L']},
|
|
112
|
+
{position: 6, monomers: ['Y', 'T']},
|
|
113
|
+
],
|
|
114
|
+
keepOriginal: true,
|
|
115
|
+
},
|
|
116
|
+
tgt: [
|
|
117
|
+
{seq: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0', name: ''},
|
|
118
|
+
{seq: 'PEPTIDE1{[Ac(1)].D.W.G.P.L.Y.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2D-[Tic]7Y'},
|
|
119
|
+
{seq: 'PEPTIDE1{[Ac(1)].L.W.G.P.L.T.[C(1)].G.[NH2]}$$$$V2.0', name: '-F2L-[Tic]7T'},
|
|
120
|
+
],
|
|
121
|
+
},
|
|
122
|
+
'parallel-single-position': {
|
|
123
|
+
src: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0',
|
|
124
|
+
params: {
|
|
125
|
+
type: PolyToolEnumeratorTypes.Parallel,
|
|
126
|
+
placeholders: [
|
|
127
|
+
{position: 4, monomers: ['K', 'P', 'F4COO']},
|
|
128
|
+
],
|
|
129
|
+
},
|
|
130
|
+
tgt: [
|
|
131
|
+
{seq: 'PEPTIDE1{[Ac(1)].F.W.G.K.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0', name: '-P5K'},
|
|
132
|
+
{seq: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0', name: '-P5P'},
|
|
133
|
+
{seq: 'PEPTIDE1{[Ac(1)].F.W.G.[F4COO].L.[Tic].[C(1)].G.[NH2]}$$$$V2.0', name: '-P5[F4COO]'},
|
|
134
|
+
],
|
|
135
|
+
},
|
|
76
136
|
'matrix1': {
|
|
77
137
|
src: 'PEPTIDE1{[Ac(1)].F.W.G.P.L.[Tic].[C(1)].G.[NH2]}$$$$V2.0',
|
|
78
138
|
params:
|