@datagrok/eda 1.4.11 → 1.4.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.eslintrc.json +0 -1
  2. package/CHANGELOG.md +15 -0
  3. package/CLAUDE.md +185 -0
  4. package/README.md +8 -0
  5. package/css/pmpo.css +35 -0
  6. package/dist/package-test.js +1 -1
  7. package/dist/package-test.js.map +1 -1
  8. package/dist/package.js +1 -1
  9. package/dist/package.js.map +1 -1
  10. package/eslintrc.json +45 -0
  11. package/files/drugs-props-test.csv +126 -0
  12. package/files/drugs-props-train-scores.csv +664 -0
  13. package/files/drugs-props-train.csv +664 -0
  14. package/package.json +9 -3
  15. package/src/anova/anova-tools.ts +1 -1
  16. package/src/anova/anova-ui.ts +1 -1
  17. package/src/package-api.ts +18 -0
  18. package/src/package-test.ts +4 -1
  19. package/src/package.g.ts +25 -0
  20. package/src/package.ts +55 -15
  21. package/src/pareto-optimization/pareto-computations.ts +6 -0
  22. package/src/pareto-optimization/utils.ts +6 -4
  23. package/src/probabilistic-scoring/data-generator.ts +157 -0
  24. package/src/probabilistic-scoring/nelder-mead.ts +204 -0
  25. package/src/probabilistic-scoring/pmpo-defs.ts +218 -0
  26. package/src/probabilistic-scoring/pmpo-utils.ts +603 -0
  27. package/src/probabilistic-scoring/prob-scoring.ts +991 -0
  28. package/src/probabilistic-scoring/stat-tools.ts +303 -0
  29. package/src/softmax-classifier.ts +1 -1
  30. package/src/tests/anova-tests.ts +1 -1
  31. package/src/tests/classifiers-tests.ts +1 -1
  32. package/src/tests/dim-reduction-tests.ts +1 -1
  33. package/src/tests/linear-methods-tests.ts +1 -1
  34. package/src/tests/mis-vals-imputation-tests.ts +1 -1
  35. package/src/tests/pareto-tests.ts +253 -0
  36. package/src/tests/pmpo-tests.ts +157 -0
  37. package/test-console-output-1.log +175 -209
  38. package/test-record-1.mp4 +0 -0
