@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/CLAUDE.md +452 -0
- 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 +2 -2
- package/src/demo/demo-st-ui.ts +1 -2
- package/src/package-api.ts +4 -0
- package/src/polytool/const.ts +6 -0
- package/src/polytool/pt-enumerate-seq-dialog.ts +137 -108
- package/src/polytool/pt-enumeration-helm.ts +37 -0
- package/src/polytool/pt-monomer-selection-dialog.ts +162 -10
- 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 -1
- package/src/tests/polytool-enumerate-tests.ts +60 -0
- package/test-console-output-1.log +89 -78
- package/test-record-1.mp4 +0 -0
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.
|
|
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.
|
|
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",
|
package/src/demo/demo-st-ui.ts
CHANGED
|
@@ -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;
|
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,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 {
|
|
43
|
-
import {
|
|
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
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
//
|
|
327
|
-
if (inputs.enumeratorType.value === PolyToolEnumeratorTypes.
|
|
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
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
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') :
|
|
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
|
-
|
|
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
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
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
|
|
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
|
-
|
|
697
|
-
|
|
698
|
-
|
|
699
|
-
|
|
700
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
745
|
-
*
|
|
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;
|