@@ -0,0 +1,603 @@
1
+ // Utility functions for probabilistic scoring (pMPO)
2
+ // Link: https://pmc.ncbi.nlm.nih.gov/articles/PMC4716604/
3
+
4
+ import * as grok from 'datagrok-api/grok';
5
+ import * as ui from 'datagrok-api/ui';
6
+ import * as DG from 'datagrok-api/dg';
7
+
8
+ import '../../css/pmpo.css';
9
+
10
+ import {COLORS, DESCR_TABLE_TITLE, DESCR_TITLE, DescriptorStatistics, DesirabilityProfileProperties,
11
+ DESIRABILITY_COL_NAME, FOLDER, P_VAL, PMPO_COMPUTE_FAILED, PmpoParams, SCORES_TITLE,
12
+ SELECTED_TITLE, STAT_TO_TITLE_MAP, TINY, WEIGHT_TITLE, CorrelationTriple,
13
+ BASIC_RANGE_SIGMA_COEFFS, EXTENDED_RANGE_SIGMA_COEFFS} from './pmpo-defs';
14
+ import {computeSigmoidParamsFromX0, getCutoffs, gaussDesirabilityFunc, sigmoidS,
15
+ solveNormalIntersection} from './stat-tools';
16
+ import {getColorScaleDiv} from '../pareto-optimization/utils';
17
+ import {OPT_TYPE} from '../pareto-optimization/defs';
18
+
19
+ /** Returns a DataFrame with descriptor statistics.
20
+ * @param stats Map of descriptor names to their statistics.
21
+ */
22
+ export function getDescriptorStatisticsTable(stats: Map<string, DescriptorStatistics>): DG.DataFrame {
23
+ const descrCount = stats.size;
24
+ const rawArrs = new Map<string, Float64Array>();
25
+
26
+ // Create raw data arrays
27
+ STAT_TO_TITLE_MAP.forEach((_, key) => {
28
+ rawArrs.set(key, new Float64Array(descrCount));
29
+ });
30
+
31
+ const descrNames = [...stats.keys()];
32
+ const cols = [
33
+ DG.Column.fromStrings(DESCR_TITLE, descrNames),
34
+ DG.Column.fromInt32Array(DESIRABILITY_COL_NAME, new Int32Array(descrCount)),
35
+ DG.Column.fromFloat32Array(WEIGHT_TITLE, new Float32Array(descrCount).fill(DG.FLOAT_NULL)),
36
+ ];
37
+
38
+ // Fill stat columns
39
+ descrNames.forEach((descr, idx) => {
40
+ const curStat = stats.get(descr);
41
+
42
+ if (curStat != null) {
43
+ STAT_TO_TITLE_MAP.forEach((_, key) => {
44
+ const val = curStat[key as keyof DescriptorStatistics];
45
+ const arr = rawArrs.get(key);
46
+ arr![idx] = val;
47
+ });
48
+ }
49
+ });
50
+
51
+ // Create stat columns
52
+ STAT_TO_TITLE_MAP.forEach((title, field) => {
53
+ cols.push(DG.Column.fromFloat64Array(title, rawArrs.get(field)!));
54
+ });
55
+
56
+ // Create the resulting table
57
+ const res = DG.DataFrame.fromColumns(cols);
58
+ res.name = DESCR_TABLE_TITLE;
59
+
60
+ return res;
61
+ } // getDescriptorStatisticsTable
62
+
63
+ /** Returns names of descriptors with p-value below the given threshold.
64
+ * @param descrStats DataFrame with descriptor statistics.
65
+ * @param pValThresh P-value threshold.
66
+ */
67
+ export function getFilteredByPvalue(descrStats: DG.DataFrame, pValThresh: number): string[] {
68
+ const selected: string[] = [];
69
+
70
+ const descrCol = descrStats.col(DESCR_TITLE);
71
+
72
+ if (descrCol == null)
73
+ throw new Error(`No column "${DESCR_TITLE} in the table with descriptors statistics.`);
74
+
75
+ const descr = descrCol.toList();
76
+
77
+ const pValCol = descrStats.col(P_VAL);
78
+
79
+ if (pValCol == null)
80
+ throw new Error(`No column "${P_VAL} in the table with descriptors statistics.`);
81
+
82
+ const pVals = pValCol.getRawData();
83
+
84
+ for (let i = 0; i < descrStats.rowCount; ++i) {
85
+ if (pVals[i] < pValThresh)
86
+ selected.push(descr[i]);
87
+ }
88
+
89
+ return selected;
90
+ } // getFilteredByPvalue
91
+
92
+ /** Adds a boolean column indicating whether each descriptor is selected.
93
+ * @param descrStats DataFrame with descriptor statistics.
94
+ * @param selected List of selected descriptor names.
95
+ */
96
+ export function addSelectedDescriptorsCol(descrStats: DG.DataFrame, selected: string[]): DG.DataFrame {
97
+ if (selected.length < 1)
98
+ throw new Error('Empty list of selected descriptors.');
99
+
100
+ const rowCount = descrStats.rowCount;
101
+ const selArr = new Array<boolean>(rowCount);
102
+ const descrCol = descrStats.col(DESCR_TITLE);
103
+
104
+ if (descrCol == null)
105
+ throw new Error(`No column "${DESCR_TITLE} in the table with descriptors statistics.`);
106
+
107
+ const descr = descrCol.toList();
108
+ let res = true;
109
+ const colors: Record<string, string> = {};
110
+
111
+ for (let i = 0; i < rowCount; ++i) {
112
+ res = selected.includes(descr[i]);
113
+ selArr[i] = res;
114
+ colors[descr[i]] = res ? COLORS.SELECTED : COLORS.SKIPPED;
115
+ }
116
+
117
+ descrCol.colors.setCategorical(colors);
118
+
119
+ // Added selected column
120
+ descrStats.columns.add(DG.Column.fromList(DG.COLUMN_TYPE.BOOL, SELECTED_TITLE, selArr));
121
+
122
+ return descrStats;
123
+ } // addSelectedDescriptorsCol
124
+
125
+ /** Returns tooltip element describing descriptor selection colors. */
126
+ export function getDescrTooltip(title: string, text: string, selected: string, excluded: string): HTMLElement {
127
+ const firstLine = ui.div();
128
+ firstLine.classList.add('eda-pmpo-tooltip-line');
129
+ const selectedBox = ui.div();
130
+ selectedBox.classList.add('eda-pmpo-box');
131
+ selectedBox.style.backgroundColor = COLORS.SELECTED;
132
+ const selectedLabel = ui.span([]);
133
+ selectedLabel.textContent = `- ${selected}`;
134
+ firstLine.appendChild(selectedBox);
135
+ firstLine.appendChild(selectedLabel);
136
+
137
+ const secondLine = ui.div();
138
+ secondLine.classList.add('eda-pmpo-tooltip-line');
139
+ const nonSelectedBox = ui.div();
140
+ nonSelectedBox.classList.add('eda-pmpo-box');
141
+ nonSelectedBox.style.backgroundColor = COLORS.SKIPPED;
142
+ const nonSelectedLabel = ui.span([]);
143
+ nonSelectedLabel.textContent = `- ${excluded}`;
144
+
145
+ secondLine.appendChild(nonSelectedBox);
146
+ secondLine.appendChild(nonSelectedLabel);
147
+
148
+ return ui.divV([ui.h2(title), ui.divText(text), firstLine, secondLine]);
149
+ } // getDescrTooltip
150
+
151
+ /** Returns tooltip element describing score colors. */
152
+ export function getScoreTooltip(): HTMLElement {
153
+ return ui.divV([
154
+ ui.h2(SCORES_TITLE),
155
+ ui.divText('Scores computed using the trained probabilistic multi-parameter optimization (pMPO) model.'),
156
+ getColorScaleDiv(OPT_TYPE.MAX, false),
157
+ ]);
158
+ } // getScoreTooltip
159
+
160
+ /** Returns list of descriptor correlation triples.
161
+ * @param descriptors Descriptor column list.
162
+ * @param selectedByPvalue List of descriptor names selected by p-value.
163
+ */
164
+ export function getCorrelationTriples(descriptors: DG.ColumnList, selectedByPvalue: string[]): CorrelationTriple[] {
165
+ const triples: CorrelationTriple[] = [];
166
+
167
+ for (let i = 0; i < selectedByPvalue.length; ++i) {
168
+ for (let j = i + 1; j < selectedByPvalue.length; ++j) {
169
+ triples.push([
170
+ selectedByPvalue[i],
171
+ selectedByPvalue[j],
172
+ descriptors.byName(selectedByPvalue[i]).stats.corr(descriptors.byName(selectedByPvalue[j]))**2,
173
+ ]);
174
+ }
175
+ }
176
+
177
+ return triples;
178
+ } // getCorrelationTriples
179
+
180
+ /** Returns names of descriptors filtered by correlation threshold.
181
+ * @param descriptors Descriptor column list.
182
+ * @param selectedByPvalue List of descriptor names selected by p-value.
183
+ */
184
+ export function getFilteredByCorrelations(descriptors: DG.ColumnList, selectedByPvalue: string[],
185
+ statistics: Map<string, DescriptorStatistics>, r2Tresh: number, correlationTriples: CorrelationTriple[]): string[] {
186
+ const correlations = correlationTriples.sort((a, b) => b[2] - a[2]);
187
+
188
+ const keep = new Set(selectedByPvalue);
189
+
190
+ correlations.filter((triple) => triple[2] > r2Tresh).forEach((triple) => {
191
+ const [descr1, descr2, _] = triple;
192
+ const pVal1 = statistics.get(descr1)!.pValue;
193
+ const pVal2 = statistics.get(descr2)!.pValue;
194
+ const tStat1 = statistics.get(descr1)!.tstat;
195
+ const tStat2 = statistics.get(descr2)!.tstat;
196
+
197
+ if (pVal1 > pVal2)
198
+ keep.delete(descr1);
199
+ else if (pVal1 < pVal2)
200
+ keep.delete(descr2);
201
+ else { // process the case of p-value = 0 (or too small)
202
+ if (Math.abs(tStat1) > Math.abs(tStat2))
203
+ keep.delete(descr2);
204
+ else
205
+ keep.delete(descr1);
206
+ }
207
+ });
208
+
209
+ return [...keep];
210
+ } // getFilteredByCorrelations
211
+
212
+ /** Computes pMPO model parameters for selected descriptors.
213
+ * @param desired DataFrame with desired compounds.
214
+ * @param nonDesired DataFrame with non-desired compounds.
215
+ * @param selected List of selected descriptor names.
216
+ * @param qCutoff Q-value cutoff.
217
+ */
218
+ export function getModelParams(desired: DG.DataFrame, nonDesired: DG.DataFrame,
219
+ selected: string[], qCutoff: number): Map<string, PmpoParams> {
220
+ const params = new Map<string, PmpoParams>();
221
+
222
+ let sum = 0;
223
+
224
+ // Compute params for each selected descriptor
225
+ selected.forEach((name) => {
226
+ const desLen = desired.rowCount;
227
+ const nonDesLen = nonDesired.rowCount;
228
+
229
+ const desCol = desired.col(name);
230
+ if (desCol == null)
231
+ throw new Error(PMPO_COMPUTE_FAILED + `: no column "${name}" in the desired table.`);
232
+
233
+ const nonDesCol = nonDesired.col(name);
234
+ if (nonDesCol == null)
235
+ throw new Error(PMPO_COMPUTE_FAILED + `: no column "${name}" in the non-desired table.`);
236
+
237
+ const muDes = desCol.stats.avg;
238
+
239
+ // Unbiased standard deviation
240
+ const sigmaDes = desCol.stats.stdev * Math.sqrt((desLen - 1) / desLen);
241
+
242
+ const muNonDes = nonDesCol.stats.avg;
243
+
244
+ // Unbiased standard deviation
245
+ const sigmaNonDes = nonDesCol.stats.stdev * Math.sqrt((nonDesLen - 1) / nonDesLen);
246
+
247
+ // Compute cutoffs
248
+ const cutoffs = getCutoffs(muDes, sigmaDes, muNonDes, sigmaNonDes);
249
+
250
+ // column_stats['inflection'] = np.exp(-np.square((column_stats['cutoff'] - column_stats['good_mean'])) /
251
+ // (2 * np.square(column_stats['good_std'])))
252
+ // Compute inflection point
253
+ const inflection = Math.exp(-((cutoffs.cutoff - muDes) ** 2) / (2 * (sigmaDes ** 2)));
254
+
255
+ // Compute intersections of the two normal distributions
256
+ const intersections = solveNormalIntersection(muDes, sigmaDes, muNonDes, sigmaNonDes);
257
+
258
+ const b = (Math.pow(inflection, -1.0) - 1.0);
259
+ const n = (Math.pow(qCutoff, -1.0) - 1.0);
260
+ const c = Math.pow(10.0, ((Math.log10(n / b)) / (-1.0 * (muNonDes - cutoffs.cutoff))));
261
+
262
+ // Compute parameters for the generalized sigmoid function TODO: delete
263
+
264
+ let x0: number | null = null;
265
+
266
+ if (intersections.length > 0) {
267
+ for (const r of intersections) {
268
+ const low = Math.min(muDes, muNonDes);
269
+ const high = Math.max(muDes, muNonDes);
270
+
271
+ if ((low - TINY <= r) && (r <= high + TINY)) {
272
+ x0 = r;
273
+ break;
274
+ }
275
+ }
276
+
277
+ if (x0 == null)
278
+ x0 = intersections[0];
279
+ } else
280
+ x0 = cutoffs.cutoff;
281
+
282
+ const xBound = cutoffs.cutoffNotDesired;
283
+ const sigmoidParams = computeSigmoidParamsFromX0(muDes, sigmaDes, x0, xBound, qCutoff);
284
+
285
+ const z = Math.abs(muDes - muNonDes) / (sigmaDes + sigmaNonDes);
286
+ sum += z;
287
+
288
+ // Store computed parameters
289
+ params.set(name, {
290
+ desAvg: muDes,
291
+ desStd: sigmaDes,
292
+ nonDesAvg: muNonDes,
293
+ nonDesStd: sigmaNonDes,
294
+ min: Math.min(desCol.stats.min, nonDesCol.stats.min),
295
+ max: Math.max(desCol.stats.max, nonDesCol.stats.max),
296
+ cutoff: cutoffs.cutoff,
297
+ cutoffDesired: cutoffs.cutoffDesired,
298
+ cutoffNotDesired: cutoffs.cutoffNotDesired,
299
+ pX0: sigmoidParams.pX0,
300
+ b: b,
301
+ c: c,
302
+ zScore: z,
303
+ weight: z,
304
+ intersections: intersections,
305
+ x0: x0,
306
+ xBound: xBound,
307
+ inflection: inflection,
308
+ });
309
+ });
310
+
311
+ // Normalize weights
312
+ params.forEach((param) => {
313
+ param.weight = param.zScore / sum;
314
+ });
315
+
316
+ return params;
317
+ } // getModelParams
318
+
319
+ /** Returns a DataFrame with descriptor weights.
320
+ * @param params Map of descriptor names to their pMPO parameters.
321
+ */
322
+ export function getWeightsTable(params: Map<string, PmpoParams>): DG.DataFrame {
323
+ const count = params.size;
324
+ const descriptors = new Array<string>(count);
325
+ const weights = new Float64Array(count);
326
+
327
+ let idx = 0;
328
+
329
+ params.forEach((param, name) => {
330
+ descriptors[idx] = name;
331
+ weights[idx] = param.weight;
332
+ ++idx;
333
+ });
334
+
335
+ return DG.DataFrame.fromColumns([
336
+ DG.Column.fromStrings(DESCR_TITLE, descriptors),
337
+ DG.Column.fromFloat64Array(WEIGHT_TITLE, weights),
338
+ ]);
339
+ } // getWeightsTable
340
+
341
+ /** Loads pMPO model parameters from a file.
342
+ * @param file FileInfo object pointing to the JSON model file.
343
+ */
344
+ export async function loadPmpoParams(file: DG.FileInfo): Promise<Map<string, PmpoParams>> {
345
+ const jsonText = await file.readAsString();
346
+ const parsedObj = JSON.parse(jsonText);
347
+
348
+ return new Map(Object.entries(parsedObj.properties));
349
+ } // loadPmpoParams
350
+
351
+ /** Returns JSON object representing an MPO Desirability Profile.
352
+ * @param params Map of descriptor names to their pMPO parameters.
353
+ * @param name Name of the desirability profile.
354
+ * @param description Description of the desirability profile.
355
+ */
356
+ export function getDesirabilityProfileJson(params: Map<string, PmpoParams>, useSigmoidalCorrection: boolean,
357
+ name: string, description: string, truncatedRange: boolean): any {
358
+ return {
359
+ 'type': 'MPO Desirability Profile',
360
+ 'name': name,
361
+ 'description': description,
362
+ 'properties': getDesirabilityProfileProperties(params, useSigmoidalCorrection, truncatedRange),
363
+ };
364
+ }
365
+
366
+ /** Saves pMPO model parameters to a file.
367
+ * @param params Map of descriptor names to their pMPO parameters.
368
+ * @param modelName Suggested model name (used as default file name).
369
+ */
370
+ export async function saveModel(params: Map<string, PmpoParams>, modelName: string,
371
+ useSigmoidalCorrection: boolean): Promise<void> {
372
+ let fileName = modelName;
373
+ const nameInput = ui.input.string('File', {
374
+ value: fileName,
375
+ nullable: false,
376
+ onValueChanged: (val) => {
377
+ fileName = val;
378
+ dlg.getButton('Save').disabled = (fileName.length < 1) || (folderName.length < 1);
379
+ },
380
+ });
381
+
382
+ let folderName = FOLDER;
383
+ const folderInput = ui.input.string('Folder', {
384
+ value: folderName,
385
+ nullable: false,
386
+ onValueChanged: (val) => {
387
+ folderName = val;
388
+ dlg.getButton('Save').disabled = (fileName.length < 1) || (folderName.length < 1);
389
+ },
390
+ });
391
+
392
+ const save = async () => {
393
+ const path = `${folderName}/${fileName}.json`;
394
+ try {
395
+ const jsonString = JSON.stringify(objectToSave(), null, 2);
396
+ await grok.dapi.files.writeAsText(path, jsonString);
397
+ grok.shell.info(`Saved to ${path}`);
398
+ } catch (err) {
399
+ grok.shell.error(`Failed to save: ${err instanceof Error ? err.message : 'the platform issue'}.`);
400
+ }
401
+ dlg.close();
402
+ };
403
+
404
+ const objectToSave = () => {
405
+ if (typeInput.value) {
406
+ return getDesirabilityProfileJson(
407
+ params,
408
+ useSigmoidalCorrection,
409
+ nameInput.value,
410
+ descriptionInput.value,
411
+ false,
412
+ );
413
+ }
414
+
415
+ return {
416
+ 'type': 'Probabilistic MPO Model',
417
+ 'name': nameInput.value,
418
+ 'description': descriptionInput.value,
419
+ 'properties': Object.fromEntries(params),
420
+ };
421
+ };
422
+
423
+ const modelNameInput = ui.input.string('Name', {value: modelName, nullable: true});
424
+ const descriptionInput = ui.input.textArea('Description', {value: ' ', nullable: true});
425
+ const typeInput = ui.input.bool('Desirability Profile', {
426
+ value: true,
427
+ tooltipText: 'Save the model as an MPO Desirability Profile. If disabled, the model is saved in the pMPO format.',
428
+ });
429
+
430
+ const dlg = ui.dialog({title: 'Save model'})
431
+ .add(ui.h2('Path'))
432
+ .add(folderInput)
433
+ .add(nameInput)
434
+ .add(ui.h2('Model'))
435
+ .add(modelNameInput)
436
+ .add(descriptionInput)
437
+ .add(typeInput)
438
+ .addButton('Save', async () => {
439
+ const exist = await grok.dapi.files.exists(`${folderName}/${fileName}.json`);
440
+ if (!exist)
441
+ await save();
442
+ else {
443
+ // Handle overwrite confirmation
444
+ ui.dialog({title: 'Warning'})
445
+ .add(ui.label('Overwrite existing file?'))
446
+ .onOK(async () => await save())
447
+ .show();
448
+ }
449
+ })
450
+ .show();
451
+ } // saveModel
452
+
453
+ /** Adds columns with correlation coefficients between descriptors.
454
+ * @param df DataFrame to which the columns will be added.
455
+ * @param descriptorNames List of descriptor names.
456
+ * @param triples List of descriptor correlation triples.
457
+ * @param selectedByCorr List of descriptor names selected after correlation filtering.
458
+ */
459
+ export function addCorrelationColumns(df: DG.DataFrame, descriptorNames: string[],
460
+ triples: CorrelationTriple[], selectedByCorr: string[]): DG.DataFrame {
461
+ const raw = new Map<string, Float32Array>();
462
+ descriptorNames.forEach((name) => {
463
+ raw.set(name, new Float32Array(descriptorNames.length).fill(DG.FLOAT_NULL));
464
+ });
465
+
466
+ const descrColVals = df.col(DESCR_TITLE)!.toList();
467
+
468
+ triples.forEach((triple) => {
469
+ const [descr1, descr2, r2] = triple;
470
+
471
+ raw.get(descr1)![descrColVals.indexOf(descr2)] = r2;
472
+ raw.get(descr2)![descrColVals.indexOf(descr1)] = r2;
473
+ raw.get(descr1)![descrColVals.indexOf(descr1)] = 1;
474
+ raw.get(descr2)![descrColVals.indexOf(descr2)] = 1;
475
+ });
476
+
477
+ selectedByCorr.forEach((name) => {
478
+ df.columns.add(DG.Column.fromFloat32Array(name, raw.get(name)!));
479
+ });
480
+
481
+ return df;
482
+ } // addCorrelationColumns
483
+
484
+ /** Sets color coding for the p-value column in the statistics table
485
+ * @param table DataFrame with descriptor statistics.
486
+ * @param pValTresh P-value threshold.
487
+ */
488
+ export function setPvalColumnColorCoding(table: DG.DataFrame, pValTresh: number): void {
489
+ const pValCol = table.col(P_VAL);
490
+ if (pValCol == null)
491
+ return;
492
+
493
+ const rules: Record<string, string> = {};
494
+ rules[`<${pValTresh}`] = COLORS.SELECTED;
495
+ rules[`>=${pValTresh}`] = COLORS.SKIPPED;
496
+
497
+ pValCol.meta.colors.setConditional(rules);
498
+ } // setPvalColumnColorCoding
499
+
500
+ /** Sets color coding for the correlation columns in the statistics table.
501
+ * @param table DataFrame with descriptor statistics.
502
+ * @param descriptorNames List of descriptor names.
503
+ * @param r2Tresh R-squared threshold.
504
+ */
505
+ export function setCorrColumnColorCoding(table: DG.DataFrame, descriptorNames: string[], r2Tresh: number): void {
506
+ descriptorNames.forEach((name) => {
507
+ const col = table.col(name);
508
+ if (col == null)
509
+ return;
510
+
511
+ const rules: Record<string, string> = {};
512
+ rules[`>=${r2Tresh}`] = COLORS.SKIPPED;
513
+ rules[`<${r2Tresh}`] = COLORS.SELECTED;
514
+
515
+ col.meta.colors.setConditional(rules);
516
+ });
517
+ } // setCorrColumnColorCoding
518
+
519
+ /** Returns desirability profile properties for the given pMPO parameters.
520
+ * @param params Map of descriptor names to their pMPO parameters.
521
+ * @param useSigmoidalCorrection Whether to use sigmoidal correction in desirability functions.
522
+ * @param displayProfile Whether to create a profile to be displayed in the stat grid (true - truncated range).
523
+ */
524
+ function getDesirabilityProfileProperties(params: Map<string, PmpoParams>,
525
+ useSigmoidalCorrection: boolean, truncatedRange: boolean): DesirabilityProfileProperties {
526
+ const props: DesirabilityProfileProperties = {};
527
+
528
+ params.forEach((param, name) => {
529
+ const range = significantPoints(param, truncatedRange);
530
+ props[name] = {
531
+ weight: param.weight,
532
+ line: getLine(param, useSigmoidalCorrection, truncatedRange),
533
+ min: Math.min(...range),
534
+ max: Math.max(...range),
535
+ };
536
+ });
537
+
538
+ return props;
539
+ } // getDesirabilityProfileProperties
540
+
541
+ /** Returns array of arguments for Gaussian function centered at mu with stddev sigma.
542
+ * @param mu Mean of the Gaussian function.
543
+ * @param sigma Standard deviation of the Gaussian function.
544
+ * @param truncatedRange Whether to use truncated range (for interactive app) or extended range (for full profile).
545
+ * @return Array of arguments for the Gaussian function.
546
+ */
547
+ function getArgsOfGaussFunc(mu: number, sigma: number, truncatedRange: boolean): number[] {
548
+ return truncatedRange ?
549
+ BASIC_RANGE_SIGMA_COEFFS.map((coeff) => mu + coeff * sigma) : // range for interactive app
550
+ EXTENDED_RANGE_SIGMA_COEFFS.map((coeff) => mu + coeff * sigma); // actual full range for desirability profile
551
+ } // getArgsOfGaussFunc
552
+
553
+ /** Basic pMPO function combining Gaussian and sigmoid functions.
554
+ * @param x Argument.
555
+ * @param param pMPO parameters.
556
+ * @param useSigmoidalCorrection Whether to use sigmoidal correction.
557
+ * @return Value of the basic pMPO function at x.
558
+ */
559
+ function basicFunction(x: number, param: PmpoParams, useSigmoidalCorrection: boolean): number {
560
+ return gaussDesirabilityFunc(x, param.desAvg, param.desStd) *
561
+ (useSigmoidalCorrection ? sigmoidS(x, param.cutoff, param.b, param.c) : 1);
562
+ }
563
+
564
+ /** Returns line points for the given pMPO parameters.
565
+ * @param param pMPO parameters.
566
+ * @param useSigmoidalCorrection Whether to use sigmoidal correction.
567
+ * @param truncatedRange Whether to use truncated range (for interactive app) or extended range (for full profile).
568
+ * @return Array of [x, y] points representing the desirability function line.
569
+ */
570
+ function getLine(param: PmpoParams, useSigmoidalCorrection: boolean, truncatedRange: boolean): [number, number][] {
571
+ const range = significantPoints(param, truncatedRange);
572
+
573
+ return range.map((x) => [x, basicFunction(x, param, useSigmoidalCorrection)]);
574
+ }
575
+
576
+ /** Returns significant points for the given pMPO parameters.
577
+ * @param param pMPO parameters.
578
+ * @param truncatedRange Whether to use truncated range (for interactive app) or extended range (for full profile).
579
+ * @return Array of significant points for the desirability function.
580
+ */
581
+ function significantPoints(param: PmpoParams, truncatedRange: boolean): number[] {
582
+ const points = getArgsOfGaussFunc(param.desAvg, param.desStd, truncatedRange);
583
+
584
+ /* Truncate range to show less points */
585
+ if (truncatedRange) {
586
+ const min = Math.min(param.min, param.desAvg - 3 * param.desStd);
587
+ const max = Math.max(param.max, param.desAvg + 3 * param.desStd);
588
+
589
+ return points
590
+ .filter((x) => (min <= x) && (x <= max))
591
+ .sort();
592
+ }
593
+
594
+ return points;
595
+ } // significantPoints
596
+
597
+ /** Custom error class for pMPO-related errors. */
598
+ export class PmpoError extends Error {
599
+ constructor(message: string) {
600
+ super(message);
601
+ this.name = 'PmpoError';
602
+ }
603
+ }