diff --git a/packages/EDA/.claude/settings.json b/packages/EDA/.claude/settings.json new file mode 100644 index 0000000000..16dcda1468 --- /dev/null +++ b/packages/EDA/.claude/settings.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Read(//c/Users/vmaka/Datagrok/diff-grok/src/solver-tools/**)" + ] + } +} diff --git a/packages/EDA/.claude/settings.local.json b/packages/EDA/.claude/settings.local.json new file mode 100644 index 0000000000..978c17e78a --- /dev/null +++ b/packages/EDA/.claude/settings.local.json @@ -0,0 +1,7 @@ +{ + "permissions": { + "allow": [ + "Read(//c/Users/vmaka/Datagrok/public/js-api/src/dataframe/**)" + ] + } +} diff --git a/packages/EDA/CLAUDE.md b/packages/EDA/CLAUDE.md index 7b089349a0..70c3be3e11 100644 --- a/packages/EDA/CLAUDE.md +++ b/packages/EDA/CLAUDE.md @@ -89,6 +89,14 @@ The package combines TypeScript, WASM modules, and web workers for performance-c - Export to MPO desirability profiles (integration with `@datagrok-libraries/statistics`) - Sample data in `files/` directory for testing and demos +### Implementation Guides + +The package has dedicated guides for implementing new methods: + +- **`src/guides/COMPUTATION-PATTERNS.md`** — raw typed array access, manual null handling, single-pass aggregation, bool column bitwise extraction, data locality, module structure. **Read this before implementing any new computational method.** +- **`src/guides/WORKER-GUIDE.md`** — `WorkerColumn`/`WorkerDataFrame` types, transform functions (`toWorker*`/`fromWorker*`), null-handling conventions, worker lifecycle. **Read this before implementing any worker-based method.** +- **`src/guides/PARALLEL-EXECUTION.md`** — fan-out/fan-in pattern, worker count configuration. **Read this when distributing work across multiple workers.** + ### WASM Integration WASM modules are initialized asynchronously in `PackageFunctions.init()`: diff --git a/packages/EDA/src/anova/anova-tools.ts b/packages/EDA/src/anova/anova-tools.ts index 043bda13ef..00bcd7e41a 100644 --- a/packages/EDA/src/anova/anova-tools.ts +++ b/packages/EDA/src/anova/anova-tools.ts @@ -19,7 +19,7 @@ import * as DG from 'datagrok-api/dg'; //@ts-ignore: no types import * as jStat from 'jstat'; -import {getNullValue} from '../missing-values-imputation/knn-imputer'; +import {getNullValue} from '../utils'; enum ERROR_MSG { NON_EQUAL_FACTORS_VALUES_SIZE = 'non-equal sizes of factor and values arrays', diff --git a/packages/EDA/src/guides/ARRAY-OPERATIONS.md b/packages/EDA/src/guides/ARRAY-OPERATIONS.md new file mode 100644 index 0000000000..2073af1345 --- /dev/null +++ b/packages/EDA/src/guides/ARRAY-OPERATIONS.md @@ -0,0 +1,302 @@ +# Array Operations Guide + +Reference for implementing efficient array operations in the EDA package. +For raw data access and null handling, see `COMPUTATION-PATTERNS.md`. +For worker-specific patterns, see `WORKER-GUIDE.md`. + +## Pre-allocate and Reuse + +The core principle: allocate buffers once before the loop, reuse them across iterations. +Every `new Float32Array(n)` inside a loop is a hidden cost — allocation + eventual GC pause. + +```typescript +// Bad: allocation per iteration +for (let iter = 0; iter < maxIter; iter++) { + const temp = new Float32Array(n); // GC pressure grows with maxIter + // ... use temp ... +} + +// Good: single allocation, reused across iterations +const temp = new Float32Array(n); +for (let iter = 0; iter < maxIter; iter++) { + // ... use temp — same memory, zero allocations ... +} +``` + +Reference: `probabilistic-scoring/nelder-mead.ts` (`nelderMead`) — `centroid`, `reflectionPoint`, +`expansionPoint`, `contractionPoint` are allocated once and reused across all Nelder-Mead iterations. + +--- + +## Out-Parameter Pattern + +Write results into a caller-provided array instead of allocating and returning a new one. +This gives the caller control over allocation and enables buffer reuse. + +```typescript +function add(a: Float32Array, b: Float32Array, out: Float32Array, len: number): void { + for (let i = 0; i < len; i++) out[i] = a[i] + b[i]; +} + +const buf = new Float32Array(n); +add(x, y, buf, n); // buf = x + y +scale(buf, 2.0, buf, n); // buf = 2 * (x + y), in-place +``` + +Reference: `probabilistic-scoring/nelder-mead.ts` — `fillPoint` and `fillCentroid` write into +pre-allocated arrays, called repeatedly inside the optimization loop. + +--- + +## Scratch Buffers for Iterative Algorithms + +When an algorithm runs many iterations, declare all temporary arrays before the loop. + +```typescript +// softmax-worker.ts: Z, dZ, dW allocated once before training loop +const Z = new Array(m); +for (let i = 0; i < m; i++) Z[i] = new Float32Array(c); +const dZ = new Array(c); +for (let i = 0; i < c; i++) dZ[i] = new Float32Array(m); + +for (let iter = 0; iter < iterations; iter++) { + // Forward/backward pass writes into Z, dZ — zero allocations per iteration +} +``` + +Reference: `workers/softmax-worker.ts` (`onmessage` handler, buffer allocation before training loop). + +--- + +## Local Aliases for Inner Loops + +Store a reference to a sub-array in a local variable before the inner loop. +The primary benefit is **readability and reduced index errors**: `wBuf[k] * xBuf[k]` is +clearer than `params[i][k] * X[j][k]`, and there is less chance of mixing up `i`/`j` indices. + +> **Note on performance:** Modern V8 often hoists loop-invariant array lookups automatically +> (loop-invariant code motion), so the performance gain may be minimal. Use this pattern +> primarily for clarity in multi-level loops. + +```typescript +// Before: dense indexing, easy to confuse i/j +for (let j = 0; j < m; j++) + for (let k = 0; k < n; k++) + sum += params[i][k] * X[j][k]; + +// After: meaningful names, less index juggling +for (let j = 0; j < m; j++) { + const xBuf = X[j]; // alias, not copy + const wBuf = params[i]; + for (let k = 0; k < n; k++) + sum += wBuf[k] * xBuf[k]; +} +``` + +Reference: `workers/softmax-worker.ts` (forward propagation loop) — `xBuf`, `wBuf`, `zBuf` aliases. + +--- + +## Accumulation into Pre-allocated Output + +Allocate the output array once and accumulate contributions in-place. + +```typescript +// regression.ts: prediction = bias + sum(weight_j * feature_j) +const prediction = new Float32Array(samplesCount); +let rawData = features.byIndex(0).getRawData(); +const bias = params[featuresCount]; + +for (let i = 0; i < samplesCount; i++) + prediction[i] = bias + params[0] * rawData[i]; + +for (let j = 1; j < featuresCount; j++) { + rawData = features.byIndex(j).getRawData(); + for (let i = 0; i < samplesCount; i++) + prediction[i] += params[j] * rawData[i]; +} +``` + +Reference: `regression.ts` (`getPredictionByLinearRegression`). + +--- + +## Logical Length vs Physical Length + +Pre-allocated buffers may have a fixed physical size but a variable logical length. +Track the logical length separately and use it for all iteration bounds. + +```typescript +const properIndices = new Uint32Array(featuresCount); +let properIndicesCount = 0; + +const getProperIndices = (idx: number) => { + properIndicesCount = 0; // reset logical length + for (let i = 0; i < featuresCount; i++) { + if (featureSource[i][idx] !== featureNullVal[i]) + properIndices[properIndicesCount++] = i; + } +}; + +// Later: iterate only over valid elements +for (let i = 0; i < properIndicesCount; i++) + sum += bufferVector[properIndices[i]]; +``` + +Reference: `missing-values-imputation/knn-imputer.ts` (`KnnImputer.transform`, `getProperIndices` helper). + +--- + +## In-Place Transforms + +When the input array is no longer needed after the transform, write results directly into it. + +```typescript +function normalizeInPlace(arr: Float32Array, len: number, avg: number, stdev: number): void { + for (let i = 0; i < len; i++) arr[i] = (arr[i] - avg) / stdev; +} +``` + +Reference: `regression.ts` (`getTestDatasetForLinearRegression`). + +**Caution:** Only use in-place transforms when you own the array. Never modify arrays obtained +via `col.getRawData()` on user data — this mutates the underlying DataFrame column. + +--- + +## Bulk Copy with TypedArray.set() + +Use the built-in `set()` instead of a manual loop — engines optimize it to a memcpy-like path. + +```typescript +dst.set(src); // full copy +dst.set(src, offset); // copy into dst starting at offset +dst.set(src.subarray(start, end)); // copy a slice (subarray is a zero-copy view) + +// Clone raw column data for safe mutation +const clone = new Float32Array(col.getRawData().length); +clone.set(col.getRawData()); +``` + +**Tip:** `subarray(start, end)` returns a zero-copy view — use it to pass a logical slice +to `set()` or to functions that accept a typed array, without allocating. + +--- + +## Array Pool for Variable-Size Buffers + +When buffer sizes vary between calls, a pool recycles previously created arrays. +Interface: `acquire(minLen)` returns a buffer of at least `minLen` (contents uninitialized), +`release(arr)` returns it to the pool, `clear()` drops all pooled arrays. + +```typescript +const pool = new Float32Pool(); + +function processChunk(chunkSize: number): void { + const tmp = pool.acquire(chunkSize); + // ... compute into tmp ... + pool.release(tmp); +} + +pool.clear(); // after all work is done +``` + +Guidelines: +- **Always release** — otherwise it degrades to plain allocation. +- **Never read stale contents** — treat as uninitialized, `arr.fill(0)` if needed. +- **Scope the lifetime** — create per invocation and `clear()` when done. +- **Keep it simple** — for fixed-size buffers, plain pre-allocation is better. + +--- + +## Ring Buffer for Fixed-Length History + +When an algorithm needs a sliding window of the last N values, pre-allocate N arrays +and use a modular head index — O(1) per step, zero allocations. + +```typescript +const HIST_LEN = 5; +const history: Float64Array[] = []; +for (let i = 0; i < HIST_LEN; i++) + history[i] = new Float64Array(dim); +let head = 0; + +computeValues(history[head]); + +for (let step = 0; step < totalSteps; step++) { + const newest = history[head]; + const oldest = history[(head - (HIST_LEN - 1) + HIST_LEN) % HIST_LEN]; + + // Advance: overwrite oldest slot — O(1) + head = (head + 1) % HIST_LEN; + computeValues(history[head]); +} +``` + +**Alternative — reference shift** (O(N) per step): when consumers expect `[0]` = newest, +`[N-1]` = oldest, shift references instead. Acceptable for small N. + +```typescript +const recycled = history[HIST_LEN - 1]; +for (let j = HIST_LEN - 1; j > 0; --j) history[j] = history[j - 1]; +history[0] = recycled; +computeValues(history[0]); +``` + +Reference: `diff-grok` library, `solver-tools/ab5-method.ts` (`ab5Step`, reference shift with N=5). + +--- + +## Multi-Purpose Scratch Buffers + +The same buffer can serve different purposes at different stages within one iteration. +Each stage must fully overwrite the buffer before reading it. + +```typescript +const scratch0 = new Float64Array(dim); +const scratch1 = new Float64Array(dim); + +while (solving) { + // Stage 1: Jacobian — fills scratch0, scratch1 entirely + jacobian(t, y, f, eps, scratch0, scratch1, W); + + // Stage 2: time derivative — overwrites all elements + tDerivative(t, y, f, eps, scratch0, scratch1, hdT); + + // Stage 3: scratch0 reused as RHS for linear solve + for (let i = 0; i < dim; i++) scratch0[i] = f0[i] + hdT[i]; + luSolve(L, U, scratch0, luBuf, k1, dim); +} +``` + +For many stages, use **stage-scoped aliases**: `const rhs = scratch0;` gives semantic +context without misleading names. Both point to the same memory — zero overhead. + +> **Aliasing hazard:** Never pass the same buffer as both `src` and `dst` of a single call. +> If the function reads `src` while writing `dst`, aliasing corrupts the result. +> When unsure, use separate buffers — the cost is negligible vs a silent data corruption bug. + +Guidelines: +- **Document the reuse** with comments at each stage. +- **Never read previous-stage contents** — each stage must fully overwrite before reading. +- **Watch for aliasing** — never pass the same buffer as both source and destination. + +Reference: `diff-grok` library, `solver-tools/mrt-method.ts` (`mrtStep`, scratch buffer reuse across Jacobian/derivative/solve stages). + +--- + +## Summary + +| Pattern | When to use | Saves | +|---------|-------------|-------| +| **Pre-allocate and reuse** | Iterative algorithms | N allocations per loop | +| **Out-parameter** | Utility functions called repeatedly | 1 allocation per call | +| **Scratch buffers** | Multi-step computations in a loop | All intermediate arrays per iteration | +| **Local aliases** | Nested loops with array-of-arrays | Index errors and readability | +| **Accumulation into output** | Aggregation from multiple sources | Intermediate result arrays | +| **Logical length** | Variable-size subsets of a fixed buffer | Re-allocation on size change | +| **In-place transforms** | Input no longer needed after transform | 1 output array | +| **Bulk copy (set())** | Copying blocks between typed arrays | Loop overhead; engine-optimized | +| **Array pool** | Variable-size temporary buffers | Repeated allocation of similar arrays | +| **Ring buffer** | Sliding window / fixed-length history | O(1) advance with modular index | +| **Multi-purpose scratch** | Multi-stage algorithms | Extra buffer per stage | diff --git a/packages/EDA/src/guides/COMPUTATION-PATTERNS.md b/packages/EDA/src/guides/COMPUTATION-PATTERNS.md new file mode 100644 index 0000000000..00ba6b6b14 --- /dev/null +++ b/packages/EDA/src/guides/COMPUTATION-PATTERNS.md @@ -0,0 +1,216 @@ +# Computation Patterns for EDA Methods + +Reference for implementing computational methods in the EDA package. +For worker-based methods, see `WORKER-GUIDE.md`. +For array allocation and reuse patterns, see `ARRAY-OPERATIONS.md`. + +## Raw Typed Arrays + +Access column data via `col.getRawData()` instead of per-element `col.get(i)`. This returns the underlying +typed array (`Float32Array`, `Float64Array`, `Int32Array`, `Uint32Array`) and avoids boxing/unboxing overhead on every iteration. + +**IMPORTANT:** The raw array's `.length` may be larger than the column's element count (due to internal buffer allocation). Always use `col.length` for iteration bounds, never `rawData.length`. + +```typescript +const vals: Float32Array = features.getRawData(); +const cats: Int32Array = categories.getRawData(); +const len = features.length; // use column length, NOT vals.length + +for (let i = 0; i < len; i++) { + // direct access — no per-element API calls + const value = vals[i]; + const category = cats[i]; +} +``` + +Reference: `anova/anova-tools.ts` (`FactorizedData` constructor), `missing-values-imputation/knn-imputer.ts` (`KnnImputer.transform`). + +--- + +## Missing Values Strategy + +**Before implementing any new method, define the missing values strategy.** Different methods require different approaches: + +| Strategy | When to use | Example | +|----------|-------------|---------| +| **Skip** | Aggregation, statistics | ANOVA skips rows where factor or value is null | +| **Impute before computation** | Methods that require complete data (e.g., matrix operations) | KNN imputation, mean/median fill | +| **Propagate** | Result column should reflect original nulls | Copy null sentinel to output at the same index | +| **Reject** | Method cannot handle nulls at all | Throw error if `missingValueCount > 0` | + +Document the chosen strategy in the method's JSDoc or function header. When multiple input columns are involved, specify per-column behavior (e.g., ANOVA: skip if factor OR value is null; KNN: skip feature columns with nulls at the target row but impute the target). + +--- + +## Null Handling in Loops + +Before processing, check `col.stats.missingValueCount`. If there are no missing values, skip all null checks +in the loop — this eliminates a branch per iteration and significantly speeds up computation. + +```typescript +const hasMissing = col.stats.missingValueCount > 0; + +if (hasMissing) { + const nullValue = getNullValue(col); + for (let i = 0; i < len; i++) { + if (raw[i] === nullValue) continue; + // process raw[i] + } +} else { + for (let i = 0; i < len; i++) { + // process raw[i] — no null checks needed + } +} +``` + +When nulls are present, use `getNullValue(col)` from `utils.ts` to obtain the sentinel value and compare +against it directly in loops. Do not use platform null-checking APIs in hot paths. + +| Column type | Sentinel | Notes | +|-------------|----------|-------| +| `int`, `string`, `bool` | `-2147483648` | Min 32-bit int | +| `float`, `datetime`, `qnum` | `2.6789344063684636e-34` | Special float constant | + +```typescript +import {getNullValue} from '../utils'; + +const nullValue = getNullValue(col); +const raw = col.getRawData(); + +for (let i = 0; i < col.length; i++) { + if (raw[i] === nullValue) continue; // skip missing + // process raw[i] +} +``` + +For categorical (string) columns, raw data stores integer category indices. Check for null categories separately: + +```typescript +const categoriesNull = categories.stats.missingValueCount > 0 ? getNullValue(categories) : -1; + +for (let i = 0; i < size; i++) { + if ((cats[i] === categoriesNull) || (vals[i] === featuresNull)) continue; + // process non-null pair +} +``` + +Reference: `anova/anova-tools.ts` (`FactorizedData.setStats`), `missing-values-imputation/knn-imputer.ts` (`KnnImputer.transform`). + +--- + +## Single-Pass Aggregation + +Compute all required statistics in one loop over the data. Pre-allocate output buffers as typed arrays. + +```typescript +const K = uniqueCategoryCount; +const sums = new Float64Array(K).fill(0); +const sumsOfSquares = new Float64Array(K).fill(0); +const subSampleSizes = new Int32Array(K).fill(0); + +for (let i = 0; i < size; i++) { + const cat = cats[i]; + if (vals[i] === nullValue) continue; + + sums[cat] += vals[i]; + sumsOfSquares[cat] += vals[i] ** 2; + ++subSampleSizes[cat]; +} +``` + +Reference: `anova/anova-tools.ts`, `FactorizedData.setStats()`. + +--- + +## Bool Column Handling + +Bool columns are stored as packed bit arrays. Extract individual bits via bitwise operations: + +```typescript +const raw = boolCol.getRawData(); // Uint32Array with packed bits +let catIdx = 0; +let shift = 0; +let packed = raw[0]; +const MAX_SHIFT = 8 * raw.BYTES_PER_ELEMENT - 1; + +for (let i = 0; i < size; i++) { + const bit = 1 & (packed >> shift); + // use `bit` as 0 or 1 + + ++shift; + if (shift > MAX_SHIFT) { + shift = 0; + ++catIdx; + packed = raw[catIdx]; + } +} +``` + +Reference: `anova/anova-tools.ts` (`FactorizedData.setStats`, bool branch). + +--- + +## Data Locality + +Typed arrays store elements contiguously in memory. Sequential access maximizes CPU cache utilization +and enables hardware prefetching. This is a key reason to prefer typed arrays over `number[]` or per-element API calls. + +### Why it matters + +- **Cache lines**: CPU loads data in 64-byte blocks. One cache line holds 16 `float32` or 8 `float64` values. + Sequential access means every loaded cache line is fully utilized. +- **Prefetching**: CPU detects sequential access patterns and preloads next cache lines automatically. + This hides memory latency almost entirely for linear traversals. +- **No boxing**: `number[]` stores boxed values as heap-allocated objects (pointer → header → value). + Typed arrays store raw values inline — more useful data per cache line. + +### Access pattern guidelines + +| Pattern | Cache behavior | Use when | +|---------|---------------|----------| +| Sequential typed array traversal | Optimal — prefetcher active, full cache line utilization | Aggregation, statistics, transforms | +| Multiple typed arrays in parallel (`vals[i]`, `cats[i]`) | Good — each array has its own prefetch stream | Multi-column single-pass (ANOVA, KNN distances) | +| Random access to typed array | Cache miss per access — up to 100x slower than sequential | Avoid; restructure if possible | +| `col.get(i)` in a loop | Method call + potential unboxing per element | Avoid in hot loops | + +### Column-major vs row-major + +When building matrices from multiple columns, the layout determines which access patterns are cache-friendly: + +- **Column-major** (`data[i + j * nRows]`): optimal when processing columns independently + (e.g., centering, scaling, per-feature statistics) +- **Row-major** (`data[i * nCols + j]`): optimal when accessing all features of one row + (e.g., distance computation, KNN, nearest neighbor search) + +Choose the layout that matches the method's primary access pattern. See `WORKER-GUIDE.md` +for `toFlatColumnMajor` and `toFlatRowMajor` helper functions. + +### Pre-allocate output buffers + +Allocate result arrays once before the loop to avoid repeated allocations and garbage collection: + +```typescript +// Good: single allocation +const result = new Float64Array(len); +for (let i = 0; i < len; i++) + result[i] = vals[i] * scale; + +// Bad: growing array triggers re-allocation and copying +const result: number[] = []; +for (let i = 0; i < len; i++) + result.push(vals[i] * scale); +``` + +--- + +## Module Structure + +Separate computation from UI into distinct files: + +| File pattern | Purpose | Dependencies | +|-------------|---------|-------------| +| `*-tools.ts` | Pure computation on raw data | `datagrok-api` types, `utils.ts`, math libraries | +| `*-ui.ts` or `ui.ts` | Dialog, inputs, validation, visualization | `datagrok-api` UI, computation module | +| `*-constants.ts` or `ui-constants.ts` | Enums, error messages, UI labels | None | + +The computation module must not import UI components. This keeps it testable and potentially reusable in workers. diff --git a/packages/EDA/src/guides/PARALLEL-EXECUTION.md b/packages/EDA/src/guides/PARALLEL-EXECUTION.md new file mode 100644 index 0000000000..a63281ac12 --- /dev/null +++ b/packages/EDA/src/guides/PARALLEL-EXECUTION.md @@ -0,0 +1,52 @@ +# Parallel Execution Guide + +Reference for distributing independent computations across multiple web workers. +For single-worker patterns, see `WORKER-GUIDE.md`. + +## Worker Count + +```typescript +import {MIN_WORKERS_COUNT, WORKERS_COUNT_DOWNSHIFT} from './worker-utils/worker-defs'; + +const workerCount = Math.max(MIN_WORKERS_COUNT, navigator.hardwareConcurrency - WORKERS_COUNT_DOWNSHIFT); +``` + +## Fan-out / Fan-in Pattern + +```typescript +async function runParallel( + inputs: TInput[], + workerUrl: URL, +): Promise { + const nWorkers = Math.min( + Math.max(MIN_WORKERS_COUNT, navigator.hardwareConcurrency - WORKERS_COUNT_DOWNSHIFT), + inputs.length, + ); + + // Distribute inputs round-robin + const chunks: TInput[][] = Array.from({length: nWorkers}, () => []); + for (let i = 0; i < inputs.length; i++) + chunks[i % nWorkers].push(inputs[i]); + + const promises = chunks.map((chunk) => + new Promise((resolve, reject) => { + const worker = new Worker(workerUrl); + worker.postMessage(chunk); + worker.onmessage = (e) => { + worker.terminate(); + if (e.data.success) + resolve(e.data.data); + else + reject(new Error(e.data.error)); + }; + worker.onerror = (e) => { + worker.terminate(); + reject(new Error(e.message)); + }; + }), + ); + + const results = await Promise.all(promises); + return results.flat(); +} +``` diff --git a/packages/EDA/src/guides/WORKER-GUIDE.md b/packages/EDA/src/guides/WORKER-GUIDE.md new file mode 100644 index 0000000000..fa69e4a0d2 --- /dev/null +++ b/packages/EDA/src/guides/WORKER-GUIDE.md @@ -0,0 +1,293 @@ +# Worker Implementation Guide for EDA ML Methods + +Reference for implementing in-worker ML and data analysis methods using the `worker-utils` infrastructure. + +## Worker-Utils Infrastructure + +### Definitions (`worker-defs.ts`) — no dependencies, safe to import in workers + +```typescript +type RawData = Int32Array | Float32Array | Float64Array | Uint32Array; +type ColumnType = 'int' | 'float32' | 'float64' | 'string' | 'bool' | 'datetime' | 'qnum' | 'bigint'; + +interface WorkerColumnStats { + totalCount: number; + missingValueCount: number; + uniqueCount: number; + valueCount: number; + min: number; max: number; + sum: number; avg: number; + stdev: number; variance: number; + skew: number; kurt: number; + med: number; + q1: number; q2: number; q3: number; + nullValue: number; // INT_NULL (-2147483648) or FLOAT_NULL (2.6789344063684636e-34) +} + +interface WorkerColumn { + name: string; + type: ColumnType; + length: number; + rawData: RawData; + stats: WorkerColumnStats; + categories?: string[]; // only for type === 'string' +} + +interface WorkerDataFrame { + name: string; + rowCount: number; + columns: WorkerColumn[]; +} +``` + +### Transforms (`worker-transforms.ts`) — requires `datagrok-api`, main-thread only + +| Function | Signature | Direction | +|----------|-----------|-----------| +| `toWorkerColumn` | `(col: DG.Column) => WorkerColumn` | DG -> Worker | +| `toWorkerColumns` | `(columns: DG.ColumnList) => WorkerColumn[]` | DG -> Worker | +| `toWorkerDataFrame` | `(df: DG.DataFrame) => WorkerDataFrame` | DG -> Worker | +| `fromWorkerColumn` | `(wc: WorkerColumn) => DG.Column` | Worker -> DG | +| `fromWorkerDataFrame` | `(wdf: WorkerDataFrame) => DG.DataFrame` | Worker -> DG | + +### Null Sentinel Values + +| ColumnType | Sentinel | Constant | +|------------|----------|----------| +| `int`, `string`, `bool` | -2147483648 | `INT_NULL` | +| `float32`, `float64`, `datetime`, `qnum` | 2.6789344063684636e-34 | `FLOAT_NULL` | + +--- + +## Missing Values Strategy + +Before implementing any new worker-based method, define the missing values strategy (skip, impute, propagate, or reject). See `COMPUTATION-PATTERNS.md` (Missing Values Strategy section) for the full decision table and per-column behavior guidelines. + +--- + +## Working with WorkerColumn Inside a Worker + +**IMPORTANT:** The `rawData` array's `.length` may be larger than `col.length` (due to internal buffer allocation). Always use `col.length` for iteration bounds, never `rawData.length`. + +### Reading numerical data + +Check `col.stats.missingValueCount` before processing. If there are no missing values, skip all null checks +in the loop — this eliminates a branch per iteration and significantly speeds up computation. + +```typescript +// worker.ts +import {WorkerColumn} from './worker-defs'; + +onmessage = (e: MessageEvent) => { + const col: WorkerColumn = e.data; + const raw = col.rawData as Float32Array; + const n = col.length; + + if (col.stats.missingValueCount > 0) { + const nullVal = col.stats.nullValue; + for (let i = 0; i < n; i++) { + if (raw[i] === nullVal) continue; // skip missing + // process raw[i] + } + } else { + for (let i = 0; i < n; i++) { + // process raw[i] — no null checks needed + } + } +}; +``` + +### Centering / scaling using stats + +```typescript +function centerAndScale(col: WorkerColumn): Float32Array { + const raw = col.rawData as Float32Array; + const result = new Float32Array(col.length); + const nullVal = col.stats.nullValue; + const avg = col.stats.avg; + const stdev = col.stats.stdev; + + for (let i = 0; i < col.length; i++) { + if (raw[i] === nullVal) + result[i] = nullVal; + else + result[i] = (raw[i] - avg) / stdev; + } + return result; +} +``` + +### Building a feature matrix from WorkerColumn[] + +Choose the matrix layout based on the method's primary access pattern — this directly affects +CPU cache utilization: + +- **Column-major**: sequential access within each column. Optimal when columns are processed + independently (centering, scaling, per-feature statistics, WASM interop). +- **Row-major flat**: sequential access across features of each row. Optimal for distance + computation, KNN, nearest neighbor search. +- **Row-major typed**: same access pattern as row-major flat, but each row is a separate + `Float32Array`. Use as a drop-in replacement for `number[][]`. + +Avoid random access patterns — a cache miss per access can be up to 100x slower than sequential traversal. +For more details on data locality, see `COMPUTATION-PATTERNS.md` (Data Locality section). + +```typescript +// Row-major flat Float32Array: data[i * nCols + j] +// Single allocation, contiguous memory, no boxing overhead. +function toFlatRowMajor(cols: WorkerColumn[]): Float32Array { + const nRows = cols[0].length; + const nCols = cols.length; + const data = new Float32Array(nRows * nCols); + for (let j = 0; j < nCols; j++) { + const raw = cols[j].rawData; + for (let i = 0; i < nRows; i++) + data[i * nCols + j] = raw[i]; + } + return data; +} + +// Row-major Float32Array[]: data[i][j] +// Drop-in replacement for number[][] with unboxed typed rows. +function toTypedRowMajor(cols: WorkerColumn[]): Float32Array[] { + const nRows = cols[0].length; + const nCols = cols.length; + const data: Float32Array[] = new Array(nRows); + for (let i = 0; i < nRows; i++) + data[i] = new Float32Array(nCols); + for (let j = 0; j < nCols; j++) { + const raw = cols[j].rawData; + for (let i = 0; i < nRows; i++) + data[i][j] = raw[i]; + } + return data; +} + +// Column-major flat Float32Array: data[i + j * nRows] +// Optimal when columns are processed independently (WASM, matrix ops). +function toFlatColumnMajor(cols: WorkerColumn[]): Float32Array { + const nRows = cols[0].length; + const nCols = cols.length; + const data = new Float32Array(nRows * nCols); + for (let j = 0; j < nCols; j++) { + const raw = cols[j].rawData; + const offset = j * nRows; + for (let i = 0; i < nRows; i++) + data[i + offset] = raw[i]; + } + return data; +} +``` + +### Creating a result WorkerColumn + +```typescript +function makeResultColumn(name: string, data: Float32Array): WorkerColumn { + return { + name: name, + type: 'float32', + length: data.length, + rawData: data, + stats: computeStats(data), // compute in worker or leave zeros if not needed + }; +} +``` + +--- + +## Worker Lifecycle Pattern + +### Main thread (caller) + +```typescript +import {toWorkerColumns, fromWorkerColumn} from './worker-utils/worker-transforms'; +import {WorkerColumn} from './worker-utils/worker-defs'; + +async function runInWorker( + features: DG.ColumnList, components: number +): Promise { + const workerFeatures = toWorkerColumns(features); + + return new Promise((resolve, reject) => { + const worker = new Worker(new URL('./workers/my-worker.ts', import.meta.url)); + + worker.postMessage({features: workerFeatures, components}); + + worker.onmessage = (e) => { + worker.terminate(); + if (e.data.success) { + const result = e.data.data.columns as WorkerColumn[]; + resolve(result.map(fromWorkerColumn)); + } else { + reject(new Error(e.data.error)); + } + }; + + worker.onerror = (e) => { + worker.terminate(); + reject(new Error(e.message)); + }; + }); +} +``` + +### Web worker + +```typescript +import {WorkerColumn} from '../worker-utils/worker-defs'; + +interface MyWorkerInput { + features: WorkerColumn[]; + components: number; +} + +interface MyWorkerOutput { + success: true; + data: {columns: WorkerColumn[]}; +} | { + success: false; + error: string; +} + +onmessage = (e: MessageEvent) => { + try { + const {features, components} = e.data; + + // Access raw data directly: + const nRows = features[0].length; + const nCols = features.length; + + // Use stats: + for (const f of features) { + const avg = f.stats.avg; + const stdev = f.stats.stdev; + const nullVal = f.stats.nullValue; + // ... + } + + // Build result columns: + const resultCols: WorkerColumn[] = []; + for (let c = 0; c < components; c++) { + const data = new Float32Array(nRows); + // ... fill data ... + resultCols.push({ + name: `Component ${c + 1}`, + type: 'float32', + length: nRows, + rawData: data, + stats: { /* fill or leave defaults */ } as any, + }); + } + + postMessage({success: true, data: {columns: resultCols}} satisfies MyWorkerOutput); + } catch (err) { + postMessage({success: false, error: String(err)}); + } +}; +``` + +--- + +## Parallel Execution + +For distributing independent computations across multiple workers, see `PARALLEL-EXECUTION.md`. diff --git a/packages/EDA/src/missing-values-imputation/knn-imputer.ts b/packages/EDA/src/missing-values-imputation/knn-imputer.ts index 029f6bfb4c..1e4734da32 100644 --- a/packages/EDA/src/missing-values-imputation/knn-imputer.ts +++ b/packages/EDA/src/missing-values-imputation/knn-imputer.ts @@ -5,6 +5,7 @@ import * as ui from 'datagrok-api/ui'; import * as DG from 'datagrok-api/dg'; import {ERROR_MSG, COPY_SUFFIX} from './ui-constants'; +import {getNullValue} from '../utils'; /** Column types supported by the missing values imputer */ export const SUPPORTED_COLUMN_TYPES = [ @@ -15,29 +16,6 @@ export const SUPPORTED_COLUMN_TYPES = [ DG.COLUMN_TYPE.QNUM, ] as string[]; -/** Return null value with respect to the column type */ -export function getNullValue(col: DG.Column): number { - switch (col.type) { - case DG.COLUMN_TYPE.INT: - return DG.INT_NULL; - - case DG.COLUMN_TYPE.FLOAT: - return DG.FLOAT_NULL; - - case DG.COLUMN_TYPE.QNUM: - return DG.FLOAT_NULL; - - case DG.COLUMN_TYPE.DATE_TIME: - return DG.FLOAT_NULL; - - case DG.COLUMN_TYPE.STRING: - return col.max; - - default: - throw new Error(ERROR_MSG.UNSUPPORTED_COLUMN_TYPE); - } -} - /** Metric types (between column elements) */ export enum METRIC_TYPE { ONE_HOT = 'One-hot', diff --git a/packages/EDA/src/utils.ts b/packages/EDA/src/utils.ts index 08eca2f15c..1c086c542a 100644 --- a/packages/EDA/src/utils.ts +++ b/packages/EDA/src/utils.ts @@ -35,6 +35,30 @@ const INCORRECT_STEPS_MES = 'steps must be non-negative.'; const INCORRECT_CYCLES_MES = 'cycles must be positive.'; const INCORRECT_CUTOFF_MES = 'cutoff must be non-negative.'; +/** + * Returns the null sentinel value for the raw typed array of the given column. + * + * - Int-backed columns (int, string, bool) use {@link DG.INT_NULL} (-2147483648) + * - Float-backed columns (float, datetime, qnum) use {@link DG.FLOAT_NULL} + * + * @param col - source Datagrok column + * @returns null sentinel value used in the column's raw data + */ +export function getNullValue(col: DG.Column): number { + switch (col.type) { + case DG.COLUMN_TYPE.INT: + case DG.COLUMN_TYPE.STRING: + case DG.COLUMN_TYPE.BOOL: + return DG.INT_NULL; + case DG.COLUMN_TYPE.FLOAT: + case DG.COLUMN_TYPE.DATE_TIME: + case DG.COLUMN_TYPE.QNUM: + return DG.FLOAT_NULL; + default: + return DG.FLOAT_NULL; + } +} + /** Check column type */ export function checkColumnType(col: DG.Column): void { if ((col.type != DG.COLUMN_TYPE.FLOAT) && (col.type != DG.COLUMN_TYPE.INT)) @@ -288,7 +312,7 @@ export function describeElements(roots: HTMLElement[], description: string[], po }, 'Go to the next viewer'); const prevBtn = ui.button('prev', () => { - idx -= 1; + idx -= 1; popup.remove(); step(); }, 'Go to the previous viewer'); @@ -296,8 +320,8 @@ export function describeElements(roots: HTMLElement[], description: string[], po const doneBtn = ui.button('done', () => popup.remove(), 'Go to the next step'); const btnsDiv = ui.divH([prevBtn, nextBtn, doneBtn]); - btnsDiv.style.marginLeft = 'auto'; - btnsDiv.style.marginRight = '0px'; + btnsDiv.style.marginLeft = 'auto'; + btnsDiv.style.marginRight = '0px'; const step = () => { if (idx < roots.length) { @@ -306,7 +330,7 @@ export function describeElements(roots: HTMLElement[], description: string[], po doneBtn.hidden = (idx < roots.length - 1); nextBtn.hidden = (idx === roots.length - 1); prevBtn.hidden = (idx < 1); - + closeIcn = popup.querySelector('i') as HTMLElement; closeIcn.onclick = () => doneBtn.click(); } diff --git a/packages/EDA/src/worker-utils/worker-defs.ts b/packages/EDA/src/worker-utils/worker-defs.ts new file mode 100644 index 0000000000..94d148c796 --- /dev/null +++ b/packages/EDA/src/worker-utils/worker-defs.ts @@ -0,0 +1,153 @@ +/** + * Raw typed array as returned by {@link DG.Column.getRawData}. + * + * The concrete type depends on the column: + * - `Int32Array` — int and string (category indices) columns + * - `Float32Array` — single-precision float columns + * - `Float64Array` — double-precision float, datetime, and qnum columns + * - `Uint32Array` — bool columns (bit array) + */ +export type RawData = Int32Array | Float32Array | Float64Array | Uint32Array; + +/** + * Worker-side column type identifier. + * + * Unlike DG.COLUMN_TYPE, this splits the DG `'double'` type into `'float32'` and `'float64'` + * based on the actual typed array storage returned by {@link DG.Column.getRawData}. + * + * @example + * // A DG column of type 'double' stored as Float32Array becomes 'float32': + * const wc = toWorkerColumn(floatCol); + * console.log(wc.type); // 'float32' + */ +export type ColumnType = 'int' | 'float32' | 'float64' | 'string' | 'bool' | 'datetime' | 'qnum' | 'bigint'; + +/** + * Plain object with all numerical fields from {@link DG.Stats}, plus the null sentinel. + * + * All values are eagerly extracted from the Dart-backed Stats object, + * making this safe to transfer via `postMessage`. + * + * @example + * // Access stats inside a web worker: + * onmessage = (e) => { + * const col: WorkerColumn = e.data; + * if (col.stats.missingValueCount > 0) + * console.log(`Column has ${col.stats.missingValueCount} missing values`); + * // Check for nulls in raw data: + * const nullVal = col.stats.nullValue; + * for (let i = 0; i < col.length; i++) + * if (col.rawData[i] === nullVal) { // handle null } + * }; + */ +export interface WorkerColumnStats { + /** Total number of values (including missing values). */ + totalCount: number; + /** Number of missing (empty) values. */ + missingValueCount: number; + /** Number of unique values. */ + uniqueCount: number; + /** Number of non-empty values. */ + valueCount: number; + /** Minimum value. */ + min: number; + /** Maximum value. */ + max: number; + /** Sum of all values. */ + sum: number; + /** Average (mean). */ + avg: number; + /** Standard deviation. */ + stdev: number; + /** Variance. */ + variance: number; + /** Skewness. */ + skew: number; + /** Kurtosis. */ + kurt: number; + /** Median value. */ + med: number; + /** First quartile. */ + q1: number; + /** Second quartile. */ + q2: number; + /** Third quartile. */ + q3: number; + /** + * Null sentinel value used in {@link WorkerColumn.rawData} for this column type. + * + * - `INT_NULL` (-2147483648) for int, string, and bool columns + * - `FLOAT_NULL` (2.6789344063684636e-34) for float32, float64, datetime, and qnum columns + */ + nullValue: number; +} + +/** + * Structured-cloneable representation of a {@link DG.Column} for web worker transfer. + * + * Contains the raw typed array, pre-computed stats, column metadata, and (for string columns) + * the categories array. All fields are plain JS values safe for `postMessage`. + * + * @example + * // Main thread — send column to worker: + * const wc: WorkerColumn = toWorkerColumn(df.col('age')); + * worker.postMessage(wc); + * + * // Worker — receive and use: + * onmessage = (e) => { + * const wc: WorkerColumn = e.data; + * const raw = wc.rawData as Float32Array; + * const mean = wc.stats.avg; + * const nullVal = wc.stats.nullValue; + * for (let i = 0; i < wc.length; i++) { + * if (raw[i] !== nullVal) + * raw[i] -= mean; // center the data + * } + * postMessage(wc); + * }; + */ +export interface WorkerColumn { + /** Column name. */ + name: string; + /** Worker-side column type (see {@link ColumnType}). */ + type: ColumnType; + /** Number of rows. */ + length: number; + /** Raw typed array extracted via {@link DG.Column.getRawData}. */ + rawData: RawData; + /** Pre-computed descriptive statistics. */ + stats: WorkerColumnStats; + /** String categories — present only for string columns (type === 'string'). */ + categories?: string[]; +} + +/** + * Structured-cloneable representation of a {@link DG.DataFrame} for web worker transfer. + * + * @example + * // Main thread — send entire table to worker: + * const wdf: WorkerDataFrame = toWorkerDataFrame(table); + * worker.postMessage(wdf); + * + * // Worker — iterate columns: + * onmessage = (e) => { + * const wdf: WorkerDataFrame = e.data; + * for (const col of wdf.columns) { + * console.log(`${col.name}: ${col.type}, ${wdf.rowCount} rows`); + * } + * }; + */ +export interface WorkerDataFrame { + /** DataFrame name. */ + name: string; + /** Number of rows. */ + rowCount: number; + /** Columns in order. */ + columns: WorkerColumn[]; +} + +/** Minimum number of workers to use for parallel processing. */ +export const MIN_WORKERS_COUNT = 1; + +/** Maximum number of workers to use for parallel processing. */ +export const WORKERS_COUNT_DOWNSHIFT = 2; diff --git a/packages/EDA/src/worker-utils/worker-transforms.ts b/packages/EDA/src/worker-utils/worker-transforms.ts new file mode 100644 index 0000000000..959d96df68 --- /dev/null +++ b/packages/EDA/src/worker-utils/worker-transforms.ts @@ -0,0 +1,165 @@ +import * as DG from 'datagrok-api/dg'; +import {WorkerColumn, WorkerDataFrame, ColumnType} from './worker-defs'; +import {getNullValue} from '../utils'; + +/** + * Resolves the worker {@link ColumnType} from a {@link DG.Column}, + * splitting DG `'double'` into `'float32'` or `'float64'` based on the actual typed array. + * + * @param col - source Datagrok column + * @returns worker-side column type string + */ +function resolveColumnType(col: DG.Column): ColumnType { + if (col.type === DG.COLUMN_TYPE.FLOAT) + return col.getRawData() instanceof Float64Array ? 'float64' : 'float32'; + return col.type as ColumnType; +} + +/** + * Extracts a plain {@link WorkerColumn} from a {@link DG.Column}. + * + * Copies all stat values eagerly and retrieves the raw typed array via `getRawData()`. + * The result is structured-cloneable and can be sent to a web worker via `postMessage`. + * + * @param col - source Datagrok column + * @returns plain object safe for worker transfer + * + * @example + * // Send a single column to a web worker: + * const worker = new Worker('my-worker.ts'); + * const wc = toWorkerColumn(df.col('age')); + * worker.postMessage(wc); + */ +export function toWorkerColumn(col: DG.Column): WorkerColumn { + const type = resolveColumnType(col); + const s = col.stats; + + const result: WorkerColumn = { + name: col.name, + type: type, + length: col.length, + rawData: col.getRawData(), + stats: { + totalCount: s.totalCount, + missingValueCount: s.missingValueCount, + uniqueCount: s.uniqueCount, + valueCount: s.valueCount, + min: s.min, + max: s.max, + sum: s.sum, + avg: s.avg, + stdev: s.stdev, + variance: s.variance, + skew: s.skew, + kurt: s.kurt, + med: s.med, + q1: s.q1, + q2: s.q2, + q3: s.q3, + nullValue: getNullValue(col), + }, + }; + + if (type === 'string') + result.categories = col.categories; + + return result; +} + +/** + * Converts a {@link DG.ColumnList} to an array of {@link WorkerColumn} objects. + * + * Each column is converted via {@link toWorkerColumn}. The resulting array is + * structured-cloneable and can be sent to a web worker via `postMessage`. + * + * @param columns - source Datagrok column list + * @returns array of plain worker columns + * + * @example + * // Send selected features to a web worker: + * const worker = new Worker('pca-worker.ts'); + * const features = toWorkerColumns(table.columns.byNames(['x', 'y', 'z'])); + * worker.postMessage({features, components: 2}); + */ +export function toWorkerColumns(columns: DG.ColumnList): WorkerColumn[] { + const result: WorkerColumn[] = []; + for (const col of columns) + result.push(toWorkerColumn(col)); + return result; +} + +/** + * Extracts a plain {@link WorkerDataFrame} from a {@link DG.DataFrame}. + * + * Converts each column via {@link toWorkerColumn}. The result is structured-cloneable + * and can be sent to a web worker via `postMessage`. + * + * @param df - source Datagrok dataframe + * @returns plain object safe for worker transfer + * + * @example + * // Send an entire table to a web worker: + * const worker = new Worker('my-worker.ts'); + * const wdf = toWorkerDataFrame(grok.shell.t); + * worker.postMessage(wdf); + */ +export function toWorkerDataFrame(df: DG.DataFrame): WorkerDataFrame { + const columns: WorkerColumn[] = []; + for (const col of df.columns) + columns.push(toWorkerColumn(col)); + return {name: df.name, rowCount: df.rowCount, columns: columns}; +} + +/** + * Reconstructs a {@link DG.DataFrame} from a {@link WorkerDataFrame} received from a web worker. + * + * Creates columns via {@link fromWorkerColumn} and restores the dataframe name. + * + * @param wdf - worker dataframe received via `postMessage` + * @returns Datagrok DataFrame + * + * @example + * // Receive results from a web worker: + * worker.onmessage = (e) => { + * const df = fromWorkerDataFrame(e.data as WorkerDataFrame); + * grok.shell.addTableView(df); + * }; + */ +export function fromWorkerDataFrame(wdf: WorkerDataFrame): DG.DataFrame { + const df = DG.DataFrame.fromColumns(wdf.columns.map(fromWorkerColumn)); + df.name = wdf.name; + return df; +} + +/** + * Reconstructs a {@link DG.Column} from a {@link WorkerColumn} received from a web worker. + * + * Selects the appropriate `DG.Column.fromXxxArray` factory based on {@link WorkerColumn.type}. + * + * @param wc - worker column received via `postMessage` + * @returns Datagrok Column + * + * @example + * // Receive a single column from a web worker: + * worker.onmessage = (e) => { + * const col = fromWorkerColumn(e.data as WorkerColumn); + * table.columns.add(col); + * }; + */ +export function fromWorkerColumn(wc: WorkerColumn): DG.Column { + switch (wc.type) { + case 'int': + return DG.Column.fromInt32Array(wc.name, wc.rawData as Int32Array, wc.length); + case 'float32': + return DG.Column.fromFloat32Array(wc.name, wc.rawData as Float32Array, wc.length); + case 'float64': + return DG.Column.fromFloat64Array(wc.name, wc.rawData as Float64Array, wc.length); + case 'qnum': + case 'datetime': + return DG.Column.fromFloat64Array(wc.name, wc.rawData as Float64Array, wc.length); + case 'string': + return DG.Column.fromIndexes(wc.name, wc.categories!, wc.rawData as Int32Array); + default: + return DG.Column.fromFloat32Array(wc.name, wc.rawData as Float32Array, wc.length); + } +} diff --git a/packages/InteractiveSciAppTest/.gitignore b/packages/InteractiveSciAppTest/.gitignore new file mode 100644 index 0000000000..fb3a960466 --- /dev/null +++ b/packages/InteractiveSciAppTest/.gitignore @@ -0,0 +1,33 @@ +# Developer keys +upload.keys.json + +# Dependency directories +node_modules/ + +# Webpack outputs +dist/ + +# IDEs +# VS Code (https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore) +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.history/ + +# Intellij IDEA/WebStorm (https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore) +.idea/inspectionProfiles/ +.idea/**/compiler.xml +.idea/**/encodings.xml +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/codeStyles/ + +**/*.d.ts +# Emitted *.js files +src/**/*.js diff --git a/packages/InteractiveSciAppTest/.npmignore b/packages/InteractiveSciAppTest/.npmignore new file mode 100644 index 0000000000..3b8fe98436 --- /dev/null +++ b/packages/InteractiveSciAppTest/.npmignore @@ -0,0 +1,30 @@ +# Developer keys +upload.keys.json + +# Dependency directories +node_modules/ + +# IDEs +# VS Code (https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore) +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.history/ + +# Intellij IDEA/WebStorm (https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore) +.idea/inspectionProfiles/ +.idea/**/compiler.xml +.idea/**/encodings.xml +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/codeStyles/ + + +**/*.d.ts +!src/package-api.d.ts diff --git a/packages/InteractiveSciAppTest/CHANGELOG.md b/packages/InteractiveSciAppTest/CHANGELOG.md new file mode 100644 index 0000000000..a972d08252 --- /dev/null +++ b/packages/InteractiveSciAppTest/CHANGELOG.md @@ -0,0 +1,3 @@ +# InteractiveSciAppTest changelog + +## 0.0.1 (2026-03-10) \ No newline at end of file diff --git a/packages/InteractiveSciAppTest/README.md b/packages/InteractiveSciAppTest/README.md new file mode 100644 index 0000000000..86a0ee9cca --- /dev/null +++ b/packages/InteractiveSciAppTest/README.md @@ -0,0 +1,3 @@ +# InteractiveSciAppTest + +`InteractiveSciAppTest` is a [package](https://datagrok.ai/help/develop/develop#packages) for the [Datagrok](https://datagrok.ai) platform diff --git a/packages/InteractiveSciAppTest/css/levins.css b/packages/InteractiveSciAppTest/css/levins.css new file mode 100644 index 0000000000..2c73271d32 --- /dev/null +++ b/packages/InteractiveSciAppTest/css/levins.css @@ -0,0 +1,25 @@ +/* Levins Metapopulation Model — Application Styles */ + +/* Rho badge (e0/m ratio indicator) */ +.levins-rho-badge { + font-size: 13px; + padding: 4px 8px; + border-radius: 4px; + display: inline-block; + margin-top: 4px; + color: white; +} + +.levins-rho-badge--persists { + background-color: #4CAF50; +} + +.levins-rho-badge--extinct { + background-color: #F44336; +} + +/* Disabled icon button (ui.iconFA) */ +.levins-btn--disabled { + pointer-events: none; + opacity: 0.4; +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/datagrok-app-specification-template.md b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/datagrok-app-specification-template.md new file mode 100644 index 0000000000..66aeb36523 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/datagrok-app-specification-template.md @@ -0,0 +1,551 @@ +# Datagrok Interactive Scientific Application Specification Template + +> This template follows the structure of `datagrok-interactive-app-guide.md`. +> Section numbers here match the guide's section numbers. +> Fill in every section. Mark sections that do not apply as "N/A" with a brief explanation. + +## 1. General Architecture + +### 1.0. General Information + +| Field | Value | +|---|---| +| Application name | | +| Package | | +| Entry function | | +| Brief description | What the application does, what scientific problem it solves | +| Main view type | `DG.TableView` / other | + +### 1.1. Core + +The core contains one or more computation tasks. Each task is described separately. + +#### Task List + +| Task ID | Name | Pipeline type | Trigger | Synchronicity | Execution environment | Parallelization | +|---|---|---|---|---|---|---| +| task_primary | ... | Primary (reactive) | Input change | Sync / async | Main thread / web worker | No / yes — strategy | +| task_secondary_1 | ... | Secondary (on demand) | Button / icon / menu item | ... | ... | ... | + +#### Dependencies Between Tasks + +``` +Example: + task_primary → result is used by task_secondary_1 + task_secondary_2 is independent of task_primary +``` + +#### Description of Each Task + +A separate block is filled in for each task. + +--- + +##### Task: `task_id` + +**General characteristics:** + +| Field | Value | +|---|---| +| Name | | +| Description | What the task computes | +| Dependency on other tasks | No / uses results of task `task_id` | + +**Computation Formulas and Model** + +> See guide section 1.1 "Computation Formulas and Model" for the two-level definition. + +**Level 1 — required minimum (before implementation):** + +Variables: + +| Variable | Meaning | Units | Domain | +|---|---|---|---| +| ... | ... | ... | e.g., `p ∈ (0, 1]` | + +Relationships (equations, recurrences, algorithmic steps connecting inputs to outputs): + +``` +... +``` + +Output properties (invariants that must hold on the result): + +| Property | Description | +|---|---| +| ... | e.g., bounds, monotonicity, conservation laws, limiting cases | + +Reference examples (at least one per computational path / mode / branch): + +| # | Inputs | Expected output | Computational path | Source | +|---|---|---|---|---| +| 1 | ... | ... | e.g., base model | Manual calculation / literature / reference implementation | + +**Level 2 — full formalization (can be developed incrementally):** + +- Complete mathematical formulation: ___(equations, initial/boundary conditions, parameterization)___ +- Analytical properties: ___(equilibria, asymptotic behavior, stability, bifurcation points)___ +- Numerical method justification: ___(why this method, stability, order of accuracy, applicability, literature reference)___ + +Level 2 document location: in this specification / separate document: ___ + +> Level 2 need not be complete before implementation begins, but must be complete before the computational part is considered verified. + +**Task input parameters:** + +| Parameter | Type | Units | Domain | Description | +|---|---|---|---|---| +| ... | ... | ... | ... | ... | + +**Task output data:** + +| Parameter | Type | Description | +|---|---|---| +| ... | ... | ... | + +**Computation implementation:** + +For each computation step: + +| Step | Implementation method | Details | Documentation | +|---|---|---|---| +| 1 | Datagrok API | Method: ... (main thread only) | [Datagrok JS API](https://datagrok.ai/api/js/) | +| 2 | External library | Library: ..., version: ..., functions: ... | Link to API reference / README (required) | +| 3 | Custom method | Brief description: ... | Link to method specification (separate document, required) | + +For **external libraries** implementing numerical methods: state which method properties are relevant (stability, order, applicability class), expected accuracy, and the verification strategy (reference problems, comparison with alternatives). See section 15.3. + +For **custom methods**: the method specification (separate document) must contain: mathematical formulation, step-by-step algorithm, input/output data, constraints, edge cases, literature references, expected accuracy, and the verification strategy (reference examples with sources). See section 15.3. + +**Execution environment constraint:** if the task uses Datagrok API — main thread only. + +--- + +### 1.2. Ports + +For each task, define input/output ports. Additionally, define application-level ports. + +#### Task ports: `task_id` + +| Port | Type | Interface / format | Description | +|---|---|---|---| +| Input | ... | Interface name / type | What parameters the task expects | +| Output | ... | Interface name / type | What the task returns | + +#### Application-level ports + +| Port | Used | Description | +|---|---|---| +| Progress | Yes / No | Interface for reporting execution progress (percentage, stage) | +| Cancellation | Yes / No | Interface for checking cancellation requests | +| Data | Yes / No | Interface for loading data from external resources | + +### 1.3. Adapters + +| Adapter | Used | Implementation | +|---|---|---| +| UI adapter | Yes / No | Datagrok inputs (`ui.input.*`), buttons, custom HTMLElement | +| Display adapter | Yes / No | Datagrok viewers, custom HTMLElement, docking | +| Worker adapter | Yes / No | Web worker wrapper for core tasks | +| Progress adapter | Yes / No | Datagrok progress bar | +| Data adapter | Yes / No | Loading from a specific resource | + +For custom HTMLElements used as adapters, specify the component ID from section 3 (Custom UI Components). + +### 1.4. Coordinator + +The coordinator connects adapters and the core. Describe: + +- How input changes are listened to (UI adapter). +- Reactivity management strategy (cascading dependencies, see section 9). +- How validation and computations are triggered. +- How control state is managed during computations. +- How computation blocking works (see section 8.4). +- How results are passed to the display adapter. +- Resource lifecycle management (subscriptions, workers — see section 12). + +### 1.5. Independence Principle + +Confirm that input behavior does not depend on the core's computational part. The UI adapter and reactivity form a standalone layer. The core receives a ready, validated set of parameters. + +## 2. Main View + +| Field | Value | +|---|---| +| View type | `DG.TableView` / custom | +| Description | ... | + +## 3. Controls (Inputs) + +### 3.1. Primary Pipeline Controls + +| ID | Label | Control type | Data type | Default | Min | Max | Format | Nullable | Tooltip text | Group | +|---|---|---|---|---|---|---|---|---|---|---| +| ... | ... | `ui.input.int` / ... | `number` / ... | ... | ... | ... | e.g., `0.000` | Yes / No | ... | ... | + +### 3.2. Secondary Task Triggers + +Buttons, icons, menu items that launch secondary pipelines: + +| ID | Label / icon | Launches task | Tooltip text | Availability condition | +|---|---|---|---|---| +| ... | ... | `task_id` | ... | ... | + +### 3.3. Secondary Task Controls + +For each secondary task that has its own UI: + +#### Task controls: `task_id` + +UI type: Datagrok dialog / other. + +| ID | Label | Control type | Data type | Default | Min | Max | Format | Nullable | Tooltip text | +|---|---|---|---|---|---|---|---|---|---| +| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | + +### 3.4. Other Buttons and Actions + +Buttons that are not secondary task triggers (data loading, export, etc.): + +| ID | Label / icon | Action | Tooltip text | Availability condition | +|---|---|---|---|---| +| ... | ... | ... | ... | ... | + +### 3.5. Custom UI Components + +If the application uses custom HTMLElements (controls or display elements), each is described in a separate document — a UI component specification. + +| Component ID | Brief description | Role (control / display / trigger) | UI component specification | +|---|---|---|---| +| ... | ... | ... | Link to separate document (required) | + +**UI component specification** (separate document) must contain: visual description (sketch/mockup), states (normal, hover, disabled, active), events (what it emits on interaction), styles (CSS classes), accessibility (tooltips, aria). + +## 4. Result Display Elements + +### 4.1. Primary Pipeline Display Elements + +| ID | Type | Associated output data (task.parameter) | Docking location | +|---|---|---|---| +| ... | Datagrok viewer (scatter plot / line chart / grid / ...) / custom HTMLElement | ... | ... | + +For custom HTMLElements, specify the component ID from section 3.5. + +### 4.2. Secondary Task Display Elements + +For each secondary task: + +#### Task display: `task_id` + +Where results are displayed: additional viewers in main view / dialog content / separate window. + +| ID | Type | Associated output data | Placement | +|---|---|---|---| +| ... | ... | ... | ... | + +## 5. Layout and UI Element Placement + +### 5.1. Control Placement + +| Area | Content (control IDs) | +|---|---| +| Left panel | ... | +| Right panel | ... | +| Top panel / Ribbon | ... | +| Toolbar | ... | +| Main area | ... | + +### 5.2. Display Element Placement + +| Element ID | Docking area | Position / ratio | +|---|---|---| +| ... | ... | ... | + +### 5.3. Styles + +CSS file: `css/.css` + +**Static styles** (do not change during operation): + +| Element / class | CSS class(es) | Description | +|---|---|---| +| ... | ... | ... | + +**Dynamic styles** (depend on application state — implemented via `classList.toggle/add/remove`): + +| Element / class | CSS class(es) | Condition | Description | +|---|---|---|---| +| ... | ... | ... | ... | + +CSS import: `import '../css/.css'` in the main application file. + +## 6. User Feedback + +### 6.1. Control Tooltips + +| Control ID | Tooltip text | Mechanism | +|---|---|---| +| ... | ... | `tooltipText` property / `ui.tooltip.bind` / `ui.iconFA` third argument | + +### 6.2. Validators as Feedback + +Validators are added to standard Datagrok inputs — they display inline hints about the validity of the current value. + +| Input ID | Validation source | Description | +|---|---|---| +| ... | `validate()` from core / custom | ... | + +### 6.3. Progress Bar + +| Task | Progress bar | Type | Cancellation support | +|---|---|---|---| +| ... | Yes / No | Determinate / indeterminate | Yes / No | + +## 7. Validation + +### 7.1. Primary Pipeline Validation + +#### Complex Validation Rules + +| Rule ID | Condition (invalid combination) | Affected inputs (ID) | Error message | +|---|---|---|---| +| ... | ... | ... | ... | + +#### Validation Order + +``` +1. Rule_A +2. Rule_B (checked only if Rule_A passed) +3. Rule_C +``` + +#### Returned Map Format + +``` +Map +``` + +### 7.2. Secondary Task Validation + +For each secondary task — a similar block. + +#### Task validation: `task_id` + +| Rule ID | Condition | Affected inputs (ID) | Error message | +|---|---|---|---| +| ... | ... | ... | ... | + +Validation order: + +``` +1. ... +``` + +Return format: `{ errors: Map, warning: string | null }` + +## 8. Main Pipeline + +### 8.1. Primary Pipeline + +| Step | Description | +|---|---| +| Parameter input | User sets values through controls → UI adapter converts to typed data | +| Validation | Input set validated by core (section 7) | +| Computation | Validated inputs passed to core task | +| Result display | Results passed to display adapter (section 4) | + +Reactive trigger: ___(e.g., `onValueChanged` with debounce ___ ms)___ + +Error behavior on validation failure: ___(clear results / keep previous / show message)___ + +### 8.2. Secondary Pipelines + +For each secondary task: + +#### Task pipeline: `task_id` + +| Step | Description | +|---|---| +| Trigger | Button / icon / menu item: `control_id` | +| Custom UI | Dialog / panel with own inputs (section 3.3) | +| Validation | Independent validation rules (section 7.2) | +| Computation | Core task execution | +| Result display | Where and how results are shown (section 4.2) | +| Feedback to primary | Values substituted into primary controls: ___ / No | + +### 8.3. Common Pipeline Aspects + +Defined for each pipeline independently: + +#### Control Behavior During Computations + +| Pipeline / task | Controls blocked | Which controls | +|---|---|---| +| task_primary | Yes / No | ... | +| task_secondary_1 | Yes / No | ... | + +#### Computation Error Handling + +| Pipeline / task | Strategy | Notification method | +|---|---|---| +| task_primary | Reset results / keep previous / message | `grok.shell.error` / inline / ... | +| task_secondary_1 | ... | ... | + +### 8.4. Computation Blocking and Batch Input Updates + +| Scenario | Source (task) | Target controls (ID) | Blocked pipelines | +|---|---|---|---| +| ... | `task_id` | ... | Primary / ... | + +Reactivity mode during batch update: + +| Scenario | Reactivity mode | +|---|---| +| ... | Active but computations not triggered / Fully suspended | + +## 9. Reactivity and Dependencies Between Inputs + +### 9.1. Dependency Graph + +| Source (input ID) | Target (input IDs) | Reaction type | Logic | +|---|---|---|---| +| ... | ... | Range / default / availability / option list / label update | ... | + +### 9.2. Debounce / Throttle + +| Input ID | Strategy | Interval (ms) | +|---|---|---| +| ... | debounce / throttle / none | ... | + +## 10. Data Lifecycle + +### 10.1. Data Input + +Primary method: manual input via controls (section 3). + +### 10.2. Loading from Resources + +| Trigger (button ID) | Resource | Format | Mapping to inputs (ID) | +|---|---|---|---| +| ... | File / URL / DB / API | ... | ... | + +## 11. Error Handling Beyond Computations + +| Error type | Strategy | Notification method | +|---|---|---| +| Data loading error | ... | `grok.shell.warning` / `grok.shell.error` / inline | +| Network error | ... | ... | +| Invalid input file | ... | ... | +| Worker creation error | ... | ... | +| Partial worker errors | ... | ... | + +## 12. Subscriptions and Resource Management + +### 12.1. Event Subscriptions + +| Subscription | Event | Cleanup mechanism | +|---|---|---| +| ... | `onValueChanged` / `onAfterDraw` / ... | `sub.unsubscribe()` in cleanup handler | + +All subscriptions must be collected and unsubscribed when the application closes. + +### 12.2. Worker Termination + +| Worker pool | Created in | Termination mechanism | +|---|---|---| +| ... | task / function name | `w.terminate()` in cleanup handler | + +## 13. Application Closure + +On view close, the coordinator performs: + +- [ ] All event subscriptions unsubscribed (section 12.1) +- [ ] All web workers terminated (section 12.2) +- [ ] All associated UI elements closed (including open secondary task dialogs) +- [ ] Pending requests cancelled (debounce timers, in-flight operations) + +Closure handler: ___(e.g., `grok.events.onViewRemoved.subscribe(...)`)___ + +## 14. Accessibility and UX + +### 14.1. Keyboard Shortcuts + +| Combination | Action | +|---|---| +| ... | ... | + +### 14.2. Context Menus + +| Context (element) | Menu items | +|---|---| +| ... | ... | + +### 14.3. Undo / Redo + +Supported: Yes / No. + +If yes — which actions support rollback: ___ + +## 15. Testing + +### 15.1. Computational Part (Core) + +Core correctness verification: unit tests for each computational task separately. The core is tested in isolation — without UI and adapters. + +Test files: + +| File | Categories | Test count | Description | +|---|---|---|---| +| ... | ... | ... | ... | + +Tests are run via `grok test` (entry point: `src/package-test.ts`). + +### 15.2. Inputs + +Input verification for each task: all cases including edge cases. + +| Category | Coverage | Description | +|---|---|---| +| Boundary values | ... | e.g., lower/upper allowed bounds | +| Invalid combinations | ... | e.g., cross-parameter constraints | +| Dependencies between rules | ... | e.g., rule B skipped when rule A fails | +| Multiple simultaneous errors | ... | ... | + +### 15.3. Mathematical Verification + +> Verification criteria are defined by the model specification (section 1.1), not invented during test writing. +> Tests implement what is specified; the specification is the source of truth. + +#### Level 1 Verification (required) + +**Formula/equation verification:** + +| Test category | Test count | What is verified | Reference source | +|---|---|---|---| +| ... | ... | Concrete inputs → expected output for each computational path | Manual calculation / literature | + +**Output property verification:** + +| Test category | Test count | Properties verified | +|---|---|---| +| ... | ... | e.g., bounds, initial conditions, convergence, monotonicity | + +#### Level 2 Verification (for full formalization) + +**Numerical method verification:** + +| Test category | Test count | Reference problems | Tolerance | Source | +|---|---|---|---|---| +| ... | ... | e.g., non-stiff 1D, stiff 1D, stiff 2D | ... | Textbook / paper / test suite | + +**Convergence verification:** + +| Status | Description | +|---|---| +| Implemented / Not yet covered | e.g., solve with two tolerance levels, verify discrepancy decreases | + +**Asymptotic/equilibrium behavior:** + +| Status | Description | +|---|---| +| Implemented / Not yet covered | e.g., p(t_end) → p* for large t_end | diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/datagrok-interactive-app-guide.md b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/datagrok-interactive-app-guide.md new file mode 100644 index 0000000000..85faf262be --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/datagrok-interactive-app-guide.md @@ -0,0 +1,511 @@ +# Guide: Building Interactive Scientific Web Applications on Datagrok + +> **End-to-end example:** application **Levins Metapopulation Model** (see `example/` directory). +> All file references are relative to the `guides/` directory. +> Application specification: `example/levins-metapopulation-spec.md`. + +## 1. General Architecture + +An application is a Datagrok package function registered with the `//tags: app` comment. + +► **Implementation:** `example/code/src/package.ts` — function `levinsMetapopulationModelApp`, registered via `//name: Levins Metapopulation Model` / `//tags: app`. Delegates logic to `example/code/src/levins/app.ts` → `levinsMetapopulationApp()`. + +The application architecture follows the "ports and adapters" pattern (hexagonal architecture). The application consists of three layers and a coordinator. + +### 1.1. Core + +Computation logic. The core knows nothing about the UI and does not depend on how data is obtained or how results are displayed. + +The core contains one or more computational tasks. Each task is a self-contained unit of computation with its own inputs and outputs. For each task, the following are defined independently: + +- Input parameters and their types. +- Output data and their format. +- Synchronicity: synchronous or asynchronous. +- Execution environment: main thread or web worker. +- Parallelization: whether parallelization is possible and the strategy. +- Complex validation logic for input parameters. + +► **Implementation:** `example/code/src/levins/core.ts` — core with two tasks: +- **`task_primary`** (synchronous, main thread) — `solve(inputs)` solves the Levins model ODE and returns the trajectory `p(t)`. +- **`task_optimize`** (asynchronous, web workers) — searches for the optimal `m` across 10,000 points, executed in parallel in a worker pool. + +Examples of computational tasks within a single application: + +- **Primary task** — solving the ODE based on model parameters. Triggered reactively when inputs change. +- **Secondary task** — model sensitivity analysis. Triggered by an explicit user action (button), has its own set of parameters. + +Tasks can be independent or linked (the secondary task uses results from the primary one). + +► **Implementation:** `task_optimize` does not depend on the results of `task_primary`, but uses the current input values of the primary pipeline (snapshot). After optimization completes, the result is written to the `ctrl_m` control via batch update, which triggers a single run of `task_primary`. + +#### Computation Formulas and Model + +Each computational task is based on a defined transformation of inputs into outputs — from a single formula to a complex system of equations. In this guide, the term "model" refers to any such definition: individual formulas, chains of transformations, ODE/PDE systems, optimization problems, or statistical procedures. The model exists independently of its implementation (custom code, external library, or platform API). The model definition is the primary source of verification criteria: what cannot be defined cannot be verified. + +The model definition has two levels. + +**Level 1 — required minimum (before implementation):** + +- **Variables:** name, meaning, units of measurement, valid domain (e.g., `p ∈ (0, 1]`, `m > 0`). +- **Relationships:** equations, recurrences, algorithmic steps that connect inputs to outputs. The notation must be unambiguous — another developer must be able to independently implement the same computation from this description. +- **Output properties:** constraints that must hold on the result — bounds, monotonicity, symmetry, conservation laws, limiting/degenerate cases. These properties directly become verification tests. +- **Reference examples:** for each computational path (each mode, branch, or regime of the model), at least one concrete input → expected output pair with the source (manual calculation, literature, reference implementation). + +**Level 2 — full formalization (can be developed incrementally alongside the implementation):** + +- **Complete mathematical formulation:** equations, initial/boundary conditions, parameterization. For ODE/PDE — the system in explicit form. +- **Analytical properties:** equilibria, asymptotic behavior, stability conditions, bifurcation points. +- **Numerical method justification:** why this particular method is chosen, its properties (stability, order of accuracy, applicability to stiff/non-stiff problems), reference to literature or documentation. +Both Level 1 and Level 2 content can be placed directly in the main application specification or extracted into a separate model specification document — depending on complexity. For simple models (a few formulas), the application specification is sufficient. For complex models (multi-step pipelines, multiple computational paths, extensive reference data), a separate document avoids cluttering the main specification. The main application specification then includes a brief model description and a link to the model specification. + +Level 2 need not be complete before implementation begins, but must be complete before the computational part is considered verified. + +► **Implementation:** +- **Level 1** is defined in the application specification (`example/levins-metapopulation-spec.md`): variables `p, m, e₀` with units and domains; ODE `dp/dt = m·p·(1−p) − e(p)·p` with two modes (`e = e₀` and `e = e₀·(1−p)`); output property `p(t) ∈ [0, 1]`; equilibrium `p* = 1 − e₀/m`; reference examples for each mode verified in tests (`Math: Levins func` — 5 tests covering base model and rescue effect at specific `p` values). +- **Level 2:** equilibrium analysis, MRT method justification (A-stable implicit method suitable for stiff ODEs), convergence properties — documented in the specification. MRT solver verified against analytical solutions in `Math: MRT solver` tests (non-stiff and stiff reference problems from Chapra & Canale). + +#### Computation Implementation + +Computations for each task are implemented using one or a combination of the following approaches: + +- **Datagrok API computation methods.** The core can use computation methods provided by the Datagrok API. Available only on the main thread — the Datagrok API is not available in web workers. + +- **External libraries.** Computations are performed using third-party libraries. At the specification stage, the following is determined: which specific libraries are used, which versions, which functions/methods are applied, and a link to the library documentation (API reference, README, or guide). A documentation link is mandatory — without it, it is impossible to correctly implement and verify the calls. The order of library usage (which calls, in what sequence, with what parameters) is either defined in the specification or deferred to a separate agreement — if the usage approach is non-trivial or allows for variations. Additionally, for libraries that implement numerical methods (solvers, optimizers, fitting): the specification states which properties of the method are relevant (stability, order of accuracy, applicability class), expected accuracy for the application's use case, and the verification strategy — how the library's results will be validated (reference problems with known solutions, comparison with an alternative implementation, etc.). See section 15.3. + +- **Custom methods.** Computations are implemented within the application. Each custom method is described in a separate document — a method specification. The method specification contains: mathematical formulation (formulas, equations), step-by-step algorithm, input and output data, constraints and assumptions, edge cases, and references to literature. The main application specification includes a brief method description and a link to the method specification document. Additionally, the method specification defines expected accuracy and the verification strategy: reference examples with expected outputs and their sources (manual calculation, literature, reference implementation). See section 15.3. + +A single task can combine multiple approaches — for example, a custom method for data preprocessing, an external library for numerical solution, and a Datagrok computation method for postprocessing. + +► **Implementation:** +- `task_primary` combines an **external library** (`diff-grok`, function `mrt`) and a **custom method** (`getEquilibrium` in `example/code/src/levins/model.ts`). +- `task_optimize` uses an **external library** (`diff-grok`, `mrt`) inside workers (`example/code/src/levins/optimize-worker.ts`) and a **custom method** (finding the maximum `p_end` in `example/code/src/levins/app.ts`, lines 421–425). +- The `mrt` call is wrapped in `createLevinsODE()` (`example/code/src/levins/model.ts`), which allows reusing the ODE specification in both the main thread and workers. + +Execution environment constraint: tasks that use Datagrok API computation methods can only run on the main thread. Tasks that use only external libraries and custom methods can run on either the main thread or in a web worker. + +► **Implementation:** `task_primary` runs on the main thread (synchronous, < 100 ms). `task_optimize` is distributed across workers — `diff-grok` does not depend on the Datagrok API. + +General core properties: + +- Does not depend on how data is obtained or how results are displayed. +- Easily testable in isolation — each task is tested separately. + +► **Implementation:** `example/code/src/levins/core.ts` does not import `datagrok-api` or `ui` — only `diff-grok` and `./model`. Core tests in `example/code/src/tests/levins-api-tests.ts` test `validate`, `solve`, `validateOptimize`, `getEquilibrium` without UI. + +Computation core implementation references: patterns for working with raw data and null handling (`reference/COMPUTATION-PATTERNS.md`), efficient typed array operations (`reference/ARRAY-OPERATIONS.md`). + +### 1.2. Ports + +Interfaces through which the core communicates with the outside world. Ports contain no implementation — only contracts. Each computational task of the core has its own set of ports: + +- **Input port** — describes what parameters and types the task expects. +- **Output port** — describes the format of the task's results. +- **Progress port** — interface for reporting execution progress (percentage, stage). +- **Cancellation port** — interface for checking whether the user has requested cancellation. + +Additionally, at the application level: + +- **Data port** — interface for loading data from external resources. + +► **Implementation:** +- **Input port `task_primary`:** interface `LevinsParams` (`example/code/src/levins/model.ts`, line 6) — `{ p0, m, e0, rescueEffect, t_start, t_end, t_step, tolerance }`. +- **Output port `task_primary`:** interface `LevinsSolution` (`example/code/src/levins/core.ts`, line 14) — `{ t: Float64Array, p: Float64Array, p_star: number }`. +- **Input port `task_optimize`:** interface `WorkerTask` (`example/code/src/levins/core.ts`, line 129) — data passed to the worker via `postMessage`. +- **Output port `task_optimize`:** interface `WorkerResult` (`example/code/src/levins/core.ts`, line 140) — `{ m_i, p_end, error? }`. +- **Progress port:** in `task_optimize` implemented via `DG.TaskBarProgressIndicator` (`example/code/src/levins/app.ts`, line 302). Progress is updated upon each completed worker. +- **Cancellation port:** `canceled` flag (`example/code/src/levins/app.ts`, line 303), checked before sending the next task to a worker. +- **Data port:** not used (the application does not load external data). + +### 1.3. Adapters + +Concrete implementations of ports for the Datagrok environment. + +- **UI adapter** — Datagrok inputs (`ui.input.*`), buttons, custom `HTMLElement`. Converts user input into typed data for the task's input port. +- **Display adapter** — Datagrok viewers, custom `HTMLElement`, docking to the main view. Receives data from the task's output port and visualizes it. +- **Worker adapter** — wrapper for running the core in a web worker. Implements input and output ports via `postMessage`. Implementation references: worker-utils infrastructure and lifecycle (`reference/WORKER-GUIDE.md`), distribution across multiple workers (`reference/PARALLEL-EXECUTION.md`). +- **Progress adapter** — Datagrok progress bar. Implements the progress port. +- **Data adapter** — loading from a specific resource. Which resource and which mechanism — defined by the specification. + +► **Implementation in `example/code/src/levins/app.ts`:** +- **UI adapter:** controls `ctrlP0`, `ctrlM`, `ctrlE0`, `ctrlRescue`, `ctrlTStart`, `ctrlTEnd`, `ctrlTStep`, `ctrlTolerance` (lines 43–99). Function `getInputs()` (line 154) converts control state to `LevinsParams`. +- **Display adapter:** `updateDataFrame()` (line 265) updates the `DG.DataFrame` and the `line chart` viewer; `updateColorCoding()` (line 188) sets conditional color coding for the `p` column. +- **Worker adapter:** `example/code/src/levins/optimize-worker.ts` — the worker receives `WorkerTask` via `onmessage`, calls `mrt(createLevinsODE(...))`, returns `WorkerResult`. The worker pool is created in `runOptimization()` (line 292). +- **Progress adapter:** `DG.TaskBarProgressIndicator.create('Optimizing m...')` (line 302), updated via `pi.update(...)` (line 365). +- **Data adapter:** not used. + +### 1.4. Coordinator (Application Service) + +The coordinator connects adapters and the core. It: + +- Listens for input changes via the UI adapter. +- Manages reactivity: cascading dependencies between inputs, range updates, defaults, availability — based on the specification. +- Triggers validation and computations through the corresponding ports. +- Manages control state (enabled/disabled) during computations. +- Manages computation blocking: can suspend reactive pipeline execution during batch input updates (see section 8.4). +- Passes results to the display adapter. +- Manages resource lifecycle (subscriptions, workers). + +► **Implementation:** function `levinsMetapopulationApp()` in `example/code/src/levins/app.ts` fulfills all coordinator roles: +- Listens to `onValueChanged` via input callbacks (lines 43–99). +- Reactivity: `updateRhoBadge()` (line 124), `updateRescueLabel()` (line 136). +- Validation and computation: `runPrimary()` (line 226). +- Blocking: `computationsBlocked` flag (line 19), used during input formatting (lines 102–110), during reset (lines 506–518), and after optimization (lines 428–437). +- Lifecycle: `subs[]` (line 21), `activeWorkers[]` (line 22), cleanup in `onViewRemoved` (line 561). + +### 1.5. Independence Principle + +Input behavior does not depend on the core's computational part. The UI adapter and reactivity between inputs form a standalone layer managed by the coordinator based on the specification. The core receives a ready, validated set of parameters. + +► **Implementation:** the function `runPrimary()` first calls `validate(inputs)` from the core, and only if `errors.size === 0` passes the data to `solve(inputs)`. The core (`core.ts`) is unaware of the inputs' existence — it accepts a plain `LevinsParams`. + +## 2. Main View + +The application has a main view. If a scientific application is based on a table, the main view should be `DG.TableView`. + +► **Implementation:** `example/code/src/levins/app.ts`, line 33: `const view = grok.shell.addTableView(df)`. + +## 3. Controls (Inputs) + +Users set input parameters for computations through controls. Control types: + +- **Standard Datagrok inputs** — created via `ui.input.*`. +- **Datagrok buttons** — perform a specified action when clicked. +- **Custom HTMLElement** — for example, a `div` element with specific styles that performs an action when clicked. Each custom element is described in a separate document — a UI component specification. The UI component specification contains: visual description (sketch/mockup), states (normal, hover, disabled, active), events (what it emits on interaction), styles (CSS classes), accessibility (tooltips, aria). The main application specification includes a brief element description and a link to the UI component specification document. + +► **Implementation:** +- **Standard inputs:** 7 numeric + 1 toggle in `example/code/src/levins/app.ts`, lines 43–99 (`ctrlP0`, `ctrlM`, `ctrlE0`, `ctrlRescue`, `ctrlTStart`, `ctrlTEnd`, `ctrlTStep`, `ctrlTolerance`). +- **Buttons:** `optimizeBtn = ui.iconFA('search', ...)` (line 503), `resetBtn = ui.iconFA('undo', ...)` (line 505). +- **Custom HTMLElement:** `rhoBadge = ui.div([], 'd4-tag levins-rho-badge')` (line 37) — a rho = e0/m indicator with dynamic color switching via CSS classes. + +### 3.1. Input Options + +When creating standard Datagrok inputs (`ui.input.*`), the specification defines options for each input. These options are passed during input creation and control its behavior: + +- **Min** — minimum allowed value. For numeric inputs, sets the lower bound. +- **Max** — maximum allowed value. For numeric inputs, sets the upper bound. +- **Format** — value display format (e.g., `0.000` for three decimal places, `0.0` for one, `0.##E+0` for scientific notation). The format determines how the value is displayed in the input. + +Input options differ from validation (section 7): options set basic control-level constraints (range, format), while validation checks complex conditions across combinations of multiple input values. + +► **Implementation:** min/max are set during input creation (e.g., `ctrlP0`: `min: 0.001, max: 1`). Formats are set in a separate block (lines 102–109), wrapped in `computationsBlocked = true/false` — so that format assignment does not trigger a side-effect recomputation. + +### 3.2. Control Classification + +Controls are classified by ownership: + +- **Main view controls** — inputs of the primary pipeline, placed in the main application interface. +- **Secondary task triggers** — buttons or icons that launch secondary pipelines (see section 8.2). +- **Secondary task controls** — inputs placed in the secondary task's own UI (e.g., in a dialog). + +► **Implementation:** +- **Main view controls:** `ctrlP0`…`ctrlTolerance` — in the left panel form. +- **Secondary task trigger:** `optimizeBtn` (`ui.iconFA('search')`, line 503) → opens the optimization dialog. +- **Secondary task controls:** `dlgMMin`, `dlgMMax` — dialog inputs in `showOptimizeDialog()` (lines 448–499). + +## 4. Result Display Elements + +Computation results are displayed using: + +- **Standard Datagrok viewers.** +- **Custom HTMLElement.** Each custom display element is described in a separate UI component specification (similar to custom controls, see section 3). + +By default, these elements are docked to the main view. + +► **Implementation:** +- **Viewer:** `line chart` — `view.addViewer('Line chart', {...})` (line 546), docked to the right of the grid. +- **Custom element:** `rhoBadge` — displays the current rho value with color indication (green/red). +- **Color coding of column `p`:** `updateColorCoding()` (line 188) sets conditional colors (green — persistence zone, red — extinction threat) with dynamic threshold recalculation `e0/m`. +- **Column `p` header tooltip:** `setupGridTooltip()` (line 202) via `view.grid.onCellTooltip`. + +## 5. Layout and UI Element Placement + +The placement of controls and display elements (panels, ribbon, toolbar, side panels, docking area) is defined by the application specification. + +► **Implementation (`example/code/src/levins/app.ts`):** +- **Left panel:** `ui.form` with groups via `ui.h2` (lines 523–540), docked as `DG.DOCK_TYPE.LEFT`, ratio `0.2` (line 543). +- **Toolbar (ribbon):** `view.setRibbonPanels([[optimizeBtn, resetBtn]])` (line 520). +- **Main area:** `DG.TableView` (grid) — default. +- **Right area:** `line chart`, docked `DG.DOCK_TYPE.RIGHT` relative to the grid, ratio `0.5` (lines 552–554). + +## 5.1. Styles + +All visual styles of the application are placed in a separate CSS file (`css/.css`). Inline styles in TypeScript code are not allowed — CSS classes are used instead. + +- **Static styles** — element styling that does not change during operation. Set via CSS class when creating the element. +- **Dynamic styles** — styles that depend on application state (e.g., indicator color, button activity). Implemented via CSS class toggling (`classList.toggle`, `classList.add/remove`), not via direct `element.style.*` assignment. + +The CSS file is imported via ES import (`import '../css/.css'`). Webpack with `style-loader` + `css-loader` injects styles into the DOM when the bundle loads. + +► **Implementation:** +- CSS file: `example/code/css/levins.css` — contains classes `.levins-rho-badge`, `.levins-rho-badge--persists`, `.levins-rho-badge--extinct`, `.levins-btn--disabled`. +- Import: `import '../../css/levins.css'` (`example/code/src/levins/app.ts`, line 12). +- **Static styles:** `rhoBadge` is created with classes `'d4-tag levins-rho-badge'` (line 37). +- **Dynamic styles:** `rhoBadge.classList.toggle('levins-rho-badge--persists', persists)` (line 130); `optimizeBtn.classList.toggle('levins-btn--disabled', !enabled)` (line 289). + +## 6. User Feedback + +### 6.1. Control Tooltips + +- For standard Datagrok inputs, tooltips are defined at input creation time via the `tooltipText` property. +- For elements that are not Datagrok inputs, tooltips are bound via `ui.tooltip.bind`. + +► **Implementation:** +- All inputs have `tooltipText` (e.g., `ctrlP0`: `tooltipText: 'Fraction of patches occupied at t=0...'`, line 46). +- `rhoBadge`: `ui.tooltip.bind(rhoBadge, '...')` (line 38). +- Buttons `optimizeBtn` and `resetBtn`: tooltip is passed as the third argument of `ui.iconFA` (lines 503, 518). + +### 6.2. Validators as Feedback + +Validators are added to standard Datagrok inputs — they display inline hints about the validity of the current value. + +► **Implementation:** function `addValidators()` (`example/code/src/levins/app.ts`, line 168) adds a validator to each input. The validator calls `validate(getInputs())` from the core and returns an error for the specific `InputId`. + +### 6.3. Progress Bar + +During long computations, the standard Datagrok progress bar indicator is displayed with cancellation support. + +► **Implementation:** `task_optimize` uses `DG.TaskBarProgressIndicator.create('Optimizing m...')` (line 302), updated via `pi.update(percent, label)` upon each completed worker (line 365). + +## 7. Validation + +Validation is performed through the Datagrok validator mechanism. Each computational task has its own validation rules. + +### 7.1. Complex Validation + +Validation is complex: the entire set of task inputs is analyzed as a whole. If a combination of values is invalid, a `Map` with error specifications is returned. This specification is then used by the Datagrok validator mechanism to display errors on the corresponding inputs. + +► **Implementation:** +- `task_primary`: function `validate(inputs: LevinsParams): ValidationErrors` (`example/code/src/levins/core.ts`, line 40) — returns `Map`. Rules val_01…val_09 check both individual values and combinations (e.g., val_05: `m <= e0` when `rescueEffect = false`; val_08: `t_step >= t_end - t_start`). +- `task_optimize`: function `validateOptimize(opt, e0, rescueEffect)` (`example/code/src/levins/core.ts`, line 106) — returns `{ errors: Map, warning: string | null }`. Rules opt_val_01…opt_val_04. + +### 7.2. Validation Order + +The order of determining input set validity is defined by the specification for each task. + +► **Implementation:** in `validate()`, combinatorial rules are checked only after basic rules pass: +- val_05 is checked only if val_03 and val_04 passed (line 59: `if (!errors.has('ctrl_m') && !errors.has('ctrl_e0') && ...)`). +- val_08 is checked only if val_06 and val_07 passed (line 73: `if (!errors.has('ctrl_t_end') && !errors.has('ctrl_t_step') && ...)`). + +## 8. Main Pipeline + +Each computational task of the core has its own pipeline. All pipelines are orchestrated by the coordinator (see section 1.4). + +### 8.1. Primary Pipeline + +The primary pipeline is bound to the main application controls and operates reactively. + +**Parameter input.** The user sets input values through controls. The UI adapter converts the input into typed data for the task's input port. + +**Validation.** The input set is validated (see section 7). Validation is performed by the core through the input port. + +**Computation.** Validated inputs are passed to the core task. Execution characteristics (synchronicity, environment, parallelization) are defined by the task specification (see section 1.1). + +**Result display.** The core returns results through the task's output port. The coordinator passes them to the display adapter (see section 4). + +► **Implementation:** function `runPrimary()` (`example/code/src/levins/app.ts`, line 226): +1. Checks `computationsBlocked` (line 227). +2. Collects inputs: `getInputs()` (line 230). +3. Validates: `validate(inputs)` (line 231). +4. On errors — visually marks invalid inputs (`d4-invalid`) and calls `clearResults()` (lines 237–245). +5. On success — `solve(inputs)` (line 248), then `updateDataFrame(result)` + `updateColorCoding()` (lines 249–250). + +Reactive trigger: each input has `onValueChanged: () => debouncedRun()` (debounce 50 ms, line 258). + +### 8.2. Secondary Pipelines + +A secondary pipeline is bound to an explicit user action (button, icon, menu item) and runs on demand. + +**Trigger.** The user initiates an action (e.g., clicks the "Sensitivity Analysis" button). + +**Custom UI.** A secondary pipeline may have its own parameter input interface — for example, a Datagrok dialog with its own set of inputs, validation, and tooltips. This UI is defined by the secondary task specification. + +**Validation.** Secondary task parameters are validated independently — using their own complex validation rules. + +**Computation.** The core task is executed. The secondary task may use primary task results as part of its input data. + +**Result display.** Secondary task results are displayed with their own elements — these can be additional viewers docked to the main view, dialog content, or a separate window. Defined by the specification. + +**Feedback to primary pipeline.** The secondary task may return values that are substituted into primary pipeline controls. In this case, the computation blocking and batch update mechanism is used (see section 8.4). + +► **Implementation of `task_optimize`:** +1. **Trigger:** `optimizeBtn` → `showOptimizeDialog()` (line 503). +2. **Custom UI:** dialog `ui.dialog('Find optimal m')` with inputs `dlgMMin`, `dlgMMax` (lines 448–499), with its own validators and tooltips. +3. **Validation:** `validateDialog()` (line 463) calls `validateOptimize()` from the core. +4. **Computation:** `runOptimization(mMin, mMax)` (line 292) — creates a worker pool, distributes 10,000 tasks, collects results. +5. **Display:** `grok.shell.info(...)` with the found optimum (line 433). +6. **Feedback:** `ctrlM.value = best.m_i` via batch update (lines 428–433). + +### 8.3. Common Pipeline Aspects + +The following aspects are defined by the specification for each pipeline independently: + +#### Control Behavior During Computations + +Controls may become disabled during computations — which ones and for which pipeline is defined by the specification. + +► **Implementation:** `task_primary` does not block controls (< 100 ms). `task_optimize` blocks only `optimizeBtn` via `setOptimizeBtnEnabled(false)` (lines 288–289, 300). + +#### Progress and Cancellation + +During long computations, the standard Datagrok progress bar is displayed with cancellation support. + +► **Implementation:** `task_primary` — no progress bar. `task_optimize` — `DG.TaskBarProgressIndicator` with completion percentage and cancellation via the `canceled` flag (line 303). + +#### Computation Error Handling + +The course of action upon computation failure is defined by the specification for each task. + +► **Implementation:** +- `task_primary`: `catch` → `clearResults()` + `grok.shell.error(msg)` (lines 251–255). +- `task_optimize`: partial errors (some workers failed) → `grok.shell.warning(...)` (line 413); complete failure → `grok.shell.error(...)` (line 416); error writing to control → `grok.shell.warning(...)` with instructions to enter manually (line 436). + +### 8.4. Computation Blocking and Batch Input Updates + +In certain scenarios, the result of a secondary task must be substituted into primary pipeline controls. In this case, each individual input change should not trigger a reactive recomputation — the computation should happen once, after all values have been substituted. + +The coordinator supports a computation blocking mode: + +**Block request.** Before batch update, the coordinator suspends reactive execution of specified pipelines. Which pipelines are blocked is defined by the specification. + +**Batch update.** Values are substituted into controls. Cascading dependencies between inputs (reactivity, section 9) can be handled in one of two modes — defined by the specification: + +- Reactivity between inputs works as usual, but computations are not triggered. +- Reactivity between inputs is also suspended until the batch update completes. + +**Unblock.** After all values are substituted, the coordinator removes the block. Full input set validation occurs, then computation runs, then results are displayed — the standard pipeline (section 8.1). + +Example: the user launches parameter optimization (secondary task). Upon optimization completion, the coordinator blocks the primary pipeline, substitutes the found parameter values into all controls, removes the block — the primary pipeline runs once for the complete set of optimal parameters. + +► **Implementation:** the `computationsBlocked` flag (`example/code/src/levins/app.ts`, line 19) is used in three scenarios: +1. **Format initialization** (lines 102–110): blocking prevents side-effect recomputations during `format` assignment. +2. **Reset** (lines 506–518): `computationsBlocked = true` → reset all values → `computationsBlocked = false` → `runPrimary()`. +3. **Optimization result** (lines 428–437): `computationsBlocked = true` → `ctrlM.value = best.m_i` → `computationsBlocked = false` → `runPrimary()`. + +Blocking check: `runPrimary()` first checks `if (computationsBlocked) return` (line 227). + +## 9. Reactivity and Dependencies Between Inputs + +Dependencies between inputs (cascading updates of ranges, defaults, availability) are defined by the application specification. Reactivity is managed by the coordinator (see section 1.4) and operates entirely at the UI adapter level — independent of the core's computational part. + +Reactivity can be temporarily suspended by the coordinator in batch input update mode (see section 8.4). + +► **Implementation:** +- **`ctrl_m` / `ctrl_e0` → `rhoBadge`:** `updateRhoBadge()` (line 124) is called from `onValueChanged` of both inputs. Recalculates `rho = e0/m`, updates text and CSS class. +- **`ctrl_rescue` → `ctrl_e0` (label + tooltip):** `updateRescueLabel()` (line 136) switches caption and tooltip based on the toggle state. +- **`ctrl_t_start` / `ctrl_t_end` → ranges:** `updateArgRanges()` (line 149) — scaffold for range updates; actual checking via complex validation (val_06, val_07, val_08). +- **Debounce:** numeric inputs use `debouncedRun()` (debounce 50 ms, line 258), toggle `ctrl_rescue` calls `runPrimary()` directly (without debounce). + +## 10. Data Lifecycle + +### 10.1. Data Input + +The primary approach is manual entry through application controls. + +► **Implementation:** all model parameters are entered by the user through the form in the left panel. The initial state uses default values from `DEFAULTS` (`example/code/src/levins/core.ts`, line 27). `task_primary` runs automatically on initialization: `solve(DEFAULTS)` (line 26). + +### 10.2. Loading from a Resource + +Via buttons or icons — loading data from an external resource. Which specific resource and loading mechanism is defined by the application specification. + +► **Implementation:** not used in this application. + +## 11. Error Handling Beyond Computations + +The strategy for handling data loading errors, network errors, invalid input files, and incorrect application state is defined by the application specification. + +► **Implementation:** see specification (`example/levins-metapopulation-spec.md`, section 11). Examples: +- Worker creation error: `reject(new Error('Failed to start parallel computations...'))` (line 380). +- Partial worker errors: `errorCount` counting and `grok.shell.warning(...)` (lines 305, 412–413). +- Error writing to control: fallback with `grok.shell.warning(...)` (line 436). + +## 12. Subscriptions and Resource Management + +### 12.1. Event Subscriptions + +All Datagrok event subscriptions (`onValueChanged`, `onAfterDraw`, etc.) must be collected and unsubscribed when the application closes via `sub.unsubscribe()`. + +► **Implementation:** array `subs` (`example/code/src/levins/app.ts`, line 21) collects subscriptions. On close — `for (const sub of subs) sub.unsubscribe()` (line 564). + +### 12.2. Worker Termination + +When the application closes, all web workers must be properly terminated. + +► **Implementation:** array `activeWorkers` (line 22), function `terminateWorkers()` (line 440) calls `w.terminate()` for each worker. Called on close (line 563) and after optimization completes (line 405). + +## 13. Application Closure + +When the application closes, the coordinator performs: + +- All event subscriptions are unsubscribed — for both primary and secondary pipelines (see section 12.1). +- All web workers are terminated — including secondary task workers (see section 12.2). +- All associated UI elements are closed (including open secondary task dialogs). +- Pending requests are cancelled. + +► **Implementation:** handler `grok.events.onViewRemoved.subscribe(...)` (`example/code/src/levins/app.ts`, lines 561–568): +1. `terminateWorkers()` — terminates all active workers. +2. `for (const sub of subs) sub.unsubscribe()` — unsubscribes subscriptions. +3. `clearTimeout(debounceTimer)` — cancels pending debounce. + +## 14. Accessibility and UX + +Keyboard shortcuts, context menus, undo/redo, and other UX elements are defined by the application specification. + +► **Implementation:** in the current version of the application, keyboard shortcuts, context menus, and undo/redo are not implemented (see specification, section 12). + +## 15. Testing + +### 15.1. Computational Part (Core) + +Core correctness verification: unit tests for each computational task separately. The core is tested in isolation — without UI and adapters. + +► **Implementation:** tests are split across two files: + +**`example/code/src/tests/levins-api-tests.ts`** — 2 categories: +- **API: Validation** — 24 tests: val_01…val_09 + boundary values + dependency order + multiple errors + defaults. +- **API: Optimization Validation** — 7 tests: opt_val_01…opt_val_04 + valid input data. + +**`example/code/src/tests/levins-math-tests.ts`** — 4 categories: +- **Math: MRT solver** — 3 tests: non-stiff 1D, stiff 1D, stiff 2D (van der Pol) — verifying the `mrt` solver against analytical/reference solutions. +- **Math: Levins func** — 5 tests: ODE right-hand side correctness for the base model and rescue effect at specific `p` values. +- **Math: Equilibrium** — 4 tests: `getEquilibrium` for the base model and rescue effect. +- **Math: Solve output properties** — 8 tests: output invariants of `solve()` — bounds `p ∈ [0, 1]`, initial conditions, convergence to `p*`, monotonicity in `m`. + +Tests are run via `grok test` (entry point: `example/code/src/package-test.ts`). + +### 15.2. Inputs + +Input verification for each task: all cases including edge cases (boundary values, invalid combinations, empty values, extreme values). + +► **Implementation:** validation tests cover: +- Boundary values: `p0 = 0.001` (lower allowed bound), `p0 = 1` (upper). +- Invalid combinations: `m <= e0` without rescue, `t_step >= t_end - t_start`. +- Dependencies between rules: val_05 is skipped when val_03 fails, val_08 is skipped when val_06/val_07 fail. +- Multiple errors: simultaneously invalid `p0`, `m`, `e0`, `t_step`, `tolerance`. + +### 15.3. Mathematical Verification + +Verification that the implemented computation matches the model definition (see section 1.1, "Computation Formulas and Model"). Test categories correspond to the two levels of the model definition. Verification criteria — reference examples, expected accuracies, output property constraints, reference problems for the numerical method — are defined by the model specification, not invented during test writing. Tests implement what is specified; the specification is the source of truth. + +#### Level 1 verification (required) + +**Formula/equation verification.** The implemented transformation is checked at control points with manually computed expected values. For each computational path (mode, branch, regime), at least one test substitutes concrete inputs and compares the output against a hand-calculated result. + +**Output property verification.** Constraints declared in the model definition (bounds, monotonicity, conservation laws, limiting cases) are checked on actual computation results. These tests do not compare against a specific expected value — they verify that the result satisfies a declared invariant. + +► **Implementation:** +- **Formula verification:** `Math: Levins func` — 5 tests. Each test substitutes specific `(m, e₀, p)` into the ODE right-hand side and compares `dp/dt` against a hand-calculated value (e.g., `func_01`: `dp/dt = 0.5·0.5·0.5 − 0.2·0.5 = 0.025`). Both computational paths are covered: base model (3 tests) and rescue effect (2 tests). +- **Equilibrium verification:** `Math: Equilibrium` — 4 tests. `getEquilibrium` is checked against the analytical formula `p* = 1 − e₀/m` for the base model, and `NaN` for the rescue effect (no closed-form equilibrium). +- **Output properties:** `Math: Solve output properties` — 8 tests. Verifies invariants declared in the specification: `p(t) ∈ [0, 1]` (solve_02, solve_06), `p(0) = p0` (solve_03, solve_08), convergence to `p*` (solve_05), monotonicity in `m` (solve_07), non-empty output arrays (solve_01), `t[0] = t_start` (solve_04). + +#### Level 2 verification (for full formalization) + +**Numerical method verification.** The solver (or library) is tested on reference problems with known analytical solutions. The test verifies that the numerical error stays within the expected tolerance. Reference problems should cover the solver's applicability range (e.g., non-stiff and stiff problems for an ODE solver). Each reference problem must cite its source (textbook, paper, test suite). + +**Convergence verification.** Solving the same problem with decreasing step size or tolerance produces solutions that converge. The test compares solutions at two different precision levels and verifies that the discrepancy decreases. + +**Asymptotic/equilibrium behavior.** The numerical solution on a sufficiently long interval approaches the analytically predicted equilibrium or asymptote. + +► **Implementation:** +- **Numerical method:** `Math: MRT solver` — 3 tests. Non-stiff 1D and stiff 1D problems verified against analytical solutions (Chapra & Canale, pp. 736, 767). Stiff 2D van der Pol (µ=1000) verified for solver stability (reference: VDPOL test set). Tolerance threshold: max absolute error < 0.1. +- **Convergence:** not yet covered. Candidate: solve the Levins ODE with `tolerance = 1e-5` and `tolerance = 1e-9`, verify that the discrepancy between solutions decreases. +- **Asymptotic behavior:** not yet covered. Candidate: verify that `p(t_end)` approaches `p* = 1 − e₀/m` for sufficiently large `t_end`. diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/css/levins.css b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/css/levins.css new file mode 100644 index 0000000000..2c73271d32 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/css/levins.css @@ -0,0 +1,25 @@ +/* Levins Metapopulation Model — Application Styles */ + +/* Rho badge (e0/m ratio indicator) */ +.levins-rho-badge { + font-size: 13px; + padding: 4px 8px; + border-radius: 4px; + display: inline-block; + margin-top: 4px; + color: white; +} + +.levins-rho-badge--persists { + background-color: #4CAF50; +} + +.levins-rho-badge--extinct { + background-color: #F44336; +} + +/* Disabled icon button (ui.iconFA) */ +.levins-btn--disabled { + pointer-events: none; + opacity: 0.4; +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/detectors.js b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/detectors.js new file mode 100644 index 0000000000..f21cf90325 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/detectors.js @@ -0,0 +1,9 @@ +/** + * The class contains semantic type detectors. + * Detectors are functions tagged with `DG.FUNC_TYPES.SEM_TYPE_DETECTOR`. + * See also: https://datagrok.ai/help/develop/how-to/define-semantic-type-detectors + * The class name is comprised of and the `PackageDetectors` suffix. + * Follow this naming convention to ensure that your detectors are properly loaded. + */ +class InteractiveSciAppTestPackageDetectors extends DG.Package { +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/package.json b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/package.json new file mode 100644 index 0000000000..592ab1df94 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/package.json @@ -0,0 +1,49 @@ +{ + "name": "interactivesciapptest", + "friendlyName": "InteractiveSciAppTest", + "version": "0.0.1", + "description": "InteractiveSciAppTest package", + "author": { + "name": "CC", + "email": "vmakarichev@datagrok.ai" + }, + "dependencies": { + "@datagrok-libraries/utils": "^4.6.5", + "cash-dom": "^8.1.5", + "datagrok-api": "^1.26.0", + "dayjs": "^1.11.13", + "diff-grok": "^1.2.0" + }, + "devDependencies": { + "css-loader": "^7.1.2", + "style-loader": "^4.0.0", + "ts-loader": "latest", + "typescript": "latest", + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4" + }, + "scripts": { + "debug-interactivesciapptest": "webpack && grok publish", + "release-interactivesciapptest": "webpack && grok publish --release", + "build-interactivesciapptest": "webpack", + "build": "grok api && grok check --soft && webpack", + "test": "grok test", + "debug-interactivesciapptest-dev": "webpack && grok publish dev", + "release-interactivesciapptest-dev": "webpack && grok publish dev --release", + "debug-interactivesciapptest-local": "webpack && grok publish local", + "release-interactivesciapptest-local": "webpack && grok publish local --release", + "debug-interactivesciapptest-release": "webpack && grok publish release", + "release-interactivesciapptest-release": "webpack && grok publish release --release" + }, + "canEdit": [ + "Developers" + ], + "canView": [ + "All users" + ], + "repository": { + "type": "git", + "url": "https://github.com/datagrok-ai/public.git", + "directory": "packages/InteractiveSciAppTest" + } +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/app.ts b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/app.ts new file mode 100644 index 0000000000..5ff21876b9 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/app.ts @@ -0,0 +1,576 @@ +// Levins Metapopulation Model — Application (Coordinator + UI) + +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; + +import { + DEFAULTS, validate, solve, validateOptimize, + LevinsParams, LevinsSolution, InputId, WorkerTask, WorkerResult, +} from './core'; + +import '../../css/levins.css'; + +const DEBOUNCE_MS = 50; +const OPTIMIZE_POINTS = 10000; + +export function levinsMetapopulationApp(): void { + // --- State --- + let computationsBlocked = false; + let debounceTimer: ReturnType | null = null; + const subs: {unsubscribe(): void}[] = []; + let activeWorkers: Worker[] = []; + let lineChart!: DG.Viewer; + + // --- Initial DataFrame --- + const initSolution = solve(DEFAULTS); + const df = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', initSolution.t), + DG.Column.fromFloat64Array('p', initSolution.p), + ]); + df.name = 'Levins Metapopulation'; + + const view = grok.shell.addTableView(df); + view.name = 'Levins Metapopulation Model'; + + // --- Rho badge (created before controls so onValueChanged callbacks can reference it) --- + const rhoBadge = ui.div([], 'd4-tag levins-rho-badge'); + ui.tooltip.bind(rhoBadge, 'Extinction-to-colonization rate ratio. \u03C1 < 1 \u2014 metapopulation persists, \u03C1 \u2265 1 \u2014 extinction.'); + + // --- Controls --- + + // Initial condition + const ctrlP0 = ui.input.float('Initial patch fraction p₀', { + value: DEFAULTS.p0, nullable: false, + min: 0.001, max: 1, + tooltipText: 'Fraction of patches occupied at t=0. If p₀=0, the population cannot recover — computation is skipped.', + onValueChanged: () => debouncedRun(), + }); + + // Parameters + const ctrlM = ui.input.float('Colonization rate m', { + value: DEFAULTS.m, nullable: false, + min: 0.001, max: 100, + tooltipText: 'How fast empty patches are colonized from occupied ones. Units: 1/time.', + onValueChanged: () => { updateRhoBadge(); debouncedRun(); }, + }); + + const ctrlE0 = ui.input.float('Extinction rate e₀', { + value: DEFAULTS.e0, nullable: false, + min: 0.001, max: 100, + tooltipText: 'Base local extinction rate of a subpopulation in a patch. With rescue effect — decreases as p grows. Units: 1/time.', + onValueChanged: () => { updateRhoBadge(); debouncedRun(); }, + }); + + const ctrlRescue = ui.input.toggle('Rescue effect', { + value: DEFAULTS.rescueEffect, + tooltipText: 'When enabled, extinction rate depends on p: e(p) = e₀·(1−p). More occupied patches — lower local extinction.', + onValueChanged: () => { updateRescueLabel(); runPrimary(); }, + }); + + // Argument + const ctrlTStart = ui.input.float('Start t₀', { + value: DEFAULTS.t_start, nullable: false, + min: 0, max: 10000, + tooltipText: 'Simulation start time. Usually 0.', + onValueChanged: () => { updateArgRanges(); debouncedRun(); }, + }); + + const ctrlTEnd = ui.input.float('End t_end', { + value: DEFAULTS.t_end, nullable: false, + min: 0.1, max: 10000, + tooltipText: 'Simulation end time. Recommended ≥ 5/e₀ so the system reaches equilibrium.', + onValueChanged: () => { updateArgRanges(); debouncedRun(); }, + }); + + const ctrlTStep = ui.input.float('Step Δt', { + value: DEFAULTS.t_step, nullable: false, + min: 0.001, max: 1000, + tooltipText: 'Grid step of the numerical solution. Affects chart detail, not stability (MRT is an implicit method).', + onValueChanged: () => debouncedRun(), + }); + + // Solver + const ctrlTolerance = ui.input.float('Tolerance', { + value: DEFAULTS.tolerance, nullable: false, + min: 1e-12, max: 1e-2, + tooltipText: 'MRT method numerical tolerance. Lower — more precise but slower. Recommended: 1e-6 … 1e-9.', + onValueChanged: () => debouncedRun(), + }); + + // Set formats (block computations to avoid spurious runs from format-triggered events) + computationsBlocked = true; + ctrlP0.format = '0.000'; + ctrlM.format = '0.000'; + ctrlE0.format = '0.000'; + ctrlTStart.format = '0.0'; + ctrlTEnd.format = '0.0'; + ctrlTStep.format = '0.000'; + ctrlTolerance.format = '0.##E+0'; + computationsBlocked = false; + + // --- Input map for validators --- + const inputMap: Record = { + 'ctrl_p0': ctrlP0, + 'ctrl_m': ctrlM, + 'ctrl_e0': ctrlE0, + 'ctrl_rescue': ctrlRescue, + 'ctrl_t_start': ctrlTStart, + 'ctrl_t_end': ctrlTEnd, + 'ctrl_t_step': ctrlTStep, + 'ctrl_tolerance': ctrlTolerance, + }; + + function updateRhoBadge(): void { + const m = ctrlM.value ?? DEFAULTS.m; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const rho = e0 / m; + const persists = rho < 1; + rhoBadge.textContent = `ρ = e₀/m = ${rho.toFixed(3)}`; + rhoBadge.classList.toggle('levins-rho-badge--persists', persists); + rhoBadge.classList.toggle('levins-rho-badge--extinct', !persists); + } + updateRhoBadge(); + + // --- Rescue effect label reactivity --- + function updateRescueLabel(): void { + if (ctrlRescue.value) { + ctrlE0.caption = 'Base extinction rate e₀'; + ctrlE0.setTooltip('Base local extinction rate. Effective rate: e(p) = e₀·(1−p)'); + } else { + ctrlE0.caption = 'Extinction rate e₀'; + ctrlE0.setTooltip('Base local extinction rate of a subpopulation in a patch. Units: 1/time.'); + } + } + + // --- Argument range reactivity --- + // Note: Datagrok InputBase does not have setOptions for changing min/max at runtime. + // Range validation is handled by the complex validator instead. + function updateArgRanges(): void { + // Ranges are enforced through validation (val_06, val_07) + } + + // --- Gather current inputs --- + function getInputs(): LevinsParams { + return { + p0: ctrlP0.value ?? DEFAULTS.p0, + m: ctrlM.value ?? DEFAULTS.m, + e0: ctrlE0.value ?? DEFAULTS.e0, + rescueEffect: ctrlRescue.value ?? DEFAULTS.rescueEffect, + t_start: ctrlTStart.value ?? DEFAULTS.t_start, + t_end: ctrlTEnd.value ?? DEFAULTS.t_end, + t_step: ctrlTStep.value ?? DEFAULTS.t_step, + tolerance: ctrlTolerance.value ?? DEFAULTS.tolerance, + }; + } + + // --- Validators --- + function addValidators(): void { + const validatorFor = (id: InputId) => { + return () => { + const inputs = getInputs(); + const errors = validate(inputs); + return errors.get(id) ?? null; + }; + }; + + ctrlP0.addValidator(validatorFor('ctrl_p0')); + ctrlM.addValidator(validatorFor('ctrl_m')); + ctrlE0.addValidator(validatorFor('ctrl_e0')); + ctrlTStart.addValidator(validatorFor('ctrl_t_start')); + ctrlTEnd.addValidator(validatorFor('ctrl_t_end')); + ctrlTStep.addValidator(validatorFor('ctrl_t_step')); + ctrlTolerance.addValidator(validatorFor('ctrl_tolerance')); + } + addValidators(); + + // --- Color coding --- + function updateColorCoding(): void { + const m = ctrlM.value ?? DEFAULTS.m; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const threshold = e0 / m; + const pCol = view.dataFrame.col('p'); + if (pCol == null) return; + + const rules: Record = {}; + rules['<' + threshold] = '#F44336'; + rules['>=' + threshold] = '#4CAF50'; + pCol.meta.colors.setConditional(rules); + } + + // --- Grid column header tooltip (via onCellTooltip, as in EDA) --- + function setupGridTooltip(): void { + view.grid.onCellTooltip((cell, x, y) => { + if (!cell.isColHeader) + return false; + + const colName = cell.tableColumn?.name; + if (colName === 'p') { + const m = ctrlM.value ?? DEFAULTS.m; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const rho = (e0 / m).toFixed(3); + ui.tooltip.show(ui.divV([ + ui.h2('Occupied patch fraction p(t)'), + ui.divText(`Color: green — persistence zone (p ≥ e₀/m)`), + ui.divText(`red — extinction threat zone (p < e₀/m)`), + ui.divText(`Threshold: e₀/m = ${rho}`), + ]), x, y); + return true; + } + + return false; + }); + } + + // --- Primary pipeline --- + function runPrimary(): void { + if (computationsBlocked) + return; + + const inputs = getInputs(); + const errors = validate(inputs); + + // Clear previous errors on all inputs + for (const input of Object.values(inputMap)) + input.input?.classList.remove('d4-invalid'); + + if (errors.size > 0) { + errors.forEach((_msg, id) => { + const input = inputMap[id]; + if (input) + input.input?.classList.add('d4-invalid'); + }); + clearResults(); + return; + } + + try { + const result = solve(inputs); + updateDataFrame(result); + updateColorCoding(); + } catch (err) { + clearResults(); + const msg = err instanceof Error ? err.message : 'Computation error'; + grok.shell.error(msg); + } + } + + function debouncedRun(): void { + if (debounceTimer !== null) + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => runPrimary(), DEBOUNCE_MS); + } + + // --- Update DataFrame --- + function updateDataFrame(result: LevinsSolution): void { + const newDf = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', result.t), + DG.Column.fromFloat64Array('p', result.p), + ]); + newDf.name = 'Levins Metapopulation'; + view.dataFrame = newDf; + lineChart.dataFrame = newDf; + } + + function clearResults(): void { + const emptyDf = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', new Float64Array(0)), + DG.Column.fromFloat64Array('p', new Float64Array(0)), + ]); + emptyDf.name = 'Levins Metapopulation'; + view.dataFrame = emptyDf; + lineChart.dataFrame = emptyDf; + } + + // --- Optimization task --- + let optimizeBtn: HTMLElement; + + function setOptimizeBtnEnabled(enabled: boolean): void { + optimizeBtn.classList.toggle('levins-btn--disabled', !enabled); + } + + async function runOptimization(mMin: number, mMax: number): Promise { + const inputs = getInputs(); + const errors = validate(inputs); + if (errors.size > 0) { + grok.shell.error('Internal error: invalid task parameters. Check the inputs and try again.'); + return; + } + + setOptimizeBtnEnabled(false); + + const pi = DG.TaskBarProgressIndicator.create('Optimizing m...'); + let canceled = false; + let completed = 0; + let errorCount = 0; + + const results: {m_i: number; p_end: number}[] = []; + const workerCount = Math.max(1, (navigator.hardwareConcurrency ?? 4) - 2); + + // Generate m values + const mValues: number[] = []; + for (let i = 0; i < OPTIMIZE_POINTS; i++) + mValues.push(mMin + i * (mMax - mMin) / (OPTIMIZE_POINTS - 1)); + + // Worker pool + const workerUrl = _package.webRoot + 'dist/optimize-worker.js'; + + try { + await new Promise((resolve, reject) => { + const taskQueue = [...mValues]; + activeWorkers = []; + + const createWorker = (): Worker | null => { + try { + const worker = new Worker(workerUrl); + activeWorkers.push(worker); + return worker; + } catch (_err) { + return null; + } + }; + + const processNext = (worker: Worker) => { + if (canceled) { + terminateWorkers(); + resolve(); + return; + } + + if (taskQueue.length === 0) { + worker.terminate(); + activeWorkers = activeWorkers.filter((w) => w !== worker); + if (activeWorkers.length === 0) + resolve(); + return; + } + + const m_i = taskQueue.shift()!; + const task: WorkerTask = { + m_i, + p0: inputs.p0, + e0: inputs.e0, + rescueEffect: inputs.rescueEffect, + t_start: inputs.t_start, + t_end: inputs.t_end, + t_step: inputs.t_step, + tolerance: inputs.tolerance, + }; + worker.postMessage(task); + }; + + const handleResult = (worker: Worker, event: MessageEvent) => { + const result = event.data; + completed++; + pi.update(Math.round(completed / OPTIMIZE_POINTS * 100), `${completed}/${OPTIMIZE_POINTS}`); + + if (result.error) + errorCount++; + else + results.push({m_i: result.m_i, p_end: result.p_end}); + + processNext(worker); + }; + + // Create worker pool + for (let i = 0; i < workerCount; i++) { + const worker = createWorker(); + if (worker == null) { + if (i === 0) { + reject(new Error('Failed to start parallel computations. Try again later.')); + return; + } + break; + } + + worker.onmessage = (event) => handleResult(worker, event); + worker.onerror = () => { + completed++; + errorCount++; + pi.update(Math.round(completed / OPTIMIZE_POINTS * 100), `${completed}/${OPTIMIZE_POINTS}`); + processNext(worker); + }; + + processNext(worker); + } + }); + } catch (err) { + grok.shell.error(err instanceof Error ? err.message : 'Failed to start parallel computations.'); + pi.close(); + setOptimizeBtnEnabled(true); + return; + } + + pi.close(); + terminateWorkers(); + setOptimizeBtnEnabled(true); + + if (canceled) + return; + + // Handle results + if (errorCount > 0 && errorCount < OPTIMIZE_POINTS) + grok.shell.warning(`${errorCount} of ${OPTIMIZE_POINTS} points failed to compute. Result based on ${OPTIMIZE_POINTS - errorCount} points.`); + + if (results.length === 0) { + grok.shell.error('Failed to compute any point. Check the parameters.'); + return; + } + + // Find optimal + let best = results[0]; + for (const r of results) { + if (r.p_end > best.p_end) + best = r; + } + + // Batch update: block primary, write m_optimal, unblock and run once + try { + computationsBlocked = true; + ctrlM.value = best.m_i; + computationsBlocked = false; + runPrimary(); + grok.shell.info(`Optimal m = ${best.m_i.toFixed(3)}\np(t_end) = ${best.p_end.toFixed(3)}`); + } catch (_err) { + computationsBlocked = false; + grok.shell.warning(`Optimal m = ${best.m_i.toFixed(3)}, but failed to update the field automatically. Enter the value manually.`); + } + } + + function terminateWorkers(): void { + for (const w of activeWorkers) + w.terminate(); + activeWorkers = []; + } + + // --- Optimize dialog --- + function showOptimizeDialog(): void { + const dlgMMin = ui.input.float('Minimum m', { + value: 0.1, nullable: false, + min: 0.001, max: 100, + tooltipText: 'Lower bound of the m search range. Must be less than the maximum value.', + }); + dlgMMin.format = '0.000'; + + const dlgMMax = ui.input.float('Maximum m', { + value: 1.0, nullable: false, + min: 0.001, max: 100, + tooltipText: 'Upper bound of the m search range. Must be greater than the minimum value.', + }); + dlgMMax.format = '0.000'; + + // Cross-validation of dialog inputs + const validateDialog = (): boolean => { + const mMin = dlgMMin.value ?? 0.1; + const mMax = dlgMMax.value ?? 1.0; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const rescueEffect = ctrlRescue.value ?? DEFAULTS.rescueEffect; + + const {errors, warning} = validateOptimize({m_min: mMin, m_max: mMax}, e0, rescueEffect); + + if (warning) + grok.shell.warning(warning); + + return errors.size === 0; + }; + + dlgMMin.addValidator(() => { + const mMin = dlgMMin.value ?? 0.1; + const mMax = dlgMMax.value ?? 1.0; + if (mMin <= 0) return 'Colonization rate must be positive'; + if (mMin >= mMax) return 'Minimum value must be less than maximum'; + return null; + }); + + dlgMMax.addValidator(() => { + const mMax = dlgMMax.value ?? 1.0; + if (mMax <= 0) return 'Colonization rate must be positive'; + return null; + }); + + ui.dialog('Find optimal m') + .add(dlgMMin) + .add(dlgMMax) + .onOK(() => { + if (!validateDialog()) + return; + runOptimization(dlgMMin.value!, dlgMMax.value!); + }) + .show(); + } + + // --- Toolbar buttons --- + optimizeBtn = ui.iconFA('search', () => showOptimizeDialog(), 'Find the m value that maximizes the occupied patch fraction at t_end'); + + const resetBtn = ui.iconFA('undo', () => { + computationsBlocked = true; + ctrlP0.value = DEFAULTS.p0; + ctrlM.value = DEFAULTS.m; + ctrlE0.value = DEFAULTS.e0; + ctrlRescue.value = DEFAULTS.rescueEffect; + ctrlTStart.value = DEFAULTS.t_start; + ctrlTEnd.value = DEFAULTS.t_end; + ctrlTStep.value = DEFAULTS.t_step; + ctrlTolerance.value = DEFAULTS.tolerance; + computationsBlocked = false; + updateRhoBadge(); + runPrimary(); + }, 'Reset all parameters to default values'); + + view.setRibbonPanels([[optimizeBtn, resetBtn]]); + + // --- Layout: left panel with form --- + const form = ui.form([]); + + form.append(ui.h2('Initial Condition')); + form.append(ctrlP0.root); + + form.append(ui.h2('Parameters')); + form.append(ctrlM.root); + form.append(ctrlE0.root); + form.append(ctrlRescue.root); + form.append(rhoBadge); + + form.append(ui.h2('Argument')); + form.append(ctrlTStart.root); + form.append(ctrlTEnd.root); + form.append(ctrlTStep.root); + + form.append(ui.h2('Solver')); + form.append(ctrlTolerance.root); + + const dockMng = view.dockManager; + dockMng.dock(form, DG.DOCK_TYPE.LEFT, null, undefined, 0.2); + + // --- Line chart --- + lineChart = view.addViewer('Line chart', { + xColumnName: 't', + yColumnNames: ['p'], + title: 'p(t) Dynamics', + }); + + const gridNode = dockMng.findNode(view.grid.root); + if (gridNode != null) + dockMng.dock(lineChart, DG.DOCK_TYPE.RIGHT, gridNode, undefined, 0.5); + + // --- Initial color coding and tooltip --- + updateColorCoding(); + setupGridTooltip(); + + // --- Cleanup on close --- + subs.push(grok.events.onViewRemoved.subscribe((v: any) => { + if (v === view) { + terminateWorkers(); + for (const sub of subs) + sub.unsubscribe(); + if (debounceTimer !== null) + clearTimeout(debounceTimer); + } + })); +} + +// Package reference (set from package.ts) +let _package: DG.Package; +export function setPackage(pkg: DG.Package): void { + _package = pkg; +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/core.ts b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/core.ts new file mode 100644 index 0000000000..b927b6a81e --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/core.ts @@ -0,0 +1,144 @@ +// Levins Metapopulation Model — Computational Core + +import {mrt} from 'diff-grok'; + +import {LevinsParams, createLevinsODE, getEquilibrium} from './model'; + +// --- Re-exports --- + +export type {LevinsParams} from './model'; +export {createLevinsODE, getEquilibrium} from './model'; + +// --- Types --- + +export interface LevinsSolution { + t: Float64Array; + p: Float64Array; + p_star: number; +} + +export type InputId = 'ctrl_p0' | 'ctrl_m' | 'ctrl_e0' | 'ctrl_rescue' | + 'ctrl_t_start' | 'ctrl_t_end' | 'ctrl_t_step' | 'ctrl_tolerance'; + +export type ValidationErrors = Map; + +// --- Defaults --- + +export const DEFAULTS: LevinsParams = { + p0: 0.5, + m: 0.5, + e0: 0.2, + rescueEffect: false, + t_start: 0, + t_end: 50, + t_step: 0.1, + tolerance: 1e-7, +}; + +// --- Validation --- + +export function validate(inputs: LevinsParams): ValidationErrors { + const errors: ValidationErrors = new Map(); + const {p0, m, e0, rescueEffect, t_start, t_end, t_step, tolerance} = inputs; + + // val_01, val_02 + if (p0 <= 0) + errors.set('ctrl_p0', 'Initial patch fraction must be greater than 0'); + else if (p0 > 1) + errors.set('ctrl_p0', 'Initial patch fraction cannot exceed 1'); + + // val_03 + if (m <= 0) + errors.set('ctrl_m', 'Colonization rate must be positive'); + + // val_04 + if (e0 <= 0) + errors.set('ctrl_e0', 'Extinction rate must be positive'); + + // val_05 — only if val_03 and val_04 passed + if (!errors.has('ctrl_m') && !errors.has('ctrl_e0') && !rescueEffect && m <= e0) + errors.set('ctrl_m', 'Colonization rate must exceed extinction rate (m > e₀). With current values the metapopulation tends to extinction'); + + // val_06 + if (t_end <= t_start) { + errors.set('ctrl_t_end', 'End of interval must be greater than start'); + errors.set('ctrl_t_start', 'End of interval must be greater than start'); + } + + // val_07 + if (t_step <= 0) + errors.set('ctrl_t_step', 'Step must be positive'); + + // val_08 — only if val_06 and val_07 passed + if (!errors.has('ctrl_t_end') && !errors.has('ctrl_t_step') && t_step >= t_end - t_start) + errors.set('ctrl_t_step', 'Step must be less than interval length'); + + // val_09 + if (tolerance <= 0) + errors.set('ctrl_tolerance', 'Tolerance must be positive'); + + return errors; +} + +// --- Solver --- + +export function solve(inputs: LevinsParams): LevinsSolution { + const task = createLevinsODE(inputs); + const solution = mrt(task); + + return { + t: solution[0], + p: solution[1], + p_star: getEquilibrium(inputs.m, inputs.e0, inputs.rescueEffect), + }; +} + +// --- Optimization validation --- + +export interface OptimizeInputs { + m_min: number; + m_max: number; +} + +export type OptInputId = 'dlg_m_min' | 'dlg_m_max'; +export type OptValidationErrors = Map; + +export function validateOptimize( + opt: OptimizeInputs, e0: number, rescueEffect: boolean, +): {errors: OptValidationErrors; warning: string | null} { + const errors: OptValidationErrors = new Map(); + let warning: string | null = null; + + if (opt.m_min <= 0) + errors.set('dlg_m_min', 'Colonization rate must be positive'); + + if (opt.m_max <= 0) + errors.set('dlg_m_max', 'Colonization rate must be positive'); + + if (!errors.has('dlg_m_min') && !errors.has('dlg_m_max') && opt.m_min >= opt.m_max) + errors.set('dlg_m_min', 'Minimum value must be less than maximum'); + + if (errors.size === 0 && !rescueEffect && opt.m_max <= e0) + warning = 'With current e₀ the entire m range leads to extinction (m ≤ e₀). Increase the maximum or decrease e₀'; + + return {errors, warning}; +} + +// --- Worker message types --- + +export interface WorkerTask { + m_i: number; + p0: number; + e0: number; + rescueEffect: boolean; + t_start: number; + t_end: number; + t_step: number; + tolerance: number; +} + +export interface WorkerResult { + m_i: number; + p_end: number; + error?: string; +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/model.ts b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/model.ts new file mode 100644 index 0000000000..6a0a27f3db --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/model.ts @@ -0,0 +1,37 @@ +// Levins Metapopulation Model — ODE specification + +import {ODEs} from 'diff-grok'; + +/** Parameters for the Levins metapopulation ODE */ +export interface LevinsParams { + p0: number; + m: number; + e0: number; + rescueEffect: boolean; + t_start: number; + t_end: number; + t_step: number; + tolerance: number; +} + +/** Creates the ODEs specification for the Levins model, usable in both main thread and workers */ +export function createLevinsODE(params: LevinsParams): ODEs { + const {p0, m, e0, rescueEffect, t_start, t_end, t_step, tolerance} = params; + + return { + name: 'Levins', + arg: {name: 't', start: t_start, finish: t_end, step: t_step}, + initial: [p0], + func: (_t: number, y: Float64Array, out: Float64Array) => { + const e = rescueEffect ? e0 * (1 - y[0]) : e0; + out[0] = m * y[0] * (1 - y[0]) - e * y[0]; + }, + tolerance: tolerance, + solutionColNames: ['p(t)'], + }; +} + +/** Computes the analytical equilibrium p* for the base Levins model */ +export function getEquilibrium(m: number, e0: number, rescueEffect: boolean): number { + return rescueEffect ? NaN : Math.max(0, 1 - e0 / m); +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/optimize-worker.ts b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/optimize-worker.ts new file mode 100644 index 0000000000..196dd6a32e --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/levins/optimize-worker.ts @@ -0,0 +1,40 @@ +// Levins Metapopulation Model — Web Worker for optimization task +// Uses mrt from diff-grok via the shared model definition + +import {mrt} from 'diff-grok'; + +import {createLevinsODE} from './model'; +import {WorkerTask, WorkerResult} from './core'; + +const ctx: Worker = self as unknown as Worker; + +ctx.onmessage = (event: MessageEvent) => { + const task = event.data; + + try { + const ode = createLevinsODE({ + p0: task.p0, + m: task.m_i, + e0: task.e0, + rescueEffect: task.rescueEffect, + t_start: task.t_start, + t_end: task.t_end, + t_step: task.t_step, + tolerance: task.tolerance, + }); + + const solution = mrt(ode); + const pValues = solution[1]; + const p_end = pValues[pValues.length - 1]; + + const result: WorkerResult = {m_i: task.m_i, p_end}; + ctx.postMessage(result); + } catch (err) { + const result: WorkerResult = { + m_i: task.m_i, + p_end: -1, + error: err instanceof Error ? err.message : 'Unknown error', + }; + ctx.postMessage(result); + } +}; diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package-api.ts b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package-api.ts new file mode 100644 index 0000000000..152e9543db --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package-api.ts @@ -0,0 +1,21 @@ +/** +This file is auto-generated by the grok api command. +If you notice any changes, please push them to the repository. +Do not edit this file manually. +*/ +import * as grok from 'datagrok-api/grok'; +import * as DG from 'datagrok-api/dg'; + + +export namespace funcs { + export async function info(): Promise { + return await grok.functions.call('InteractiveSciAppTest:Info', {}); + } + + /** + Interactive ODE solver for the Levins model: simulation of occupied patch fraction p(t) dynamics + */ + export async function levinsMetapopulationModelApp(): Promise { + return await grok.functions.call('InteractiveSciAppTest:LevinsMetapopulationModelApp', {}); + } +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package-test.ts b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package-test.ts new file mode 100644 index 0000000000..15255df649 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package-test.ts @@ -0,0 +1,23 @@ +import { runTests, tests, TestContext , initAutoTests as initTests } from '@datagrok-libraries/utils/src/test'; +import * as DG from 'datagrok-api/dg'; + +import './tests/levins-api-tests'; +import './tests/levins-math-tests'; + +export let _package = new DG.Package(); +export { tests }; + +//name: test +//input: string category {optional: true} +//input: string test {optional: true} +//input: object testContext {optional: true} +//output: dataframe result +export async function test(category: string, test: string, testContext: TestContext): Promise { + const data = await runTests({ category, test, testContext }); + return DG.DataFrame.fromObjects(data)!; +} + +//name: initAutoTests +export async function initAutoTests() { + await initTests(_package, _package.getModule('package-test.js')); +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package.g.ts b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package.g.ts new file mode 100644 index 0000000000..8de619387a --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package.g.ts @@ -0,0 +1 @@ +import * as DG from 'datagrok-api/dg'; diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package.ts b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package.ts new file mode 100644 index 0000000000..93aec6ce82 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/package.ts @@ -0,0 +1,22 @@ +/* Do not change these import lines to match external modules in webpack configuration */ +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; +export * from './package.g'; + +import {levinsMetapopulationApp, setPackage} from './levins/app'; + +export const _package = new DG.Package(); + +//name: info +export function info() { + grok.shell.info(_package.webRoot); +} + +//name: Levins Metapopulation Model +//tags: app +//description: Interactive ODE solver for the Levins model: simulation of occupied patch fraction p(t) dynamics +export function levinsMetapopulationModelApp(): void { + setPackage(_package); + levinsMetapopulationApp(); +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/tests/levins-api-tests.ts b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/tests/levins-api-tests.ts new file mode 100644 index 0000000000..d6db9d2b21 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/tests/levins-api-tests.ts @@ -0,0 +1,191 @@ +// Levins Metapopulation Model — API tests + +import {category, test, expect} from '@datagrok-libraries/utils/src/test'; + +import {DEFAULTS, validate, validateOptimize} from '../levins/core'; + +category('API: Validation', () => { + // --- val_01: p0 <= 0 --- + test('val_01: p0 = 0', async () => { + const errors = validate({...DEFAULTS, p0: 0}); + expect(errors.has('ctrl_p0'), true, 'Should reject p0 = 0'); + }); + + test('val_01: p0 = -1', async () => { + const errors = validate({...DEFAULTS, p0: -1}); + expect(errors.has('ctrl_p0'), true, 'Should reject p0 < 0'); + }); + + // --- val_02: p0 > 1 --- + test('val_02: p0 = 1.1', async () => { + const errors = validate({...DEFAULTS, p0: 1.1}); + expect(errors.has('ctrl_p0'), true, 'Should reject p0 > 1'); + }); + + // --- p0 valid boundary --- + test('p0 = 0.001 (valid boundary)', async () => { + const errors = validate({...DEFAULTS, p0: 0.001}); + expect(errors.has('ctrl_p0'), false, 'Should accept p0 = 0.001'); + }); + + test('p0 = 1 (valid boundary)', async () => { + const errors = validate({...DEFAULTS, p0: 1}); + expect(errors.has('ctrl_p0'), false, 'Should accept p0 = 1'); + }); + + // --- val_03: m <= 0 --- + test('val_03: m = 0', async () => { + const errors = validate({...DEFAULTS, m: 0}); + expect(errors.has('ctrl_m'), true, 'Should reject m = 0'); + }); + + test('val_03: m = -0.5', async () => { + const errors = validate({...DEFAULTS, m: -0.5}); + expect(errors.has('ctrl_m'), true, 'Should reject m < 0'); + }); + + // --- val_04: e0 <= 0 --- + test('val_04: e0 = 0', async () => { + const errors = validate({...DEFAULTS, e0: 0}); + expect(errors.has('ctrl_e0'), true, 'Should reject e0 = 0'); + }); + + test('val_04: e0 = -0.1', async () => { + const errors = validate({...DEFAULTS, e0: -0.1}); + expect(errors.has('ctrl_e0'), true, 'Should reject e0 < 0'); + }); + + // --- val_05: m <= e0 (no rescue) --- + test('val_05: m = e0, no rescue', async () => { + const errors = validate({...DEFAULTS, m: 0.5, e0: 0.5, rescueEffect: false}); + expect(errors.has('ctrl_m'), true, 'Should reject m = e0 without rescue'); + }); + + test('val_05: m < e0, no rescue', async () => { + const errors = validate({...DEFAULTS, m: 0.3, e0: 0.5, rescueEffect: false}); + expect(errors.has('ctrl_m'), true, 'Should reject m < e0 without rescue'); + }); + + test('val_05: m <= e0 with rescue (allowed)', async () => { + const errors = validate({...DEFAULTS, m: 0.3, e0: 0.5, rescueEffect: true}); + expect(errors.has('ctrl_m'), false, 'Should allow m <= e0 with rescue'); + }); + + test('val_05: skipped when val_03 fails', async () => { + const errors = validate({...DEFAULTS, m: 0, e0: 0.5, rescueEffect: false}); + expect(errors.get('ctrl_m'), 'Colonization rate must be positive'); + }); + + // --- val_06: t_end <= t_start --- + test('val_06: t_end = t_start', async () => { + const errors = validate({...DEFAULTS, t_start: 10, t_end: 10}); + expect(errors.has('ctrl_t_end'), true, 'Should reject t_end = t_start'); + expect(errors.has('ctrl_t_start'), true, 'Should set error on t_start too'); + }); + + test('val_06: t_end < t_start', async () => { + const errors = validate({...DEFAULTS, t_start: 10, t_end: 5}); + expect(errors.has('ctrl_t_end'), true, 'Should reject t_end < t_start'); + }); + + // --- val_07: t_step <= 0 --- + test('val_07: t_step = 0', async () => { + const errors = validate({...DEFAULTS, t_step: 0}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step = 0'); + }); + + test('val_07: t_step = -0.1', async () => { + const errors = validate({...DEFAULTS, t_step: -0.1}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step < 0'); + }); + + // --- val_08: t_step >= t_end - t_start --- + test('val_08: t_step = interval length', async () => { + const errors = validate({...DEFAULTS, t_start: 0, t_end: 50, t_step: 50}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step = interval length'); + }); + + test('val_08: t_step > interval length', async () => { + const errors = validate({...DEFAULTS, t_start: 0, t_end: 50, t_step: 100}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step > interval length'); + }); + + test('val_08: skipped when val_07 fails', async () => { + const errors = validate({...DEFAULTS, t_step: -1}); + expect(errors.get('ctrl_t_step'), 'Step must be positive'); + }); + + // --- val_09: tolerance <= 0 --- + test('val_09: tolerance = 0', async () => { + const errors = validate({...DEFAULTS, tolerance: 0}); + expect(errors.has('ctrl_tolerance'), true, 'Should reject tolerance = 0'); + }); + + test('val_09: tolerance = -1e-7', async () => { + const errors = validate({...DEFAULTS, tolerance: -1e-7}); + expect(errors.has('ctrl_tolerance'), true, 'Should reject tolerance < 0'); + }); + + // --- valid defaults --- + test('Defaults pass validation', async () => { + const errors = validate(DEFAULTS); + expect(errors.size, 0, 'Default parameters should be valid'); + }); + + // --- multiple errors --- + test('Multiple simultaneous errors', async () => { + const errors = validate({...DEFAULTS, p0: 0, m: 0, e0: 0, t_step: 0, tolerance: 0}); + expect(errors.size >= 4, true, 'Should report multiple errors'); + expect(errors.has('ctrl_p0'), true); + expect(errors.has('ctrl_m'), true); + expect(errors.has('ctrl_e0'), true); + expect(errors.has('ctrl_t_step'), true); + expect(errors.has('ctrl_tolerance'), true); + }); +}); + +category('API: Optimization Validation', () => { + // --- opt_val_01: m_min <= 0 --- + test('opt_val_01: m_min = 0', async () => { + const {errors} = validateOptimize({m_min: 0, m_max: 1}, 0.2, false); + expect(errors.has('dlg_m_min'), true, 'Should reject m_min = 0'); + }); + + // --- opt_val_02: m_max <= 0 --- + test('opt_val_02: m_max = -1', async () => { + const {errors} = validateOptimize({m_min: 0.1, m_max: -1}, 0.2, false); + expect(errors.has('dlg_m_max'), true, 'Should reject m_max < 0'); + }); + + // --- opt_val_03: m_min >= m_max --- + test('opt_val_03: m_min = m_max', async () => { + const {errors} = validateOptimize({m_min: 0.5, m_max: 0.5}, 0.2, false); + expect(errors.has('dlg_m_min'), true, 'Should reject m_min = m_max'); + }); + + test('opt_val_03: m_min > m_max', async () => { + const {errors} = validateOptimize({m_min: 1.0, m_max: 0.5}, 0.2, false); + expect(errors.has('dlg_m_min'), true, 'Should reject m_min > m_max'); + }); + + // --- opt_val_04: warning when m_max <= e0 --- + test('opt_val_04: m_max <= e0, no rescue — warning', async () => { + const {errors, warning} = validateOptimize({m_min: 0.05, m_max: 0.1}, 0.2, false); + expect(errors.size, 0, 'Should not block'); + expect(warning !== null, true, 'Should produce warning'); + }); + + test('opt_val_04: m_max <= e0 with rescue — no warning', async () => { + const {errors, warning} = validateOptimize({m_min: 0.05, m_max: 0.1}, 0.2, true); + expect(errors.size, 0); + expect(warning, null, 'No warning with rescue effect'); + }); + + // --- valid --- + test('Valid optimization inputs', async () => { + const {errors, warning} = validateOptimize({m_min: 0.1, m_max: 1.0}, 0.2, false); + expect(errors.size, 0, 'Should pass'); + expect(warning, null, 'No warning'); + }); +}); + diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/tests/levins-math-tests.ts b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/tests/levins-math-tests.ts new file mode 100644 index 0000000000..a1c0cb397b --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/src/tests/levins-math-tests.ts @@ -0,0 +1,201 @@ +// Levins Metapopulation Model — Math tests + +import {category, test, expect, expectFloat} from '@datagrok-libraries/utils/src/test'; +import {mrt, ODEs} from 'diff-grok'; + +import {createLevinsODE, LevinsParams} from '../levins/model'; +import {DEFAULTS, solve, getEquilibrium} from '../levins/core'; + +// ── Helpers ── + +/** Max absolute error between numerical and exact solutions across all grid points */ +function getMaxError(odes: ODEs, exact: (t: number) => number): number { + const solution = mrt(odes); + const tArr = solution[0]; + const yArr = solution[1]; + let error = 0; + + for (let i = 0; i < tArr.length; i++) + error = Math.max(error, Math.abs(exact(tArr[i]) - yArr[i])); + + return error; +} + +/** Evaluates func at given p and returns dp/dt */ +function evalFunc(params: LevinsParams, p: number): number { + const ode = createLevinsODE(params); + const y = new Float64Array([p]); + const out = new Float64Array(1); + ode.func(0, y, out); + return out[0]; +} + +// ── Correctness: MRT solver ── + +const TINY = 0.1; + +category('Math: MRT solver', () => { + test('Non-stiff 1D: dy/dt = 4·exp(0.8t) − 0.5y', async () => { + // Reference: Chapra & Canale, p. 736 + const odes: ODEs = { + name: 'Non-stiff 1D', + arg: {name: 't', start: 0, finish: 4, step: 0.01}, + initial: [2], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = 4 * Math.exp(0.8 * _t) - 0.5 * y[0]; + }, + tolerance: 1e-6, + solutionColNames: ['y'], + }; + + const exact = (t: number) => + (4 / 1.3) * (Math.exp(0.8 * t) - Math.exp(-0.5 * t)) + 2 * Math.exp(-0.5 * t); + + const error = getMaxError(odes, exact); + expectFloat(error, 0, TINY); + }); + + test('Stiff 1D: dy/dt = −1000y + 3000 − 2000·exp(−t)', async () => { + // Reference: Chapra & Canale, p. 767 + const odes: ODEs = { + name: 'Stiff 1D', + arg: {name: 't', start: 0, finish: 4, step: 0.01}, + initial: [0], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = -1000 * y[0] + 3000 - 2000 * Math.exp(-_t); + }, + tolerance: 5e-7, + solutionColNames: ['y'], + }; + + const exact = (t: number) => + 3 - 0.998 * Math.exp(-1000 * t) - 2.002 * Math.exp(-t); + + const error = getMaxError(odes, exact); + expectFloat(error, 0, TINY); + }); + + test('Stiff 2D: VDPOL (van der Pol, µ=1000)', async () => { + // Reference: https://archimede.uniba.it/~testset/report/vdpol.pdf + const vdpol: ODEs = { + name: 'van der Pol', + arg: {name: 't', start: 0, finish: 2000, step: 0.1}, + initial: [-1, 1], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = y[1]; + out[1] = -y[0] + 1000 * (1 - y[0] * y[0]) * y[1]; + }, + tolerance: 1e-12, + solutionColNames: ['x1', 'x2'], + }; + + mrt(vdpol); + }, {benchmark: true, timeout: 2000}); +}); + +// ── Correctness: Levins func ── + +const BASE: LevinsParams = { + p0: 0.5, m: 0.5, e0: 0.2, rescueEffect: false, + t_start: 0, t_end: 50, t_step: 0.1, tolerance: 1e-7, +}; + +category('Math: Levins func', () => { + // dp/dt = m·p·(1−p) − e₀·p = 0.5·0.5·0.5 − 0.2·0.5 = 0.125 − 0.1 = 0.025 + test('func_01: base model, p=0.5', async () => { + expectFloat(evalFunc({...BASE, m: 0.5, e0: 0.2, rescueEffect: false}, 0.5), 0.025, 1e-12); + }); + + // dp/dt = 1.0·0.1·0.9 − 0.3·0.1 = 0.09 − 0.03 = 0.06 + test('func_02: base model, low p=0.1', async () => { + expectFloat(evalFunc({...BASE, m: 1.0, e0: 0.3, rescueEffect: false}, 0.1), 0.06, 1e-12); + }); + + // At equilibrium p*=1−e₀/m=0.6: dp/dt = 0.5·0.6·0.4 − 0.2·0.6 = 0.12 − 0.12 = 0.0 + test('func_03: equilibrium p*=0.6, dp/dt=0', async () => { + expectFloat(evalFunc({...BASE, m: 0.5, e0: 0.2, rescueEffect: false}, 0.6), 0.0, 1e-12); + }); + + // Rescue: e=e₀·(1−p)=0.2·0.5=0.1; dp/dt = 0.5·0.5·0.5 − 0.1·0.5 = 0.125 − 0.05 = 0.075 + test('func_04: rescue effect, p=0.5', async () => { + expectFloat(evalFunc({...BASE, m: 0.5, e0: 0.2, rescueEffect: true}, 0.5), 0.075, 1e-12); + }); + + // Rescue: e=0.5·(1−0.8)=0.1; dp/dt = 0.3·0.8·0.2 − 0.1·0.8 = 0.048 − 0.08 = −0.032 + test('func_05: rescue + decline, p=0.8', async () => { + expectFloat(evalFunc({...BASE, m: 0.3, e0: 0.5, rescueEffect: true}, 0.8), -0.032, 1e-12); + }); +}); + +// ── Correctness: Levins equilibrium ── + +category('Math: Equilibrium', () => { + test('p* = 1 - e0/m (base model)', async () => { + expectFloat(getEquilibrium(0.5, 0.2, false), 0.6, 1e-10); + }); + + test('p* = 0 when m <= e0', async () => { + expectFloat(getEquilibrium(0.2, 0.5, false), 0, 1e-10); + }); + + test('p* = 0 when m = e0', async () => { + expectFloat(getEquilibrium(0.5, 0.5, false), 0, 1e-10); + }); + + test('p* = NaN with rescue effect', async () => { + expect(isNaN(getEquilibrium(0.5, 0.2, true)), true, 'Should be NaN with rescue'); + }); +}); + +// ── Output property verification: solve ── + +category('Math: Solve output properties', () => { + test('solve_01: default parameters produce non-empty arrays of equal length', async () => { + const result = solve(DEFAULTS); + expect(result.t.length > 0, true, 't should be non-empty'); + expect(result.p.length > 0, true, 'p should be non-empty'); + expect(result.t.length, result.p.length, 't and p should have equal length'); + }); + + test('solve_02: p values in [0, 1]', async () => { + const result = solve(DEFAULTS); + for (let i = 0; i < result.p.length; i++) + expect(result.p[i] >= 0 && result.p[i] <= 1, true, `p[${i}] = ${result.p[i]} out of [0, 1]`); + }); + + test('solve_03: p(0) = p0', async () => { + const result = solve(DEFAULTS); + expectFloat(result.p[0], DEFAULTS.p0, 1e-6); + }); + + test('solve_04: t starts at t_start', async () => { + const result = solve(DEFAULTS); + expectFloat(result.t[0], DEFAULTS.t_start, 1e-12); + }); + + test('solve_05: convergence to p*', async () => { + const params = {...DEFAULTS, m: 0.5, e0: 0.2, rescueEffect: false, t_end: 200}; + const result = solve(params); + const pStar = getEquilibrium(params.m, params.e0, params.rescueEffect); + expectFloat(result.p[result.p.length - 1], pStar, 0.01); + }); + + test('solve_06: rescue effect — p in [0, 1]', async () => { + const params = {...DEFAULTS, m: 0.3, e0: 0.5, rescueEffect: true, t_end: 100}; + const result = solve(params); + for (let i = 0; i < result.p.length; i++) + expect(result.p[i] >= 0 && result.p[i] <= 1, true, `p[${i}] = ${result.p[i]} out of [0, 1]`); + }); + + test('solve_07: higher m → higher p(t_end)', async () => { + const r1 = solve({...DEFAULTS, m: 0.5, e0: 0.2}); + const r2 = solve({...DEFAULTS, m: 1.0, e0: 0.2}); + expect(r2.p[r2.p.length - 1] > r1.p[r1.p.length - 1], true, + 'p(t_end) with m=1.0 should exceed p(t_end) with m=0.5'); + }); + + test('solve_08: custom p0', async () => { + const result = solve({...DEFAULTS, p0: 0.9}); + expectFloat(result.p[0], 0.9, 1e-6); + }); +}); diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/tsconfig.json b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/tsconfig.json new file mode 100644 index 0000000000..b9b0997746 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/tsconfig.json @@ -0,0 +1,71 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "lib": ["ES2022", "dom"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/webpack.config.js b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/webpack.config.js new file mode 100644 index 0000000000..a06441f8ba --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/code/webpack.config.js @@ -0,0 +1,69 @@ +const path = require('path'); +const {execSync} = require('child_process'); +const packageName = path.parse(require('./package.json').name).name.toLowerCase().replace(/-/g, ''); + +function getDatagrokTools() { + const pluginPath = 'datagrok-tools/plugins/func-gen-plugin'; + try { + return require(pluginPath); + } catch (e) { + try { + const globalPath = execSync('npm root -g').toString().trim(); + return require(path.join(globalPath, pluginPath)); + } catch (globalErr) { + console.error('\n' + '='.repeat(60)); + console.error('ERROR: datagrok-tools not found!'); + console.error('To fix this, please install the tools globally by running:'); + console.error('\n npm install -g datagrok-tools\n'); + console.error('='.repeat(60) + '\n'); + process.exit(1); + } + } +} + +const FuncGeneratorPlugin = getDatagrokTools(); + +module.exports = { + cache: { + type: 'filesystem', + }, + mode: 'development', + entry: { + test: {filename: 'package-test.js', library: {type: 'var', name: `${packageName}_test`}, import: './src/package-test.ts'}, + package: './src/package.ts', + 'optimize-worker': {filename: 'optimize-worker.js', import: './src/levins/optimize-worker.ts'}, + }, + resolve: { + symlinks: false, + extensions: ['.wasm', '.mjs', '.ts', '.json', '.js', '.tsx'], + }, + module: { + rules: [ + {test: /\.tsx?$/, loader: 'ts-loader', options: {allowTsInNodeModules: true}}, + {test: /\.css$/i, use: ['style-loader', 'css-loader']}, + ], + }, + plugins: [ + new FuncGeneratorPlugin({outputPath: './src/package.g.ts'}), + ], + devtool: 'source-map', + externals: { + 'datagrok-api/dg': 'DG', + 'datagrok-api/grok': 'grok', + 'datagrok-api/ui': 'ui', + 'openchemlib/full.js': 'OCL', + 'rxjs': 'rxjs', + 'rxjs/operators': 'rxjs.operators', + 'cash-dom': '$', + 'dayjs': 'dayjs', + 'wu': 'wu', + 'exceljs': 'ExcelJS', + 'html2canvas': 'html2canvas', + }, + output: { + filename: '[name].js', + library: packageName, + libraryTarget: 'var', + path: path.resolve(__dirname, 'dist'), + }, +}; diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/levins-metapopulation-spec.md b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/levins-metapopulation-spec.md new file mode 100644 index 0000000000..37673a36b7 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/example/levins-metapopulation-spec.md @@ -0,0 +1,702 @@ +# Application Specification: Levins Metapopulation Model + +## 1. General Information + +| Field | Value | +|---|---| +| Application name | Levins Metapopulation Model | +| Package | InteractiveSciAppTest | +| Entry function | `levinsMetapopulationApp()` | +| Brief description | Interactive ODE solution for the Levins model: simulation of occupied patch fraction dynamics p(t) with support for the basic model and the extended model (rescue effect). | +| Main view | `DG.TableView` | + +--- + +## 2. Computational Tasks (Core) + +### 2.1. Task List + +| Task ID | Name | Pipeline type | Trigger | +|---|---|---|---| +| `task_primary` | Levins model ODE solution | Primary (reactive) | Any input change | +| `task_optimize` | Find optimal m by p(t_end) | Secondary (on demand) | Button `btn_optimize` | + +### 2.2. Task Description: `task_primary` + +**General characteristics:** + +| Field | Value | +|---|---| +| Name | Levins model ODE solution | +| Description | Numerical solution of ODE `dp/dt = m·p·(1−p) − e(p)·p`, returning the trajectory p(t) and the equilibrium value p* | +| Synchronicity | Synchronous | +| Execution environment | Main thread | +| Parallelization | No | +| Dependency on other tasks | No | + +**Input parameters:** + +| Parameter | Type | Units | Domain | Description | +|---|---|---|---|---| +| `p0` | `number` | dimensionless | `(0, 1]` | Initial fraction of occupied patches | +| `m` | `number` | 1/time | `> 0` | Colonization rate | +| `e0` | `number` | 1/time | `> 0` | Baseline extinction rate | +| `rescueEffect` | `boolean` | — | — | Enable rescue effect: `e(p) = e0·(1−p)` | +| `t_start` | `number` | time | `≥ 0` | Start of integration interval | +| `t_end` | `number` | time | `> t_start` | End of interval | +| `t_step` | `number` | time | `> 0, < t_end − t_start` | Grid step | +| `tolerance` | `number` | dimensionless | `> 0` | Numerical tolerance of the MRT method | + +**Output data:** + +| Parameter | Type | Description | +|---|---|---| +| `t` | `Float64Array` | Values of argument t | +| `p` | `Float64Array` | Values of p(t) | +| `p_star` | `number` | Equilibrium value: `1 − e0/m` (for the basic model) | + +**Output properties (invariants):** + +- `p(t) ∈ [0, 1]` for all `t` — the fraction of occupied patches is bounded. +- `p(0) = p0` — the initial condition is preserved. +- For the basic model with `m > e0`: `p(t) → p* = 1 − e0/m` as `t → ∞` — convergence to equilibrium. +- Higher `m` (other parameters fixed) → higher `p(t_end)` — monotonicity in colonization rate. + +**Computation implementation:** + +| Step | Implementation method | Details | Documentation | +|---|---|---|---| +| 1 | External library | `diff-grok` v1.2.0, function `mrt(task: ODEs)`. MRT is an A-stable implicit method suitable for both stiff and non-stiff ODEs. | [diff-grok README](https://github.com/datagrok-ai/diff-grok) | +| 2 | Custom method | Computation of `p_star = 1 − e0/m` (basic model); for rescue effect (`e(p) = e0·(1−p)`), no closed-form equilibrium exists — `p_star` is not computed | — | + +**ODE right-hand side reference examples:** + +| # | Mode | m | e0 | p | Hand-calculated dp/dt | Derivation | +|---|---|---|---|---|---|---| +| 1 | Base | 0.5 | 0.2 | 0.5 | `0.5·0.5·0.5 − 0.2·0.5 = 0.025` | `m·p·(1−p) − e0·p` | +| 2 | Base | 1.0 | 0.3 | 0.1 | `1.0·0.1·0.9 − 0.3·0.1 = 0.06` | `m·p·(1−p) − e0·p` | +| 3 | Base | 0.5 | 0.2 | 0.6 | `0.5·0.6·0.4 − 0.2·0.6 = 0.0` | At equilibrium `p* = 1 − e0/m = 0.6` | +| 4 | Rescue | 0.5 | 0.2 | 0.5 | `0.5·0.5·0.5 − 0.2·0.5·0.5 = 0.075` | `e = e0·(1−p) = 0.1` | +| 5 | Rescue | 0.3 | 0.5 | 0.8 | `0.3·0.8·0.2 − 0.5·0.2·0.8 = −0.032` | `e = e0·(1−p) = 0.1` | + +**Library call:** + +```typescript +import { ODEs, mrt } from 'diff-grok'; + +const task: ODEs = { + name: 'Levins', + arg: { name: 't', start: t_start, finish: t_end, step: t_step }, + initial: [p0], + func: (t, y, out) => { + const e = rescueEffect ? e0 * (1 - y[0]) : e0; + out[0] = m * y[0] * (1 - y[0]) - e * y[0]; + }, + tolerance: tolerance, + solutionColNames: ['p(t)'], +}; +const solution = mrt(task); +``` + +--- + +### 2.3. Task Description: `task_optimize` + +**General characteristics:** + +| Field | Value | +|---|---| +| Name | Find optimal m by p(t_end) | +| Description | For 10000 uniformly distributed values of m in [min, max], solves the ODE, computes p(t_end), returns m with the maximum p(t_end) | +| Synchronicity | Asynchronous | +| Execution environment | WebWorkers (parallel) | +| Parallelization | Yes — 10000 independent tasks, worker pool of size `Math.max(1, navigator.hardwareConcurrency - 2)` | +| Dependency on other tasks | Uses current values of all `task_primary` inputs except `m` | + +**Input parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `m_min` | `number` | Lower bound for m search | +| `m_max` | `number` | Upper bound for m search | +| `p0, e0, rescueEffect` | `number / boolean` | Taken from current state of main UI controls (snapshot) | +| `t_start, t_end, t_step, tolerance` | `number` | Taken from current state of main UI controls (snapshot) | + +**Output data:** + +| Parameter | Type | Description | +|---|---|---| +| `m_optimal` | `number` | Value of m at which p(t_end) is maximized | +| `p_at_t_end_max` | `number` | Achieved maximum value of p(t_end) | + +**Computation implementation:** + +| Step | Implementation method | Details | Documentation | +|---|---|---|---| +| 1 | Custom method | Generate 10000 points: `m_i = m_min + i · (m_max − m_min) / 9999`, i = 0..9999 | — | +| 2 | External library in workers | `diff-grok` v1.2.0, `mrt(task)` — for each point `m_i` in a WebWorker. Passed through pipeline API: `getIvp2WebWorker(ivp)` | [diff-grok pipeline](https://github.com/datagrok-ai/diff-grok) | +| 3 | Custom method | After receiving all results: `m_optimal = m_i` where `p_end` is maximal | — | +| 4 | Datagrok API | Write `m_optimal` to `ctrl_m` via batch update (section 7) | [Datagrok JS API](https://datagrok.ai/api/js/) | + +**Parallelization strategy:** + +``` +Number of workers = Math.max(1, navigator.hardwareConcurrency - 2) + +10000 values of m_i + → worker pool + → tasks are distributed to workers as they become available (queue) + → each worker receives: { m_i, p0, e0, rescueEffect, + t_start, t_end, t_step, tolerance } + → each worker returns: { m_i, p_end } + → as each task completes: progressBar += 1/10000 + → after all 10000: find max(p_end) → m_optimal +``` + +### 2.4. Dependencies Between Tasks + +``` +task_primary — independent +task_optimize — does not depend on task_primary results; + after completion, triggers a single run of task_primary +``` + +--- + +## 3. Controls + +### 3.1. Primary Pipeline Controls + +| ID | Name (label) | Control type | Data type | Default | Min | Max | Format | Nullable | Tooltip text | Group | +|---|---|---|---|---|---|---|---|---|---|---| +| `ctrl_p0` | Initial patch fraction p₀ | `ui.input.float` | `number` | `0.5` | `0.001` | `1` | `0.000` | No | Fraction of patches occupied at time t=0. At p₀=0 the population will not recover — computation is not performed. | Initial condition | +| `ctrl_m` | Colonization rate m | `ui.input.float` | `number` | `0.5` | `0.001` | `100` | `0.000` | No | How quickly free patches are colonized from occupied ones. Units: 1/time. | Parameters | +| `ctrl_e0` | Extinction rate e₀ | `ui.input.float` | `number` | `0.2` | `0.001` | `100` | `0.000` | No | Baseline rate of local subpopulation extinction in a patch. With rescue effect — decreases as p grows. Units: 1/time. | Parameters | +| `ctrl_rescue` | Rescue effect | `ui.input.toggle` | `boolean` | `false` | — | — | — | No | If enabled, the extinction rate depends on p: e(p) = e₀·(1−p). The more occupied patches — the lower the local extinction. | Parameters | +| `ctrl_t_start` | Start t₀ | `ui.input.float` | `number` | `0` | `0` | `10000` | `0.0` | No | Simulation start time. Usually 0. | Argument | +| `ctrl_t_end` | End t_end | `ui.input.float` | `number` | `50` | `0.1` | `10000` | `0.0` | No | End time. Recommended ≥ 5/e₀ so the system reaches equilibrium. | Argument | +| `ctrl_t_step` | Step Δt | `ui.input.float` | `number` | `0.1` | `0.001` | `1000` | `0.000` | No | Numerical solution grid step. Affects chart detail but not stability (MRT is an implicit method). | Argument | +| `ctrl_tolerance` | Tolerance | `ui.input.float` | `number` | `1e-7` | `1e-12` | `1e-2` | `0.##E+0` | No | Numerical tolerance of the MRT method. Smaller — more precise, but slower. Recommended 1e-6 … 1e-9. | Solver | + +### 3.2. Secondary Task Triggers + +| ID | Name / icon | Triggers task | Tooltip text | Availability condition | +|---|---|---|---|---| +| `btn_optimize` | `ui.iconFA('search')` | `task_optimize` | Find the value of m that maximizes the fraction of occupied patches at time t_end | Always | + +### 3.3. Controls for `task_optimize` + +UI type: Datagrok dialog window. + +| ID | Name (label) | Control type | Data type | Default | Min | Max | Format | Nullable | Tooltip text | +|---|---|---|---|---|---|---|---|---|---| +| `dlg_m_min` | Minimum m | `ui.input.float` | `number` | `0.1` | `0.001` | `100` | `0.000` | No | Lower bound of the m search range. Must be less than the maximum value. | +| `dlg_m_max` | Maximum m | `ui.input.float` | `number` | `1.0` | `0.001` | `100` | `0.000` | No | Upper bound of the m search range. Must be greater than the minimum value. | + +**Dialog buttons:** + +| ID | Name | Action | Availability condition | +|---|---|---|---| +| `dlg_btn_ok` | OK | Close dialog → launch `task_optimize` | Only if there are no validation errors | +| `dlg_btn_cancel` | Cancel | Close dialog, task is not launched | Always | + +### 3.4. Other Buttons and Actions + +| ID | Name / icon | Action | Tooltip text | Availability condition | +|---|---|---|---|---| +| `btn_reset` | `ui.iconFA('undo')` | Reset all controls to default values | Reset all parameters to default values | Always | + +### 3.5. Custom UI Components + +| Component ID | Brief description | Role | UI component specification | +|---|---|---|---| +| `comp_rho_badge` | Indicator for ρ = e₀/m with color status (green / red) | Display | — | + +> **Note on `comp_rho_badge`:** displays the current value of ρ = e₀/m in real time. Green — the metapopulation persists (ρ < 1), red — heading toward extinction (ρ ≥ 1). Updated reactively when `ctrl_m` or `ctrl_e0` changes. +> +> **Tooltip:** "Ratio of extinction rate to colonization rate. ρ < 1 — metapopulation persists, ρ ≥ 1 — extinction." + +--- + +## 4. Validation + +### 4.1. Primary Pipeline Validation (`task_primary`) + +#### Complex Validation Rules + +| Rule ID | Condition (invalid) | Affected inputs (ID) | Error message | +|---|---|---|---| +| `val_01` | `p0 ≤ 0` | `ctrl_p0` | "Initial patch fraction must be greater than 0" | +| `val_02` | `p0 > 1` | `ctrl_p0` | "Initial patch fraction cannot exceed 1" | +| `val_03` | `m ≤ 0` | `ctrl_m` | "Colonization rate must be positive" | +| `val_04` | `e0 ≤ 0` | `ctrl_e0` | "Extinction rate must be positive" | +| `val_05` | `m ≤ e0` (when `rescueEffect = false`) | `ctrl_m`, `ctrl_e0` | "Colonization rate must exceed extinction rate (m > e₀). At current values, the metapopulation is heading toward extinction" | +| `val_06` | `t_end ≤ t_start` | `ctrl_t_end`, `ctrl_t_start` | "End of interval must be greater than start" | +| `val_07` | `t_step ≤ 0` | `ctrl_t_step` | "Step must be positive" | +| `val_08` | `t_step ≥ t_end − t_start` | `ctrl_t_step` | "Step must be less than the interval length" | +| `val_09` | `tolerance ≤ 0` | `ctrl_tolerance` | "Tolerance must be positive" | + +#### Validation Order + +``` +1. val_01, val_02 (single, ctrl_p0) +2. val_03 (single, ctrl_m) +3. val_04 (single, ctrl_e0) +4. val_05 (combinatorial ctrl_m + ctrl_e0 — only if val_03 and val_04 passed) +5. val_06 (combinatorial ctrl_t_start + ctrl_t_end) +6. val_07 (single, ctrl_t_step) +7. val_08 (combinatorial ctrl_t_step + ctx — only if val_06 and val_07 passed) +8. val_09 (single, ctrl_tolerance) +``` + +#### Return Map Format + +``` +Map +``` + +### 4.2. Validation for `task_optimize` + +#### Complex Validation Rules + +| Rule ID | Condition (invalid) | Affected inputs (ID) | Error message | +|---|---|---|---| +| `opt_val_01` | `m_min ≤ 0` | `dlg_m_min` | "Colonization rate must be positive" | +| `opt_val_02` | `m_max ≤ 0` | `dlg_m_max` | "Colonization rate must be positive" | +| `opt_val_03` | `m_min ≥ m_max` | `dlg_m_min`, `dlg_m_max` | "Minimum value must be less than maximum" | +| `opt_val_04` | `m_max ≤ e0` (when `rescueEffect = false`) | `dlg_m_max` | "At the current e₀, the entire m range leads to extinction (m ≤ e₀). Increase the maximum or decrease e₀" | + +> `opt_val_04` — warning, does not block OK. + +#### Validation Order + +``` +1. opt_val_01 (single) +2. opt_val_02 (single) +3. opt_val_03 (combinatorial — only if 01 and 02 passed) +4. opt_val_04 (combinatorial with external parameter e₀ — only if 01-03 passed) +``` + +--- + +## 5. Reactivity and Input Dependencies + +### 5.1. Dependency Graph + +| Source (input ID) | Target (input IDs / components) | Reaction type | Logic | +|---|---|---|---| +| `ctrl_m` | `comp_rho_badge` | Display update | Recalculate ρ = e₀ / m, update badge value and color | +| `ctrl_e0` | `comp_rho_badge` | Display update | Same | +| `ctrl_m` | `comp_rho_badge` | Availability update | If m ≤ e₀ → badge is red, otherwise green | +| `ctrl_e0` | `comp_rho_badge` | Availability update | Same | +| `ctrl_rescue` | `ctrl_e0` (label + tooltip) | Label update | If `rescue = true` → label changes to "Baseline extinction rate e₀", tooltip adds: "Effective rate: e(p) = e₀·(1−p)" | +| `ctrl_t_start` | `ctrl_t_end` | Range update | Minimum allowed value of `ctrl_t_end` = `t_start + t_step` | +| `ctrl_t_start` | `ctrl_t_step` | Range update | Maximum allowed value of `ctrl_t_step` = `t_end − t_start` | +| `ctrl_t_end` | `ctrl_t_step` | Range update | Same: `ctrl_t_step` ≤ `t_end − t_start` | +| `dlg_m_min` | `dlg_m_max` | Range update | Minimum allowed value of `dlg_m_max` = `m_min + ε` | +| `dlg_m_max` | `dlg_m_min` | Range update | Maximum allowed value of `dlg_m_min` = `m_max − ε` | +| `dlg_m_min` | `dlg_btn_ok` | Availability update | OK button is available only if there are no validation errors `opt_val_01–03` | +| `dlg_m_max` | `dlg_btn_ok` | Availability update | Same | + +### 5.2. Debounce / Throttle + +| Input ID | Strategy | Interval (ms) | +|---|---|---| +| `ctrl_p0` | debounce | 50 | +| `ctrl_m` | debounce | 50 | +| `ctrl_e0` | debounce | 50 | +| `ctrl_t_start` | debounce | 50 | +| `ctrl_t_end` | debounce | 50 | +| `ctrl_t_step` | debounce | 50 | +| `ctrl_tolerance` | debounce | 50 | +| `ctrl_rescue` | none | — | +| `dlg_m_min` | debounce | 50 | +| `dlg_m_max` | debounce | 50 | + +--- + +## 6. Behavior During Computations + +### 6.1. Primary Pipeline (`task_primary`) + +#### Control Blocking + +| Control ID | Blocked | Note | +|---|---|---| +| `ctrl_p0` | No | Computation takes < 100 ms — blocking is unnecessary | +| `ctrl_m` | No | Same | +| `ctrl_e0` | No | Same | +| `ctrl_rescue` | No | Same | +| `ctrl_t_start` | No | Same | +| `ctrl_t_end` | No | Same | +| `ctrl_t_step` | No | Same | +| `ctrl_tolerance` | No | Same | +| `btn_optimize` | No | — | +| `btn_reset` | No | — | + +#### Progress Bar + +| Field | Value | +|---|---| +| Display | No | +| Type | — | +| Cancellation support | No | + +#### Error Behavior + +Selected strategy: **Reset results + message**. + +| Strategy | Description | +|---|---| +| Reset results | Clear the p(t) chart and the p* value | +| Message | `grok.shell.error(msg)` — Datagrok platform toast notification with the error text | + +### 6.2. Secondary Pipeline (`task_optimize`) + +#### Control Blocking + +| Control ID | Blocked | Note | +|---|---|---| +| `btn_optimize` | Yes | Re-launch is not possible until completion or cancellation | +| All other controls | No | The main UI remains fully accessible | + +> Changing inputs while `task_optimize` is running triggers a `task_primary` recalculation in normal mode but does not affect the already running search — it uses the parameter snapshot from the moment OK was pressed. + +#### Progress Bar + +| Field | Value | +|---|---| +| Display | Yes | +| Type | Determinate (0–100%, +1/10000 for each completed worker) | +| Cancellation support | Yes — Cancel button terminates all active workers | + +#### Error Behavior + +Selected strategy: **Last valid + message**. + +| Strategy | Description | +|---|---| +| Last valid | The value of `ctrl_m` is not changed | +| Message | `grok.shell.error` with error text | + +--- + +## 7. Computation Blocking and Batch Update + +### 7.1. Batch Update Scenarios + +| Source (task) | Target controls (ID) | Locked pipelines | +|---|---|---| +| `task_optimize` | `ctrl_m` | Primary (blocked) | + +### 7.2. Reactivity Mode During Batch Update + +| Scenario | Reactivity mode | +|---|---| +| Writing `m_optimal` → `ctrl_m` | Primary pipeline is paused during writing, then runs once with the new value | + +--- + +## 8. Result Display + +### 8.1. Primary Pipeline Display Elements + +| ID | Type | Associated output data | Docking location | +|---|---|---|---| +| `view_p_t` | Datagrok viewer `line chart` | `task_primary.t`, `task_primary.p` | Main area | +| `view_rho_badge` | Custom HTMLElement `comp_rho_badge` | `ctrl_e0`, `ctrl_m` (displays ρ = e₀/m) | Left panel, below "Parameters" group controls | + +**Details for `view_p_t`:** + +| Property | Value | +|---|---| +| X axis | `t`, label "Time" | +| Y axis | `p(t)`, range `[0, 1]`, label "Fraction of occupied patches" | +| Series | `p(t)` — main trajectory, solid line | +| Update | Reactive — redrawn on each `task_primary` completion | + +**Color coding:** + +The `p` column of the results table receives `colorCoding` by value: + +| Range | Color | Meaning | +|---|---|---| +| `p < e₀/m` | Red | Extinction threat zone | +| `p ≥ e₀/m` | Green | Persistence zone | + +> The threshold value `e₀/m` is recalculated and updated in `colorCoding` on each change of `ctrl_m` or `ctrl_e0`. + +**Tooltip for `p` column header:** + +The `p` column header in the grid receives a tooltip explaining the color coding: + +``` +Fraction of occupied patches p(t). +Color: green — persistence zone (p ≥ e₀/m), +red — extinction threat zone (p < e₀/m). +Threshold: e₀/m = {current ρ value}. +``` + +> The tooltip text is updated reactively when `ctrl_m` or `ctrl_e0` changes. + +### 8.2. Display Elements for `task_optimize` + +Where results are displayed: toast notification + writing to the main control. + +| ID | Type | Associated output data | Placement | +|---|---|---|---| +| `view_optimize_result` | `grok.shell.info` | `task_optimize.m_optimal`, `task_optimize.p_at_t_end_max` | Datagrok platform toast notification | +| `ctrl_m` | `ui.input.float` (main UI control) | `task_optimize.m_optimal` | Left panel, "Parameters" group | + +**Toast notification text:** +``` +Optimal m = {m_optimal} +p(t_end) = {p_at_t_end_max} +``` + +**Behavior after writing the result:** + +``` +1. m_optimal → ctrl_m (batch update, section 7) +2. task_primary runs once with the new m +3. view_p_t is redrawn with the new trajectory +4. grok.shell.info is shown +``` + +--- + +## 9. Layout + +### 9.1. Control Placement + +| Area | Content | Docking | Ratio | +|---|---|---|---| +| Left panel | `ui.form` with groups separated by `ui.h2` headers | `DG.DOCK_TYPE.LEFT` | `0.2` | +| Toolbar | `btn_optimize`, `btn_reset` | — | — | +| Main area (grid) | `DG.TableView` (results table) | Default | — | +| Right area | `view_p_t` (line chart) | `DG.DOCK_TYPE.RIGHT` (relative to grid) | `0.5` | + +**Structure of `ui.form` in the left panel:** + +``` +ui.h2('Initial condition') + ctrl_p0 + +ui.h2('Parameters') + ctrl_m + ctrl_e0 + ctrl_rescue + comp_rho_badge + +ui.h2('Argument') + ctrl_t_start + ctrl_t_end + ctrl_t_step + +ui.h2('Solver') + ctrl_tolerance +``` + +### 9.2. Display Element Placement + +| Element ID | Type | Area | Note | +|---|---|---|---| +| `view_p_t` | Viewer `line chart` | Right area (dock right, ratio `0.5`) | Docked to the right of the grid, splitting space 50/50 | +| `view_optimize_result` | `grok.shell.info` | — | Datagrok platform toast, placed automatically by the platform | + +--- + +## 10. Data Lifecycle + +### 10.1. Data Input + +Primary method: manual input via `ui.form` controls (section 3). + +Initial application state: all controls are initialized with default values from section 3.1 at the time `levinsMetapopulationApp()` is called. `task_primary` runs automatically immediately after initialization. + +### 10.2. Loading from Resources + +External data loading is not supported. + +| Trigger | Resource | Format | Mapping to inputs | +|---|---|---|---| +| — | — | — | — | + +### 10.3. Results Table Lifecycle + +``` +1. Application initialization + → an empty DG.DataFrame is created with columns [t, p] + → the DataFrame is added to the TableView + +2. task_primary completion + → the DataFrame is updated: columns [t, p] are overwritten with new Float64Array + → colorCoding for column p is recalculated (threshold e₀/m) + → view_p_t is redrawn reactively + +3. task_optimize completion + → m_optimal is written to ctrl_m (batch update, section 7) + → task_primary runs once → DataFrame is updated per step 2 + → grok.shell.info is shown + +4. btn_reset press + → all controls are reset to default values + → task_primary runs → DataFrame is updated per step 2 + +5. task_primary error + → the DataFrame is cleared (columns [t, p] are zeroed out) + → view_p_t displays an empty chart + → grok.shell.error(msg) shows a toast notification with the error text +``` + +### 10.4. Data Lifecycle for `task_optimize` + +``` +1. Pressing OK in the dialog + → a snapshot of current parameters { p0, e0, rescueEffect, + t_start, t_end, t_step, tolerance } is captured + → an array of 10000 m_i values is generated + +2. Worker execution + → each worker receives { m_i, snapshot } + → each worker returns { m_i, p_end } + → intermediate results are not stored anywhere + +3. All workers complete + → m_optimal = m_i at max(p_end) is computed + → the array { m_i, p_end } is freed from memory + +4. Cancellation (progress bar Cancel) + → all active workers are terminated + → intermediate results are discarded + → ctrl_m is not changed +``` + +--- + +## 11. Error Handling Beyond Computations + +| Error type | Strategy | Notification method | +|---|---|---| +| `diff-grok` initialization error (library failed to load) | Lock `btn_optimize` and all controls, show message | `grok.shell.error`: "Failed to load the solver library. Reload the page." | +| WebWorker creation error (browser does not support or limit exceeded) | Abort `task_optimize`, do not change `ctrl_m` | `grok.shell.error`: "Failed to start parallel computations. Try again later." | +| Worker terminated with error (one or more m_i points) | Skip the point, continue remaining workers, consider only valid results | `grok.shell.warning`: "{N} out of 10000 points were not computed. Result obtained from {10000−N} points." | +| All 10000 workers terminated with error | Abort `task_optimize`, do not change `ctrl_m` | `grok.shell.error`: "Failed to compute any points. Check the parameters." | +| Invalid application state (snapshot contains invalid values) | Abort `task_optimize` before creating workers | `grok.shell.error`: "Internal error: invalid task parameters. Check inputs and retry." | +| Error updating `ctrl_m` after `task_optimize` completion | Show result via `grok.shell.info`, do not write to control | `grok.shell.warning`: "Optimal m = {m_optimal}, but the field could not be updated automatically. Enter the value manually." | + +--- + +## 12. UX + +### 12.1. Keyboard Shortcuts + +| Combination | Action | +|---|---| +| — | — | + +### 12.2. Context Menus + +| Context (element) | Menu items | +|---|---| +| — | — | + +### 12.3. Undo / Redo + +Supported: **No**. + +--- + +## 13. Testing + +### 13.1. Mathematical Verification + +Tests in this section verify that the implementation matches the mathematical model. Verification criteria are defined by the model (section 2.2: ODE, output properties, reference examples). + +#### 13.1.1. ODE Right-Hand Side Verification (formula verification) + +Verifies that the implemented ODE function produces correct `dp/dt` at specific points. Reference values are hand-calculated (see section 2.2, "ODE right-hand side reference examples"). + +| Test ID | Mode | Input `(m, e0, p)` | Expected `dp/dt` | +|---|---|---|---| +| `func_01` | Base | `(0.5, 0.2, 0.5)` | `0.025` | +| `func_02` | Base | `(1.0, 0.3, 0.1)` | `0.06` | +| `func_03` | Base (equilibrium) | `(0.5, 0.2, 0.6)` | `0.0` | +| `func_04` | Rescue | `(0.5, 0.2, 0.5)` | `0.075` | +| `func_05` | Rescue | `(0.3, 0.5, 0.8)` | `−0.032` | + +#### 13.1.2. Equilibrium Verification + +| Test ID | Description | Input data | Expected result | +|---|---|---|---| +| `eq_01` | Basic model equilibrium | `m=0.5, e0=0.2, rescue=false` | `p* = 0.6` | +| `eq_02` | p* = 0 when m ≤ e0 | `m=0.2, e0=0.5, rescue=false` | `p* = 0` | +| `eq_03` | p* = 0 when m = e0 | `m=0.5, e0=0.5, rescue=false` | `p* = 0` | +| `eq_04` | NaN with rescue effect | `m=0.5, e0=0.2, rescue=true` | `NaN` | + +#### 13.1.3. Output Property Verification (`solve`) + +Verifies that output invariants declared in section 2.2 ("Output properties") hold on actual `solve()` results. + +| Test ID | Description | Input data | Verified property | +|---|---|---|---| +| `solve_01` | Default parameters | DEFAULTS | `t.length > 0`, `p.length > 0`, `t.length = p.length` | +| `solve_02` | p values in [0, 1] | DEFAULTS | All `p[i] ∈ [0, 1]` | +| `solve_03` | p(0) = p0 | DEFAULTS | `p[0] ≈ 0.5` | +| `solve_04` | t starts at t_start | DEFAULTS | `t[0] = 0` | +| `solve_05` | Convergence to p* | `m=0.5, e0=0.2, t_end=200` | `p(t_end) ≈ p*` (tolerance 0.01) | +| `solve_06` | Rescue effect: p in [0, 1] | `m=0.3, e0=0.5, rescue=true, t_end=100` | All `p[i] ∈ [0, 1]` | +| `solve_07` | Higher m → higher p(t_end) | `m=0.5` vs `m=1.0`, `e0=0.2` | `p_end(m=1) > p_end(m=0.5)` | +| `solve_08` | Custom p0 | `p0=0.9` | `p[0] ≈ 0.9` | + +#### 13.1.4. Numerical Method Verification (MRT solver) + +Verifies that the `mrt` solver from `diff-grok` produces correct results on reference problems with known analytical solutions. + +| Test ID | Description | Reference | Expected | +|---|---|---|---| +| `mrt_01` | Non-stiff 1D: `dy/dt = 4·exp(0.8t) − 0.5y` | Chapra & Canale, p. 736 | Max absolute error < 0.1 | +| `mrt_02` | Stiff 1D: `dy/dt = −1000y + 3000 − 2000·exp(−t)` | Chapra & Canale, p. 767 | Max absolute error < 0.1 | +| `mrt_03` | Stiff 2D: van der Pol (µ=1000) | [VDPOL test set](https://archimede.uniba.it/~testset/report/vdpol.pdf) | Solver completes without divergence | + +### 13.2. Validation for task_primary + +| Test ID | Rule | Input data | Expected result | +|---|---|---|---| +| `v_01a` | val_01 | `p0=0` | Error on `ctrl_p0` | +| `v_01b` | val_01 | `p0=-1` | Error on `ctrl_p0` | +| `v_02` | val_02 | `p0=1.1` | Error on `ctrl_p0` | +| `v_p0_lo` | boundary | `p0=0.001` | No error on `ctrl_p0` | +| `v_p0_hi` | boundary | `p0=1` | No error on `ctrl_p0` | +| `v_03a` | val_03 | `m=0` | Error on `ctrl_m` | +| `v_03b` | val_03 | `m=-0.5` | Error on `ctrl_m` | +| `v_04a` | val_04 | `e0=0` | Error on `ctrl_e0` | +| `v_04b` | val_04 | `e0=-0.1` | Error on `ctrl_e0` | +| `v_05a` | val_05 | `m=0.5, e0=0.5, rescue=false` | Error on `ctrl_m` | +| `v_05b` | val_05 | `m=0.3, e0=0.5, rescue=false` | Error on `ctrl_m` | +| `v_05c` | val_05 (rescue) | `m=0.3, e0=0.5, rescue=true` | No error | +| `v_05d` | val_05 dependency | `m=0, e0=0.5` | Message val_03, not val_05 | +| `v_06a` | val_06 | `t_start=10, t_end=10` | Error on `ctrl_t_end` and `ctrl_t_start` | +| `v_06b` | val_06 | `t_start=10, t_end=5` | Error on `ctrl_t_end` | +| `v_07a` | val_07 | `t_step=0` | Error on `ctrl_t_step` | +| `v_07b` | val_07 | `t_step=-0.1` | Error on `ctrl_t_step` | +| `v_08a` | val_08 | `t_step=50 (= t_end-t_start)` | Error on `ctrl_t_step` | +| `v_08b` | val_08 | `t_step=100 (> t_end-t_start)` | Error on `ctrl_t_step` | +| `v_08c` | val_08 dependency val_06 | `t_start=10, t_end=5, t_step=100` | No error on `ctrl_t_step` | +| `v_08d` | val_08 dependency val_07 | `t_step=-1` | Message val_07, not val_08 | +| `v_09a` | val_09 | `tolerance=0` | Error on `ctrl_tolerance` | +| `v_09b` | val_09 | `tolerance=-1e-7` | Error on `ctrl_tolerance` | +| `v_def` | all defaults | DEFAULTS | `errors.size = 0` | +| `v_multi` | multiple | `p0=0, m=0, e0=0, t_step=0, tolerance=0` | ≥ 4 errors | + +### 13.3. Validation for task_optimize + +| Test ID | Rule | Input data | Expected result | +|---|---|---|---| +| `ov_01` | opt_val_01 | `m_min=0` | Error on `dlg_m_min` | +| `ov_02` | opt_val_02 | `m_max=-1` | Error on `dlg_m_max` | +| `ov_03a` | opt_val_03 | `m_min=0.5, m_max=0.5` | Error on `dlg_m_min` | +| `ov_03b` | opt_val_03 | `m_min=1.0, m_max=0.5` | Error on `dlg_m_min` | +| `ov_04a` | opt_val_04 | `m_max=0.1, e0=0.2, rescue=false` | Warning ≠ null | +| `ov_04b` | opt_val_04 (rescue) | `m_max=0.1, e0=0.2, rescue=true` | Warning = null | +| `ov_valid` | valid | `m_min=0.1, m_max=1.0, e0=0.2` | `errors.size = 0`, `warning = null` | diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/ARRAY-OPERATIONS.md b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/ARRAY-OPERATIONS.md new file mode 100644 index 0000000000..abbcc6e003 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/ARRAY-OPERATIONS.md @@ -0,0 +1,305 @@ +# Array Operations Guide + +Reference for implementing efficient array operations in Datagrok packages. + +> **End-to-end example:** typed arrays in `../example/code/src/levins/core.ts` (`Float64Array` results) and `../example/code/src/levins/optimize-worker.ts`. + +For raw data access and null handling, see `COMPUTATION-PATTERNS.md`. +For worker-specific patterns, see `WORKER-GUIDE.md`. + +## Pre-allocate and Reuse + +The core principle: allocate buffers once before the loop, reuse them across iterations. +Every `new Float32Array(n)` inside a loop is a hidden cost — allocation + eventual GC pause. + +```typescript +// Bad: allocation per iteration +for (let iter = 0; iter < maxIter; iter++) { + const temp = new Float32Array(n); // GC pressure grows with maxIter + // ... use temp ... +} + +// Good: single allocation, reused across iterations +const temp = new Float32Array(n); +for (let iter = 0; iter < maxIter; iter++) { + // ... use temp — same memory, zero allocations ... +} +``` + +Reference: `probabilistic-scoring/nelder-mead.ts` (`nelderMead`) — `centroid`, `reflectionPoint`, +`expansionPoint`, `contractionPoint` are allocated once and reused across all Nelder-Mead iterations. + +--- + +## Out-Parameter Pattern + +Write results into a caller-provided array instead of allocating and returning a new one. +This gives the caller control over allocation and enables buffer reuse. + +```typescript +function add(a: Float32Array, b: Float32Array, out: Float32Array, len: number): void { + for (let i = 0; i < len; i++) out[i] = a[i] + b[i]; +} + +const buf = new Float32Array(n); +add(x, y, buf, n); // buf = x + y +scale(buf, 2.0, buf, n); // buf = 2 * (x + y), in-place +``` + +Reference: `probabilistic-scoring/nelder-mead.ts` — `fillPoint` and `fillCentroid` write into +pre-allocated arrays, called repeatedly inside the optimization loop. + +--- + +## Scratch Buffers for Iterative Algorithms + +When an algorithm runs many iterations, declare all temporary arrays before the loop. + +```typescript +// softmax-worker.ts: Z, dZ, dW allocated once before training loop +const Z = new Array(m); +for (let i = 0; i < m; i++) Z[i] = new Float32Array(c); +const dZ = new Array(c); +for (let i = 0; i < c; i++) dZ[i] = new Float32Array(m); + +for (let iter = 0; iter < iterations; iter++) { + // Forward/backward pass writes into Z, dZ — zero allocations per iteration +} +``` + +Reference: `workers/softmax-worker.ts` (`onmessage` handler, buffer allocation before training loop). + +--- + +## Local Aliases for Inner Loops + +Store a reference to a sub-array in a local variable before the inner loop. +The primary benefit is **readability and reduced index errors**: `wBuf[k] * xBuf[k]` is +clearer than `params[i][k] * X[j][k]`, and there is less chance of mixing up `i`/`j` indices. + +> **Note on performance:** Modern V8 often hoists loop-invariant array lookups automatically +> (loop-invariant code motion), so the performance gain may be minimal. Use this pattern +> primarily for clarity in multi-level loops. + +```typescript +// Before: dense indexing, easy to confuse i/j +for (let j = 0; j < m; j++) + for (let k = 0; k < n; k++) + sum += params[i][k] * X[j][k]; + +// After: meaningful names, less index juggling +for (let j = 0; j < m; j++) { + const xBuf = X[j]; // alias, not copy + const wBuf = params[i]; + for (let k = 0; k < n; k++) + sum += wBuf[k] * xBuf[k]; +} +``` + +Reference: `workers/softmax-worker.ts` (forward propagation loop) — `xBuf`, `wBuf`, `zBuf` aliases. + +--- + +## Accumulation into Pre-allocated Output + +Allocate the output array once and accumulate contributions in-place. + +```typescript +// regression.ts: prediction = bias + sum(weight_j * feature_j) +const prediction = new Float32Array(samplesCount); +let rawData = features.byIndex(0).getRawData(); +const bias = params[featuresCount]; + +for (let i = 0; i < samplesCount; i++) + prediction[i] = bias + params[0] * rawData[i]; + +for (let j = 1; j < featuresCount; j++) { + rawData = features.byIndex(j).getRawData(); + for (let i = 0; i < samplesCount; i++) + prediction[i] += params[j] * rawData[i]; +} +``` + +Reference: `regression.ts` (`getPredictionByLinearRegression`). + +--- + +## Logical Length vs Physical Length + +Pre-allocated buffers may have a fixed physical size but a variable logical length. +Track the logical length separately and use it for all iteration bounds. + +```typescript +const properIndices = new Uint32Array(featuresCount); +let properIndicesCount = 0; + +const getProperIndices = (idx: number) => { + properIndicesCount = 0; // reset logical length + for (let i = 0; i < featuresCount; i++) { + if (featureSource[i][idx] !== featureNullVal[i]) + properIndices[properIndicesCount++] = i; + } +}; + +// Later: iterate only over valid elements +for (let i = 0; i < properIndicesCount; i++) + sum += bufferVector[properIndices[i]]; +``` + +Reference: `missing-values-imputation/knn-imputer.ts` (`KnnImputer.transform`, `getProperIndices` helper). + +--- + +## In-Place Transforms + +When the input array is no longer needed after the transform, write results directly into it. + +```typescript +function normalizeInPlace(arr: Float32Array, len: number, avg: number, stdev: number): void { + for (let i = 0; i < len; i++) arr[i] = (arr[i] - avg) / stdev; +} +``` + +Reference: `regression.ts` (`getTestDatasetForLinearRegression`). + +**Caution:** Only use in-place transforms when you own the array. Never modify arrays obtained +via `col.getRawData()` on user data — this mutates the underlying DataFrame column. + +--- + +## Bulk Copy with TypedArray.set() + +Use the built-in `set()` instead of a manual loop — engines optimize it to a memcpy-like path. + +```typescript +dst.set(src); // full copy +dst.set(src, offset); // copy into dst starting at offset +dst.set(src.subarray(start, end)); // copy a slice (subarray is a zero-copy view) + +// Clone raw column data for safe mutation +const clone = new Float32Array(col.getRawData().length); +clone.set(col.getRawData()); +``` + +**Tip:** `subarray(start, end)` returns a zero-copy view — use it to pass a logical slice +to `set()` or to functions that accept a typed array, without allocating. + +--- + +## Array Pool for Variable-Size Buffers + +When buffer sizes vary between calls, a pool recycles previously created arrays. +Interface: `acquire(minLen)` returns a buffer of at least `minLen` (contents uninitialized), +`release(arr)` returns it to the pool, `clear()` drops all pooled arrays. + +```typescript +const pool = new Float32Pool(); + +function processChunk(chunkSize: number): void { + const tmp = pool.acquire(chunkSize); + // ... compute into tmp ... + pool.release(tmp); +} + +pool.clear(); // after all work is done +``` + +Guidelines: +- **Always release** — otherwise it degrades to plain allocation. +- **Never read stale contents** — treat as uninitialized, `arr.fill(0)` if needed. +- **Scope the lifetime** — create per invocation and `clear()` when done. +- **Keep it simple** — for fixed-size buffers, plain pre-allocation is better. + +--- + +## Ring Buffer for Fixed-Length History + +When an algorithm needs a sliding window of the last N values, pre-allocate N arrays +and use a modular head index — O(1) per step, zero allocations. + +```typescript +const HIST_LEN = 5; +const history: Float64Array[] = []; +for (let i = 0; i < HIST_LEN; i++) + history[i] = new Float64Array(dim); +let head = 0; + +computeValues(history[head]); + +for (let step = 0; step < totalSteps; step++) { + const newest = history[head]; + const oldest = history[(head - (HIST_LEN - 1) + HIST_LEN) % HIST_LEN]; + + // Advance: overwrite oldest slot — O(1) + head = (head + 1) % HIST_LEN; + computeValues(history[head]); +} +``` + +**Alternative — reference shift** (O(N) per step): when consumers expect `[0]` = newest, +`[N-1]` = oldest, shift references instead. Acceptable for small N. + +```typescript +const recycled = history[HIST_LEN - 1]; +for (let j = HIST_LEN - 1; j > 0; --j) history[j] = history[j - 1]; +history[0] = recycled; +computeValues(history[0]); +``` + +Reference: `diff-grok` library, `solver-tools/ab5-method.ts` (`ab5Step`, reference shift with N=5). + +--- + +## Multi-Purpose Scratch Buffers + +The same buffer can serve different purposes at different stages within one iteration. +Each stage must fully overwrite the buffer before reading it. + +```typescript +const scratch0 = new Float64Array(dim); +const scratch1 = new Float64Array(dim); + +while (solving) { + // Stage 1: Jacobian — fills scratch0, scratch1 entirely + jacobian(t, y, f, eps, scratch0, scratch1, W); + + // Stage 2: time derivative — overwrites all elements + tDerivative(t, y, f, eps, scratch0, scratch1, hdT); + + // Stage 3: scratch0 reused as RHS for linear solve + for (let i = 0; i < dim; i++) scratch0[i] = f0[i] + hdT[i]; + luSolve(L, U, scratch0, luBuf, k1, dim); +} +``` + +For many stages, use **stage-scoped aliases**: `const rhs = scratch0;` gives semantic +context without misleading names. Both point to the same memory — zero overhead. + +> **Aliasing hazard:** Never pass the same buffer as both `src` and `dst` of a single call. +> If the function reads `src` while writing `dst`, aliasing corrupts the result. +> When unsure, use separate buffers — the cost is negligible vs a silent data corruption bug. + +Guidelines: +- **Document the reuse** with comments at each stage. +- **Never read previous-stage contents** — each stage must fully overwrite before reading. +- **Watch for aliasing** — never pass the same buffer as both source and destination. + +Reference: `diff-grok` library, `solver-tools/mrt-method.ts` (`mrtStep`, scratch buffer reuse across Jacobian/derivative/solve stages). + +--- + +## Summary + +| Pattern | When to use | Saves | +|---------|-------------|-------| +| **Pre-allocate and reuse** | Iterative algorithms | N allocations per loop | +| **Out-parameter** | Utility functions called repeatedly | 1 allocation per call | +| **Scratch buffers** | Multi-step computations in a loop | All intermediate arrays per iteration | +| **Local aliases** | Nested loops with array-of-arrays | Index errors and readability | +| **Accumulation into output** | Aggregation from multiple sources | Intermediate result arrays | +| **Logical length** | Variable-size subsets of a fixed buffer | Re-allocation on size change | +| **In-place transforms** | Input no longer needed after transform | 1 output array | +| **Bulk copy (set())** | Copying blocks between typed arrays | Loop overhead; engine-optimized | +| **Array pool** | Variable-size temporary buffers | Repeated allocation of similar arrays | +| **Ring buffer** | Sliding window / fixed-length history | O(1) advance with modular index | +| **Multi-purpose scratch** | Multi-stage algorithms | Extra buffer per stage | diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/COMPUTATION-PATTERNS.md b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/COMPUTATION-PATTERNS.md new file mode 100644 index 0000000000..0fc5cb74d0 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/COMPUTATION-PATTERNS.md @@ -0,0 +1,220 @@ +# Computation Patterns + +Reference for implementing computational methods in Datagrok packages. + +> **End-to-end example:** core `../example/code/src/levins/core.ts` + `../example/code/src/levins/model.ts`. + +For worker-based methods, see `WORKER-GUIDE.md`. +For array allocation and reuse patterns, see `ARRAY-OPERATIONS.md`. +For architectural context (core tasks, pipelines, specification structure), see `../datagrok-interactive-app-guide.md` and `../datagrok-app-specification-template.md`. + +## Raw Typed Arrays + +Access column data via `col.getRawData()` instead of per-element `col.get(i)`. This returns the underlying +typed array (`Float32Array`, `Float64Array`, `Int32Array`, `Uint32Array`) and avoids boxing/unboxing overhead on every iteration. + +**IMPORTANT:** The raw array's `.length` may be larger than the column's element count (due to internal buffer allocation). Always use `col.length` for iteration bounds, never `rawData.length`. + +```typescript +const vals: Float32Array = features.getRawData(); +const cats: Int32Array = categories.getRawData(); +const len = features.length; // use column length, NOT vals.length + +for (let i = 0; i < len; i++) { + // direct access — no per-element API calls + const value = vals[i]; + const category = cats[i]; +} +``` + +Reference: `anova/anova-tools.ts` (`FactorizedData` constructor), `missing-values-imputation/knn-imputer.ts` (`KnnImputer.transform`). + +--- + +## Missing Values Strategy + +**Before implementing any new method, define the missing values strategy.** Different methods require different approaches: + +| Strategy | When to use | Example | +|----------|-------------|---------| +| **Skip** | Aggregation, statistics | ANOVA skips rows where factor or value is null | +| **Impute before computation** | Methods that require complete data (e.g., matrix operations) | KNN imputation, mean/median fill | +| **Propagate** | Result column should reflect original nulls | Copy null sentinel to output at the same index | +| **Reject** | Method cannot handle nulls at all | Throw error if `missingValueCount > 0` | + +Document the chosen strategy in the method's JSDoc or function header. When multiple input columns are involved, specify per-column behavior (e.g., ANOVA: skip if factor OR value is null; KNN: skip feature columns with nulls at the target row but impute the target). + +--- + +## Null Handling in Loops + +Before processing, check `col.stats.missingValueCount`. If there are no missing values, skip all null checks +in the loop — this eliminates a branch per iteration and significantly speeds up computation. + +```typescript +const hasMissing = col.stats.missingValueCount > 0; + +if (hasMissing) { + const nullValue = getNullValue(col); + for (let i = 0; i < len; i++) { + if (raw[i] === nullValue) continue; + // process raw[i] + } +} else { + for (let i = 0; i < len; i++) { + // process raw[i] — no null checks needed + } +} +``` + +When nulls are present, use `getNullValue(col)` from `utils.ts` to obtain the sentinel value and compare +against it directly in loops. Do not use platform null-checking APIs in hot paths. + +| Column type | Sentinel | Notes | +|-------------|----------|-------| +| `int`, `string`, `bool` | `-2147483648` | Min 32-bit int | +| `float`, `datetime`, `qnum` | `2.6789344063684636e-34` | Special float constant | + +```typescript +import {getNullValue} from '../utils'; + +const nullValue = getNullValue(col); +const raw = col.getRawData(); + +for (let i = 0; i < col.length; i++) { + if (raw[i] === nullValue) continue; // skip missing + // process raw[i] +} +``` + +For categorical (string) columns, raw data stores integer category indices. Check for null categories separately: + +```typescript +const categoriesNull = categories.stats.missingValueCount > 0 ? getNullValue(categories) : -1; + +for (let i = 0; i < size; i++) { + if ((cats[i] === categoriesNull) || (vals[i] === featuresNull)) continue; + // process non-null pair +} +``` + +Reference: `anova/anova-tools.ts` (`FactorizedData.setStats`), `missing-values-imputation/knn-imputer.ts` (`KnnImputer.transform`). + +--- + +## Single-Pass Aggregation + +Compute all required statistics in one loop over the data. Pre-allocate output buffers as typed arrays. + +```typescript +const K = uniqueCategoryCount; +const sums = new Float64Array(K).fill(0); +const sumsOfSquares = new Float64Array(K).fill(0); +const subSampleSizes = new Int32Array(K).fill(0); + +for (let i = 0; i < size; i++) { + const cat = cats[i]; + if (vals[i] === nullValue) continue; + + sums[cat] += vals[i]; + sumsOfSquares[cat] += vals[i] ** 2; + ++subSampleSizes[cat]; +} +``` + +Reference: `anova/anova-tools.ts`, `FactorizedData.setStats()`. + +--- + +## Bool Column Handling + +Bool columns are stored as packed bit arrays. Extract individual bits via bitwise operations: + +```typescript +const raw = boolCol.getRawData(); // Uint32Array with packed bits +let catIdx = 0; +let shift = 0; +let packed = raw[0]; +const MAX_SHIFT = 8 * raw.BYTES_PER_ELEMENT - 1; + +for (let i = 0; i < size; i++) { + const bit = 1 & (packed >> shift); + // use `bit` as 0 or 1 + + ++shift; + if (shift > MAX_SHIFT) { + shift = 0; + ++catIdx; + packed = raw[catIdx]; + } +} +``` + +Reference: `anova/anova-tools.ts` (`FactorizedData.setStats`, bool branch). + +--- + +## Data Locality + +Typed arrays store elements contiguously in memory. Sequential access maximizes CPU cache utilization +and enables hardware prefetching. This is a key reason to prefer typed arrays over `number[]` or per-element API calls. + +### Why it matters + +- **Cache lines**: CPU loads data in 64-byte blocks. One cache line holds 16 `float32` or 8 `float64` values. + Sequential access means every loaded cache line is fully utilized. +- **Prefetching**: CPU detects sequential access patterns and preloads next cache lines automatically. + This hides memory latency almost entirely for linear traversals. +- **No boxing**: `number[]` stores boxed values as heap-allocated objects (pointer → header → value). + Typed arrays store raw values inline — more useful data per cache line. + +### Access pattern guidelines + +| Pattern | Cache behavior | Use when | +|---------|---------------|----------| +| Sequential typed array traversal | Optimal — prefetcher active, full cache line utilization | Aggregation, statistics, transforms | +| Multiple typed arrays in parallel (`vals[i]`, `cats[i]`) | Good — each array has its own prefetch stream | Multi-column single-pass (ANOVA, KNN distances) | +| Random access to typed array | Cache miss per access — up to 100x slower than sequential | Avoid; restructure if possible | +| `col.get(i)` in a loop | Method call + potential unboxing per element | Avoid in hot loops | + +### Column-major vs row-major + +When building matrices from multiple columns, the layout determines which access patterns are cache-friendly: + +- **Column-major** (`data[i + j * nRows]`): optimal when processing columns independently + (e.g., centering, scaling, per-feature statistics) +- **Row-major** (`data[i * nCols + j]`): optimal when accessing all features of one row + (e.g., distance computation, KNN, nearest neighbor search) + +Choose the layout that matches the method's primary access pattern. See `WORKER-GUIDE.md` +for `toFlatColumnMajor` and `toFlatRowMajor` helper functions. + +### Pre-allocate output buffers + +Allocate result arrays once before the loop to avoid repeated allocations and garbage collection: + +```typescript +// Good: single allocation +const result = new Float64Array(len); +for (let i = 0; i < len; i++) + result[i] = vals[i] * scale; + +// Bad: growing array triggers re-allocation and copying +const result: number[] = []; +for (let i = 0; i < len; i++) + result.push(vals[i] * scale); +``` + +--- + +## Module Structure + +Separate computation from UI into distinct files: + +| File pattern | Purpose | Dependencies | +|-------------|---------|-------------| +| `*-tools.ts` | Pure computation on raw data | `datagrok-api` types, `utils.ts`, math libraries | +| `*-ui.ts` or `ui.ts` | Dialog, inputs, validation, visualization | `datagrok-api` UI, computation module | +| `*-constants.ts` or `ui-constants.ts` | Enums, error messages, UI labels | None | + +The computation module must not import UI components. This keeps it testable and potentially reusable in workers. diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/PARALLEL-EXECUTION.md b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/PARALLEL-EXECUTION.md new file mode 100644 index 0000000000..516a38d73a --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/PARALLEL-EXECUTION.md @@ -0,0 +1,55 @@ +# Parallel Execution Guide + +Reference for distributing independent computations across multiple web workers. + +> **End-to-end example:** function `runOptimization()` in `../example/code/src/levins/app.ts` — worker pool with task queue, progress bar, partial error handling. + +For single-worker patterns, see `WORKER-GUIDE.md`. + +## Worker Count + +```typescript +import {MIN_WORKERS_COUNT, WORKERS_COUNT_DOWNSHIFT} from './worker-utils/worker-defs'; + +const workerCount = Math.max(MIN_WORKERS_COUNT, navigator.hardwareConcurrency - WORKERS_COUNT_DOWNSHIFT); +``` + +## Fan-out / Fan-in Pattern + +```typescript +async function runParallel( + inputs: TInput[], + workerUrl: URL, +): Promise { + const nWorkers = Math.min( + Math.max(MIN_WORKERS_COUNT, navigator.hardwareConcurrency - WORKERS_COUNT_DOWNSHIFT), + inputs.length, + ); + + // Distribute inputs round-robin + const chunks: TInput[][] = Array.from({length: nWorkers}, () => []); + for (let i = 0; i < inputs.length; i++) + chunks[i % nWorkers].push(inputs[i]); + + const promises = chunks.map((chunk) => + new Promise((resolve, reject) => { + const worker = new Worker(workerUrl); + worker.postMessage(chunk); + worker.onmessage = (e) => { + worker.terminate(); + if (e.data.success) + resolve(e.data.data); + else + reject(new Error(e.data.error)); + }; + worker.onerror = (e) => { + worker.terminate(); + reject(new Error(e.message)); + }; + }), + ); + + const results = await Promise.all(promises); + return results.flat(); +} +``` diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/WORKER-GUIDE.md b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/WORKER-GUIDE.md new file mode 100644 index 0000000000..40411e97c6 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/WORKER-GUIDE.md @@ -0,0 +1,296 @@ +# Worker Implementation Guide + +Reference for implementing in-worker computations using the `worker-utils` infrastructure. + +> **End-to-end example:** optimization worker `../example/code/src/levins/optimize-worker.ts`. +> For architectural context (ports, adapters, coordinator) see `../datagrok-interactive-app-guide.md` (sections 1.2–1.3). + +## Worker-Utils Infrastructure + +### Definitions (`worker-defs.ts`) — no dependencies, safe to import in workers + +```typescript +type RawData = Int32Array | Float32Array | Float64Array | Uint32Array; +type ColumnType = 'int' | 'float32' | 'float64' | 'string' | 'bool' | 'datetime' | 'qnum' | 'bigint'; + +interface WorkerColumnStats { + totalCount: number; + missingValueCount: number; + uniqueCount: number; + valueCount: number; + min: number; max: number; + sum: number; avg: number; + stdev: number; variance: number; + skew: number; kurt: number; + med: number; + q1: number; q2: number; q3: number; + nullValue: number; // INT_NULL (-2147483648) or FLOAT_NULL (2.6789344063684636e-34) +} + +interface WorkerColumn { + name: string; + type: ColumnType; + length: number; + rawData: RawData; + stats: WorkerColumnStats; + categories?: string[]; // only for type === 'string' +} + +interface WorkerDataFrame { + name: string; + rowCount: number; + columns: WorkerColumn[]; +} +``` + +### Transforms (`worker-transforms.ts`) — requires `datagrok-api`, main-thread only + +| Function | Signature | Direction | +|----------|-----------|-----------| +| `toWorkerColumn` | `(col: DG.Column) => WorkerColumn` | DG -> Worker | +| `toWorkerColumns` | `(columns: DG.ColumnList) => WorkerColumn[]` | DG -> Worker | +| `toWorkerDataFrame` | `(df: DG.DataFrame) => WorkerDataFrame` | DG -> Worker | +| `fromWorkerColumn` | `(wc: WorkerColumn) => DG.Column` | Worker -> DG | +| `fromWorkerDataFrame` | `(wdf: WorkerDataFrame) => DG.DataFrame` | Worker -> DG | + +### Null Sentinel Values + +| ColumnType | Sentinel | Constant | +|------------|----------|----------| +| `int`, `string`, `bool` | -2147483648 | `INT_NULL` | +| `float32`, `float64`, `datetime`, `qnum` | 2.6789344063684636e-34 | `FLOAT_NULL` | + +--- + +## Missing Values Strategy + +Before implementing any new worker-based method, define the missing values strategy (skip, impute, propagate, or reject). See `COMPUTATION-PATTERNS.md` (Missing Values Strategy section) for the full decision table and per-column behavior guidelines. + +--- + +## Working with WorkerColumn Inside a Worker + +**IMPORTANT:** The `rawData` array's `.length` may be larger than `col.length` (due to internal buffer allocation). Always use `col.length` for iteration bounds, never `rawData.length`. + +### Reading numerical data + +Check `col.stats.missingValueCount` before processing. If there are no missing values, skip all null checks +in the loop — this eliminates a branch per iteration and significantly speeds up computation. + +```typescript +// worker.ts +import {WorkerColumn} from './worker-defs'; + +onmessage = (e: MessageEvent) => { + const col: WorkerColumn = e.data; + const raw = col.rawData as Float32Array; + const n = col.length; + + if (col.stats.missingValueCount > 0) { + const nullVal = col.stats.nullValue; + for (let i = 0; i < n; i++) { + if (raw[i] === nullVal) continue; // skip missing + // process raw[i] + } + } else { + for (let i = 0; i < n; i++) { + // process raw[i] — no null checks needed + } + } +}; +``` + +### Centering / scaling using stats + +```typescript +function centerAndScale(col: WorkerColumn): Float32Array { + const raw = col.rawData as Float32Array; + const result = new Float32Array(col.length); + const nullVal = col.stats.nullValue; + const avg = col.stats.avg; + const stdev = col.stats.stdev; + + for (let i = 0; i < col.length; i++) { + if (raw[i] === nullVal) + result[i] = nullVal; + else + result[i] = (raw[i] - avg) / stdev; + } + return result; +} +``` + +### Building a feature matrix from WorkerColumn[] + +Choose the matrix layout based on the method's primary access pattern — this directly affects +CPU cache utilization: + +- **Column-major**: sequential access within each column. Optimal when columns are processed + independently (centering, scaling, per-feature statistics, WASM interop). +- **Row-major flat**: sequential access across features of each row. Optimal for distance + computation, KNN, nearest neighbor search. +- **Row-major typed**: same access pattern as row-major flat, but each row is a separate + `Float32Array`. Use as a drop-in replacement for `number[][]`. + +Avoid random access patterns — a cache miss per access can be up to 100x slower than sequential traversal. +For more details on data locality, see `COMPUTATION-PATTERNS.md` (Data Locality section). + +```typescript +// Row-major flat Float32Array: data[i * nCols + j] +// Single allocation, contiguous memory, no boxing overhead. +function toFlatRowMajor(cols: WorkerColumn[]): Float32Array { + const nRows = cols[0].length; + const nCols = cols.length; + const data = new Float32Array(nRows * nCols); + for (let j = 0; j < nCols; j++) { + const raw = cols[j].rawData; + for (let i = 0; i < nRows; i++) + data[i * nCols + j] = raw[i]; + } + return data; +} + +// Row-major Float32Array[]: data[i][j] +// Drop-in replacement for number[][] with unboxed typed rows. +function toTypedRowMajor(cols: WorkerColumn[]): Float32Array[] { + const nRows = cols[0].length; + const nCols = cols.length; + const data: Float32Array[] = new Array(nRows); + for (let i = 0; i < nRows; i++) + data[i] = new Float32Array(nCols); + for (let j = 0; j < nCols; j++) { + const raw = cols[j].rawData; + for (let i = 0; i < nRows; i++) + data[i][j] = raw[i]; + } + return data; +} + +// Column-major flat Float32Array: data[i + j * nRows] +// Optimal when columns are processed independently (WASM, matrix ops). +function toFlatColumnMajor(cols: WorkerColumn[]): Float32Array { + const nRows = cols[0].length; + const nCols = cols.length; + const data = new Float32Array(nRows * nCols); + for (let j = 0; j < nCols; j++) { + const raw = cols[j].rawData; + const offset = j * nRows; + for (let i = 0; i < nRows; i++) + data[i + offset] = raw[i]; + } + return data; +} +``` + +### Creating a result WorkerColumn + +```typescript +function makeResultColumn(name: string, data: Float32Array): WorkerColumn { + return { + name: name, + type: 'float32', + length: data.length, + rawData: data, + stats: computeStats(data), // compute in worker or leave zeros if not needed + }; +} +``` + +--- + +## Worker Lifecycle Pattern + +### Main thread (caller) + +```typescript +import {toWorkerColumns, fromWorkerColumn} from './worker-utils/worker-transforms'; +import {WorkerColumn} from './worker-utils/worker-defs'; + +async function runInWorker( + features: DG.ColumnList, components: number +): Promise { + const workerFeatures = toWorkerColumns(features); + + return new Promise((resolve, reject) => { + const worker = new Worker(new URL('./workers/my-worker.ts', import.meta.url)); + + worker.postMessage({features: workerFeatures, components}); + + worker.onmessage = (e) => { + worker.terminate(); + if (e.data.success) { + const result = e.data.data.columns as WorkerColumn[]; + resolve(result.map(fromWorkerColumn)); + } else { + reject(new Error(e.data.error)); + } + }; + + worker.onerror = (e) => { + worker.terminate(); + reject(new Error(e.message)); + }; + }); +} +``` + +### Web worker + +```typescript +import {WorkerColumn} from '../worker-utils/worker-defs'; + +interface MyWorkerInput { + features: WorkerColumn[]; + components: number; +} + +interface MyWorkerOutput { + success: true; + data: {columns: WorkerColumn[]}; +} | { + success: false; + error: string; +} + +onmessage = (e: MessageEvent) => { + try { + const {features, components} = e.data; + + // Access raw data directly: + const nRows = features[0].length; + const nCols = features.length; + + // Use stats: + for (const f of features) { + const avg = f.stats.avg; + const stdev = f.stats.stdev; + const nullVal = f.stats.nullValue; + // ... + } + + // Build result columns: + const resultCols: WorkerColumn[] = []; + for (let c = 0; c < components; c++) { + const data = new Float32Array(nRows); + // ... fill data ... + resultCols.push({ + name: `Component ${c + 1}`, + type: 'float32', + length: nRows, + rawData: data, + stats: { /* fill or leave defaults */ } as any, + }); + } + + postMessage({success: true, data: {columns: resultCols}} satisfies MyWorkerOutput); + } catch (err) { + postMessage({success: false, error: String(err)}); + } +}; +``` + +--- + +## Parallel Execution + +For distributing independent computations across multiple workers, see `PARALLEL-EXECUTION.md`. diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/datagrok-api-reference.md b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/datagrok-api-reference.md new file mode 100644 index 0000000000..310c41c97a --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/datagrok-api-reference.md @@ -0,0 +1,289 @@ +# Datagrok API Reference: Inputs and Viewers + +For interactive scientific applications. + +> **End-to-end example:** UI setup in `../example/code/src/levins/app.ts`. + +## 1. Inputs (`ui.input.*`) + +All inputs return an object inheriting from [`InputBase`](https://datagrok.ai/api/js/dg/classes/InputBase). + +UI Documentation: [Datagrok UI](https://datagrok.ai/help/develop/advanced/ui.md) | Namespace [`ui.input`](https://datagrok.ai/api/js/ui/namespaces/input/) + +### 1.1. Input Catalog + +| Method | Value Type | Description | Docs | +|---|---|---|---| +| `ui.input.int(label, options?)` | `number` | Integer | [int()](https://datagrok.ai/api/js/ui/namespaces/input/functions/int) | +| `ui.input.float(label, options?)` | `number` | Floating-point number | [float()](https://datagrok.ai/api/js/ui/namespaces/input/functions/float) | +| `ui.input.string(label, options?)` | `string` | String | [string()](https://datagrok.ai/api/js/ui/namespaces/input/functions/string) | +| `ui.input.bool(label, options?)` | `boolean` | Checkbox (true/false) | [bool()](https://datagrok.ai/api/js/ui/namespaces/input/functions/bool) | +| `ui.input.toggle(label, options?)` | `boolean` | Toggle switch | [toggle()](https://datagrok.ai/api/js/ui/namespaces/input/functions/toggle) | +| `ui.input.choice(label, options?)` | `string` | Selection from a list (dropdown) | [choice()](https://datagrok.ai/api/js/ui/namespaces/input/functions/choice) | +| `ui.input.multiChoice(label, options?)` | `string[]` | Multiple selection | [multiChoice()](https://datagrok.ai/api/js/ui/namespaces/input/functions/multiChoice) | +| `ui.input.dateTime(label, options?)` | `DateTime` | Date and time | [dateTime()](https://datagrok.ai/api/js/ui/namespaces/input/functions/dateTime) | +| `ui.input.textArea(label, options?)` | `string` | Multiline text | [textArea()](https://datagrok.ai/api/js/ui/namespaces/input/functions/textArea) | +| `ui.input.search(label, options?)` | `string` | String with search icon, Esc to clear | [search()](https://datagrok.ai/api/js/ui/namespaces/input/functions/search) | +| `ui.input.column(label, options?)` | `DG.Column` | Column selection | [column()](https://datagrok.ai/api/js/ui/namespaces/input/functions/column) | +| `ui.input.columnList(label, options?)` | `DG.Column[]` | Multiple column selection | [columnList()](https://datagrok.ai/api/js/ui/namespaces/input/functions/columnList) | +| `ui.input.markdown(label, options?)` | `string` | Markdown editor | [markdown()](https://datagrok.ai/api/js/ui/namespaces/input/functions/markdown) | + +### 1.2. Common Options (`options`) + +| Option | Type | Description | +|---|---|---| +| `value` | matches the type | Initial value | +| `items` | `string[]` | List of choices (for `choice`, `multiChoice`) | +| `nullable` | `boolean` | Whether the value can be `null` | +| `min` | `number` | Minimum value (for numeric types) | +| `max` | `number` | Maximum value (for numeric types) | +| `step` | `number` | Step (for numeric types) | +| `placeholder` | `string` | Hint text in an empty field | +| `icon` | `string \| HTMLElement` | Icon in the field (for string types) | +| `clearIcon` | `boolean` | Clear icon (for string types) | +| `escClears` | `boolean` | Esc clears the field (for string types) | +| `tooltipText` | `string` | Tooltip text displayed on hover | +| `onValueChanged` | `(value) => void` | Callback fired when the value changes (shorthand for subscribing to `.onChanged`) | + +### 1.3. Properties and Methods of [`InputBase`](https://datagrok.ai/api/js/dg/classes/InputBase) + +#### Value and State + +| Property/Method | Description | +|---|---| +| `.value` | Current value (get/set) | +| `.enabled` | Input enabled state (get/set) | +| `.root` | Root `HTMLElement` (div with label and input) | +| `.input` | `HTMLElement` of the input field itself | +| `.captionLabel` | `HTMLElement` of the label | + +#### Tooltips + +```typescript +// At creation time (preferred): +const input = ui.input.float('Rate', { tooltipText: 'Parameter description' }); + +// Or after creation: +input.setTooltip('Parameter description'); +``` + +#### Validators + +```typescript +// Adding a validator: returns null (valid) or an error string +input.addValidator((value) => { + if (value < 0) return 'Value must be non-negative'; + return null; +}); + +// Checking validity +const isValid = input.validate(); // true / false +``` + +#### Events + +| Event | Description | +|---|---| +| `.onChanged` | Value changed (by user or programmatically). Subscribe: `.onChanged.subscribe(callback)` | +| `.onInput` | Value changed by user. Subscribe: `.onInput.subscribe(callback)` | +| `.fireChanged()` | Programmatically trigger the `onChanged` event | +| `.fireInput()` | Programmatically trigger the `onInput` event | + +> **Note:** The `onValueChanged` callback in input options (section 1.2) is a convenience shorthand equivalent to subscribing to `.onChanged`. Use `onValueChanged` when setting up the callback at creation time; use `.onChanged.subscribe(...)` when subscribing later or when you need the `rxjs.Subscription` for cleanup. + +### 1.4. Binding Inputs ([`ui.bindInputs`](https://datagrok.ai/api/js/ui/functions/bindInputs)) + +```typescript +// Combining subscriptions from multiple inputs +const subs: rxjs.Subscription[] = ui.bindInputs([input1, input2, input3]); +``` + +### 1.5. Grouping Inputs into a Form ([`ui.inputs`](https://datagrok.ai/api/js/ui/functions/inputs) | [UI: Forms](https://datagrok.ai/help/develop/advanced/ui.md#forms)) + +```typescript +// Vertical form +const form = ui.inputs([ + ui.input.string('Name'), + ui.input.int('Age'), + ui.buttonsInput([ + ui.bigButton('Apply'), + ui.button('Cancel'), + ]), +]); + +// Also: ui.form([...]), ui.narrowForm([...]), ui.wideForm([...]) +``` + +## 2. Buttons and Icons + +| Method | Description | Docs | +|---|---|---| +| `ui.button(text, onClick, tooltip?)` | Standard button | [button()](https://datagrok.ai/api/js/ui/functions/button) | +| `ui.bigButton(text, onClick, tooltip?)` | Accent button (for the primary action) | [bigButton()](https://datagrok.ai/api/js/ui/functions/bigButton) | +| `ui.iconFA(name, onClick, tooltip?)` | FontAwesome icon as a button | [iconFA()](https://datagrok.ai/api/js/ui/functions/iconFA) | +| `ui.iconFAB(name, onClick, tooltip?)` | FontAwesome icon (blue) | [iconFAB()](https://datagrok.ai/api/js/ui/functions/iconFAB) | +| `ui.iconSvg(svgContent, onClick, tooltip?)` | SVG icon as a button | [iconSvg()](https://datagrok.ai/api/js/ui/functions/iconSvg) | + +## 3. Tooltips ([`ui.tooltip`](https://datagrok.ai/api/js/ui/classes/Tooltip)) + +```typescript +// Binding a tooltip to any HTMLElement +ui.tooltip.bind(element, 'Tooltip text'); + +// Showing a tooltip programmatically +ui.tooltip.show('Text', x, y); + +// Hiding a tooltip +ui.tooltip.hide(); + +// Showing a tooltip for a group of table rows +ui.tooltip.showRowGroup(dataFrame, predicate, x, y); +``` + +## 4. Viewers ([`DG.Viewer`](https://datagrok.ai/api/js/dg/classes/JsViewer)) + +Documentation: [Viewers](https://datagrok.ai/help/visualize/viewers/) | [Viewer API](https://datagrok.ai/api/js/dg/classes/JsViewer) | [UI: Viewers](https://datagrok.ai/help/develop/advanced/ui.md#viewers) + +### 4.1. Standard Viewer Catalog + +| Factory Method | TableView Method | Description | Docs | +|---|---|---|---| +| `DG.Viewer.barChart(df, options?)` | `view.barChart(options?)` | Bar chart | [Bar Chart](https://datagrok.ai/help/visualize/viewers/bar-chart.md) | +| `DG.Viewer.boxPlot(df, options?)` | `view.boxPlot(options?)` | Box plot | [Box Plot](https://datagrok.ai/help/visualize/viewers/box-plot.md) | +| `DG.Viewer.calendar(df, options?)` | `view.calendar(options?)` | Calendar | [Calendar](https://datagrok.ai/help/visualize/viewers/calendar.md) | +| `DG.Viewer.correlationPlot(df, options?)` | `view.corrPlot(options?)` | Correlation matrix | [Correlation Plot](https://datagrok.ai/help/visualize/viewers/correlation-plot.md) | +| `DG.Viewer.densityPlot(df, options?)` | `view.densityPlot(options?)` | Point density | [Density Plot](https://datagrok.ai/help/visualize/viewers/density-plot.md) | +| `DG.Viewer.filters(df, options?)` | `view.filters(options?)` | Filter set | [Filters](https://datagrok.ai/help/visualize/viewers/filters.md) | +| `DG.Viewer.form(df, options?)` | `view.form(options?)` | Form (single row) | [Form](https://datagrok.ai/help/visualize/viewers/form.md) | +| `DG.Viewer.grid(df, options?)` | `view.grid` | Table grid | [Grid](https://datagrok.ai/help/visualize/viewers/grid.md) | +| `DG.Viewer.heatMap(df, options?)` | `view.heatMap(options?)` | Heat map | [Heat Map](https://datagrok.ai/help/visualize/viewers/heat-map.md) | +| `DG.Viewer.histogram(df, options?)` | `view.histogram(options?)` | Histogram | [Histogram](https://datagrok.ai/help/visualize/viewers/histogram.md) | +| `DG.Viewer.lineChart(df, options?)` | `view.lineChart(options?)` | Line chart | [Line Chart](https://datagrok.ai/help/visualize/viewers/line-chart.md) | +| `DG.Viewer.markup(df, options?)` | `view.markup(options?)` | HTML/Markdown | [Markup](https://datagrok.ai/help/visualize/viewers/markup.md) | +| `DG.Viewer.matrixPlot(df, options?)` | `view.matrixPlot(options?)` | Matrix of plots | [Matrix Plot](https://datagrok.ai/help/visualize/viewers/matrix-plot.md) | +| `DG.Viewer.network(df, options?)` | `view.networkDiagram(options?)` | Network diagram | [Network Diagram](https://datagrok.ai/help/visualize/viewers/network-diagram.md) | +| `DG.Viewer.pcPlot(df, options?)` | `view.pcPlot(options?)` | Parallel coordinates | [PC Plot](https://datagrok.ai/help/visualize/viewers/pc-plot.md) | +| `DG.Viewer.pieChart(df, options?)` | — | Pie chart | [Pie Chart](https://datagrok.ai/help/visualize/viewers/pie-chart.md) | +| `DG.Viewer.scatterPlot(df, options?)` | `view.scatterPlot(options?)` | Scatter plot | [Scatter Plot](https://datagrok.ai/help/visualize/viewers/scatter-plot.md) | +| `DG.Viewer.scatterPlot3d(df, options?)` | `view.scatterPlot3d(options?)` | 3D scatter plot | [3D Scatter Plot](https://datagrok.ai/help/visualize/viewers/3d-scatter-plot.md) | +| `DG.Viewer.statistics(df, options?)` | `view.statistics(options?)` | Descriptive statistics | [Statistics](https://datagrok.ai/help/visualize/viewers/statistics.md) | +| `DG.Viewer.tile(df, options?)` | `view.tileViewer(options?)` | Tile view | [Tile Viewer](https://datagrok.ai/help/visualize/viewers/tile-viewer.md) | +| `DG.Viewer.treeMap(df, options?)` | `view.treeMap(options?)` | Tree map | [Tree Map](https://datagrok.ai/help/visualize/viewers/tree-map.md) | +| `DG.Viewer.trellisPlot(df, options?)` | — | Facet grid | [Trellis Plot](https://datagrok.ai/help/visualize/viewers/trellis-plot.md) | +| `DG.Viewer.wordCloud(df, options?)` | — | Word cloud | [Word Cloud](https://datagrok.ai/help/visualize/viewers/word-cloud.md) | + +Additional viewers (require data with coordinates): + +| Viewer | Description | Docs | +|---|---|---| +| `view.googleMap(options?)` | Google Maps with data overlay | [Google Map](https://datagrok.ai/help/visualize/viewers/google-map.md) | +| `DG.Viewer.fromType('Globe', df)` | 3D globe | [Globe](https://datagrok.ai/help/visualize/viewers/globe.md) | +| `view.shapeMap(options?)` | Region map | [Shape Map](https://datagrok.ai/help/visualize/viewers/shape-map.md) | + +### 4.2. Creating by Type + +```typescript +// Creating a viewer by string type +const viewer = DG.Viewer.fromType('Scatter plot', dataFrame); +``` + +### 4.3. Configuring Options + +```typescript +// At creation time +const plot = view.scatterPlot({ + x: 'height', + y: 'weight', + size: 'age', + color: 'race', +}); + +// After creation +plot.setOptions({ + showRegressionLine: true, + markerType: 'square', +}); +``` + +### 4.4. Docking to TableView ([UI: Docking](https://datagrok.ai/help/develop/advanced/ui.md#docking)) + +```typescript +const view = grok.shell.addTableView(df); + +// Docking a viewer +const chart = DG.Viewer.lineChart(df); +view.dockManager.dock(chart, 'right', null, 'Line Chart'); + +// Docking an arbitrary element +const div = ui.div([/* content */]); +const node = view.dockManager.dock(div, 'down', null, 'Panel', 0.3); + +// Docking types: 'left', 'right', 'top', 'down', 'fill' +// Last parameter is the dock ratio (0..1) +``` + +## 5. Notifications ([`grok.shell`](https://datagrok.ai/api/js/dg/classes/Shell)) + +```typescript +grok.shell.info('Informational message'); +grok.shell.warning('Warning'); +grok.shell.error('Error message'); +``` + +## 6. Dialogs ([`ui.dialog`](https://datagrok.ai/api/js/ui/functions/dialog) | [UI: Dialogs](https://datagrok.ai/help/develop/advanced/ui.md#dialogs)) + +```typescript +// Standard dialog +ui.dialog('Title') + .add(ui.inputs([ + ui.input.float('Parameter 1', {value: 1.0}), + ui.input.float('Parameter 2', {value: 2.0}), + ])) + .onOK(() => { /* handling */ }) + .show(); + +// Modal dialog +ui.dialog('Title') + .add(/* content */) + .onOK(() => { /* handling */ }) + .showModal(); +``` + +## 7. Subscriptions and Cleanup + +```typescript +// Subscribing to an event +const sub = input.onChanged.subscribe((value) => { + // handling +}); + +// Unsubscribing +sub.unsubscribe(); + +// For viewers +viewer.sub(eventId, callback); // registers a subscription +viewer.registerCleanup(cleanupFunc); // will be called on close +``` + +## 8. Progress Bar + +```typescript +const pi = DG.TaskBarProgressIndicator.create('Task description...'); +pi.update(50, 'Progress 50%'); +// ... +pi.close(); +``` + +## 9. Layouts and Containers ([UI: Layouts](https://datagrok.ai/help/develop/advanced/ui.md#layouts)) + +| Method | Description | Docs | +|---|---|---| +| `ui.div([...])` | Container | [div()](https://datagrok.ai/api/js/ui/functions/div) | +| `ui.divH([...])` | Horizontal flex container | [divH()](https://datagrok.ai/api/js/ui/functions/divH) | +| `ui.divV([...])` | Vertical flex container | [divV()](https://datagrok.ai/api/js/ui/functions/divV) | +| `ui.panel([...])` | Panel with padding | [panel()](https://datagrok.ai/api/js/ui/functions/panel) | +| `ui.box(element)` | Fixed-size container | [box()](https://datagrok.ai/api/js/ui/functions/box) | +| `ui.splitH([...])` | Horizontal splitter (resizable) | [splitH()](https://datagrok.ai/api/js/ui/functions/splitH) | +| `ui.splitV([...])` | Vertical splitter (resizable) | [splitV()](https://datagrok.ai/api/js/ui/functions/splitV) | +| `ui.tabControl({...})` | Tabs | [tabControl()](https://datagrok.ai/api/js/ui/functions/tabControl) | +| `ui.accordion()` | Accordion | [accordion()](https://datagrok.ai/api/js/ui/functions/accordion) | diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/datagrok-coding-conventions.md b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/datagrok-coding-conventions.md new file mode 100644 index 0000000000..0ff7fa7c78 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/datagrok-coding-conventions.md @@ -0,0 +1,396 @@ +# Coding Standard: Datagrok Interactive Scientific Applications + +> **End-to-end example:** application code in `../example/code/src/levins/`. + +Extracted from the `DiffStudio` package (`packages/DiffStudio`). + +## 1. File Structure + +### 1.1. Module Organization + +Each file has a single responsibility: + +- `package.ts` — entry point, package function registration via decorators. +- `app.ts` — main UI application class. +- `constants.ts` — domain constants (not UI). +- `ui-constants.ts` — UI constants: tooltips, titles, errors, links, timeouts. +- `error-utils.ts` — custom errors and error display utilities. +- `utils.ts` — general utilities. +- `model.ts` — types and model class. +- `solver-tools.ts` — wrappers over external library (core). +- `callbacks/` — pattern: base class + concrete implementations in separate files. +- `demo/` — standalone demo models in separate files. +- `tests/` — tests, separated by categories. + +### 1.2. Comment at the Beginning of a File + +Each file starts with a single-line comment describing the module's purpose: + +```typescript +// Solver of initial value problem +``` + +For files with a detailed description, a block comment is used: + +```typescript +/* Scripting tools for the Initial Value Problem (IVP) solver: + - parser of formulas defining IVP; + - JS-script generator. +*/ +``` + +### 1.3. Import Order + +Imports follow a fixed order: + +1. Datagrok API (always three lines): + +```typescript +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; +``` + +2. External libraries (`diff-grok`, `codemirror`, `dayjs`, etc.). +3. Internal package modules (`./solver-tools`, `./constants`, etc.). +4. CSS styles (`'../css/app-styles.css'`). + +Groups are separated by a blank line. + +## 2. Constants + +### 2.1. All String Literals Go Into Constants + +String literals are not used inline in the code. All messages, titles, tooltips, and keywords are extracted into enums or consts. + +### 2.2. Constants Are Grouped Into Enums by Purpose + +```typescript +/** Tooltips messages */ +export enum HINT { + HELP = 'Open help in a new tab', + OPEN = 'Open model', + // ... +}; // HINT + +/** UI titles */ +export enum TITLE { + LOAD = 'Load...', + IMPORT = 'Import...', + // ... +}; // TITLE + +/** Error messages */ +export enum ERROR_MSG { + SOLVING_FAILS = 'Solving fails', + // ... +}; +``` + +### 2.3. Enum Naming + +- Enum name — `UPPER_CASE` (e.g., `HINT`, `TITLE`, `ERROR_MSG`, `UI_TIME`). +- Enum values — `UPPER_CASE` (e.g., `HINT.OPEN`, `UI_TIME.DOCK_EDITOR_TIMEOUT`). + +### 2.4. Composite Constants Are Built from Base Ones + +```typescript +const META = `${CONTROL_TAG}meta`; + +export enum CONTROL_EXPR { + NAME = `${CONTROL_TAG}name`, + SOLVER = `${META}.solver`, + // ... +}; +``` + +### 2.5. Maps for Linking Constants + +`Map` is used for mapping between sets of constants: + +```typescript +export const MODEL_HINT = new Map([ + [TITLE.BASIC, HINT.BASIC], + [TITLE.ADV, HINT.ADV], + // ... +]); +``` + +### 2.6. Separation of Domain and UI Constants + +- `constants.ts` — domain: parser formulas, solver settings, column names. +- `ui-constants.ts` — UI: tooltips, titles, errors, links, timeouts, dock ratios. + +## 3. Types + +### 3.1. Local Types Are Defined Near Their Usage + +```typescript +/** Numerical input specification */ +export type Input = { + value: number, + annot: string | null, +}; + +/** Argument of IVP specification */ +type Arg = { + name: string, + initial: Input, + final: Input, + step: Input, +}; +``` + +### 3.2. Types Used in Multiple Modules Are Exported + +Types needed in other files are exported from the defining file and imported at the point of use. + +## 4. Classes + +### 4.1. Access Modifiers + +Fields and methods are explicitly marked with `private` or `public`: + +```typescript +export class ModelError extends Error { + private helpUrl: string; + private toHighlight: string = undefined; + + public getHelpUrl() { return this.helpUrl; } + public getToHighlight() { return this.toHighlight; } +} +``` + +### 4.2. Closing Comment for a Class + +A comment with the class name is placed after the closing brace: + +```typescript +}; // ModelError +``` + +```typescript +}; // Model +``` + +Similarly for large enums and functions: + +```typescript +}; // HINT + +} // error + +} // showModelErrorHint +``` + +### 4.3. Inheritance Pattern (Callbacks) + +The base class is in a separate file, concrete implementations are each in their own file: + +``` +callbacks/ + callback-base.ts — base class Callback + callback-tools.ts — factory function getCallback + iter-checker-callback.ts — IterCheckerCallback extends Callback + time-checker-callback.ts — TimeCheckerCallback extends Callback +``` + +## 5. Functions + +### 5.1. JSDoc Comment Before Each Function + +Every function (exported and internal) has a single-line JSDoc comment: + +```typescript +/** Return solution as a dataframe */ +function getSolutionDF(odes: ODEs, solutionArrs: Float64Array[]): DG.DataFrame { + +/** Default solver of initial value problem. */ +export function solveDefault(odes: ODEs): DG.DataFrame { + +/** Return unused IVP-file name */ +export function unusedFileName(name: string, files: string[]): string { +``` + +### 5.2. Arrow Functions for Short Utilities + +```typescript +const getMethod = (options?: Partial) => { + // ... +}; + +const strToVal = (s: string) => { + const num = Number(s); + return !isNaN(num) ? num : s === 'true' ? true : s === 'false' ? false : s; +}; +``` + +### 5.3. Comments Inside Functions Mark Logical Steps + +```typescript +// Get numerical solution +const approxSolution = method(corProb.odes); + +// Compute error +for (let i = 0; i < pointsCount; ++i) { +``` + +```typescript +// extract function values +const vx = _y[2]; + +// evaluate expressions +const v = Math.PI * dB ** 3 / 6; + +// compute output +_output[0] = vx; +``` + +## 6. Error Handling + +### 6.1. Custom Error Class + +A separate class extending `Error` is created for domain errors: + +```typescript +export class ModelError extends Error { + private helpUrl: string; + constructor(message: string, helpUrl: string) { + super(message); + this.helpUrl = helpUrl; + } +} +``` + +### 6.2. Factory Functions for Common Errors + +```typescript +/** Return ModelError corresponding to ".. is not defined" */ +export function getIsNotDefined(msg: string): ModelError { + // ... +} +``` + +### 6.3. User Notification via Datagrok Utilities + +- `grok.shell.warning(...)` — warnings. +- `grok.shell.error(...)` — errors. +- `grok.shell.info(...)` — informational messages. + +## 7. Package Function Registration + +Datagrok supports two function registration approaches: + +1. **JSDoc-style comments** (traditional) — `//name:`, `//tags: app`, `//input:`, `//output:`. Processed by `grok api` and `grok check`. +2. **Decorators** `@grok.decorators.*` (modern) — type-safe alternative used in newer packages. + +Both approaches are valid. The example below uses decorators (as in the DiffStudio package): + +```typescript +export class PackageFunctions { + @grok.decorators.app({ + name: 'Diff Studio', + description: 'Solver of ordinary differential equations systems', + browsePath: 'Compute', + }) + static async runDiffStudio(): Promise { + // ... + } + + @grok.decorators.func({}) + static solve(@grok.decorators.param({type: 'object'}) problem: ODEs): DG.DataFrame { + return solveDefault(problem); + } + + @grok.decorators.model({ + name: 'Ball flight', + description: 'Ball flight simulation', + // ... + }) + static ballFlight(/* params */) { + // ... + } +} +``` + +## 8. Formatting (ESLint) + +The configuration extends the `google` style guide. + +- **Indentation**: 2 spaces. +- **Maximum line length**: 120 characters. +- **Curly braces**: `multi-or-nest` — single-line block without braces, multi-line — with braces. +- **Brace style**: `1tbs` with `allowSingleLine: true`. +- **Unused variables**: `warn`, exceptions — `_`, `ui`, `grok`, `DG`. +- **JSDoc**: `require-jsdoc: off`, `valid-jsdoc: off` — single-line `/** */` comments are used instead. +- **Line break**: `linebreak-style: off`. + +## 9. Testing + +### 9.1. Test Organization + +Tests are separated by categories in individual files: + +- `numerical-methods-tests.ts` — solver correctness and performance. +- `features-tests.ts` — IVP format features. +- `platform-funcs-tests.ts` — platform integration. +- `pipeline-tests.ts` — end-to-end pipelines. +- `test-utils.ts` — test utilities. + +### 9.2. Framework + +`category`, `test`, `expect` from `@datagrok-libraries/test` are used: + +```typescript +import {category, expect, test} from '@datagrok-libraries/test/src/test'; + +category(`Correctness: ${name}`, () => { + corrProbs.forEach((problem) => test(problem.odes.name, async () => { + const error = getError(method, problem); + expect( + error < TINY, + true, + `The ${name} method failed to solve "${problem.odes.name}", too big error: ${error}`, + ); + }, {timeout: TIMEOUT})); +}); +``` + +### 9.3. Test Options + +- `{timeout: N}` — for tests with a time limit. +- `{benchmark: true}` — for performance tests. + +## 10. Naming + +### 10.1. Variables and Functions + +- `camelCase`: `solveDefault`, `getScriptLines`, `showModelErrorHint`. + +### 10.2. Classes and Types + +- `PascalCase`: `DiffStudio`, `ModelError`, `CallbackAction`, `ModelInfo`. + +### 10.3. Enums + +- Enum name: `UPPER_CASE` (`HINT`, `TITLE`, `ERROR_MSG`). +- Values: `UPPER_CASE` (`HINT.OPEN`, `UI_TIME.DOCK_EDITOR_TIMEOUT`). + +### 10.4. Constants Outside Enums + +- `UPPER_CASE` for primitives: `const TINY = 0.0001;` +- `camelCase` for complex objects: `const completions = [...]`, `const modelImageLink = new Map(...)`. + +### 10.5. Files + +- `kebab-case`: `solver-tools.ts`, `error-utils.ts`, `ui-constants.ts`, `ball-flight.ts`. + +## 11. CSS + +Styles are placed in a separate CSS file (`css/app-styles.css`) and imported in the modules that use them: + +```typescript +import '../css/app-styles.css'; +``` + +CSS classes are named with a package prefix: `diff-studio-hint-btns-div`, `diff-studio-highlight-text`. diff --git a/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/datagrok-document-schema.md b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/datagrok-document-schema.md new file mode 100644 index 0000000000..0726ca0383 --- /dev/null +++ b/packages/InteractiveSciAppTest/datagrok-interactive-app-guide/reference/datagrok-document-schema.md @@ -0,0 +1,146 @@ +# Document Schema: Datagrok Interactive Scientific Applications + +## Common Part (reusable, identical for all applications) + +``` +guides/ +│ +├── 1. Architecture and Philosophy +│ datagrok-interactive-app-guide.md +│ "Ports and adapters" pattern, pipelines, lifecycle, +│ subscriptions, teardown, testing. +│ +├── 2. Specification Template +│ datagrok-app-specification-template.md +│ Empty template defining the specification structure +│ for a specific application. +│ +├── 3. End-to-End Application Example +│ levins-metapopulation-spec.md +│ Completed specification for the Levins Metapopulation Model. +│ Serves as a reference for generation. +│ +└── reference/ + │ + ├── 4. Datagrok API Reference + │ datagrok-api-reference.md + │ Catalog of inputs, viewers, buttons, tooltips, dialogs, + │ layouts, notifications, subscriptions. With links to documentation. + │ + ├── 5. Coding Standard + │ datagrok-coding-conventions.md + │ File structure, constants, types, classes, functions, + │ error handling, naming, formatting, testing. + │ + └── 6. Implementation References (provided during core implementation phase) + COMPUTATION-PATTERNS.md + Working with raw data, null handling, single-pass aggregation, + data locality, bool columns, module structure. + + ARRAY-OPERATIONS.md + Typed array patterns: buffer reuse, out-parameter, + scratch buffers, ring buffer, array pool, in-place transforms. + + WORKER-GUIDE.md + Worker-utils infrastructure, WorkerColumn/WorkerDataFrame, + DG↔Worker transforms, matrix layouts, lifecycle. + + PARALLEL-EXECUTION.md + Fan-out/fan-in, distribution across workers, worker count. +``` + +## Application-Specific Part (unique for each application) + +``` +my-app-spec/ +│ +├── Application Specification (required) +│ my-app-specification.md +│ Completed specification template: core tasks, controls, +│ validation, reactivity, computation behavior, +│ rendering, layout, data lifecycle, UX. +│ +├── Method Specifications (if the application has its own methods) +│ methods/ +│ ├── method-A.md +│ │ Mathematical formulation, step-by-step algorithm, +│ │ inputs/outputs, constraints, edge cases, +│ │ literature references. +│ ├── method-B.md +│ └── ... +│ +├── UI Component Specifications (if there are custom elements) +│ ui-components/ +│ ├── component-X.md +│ │ Visual description (sketch/mockup), states, +│ │ events, styles (CSS), accessibility. +│ ├── component-Y.md +│ └── ... +│ +└── External Library Documentation (if used) + Not created, but referenced from the application specification. + Links to API reference, README, guides. +``` + +## Relationship Between Parts + +``` +┌─────────────────────────────────────────────────────┐ +│ COMMON PART │ +│ │ +│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │ +│ │Architecture │ │ API │ │ Coding │ │ +│ │& Philosophy │ │ Reference │ │ Standard │ │ +│ └─────────────┘ └──────────────┘ └────────────┘ │ +│ ┌─────────────┐ ┌──────────────────────────────┐ │ +│ │ Specification│ │ End-to-end example │ │ +│ │ Template │ │ (specification + code) │ │ +│ └─────────────┘ └──────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Implementation References │ │ +│ │ (computation, arrays, workers, parallelism) │ │ +│ └──────────────────────────────────────────────┘ │ +└──────────────────────────┬──────────────────────────┘ + │ + │ provided in context + │ together with + ▼ +┌─────────────────────────────────────────────────────┐ +│ APPLICATION-SPECIFIC PART │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Application Specification │ │ +│ │ (completed template) │ │ +│ └──────┬──────────────┬───────────────┬────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ │ +│ │ Method │ │ UI Component│ │ External │ │ +│ │ Specs │ │ Specs │ │ Library │ │ +│ │ (custom) │ │ (custom) │ │ Documentation│ │ +│ └────────────┘ └─────────────┘ │ (links) │ │ +│ └──────────────┘ │ +└─────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ Generated │ + │ Application │ + │ Code │ + └─────────────────┘ +``` + +## Summary: What Is Needed to Create a New Application + +| What | Source | Created from scratch? | +|---|---|---| +| Architecture and Philosophy | Common Part | No | +| Datagrok API Reference | Common Part | No | +| Coding Standard | Common Part | No | +| Specification Template | Common Part | No | +| End-to-End Example | Common Part | No | +| Implementation References | Common Part | No | +| Application Specification | Application-Specific Part | Yes, for each application | +| Method Specifications | Application-Specific Part | Yes, if the application has its own methods | +| UI Component Specifications | Application-Specific Part | Yes, if there are custom elements | +| External Library Documentation | Application-Specific Part (links) | No, already exists | diff --git a/packages/InteractiveSciAppTest/detectors.js b/packages/InteractiveSciAppTest/detectors.js new file mode 100644 index 0000000000..f21cf90325 --- /dev/null +++ b/packages/InteractiveSciAppTest/detectors.js @@ -0,0 +1,9 @@ +/** + * The class contains semantic type detectors. + * Detectors are functions tagged with `DG.FUNC_TYPES.SEM_TYPE_DETECTOR`. + * See also: https://datagrok.ai/help/develop/how-to/define-semantic-type-detectors + * The class name is comprised of and the `PackageDetectors` suffix. + * Follow this naming convention to ensure that your detectors are properly loaded. + */ +class InteractiveSciAppTestPackageDetectors extends DG.Package { +} diff --git a/packages/InteractiveSciAppTest/package-lock.json b/packages/InteractiveSciAppTest/package-lock.json new file mode 100644 index 0000000000..377b14c85d --- /dev/null +++ b/packages/InteractiveSciAppTest/package-lock.json @@ -0,0 +1,2436 @@ +{ + "name": "interactivesciapptest", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "interactivesciapptest", + "version": "0.0.1", + "dependencies": { + "@datagrok-libraries/utils": "^4.6.5", + "cash-dom": "^8.1.5", + "datagrok-api": "^1.26.0", + "dayjs": "^1.11.13", + "diff-grok": "^1.2.0" + }, + "devDependencies": { + "css-loader": "^7.1.2", + "style-loader": "^4.0.0", + "ts-loader": "latest", + "typescript": "latest", + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@datagrok-libraries/chem-meta": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@datagrok-libraries/chem-meta/-/chem-meta-1.2.10.tgz", + "integrity": "sha512-05Qfw1ul3I1lipNQNBTeO8zNBQisF2bJupw7nw/SfEhohH5578SXVMUCjwMAGxkTP//rnsDcfn9pZBN+fjj//A==", + "dependencies": { + "wu": "^2.1.0" + } + }, + "node_modules/@datagrok-libraries/utils": { + "version": "4.6.14", + "resolved": "https://registry.npmjs.org/@datagrok-libraries/utils/-/utils-4.6.14.tgz", + "integrity": "sha512-3hgNM3m30tsn3mdwlXJb8z5k4RnW3KkJdIiTJaib7rojo47AyERT9ix5gzaCj9ipIMmDoI8yljyLhiKR5CBNAw==", + "dependencies": { + "cash-dom": "^8.1.1", + "datagrok-api": "^1.26.0", + "dayjs": "=1.11.10", + "fast-sha256": "^1.3.0", + "js-base64": "^3.7.5", + "rxjs": "^6.5.5", + "wu": "^2.1.0" + } + }, + "node_modules/@datagrok-libraries/utils/node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "license": "MIT" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", + "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/wu": { + "version": "2.1.44", + "resolved": "https://registry.npmjs.org/@types/wu/-/wu-2.1.44.tgz", + "integrity": "sha512-veqvAklPyeT4DJFD66iBwzUKW5zicMDwaDShIvJmDkteQhwhXBKvgydA+yrNN8FnxWUFCI5y9+a2DI5sSwMUlQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cash-dom": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/cash-dom/-/cash-dom-8.1.5.tgz", + "integrity": "sha512-/BS05CfzyHR5xT2ksKj1sDLPaOv5rSmIwoGxNgdKwUtnIuiJ5neMxVEmZxvfyJiSjGbOMD0Lwe+9v+fszDqHew==", + "license": "MIT" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "7.1.4", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-7.1.4.tgz", + "integrity": "sha512-vv3J9tlOl04WjiMvHQI/9tmIrCxVrj6PFbHemBB1iihpeRbi/I4h033eoFIhwxBBqLhI0KYFS7yvynBFhIZfTw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.40", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.6.3" + }, + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || ^1.0.0 || ^2.0.0-0", + "webpack": "^5.27.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/datagrok-api": { + "version": "1.26.8", + "resolved": "https://registry.npmjs.org/datagrok-api/-/datagrok-api-1.26.8.tgz", + "integrity": "sha512-MaXfheRRsk4ZJjGr+ATPcw/NGCGcIwi56QCT4QIbyiZSbedwhPeRoXYEvFvwdYCNwZtouQuWp8U6dOXSM4GLbA==", + "dependencies": { + "@babel/core": "^7.27.1", + "@datagrok-libraries/chem-meta": "^1.0.12", + "@types/react": "^18.3.11", + "@types/wu": "^2.1.44", + "cash-dom": "^8.1.5", + "dayjs": "^1.11.10", + "openchemlib": "^7.2.3", + "react": "^18.3.1", + "rxjs": "^6.5.5", + "typeahead-standalone": "4.14.1", + "ws": "^8.18.2", + "wu": "^2.1.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff-grok": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/diff-grok/-/diff-grok-1.2.0.tgz", + "integrity": "sha512-qjU07sXsLVy/Z5YTSTYwzHvFnFCZyUPd2JYpGTVSi06R6b6qBCzpa4ZEmWoybeRFproGmNlfam7JhhFMPPcjoQ==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "license": "MIT" + }, + "node_modules/openchemlib": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/openchemlib/-/openchemlib-7.5.0.tgz", + "integrity": "sha512-cxEmgL1Szuw5zPDX29PyuAIkokSKPkzEIc/61oPA84GqvGyjMMRrGaF4tbFCDOT4c7ULZ/qmIWk9/ERj3wOg1w==", + "license": "BSD-3-Clause" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-loader": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-4.0.0.tgz", + "integrity": "sha512-1V4WqhhZZgjVAVJyt7TdDPZoPBPNHbekX4fWnCJL1yQukhCeZhJySUL+gL9y6sNdN95uEOS83Y55SqHcP7MzLA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 18.12.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.27.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.3.17", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.3.17.tgz", + "integrity": "sha512-YR7PtUp6GMU91BgSJmlaX/rS2lGDbAF7D+Wtq7hRO+MiljNmodYvqslzCFiYVAgW+Qoaaia/QUIP4lGXufjdZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/typeahead-standalone": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/typeahead-standalone/-/typeahead-standalone-4.14.1.tgz", + "integrity": "sha512-K+mqXmHferhxlyFD5blmOV9UIlazUxumyLWxO5QXnD1cjL6uQ6JGuqvjk0rHt4uz5cD2POAFxnyfkyZUD1ce7A==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wu": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wu/-/wu-2.1.0.tgz", + "integrity": "sha512-j+Gdt5IUK4eoLO6mrN/ZurInHacaxr/EPCvQHf1ARq6ROdKRN/aFtc0PGdH9lnRPMg6vhJOOqIYdNhMN6uWtUg==" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/packages/InteractiveSciAppTest/package.json b/packages/InteractiveSciAppTest/package.json new file mode 100644 index 0000000000..592ab1df94 --- /dev/null +++ b/packages/InteractiveSciAppTest/package.json @@ -0,0 +1,49 @@ +{ + "name": "interactivesciapptest", + "friendlyName": "InteractiveSciAppTest", + "version": "0.0.1", + "description": "InteractiveSciAppTest package", + "author": { + "name": "CC", + "email": "vmakarichev@datagrok.ai" + }, + "dependencies": { + "@datagrok-libraries/utils": "^4.6.5", + "cash-dom": "^8.1.5", + "datagrok-api": "^1.26.0", + "dayjs": "^1.11.13", + "diff-grok": "^1.2.0" + }, + "devDependencies": { + "css-loader": "^7.1.2", + "style-loader": "^4.0.0", + "ts-loader": "latest", + "typescript": "latest", + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4" + }, + "scripts": { + "debug-interactivesciapptest": "webpack && grok publish", + "release-interactivesciapptest": "webpack && grok publish --release", + "build-interactivesciapptest": "webpack", + "build": "grok api && grok check --soft && webpack", + "test": "grok test", + "debug-interactivesciapptest-dev": "webpack && grok publish dev", + "release-interactivesciapptest-dev": "webpack && grok publish dev --release", + "debug-interactivesciapptest-local": "webpack && grok publish local", + "release-interactivesciapptest-local": "webpack && grok publish local --release", + "debug-interactivesciapptest-release": "webpack && grok publish release", + "release-interactivesciapptest-release": "webpack && grok publish release --release" + }, + "canEdit": [ + "Developers" + ], + "canView": [ + "All users" + ], + "repository": { + "type": "git", + "url": "https://github.com/datagrok-ai/public.git", + "directory": "packages/InteractiveSciAppTest" + } +} diff --git a/packages/InteractiveSciAppTest/package.png b/packages/InteractiveSciAppTest/package.png new file mode 100644 index 0000000000..77aceb1bab Binary files /dev/null and b/packages/InteractiveSciAppTest/package.png differ diff --git a/packages/InteractiveSciAppTest/src/levins/app.ts b/packages/InteractiveSciAppTest/src/levins/app.ts new file mode 100644 index 0000000000..5ff21876b9 --- /dev/null +++ b/packages/InteractiveSciAppTest/src/levins/app.ts @@ -0,0 +1,576 @@ +// Levins Metapopulation Model — Application (Coordinator + UI) + +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; + +import { + DEFAULTS, validate, solve, validateOptimize, + LevinsParams, LevinsSolution, InputId, WorkerTask, WorkerResult, +} from './core'; + +import '../../css/levins.css'; + +const DEBOUNCE_MS = 50; +const OPTIMIZE_POINTS = 10000; + +export function levinsMetapopulationApp(): void { + // --- State --- + let computationsBlocked = false; + let debounceTimer: ReturnType | null = null; + const subs: {unsubscribe(): void}[] = []; + let activeWorkers: Worker[] = []; + let lineChart!: DG.Viewer; + + // --- Initial DataFrame --- + const initSolution = solve(DEFAULTS); + const df = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', initSolution.t), + DG.Column.fromFloat64Array('p', initSolution.p), + ]); + df.name = 'Levins Metapopulation'; + + const view = grok.shell.addTableView(df); + view.name = 'Levins Metapopulation Model'; + + // --- Rho badge (created before controls so onValueChanged callbacks can reference it) --- + const rhoBadge = ui.div([], 'd4-tag levins-rho-badge'); + ui.tooltip.bind(rhoBadge, 'Extinction-to-colonization rate ratio. \u03C1 < 1 \u2014 metapopulation persists, \u03C1 \u2265 1 \u2014 extinction.'); + + // --- Controls --- + + // Initial condition + const ctrlP0 = ui.input.float('Initial patch fraction p₀', { + value: DEFAULTS.p0, nullable: false, + min: 0.001, max: 1, + tooltipText: 'Fraction of patches occupied at t=0. If p₀=0, the population cannot recover — computation is skipped.', + onValueChanged: () => debouncedRun(), + }); + + // Parameters + const ctrlM = ui.input.float('Colonization rate m', { + value: DEFAULTS.m, nullable: false, + min: 0.001, max: 100, + tooltipText: 'How fast empty patches are colonized from occupied ones. Units: 1/time.', + onValueChanged: () => { updateRhoBadge(); debouncedRun(); }, + }); + + const ctrlE0 = ui.input.float('Extinction rate e₀', { + value: DEFAULTS.e0, nullable: false, + min: 0.001, max: 100, + tooltipText: 'Base local extinction rate of a subpopulation in a patch. With rescue effect — decreases as p grows. Units: 1/time.', + onValueChanged: () => { updateRhoBadge(); debouncedRun(); }, + }); + + const ctrlRescue = ui.input.toggle('Rescue effect', { + value: DEFAULTS.rescueEffect, + tooltipText: 'When enabled, extinction rate depends on p: e(p) = e₀·(1−p). More occupied patches — lower local extinction.', + onValueChanged: () => { updateRescueLabel(); runPrimary(); }, + }); + + // Argument + const ctrlTStart = ui.input.float('Start t₀', { + value: DEFAULTS.t_start, nullable: false, + min: 0, max: 10000, + tooltipText: 'Simulation start time. Usually 0.', + onValueChanged: () => { updateArgRanges(); debouncedRun(); }, + }); + + const ctrlTEnd = ui.input.float('End t_end', { + value: DEFAULTS.t_end, nullable: false, + min: 0.1, max: 10000, + tooltipText: 'Simulation end time. Recommended ≥ 5/e₀ so the system reaches equilibrium.', + onValueChanged: () => { updateArgRanges(); debouncedRun(); }, + }); + + const ctrlTStep = ui.input.float('Step Δt', { + value: DEFAULTS.t_step, nullable: false, + min: 0.001, max: 1000, + tooltipText: 'Grid step of the numerical solution. Affects chart detail, not stability (MRT is an implicit method).', + onValueChanged: () => debouncedRun(), + }); + + // Solver + const ctrlTolerance = ui.input.float('Tolerance', { + value: DEFAULTS.tolerance, nullable: false, + min: 1e-12, max: 1e-2, + tooltipText: 'MRT method numerical tolerance. Lower — more precise but slower. Recommended: 1e-6 … 1e-9.', + onValueChanged: () => debouncedRun(), + }); + + // Set formats (block computations to avoid spurious runs from format-triggered events) + computationsBlocked = true; + ctrlP0.format = '0.000'; + ctrlM.format = '0.000'; + ctrlE0.format = '0.000'; + ctrlTStart.format = '0.0'; + ctrlTEnd.format = '0.0'; + ctrlTStep.format = '0.000'; + ctrlTolerance.format = '0.##E+0'; + computationsBlocked = false; + + // --- Input map for validators --- + const inputMap: Record = { + 'ctrl_p0': ctrlP0, + 'ctrl_m': ctrlM, + 'ctrl_e0': ctrlE0, + 'ctrl_rescue': ctrlRescue, + 'ctrl_t_start': ctrlTStart, + 'ctrl_t_end': ctrlTEnd, + 'ctrl_t_step': ctrlTStep, + 'ctrl_tolerance': ctrlTolerance, + }; + + function updateRhoBadge(): void { + const m = ctrlM.value ?? DEFAULTS.m; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const rho = e0 / m; + const persists = rho < 1; + rhoBadge.textContent = `ρ = e₀/m = ${rho.toFixed(3)}`; + rhoBadge.classList.toggle('levins-rho-badge--persists', persists); + rhoBadge.classList.toggle('levins-rho-badge--extinct', !persists); + } + updateRhoBadge(); + + // --- Rescue effect label reactivity --- + function updateRescueLabel(): void { + if (ctrlRescue.value) { + ctrlE0.caption = 'Base extinction rate e₀'; + ctrlE0.setTooltip('Base local extinction rate. Effective rate: e(p) = e₀·(1−p)'); + } else { + ctrlE0.caption = 'Extinction rate e₀'; + ctrlE0.setTooltip('Base local extinction rate of a subpopulation in a patch. Units: 1/time.'); + } + } + + // --- Argument range reactivity --- + // Note: Datagrok InputBase does not have setOptions for changing min/max at runtime. + // Range validation is handled by the complex validator instead. + function updateArgRanges(): void { + // Ranges are enforced through validation (val_06, val_07) + } + + // --- Gather current inputs --- + function getInputs(): LevinsParams { + return { + p0: ctrlP0.value ?? DEFAULTS.p0, + m: ctrlM.value ?? DEFAULTS.m, + e0: ctrlE0.value ?? DEFAULTS.e0, + rescueEffect: ctrlRescue.value ?? DEFAULTS.rescueEffect, + t_start: ctrlTStart.value ?? DEFAULTS.t_start, + t_end: ctrlTEnd.value ?? DEFAULTS.t_end, + t_step: ctrlTStep.value ?? DEFAULTS.t_step, + tolerance: ctrlTolerance.value ?? DEFAULTS.tolerance, + }; + } + + // --- Validators --- + function addValidators(): void { + const validatorFor = (id: InputId) => { + return () => { + const inputs = getInputs(); + const errors = validate(inputs); + return errors.get(id) ?? null; + }; + }; + + ctrlP0.addValidator(validatorFor('ctrl_p0')); + ctrlM.addValidator(validatorFor('ctrl_m')); + ctrlE0.addValidator(validatorFor('ctrl_e0')); + ctrlTStart.addValidator(validatorFor('ctrl_t_start')); + ctrlTEnd.addValidator(validatorFor('ctrl_t_end')); + ctrlTStep.addValidator(validatorFor('ctrl_t_step')); + ctrlTolerance.addValidator(validatorFor('ctrl_tolerance')); + } + addValidators(); + + // --- Color coding --- + function updateColorCoding(): void { + const m = ctrlM.value ?? DEFAULTS.m; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const threshold = e0 / m; + const pCol = view.dataFrame.col('p'); + if (pCol == null) return; + + const rules: Record = {}; + rules['<' + threshold] = '#F44336'; + rules['>=' + threshold] = '#4CAF50'; + pCol.meta.colors.setConditional(rules); + } + + // --- Grid column header tooltip (via onCellTooltip, as in EDA) --- + function setupGridTooltip(): void { + view.grid.onCellTooltip((cell, x, y) => { + if (!cell.isColHeader) + return false; + + const colName = cell.tableColumn?.name; + if (colName === 'p') { + const m = ctrlM.value ?? DEFAULTS.m; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const rho = (e0 / m).toFixed(3); + ui.tooltip.show(ui.divV([ + ui.h2('Occupied patch fraction p(t)'), + ui.divText(`Color: green — persistence zone (p ≥ e₀/m)`), + ui.divText(`red — extinction threat zone (p < e₀/m)`), + ui.divText(`Threshold: e₀/m = ${rho}`), + ]), x, y); + return true; + } + + return false; + }); + } + + // --- Primary pipeline --- + function runPrimary(): void { + if (computationsBlocked) + return; + + const inputs = getInputs(); + const errors = validate(inputs); + + // Clear previous errors on all inputs + for (const input of Object.values(inputMap)) + input.input?.classList.remove('d4-invalid'); + + if (errors.size > 0) { + errors.forEach((_msg, id) => { + const input = inputMap[id]; + if (input) + input.input?.classList.add('d4-invalid'); + }); + clearResults(); + return; + } + + try { + const result = solve(inputs); + updateDataFrame(result); + updateColorCoding(); + } catch (err) { + clearResults(); + const msg = err instanceof Error ? err.message : 'Computation error'; + grok.shell.error(msg); + } + } + + function debouncedRun(): void { + if (debounceTimer !== null) + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => runPrimary(), DEBOUNCE_MS); + } + + // --- Update DataFrame --- + function updateDataFrame(result: LevinsSolution): void { + const newDf = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', result.t), + DG.Column.fromFloat64Array('p', result.p), + ]); + newDf.name = 'Levins Metapopulation'; + view.dataFrame = newDf; + lineChart.dataFrame = newDf; + } + + function clearResults(): void { + const emptyDf = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', new Float64Array(0)), + DG.Column.fromFloat64Array('p', new Float64Array(0)), + ]); + emptyDf.name = 'Levins Metapopulation'; + view.dataFrame = emptyDf; + lineChart.dataFrame = emptyDf; + } + + // --- Optimization task --- + let optimizeBtn: HTMLElement; + + function setOptimizeBtnEnabled(enabled: boolean): void { + optimizeBtn.classList.toggle('levins-btn--disabled', !enabled); + } + + async function runOptimization(mMin: number, mMax: number): Promise { + const inputs = getInputs(); + const errors = validate(inputs); + if (errors.size > 0) { + grok.shell.error('Internal error: invalid task parameters. Check the inputs and try again.'); + return; + } + + setOptimizeBtnEnabled(false); + + const pi = DG.TaskBarProgressIndicator.create('Optimizing m...'); + let canceled = false; + let completed = 0; + let errorCount = 0; + + const results: {m_i: number; p_end: number}[] = []; + const workerCount = Math.max(1, (navigator.hardwareConcurrency ?? 4) - 2); + + // Generate m values + const mValues: number[] = []; + for (let i = 0; i < OPTIMIZE_POINTS; i++) + mValues.push(mMin + i * (mMax - mMin) / (OPTIMIZE_POINTS - 1)); + + // Worker pool + const workerUrl = _package.webRoot + 'dist/optimize-worker.js'; + + try { + await new Promise((resolve, reject) => { + const taskQueue = [...mValues]; + activeWorkers = []; + + const createWorker = (): Worker | null => { + try { + const worker = new Worker(workerUrl); + activeWorkers.push(worker); + return worker; + } catch (_err) { + return null; + } + }; + + const processNext = (worker: Worker) => { + if (canceled) { + terminateWorkers(); + resolve(); + return; + } + + if (taskQueue.length === 0) { + worker.terminate(); + activeWorkers = activeWorkers.filter((w) => w !== worker); + if (activeWorkers.length === 0) + resolve(); + return; + } + + const m_i = taskQueue.shift()!; + const task: WorkerTask = { + m_i, + p0: inputs.p0, + e0: inputs.e0, + rescueEffect: inputs.rescueEffect, + t_start: inputs.t_start, + t_end: inputs.t_end, + t_step: inputs.t_step, + tolerance: inputs.tolerance, + }; + worker.postMessage(task); + }; + + const handleResult = (worker: Worker, event: MessageEvent) => { + const result = event.data; + completed++; + pi.update(Math.round(completed / OPTIMIZE_POINTS * 100), `${completed}/${OPTIMIZE_POINTS}`); + + if (result.error) + errorCount++; + else + results.push({m_i: result.m_i, p_end: result.p_end}); + + processNext(worker); + }; + + // Create worker pool + for (let i = 0; i < workerCount; i++) { + const worker = createWorker(); + if (worker == null) { + if (i === 0) { + reject(new Error('Failed to start parallel computations. Try again later.')); + return; + } + break; + } + + worker.onmessage = (event) => handleResult(worker, event); + worker.onerror = () => { + completed++; + errorCount++; + pi.update(Math.round(completed / OPTIMIZE_POINTS * 100), `${completed}/${OPTIMIZE_POINTS}`); + processNext(worker); + }; + + processNext(worker); + } + }); + } catch (err) { + grok.shell.error(err instanceof Error ? err.message : 'Failed to start parallel computations.'); + pi.close(); + setOptimizeBtnEnabled(true); + return; + } + + pi.close(); + terminateWorkers(); + setOptimizeBtnEnabled(true); + + if (canceled) + return; + + // Handle results + if (errorCount > 0 && errorCount < OPTIMIZE_POINTS) + grok.shell.warning(`${errorCount} of ${OPTIMIZE_POINTS} points failed to compute. Result based on ${OPTIMIZE_POINTS - errorCount} points.`); + + if (results.length === 0) { + grok.shell.error('Failed to compute any point. Check the parameters.'); + return; + } + + // Find optimal + let best = results[0]; + for (const r of results) { + if (r.p_end > best.p_end) + best = r; + } + + // Batch update: block primary, write m_optimal, unblock and run once + try { + computationsBlocked = true; + ctrlM.value = best.m_i; + computationsBlocked = false; + runPrimary(); + grok.shell.info(`Optimal m = ${best.m_i.toFixed(3)}\np(t_end) = ${best.p_end.toFixed(3)}`); + } catch (_err) { + computationsBlocked = false; + grok.shell.warning(`Optimal m = ${best.m_i.toFixed(3)}, but failed to update the field automatically. Enter the value manually.`); + } + } + + function terminateWorkers(): void { + for (const w of activeWorkers) + w.terminate(); + activeWorkers = []; + } + + // --- Optimize dialog --- + function showOptimizeDialog(): void { + const dlgMMin = ui.input.float('Minimum m', { + value: 0.1, nullable: false, + min: 0.001, max: 100, + tooltipText: 'Lower bound of the m search range. Must be less than the maximum value.', + }); + dlgMMin.format = '0.000'; + + const dlgMMax = ui.input.float('Maximum m', { + value: 1.0, nullable: false, + min: 0.001, max: 100, + tooltipText: 'Upper bound of the m search range. Must be greater than the minimum value.', + }); + dlgMMax.format = '0.000'; + + // Cross-validation of dialog inputs + const validateDialog = (): boolean => { + const mMin = dlgMMin.value ?? 0.1; + const mMax = dlgMMax.value ?? 1.0; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const rescueEffect = ctrlRescue.value ?? DEFAULTS.rescueEffect; + + const {errors, warning} = validateOptimize({m_min: mMin, m_max: mMax}, e0, rescueEffect); + + if (warning) + grok.shell.warning(warning); + + return errors.size === 0; + }; + + dlgMMin.addValidator(() => { + const mMin = dlgMMin.value ?? 0.1; + const mMax = dlgMMax.value ?? 1.0; + if (mMin <= 0) return 'Colonization rate must be positive'; + if (mMin >= mMax) return 'Minimum value must be less than maximum'; + return null; + }); + + dlgMMax.addValidator(() => { + const mMax = dlgMMax.value ?? 1.0; + if (mMax <= 0) return 'Colonization rate must be positive'; + return null; + }); + + ui.dialog('Find optimal m') + .add(dlgMMin) + .add(dlgMMax) + .onOK(() => { + if (!validateDialog()) + return; + runOptimization(dlgMMin.value!, dlgMMax.value!); + }) + .show(); + } + + // --- Toolbar buttons --- + optimizeBtn = ui.iconFA('search', () => showOptimizeDialog(), 'Find the m value that maximizes the occupied patch fraction at t_end'); + + const resetBtn = ui.iconFA('undo', () => { + computationsBlocked = true; + ctrlP0.value = DEFAULTS.p0; + ctrlM.value = DEFAULTS.m; + ctrlE0.value = DEFAULTS.e0; + ctrlRescue.value = DEFAULTS.rescueEffect; + ctrlTStart.value = DEFAULTS.t_start; + ctrlTEnd.value = DEFAULTS.t_end; + ctrlTStep.value = DEFAULTS.t_step; + ctrlTolerance.value = DEFAULTS.tolerance; + computationsBlocked = false; + updateRhoBadge(); + runPrimary(); + }, 'Reset all parameters to default values'); + + view.setRibbonPanels([[optimizeBtn, resetBtn]]); + + // --- Layout: left panel with form --- + const form = ui.form([]); + + form.append(ui.h2('Initial Condition')); + form.append(ctrlP0.root); + + form.append(ui.h2('Parameters')); + form.append(ctrlM.root); + form.append(ctrlE0.root); + form.append(ctrlRescue.root); + form.append(rhoBadge); + + form.append(ui.h2('Argument')); + form.append(ctrlTStart.root); + form.append(ctrlTEnd.root); + form.append(ctrlTStep.root); + + form.append(ui.h2('Solver')); + form.append(ctrlTolerance.root); + + const dockMng = view.dockManager; + dockMng.dock(form, DG.DOCK_TYPE.LEFT, null, undefined, 0.2); + + // --- Line chart --- + lineChart = view.addViewer('Line chart', { + xColumnName: 't', + yColumnNames: ['p'], + title: 'p(t) Dynamics', + }); + + const gridNode = dockMng.findNode(view.grid.root); + if (gridNode != null) + dockMng.dock(lineChart, DG.DOCK_TYPE.RIGHT, gridNode, undefined, 0.5); + + // --- Initial color coding and tooltip --- + updateColorCoding(); + setupGridTooltip(); + + // --- Cleanup on close --- + subs.push(grok.events.onViewRemoved.subscribe((v: any) => { + if (v === view) { + terminateWorkers(); + for (const sub of subs) + sub.unsubscribe(); + if (debounceTimer !== null) + clearTimeout(debounceTimer); + } + })); +} + +// Package reference (set from package.ts) +let _package: DG.Package; +export function setPackage(pkg: DG.Package): void { + _package = pkg; +} diff --git a/packages/InteractiveSciAppTest/src/levins/core.ts b/packages/InteractiveSciAppTest/src/levins/core.ts new file mode 100644 index 0000000000..b927b6a81e --- /dev/null +++ b/packages/InteractiveSciAppTest/src/levins/core.ts @@ -0,0 +1,144 @@ +// Levins Metapopulation Model — Computational Core + +import {mrt} from 'diff-grok'; + +import {LevinsParams, createLevinsODE, getEquilibrium} from './model'; + +// --- Re-exports --- + +export type {LevinsParams} from './model'; +export {createLevinsODE, getEquilibrium} from './model'; + +// --- Types --- + +export interface LevinsSolution { + t: Float64Array; + p: Float64Array; + p_star: number; +} + +export type InputId = 'ctrl_p0' | 'ctrl_m' | 'ctrl_e0' | 'ctrl_rescue' | + 'ctrl_t_start' | 'ctrl_t_end' | 'ctrl_t_step' | 'ctrl_tolerance'; + +export type ValidationErrors = Map; + +// --- Defaults --- + +export const DEFAULTS: LevinsParams = { + p0: 0.5, + m: 0.5, + e0: 0.2, + rescueEffect: false, + t_start: 0, + t_end: 50, + t_step: 0.1, + tolerance: 1e-7, +}; + +// --- Validation --- + +export function validate(inputs: LevinsParams): ValidationErrors { + const errors: ValidationErrors = new Map(); + const {p0, m, e0, rescueEffect, t_start, t_end, t_step, tolerance} = inputs; + + // val_01, val_02 + if (p0 <= 0) + errors.set('ctrl_p0', 'Initial patch fraction must be greater than 0'); + else if (p0 > 1) + errors.set('ctrl_p0', 'Initial patch fraction cannot exceed 1'); + + // val_03 + if (m <= 0) + errors.set('ctrl_m', 'Colonization rate must be positive'); + + // val_04 + if (e0 <= 0) + errors.set('ctrl_e0', 'Extinction rate must be positive'); + + // val_05 — only if val_03 and val_04 passed + if (!errors.has('ctrl_m') && !errors.has('ctrl_e0') && !rescueEffect && m <= e0) + errors.set('ctrl_m', 'Colonization rate must exceed extinction rate (m > e₀). With current values the metapopulation tends to extinction'); + + // val_06 + if (t_end <= t_start) { + errors.set('ctrl_t_end', 'End of interval must be greater than start'); + errors.set('ctrl_t_start', 'End of interval must be greater than start'); + } + + // val_07 + if (t_step <= 0) + errors.set('ctrl_t_step', 'Step must be positive'); + + // val_08 — only if val_06 and val_07 passed + if (!errors.has('ctrl_t_end') && !errors.has('ctrl_t_step') && t_step >= t_end - t_start) + errors.set('ctrl_t_step', 'Step must be less than interval length'); + + // val_09 + if (tolerance <= 0) + errors.set('ctrl_tolerance', 'Tolerance must be positive'); + + return errors; +} + +// --- Solver --- + +export function solve(inputs: LevinsParams): LevinsSolution { + const task = createLevinsODE(inputs); + const solution = mrt(task); + + return { + t: solution[0], + p: solution[1], + p_star: getEquilibrium(inputs.m, inputs.e0, inputs.rescueEffect), + }; +} + +// --- Optimization validation --- + +export interface OptimizeInputs { + m_min: number; + m_max: number; +} + +export type OptInputId = 'dlg_m_min' | 'dlg_m_max'; +export type OptValidationErrors = Map; + +export function validateOptimize( + opt: OptimizeInputs, e0: number, rescueEffect: boolean, +): {errors: OptValidationErrors; warning: string | null} { + const errors: OptValidationErrors = new Map(); + let warning: string | null = null; + + if (opt.m_min <= 0) + errors.set('dlg_m_min', 'Colonization rate must be positive'); + + if (opt.m_max <= 0) + errors.set('dlg_m_max', 'Colonization rate must be positive'); + + if (!errors.has('dlg_m_min') && !errors.has('dlg_m_max') && opt.m_min >= opt.m_max) + errors.set('dlg_m_min', 'Minimum value must be less than maximum'); + + if (errors.size === 0 && !rescueEffect && opt.m_max <= e0) + warning = 'With current e₀ the entire m range leads to extinction (m ≤ e₀). Increase the maximum or decrease e₀'; + + return {errors, warning}; +} + +// --- Worker message types --- + +export interface WorkerTask { + m_i: number; + p0: number; + e0: number; + rescueEffect: boolean; + t_start: number; + t_end: number; + t_step: number; + tolerance: number; +} + +export interface WorkerResult { + m_i: number; + p_end: number; + error?: string; +} diff --git a/packages/InteractiveSciAppTest/src/levins/model.ts b/packages/InteractiveSciAppTest/src/levins/model.ts new file mode 100644 index 0000000000..6a0a27f3db --- /dev/null +++ b/packages/InteractiveSciAppTest/src/levins/model.ts @@ -0,0 +1,37 @@ +// Levins Metapopulation Model — ODE specification + +import {ODEs} from 'diff-grok'; + +/** Parameters for the Levins metapopulation ODE */ +export interface LevinsParams { + p0: number; + m: number; + e0: number; + rescueEffect: boolean; + t_start: number; + t_end: number; + t_step: number; + tolerance: number; +} + +/** Creates the ODEs specification for the Levins model, usable in both main thread and workers */ +export function createLevinsODE(params: LevinsParams): ODEs { + const {p0, m, e0, rescueEffect, t_start, t_end, t_step, tolerance} = params; + + return { + name: 'Levins', + arg: {name: 't', start: t_start, finish: t_end, step: t_step}, + initial: [p0], + func: (_t: number, y: Float64Array, out: Float64Array) => { + const e = rescueEffect ? e0 * (1 - y[0]) : e0; + out[0] = m * y[0] * (1 - y[0]) - e * y[0]; + }, + tolerance: tolerance, + solutionColNames: ['p(t)'], + }; +} + +/** Computes the analytical equilibrium p* for the base Levins model */ +export function getEquilibrium(m: number, e0: number, rescueEffect: boolean): number { + return rescueEffect ? NaN : Math.max(0, 1 - e0 / m); +} diff --git a/packages/InteractiveSciAppTest/src/levins/optimize-worker.ts b/packages/InteractiveSciAppTest/src/levins/optimize-worker.ts new file mode 100644 index 0000000000..196dd6a32e --- /dev/null +++ b/packages/InteractiveSciAppTest/src/levins/optimize-worker.ts @@ -0,0 +1,40 @@ +// Levins Metapopulation Model — Web Worker for optimization task +// Uses mrt from diff-grok via the shared model definition + +import {mrt} from 'diff-grok'; + +import {createLevinsODE} from './model'; +import {WorkerTask, WorkerResult} from './core'; + +const ctx: Worker = self as unknown as Worker; + +ctx.onmessage = (event: MessageEvent) => { + const task = event.data; + + try { + const ode = createLevinsODE({ + p0: task.p0, + m: task.m_i, + e0: task.e0, + rescueEffect: task.rescueEffect, + t_start: task.t_start, + t_end: task.t_end, + t_step: task.t_step, + tolerance: task.tolerance, + }); + + const solution = mrt(ode); + const pValues = solution[1]; + const p_end = pValues[pValues.length - 1]; + + const result: WorkerResult = {m_i: task.m_i, p_end}; + ctx.postMessage(result); + } catch (err) { + const result: WorkerResult = { + m_i: task.m_i, + p_end: -1, + error: err instanceof Error ? err.message : 'Unknown error', + }; + ctx.postMessage(result); + } +}; diff --git a/packages/InteractiveSciAppTest/src/package-api.ts b/packages/InteractiveSciAppTest/src/package-api.ts new file mode 100644 index 0000000000..152e9543db --- /dev/null +++ b/packages/InteractiveSciAppTest/src/package-api.ts @@ -0,0 +1,21 @@ +/** +This file is auto-generated by the grok api command. +If you notice any changes, please push them to the repository. +Do not edit this file manually. +*/ +import * as grok from 'datagrok-api/grok'; +import * as DG from 'datagrok-api/dg'; + + +export namespace funcs { + export async function info(): Promise { + return await grok.functions.call('InteractiveSciAppTest:Info', {}); + } + + /** + Interactive ODE solver for the Levins model: simulation of occupied patch fraction p(t) dynamics + */ + export async function levinsMetapopulationModelApp(): Promise { + return await grok.functions.call('InteractiveSciAppTest:LevinsMetapopulationModelApp', {}); + } +} diff --git a/packages/InteractiveSciAppTest/src/package-test.ts b/packages/InteractiveSciAppTest/src/package-test.ts new file mode 100644 index 0000000000..15255df649 --- /dev/null +++ b/packages/InteractiveSciAppTest/src/package-test.ts @@ -0,0 +1,23 @@ +import { runTests, tests, TestContext , initAutoTests as initTests } from '@datagrok-libraries/utils/src/test'; +import * as DG from 'datagrok-api/dg'; + +import './tests/levins-api-tests'; +import './tests/levins-math-tests'; + +export let _package = new DG.Package(); +export { tests }; + +//name: test +//input: string category {optional: true} +//input: string test {optional: true} +//input: object testContext {optional: true} +//output: dataframe result +export async function test(category: string, test: string, testContext: TestContext): Promise { + const data = await runTests({ category, test, testContext }); + return DG.DataFrame.fromObjects(data)!; +} + +//name: initAutoTests +export async function initAutoTests() { + await initTests(_package, _package.getModule('package-test.js')); +} diff --git a/packages/InteractiveSciAppTest/src/package.g.ts b/packages/InteractiveSciAppTest/src/package.g.ts new file mode 100644 index 0000000000..8de619387a --- /dev/null +++ b/packages/InteractiveSciAppTest/src/package.g.ts @@ -0,0 +1 @@ +import * as DG from 'datagrok-api/dg'; diff --git a/packages/InteractiveSciAppTest/src/package.ts b/packages/InteractiveSciAppTest/src/package.ts new file mode 100644 index 0000000000..93aec6ce82 --- /dev/null +++ b/packages/InteractiveSciAppTest/src/package.ts @@ -0,0 +1,22 @@ +/* Do not change these import lines to match external modules in webpack configuration */ +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; +export * from './package.g'; + +import {levinsMetapopulationApp, setPackage} from './levins/app'; + +export const _package = new DG.Package(); + +//name: info +export function info() { + grok.shell.info(_package.webRoot); +} + +//name: Levins Metapopulation Model +//tags: app +//description: Interactive ODE solver for the Levins model: simulation of occupied patch fraction p(t) dynamics +export function levinsMetapopulationModelApp(): void { + setPackage(_package); + levinsMetapopulationApp(); +} diff --git a/packages/InteractiveSciAppTest/src/tests/levins-api-tests.ts b/packages/InteractiveSciAppTest/src/tests/levins-api-tests.ts new file mode 100644 index 0000000000..d6db9d2b21 --- /dev/null +++ b/packages/InteractiveSciAppTest/src/tests/levins-api-tests.ts @@ -0,0 +1,191 @@ +// Levins Metapopulation Model — API tests + +import {category, test, expect} from '@datagrok-libraries/utils/src/test'; + +import {DEFAULTS, validate, validateOptimize} from '../levins/core'; + +category('API: Validation', () => { + // --- val_01: p0 <= 0 --- + test('val_01: p0 = 0', async () => { + const errors = validate({...DEFAULTS, p0: 0}); + expect(errors.has('ctrl_p0'), true, 'Should reject p0 = 0'); + }); + + test('val_01: p0 = -1', async () => { + const errors = validate({...DEFAULTS, p0: -1}); + expect(errors.has('ctrl_p0'), true, 'Should reject p0 < 0'); + }); + + // --- val_02: p0 > 1 --- + test('val_02: p0 = 1.1', async () => { + const errors = validate({...DEFAULTS, p0: 1.1}); + expect(errors.has('ctrl_p0'), true, 'Should reject p0 > 1'); + }); + + // --- p0 valid boundary --- + test('p0 = 0.001 (valid boundary)', async () => { + const errors = validate({...DEFAULTS, p0: 0.001}); + expect(errors.has('ctrl_p0'), false, 'Should accept p0 = 0.001'); + }); + + test('p0 = 1 (valid boundary)', async () => { + const errors = validate({...DEFAULTS, p0: 1}); + expect(errors.has('ctrl_p0'), false, 'Should accept p0 = 1'); + }); + + // --- val_03: m <= 0 --- + test('val_03: m = 0', async () => { + const errors = validate({...DEFAULTS, m: 0}); + expect(errors.has('ctrl_m'), true, 'Should reject m = 0'); + }); + + test('val_03: m = -0.5', async () => { + const errors = validate({...DEFAULTS, m: -0.5}); + expect(errors.has('ctrl_m'), true, 'Should reject m < 0'); + }); + + // --- val_04: e0 <= 0 --- + test('val_04: e0 = 0', async () => { + const errors = validate({...DEFAULTS, e0: 0}); + expect(errors.has('ctrl_e0'), true, 'Should reject e0 = 0'); + }); + + test('val_04: e0 = -0.1', async () => { + const errors = validate({...DEFAULTS, e0: -0.1}); + expect(errors.has('ctrl_e0'), true, 'Should reject e0 < 0'); + }); + + // --- val_05: m <= e0 (no rescue) --- + test('val_05: m = e0, no rescue', async () => { + const errors = validate({...DEFAULTS, m: 0.5, e0: 0.5, rescueEffect: false}); + expect(errors.has('ctrl_m'), true, 'Should reject m = e0 without rescue'); + }); + + test('val_05: m < e0, no rescue', async () => { + const errors = validate({...DEFAULTS, m: 0.3, e0: 0.5, rescueEffect: false}); + expect(errors.has('ctrl_m'), true, 'Should reject m < e0 without rescue'); + }); + + test('val_05: m <= e0 with rescue (allowed)', async () => { + const errors = validate({...DEFAULTS, m: 0.3, e0: 0.5, rescueEffect: true}); + expect(errors.has('ctrl_m'), false, 'Should allow m <= e0 with rescue'); + }); + + test('val_05: skipped when val_03 fails', async () => { + const errors = validate({...DEFAULTS, m: 0, e0: 0.5, rescueEffect: false}); + expect(errors.get('ctrl_m'), 'Colonization rate must be positive'); + }); + + // --- val_06: t_end <= t_start --- + test('val_06: t_end = t_start', async () => { + const errors = validate({...DEFAULTS, t_start: 10, t_end: 10}); + expect(errors.has('ctrl_t_end'), true, 'Should reject t_end = t_start'); + expect(errors.has('ctrl_t_start'), true, 'Should set error on t_start too'); + }); + + test('val_06: t_end < t_start', async () => { + const errors = validate({...DEFAULTS, t_start: 10, t_end: 5}); + expect(errors.has('ctrl_t_end'), true, 'Should reject t_end < t_start'); + }); + + // --- val_07: t_step <= 0 --- + test('val_07: t_step = 0', async () => { + const errors = validate({...DEFAULTS, t_step: 0}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step = 0'); + }); + + test('val_07: t_step = -0.1', async () => { + const errors = validate({...DEFAULTS, t_step: -0.1}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step < 0'); + }); + + // --- val_08: t_step >= t_end - t_start --- + test('val_08: t_step = interval length', async () => { + const errors = validate({...DEFAULTS, t_start: 0, t_end: 50, t_step: 50}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step = interval length'); + }); + + test('val_08: t_step > interval length', async () => { + const errors = validate({...DEFAULTS, t_start: 0, t_end: 50, t_step: 100}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step > interval length'); + }); + + test('val_08: skipped when val_07 fails', async () => { + const errors = validate({...DEFAULTS, t_step: -1}); + expect(errors.get('ctrl_t_step'), 'Step must be positive'); + }); + + // --- val_09: tolerance <= 0 --- + test('val_09: tolerance = 0', async () => { + const errors = validate({...DEFAULTS, tolerance: 0}); + expect(errors.has('ctrl_tolerance'), true, 'Should reject tolerance = 0'); + }); + + test('val_09: tolerance = -1e-7', async () => { + const errors = validate({...DEFAULTS, tolerance: -1e-7}); + expect(errors.has('ctrl_tolerance'), true, 'Should reject tolerance < 0'); + }); + + // --- valid defaults --- + test('Defaults pass validation', async () => { + const errors = validate(DEFAULTS); + expect(errors.size, 0, 'Default parameters should be valid'); + }); + + // --- multiple errors --- + test('Multiple simultaneous errors', async () => { + const errors = validate({...DEFAULTS, p0: 0, m: 0, e0: 0, t_step: 0, tolerance: 0}); + expect(errors.size >= 4, true, 'Should report multiple errors'); + expect(errors.has('ctrl_p0'), true); + expect(errors.has('ctrl_m'), true); + expect(errors.has('ctrl_e0'), true); + expect(errors.has('ctrl_t_step'), true); + expect(errors.has('ctrl_tolerance'), true); + }); +}); + +category('API: Optimization Validation', () => { + // --- opt_val_01: m_min <= 0 --- + test('opt_val_01: m_min = 0', async () => { + const {errors} = validateOptimize({m_min: 0, m_max: 1}, 0.2, false); + expect(errors.has('dlg_m_min'), true, 'Should reject m_min = 0'); + }); + + // --- opt_val_02: m_max <= 0 --- + test('opt_val_02: m_max = -1', async () => { + const {errors} = validateOptimize({m_min: 0.1, m_max: -1}, 0.2, false); + expect(errors.has('dlg_m_max'), true, 'Should reject m_max < 0'); + }); + + // --- opt_val_03: m_min >= m_max --- + test('opt_val_03: m_min = m_max', async () => { + const {errors} = validateOptimize({m_min: 0.5, m_max: 0.5}, 0.2, false); + expect(errors.has('dlg_m_min'), true, 'Should reject m_min = m_max'); + }); + + test('opt_val_03: m_min > m_max', async () => { + const {errors} = validateOptimize({m_min: 1.0, m_max: 0.5}, 0.2, false); + expect(errors.has('dlg_m_min'), true, 'Should reject m_min > m_max'); + }); + + // --- opt_val_04: warning when m_max <= e0 --- + test('opt_val_04: m_max <= e0, no rescue — warning', async () => { + const {errors, warning} = validateOptimize({m_min: 0.05, m_max: 0.1}, 0.2, false); + expect(errors.size, 0, 'Should not block'); + expect(warning !== null, true, 'Should produce warning'); + }); + + test('opt_val_04: m_max <= e0 with rescue — no warning', async () => { + const {errors, warning} = validateOptimize({m_min: 0.05, m_max: 0.1}, 0.2, true); + expect(errors.size, 0); + expect(warning, null, 'No warning with rescue effect'); + }); + + // --- valid --- + test('Valid optimization inputs', async () => { + const {errors, warning} = validateOptimize({m_min: 0.1, m_max: 1.0}, 0.2, false); + expect(errors.size, 0, 'Should pass'); + expect(warning, null, 'No warning'); + }); +}); + diff --git a/packages/InteractiveSciAppTest/src/tests/levins-math-tests.ts b/packages/InteractiveSciAppTest/src/tests/levins-math-tests.ts new file mode 100644 index 0000000000..a1c0cb397b --- /dev/null +++ b/packages/InteractiveSciAppTest/src/tests/levins-math-tests.ts @@ -0,0 +1,201 @@ +// Levins Metapopulation Model — Math tests + +import {category, test, expect, expectFloat} from '@datagrok-libraries/utils/src/test'; +import {mrt, ODEs} from 'diff-grok'; + +import {createLevinsODE, LevinsParams} from '../levins/model'; +import {DEFAULTS, solve, getEquilibrium} from '../levins/core'; + +// ── Helpers ── + +/** Max absolute error between numerical and exact solutions across all grid points */ +function getMaxError(odes: ODEs, exact: (t: number) => number): number { + const solution = mrt(odes); + const tArr = solution[0]; + const yArr = solution[1]; + let error = 0; + + for (let i = 0; i < tArr.length; i++) + error = Math.max(error, Math.abs(exact(tArr[i]) - yArr[i])); + + return error; +} + +/** Evaluates func at given p and returns dp/dt */ +function evalFunc(params: LevinsParams, p: number): number { + const ode = createLevinsODE(params); + const y = new Float64Array([p]); + const out = new Float64Array(1); + ode.func(0, y, out); + return out[0]; +} + +// ── Correctness: MRT solver ── + +const TINY = 0.1; + +category('Math: MRT solver', () => { + test('Non-stiff 1D: dy/dt = 4·exp(0.8t) − 0.5y', async () => { + // Reference: Chapra & Canale, p. 736 + const odes: ODEs = { + name: 'Non-stiff 1D', + arg: {name: 't', start: 0, finish: 4, step: 0.01}, + initial: [2], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = 4 * Math.exp(0.8 * _t) - 0.5 * y[0]; + }, + tolerance: 1e-6, + solutionColNames: ['y'], + }; + + const exact = (t: number) => + (4 / 1.3) * (Math.exp(0.8 * t) - Math.exp(-0.5 * t)) + 2 * Math.exp(-0.5 * t); + + const error = getMaxError(odes, exact); + expectFloat(error, 0, TINY); + }); + + test('Stiff 1D: dy/dt = −1000y + 3000 − 2000·exp(−t)', async () => { + // Reference: Chapra & Canale, p. 767 + const odes: ODEs = { + name: 'Stiff 1D', + arg: {name: 't', start: 0, finish: 4, step: 0.01}, + initial: [0], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = -1000 * y[0] + 3000 - 2000 * Math.exp(-_t); + }, + tolerance: 5e-7, + solutionColNames: ['y'], + }; + + const exact = (t: number) => + 3 - 0.998 * Math.exp(-1000 * t) - 2.002 * Math.exp(-t); + + const error = getMaxError(odes, exact); + expectFloat(error, 0, TINY); + }); + + test('Stiff 2D: VDPOL (van der Pol, µ=1000)', async () => { + // Reference: https://archimede.uniba.it/~testset/report/vdpol.pdf + const vdpol: ODEs = { + name: 'van der Pol', + arg: {name: 't', start: 0, finish: 2000, step: 0.1}, + initial: [-1, 1], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = y[1]; + out[1] = -y[0] + 1000 * (1 - y[0] * y[0]) * y[1]; + }, + tolerance: 1e-12, + solutionColNames: ['x1', 'x2'], + }; + + mrt(vdpol); + }, {benchmark: true, timeout: 2000}); +}); + +// ── Correctness: Levins func ── + +const BASE: LevinsParams = { + p0: 0.5, m: 0.5, e0: 0.2, rescueEffect: false, + t_start: 0, t_end: 50, t_step: 0.1, tolerance: 1e-7, +}; + +category('Math: Levins func', () => { + // dp/dt = m·p·(1−p) − e₀·p = 0.5·0.5·0.5 − 0.2·0.5 = 0.125 − 0.1 = 0.025 + test('func_01: base model, p=0.5', async () => { + expectFloat(evalFunc({...BASE, m: 0.5, e0: 0.2, rescueEffect: false}, 0.5), 0.025, 1e-12); + }); + + // dp/dt = 1.0·0.1·0.9 − 0.3·0.1 = 0.09 − 0.03 = 0.06 + test('func_02: base model, low p=0.1', async () => { + expectFloat(evalFunc({...BASE, m: 1.0, e0: 0.3, rescueEffect: false}, 0.1), 0.06, 1e-12); + }); + + // At equilibrium p*=1−e₀/m=0.6: dp/dt = 0.5·0.6·0.4 − 0.2·0.6 = 0.12 − 0.12 = 0.0 + test('func_03: equilibrium p*=0.6, dp/dt=0', async () => { + expectFloat(evalFunc({...BASE, m: 0.5, e0: 0.2, rescueEffect: false}, 0.6), 0.0, 1e-12); + }); + + // Rescue: e=e₀·(1−p)=0.2·0.5=0.1; dp/dt = 0.5·0.5·0.5 − 0.1·0.5 = 0.125 − 0.05 = 0.075 + test('func_04: rescue effect, p=0.5', async () => { + expectFloat(evalFunc({...BASE, m: 0.5, e0: 0.2, rescueEffect: true}, 0.5), 0.075, 1e-12); + }); + + // Rescue: e=0.5·(1−0.8)=0.1; dp/dt = 0.3·0.8·0.2 − 0.1·0.8 = 0.048 − 0.08 = −0.032 + test('func_05: rescue + decline, p=0.8', async () => { + expectFloat(evalFunc({...BASE, m: 0.3, e0: 0.5, rescueEffect: true}, 0.8), -0.032, 1e-12); + }); +}); + +// ── Correctness: Levins equilibrium ── + +category('Math: Equilibrium', () => { + test('p* = 1 - e0/m (base model)', async () => { + expectFloat(getEquilibrium(0.5, 0.2, false), 0.6, 1e-10); + }); + + test('p* = 0 when m <= e0', async () => { + expectFloat(getEquilibrium(0.2, 0.5, false), 0, 1e-10); + }); + + test('p* = 0 when m = e0', async () => { + expectFloat(getEquilibrium(0.5, 0.5, false), 0, 1e-10); + }); + + test('p* = NaN with rescue effect', async () => { + expect(isNaN(getEquilibrium(0.5, 0.2, true)), true, 'Should be NaN with rescue'); + }); +}); + +// ── Output property verification: solve ── + +category('Math: Solve output properties', () => { + test('solve_01: default parameters produce non-empty arrays of equal length', async () => { + const result = solve(DEFAULTS); + expect(result.t.length > 0, true, 't should be non-empty'); + expect(result.p.length > 0, true, 'p should be non-empty'); + expect(result.t.length, result.p.length, 't and p should have equal length'); + }); + + test('solve_02: p values in [0, 1]', async () => { + const result = solve(DEFAULTS); + for (let i = 0; i < result.p.length; i++) + expect(result.p[i] >= 0 && result.p[i] <= 1, true, `p[${i}] = ${result.p[i]} out of [0, 1]`); + }); + + test('solve_03: p(0) = p0', async () => { + const result = solve(DEFAULTS); + expectFloat(result.p[0], DEFAULTS.p0, 1e-6); + }); + + test('solve_04: t starts at t_start', async () => { + const result = solve(DEFAULTS); + expectFloat(result.t[0], DEFAULTS.t_start, 1e-12); + }); + + test('solve_05: convergence to p*', async () => { + const params = {...DEFAULTS, m: 0.5, e0: 0.2, rescueEffect: false, t_end: 200}; + const result = solve(params); + const pStar = getEquilibrium(params.m, params.e0, params.rescueEffect); + expectFloat(result.p[result.p.length - 1], pStar, 0.01); + }); + + test('solve_06: rescue effect — p in [0, 1]', async () => { + const params = {...DEFAULTS, m: 0.3, e0: 0.5, rescueEffect: true, t_end: 100}; + const result = solve(params); + for (let i = 0; i < result.p.length; i++) + expect(result.p[i] >= 0 && result.p[i] <= 1, true, `p[${i}] = ${result.p[i]} out of [0, 1]`); + }); + + test('solve_07: higher m → higher p(t_end)', async () => { + const r1 = solve({...DEFAULTS, m: 0.5, e0: 0.2}); + const r2 = solve({...DEFAULTS, m: 1.0, e0: 0.2}); + expect(r2.p[r2.p.length - 1] > r1.p[r1.p.length - 1], true, + 'p(t_end) with m=1.0 should exceed p(t_end) with m=0.5'); + }); + + test('solve_08: custom p0', async () => { + const result = solve({...DEFAULTS, p0: 0.9}); + expectFloat(result.p[0], 0.9, 1e-6); + }); +}); diff --git a/packages/InteractiveSciAppTest/tsconfig.json b/packages/InteractiveSciAppTest/tsconfig.json new file mode 100644 index 0000000000..b9b0997746 --- /dev/null +++ b/packages/InteractiveSciAppTest/tsconfig.json @@ -0,0 +1,71 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "lib": ["ES2022", "dom"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/packages/InteractiveSciAppTest/webpack.config.js b/packages/InteractiveSciAppTest/webpack.config.js new file mode 100644 index 0000000000..a06441f8ba --- /dev/null +++ b/packages/InteractiveSciAppTest/webpack.config.js @@ -0,0 +1,69 @@ +const path = require('path'); +const {execSync} = require('child_process'); +const packageName = path.parse(require('./package.json').name).name.toLowerCase().replace(/-/g, ''); + +function getDatagrokTools() { + const pluginPath = 'datagrok-tools/plugins/func-gen-plugin'; + try { + return require(pluginPath); + } catch (e) { + try { + const globalPath = execSync('npm root -g').toString().trim(); + return require(path.join(globalPath, pluginPath)); + } catch (globalErr) { + console.error('\n' + '='.repeat(60)); + console.error('ERROR: datagrok-tools not found!'); + console.error('To fix this, please install the tools globally by running:'); + console.error('\n npm install -g datagrok-tools\n'); + console.error('='.repeat(60) + '\n'); + process.exit(1); + } + } +} + +const FuncGeneratorPlugin = getDatagrokTools(); + +module.exports = { + cache: { + type: 'filesystem', + }, + mode: 'development', + entry: { + test: {filename: 'package-test.js', library: {type: 'var', name: `${packageName}_test`}, import: './src/package-test.ts'}, + package: './src/package.ts', + 'optimize-worker': {filename: 'optimize-worker.js', import: './src/levins/optimize-worker.ts'}, + }, + resolve: { + symlinks: false, + extensions: ['.wasm', '.mjs', '.ts', '.json', '.js', '.tsx'], + }, + module: { + rules: [ + {test: /\.tsx?$/, loader: 'ts-loader', options: {allowTsInNodeModules: true}}, + {test: /\.css$/i, use: ['style-loader', 'css-loader']}, + ], + }, + plugins: [ + new FuncGeneratorPlugin({outputPath: './src/package.g.ts'}), + ], + devtool: 'source-map', + externals: { + 'datagrok-api/dg': 'DG', + 'datagrok-api/grok': 'grok', + 'datagrok-api/ui': 'ui', + 'openchemlib/full.js': 'OCL', + 'rxjs': 'rxjs', + 'rxjs/operators': 'rxjs.operators', + 'cash-dom': '$', + 'dayjs': 'dayjs', + 'wu': 'wu', + 'exceljs': 'ExcelJS', + 'html2canvas': 'html2canvas', + }, + output: { + filename: '[name].js', + library: packageName, + libraryTarget: 'var', + path: path.resolve(__dirname, 'dist'), + }, +}; diff --git a/packages/LotkaVolterraGuided/.gitignore b/packages/LotkaVolterraGuided/.gitignore new file mode 100644 index 0000000000..fb3a960466 --- /dev/null +++ b/packages/LotkaVolterraGuided/.gitignore @@ -0,0 +1,33 @@ +# Developer keys +upload.keys.json + +# Dependency directories +node_modules/ + +# Webpack outputs +dist/ + +# IDEs +# VS Code (https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore) +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.history/ + +# Intellij IDEA/WebStorm (https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore) +.idea/inspectionProfiles/ +.idea/**/compiler.xml +.idea/**/encodings.xml +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/codeStyles/ + +**/*.d.ts +# Emitted *.js files +src/**/*.js diff --git a/packages/LotkaVolterraGuided/.npmignore b/packages/LotkaVolterraGuided/.npmignore new file mode 100644 index 0000000000..3b8fe98436 --- /dev/null +++ b/packages/LotkaVolterraGuided/.npmignore @@ -0,0 +1,30 @@ +# Developer keys +upload.keys.json + +# Dependency directories +node_modules/ + +# IDEs +# VS Code (https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore) +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.history/ + +# Intellij IDEA/WebStorm (https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore) +.idea/inspectionProfiles/ +.idea/**/compiler.xml +.idea/**/encodings.xml +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/codeStyles/ + + +**/*.d.ts +!src/package-api.d.ts diff --git a/packages/LotkaVolterraGuided/CHANGELOG.md b/packages/LotkaVolterraGuided/CHANGELOG.md new file mode 100644 index 0000000000..9f4255f9ac --- /dev/null +++ b/packages/LotkaVolterraGuided/CHANGELOG.md @@ -0,0 +1,3 @@ +# LotkaVolterraGuided changelog + +## 0.0.1 (2026-03-11) \ No newline at end of file diff --git a/packages/LotkaVolterraGuided/README.md b/packages/LotkaVolterraGuided/README.md new file mode 100644 index 0000000000..291a2c5b27 --- /dev/null +++ b/packages/LotkaVolterraGuided/README.md @@ -0,0 +1,3 @@ +# LotkaVolterraGuided + +`LotkaVolterraGuided` is a [package](https://datagrok.ai/help/develop/develop#packages) for the [Datagrok](https://datagrok.ai) platform diff --git a/packages/LotkaVolterraGuided/app-specification.md b/packages/LotkaVolterraGuided/app-specification.md new file mode 100644 index 0000000000..14e568908d --- /dev/null +++ b/packages/LotkaVolterraGuided/app-specification.md @@ -0,0 +1,459 @@ +# Application Specification: Lotka-Volterra Predator-Prey Simulation + +## 1. General Information + +| Field | Value | +|---|---| +| Application name | Lotka-Volterra Predator-Prey Simulation | +| Package | LotkaVolterraGuided | +| Entry function | `lotkaVolterraApp()` | +| Brief description | Interactive ODE simulation of the Lotka-Volterra predator-prey model with real-time visualization, phase portrait, and brute-force parameter optimization via web workers. | +| Main view | `DG.TableView` | + +--- + +## 2. Computational Tasks (Core) + +### 2.1. Task List + +| Task ID | Name | Pipeline type | Trigger | +|---|---|---|---| +| `task_primary` | Lotka-Volterra RK4 solution | Primary (reactive) | Any input change | +| `task_optimize` | Optimize max prey (grid search) | Secondary (on demand) | Button `btn_optimize` | + +### 2.2. Task Description: `task_primary` + +**General characteristics:** + +| Field | Value | +|---|---| +| Name | Lotka-Volterra RK4 solution | +| Description | Numerical solution of the 2D ODE system `dx/dt = αx − βxy`, `dy/dt = δxy − γy` using RK4, returning trajectories x(t) and y(t), equilibrium points, and summary statistics | +| Synchronicity | Synchronous | +| Execution environment | Main thread | +| Parallelization | No | +| Dependency on other tasks | No | + +**Computation Formulas and Model** + +**Level 1 — required minimum:** + +Variables: + +| Variable | Meaning | Units | Domain | +|---|---|---|---| +| `x` | Prey population | individuals | `x ≥ 0` | +| `y` | Predator population | individuals | `y ≥ 0` | +| `α` (alpha) | Prey birth rate | 1/time | `α > 0` | +| `β` (beta) | Predation rate | 1/(individuals·time) | `β > 0` | +| `δ` (delta) | Predator growth efficiency | 1/(individuals·time) | `δ > 0` | +| `γ` (gamma) | Predator death rate | 1/time | `γ > 0` | +| `x₀` | Initial prey population | individuals | `x₀ > 0` | +| `y₀` | Initial predator population | individuals | `y₀ > 0` | +| `T` | Total simulation time | time | `T > 0` | + +Relationships: + +``` +dx/dt = α·x − β·x·y +dy/dt = δ·x·y − γ·y +``` + +Equilibrium points: +- Trivial: (0, 0) +- Non-trivial: (x*, y*) = (γ/δ, α/β) + +Output properties (invariants): + +| Property | Description | +|---|---| +| `x(t) ≥ 0` for all `t` | Prey population is non-negative | +| `y(t) ≥ 0` for all `t` | Predator population is non-negative | +| `x(0) = x₀` | Initial prey condition preserved | +| `y(0) = y₀` | Initial predator condition preserved | +| Phase portrait is a closed orbit (for default params) | Conservation of Hamiltonian `H = δx + βy − γ ln(x) − α ln(y)` | + +Reference examples: + +| # | Inputs | Expected output | Source | +|---|---|---|---| +| 1 | `α=1.0, β=0.1, δ=0.075, γ=1.5, x₀=10, y₀=5` | `dx/dt = 1.0·10 − 0.1·10·5 = 5.0`, `dy/dt = 0.075·10·5 − 1.5·5 = −3.75` | Manual calculation | +| 2 | At equilibrium `x*=γ/δ=20, y*=α/β=10` | `dx/dt = 0`, `dy/dt = 0` | Manual calculation | +| 3 | `α=1.0, β=0.1, δ=0.075, γ=1.5, x=30, y=4` | `dx/dt = 1.0·30 − 0.1·30·4 = 18`, `dy/dt = 0.075·30·4 − 1.5·4 = 3.0` | Manual calculation | + +**Level 2 — full formalization:** + +- Complete mathematical formulation: classical Lotka-Volterra predator-prey ODE system with constant coefficients. No boundary conditions (IVP). +- Analytical properties: non-trivial equilibrium at (γ/δ, α/β); solutions are periodic orbits around the equilibrium; conserved quantity H = δx + βy − γ ln(x) − α ln(y). +- Numerical method: RK4 (4th-order Runge-Kutta), explicit method, O(h⁴) local truncation error, suitable for non-stiff problems. + +**Input parameters:** + +| Parameter | Type | Units | Domain | Description | +|---|---|---|---|---| +| `alpha` | `number` | 1/time | `> 0` | Prey birth rate | +| `beta` | `number` | 1/(ind·time) | `> 0` | Predation rate | +| `delta` | `number` | 1/(ind·time) | `> 0` | Predator growth efficiency | +| `gamma` | `number` | 1/time | `> 0` | Predator death rate | +| `x0` | `number` | individuals | `> 0` | Initial prey population | +| `y0` | `number` | individuals | `> 0` | Initial predator population | +| `T` | `number` | time | `> 0` | Total simulation time | + +**Output data:** + +| Parameter | Type | Description | +|---|---|---| +| `t` | `Float64Array` | Time values | +| `x` | `Float64Array` | Prey population values | +| `y` | `Float64Array` | Predator population values | +| `xStar` | `number` | Equilibrium prey: γ/δ | +| `yStar` | `number` | Equilibrium predator: α/β | + +**Computation implementation:** + +| Step | Implementation method | Details | Documentation | +|---|---|---|---| +| 1 | External library | `diff-grok` v1.2.0, function `rk4(task: ODEs)`. RK4 is a 4th-order explicit method suitable for non-stiff ODEs. | [diff-grok README](https://github.com/datagrok-ai/diff-grok) | +| 2 | Custom method | Equilibrium: `xStar = gamma / delta`, `yStar = alpha / beta` | — | +| 3 | Custom method | Summary stats: `maxPrey = max(x)`, `maxPredators = max(y)`, `stepCount = t.length` | — | + +**Library call:** + +```typescript +import { ODEs, rk4 } from 'diff-grok'; + +const task: ODEs = { + name: 'LotkaVolterra', + arg: { name: 't', start: 0, finish: T, step: 0.05 }, + initial: [x0, y0], + func: (t, y, out) => { + out[0] = alpha * y[0] - beta * y[0] * y[1]; + out[1] = delta * y[0] * y[1] - gamma * y[1]; + }, + tolerance: 1e-6, + solutionColNames: ['x(t)', 'y(t)'], +}; +const solution = rk4(task); +``` + +**ODE right-hand side reference examples:** + +| # | α | β | δ | γ | x | y | Expected dx/dt | Expected dy/dt | Derivation | +|---|---|---|---|---|---|---|---|---|---| +| 1 | 1.0 | 0.1 | 0.075 | 1.5 | 10 | 5 | 5.0 | −3.75 | `1.0·10−0.1·10·5=5`, `0.075·10·5−1.5·5=−3.75` | +| 2 | 1.0 | 0.1 | 0.075 | 1.5 | 20 | 10 | 0.0 | 0.0 | At equilibrium (γ/δ=20, α/β=10) | +| 3 | 1.0 | 0.1 | 0.075 | 1.5 | 30 | 4 | 18.0 | 3.0 | `1.0·30−0.1·30·4=18`, `0.075·30·4−1.5·4=3` | +| 4 | 2.0 | 0.5 | 0.3 | 1.0 | 5 | 2 | 5.0 | 1.0 | `2.0·5−0.5·5·2=5`, `0.3·5·2−1.0·2=1` | +| 5 | 0.5 | 0.02 | 0.01 | 0.3 | 50 | 10 | 15.0 | −2.0 | `0.5·50−0.02·50·10=15`, `0.01·50·10−0.3·10=2` | + +--- + +### 2.3. Task Description: `task_optimize` + +**General characteristics:** + +| Field | Value | +|---|---| +| Name | Optimize Max Prey (grid search) | +| Description | Brute-force grid search over 4 coefficients (α, β, δ, γ) with 10% step size. For each combination, solves the ODE and finds the peak prey population. Returns the combination that maximizes peak prey. | +| Synchronicity | Asynchronous | +| Execution environment | WebWorkers (parallel) | +| Parallelization | Yes — grid points distributed across worker pool of size `Math.max(1, navigator.hardwareConcurrency - 2)` | +| Dependency on other tasks | Uses current x₀, y₀, T from primary pipeline controls | + +**Input parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `alpha_min, alpha_max` | `number` | Current slider range for α | +| `beta_min, beta_max` | `number` | Current slider range for β | +| `delta_min, delta_max` | `number` | Current slider range for δ | +| `gamma_min, gamma_max` | `number` | Current slider range for γ | +| `x0, y0, T` | `number` | Taken from current state of main UI controls (snapshot) | + +**Output data:** + +| Parameter | Type | Description | +|---|---|---| +| `alpha_opt` | `number` | Optimal α | +| `beta_opt` | `number` | Optimal β | +| `delta_opt` | `number` | Optimal δ | +| `gamma_opt` | `number` | Optimal γ | +| `maxPrey` | `number` | Maximum peak prey achieved | + +**Computation implementation:** + +| Step | Implementation method | Details | +|---|---|---| +| 1 | Custom method | Generate grid: 11 values per parameter (0%, 10%, ..., 100% of range), total 11⁴ = 14641 points | +| 2 | External library in workers | `diff-grok` v1.2.0, `rk4(task)` — for each grid point in a WebWorker | +| 3 | Custom method | Find grid point with maximum peak prey | +| 4 | Datagrok API | Write optimal values to sliders via batch update | + +**Parallelization strategy:** + +``` +Number of workers = Math.max(1, navigator.hardwareConcurrency - 2) + +14641 grid points (11^4) + → worker pool + → tasks distributed to workers as they become available (queue) + → each worker receives: { alpha, beta, delta, gamma, x0, y0, T } + → each worker returns: { alpha, beta, delta, gamma, maxPrey } + → as each task completes: progressBar += 1/totalPoints + → after all complete: find max(maxPrey) → optimal params +``` + +### 2.4. Dependencies Between Tasks + +``` +task_primary — independent +task_optimize — does not depend on task_primary results; + after completion, triggers a single run of task_primary +``` + +--- + +## 3. Controls + +### 3.1. Primary Pipeline Controls + +| ID | Name (label) | Control type | Data type | Default | Min | Max | Step | Format | Nullable | Tooltip text | Group | +|---|---|---|---|---|---|---|---|---|---|---|---| +| `ctrl_alpha` | Prey birth rate α | `ui.input.float` | `number` | `1.0` | `0.1` | `3.0` | — | `0.00` | No | Rate at which prey reproduce. Higher α → faster prey growth in the absence of predators. | Model Coefficients | +| `ctrl_beta` | Predation rate β | `ui.input.float` | `number` | `0.1` | `0.01` | `0.5` | — | `0.000` | No | Rate at which predators consume prey. Higher β → more prey eaten per encounter, reducing prey population faster. | Model Coefficients | +| `ctrl_delta` | Predator efficiency δ | `ui.input.float` | `number` | `0.075` | `0.01` | `0.5` | — | `0.000` | No | Efficiency of converting consumed prey into predator growth. Higher δ → predators grow faster from each prey consumed. | Model Coefficients | +| `ctrl_gamma` | Predator death rate γ | `ui.input.float` | `number` | `1.5` | `0.1` | `3.0` | — | `0.00` | No | Natural death rate of predators. Higher γ → predators die off faster without sufficient prey. | Model Coefficients | +| `ctrl_x0` | Initial prey x₀ | `ui.input.float` | `number` | `10` | `1` | `200` | — | `0.0` | No | Starting prey population at time t=0. | Initial Conditions | +| `ctrl_y0` | Initial predators y₀ | `ui.input.float` | `number` | `5` | `1` | `100` | — | `0.0` | No | Starting predator population at time t=0. | Initial Conditions | +| `ctrl_T` | Simulation time T | `ui.input.float` | `number` | `100` | `10` | `500` | — | `0.0` | No | Total simulation time. Longer T shows more oscillation cycles. | Initial Conditions | + +### 3.2. Secondary Task Triggers + +| ID | Name / icon | Triggers task | Tooltip text | Availability condition | +|---|---|---|---|---| +| `btn_optimize` | `Optimize Max Prey` button | `task_optimize` | Run brute-force grid search over all four model coefficients to maximize peak prey population | Always (disabled during optimization) | + +### 3.3. Other Buttons and Actions + +| ID | Name / icon | Action | Tooltip text | Availability condition | +|---|---|---|---|---| +| `btn_reset` | `ui.iconFA('undo')` | Reset all controls to default values | Reset all parameters to default values | Always | + +### 3.4. Custom UI Components + +| Component ID | Brief description | Role | +|---|---|---| +| `comp_equilibrium` | Display panel showing equilibrium (x*, y*) and summary stats | Display | +| `comp_start_marker` | Scatter marker showing the starting point on the phase portrait | Display (via viewer options) | + +--- + +## 4. Validation + +### 4.1. Primary Pipeline Validation + +| Rule ID | Condition (invalid) | Affected inputs (ID) | Error message | +|---|---|---|---| +| `val_01` | `alpha ≤ 0` | `ctrl_alpha` | "Prey birth rate must be positive" | +| `val_02` | `beta ≤ 0` | `ctrl_beta` | "Predation rate must be positive" | +| `val_03` | `delta ≤ 0` | `ctrl_delta` | "Predator efficiency must be positive" | +| `val_04` | `gamma ≤ 0` | `ctrl_gamma` | "Predator death rate must be positive" | +| `val_05` | `x0 ≤ 0` | `ctrl_x0` | "Initial prey population must be positive" | +| `val_06` | `y0 ≤ 0` | `ctrl_y0` | "Initial predator population must be positive" | +| `val_07` | `T ≤ 0` | `ctrl_T` | "Simulation time must be positive" | + +Validation order: val_01 through val_07 (all independent, checked in sequence). + +--- + +## 5. Reactivity and Input Dependencies + +### 5.1. Dependency Graph + +| Source (input ID) | Target | Reaction type | Logic | +|---|---|---|---| +| Any `ctrl_*` | `comp_equilibrium` | Display update | Recalculate equilibrium and stats | +| Any `ctrl_*` | All viewers | Reactive update | Rerun `task_primary`, update charts and table | + +### 5.2. Debounce / Throttle + +| Input ID | Strategy | Interval (ms) | +|---|---|---| +| All `ctrl_*` | debounce | 50 | + +--- + +## 6. Behavior During Computations + +### 6.1. Primary Pipeline (`task_primary`) + +- Controls blocked: No (computation < 100ms) +- Progress bar: No +- Error behavior: Clear results + `grok.shell.error(msg)` + +### 6.2. Secondary Pipeline (`task_optimize`) + +- `btn_optimize` blocked: Yes (shows progress bar percentage) +- All other controls: No +- Progress bar: Yes — determinate (0–100%), with percentage shown on button +- Cancellation support: No +- Error behavior: Keep previous results + `grok.shell.error(msg)` + +--- + +## 7. Computation Blocking and Batch Update + +| Source (task) | Target controls (ID) | Locked pipelines | +|---|---|---| +| `task_optimize` | `ctrl_alpha`, `ctrl_beta`, `ctrl_delta`, `ctrl_gamma` | Primary (blocked) | + +Reactivity mode during batch update: Primary pipeline is paused during writing, then runs once with the new values. + +--- + +## 8. Result Display + +### 8.1. Primary Pipeline Display Elements + +| ID | Type | Associated output data | Docking location | +|---|---|---|---| +| `view_timeseries` | Datagrok viewer `line chart` | `task_primary.t`, `task_primary.x`, `task_primary.y` | Center area, top | +| `view_phase` | Datagrok viewer `scatter plot` | `task_primary.x`, `task_primary.y` | Center area, bottom | +| `view_table` | `DG.TableView` grid | `task_primary.t`, `task_primary.x`, `task_primary.y` | Right area | +| `comp_equilibrium` | Custom HTMLElement | Equilibrium + stats | Left panel, below controls | + +**Details for `view_timeseries`:** + +| Property | Value | +|---|---| +| X axis | `t`, label "Time" | +| Y axis | `x(t)` and `y(t)`, two series | +| Series | Prey (x) — line, Predators (y) — line | + +**Details for `view_phase`:** + +| Property | Value | +|---|---| +| X axis | `x` (prey) | +| Y axis | `y` (predators) | +| Start point | Marked with distinct marker at (x₀, y₀) | + +--- + +## 9. Layout + +### 9.1. Control Placement + +| Area | Content | +|---|---| +| Left panel | `ui.form` with grouped controls + equilibrium/stats display | +| Center area | Time-series chart (top) + Phase portrait (bottom) | +| Right area | Data table (grid) | + +**Structure of left panel form:** + +``` +ui.h2('Model Coefficients') + ctrl_alpha + ctrl_beta + ctrl_delta + ctrl_gamma + +ui.h2('Initial Conditions') + ctrl_x0 + ctrl_y0 + ctrl_T + +btn_optimize (button with progress) + +ui.h2('Equilibrium & Stats') + comp_equilibrium +``` + +--- + +## 10. Data Lifecycle + +### 10.1. Data Input + +Primary method: manual input via `ui.form` controls. + +Initial state: all controls initialized with defaults. `task_primary` runs automatically on initialization. + +### 10.2. Results Table Lifecycle + +``` +1. Application initialization + → solve with DEFAULTS → DG.DataFrame with columns [t, x, y] + → DataFrame added to TableView + +2. task_primary completion + → DataFrame updated with new arrays + → All viewers redrawn reactively + +3. task_optimize completion + → Optimal values written to sliders (batch update) + → task_primary runs once → DataFrame updated + +4. btn_reset press + → All controls reset to defaults + → task_primary runs → DataFrame updated +``` + +--- + +## 11. Error Handling + +| Error type | Strategy | Notification method | +|---|---|---| +| ODE solver error | Clear results, show message | `grok.shell.error` | +| Worker creation error | Abort optimization | `grok.shell.error` | +| Partial worker errors | Skip failed points, use valid results | `grok.shell.warning` | +| All workers fail | Abort optimization | `grok.shell.error` | + +--- + +## 12. Testing + +### 12.1. Mathematical Verification + +#### ODE Right-Hand Side Verification + +| Test ID | Input `(α, β, δ, γ, x, y)` | Expected `(dx/dt, dy/dt)` | +|---|---|---| +| `func_01` | `(1.0, 0.1, 0.075, 1.5, 10, 5)` | `(5.0, −3.75)` | +| `func_02` | `(1.0, 0.1, 0.075, 1.5, 20, 10)` | `(0.0, 0.0)` | +| `func_03` | `(1.0, 0.1, 0.075, 1.5, 30, 4)` | `(18.0, 3.0)` | +| `func_04` | `(2.0, 0.5, 0.3, 1.0, 5, 2)` | `(5.0, 1.0)` | +| `func_05` | `(0.5, 0.02, 0.01, 0.3, 50, 10)` | `(15.0, −2.0)` | + +#### Equilibrium Verification + +| Test ID | Input `(α, β, δ, γ)` | Expected `(x*, y*)` | +|---|---|---| +| `eq_01` | `(1.0, 0.1, 0.075, 1.5)` | `(20, 10)` | +| `eq_02` | `(2.0, 0.5, 0.3, 1.0)` | `(10/3, 4)` | + +#### Output Property Verification (solve) + +| Test ID | Description | Verified property | +|---|---|---| +| `solve_01` | Default parameters | Non-empty arrays, equal length | +| `solve_02` | x,y values ≥ 0 | All `x[i] ≥ 0`, `y[i] ≥ 0` | +| `solve_03` | Initial conditions | `x[0] = x₀`, `y[0] = y₀` | +| `solve_04` | Equilibrium: dx/dt ≈ 0, dy/dt ≈ 0 at (x*, y*) | `dx/dt < ε`, `dy/dt < ε` | + +### 12.2. Validation Tests + +| Test ID | Rule | Input data | Expected result | +|---|---|---|---| +| `v_01` | val_01 | `alpha=0` | Error on `ctrl_alpha` | +| `v_02` | val_02 | `beta=-0.1` | Error on `ctrl_beta` | +| `v_03` | val_03 | `delta=0` | Error on `ctrl_delta` | +| `v_04` | val_04 | `gamma=-1` | Error on `ctrl_gamma` | +| `v_05` | val_05 | `x0=0` | Error on `ctrl_x0` | +| `v_06` | val_06 | `y0=-5` | Error on `ctrl_y0` | +| `v_07` | val_07 | `T=0` | Error on `ctrl_T` | +| `v_def` | all defaults | DEFAULTS | `errors.size = 0` | +| `v_multi` | multiple | `alpha=0, beta=0, x0=0, T=0` | ≥ 4 errors | diff --git a/packages/LotkaVolterraGuided/css/lotka-volterra.css b/packages/LotkaVolterraGuided/css/lotka-volterra.css new file mode 100644 index 0000000000..8b12e075bb --- /dev/null +++ b/packages/LotkaVolterraGuided/css/lotka-volterra.css @@ -0,0 +1,33 @@ +/* Lotka-Volterra Predator-Prey Simulation — Application Styles */ + +/* Equilibrium and stats display panel */ +.lv-stats-panel { + font-size: 13px; + padding: 8px; + border-radius: 4px; + background-color: #f5f5f5; + margin-top: 8px; + line-height: 1.6; +} + +.lv-stats-panel .lv-stats-label { + font-weight: 600; + color: #555; +} + +.lv-stats-panel .lv-stats-value { + font-family: monospace; + color: #333; +} + +.lv-optimize-btn--disabled { + pointer-events: none; + opacity: 0.7; + cursor: not-allowed; +} + +/* Disabled icon button (ui.iconFA) */ +.lv-btn--disabled { + pointer-events: none; + opacity: 0.4; +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/datagrok-app-specification-template.md b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/datagrok-app-specification-template.md new file mode 100644 index 0000000000..66aeb36523 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/datagrok-app-specification-template.md @@ -0,0 +1,551 @@ +# Datagrok Interactive Scientific Application Specification Template + +> This template follows the structure of `datagrok-interactive-app-guide.md`. +> Section numbers here match the guide's section numbers. +> Fill in every section. Mark sections that do not apply as "N/A" with a brief explanation. + +## 1. General Architecture + +### 1.0. General Information + +| Field | Value | +|---|---| +| Application name | | +| Package | | +| Entry function | | +| Brief description | What the application does, what scientific problem it solves | +| Main view type | `DG.TableView` / other | + +### 1.1. Core + +The core contains one or more computation tasks. Each task is described separately. + +#### Task List + +| Task ID | Name | Pipeline type | Trigger | Synchronicity | Execution environment | Parallelization | +|---|---|---|---|---|---|---| +| task_primary | ... | Primary (reactive) | Input change | Sync / async | Main thread / web worker | No / yes — strategy | +| task_secondary_1 | ... | Secondary (on demand) | Button / icon / menu item | ... | ... | ... | + +#### Dependencies Between Tasks + +``` +Example: + task_primary → result is used by task_secondary_1 + task_secondary_2 is independent of task_primary +``` + +#### Description of Each Task + +A separate block is filled in for each task. + +--- + +##### Task: `task_id` + +**General characteristics:** + +| Field | Value | +|---|---| +| Name | | +| Description | What the task computes | +| Dependency on other tasks | No / uses results of task `task_id` | + +**Computation Formulas and Model** + +> See guide section 1.1 "Computation Formulas and Model" for the two-level definition. + +**Level 1 — required minimum (before implementation):** + +Variables: + +| Variable | Meaning | Units | Domain | +|---|---|---|---| +| ... | ... | ... | e.g., `p ∈ (0, 1]` | + +Relationships (equations, recurrences, algorithmic steps connecting inputs to outputs): + +``` +... +``` + +Output properties (invariants that must hold on the result): + +| Property | Description | +|---|---| +| ... | e.g., bounds, monotonicity, conservation laws, limiting cases | + +Reference examples (at least one per computational path / mode / branch): + +| # | Inputs | Expected output | Computational path | Source | +|---|---|---|---|---| +| 1 | ... | ... | e.g., base model | Manual calculation / literature / reference implementation | + +**Level 2 — full formalization (can be developed incrementally):** + +- Complete mathematical formulation: ___(equations, initial/boundary conditions, parameterization)___ +- Analytical properties: ___(equilibria, asymptotic behavior, stability, bifurcation points)___ +- Numerical method justification: ___(why this method, stability, order of accuracy, applicability, literature reference)___ + +Level 2 document location: in this specification / separate document: ___ + +> Level 2 need not be complete before implementation begins, but must be complete before the computational part is considered verified. + +**Task input parameters:** + +| Parameter | Type | Units | Domain | Description | +|---|---|---|---|---| +| ... | ... | ... | ... | ... | + +**Task output data:** + +| Parameter | Type | Description | +|---|---|---| +| ... | ... | ... | + +**Computation implementation:** + +For each computation step: + +| Step | Implementation method | Details | Documentation | +|---|---|---|---| +| 1 | Datagrok API | Method: ... (main thread only) | [Datagrok JS API](https://datagrok.ai/api/js/) | +| 2 | External library | Library: ..., version: ..., functions: ... | Link to API reference / README (required) | +| 3 | Custom method | Brief description: ... | Link to method specification (separate document, required) | + +For **external libraries** implementing numerical methods: state which method properties are relevant (stability, order, applicability class), expected accuracy, and the verification strategy (reference problems, comparison with alternatives). See section 15.3. + +For **custom methods**: the method specification (separate document) must contain: mathematical formulation, step-by-step algorithm, input/output data, constraints, edge cases, literature references, expected accuracy, and the verification strategy (reference examples with sources). See section 15.3. + +**Execution environment constraint:** if the task uses Datagrok API — main thread only. + +--- + +### 1.2. Ports + +For each task, define input/output ports. Additionally, define application-level ports. + +#### Task ports: `task_id` + +| Port | Type | Interface / format | Description | +|---|---|---|---| +| Input | ... | Interface name / type | What parameters the task expects | +| Output | ... | Interface name / type | What the task returns | + +#### Application-level ports + +| Port | Used | Description | +|---|---|---| +| Progress | Yes / No | Interface for reporting execution progress (percentage, stage) | +| Cancellation | Yes / No | Interface for checking cancellation requests | +| Data | Yes / No | Interface for loading data from external resources | + +### 1.3. Adapters + +| Adapter | Used | Implementation | +|---|---|---| +| UI adapter | Yes / No | Datagrok inputs (`ui.input.*`), buttons, custom HTMLElement | +| Display adapter | Yes / No | Datagrok viewers, custom HTMLElement, docking | +| Worker adapter | Yes / No | Web worker wrapper for core tasks | +| Progress adapter | Yes / No | Datagrok progress bar | +| Data adapter | Yes / No | Loading from a specific resource | + +For custom HTMLElements used as adapters, specify the component ID from section 3 (Custom UI Components). + +### 1.4. Coordinator + +The coordinator connects adapters and the core. Describe: + +- How input changes are listened to (UI adapter). +- Reactivity management strategy (cascading dependencies, see section 9). +- How validation and computations are triggered. +- How control state is managed during computations. +- How computation blocking works (see section 8.4). +- How results are passed to the display adapter. +- Resource lifecycle management (subscriptions, workers — see section 12). + +### 1.5. Independence Principle + +Confirm that input behavior does not depend on the core's computational part. The UI adapter and reactivity form a standalone layer. The core receives a ready, validated set of parameters. + +## 2. Main View + +| Field | Value | +|---|---| +| View type | `DG.TableView` / custom | +| Description | ... | + +## 3. Controls (Inputs) + +### 3.1. Primary Pipeline Controls + +| ID | Label | Control type | Data type | Default | Min | Max | Format | Nullable | Tooltip text | Group | +|---|---|---|---|---|---|---|---|---|---|---| +| ... | ... | `ui.input.int` / ... | `number` / ... | ... | ... | ... | e.g., `0.000` | Yes / No | ... | ... | + +### 3.2. Secondary Task Triggers + +Buttons, icons, menu items that launch secondary pipelines: + +| ID | Label / icon | Launches task | Tooltip text | Availability condition | +|---|---|---|---|---| +| ... | ... | `task_id` | ... | ... | + +### 3.3. Secondary Task Controls + +For each secondary task that has its own UI: + +#### Task controls: `task_id` + +UI type: Datagrok dialog / other. + +| ID | Label | Control type | Data type | Default | Min | Max | Format | Nullable | Tooltip text | +|---|---|---|---|---|---|---|---|---|---| +| ... | ... | ... | ... | ... | ... | ... | ... | ... | ... | + +### 3.4. Other Buttons and Actions + +Buttons that are not secondary task triggers (data loading, export, etc.): + +| ID | Label / icon | Action | Tooltip text | Availability condition | +|---|---|---|---|---| +| ... | ... | ... | ... | ... | + +### 3.5. Custom UI Components + +If the application uses custom HTMLElements (controls or display elements), each is described in a separate document — a UI component specification. + +| Component ID | Brief description | Role (control / display / trigger) | UI component specification | +|---|---|---|---| +| ... | ... | ... | Link to separate document (required) | + +**UI component specification** (separate document) must contain: visual description (sketch/mockup), states (normal, hover, disabled, active), events (what it emits on interaction), styles (CSS classes), accessibility (tooltips, aria). + +## 4. Result Display Elements + +### 4.1. Primary Pipeline Display Elements + +| ID | Type | Associated output data (task.parameter) | Docking location | +|---|---|---|---| +| ... | Datagrok viewer (scatter plot / line chart / grid / ...) / custom HTMLElement | ... | ... | + +For custom HTMLElements, specify the component ID from section 3.5. + +### 4.2. Secondary Task Display Elements + +For each secondary task: + +#### Task display: `task_id` + +Where results are displayed: additional viewers in main view / dialog content / separate window. + +| ID | Type | Associated output data | Placement | +|---|---|---|---| +| ... | ... | ... | ... | + +## 5. Layout and UI Element Placement + +### 5.1. Control Placement + +| Area | Content (control IDs) | +|---|---| +| Left panel | ... | +| Right panel | ... | +| Top panel / Ribbon | ... | +| Toolbar | ... | +| Main area | ... | + +### 5.2. Display Element Placement + +| Element ID | Docking area | Position / ratio | +|---|---|---| +| ... | ... | ... | + +### 5.3. Styles + +CSS file: `css/.css` + +**Static styles** (do not change during operation): + +| Element / class | CSS class(es) | Description | +|---|---|---| +| ... | ... | ... | + +**Dynamic styles** (depend on application state — implemented via `classList.toggle/add/remove`): + +| Element / class | CSS class(es) | Condition | Description | +|---|---|---|---| +| ... | ... | ... | ... | + +CSS import: `import '../css/.css'` in the main application file. + +## 6. User Feedback + +### 6.1. Control Tooltips + +| Control ID | Tooltip text | Mechanism | +|---|---|---| +| ... | ... | `tooltipText` property / `ui.tooltip.bind` / `ui.iconFA` third argument | + +### 6.2. Validators as Feedback + +Validators are added to standard Datagrok inputs — they display inline hints about the validity of the current value. + +| Input ID | Validation source | Description | +|---|---|---| +| ... | `validate()` from core / custom | ... | + +### 6.3. Progress Bar + +| Task | Progress bar | Type | Cancellation support | +|---|---|---|---| +| ... | Yes / No | Determinate / indeterminate | Yes / No | + +## 7. Validation + +### 7.1. Primary Pipeline Validation + +#### Complex Validation Rules + +| Rule ID | Condition (invalid combination) | Affected inputs (ID) | Error message | +|---|---|---|---| +| ... | ... | ... | ... | + +#### Validation Order + +``` +1. Rule_A +2. Rule_B (checked only if Rule_A passed) +3. Rule_C +``` + +#### Returned Map Format + +``` +Map +``` + +### 7.2. Secondary Task Validation + +For each secondary task — a similar block. + +#### Task validation: `task_id` + +| Rule ID | Condition | Affected inputs (ID) | Error message | +|---|---|---|---| +| ... | ... | ... | ... | + +Validation order: + +``` +1. ... +``` + +Return format: `{ errors: Map, warning: string | null }` + +## 8. Main Pipeline + +### 8.1. Primary Pipeline + +| Step | Description | +|---|---| +| Parameter input | User sets values through controls → UI adapter converts to typed data | +| Validation | Input set validated by core (section 7) | +| Computation | Validated inputs passed to core task | +| Result display | Results passed to display adapter (section 4) | + +Reactive trigger: ___(e.g., `onValueChanged` with debounce ___ ms)___ + +Error behavior on validation failure: ___(clear results / keep previous / show message)___ + +### 8.2. Secondary Pipelines + +For each secondary task: + +#### Task pipeline: `task_id` + +| Step | Description | +|---|---| +| Trigger | Button / icon / menu item: `control_id` | +| Custom UI | Dialog / panel with own inputs (section 3.3) | +| Validation | Independent validation rules (section 7.2) | +| Computation | Core task execution | +| Result display | Where and how results are shown (section 4.2) | +| Feedback to primary | Values substituted into primary controls: ___ / No | + +### 8.3. Common Pipeline Aspects + +Defined for each pipeline independently: + +#### Control Behavior During Computations + +| Pipeline / task | Controls blocked | Which controls | +|---|---|---| +| task_primary | Yes / No | ... | +| task_secondary_1 | Yes / No | ... | + +#### Computation Error Handling + +| Pipeline / task | Strategy | Notification method | +|---|---|---| +| task_primary | Reset results / keep previous / message | `grok.shell.error` / inline / ... | +| task_secondary_1 | ... | ... | + +### 8.4. Computation Blocking and Batch Input Updates + +| Scenario | Source (task) | Target controls (ID) | Blocked pipelines | +|---|---|---|---| +| ... | `task_id` | ... | Primary / ... | + +Reactivity mode during batch update: + +| Scenario | Reactivity mode | +|---|---| +| ... | Active but computations not triggered / Fully suspended | + +## 9. Reactivity and Dependencies Between Inputs + +### 9.1. Dependency Graph + +| Source (input ID) | Target (input IDs) | Reaction type | Logic | +|---|---|---|---| +| ... | ... | Range / default / availability / option list / label update | ... | + +### 9.2. Debounce / Throttle + +| Input ID | Strategy | Interval (ms) | +|---|---|---| +| ... | debounce / throttle / none | ... | + +## 10. Data Lifecycle + +### 10.1. Data Input + +Primary method: manual input via controls (section 3). + +### 10.2. Loading from Resources + +| Trigger (button ID) | Resource | Format | Mapping to inputs (ID) | +|---|---|---|---| +| ... | File / URL / DB / API | ... | ... | + +## 11. Error Handling Beyond Computations + +| Error type | Strategy | Notification method | +|---|---|---| +| Data loading error | ... | `grok.shell.warning` / `grok.shell.error` / inline | +| Network error | ... | ... | +| Invalid input file | ... | ... | +| Worker creation error | ... | ... | +| Partial worker errors | ... | ... | + +## 12. Subscriptions and Resource Management + +### 12.1. Event Subscriptions + +| Subscription | Event | Cleanup mechanism | +|---|---|---| +| ... | `onValueChanged` / `onAfterDraw` / ... | `sub.unsubscribe()` in cleanup handler | + +All subscriptions must be collected and unsubscribed when the application closes. + +### 12.2. Worker Termination + +| Worker pool | Created in | Termination mechanism | +|---|---|---| +| ... | task / function name | `w.terminate()` in cleanup handler | + +## 13. Application Closure + +On view close, the coordinator performs: + +- [ ] All event subscriptions unsubscribed (section 12.1) +- [ ] All web workers terminated (section 12.2) +- [ ] All associated UI elements closed (including open secondary task dialogs) +- [ ] Pending requests cancelled (debounce timers, in-flight operations) + +Closure handler: ___(e.g., `grok.events.onViewRemoved.subscribe(...)`)___ + +## 14. Accessibility and UX + +### 14.1. Keyboard Shortcuts + +| Combination | Action | +|---|---| +| ... | ... | + +### 14.2. Context Menus + +| Context (element) | Menu items | +|---|---| +| ... | ... | + +### 14.3. Undo / Redo + +Supported: Yes / No. + +If yes — which actions support rollback: ___ + +## 15. Testing + +### 15.1. Computational Part (Core) + +Core correctness verification: unit tests for each computational task separately. The core is tested in isolation — without UI and adapters. + +Test files: + +| File | Categories | Test count | Description | +|---|---|---|---| +| ... | ... | ... | ... | + +Tests are run via `grok test` (entry point: `src/package-test.ts`). + +### 15.2. Inputs + +Input verification for each task: all cases including edge cases. + +| Category | Coverage | Description | +|---|---|---| +| Boundary values | ... | e.g., lower/upper allowed bounds | +| Invalid combinations | ... | e.g., cross-parameter constraints | +| Dependencies between rules | ... | e.g., rule B skipped when rule A fails | +| Multiple simultaneous errors | ... | ... | + +### 15.3. Mathematical Verification + +> Verification criteria are defined by the model specification (section 1.1), not invented during test writing. +> Tests implement what is specified; the specification is the source of truth. + +#### Level 1 Verification (required) + +**Formula/equation verification:** + +| Test category | Test count | What is verified | Reference source | +|---|---|---|---| +| ... | ... | Concrete inputs → expected output for each computational path | Manual calculation / literature | + +**Output property verification:** + +| Test category | Test count | Properties verified | +|---|---|---| +| ... | ... | e.g., bounds, initial conditions, convergence, monotonicity | + +#### Level 2 Verification (for full formalization) + +**Numerical method verification:** + +| Test category | Test count | Reference problems | Tolerance | Source | +|---|---|---|---|---| +| ... | ... | e.g., non-stiff 1D, stiff 1D, stiff 2D | ... | Textbook / paper / test suite | + +**Convergence verification:** + +| Status | Description | +|---|---| +| Implemented / Not yet covered | e.g., solve with two tolerance levels, verify discrepancy decreases | + +**Asymptotic/equilibrium behavior:** + +| Status | Description | +|---|---| +| Implemented / Not yet covered | e.g., p(t_end) → p* for large t_end | diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/datagrok-interactive-app-guide.md b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/datagrok-interactive-app-guide.md new file mode 100644 index 0000000000..85faf262be --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/datagrok-interactive-app-guide.md @@ -0,0 +1,511 @@ +# Guide: Building Interactive Scientific Web Applications on Datagrok + +> **End-to-end example:** application **Levins Metapopulation Model** (see `example/` directory). +> All file references are relative to the `guides/` directory. +> Application specification: `example/levins-metapopulation-spec.md`. + +## 1. General Architecture + +An application is a Datagrok package function registered with the `//tags: app` comment. + +► **Implementation:** `example/code/src/package.ts` — function `levinsMetapopulationModelApp`, registered via `//name: Levins Metapopulation Model` / `//tags: app`. Delegates logic to `example/code/src/levins/app.ts` → `levinsMetapopulationApp()`. + +The application architecture follows the "ports and adapters" pattern (hexagonal architecture). The application consists of three layers and a coordinator. + +### 1.1. Core + +Computation logic. The core knows nothing about the UI and does not depend on how data is obtained or how results are displayed. + +The core contains one or more computational tasks. Each task is a self-contained unit of computation with its own inputs and outputs. For each task, the following are defined independently: + +- Input parameters and their types. +- Output data and their format. +- Synchronicity: synchronous or asynchronous. +- Execution environment: main thread or web worker. +- Parallelization: whether parallelization is possible and the strategy. +- Complex validation logic for input parameters. + +► **Implementation:** `example/code/src/levins/core.ts` — core with two tasks: +- **`task_primary`** (synchronous, main thread) — `solve(inputs)` solves the Levins model ODE and returns the trajectory `p(t)`. +- **`task_optimize`** (asynchronous, web workers) — searches for the optimal `m` across 10,000 points, executed in parallel in a worker pool. + +Examples of computational tasks within a single application: + +- **Primary task** — solving the ODE based on model parameters. Triggered reactively when inputs change. +- **Secondary task** — model sensitivity analysis. Triggered by an explicit user action (button), has its own set of parameters. + +Tasks can be independent or linked (the secondary task uses results from the primary one). + +► **Implementation:** `task_optimize` does not depend on the results of `task_primary`, but uses the current input values of the primary pipeline (snapshot). After optimization completes, the result is written to the `ctrl_m` control via batch update, which triggers a single run of `task_primary`. + +#### Computation Formulas and Model + +Each computational task is based on a defined transformation of inputs into outputs — from a single formula to a complex system of equations. In this guide, the term "model" refers to any such definition: individual formulas, chains of transformations, ODE/PDE systems, optimization problems, or statistical procedures. The model exists independently of its implementation (custom code, external library, or platform API). The model definition is the primary source of verification criteria: what cannot be defined cannot be verified. + +The model definition has two levels. + +**Level 1 — required minimum (before implementation):** + +- **Variables:** name, meaning, units of measurement, valid domain (e.g., `p ∈ (0, 1]`, `m > 0`). +- **Relationships:** equations, recurrences, algorithmic steps that connect inputs to outputs. The notation must be unambiguous — another developer must be able to independently implement the same computation from this description. +- **Output properties:** constraints that must hold on the result — bounds, monotonicity, symmetry, conservation laws, limiting/degenerate cases. These properties directly become verification tests. +- **Reference examples:** for each computational path (each mode, branch, or regime of the model), at least one concrete input → expected output pair with the source (manual calculation, literature, reference implementation). + +**Level 2 — full formalization (can be developed incrementally alongside the implementation):** + +- **Complete mathematical formulation:** equations, initial/boundary conditions, parameterization. For ODE/PDE — the system in explicit form. +- **Analytical properties:** equilibria, asymptotic behavior, stability conditions, bifurcation points. +- **Numerical method justification:** why this particular method is chosen, its properties (stability, order of accuracy, applicability to stiff/non-stiff problems), reference to literature or documentation. +Both Level 1 and Level 2 content can be placed directly in the main application specification or extracted into a separate model specification document — depending on complexity. For simple models (a few formulas), the application specification is sufficient. For complex models (multi-step pipelines, multiple computational paths, extensive reference data), a separate document avoids cluttering the main specification. The main application specification then includes a brief model description and a link to the model specification. + +Level 2 need not be complete before implementation begins, but must be complete before the computational part is considered verified. + +► **Implementation:** +- **Level 1** is defined in the application specification (`example/levins-metapopulation-spec.md`): variables `p, m, e₀` with units and domains; ODE `dp/dt = m·p·(1−p) − e(p)·p` with two modes (`e = e₀` and `e = e₀·(1−p)`); output property `p(t) ∈ [0, 1]`; equilibrium `p* = 1 − e₀/m`; reference examples for each mode verified in tests (`Math: Levins func` — 5 tests covering base model and rescue effect at specific `p` values). +- **Level 2:** equilibrium analysis, MRT method justification (A-stable implicit method suitable for stiff ODEs), convergence properties — documented in the specification. MRT solver verified against analytical solutions in `Math: MRT solver` tests (non-stiff and stiff reference problems from Chapra & Canale). + +#### Computation Implementation + +Computations for each task are implemented using one or a combination of the following approaches: + +- **Datagrok API computation methods.** The core can use computation methods provided by the Datagrok API. Available only on the main thread — the Datagrok API is not available in web workers. + +- **External libraries.** Computations are performed using third-party libraries. At the specification stage, the following is determined: which specific libraries are used, which versions, which functions/methods are applied, and a link to the library documentation (API reference, README, or guide). A documentation link is mandatory — without it, it is impossible to correctly implement and verify the calls. The order of library usage (which calls, in what sequence, with what parameters) is either defined in the specification or deferred to a separate agreement — if the usage approach is non-trivial or allows for variations. Additionally, for libraries that implement numerical methods (solvers, optimizers, fitting): the specification states which properties of the method are relevant (stability, order of accuracy, applicability class), expected accuracy for the application's use case, and the verification strategy — how the library's results will be validated (reference problems with known solutions, comparison with an alternative implementation, etc.). See section 15.3. + +- **Custom methods.** Computations are implemented within the application. Each custom method is described in a separate document — a method specification. The method specification contains: mathematical formulation (formulas, equations), step-by-step algorithm, input and output data, constraints and assumptions, edge cases, and references to literature. The main application specification includes a brief method description and a link to the method specification document. Additionally, the method specification defines expected accuracy and the verification strategy: reference examples with expected outputs and their sources (manual calculation, literature, reference implementation). See section 15.3. + +A single task can combine multiple approaches — for example, a custom method for data preprocessing, an external library for numerical solution, and a Datagrok computation method for postprocessing. + +► **Implementation:** +- `task_primary` combines an **external library** (`diff-grok`, function `mrt`) and a **custom method** (`getEquilibrium` in `example/code/src/levins/model.ts`). +- `task_optimize` uses an **external library** (`diff-grok`, `mrt`) inside workers (`example/code/src/levins/optimize-worker.ts`) and a **custom method** (finding the maximum `p_end` in `example/code/src/levins/app.ts`, lines 421–425). +- The `mrt` call is wrapped in `createLevinsODE()` (`example/code/src/levins/model.ts`), which allows reusing the ODE specification in both the main thread and workers. + +Execution environment constraint: tasks that use Datagrok API computation methods can only run on the main thread. Tasks that use only external libraries and custom methods can run on either the main thread or in a web worker. + +► **Implementation:** `task_primary` runs on the main thread (synchronous, < 100 ms). `task_optimize` is distributed across workers — `diff-grok` does not depend on the Datagrok API. + +General core properties: + +- Does not depend on how data is obtained or how results are displayed. +- Easily testable in isolation — each task is tested separately. + +► **Implementation:** `example/code/src/levins/core.ts` does not import `datagrok-api` or `ui` — only `diff-grok` and `./model`. Core tests in `example/code/src/tests/levins-api-tests.ts` test `validate`, `solve`, `validateOptimize`, `getEquilibrium` without UI. + +Computation core implementation references: patterns for working with raw data and null handling (`reference/COMPUTATION-PATTERNS.md`), efficient typed array operations (`reference/ARRAY-OPERATIONS.md`). + +### 1.2. Ports + +Interfaces through which the core communicates with the outside world. Ports contain no implementation — only contracts. Each computational task of the core has its own set of ports: + +- **Input port** — describes what parameters and types the task expects. +- **Output port** — describes the format of the task's results. +- **Progress port** — interface for reporting execution progress (percentage, stage). +- **Cancellation port** — interface for checking whether the user has requested cancellation. + +Additionally, at the application level: + +- **Data port** — interface for loading data from external resources. + +► **Implementation:** +- **Input port `task_primary`:** interface `LevinsParams` (`example/code/src/levins/model.ts`, line 6) — `{ p0, m, e0, rescueEffect, t_start, t_end, t_step, tolerance }`. +- **Output port `task_primary`:** interface `LevinsSolution` (`example/code/src/levins/core.ts`, line 14) — `{ t: Float64Array, p: Float64Array, p_star: number }`. +- **Input port `task_optimize`:** interface `WorkerTask` (`example/code/src/levins/core.ts`, line 129) — data passed to the worker via `postMessage`. +- **Output port `task_optimize`:** interface `WorkerResult` (`example/code/src/levins/core.ts`, line 140) — `{ m_i, p_end, error? }`. +- **Progress port:** in `task_optimize` implemented via `DG.TaskBarProgressIndicator` (`example/code/src/levins/app.ts`, line 302). Progress is updated upon each completed worker. +- **Cancellation port:** `canceled` flag (`example/code/src/levins/app.ts`, line 303), checked before sending the next task to a worker. +- **Data port:** not used (the application does not load external data). + +### 1.3. Adapters + +Concrete implementations of ports for the Datagrok environment. + +- **UI adapter** — Datagrok inputs (`ui.input.*`), buttons, custom `HTMLElement`. Converts user input into typed data for the task's input port. +- **Display adapter** — Datagrok viewers, custom `HTMLElement`, docking to the main view. Receives data from the task's output port and visualizes it. +- **Worker adapter** — wrapper for running the core in a web worker. Implements input and output ports via `postMessage`. Implementation references: worker-utils infrastructure and lifecycle (`reference/WORKER-GUIDE.md`), distribution across multiple workers (`reference/PARALLEL-EXECUTION.md`). +- **Progress adapter** — Datagrok progress bar. Implements the progress port. +- **Data adapter** — loading from a specific resource. Which resource and which mechanism — defined by the specification. + +► **Implementation in `example/code/src/levins/app.ts`:** +- **UI adapter:** controls `ctrlP0`, `ctrlM`, `ctrlE0`, `ctrlRescue`, `ctrlTStart`, `ctrlTEnd`, `ctrlTStep`, `ctrlTolerance` (lines 43–99). Function `getInputs()` (line 154) converts control state to `LevinsParams`. +- **Display adapter:** `updateDataFrame()` (line 265) updates the `DG.DataFrame` and the `line chart` viewer; `updateColorCoding()` (line 188) sets conditional color coding for the `p` column. +- **Worker adapter:** `example/code/src/levins/optimize-worker.ts` — the worker receives `WorkerTask` via `onmessage`, calls `mrt(createLevinsODE(...))`, returns `WorkerResult`. The worker pool is created in `runOptimization()` (line 292). +- **Progress adapter:** `DG.TaskBarProgressIndicator.create('Optimizing m...')` (line 302), updated via `pi.update(...)` (line 365). +- **Data adapter:** not used. + +### 1.4. Coordinator (Application Service) + +The coordinator connects adapters and the core. It: + +- Listens for input changes via the UI adapter. +- Manages reactivity: cascading dependencies between inputs, range updates, defaults, availability — based on the specification. +- Triggers validation and computations through the corresponding ports. +- Manages control state (enabled/disabled) during computations. +- Manages computation blocking: can suspend reactive pipeline execution during batch input updates (see section 8.4). +- Passes results to the display adapter. +- Manages resource lifecycle (subscriptions, workers). + +► **Implementation:** function `levinsMetapopulationApp()` in `example/code/src/levins/app.ts` fulfills all coordinator roles: +- Listens to `onValueChanged` via input callbacks (lines 43–99). +- Reactivity: `updateRhoBadge()` (line 124), `updateRescueLabel()` (line 136). +- Validation and computation: `runPrimary()` (line 226). +- Blocking: `computationsBlocked` flag (line 19), used during input formatting (lines 102–110), during reset (lines 506–518), and after optimization (lines 428–437). +- Lifecycle: `subs[]` (line 21), `activeWorkers[]` (line 22), cleanup in `onViewRemoved` (line 561). + +### 1.5. Independence Principle + +Input behavior does not depend on the core's computational part. The UI adapter and reactivity between inputs form a standalone layer managed by the coordinator based on the specification. The core receives a ready, validated set of parameters. + +► **Implementation:** the function `runPrimary()` first calls `validate(inputs)` from the core, and only if `errors.size === 0` passes the data to `solve(inputs)`. The core (`core.ts`) is unaware of the inputs' existence — it accepts a plain `LevinsParams`. + +## 2. Main View + +The application has a main view. If a scientific application is based on a table, the main view should be `DG.TableView`. + +► **Implementation:** `example/code/src/levins/app.ts`, line 33: `const view = grok.shell.addTableView(df)`. + +## 3. Controls (Inputs) + +Users set input parameters for computations through controls. Control types: + +- **Standard Datagrok inputs** — created via `ui.input.*`. +- **Datagrok buttons** — perform a specified action when clicked. +- **Custom HTMLElement** — for example, a `div` element with specific styles that performs an action when clicked. Each custom element is described in a separate document — a UI component specification. The UI component specification contains: visual description (sketch/mockup), states (normal, hover, disabled, active), events (what it emits on interaction), styles (CSS classes), accessibility (tooltips, aria). The main application specification includes a brief element description and a link to the UI component specification document. + +► **Implementation:** +- **Standard inputs:** 7 numeric + 1 toggle in `example/code/src/levins/app.ts`, lines 43–99 (`ctrlP0`, `ctrlM`, `ctrlE0`, `ctrlRescue`, `ctrlTStart`, `ctrlTEnd`, `ctrlTStep`, `ctrlTolerance`). +- **Buttons:** `optimizeBtn = ui.iconFA('search', ...)` (line 503), `resetBtn = ui.iconFA('undo', ...)` (line 505). +- **Custom HTMLElement:** `rhoBadge = ui.div([], 'd4-tag levins-rho-badge')` (line 37) — a rho = e0/m indicator with dynamic color switching via CSS classes. + +### 3.1. Input Options + +When creating standard Datagrok inputs (`ui.input.*`), the specification defines options for each input. These options are passed during input creation and control its behavior: + +- **Min** — minimum allowed value. For numeric inputs, sets the lower bound. +- **Max** — maximum allowed value. For numeric inputs, sets the upper bound. +- **Format** — value display format (e.g., `0.000` for three decimal places, `0.0` for one, `0.##E+0` for scientific notation). The format determines how the value is displayed in the input. + +Input options differ from validation (section 7): options set basic control-level constraints (range, format), while validation checks complex conditions across combinations of multiple input values. + +► **Implementation:** min/max are set during input creation (e.g., `ctrlP0`: `min: 0.001, max: 1`). Formats are set in a separate block (lines 102–109), wrapped in `computationsBlocked = true/false` — so that format assignment does not trigger a side-effect recomputation. + +### 3.2. Control Classification + +Controls are classified by ownership: + +- **Main view controls** — inputs of the primary pipeline, placed in the main application interface. +- **Secondary task triggers** — buttons or icons that launch secondary pipelines (see section 8.2). +- **Secondary task controls** — inputs placed in the secondary task's own UI (e.g., in a dialog). + +► **Implementation:** +- **Main view controls:** `ctrlP0`…`ctrlTolerance` — in the left panel form. +- **Secondary task trigger:** `optimizeBtn` (`ui.iconFA('search')`, line 503) → opens the optimization dialog. +- **Secondary task controls:** `dlgMMin`, `dlgMMax` — dialog inputs in `showOptimizeDialog()` (lines 448–499). + +## 4. Result Display Elements + +Computation results are displayed using: + +- **Standard Datagrok viewers.** +- **Custom HTMLElement.** Each custom display element is described in a separate UI component specification (similar to custom controls, see section 3). + +By default, these elements are docked to the main view. + +► **Implementation:** +- **Viewer:** `line chart` — `view.addViewer('Line chart', {...})` (line 546), docked to the right of the grid. +- **Custom element:** `rhoBadge` — displays the current rho value with color indication (green/red). +- **Color coding of column `p`:** `updateColorCoding()` (line 188) sets conditional colors (green — persistence zone, red — extinction threat) with dynamic threshold recalculation `e0/m`. +- **Column `p` header tooltip:** `setupGridTooltip()` (line 202) via `view.grid.onCellTooltip`. + +## 5. Layout and UI Element Placement + +The placement of controls and display elements (panels, ribbon, toolbar, side panels, docking area) is defined by the application specification. + +► **Implementation (`example/code/src/levins/app.ts`):** +- **Left panel:** `ui.form` with groups via `ui.h2` (lines 523–540), docked as `DG.DOCK_TYPE.LEFT`, ratio `0.2` (line 543). +- **Toolbar (ribbon):** `view.setRibbonPanels([[optimizeBtn, resetBtn]])` (line 520). +- **Main area:** `DG.TableView` (grid) — default. +- **Right area:** `line chart`, docked `DG.DOCK_TYPE.RIGHT` relative to the grid, ratio `0.5` (lines 552–554). + +## 5.1. Styles + +All visual styles of the application are placed in a separate CSS file (`css/.css`). Inline styles in TypeScript code are not allowed — CSS classes are used instead. + +- **Static styles** — element styling that does not change during operation. Set via CSS class when creating the element. +- **Dynamic styles** — styles that depend on application state (e.g., indicator color, button activity). Implemented via CSS class toggling (`classList.toggle`, `classList.add/remove`), not via direct `element.style.*` assignment. + +The CSS file is imported via ES import (`import '../css/.css'`). Webpack with `style-loader` + `css-loader` injects styles into the DOM when the bundle loads. + +► **Implementation:** +- CSS file: `example/code/css/levins.css` — contains classes `.levins-rho-badge`, `.levins-rho-badge--persists`, `.levins-rho-badge--extinct`, `.levins-btn--disabled`. +- Import: `import '../../css/levins.css'` (`example/code/src/levins/app.ts`, line 12). +- **Static styles:** `rhoBadge` is created with classes `'d4-tag levins-rho-badge'` (line 37). +- **Dynamic styles:** `rhoBadge.classList.toggle('levins-rho-badge--persists', persists)` (line 130); `optimizeBtn.classList.toggle('levins-btn--disabled', !enabled)` (line 289). + +## 6. User Feedback + +### 6.1. Control Tooltips + +- For standard Datagrok inputs, tooltips are defined at input creation time via the `tooltipText` property. +- For elements that are not Datagrok inputs, tooltips are bound via `ui.tooltip.bind`. + +► **Implementation:** +- All inputs have `tooltipText` (e.g., `ctrlP0`: `tooltipText: 'Fraction of patches occupied at t=0...'`, line 46). +- `rhoBadge`: `ui.tooltip.bind(rhoBadge, '...')` (line 38). +- Buttons `optimizeBtn` and `resetBtn`: tooltip is passed as the third argument of `ui.iconFA` (lines 503, 518). + +### 6.2. Validators as Feedback + +Validators are added to standard Datagrok inputs — they display inline hints about the validity of the current value. + +► **Implementation:** function `addValidators()` (`example/code/src/levins/app.ts`, line 168) adds a validator to each input. The validator calls `validate(getInputs())` from the core and returns an error for the specific `InputId`. + +### 6.3. Progress Bar + +During long computations, the standard Datagrok progress bar indicator is displayed with cancellation support. + +► **Implementation:** `task_optimize` uses `DG.TaskBarProgressIndicator.create('Optimizing m...')` (line 302), updated via `pi.update(percent, label)` upon each completed worker (line 365). + +## 7. Validation + +Validation is performed through the Datagrok validator mechanism. Each computational task has its own validation rules. + +### 7.1. Complex Validation + +Validation is complex: the entire set of task inputs is analyzed as a whole. If a combination of values is invalid, a `Map` with error specifications is returned. This specification is then used by the Datagrok validator mechanism to display errors on the corresponding inputs. + +► **Implementation:** +- `task_primary`: function `validate(inputs: LevinsParams): ValidationErrors` (`example/code/src/levins/core.ts`, line 40) — returns `Map`. Rules val_01…val_09 check both individual values and combinations (e.g., val_05: `m <= e0` when `rescueEffect = false`; val_08: `t_step >= t_end - t_start`). +- `task_optimize`: function `validateOptimize(opt, e0, rescueEffect)` (`example/code/src/levins/core.ts`, line 106) — returns `{ errors: Map, warning: string | null }`. Rules opt_val_01…opt_val_04. + +### 7.2. Validation Order + +The order of determining input set validity is defined by the specification for each task. + +► **Implementation:** in `validate()`, combinatorial rules are checked only after basic rules pass: +- val_05 is checked only if val_03 and val_04 passed (line 59: `if (!errors.has('ctrl_m') && !errors.has('ctrl_e0') && ...)`). +- val_08 is checked only if val_06 and val_07 passed (line 73: `if (!errors.has('ctrl_t_end') && !errors.has('ctrl_t_step') && ...)`). + +## 8. Main Pipeline + +Each computational task of the core has its own pipeline. All pipelines are orchestrated by the coordinator (see section 1.4). + +### 8.1. Primary Pipeline + +The primary pipeline is bound to the main application controls and operates reactively. + +**Parameter input.** The user sets input values through controls. The UI adapter converts the input into typed data for the task's input port. + +**Validation.** The input set is validated (see section 7). Validation is performed by the core through the input port. + +**Computation.** Validated inputs are passed to the core task. Execution characteristics (synchronicity, environment, parallelization) are defined by the task specification (see section 1.1). + +**Result display.** The core returns results through the task's output port. The coordinator passes them to the display adapter (see section 4). + +► **Implementation:** function `runPrimary()` (`example/code/src/levins/app.ts`, line 226): +1. Checks `computationsBlocked` (line 227). +2. Collects inputs: `getInputs()` (line 230). +3. Validates: `validate(inputs)` (line 231). +4. On errors — visually marks invalid inputs (`d4-invalid`) and calls `clearResults()` (lines 237–245). +5. On success — `solve(inputs)` (line 248), then `updateDataFrame(result)` + `updateColorCoding()` (lines 249–250). + +Reactive trigger: each input has `onValueChanged: () => debouncedRun()` (debounce 50 ms, line 258). + +### 8.2. Secondary Pipelines + +A secondary pipeline is bound to an explicit user action (button, icon, menu item) and runs on demand. + +**Trigger.** The user initiates an action (e.g., clicks the "Sensitivity Analysis" button). + +**Custom UI.** A secondary pipeline may have its own parameter input interface — for example, a Datagrok dialog with its own set of inputs, validation, and tooltips. This UI is defined by the secondary task specification. + +**Validation.** Secondary task parameters are validated independently — using their own complex validation rules. + +**Computation.** The core task is executed. The secondary task may use primary task results as part of its input data. + +**Result display.** Secondary task results are displayed with their own elements — these can be additional viewers docked to the main view, dialog content, or a separate window. Defined by the specification. + +**Feedback to primary pipeline.** The secondary task may return values that are substituted into primary pipeline controls. In this case, the computation blocking and batch update mechanism is used (see section 8.4). + +► **Implementation of `task_optimize`:** +1. **Trigger:** `optimizeBtn` → `showOptimizeDialog()` (line 503). +2. **Custom UI:** dialog `ui.dialog('Find optimal m')` with inputs `dlgMMin`, `dlgMMax` (lines 448–499), with its own validators and tooltips. +3. **Validation:** `validateDialog()` (line 463) calls `validateOptimize()` from the core. +4. **Computation:** `runOptimization(mMin, mMax)` (line 292) — creates a worker pool, distributes 10,000 tasks, collects results. +5. **Display:** `grok.shell.info(...)` with the found optimum (line 433). +6. **Feedback:** `ctrlM.value = best.m_i` via batch update (lines 428–433). + +### 8.3. Common Pipeline Aspects + +The following aspects are defined by the specification for each pipeline independently: + +#### Control Behavior During Computations + +Controls may become disabled during computations — which ones and for which pipeline is defined by the specification. + +► **Implementation:** `task_primary` does not block controls (< 100 ms). `task_optimize` blocks only `optimizeBtn` via `setOptimizeBtnEnabled(false)` (lines 288–289, 300). + +#### Progress and Cancellation + +During long computations, the standard Datagrok progress bar is displayed with cancellation support. + +► **Implementation:** `task_primary` — no progress bar. `task_optimize` — `DG.TaskBarProgressIndicator` with completion percentage and cancellation via the `canceled` flag (line 303). + +#### Computation Error Handling + +The course of action upon computation failure is defined by the specification for each task. + +► **Implementation:** +- `task_primary`: `catch` → `clearResults()` + `grok.shell.error(msg)` (lines 251–255). +- `task_optimize`: partial errors (some workers failed) → `grok.shell.warning(...)` (line 413); complete failure → `grok.shell.error(...)` (line 416); error writing to control → `grok.shell.warning(...)` with instructions to enter manually (line 436). + +### 8.4. Computation Blocking and Batch Input Updates + +In certain scenarios, the result of a secondary task must be substituted into primary pipeline controls. In this case, each individual input change should not trigger a reactive recomputation — the computation should happen once, after all values have been substituted. + +The coordinator supports a computation blocking mode: + +**Block request.** Before batch update, the coordinator suspends reactive execution of specified pipelines. Which pipelines are blocked is defined by the specification. + +**Batch update.** Values are substituted into controls. Cascading dependencies between inputs (reactivity, section 9) can be handled in one of two modes — defined by the specification: + +- Reactivity between inputs works as usual, but computations are not triggered. +- Reactivity between inputs is also suspended until the batch update completes. + +**Unblock.** After all values are substituted, the coordinator removes the block. Full input set validation occurs, then computation runs, then results are displayed — the standard pipeline (section 8.1). + +Example: the user launches parameter optimization (secondary task). Upon optimization completion, the coordinator blocks the primary pipeline, substitutes the found parameter values into all controls, removes the block — the primary pipeline runs once for the complete set of optimal parameters. + +► **Implementation:** the `computationsBlocked` flag (`example/code/src/levins/app.ts`, line 19) is used in three scenarios: +1. **Format initialization** (lines 102–110): blocking prevents side-effect recomputations during `format` assignment. +2. **Reset** (lines 506–518): `computationsBlocked = true` → reset all values → `computationsBlocked = false` → `runPrimary()`. +3. **Optimization result** (lines 428–437): `computationsBlocked = true` → `ctrlM.value = best.m_i` → `computationsBlocked = false` → `runPrimary()`. + +Blocking check: `runPrimary()` first checks `if (computationsBlocked) return` (line 227). + +## 9. Reactivity and Dependencies Between Inputs + +Dependencies between inputs (cascading updates of ranges, defaults, availability) are defined by the application specification. Reactivity is managed by the coordinator (see section 1.4) and operates entirely at the UI adapter level — independent of the core's computational part. + +Reactivity can be temporarily suspended by the coordinator in batch input update mode (see section 8.4). + +► **Implementation:** +- **`ctrl_m` / `ctrl_e0` → `rhoBadge`:** `updateRhoBadge()` (line 124) is called from `onValueChanged` of both inputs. Recalculates `rho = e0/m`, updates text and CSS class. +- **`ctrl_rescue` → `ctrl_e0` (label + tooltip):** `updateRescueLabel()` (line 136) switches caption and tooltip based on the toggle state. +- **`ctrl_t_start` / `ctrl_t_end` → ranges:** `updateArgRanges()` (line 149) — scaffold for range updates; actual checking via complex validation (val_06, val_07, val_08). +- **Debounce:** numeric inputs use `debouncedRun()` (debounce 50 ms, line 258), toggle `ctrl_rescue` calls `runPrimary()` directly (without debounce). + +## 10. Data Lifecycle + +### 10.1. Data Input + +The primary approach is manual entry through application controls. + +► **Implementation:** all model parameters are entered by the user through the form in the left panel. The initial state uses default values from `DEFAULTS` (`example/code/src/levins/core.ts`, line 27). `task_primary` runs automatically on initialization: `solve(DEFAULTS)` (line 26). + +### 10.2. Loading from a Resource + +Via buttons or icons — loading data from an external resource. Which specific resource and loading mechanism is defined by the application specification. + +► **Implementation:** not used in this application. + +## 11. Error Handling Beyond Computations + +The strategy for handling data loading errors, network errors, invalid input files, and incorrect application state is defined by the application specification. + +► **Implementation:** see specification (`example/levins-metapopulation-spec.md`, section 11). Examples: +- Worker creation error: `reject(new Error('Failed to start parallel computations...'))` (line 380). +- Partial worker errors: `errorCount` counting and `grok.shell.warning(...)` (lines 305, 412–413). +- Error writing to control: fallback with `grok.shell.warning(...)` (line 436). + +## 12. Subscriptions and Resource Management + +### 12.1. Event Subscriptions + +All Datagrok event subscriptions (`onValueChanged`, `onAfterDraw`, etc.) must be collected and unsubscribed when the application closes via `sub.unsubscribe()`. + +► **Implementation:** array `subs` (`example/code/src/levins/app.ts`, line 21) collects subscriptions. On close — `for (const sub of subs) sub.unsubscribe()` (line 564). + +### 12.2. Worker Termination + +When the application closes, all web workers must be properly terminated. + +► **Implementation:** array `activeWorkers` (line 22), function `terminateWorkers()` (line 440) calls `w.terminate()` for each worker. Called on close (line 563) and after optimization completes (line 405). + +## 13. Application Closure + +When the application closes, the coordinator performs: + +- All event subscriptions are unsubscribed — for both primary and secondary pipelines (see section 12.1). +- All web workers are terminated — including secondary task workers (see section 12.2). +- All associated UI elements are closed (including open secondary task dialogs). +- Pending requests are cancelled. + +► **Implementation:** handler `grok.events.onViewRemoved.subscribe(...)` (`example/code/src/levins/app.ts`, lines 561–568): +1. `terminateWorkers()` — terminates all active workers. +2. `for (const sub of subs) sub.unsubscribe()` — unsubscribes subscriptions. +3. `clearTimeout(debounceTimer)` — cancels pending debounce. + +## 14. Accessibility and UX + +Keyboard shortcuts, context menus, undo/redo, and other UX elements are defined by the application specification. + +► **Implementation:** in the current version of the application, keyboard shortcuts, context menus, and undo/redo are not implemented (see specification, section 12). + +## 15. Testing + +### 15.1. Computational Part (Core) + +Core correctness verification: unit tests for each computational task separately. The core is tested in isolation — without UI and adapters. + +► **Implementation:** tests are split across two files: + +**`example/code/src/tests/levins-api-tests.ts`** — 2 categories: +- **API: Validation** — 24 tests: val_01…val_09 + boundary values + dependency order + multiple errors + defaults. +- **API: Optimization Validation** — 7 tests: opt_val_01…opt_val_04 + valid input data. + +**`example/code/src/tests/levins-math-tests.ts`** — 4 categories: +- **Math: MRT solver** — 3 tests: non-stiff 1D, stiff 1D, stiff 2D (van der Pol) — verifying the `mrt` solver against analytical/reference solutions. +- **Math: Levins func** — 5 tests: ODE right-hand side correctness for the base model and rescue effect at specific `p` values. +- **Math: Equilibrium** — 4 tests: `getEquilibrium` for the base model and rescue effect. +- **Math: Solve output properties** — 8 tests: output invariants of `solve()` — bounds `p ∈ [0, 1]`, initial conditions, convergence to `p*`, monotonicity in `m`. + +Tests are run via `grok test` (entry point: `example/code/src/package-test.ts`). + +### 15.2. Inputs + +Input verification for each task: all cases including edge cases (boundary values, invalid combinations, empty values, extreme values). + +► **Implementation:** validation tests cover: +- Boundary values: `p0 = 0.001` (lower allowed bound), `p0 = 1` (upper). +- Invalid combinations: `m <= e0` without rescue, `t_step >= t_end - t_start`. +- Dependencies between rules: val_05 is skipped when val_03 fails, val_08 is skipped when val_06/val_07 fail. +- Multiple errors: simultaneously invalid `p0`, `m`, `e0`, `t_step`, `tolerance`. + +### 15.3. Mathematical Verification + +Verification that the implemented computation matches the model definition (see section 1.1, "Computation Formulas and Model"). Test categories correspond to the two levels of the model definition. Verification criteria — reference examples, expected accuracies, output property constraints, reference problems for the numerical method — are defined by the model specification, not invented during test writing. Tests implement what is specified; the specification is the source of truth. + +#### Level 1 verification (required) + +**Formula/equation verification.** The implemented transformation is checked at control points with manually computed expected values. For each computational path (mode, branch, regime), at least one test substitutes concrete inputs and compares the output against a hand-calculated result. + +**Output property verification.** Constraints declared in the model definition (bounds, monotonicity, conservation laws, limiting cases) are checked on actual computation results. These tests do not compare against a specific expected value — they verify that the result satisfies a declared invariant. + +► **Implementation:** +- **Formula verification:** `Math: Levins func` — 5 tests. Each test substitutes specific `(m, e₀, p)` into the ODE right-hand side and compares `dp/dt` against a hand-calculated value (e.g., `func_01`: `dp/dt = 0.5·0.5·0.5 − 0.2·0.5 = 0.025`). Both computational paths are covered: base model (3 tests) and rescue effect (2 tests). +- **Equilibrium verification:** `Math: Equilibrium` — 4 tests. `getEquilibrium` is checked against the analytical formula `p* = 1 − e₀/m` for the base model, and `NaN` for the rescue effect (no closed-form equilibrium). +- **Output properties:** `Math: Solve output properties` — 8 tests. Verifies invariants declared in the specification: `p(t) ∈ [0, 1]` (solve_02, solve_06), `p(0) = p0` (solve_03, solve_08), convergence to `p*` (solve_05), monotonicity in `m` (solve_07), non-empty output arrays (solve_01), `t[0] = t_start` (solve_04). + +#### Level 2 verification (for full formalization) + +**Numerical method verification.** The solver (or library) is tested on reference problems with known analytical solutions. The test verifies that the numerical error stays within the expected tolerance. Reference problems should cover the solver's applicability range (e.g., non-stiff and stiff problems for an ODE solver). Each reference problem must cite its source (textbook, paper, test suite). + +**Convergence verification.** Solving the same problem with decreasing step size or tolerance produces solutions that converge. The test compares solutions at two different precision levels and verifies that the discrepancy decreases. + +**Asymptotic/equilibrium behavior.** The numerical solution on a sufficiently long interval approaches the analytically predicted equilibrium or asymptote. + +► **Implementation:** +- **Numerical method:** `Math: MRT solver` — 3 tests. Non-stiff 1D and stiff 1D problems verified against analytical solutions (Chapra & Canale, pp. 736, 767). Stiff 2D van der Pol (µ=1000) verified for solver stability (reference: VDPOL test set). Tolerance threshold: max absolute error < 0.1. +- **Convergence:** not yet covered. Candidate: solve the Levins ODE with `tolerance = 1e-5` and `tolerance = 1e-9`, verify that the discrepancy between solutions decreases. +- **Asymptotic behavior:** not yet covered. Candidate: verify that `p(t_end)` approaches `p* = 1 − e₀/m` for sufficiently large `t_end`. diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/css/levins.css b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/css/levins.css new file mode 100644 index 0000000000..2c73271d32 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/css/levins.css @@ -0,0 +1,25 @@ +/* Levins Metapopulation Model — Application Styles */ + +/* Rho badge (e0/m ratio indicator) */ +.levins-rho-badge { + font-size: 13px; + padding: 4px 8px; + border-radius: 4px; + display: inline-block; + margin-top: 4px; + color: white; +} + +.levins-rho-badge--persists { + background-color: #4CAF50; +} + +.levins-rho-badge--extinct { + background-color: #F44336; +} + +/* Disabled icon button (ui.iconFA) */ +.levins-btn--disabled { + pointer-events: none; + opacity: 0.4; +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/detectors.js b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/detectors.js new file mode 100644 index 0000000000..f21cf90325 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/detectors.js @@ -0,0 +1,9 @@ +/** + * The class contains semantic type detectors. + * Detectors are functions tagged with `DG.FUNC_TYPES.SEM_TYPE_DETECTOR`. + * See also: https://datagrok.ai/help/develop/how-to/define-semantic-type-detectors + * The class name is comprised of and the `PackageDetectors` suffix. + * Follow this naming convention to ensure that your detectors are properly loaded. + */ +class InteractiveSciAppTestPackageDetectors extends DG.Package { +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/package.json b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/package.json new file mode 100644 index 0000000000..592ab1df94 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/package.json @@ -0,0 +1,49 @@ +{ + "name": "interactivesciapptest", + "friendlyName": "InteractiveSciAppTest", + "version": "0.0.1", + "description": "InteractiveSciAppTest package", + "author": { + "name": "CC", + "email": "vmakarichev@datagrok.ai" + }, + "dependencies": { + "@datagrok-libraries/utils": "^4.6.5", + "cash-dom": "^8.1.5", + "datagrok-api": "^1.26.0", + "dayjs": "^1.11.13", + "diff-grok": "^1.2.0" + }, + "devDependencies": { + "css-loader": "^7.1.2", + "style-loader": "^4.0.0", + "ts-loader": "latest", + "typescript": "latest", + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4" + }, + "scripts": { + "debug-interactivesciapptest": "webpack && grok publish", + "release-interactivesciapptest": "webpack && grok publish --release", + "build-interactivesciapptest": "webpack", + "build": "grok api && grok check --soft && webpack", + "test": "grok test", + "debug-interactivesciapptest-dev": "webpack && grok publish dev", + "release-interactivesciapptest-dev": "webpack && grok publish dev --release", + "debug-interactivesciapptest-local": "webpack && grok publish local", + "release-interactivesciapptest-local": "webpack && grok publish local --release", + "debug-interactivesciapptest-release": "webpack && grok publish release", + "release-interactivesciapptest-release": "webpack && grok publish release --release" + }, + "canEdit": [ + "Developers" + ], + "canView": [ + "All users" + ], + "repository": { + "type": "git", + "url": "https://github.com/datagrok-ai/public.git", + "directory": "packages/InteractiveSciAppTest" + } +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/app.ts b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/app.ts new file mode 100644 index 0000000000..d662589774 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/app.ts @@ -0,0 +1,557 @@ +// Levins Metapopulation Model — Application (Coordinator + UI) + +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; + +import { + DEFAULTS, validate, solve, validateOptimize, + LevinsParams, LevinsSolution, InputId, WorkerTask, WorkerResult, +} from './core'; + +import '../../css/levins.css'; + +const DEBOUNCE_MS = 50; +const OPTIMIZE_POINTS = 10000; + +export function levinsMetapopulationApp(): void { + // --- State --- + let computationsBlocked = false; + let debounceTimer: ReturnType | null = null; + const subs: {unsubscribe(): void}[] = []; + let activeWorkers: Worker[] = []; + let lineChart!: DG.Viewer; + + // --- Initial DataFrame --- + const initSolution = solve(DEFAULTS); + const df = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', initSolution.t), + DG.Column.fromFloat64Array('p', initSolution.p), + ]); + df.name = 'Levins Metapopulation'; + + const view = grok.shell.addTableView(df); + view.name = 'Levins Metapopulation Model'; + + // --- Rho badge (created before controls so onValueChanged callbacks can reference it) --- + const rhoBadge = ui.div([], 'd4-tag levins-rho-badge'); + ui.tooltip.bind(rhoBadge, 'Extinction-to-colonization rate ratio. \u03C1 < 1 \u2014 metapopulation persists, \u03C1 \u2265 1 \u2014 extinction.'); + + // --- Controls --- + + // Initial condition + const ctrlP0 = ui.input.float('Initial patch fraction p₀', { + value: DEFAULTS.p0, nullable: false, + min: 0.001, max: 1, + tooltipText: 'Fraction of patches occupied at t=0. If p₀=0, the population cannot recover — computation is skipped.', + onValueChanged: () => debouncedRun(), + }); + + // Parameters + const ctrlM = ui.input.float('Colonization rate m', { + value: DEFAULTS.m, nullable: false, + min: 0.001, max: 100, + tooltipText: 'How fast empty patches are colonized from occupied ones. Units: 1/time.', + onValueChanged: () => { updateRhoBadge(); debouncedRun(); }, + }); + + const ctrlE0 = ui.input.float('Extinction rate e₀', { + value: DEFAULTS.e0, nullable: false, + min: 0.001, max: 100, + tooltipText: 'Base local extinction rate of a subpopulation in a patch. With rescue effect — decreases as p grows. Units: 1/time.', + onValueChanged: () => { updateRhoBadge(); debouncedRun(); }, + }); + + const ctrlRescue = ui.input.toggle('Rescue effect', { + value: DEFAULTS.rescueEffect, + tooltipText: 'When enabled, extinction rate depends on p: e(p) = e₀·(1−p). More occupied patches — lower local extinction.', + onValueChanged: () => { updateRescueLabel(); runPrimary(); }, + }); + + // Argument + const ctrlTStart = ui.input.float('Start t₀', { + value: DEFAULTS.t_start, nullable: false, + min: 0, max: 10000, + tooltipText: 'Simulation start time. Usually 0.', + onValueChanged: () => { updateArgRanges(); debouncedRun(); }, + }); + + const ctrlTEnd = ui.input.float('End t_end', { + value: DEFAULTS.t_end, nullable: false, + min: 0.1, max: 10000, + tooltipText: 'Simulation end time. Recommended ≥ 5/e₀ so the system reaches equilibrium.', + onValueChanged: () => { updateArgRanges(); debouncedRun(); }, + }); + + const ctrlTStep = ui.input.float('Step Δt', { + value: DEFAULTS.t_step, nullable: false, + min: 0.001, max: 1000, + tooltipText: 'Grid step of the numerical solution. Affects chart detail, not stability (MRT is an implicit method).', + onValueChanged: () => debouncedRun(), + }); + + // Solver + const ctrlTolerance = ui.input.float('Tolerance', { + value: DEFAULTS.tolerance, nullable: false, + min: 1e-12, max: 1e-2, + tooltipText: 'MRT method numerical tolerance. Lower — more precise but slower. Recommended: 1e-6 … 1e-9.', + onValueChanged: () => debouncedRun(), + }); + + // Set formats (block computations to avoid spurious runs from format-triggered events) + computationsBlocked = true; + ctrlP0.format = '0.000'; + ctrlM.format = '0.000'; + ctrlE0.format = '0.000'; + ctrlTStart.format = '0.0'; + ctrlTEnd.format = '0.0'; + ctrlTStep.format = '0.000'; + ctrlTolerance.format = '0.##E+0'; + computationsBlocked = false; + + // --- Input map for validators --- + const inputMap: Record = { + 'ctrl_p0': ctrlP0, + 'ctrl_m': ctrlM, + 'ctrl_e0': ctrlE0, + 'ctrl_rescue': ctrlRescue, + 'ctrl_t_start': ctrlTStart, + 'ctrl_t_end': ctrlTEnd, + 'ctrl_t_step': ctrlTStep, + 'ctrl_tolerance': ctrlTolerance, + }; + + function updateRhoBadge(): void { + const m = ctrlM.value ?? DEFAULTS.m; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const rho = e0 / m; + const persists = rho < 1; + rhoBadge.textContent = `ρ = e₀/m = ${rho.toFixed(3)}`; + rhoBadge.classList.toggle('levins-rho-badge--persists', persists); + rhoBadge.classList.toggle('levins-rho-badge--extinct', !persists); + } + updateRhoBadge(); + + // --- Rescue effect label reactivity --- + function updateRescueLabel(): void { + if (ctrlRescue.value) { + ctrlE0.caption = 'Base extinction rate e₀'; + ctrlE0.setTooltip('Base local extinction rate. Effective rate: e(p) = e₀·(1−p)'); + } else { + ctrlE0.caption = 'Extinction rate e₀'; + ctrlE0.setTooltip('Base local extinction rate of a subpopulation in a patch. Units: 1/time.'); + } + } + + // --- Argument range reactivity --- + // Note: Datagrok InputBase does not have setOptions for changing min/max at runtime. + // Range validation is handled by the complex validator instead. + function updateArgRanges(): void { + // Ranges are enforced through validation (val_06, val_07) + } + + // --- Gather current inputs --- + function getInputs(): LevinsParams { + return { + p0: ctrlP0.value ?? DEFAULTS.p0, + m: ctrlM.value ?? DEFAULTS.m, + e0: ctrlE0.value ?? DEFAULTS.e0, + rescueEffect: ctrlRescue.value ?? DEFAULTS.rescueEffect, + t_start: ctrlTStart.value ?? DEFAULTS.t_start, + t_end: ctrlTEnd.value ?? DEFAULTS.t_end, + t_step: ctrlTStep.value ?? DEFAULTS.t_step, + tolerance: ctrlTolerance.value ?? DEFAULTS.tolerance, + }; + } + + // --- Validators --- + function addValidators(): void { + const validatorFor = (id: InputId) => { + return () => { + const inputs = getInputs(); + const errors = validate(inputs); + return errors.get(id) ?? null; + }; + }; + + ctrlP0.addValidator(validatorFor('ctrl_p0')); + ctrlM.addValidator(validatorFor('ctrl_m')); + ctrlE0.addValidator(validatorFor('ctrl_e0')); + ctrlTStart.addValidator(validatorFor('ctrl_t_start')); + ctrlTEnd.addValidator(validatorFor('ctrl_t_end')); + ctrlTStep.addValidator(validatorFor('ctrl_t_step')); + ctrlTolerance.addValidator(validatorFor('ctrl_tolerance')); + } + addValidators(); + + // --- Color coding --- + function updateColorCoding(): void { + const m = ctrlM.value ?? DEFAULTS.m; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const threshold = e0 / m; + const pCol = view.dataFrame.col('p'); + if (pCol == null) return; + + const rules: Record = {}; + rules['<' + threshold] = '#F44336'; + rules['>=' + threshold] = '#4CAF50'; + pCol.meta.colors.setConditional(rules); + } + + // --- Grid column header tooltip (via onCellTooltip, as in EDA) --- + function setupGridTooltip(): void { + view.grid.onCellTooltip((cell, x, y) => { + if (!cell.isColHeader) + return false; + + const colName = cell.tableColumn?.name; + if (colName === 'p') { + const m = ctrlM.value ?? DEFAULTS.m; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const rho = (e0 / m).toFixed(3); + ui.tooltip.show(ui.divV([ + ui.h2('Occupied patch fraction p(t)'), + ui.divText(`Color: green — persistence zone (p ≥ e₀/m)`), + ui.divText(`red — extinction threat zone (p < e₀/m)`), + ui.divText(`Threshold: e₀/m = ${rho}`), + ]), x, y); + return true; + } + + return false; + }); + } + + // --- Primary pipeline --- + function runPrimary(): void { + if (computationsBlocked) + return; + + const inputs = getInputs(); + const errors = validate(inputs); + + // Clear previous errors on all inputs + for (const input of Object.values(inputMap)) + input.input?.classList.remove('d4-invalid'); + + if (errors.size > 0) { + errors.forEach((_msg, id) => { + const input = inputMap[id]; + if (input) + input.input?.classList.add('d4-invalid'); + }); + clearResults(); + return; + } + + try { + const result = solve(inputs); + updateDataFrame(result); + updateColorCoding(); + } catch (err) { + clearResults(); + const msg = err instanceof Error ? err.message : 'Computation error'; + grok.shell.error(msg); + } + } + + function debouncedRun(): void { + if (debounceTimer !== null) + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => runPrimary(), DEBOUNCE_MS); + } + + // --- Update DataFrame --- + function updateDataFrame(result: LevinsSolution): void { + const newDf = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', result.t), + DG.Column.fromFloat64Array('p', result.p), + ]); + newDf.name = 'Levins Metapopulation'; + view.dataFrame = newDf; + lineChart.dataFrame = newDf; + } + + function clearResults(): void { + const emptyDf = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', new Float64Array(0)), + DG.Column.fromFloat64Array('p', new Float64Array(0)), + ]); + emptyDf.name = 'Levins Metapopulation'; + view.dataFrame = emptyDf; + lineChart.dataFrame = emptyDf; + } + + // --- Optimization task --- + let optimizeBtn: HTMLElement; + + function setOptimizeBtnEnabled(enabled: boolean): void { + optimizeBtn.classList.toggle('levins-btn--disabled', !enabled); + } + + async function runOptimization(mMin: number, mMax: number): Promise { + const inputs = getInputs(); + const errors = validate(inputs); + if (errors.size > 0) { + grok.shell.error('Internal error: invalid task parameters. Check the inputs and try again.'); + return; + } + + setOptimizeBtnEnabled(false); + + const pi = DG.TaskBarProgressIndicator.create('Optimizing m...', {cancelable: true, spinner: true}); + let errorCount = 0; + + const results: {m_i: number; p_end: number}[] = []; + const workerCount = Math.max(1, (navigator.hardwareConcurrency ?? 4) - 2); + + // Generate m values + const mValues: number[] = []; + for (let i = 0; i < OPTIMIZE_POINTS; i++) + mValues.push(mMin + i * (mMax - mMin) / (OPTIMIZE_POINTS - 1)); + + // Fan-out: distribute tasks round-robin across workers + const workerUrl = _package.webRoot + 'dist/optimize-worker.js'; + const tasks: WorkerTask[] = mValues.map((m_i) => ({ + m_i, + p0: inputs.p0, + e0: inputs.e0, + rescueEffect: inputs.rescueEffect, + t_start: inputs.t_start, + t_end: inputs.t_end, + t_step: inputs.t_step, + tolerance: inputs.tolerance, + })); + + const nWorkers = Math.min(workerCount, tasks.length); + const chunks: WorkerTask[][] = Array.from({length: nWorkers}, () => []); + for (let i = 0; i < tasks.length; i++) + chunks[i % nWorkers].push(tasks[i]); + + activeWorkers = []; + + const resolvers = new Array<(value: WorkerResult[]) => void>(chunks.length); + const batchPromises = chunks.map((batch, i) => + new Promise((resolve, reject) => { + resolvers[i] = resolve; + let worker: Worker; + try { + worker = new Worker(workerUrl); + } catch (_err) { + reject(new Error('Failed to start parallel computations. Try again later.')); + return; + } + activeWorkers.push(worker); + + worker.onmessage = (event: MessageEvent) => { + worker.terminate(); + activeWorkers = activeWorkers.filter((w) => w !== worker); + resolve(event.data); + }; + + worker.onerror = (err) => { + worker.terminate(); + activeWorkers = activeWorkers.filter((w) => w !== worker); + reject(new Error(err.message ?? 'Worker error')); + }; + + worker.postMessage(batch); + }), + ); + + const cancelSub = pi.onCanceled.subscribe(() => { + terminateWorkers(); + for (const resolve of resolvers) + resolve([]); + }); + + const settled = await Promise.allSettled(batchPromises); + + cancelSub.unsubscribe(); + pi.close(); + terminateWorkers(); + setOptimizeBtnEnabled(true); + + if (pi.canceled) + return; + + // Fan-in: collect results + for (let i = 0; i < settled.length; i++) { + const outcome = settled[i]; + if (outcome.status === 'fulfilled') { + for (const r of outcome.value) { + if (r.error) + errorCount++; + else + results.push({m_i: r.m_i, p_end: r.p_end}); + } + } else { + errorCount += chunks[i].length; + } + } + + if (errorCount > 0 && errorCount < OPTIMIZE_POINTS) + grok.shell.warning(`${errorCount} of ${OPTIMIZE_POINTS} points failed to compute. Result based on ${OPTIMIZE_POINTS - errorCount} points.`); + + if (results.length === 0) { + grok.shell.error('Failed to compute any point. Check the parameters.'); + return; + } + + // Find optimal + let best = results[0]; + for (const r of results) { + if (r.p_end > best.p_end) + best = r; + } + + // Batch update: block primary, write m_optimal, unblock and run once + try { + computationsBlocked = true; + ctrlM.value = best.m_i; + computationsBlocked = false; + runPrimary(); + grok.shell.info(`Optimal m = ${best.m_i.toFixed(3)}\np(t_end) = ${best.p_end.toFixed(3)}`); + } catch (_err) { + computationsBlocked = false; + grok.shell.warning(`Optimal m = ${best.m_i.toFixed(3)}, but failed to update the field automatically. Enter the value manually.`); + } + } + + function terminateWorkers(): void { + for (const w of activeWorkers) + w.terminate(); + activeWorkers = []; + } + + // --- Optimize dialog --- + function showOptimizeDialog(): void { + const dlgMMin = ui.input.float('Minimum m', { + value: 0.1, nullable: false, + min: 0.001, max: 100, + tooltipText: 'Lower bound of the m search range. Must be less than the maximum value.', + }); + dlgMMin.format = '0.000'; + + const dlgMMax = ui.input.float('Maximum m', { + value: 1.0, nullable: false, + min: 0.001, max: 100, + tooltipText: 'Upper bound of the m search range. Must be greater than the minimum value.', + }); + dlgMMax.format = '0.000'; + + // Cross-validation of dialog inputs + const validateDialog = (): boolean => { + const mMin = dlgMMin.value ?? 0.1; + const mMax = dlgMMax.value ?? 1.0; + const e0 = ctrlE0.value ?? DEFAULTS.e0; + const rescueEffect = ctrlRescue.value ?? DEFAULTS.rescueEffect; + + const {errors, warning} = validateOptimize({m_min: mMin, m_max: mMax}, e0, rescueEffect); + + if (warning) + grok.shell.warning(warning); + + return errors.size === 0; + }; + + dlgMMin.addValidator(() => { + const mMin = dlgMMin.value ?? 0.1; + const mMax = dlgMMax.value ?? 1.0; + if (mMin <= 0) return 'Colonization rate must be positive'; + if (mMin >= mMax) return 'Minimum value must be less than maximum'; + return null; + }); + + dlgMMax.addValidator(() => { + const mMax = dlgMMax.value ?? 1.0; + if (mMax <= 0) return 'Colonization rate must be positive'; + return null; + }); + + ui.dialog('Find optimal m') + .add(dlgMMin) + .add(dlgMMax) + .onOK(() => { + if (!validateDialog()) + return; + runOptimization(dlgMMin.value!, dlgMMax.value!); + }) + .show(); + } + + // --- Toolbar buttons --- + optimizeBtn = ui.iconFA('search', () => showOptimizeDialog(), 'Find the m value that maximizes the occupied patch fraction at t_end'); + + const resetBtn = ui.iconFA('undo', () => { + computationsBlocked = true; + ctrlP0.value = DEFAULTS.p0; + ctrlM.value = DEFAULTS.m; + ctrlE0.value = DEFAULTS.e0; + ctrlRescue.value = DEFAULTS.rescueEffect; + ctrlTStart.value = DEFAULTS.t_start; + ctrlTEnd.value = DEFAULTS.t_end; + ctrlTStep.value = DEFAULTS.t_step; + ctrlTolerance.value = DEFAULTS.tolerance; + computationsBlocked = false; + updateRhoBadge(); + runPrimary(); + }, 'Reset all parameters to default values'); + + view.setRibbonPanels([[optimizeBtn, resetBtn]]); + + // --- Layout: left panel with form --- + const form = ui.form([]); + + form.append(ui.h2('Initial Condition')); + form.append(ctrlP0.root); + + form.append(ui.h2('Parameters')); + form.append(ctrlM.root); + form.append(ctrlE0.root); + form.append(ctrlRescue.root); + form.append(rhoBadge); + + form.append(ui.h2('Argument')); + form.append(ctrlTStart.root); + form.append(ctrlTEnd.root); + form.append(ctrlTStep.root); + + form.append(ui.h2('Solver')); + form.append(ctrlTolerance.root); + + const dockMng = view.dockManager; + dockMng.dock(form, DG.DOCK_TYPE.LEFT, null, undefined, 0.2); + + // --- Line chart --- + lineChart = view.addViewer('Line chart', { + xColumnName: 't', + yColumnNames: ['p'], + title: 'p(t) Dynamics', + }); + + const gridNode = dockMng.findNode(view.grid.root); + if (gridNode != null) + dockMng.dock(lineChart, DG.DOCK_TYPE.RIGHT, gridNode, undefined, 0.5); + + // --- Initial color coding and tooltip --- + updateColorCoding(); + setupGridTooltip(); + + // --- Cleanup on close --- + subs.push(grok.events.onViewRemoved.subscribe((v: any) => { + if (v === view) { + terminateWorkers(); + for (const sub of subs) + sub.unsubscribe(); + if (debounceTimer !== null) + clearTimeout(debounceTimer); + } + })); +} + +// Package reference (set from package.ts) +let _package: DG.Package; +export function setPackage(pkg: DG.Package): void { + _package = pkg; +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/core.ts b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/core.ts new file mode 100644 index 0000000000..b927b6a81e --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/core.ts @@ -0,0 +1,144 @@ +// Levins Metapopulation Model — Computational Core + +import {mrt} from 'diff-grok'; + +import {LevinsParams, createLevinsODE, getEquilibrium} from './model'; + +// --- Re-exports --- + +export type {LevinsParams} from './model'; +export {createLevinsODE, getEquilibrium} from './model'; + +// --- Types --- + +export interface LevinsSolution { + t: Float64Array; + p: Float64Array; + p_star: number; +} + +export type InputId = 'ctrl_p0' | 'ctrl_m' | 'ctrl_e0' | 'ctrl_rescue' | + 'ctrl_t_start' | 'ctrl_t_end' | 'ctrl_t_step' | 'ctrl_tolerance'; + +export type ValidationErrors = Map; + +// --- Defaults --- + +export const DEFAULTS: LevinsParams = { + p0: 0.5, + m: 0.5, + e0: 0.2, + rescueEffect: false, + t_start: 0, + t_end: 50, + t_step: 0.1, + tolerance: 1e-7, +}; + +// --- Validation --- + +export function validate(inputs: LevinsParams): ValidationErrors { + const errors: ValidationErrors = new Map(); + const {p0, m, e0, rescueEffect, t_start, t_end, t_step, tolerance} = inputs; + + // val_01, val_02 + if (p0 <= 0) + errors.set('ctrl_p0', 'Initial patch fraction must be greater than 0'); + else if (p0 > 1) + errors.set('ctrl_p0', 'Initial patch fraction cannot exceed 1'); + + // val_03 + if (m <= 0) + errors.set('ctrl_m', 'Colonization rate must be positive'); + + // val_04 + if (e0 <= 0) + errors.set('ctrl_e0', 'Extinction rate must be positive'); + + // val_05 — only if val_03 and val_04 passed + if (!errors.has('ctrl_m') && !errors.has('ctrl_e0') && !rescueEffect && m <= e0) + errors.set('ctrl_m', 'Colonization rate must exceed extinction rate (m > e₀). With current values the metapopulation tends to extinction'); + + // val_06 + if (t_end <= t_start) { + errors.set('ctrl_t_end', 'End of interval must be greater than start'); + errors.set('ctrl_t_start', 'End of interval must be greater than start'); + } + + // val_07 + if (t_step <= 0) + errors.set('ctrl_t_step', 'Step must be positive'); + + // val_08 — only if val_06 and val_07 passed + if (!errors.has('ctrl_t_end') && !errors.has('ctrl_t_step') && t_step >= t_end - t_start) + errors.set('ctrl_t_step', 'Step must be less than interval length'); + + // val_09 + if (tolerance <= 0) + errors.set('ctrl_tolerance', 'Tolerance must be positive'); + + return errors; +} + +// --- Solver --- + +export function solve(inputs: LevinsParams): LevinsSolution { + const task = createLevinsODE(inputs); + const solution = mrt(task); + + return { + t: solution[0], + p: solution[1], + p_star: getEquilibrium(inputs.m, inputs.e0, inputs.rescueEffect), + }; +} + +// --- Optimization validation --- + +export interface OptimizeInputs { + m_min: number; + m_max: number; +} + +export type OptInputId = 'dlg_m_min' | 'dlg_m_max'; +export type OptValidationErrors = Map; + +export function validateOptimize( + opt: OptimizeInputs, e0: number, rescueEffect: boolean, +): {errors: OptValidationErrors; warning: string | null} { + const errors: OptValidationErrors = new Map(); + let warning: string | null = null; + + if (opt.m_min <= 0) + errors.set('dlg_m_min', 'Colonization rate must be positive'); + + if (opt.m_max <= 0) + errors.set('dlg_m_max', 'Colonization rate must be positive'); + + if (!errors.has('dlg_m_min') && !errors.has('dlg_m_max') && opt.m_min >= opt.m_max) + errors.set('dlg_m_min', 'Minimum value must be less than maximum'); + + if (errors.size === 0 && !rescueEffect && opt.m_max <= e0) + warning = 'With current e₀ the entire m range leads to extinction (m ≤ e₀). Increase the maximum or decrease e₀'; + + return {errors, warning}; +} + +// --- Worker message types --- + +export interface WorkerTask { + m_i: number; + p0: number; + e0: number; + rescueEffect: boolean; + t_start: number; + t_end: number; + t_step: number; + tolerance: number; +} + +export interface WorkerResult { + m_i: number; + p_end: number; + error?: string; +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/model.ts b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/model.ts new file mode 100644 index 0000000000..6a0a27f3db --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/model.ts @@ -0,0 +1,37 @@ +// Levins Metapopulation Model — ODE specification + +import {ODEs} from 'diff-grok'; + +/** Parameters for the Levins metapopulation ODE */ +export interface LevinsParams { + p0: number; + m: number; + e0: number; + rescueEffect: boolean; + t_start: number; + t_end: number; + t_step: number; + tolerance: number; +} + +/** Creates the ODEs specification for the Levins model, usable in both main thread and workers */ +export function createLevinsODE(params: LevinsParams): ODEs { + const {p0, m, e0, rescueEffect, t_start, t_end, t_step, tolerance} = params; + + return { + name: 'Levins', + arg: {name: 't', start: t_start, finish: t_end, step: t_step}, + initial: [p0], + func: (_t: number, y: Float64Array, out: Float64Array) => { + const e = rescueEffect ? e0 * (1 - y[0]) : e0; + out[0] = m * y[0] * (1 - y[0]) - e * y[0]; + }, + tolerance: tolerance, + solutionColNames: ['p(t)'], + }; +} + +/** Computes the analytical equilibrium p* for the base Levins model */ +export function getEquilibrium(m: number, e0: number, rescueEffect: boolean): number { + return rescueEffect ? NaN : Math.max(0, 1 - e0 / m); +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/optimize-worker.ts b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/optimize-worker.ts new file mode 100644 index 0000000000..3a6282eb86 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/levins/optimize-worker.ts @@ -0,0 +1,43 @@ +// Levins Metapopulation Model — Web Worker for optimization task +// Uses mrt from diff-grok via the shared model definition + +import {mrt} from 'diff-grok'; + +import {createLevinsODE} from './model'; +import {WorkerTask, WorkerResult} from './core'; + +const ctx: Worker = self as unknown as Worker; + +ctx.onmessage = (event: MessageEvent) => { + const tasks = event.data; + const results: WorkerResult[] = []; + + for (const task of tasks) { + try { + const ode = createLevinsODE({ + p0: task.p0, + m: task.m_i, + e0: task.e0, + rescueEffect: task.rescueEffect, + t_start: task.t_start, + t_end: task.t_end, + t_step: task.t_step, + tolerance: task.tolerance, + }); + + const solution = mrt(ode); + const pValues = solution[1]; + const p_end = pValues[pValues.length - 1]; + + results.push({m_i: task.m_i, p_end}); + } catch (err) { + results.push({ + m_i: task.m_i, + p_end: -1, + error: err instanceof Error ? err.message : 'Unknown error', + }); + } + } + + ctx.postMessage(results); +}; diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package-api.ts b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package-api.ts new file mode 100644 index 0000000000..152e9543db --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package-api.ts @@ -0,0 +1,21 @@ +/** +This file is auto-generated by the grok api command. +If you notice any changes, please push them to the repository. +Do not edit this file manually. +*/ +import * as grok from 'datagrok-api/grok'; +import * as DG from 'datagrok-api/dg'; + + +export namespace funcs { + export async function info(): Promise { + return await grok.functions.call('InteractiveSciAppTest:Info', {}); + } + + /** + Interactive ODE solver for the Levins model: simulation of occupied patch fraction p(t) dynamics + */ + export async function levinsMetapopulationModelApp(): Promise { + return await grok.functions.call('InteractiveSciAppTest:LevinsMetapopulationModelApp', {}); + } +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package-test.ts b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package-test.ts new file mode 100644 index 0000000000..15255df649 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package-test.ts @@ -0,0 +1,23 @@ +import { runTests, tests, TestContext , initAutoTests as initTests } from '@datagrok-libraries/utils/src/test'; +import * as DG from 'datagrok-api/dg'; + +import './tests/levins-api-tests'; +import './tests/levins-math-tests'; + +export let _package = new DG.Package(); +export { tests }; + +//name: test +//input: string category {optional: true} +//input: string test {optional: true} +//input: object testContext {optional: true} +//output: dataframe result +export async function test(category: string, test: string, testContext: TestContext): Promise { + const data = await runTests({ category, test, testContext }); + return DG.DataFrame.fromObjects(data)!; +} + +//name: initAutoTests +export async function initAutoTests() { + await initTests(_package, _package.getModule('package-test.js')); +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package.g.ts b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package.g.ts new file mode 100644 index 0000000000..8de619387a --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package.g.ts @@ -0,0 +1 @@ +import * as DG from 'datagrok-api/dg'; diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package.ts b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package.ts new file mode 100644 index 0000000000..93aec6ce82 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/package.ts @@ -0,0 +1,22 @@ +/* Do not change these import lines to match external modules in webpack configuration */ +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; +export * from './package.g'; + +import {levinsMetapopulationApp, setPackage} from './levins/app'; + +export const _package = new DG.Package(); + +//name: info +export function info() { + grok.shell.info(_package.webRoot); +} + +//name: Levins Metapopulation Model +//tags: app +//description: Interactive ODE solver for the Levins model: simulation of occupied patch fraction p(t) dynamics +export function levinsMetapopulationModelApp(): void { + setPackage(_package); + levinsMetapopulationApp(); +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/tests/levins-api-tests.ts b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/tests/levins-api-tests.ts new file mode 100644 index 0000000000..d6db9d2b21 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/tests/levins-api-tests.ts @@ -0,0 +1,191 @@ +// Levins Metapopulation Model — API tests + +import {category, test, expect} from '@datagrok-libraries/utils/src/test'; + +import {DEFAULTS, validate, validateOptimize} from '../levins/core'; + +category('API: Validation', () => { + // --- val_01: p0 <= 0 --- + test('val_01: p0 = 0', async () => { + const errors = validate({...DEFAULTS, p0: 0}); + expect(errors.has('ctrl_p0'), true, 'Should reject p0 = 0'); + }); + + test('val_01: p0 = -1', async () => { + const errors = validate({...DEFAULTS, p0: -1}); + expect(errors.has('ctrl_p0'), true, 'Should reject p0 < 0'); + }); + + // --- val_02: p0 > 1 --- + test('val_02: p0 = 1.1', async () => { + const errors = validate({...DEFAULTS, p0: 1.1}); + expect(errors.has('ctrl_p0'), true, 'Should reject p0 > 1'); + }); + + // --- p0 valid boundary --- + test('p0 = 0.001 (valid boundary)', async () => { + const errors = validate({...DEFAULTS, p0: 0.001}); + expect(errors.has('ctrl_p0'), false, 'Should accept p0 = 0.001'); + }); + + test('p0 = 1 (valid boundary)', async () => { + const errors = validate({...DEFAULTS, p0: 1}); + expect(errors.has('ctrl_p0'), false, 'Should accept p0 = 1'); + }); + + // --- val_03: m <= 0 --- + test('val_03: m = 0', async () => { + const errors = validate({...DEFAULTS, m: 0}); + expect(errors.has('ctrl_m'), true, 'Should reject m = 0'); + }); + + test('val_03: m = -0.5', async () => { + const errors = validate({...DEFAULTS, m: -0.5}); + expect(errors.has('ctrl_m'), true, 'Should reject m < 0'); + }); + + // --- val_04: e0 <= 0 --- + test('val_04: e0 = 0', async () => { + const errors = validate({...DEFAULTS, e0: 0}); + expect(errors.has('ctrl_e0'), true, 'Should reject e0 = 0'); + }); + + test('val_04: e0 = -0.1', async () => { + const errors = validate({...DEFAULTS, e0: -0.1}); + expect(errors.has('ctrl_e0'), true, 'Should reject e0 < 0'); + }); + + // --- val_05: m <= e0 (no rescue) --- + test('val_05: m = e0, no rescue', async () => { + const errors = validate({...DEFAULTS, m: 0.5, e0: 0.5, rescueEffect: false}); + expect(errors.has('ctrl_m'), true, 'Should reject m = e0 without rescue'); + }); + + test('val_05: m < e0, no rescue', async () => { + const errors = validate({...DEFAULTS, m: 0.3, e0: 0.5, rescueEffect: false}); + expect(errors.has('ctrl_m'), true, 'Should reject m < e0 without rescue'); + }); + + test('val_05: m <= e0 with rescue (allowed)', async () => { + const errors = validate({...DEFAULTS, m: 0.3, e0: 0.5, rescueEffect: true}); + expect(errors.has('ctrl_m'), false, 'Should allow m <= e0 with rescue'); + }); + + test('val_05: skipped when val_03 fails', async () => { + const errors = validate({...DEFAULTS, m: 0, e0: 0.5, rescueEffect: false}); + expect(errors.get('ctrl_m'), 'Colonization rate must be positive'); + }); + + // --- val_06: t_end <= t_start --- + test('val_06: t_end = t_start', async () => { + const errors = validate({...DEFAULTS, t_start: 10, t_end: 10}); + expect(errors.has('ctrl_t_end'), true, 'Should reject t_end = t_start'); + expect(errors.has('ctrl_t_start'), true, 'Should set error on t_start too'); + }); + + test('val_06: t_end < t_start', async () => { + const errors = validate({...DEFAULTS, t_start: 10, t_end: 5}); + expect(errors.has('ctrl_t_end'), true, 'Should reject t_end < t_start'); + }); + + // --- val_07: t_step <= 0 --- + test('val_07: t_step = 0', async () => { + const errors = validate({...DEFAULTS, t_step: 0}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step = 0'); + }); + + test('val_07: t_step = -0.1', async () => { + const errors = validate({...DEFAULTS, t_step: -0.1}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step < 0'); + }); + + // --- val_08: t_step >= t_end - t_start --- + test('val_08: t_step = interval length', async () => { + const errors = validate({...DEFAULTS, t_start: 0, t_end: 50, t_step: 50}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step = interval length'); + }); + + test('val_08: t_step > interval length', async () => { + const errors = validate({...DEFAULTS, t_start: 0, t_end: 50, t_step: 100}); + expect(errors.has('ctrl_t_step'), true, 'Should reject t_step > interval length'); + }); + + test('val_08: skipped when val_07 fails', async () => { + const errors = validate({...DEFAULTS, t_step: -1}); + expect(errors.get('ctrl_t_step'), 'Step must be positive'); + }); + + // --- val_09: tolerance <= 0 --- + test('val_09: tolerance = 0', async () => { + const errors = validate({...DEFAULTS, tolerance: 0}); + expect(errors.has('ctrl_tolerance'), true, 'Should reject tolerance = 0'); + }); + + test('val_09: tolerance = -1e-7', async () => { + const errors = validate({...DEFAULTS, tolerance: -1e-7}); + expect(errors.has('ctrl_tolerance'), true, 'Should reject tolerance < 0'); + }); + + // --- valid defaults --- + test('Defaults pass validation', async () => { + const errors = validate(DEFAULTS); + expect(errors.size, 0, 'Default parameters should be valid'); + }); + + // --- multiple errors --- + test('Multiple simultaneous errors', async () => { + const errors = validate({...DEFAULTS, p0: 0, m: 0, e0: 0, t_step: 0, tolerance: 0}); + expect(errors.size >= 4, true, 'Should report multiple errors'); + expect(errors.has('ctrl_p0'), true); + expect(errors.has('ctrl_m'), true); + expect(errors.has('ctrl_e0'), true); + expect(errors.has('ctrl_t_step'), true); + expect(errors.has('ctrl_tolerance'), true); + }); +}); + +category('API: Optimization Validation', () => { + // --- opt_val_01: m_min <= 0 --- + test('opt_val_01: m_min = 0', async () => { + const {errors} = validateOptimize({m_min: 0, m_max: 1}, 0.2, false); + expect(errors.has('dlg_m_min'), true, 'Should reject m_min = 0'); + }); + + // --- opt_val_02: m_max <= 0 --- + test('opt_val_02: m_max = -1', async () => { + const {errors} = validateOptimize({m_min: 0.1, m_max: -1}, 0.2, false); + expect(errors.has('dlg_m_max'), true, 'Should reject m_max < 0'); + }); + + // --- opt_val_03: m_min >= m_max --- + test('opt_val_03: m_min = m_max', async () => { + const {errors} = validateOptimize({m_min: 0.5, m_max: 0.5}, 0.2, false); + expect(errors.has('dlg_m_min'), true, 'Should reject m_min = m_max'); + }); + + test('opt_val_03: m_min > m_max', async () => { + const {errors} = validateOptimize({m_min: 1.0, m_max: 0.5}, 0.2, false); + expect(errors.has('dlg_m_min'), true, 'Should reject m_min > m_max'); + }); + + // --- opt_val_04: warning when m_max <= e0 --- + test('opt_val_04: m_max <= e0, no rescue — warning', async () => { + const {errors, warning} = validateOptimize({m_min: 0.05, m_max: 0.1}, 0.2, false); + expect(errors.size, 0, 'Should not block'); + expect(warning !== null, true, 'Should produce warning'); + }); + + test('opt_val_04: m_max <= e0 with rescue — no warning', async () => { + const {errors, warning} = validateOptimize({m_min: 0.05, m_max: 0.1}, 0.2, true); + expect(errors.size, 0); + expect(warning, null, 'No warning with rescue effect'); + }); + + // --- valid --- + test('Valid optimization inputs', async () => { + const {errors, warning} = validateOptimize({m_min: 0.1, m_max: 1.0}, 0.2, false); + expect(errors.size, 0, 'Should pass'); + expect(warning, null, 'No warning'); + }); +}); + diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/tests/levins-math-tests.ts b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/tests/levins-math-tests.ts new file mode 100644 index 0000000000..a1c0cb397b --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/src/tests/levins-math-tests.ts @@ -0,0 +1,201 @@ +// Levins Metapopulation Model — Math tests + +import {category, test, expect, expectFloat} from '@datagrok-libraries/utils/src/test'; +import {mrt, ODEs} from 'diff-grok'; + +import {createLevinsODE, LevinsParams} from '../levins/model'; +import {DEFAULTS, solve, getEquilibrium} from '../levins/core'; + +// ── Helpers ── + +/** Max absolute error between numerical and exact solutions across all grid points */ +function getMaxError(odes: ODEs, exact: (t: number) => number): number { + const solution = mrt(odes); + const tArr = solution[0]; + const yArr = solution[1]; + let error = 0; + + for (let i = 0; i < tArr.length; i++) + error = Math.max(error, Math.abs(exact(tArr[i]) - yArr[i])); + + return error; +} + +/** Evaluates func at given p and returns dp/dt */ +function evalFunc(params: LevinsParams, p: number): number { + const ode = createLevinsODE(params); + const y = new Float64Array([p]); + const out = new Float64Array(1); + ode.func(0, y, out); + return out[0]; +} + +// ── Correctness: MRT solver ── + +const TINY = 0.1; + +category('Math: MRT solver', () => { + test('Non-stiff 1D: dy/dt = 4·exp(0.8t) − 0.5y', async () => { + // Reference: Chapra & Canale, p. 736 + const odes: ODEs = { + name: 'Non-stiff 1D', + arg: {name: 't', start: 0, finish: 4, step: 0.01}, + initial: [2], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = 4 * Math.exp(0.8 * _t) - 0.5 * y[0]; + }, + tolerance: 1e-6, + solutionColNames: ['y'], + }; + + const exact = (t: number) => + (4 / 1.3) * (Math.exp(0.8 * t) - Math.exp(-0.5 * t)) + 2 * Math.exp(-0.5 * t); + + const error = getMaxError(odes, exact); + expectFloat(error, 0, TINY); + }); + + test('Stiff 1D: dy/dt = −1000y + 3000 − 2000·exp(−t)', async () => { + // Reference: Chapra & Canale, p. 767 + const odes: ODEs = { + name: 'Stiff 1D', + arg: {name: 't', start: 0, finish: 4, step: 0.01}, + initial: [0], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = -1000 * y[0] + 3000 - 2000 * Math.exp(-_t); + }, + tolerance: 5e-7, + solutionColNames: ['y'], + }; + + const exact = (t: number) => + 3 - 0.998 * Math.exp(-1000 * t) - 2.002 * Math.exp(-t); + + const error = getMaxError(odes, exact); + expectFloat(error, 0, TINY); + }); + + test('Stiff 2D: VDPOL (van der Pol, µ=1000)', async () => { + // Reference: https://archimede.uniba.it/~testset/report/vdpol.pdf + const vdpol: ODEs = { + name: 'van der Pol', + arg: {name: 't', start: 0, finish: 2000, step: 0.1}, + initial: [-1, 1], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = y[1]; + out[1] = -y[0] + 1000 * (1 - y[0] * y[0]) * y[1]; + }, + tolerance: 1e-12, + solutionColNames: ['x1', 'x2'], + }; + + mrt(vdpol); + }, {benchmark: true, timeout: 2000}); +}); + +// ── Correctness: Levins func ── + +const BASE: LevinsParams = { + p0: 0.5, m: 0.5, e0: 0.2, rescueEffect: false, + t_start: 0, t_end: 50, t_step: 0.1, tolerance: 1e-7, +}; + +category('Math: Levins func', () => { + // dp/dt = m·p·(1−p) − e₀·p = 0.5·0.5·0.5 − 0.2·0.5 = 0.125 − 0.1 = 0.025 + test('func_01: base model, p=0.5', async () => { + expectFloat(evalFunc({...BASE, m: 0.5, e0: 0.2, rescueEffect: false}, 0.5), 0.025, 1e-12); + }); + + // dp/dt = 1.0·0.1·0.9 − 0.3·0.1 = 0.09 − 0.03 = 0.06 + test('func_02: base model, low p=0.1', async () => { + expectFloat(evalFunc({...BASE, m: 1.0, e0: 0.3, rescueEffect: false}, 0.1), 0.06, 1e-12); + }); + + // At equilibrium p*=1−e₀/m=0.6: dp/dt = 0.5·0.6·0.4 − 0.2·0.6 = 0.12 − 0.12 = 0.0 + test('func_03: equilibrium p*=0.6, dp/dt=0', async () => { + expectFloat(evalFunc({...BASE, m: 0.5, e0: 0.2, rescueEffect: false}, 0.6), 0.0, 1e-12); + }); + + // Rescue: e=e₀·(1−p)=0.2·0.5=0.1; dp/dt = 0.5·0.5·0.5 − 0.1·0.5 = 0.125 − 0.05 = 0.075 + test('func_04: rescue effect, p=0.5', async () => { + expectFloat(evalFunc({...BASE, m: 0.5, e0: 0.2, rescueEffect: true}, 0.5), 0.075, 1e-12); + }); + + // Rescue: e=0.5·(1−0.8)=0.1; dp/dt = 0.3·0.8·0.2 − 0.1·0.8 = 0.048 − 0.08 = −0.032 + test('func_05: rescue + decline, p=0.8', async () => { + expectFloat(evalFunc({...BASE, m: 0.3, e0: 0.5, rescueEffect: true}, 0.8), -0.032, 1e-12); + }); +}); + +// ── Correctness: Levins equilibrium ── + +category('Math: Equilibrium', () => { + test('p* = 1 - e0/m (base model)', async () => { + expectFloat(getEquilibrium(0.5, 0.2, false), 0.6, 1e-10); + }); + + test('p* = 0 when m <= e0', async () => { + expectFloat(getEquilibrium(0.2, 0.5, false), 0, 1e-10); + }); + + test('p* = 0 when m = e0', async () => { + expectFloat(getEquilibrium(0.5, 0.5, false), 0, 1e-10); + }); + + test('p* = NaN with rescue effect', async () => { + expect(isNaN(getEquilibrium(0.5, 0.2, true)), true, 'Should be NaN with rescue'); + }); +}); + +// ── Output property verification: solve ── + +category('Math: Solve output properties', () => { + test('solve_01: default parameters produce non-empty arrays of equal length', async () => { + const result = solve(DEFAULTS); + expect(result.t.length > 0, true, 't should be non-empty'); + expect(result.p.length > 0, true, 'p should be non-empty'); + expect(result.t.length, result.p.length, 't and p should have equal length'); + }); + + test('solve_02: p values in [0, 1]', async () => { + const result = solve(DEFAULTS); + for (let i = 0; i < result.p.length; i++) + expect(result.p[i] >= 0 && result.p[i] <= 1, true, `p[${i}] = ${result.p[i]} out of [0, 1]`); + }); + + test('solve_03: p(0) = p0', async () => { + const result = solve(DEFAULTS); + expectFloat(result.p[0], DEFAULTS.p0, 1e-6); + }); + + test('solve_04: t starts at t_start', async () => { + const result = solve(DEFAULTS); + expectFloat(result.t[0], DEFAULTS.t_start, 1e-12); + }); + + test('solve_05: convergence to p*', async () => { + const params = {...DEFAULTS, m: 0.5, e0: 0.2, rescueEffect: false, t_end: 200}; + const result = solve(params); + const pStar = getEquilibrium(params.m, params.e0, params.rescueEffect); + expectFloat(result.p[result.p.length - 1], pStar, 0.01); + }); + + test('solve_06: rescue effect — p in [0, 1]', async () => { + const params = {...DEFAULTS, m: 0.3, e0: 0.5, rescueEffect: true, t_end: 100}; + const result = solve(params); + for (let i = 0; i < result.p.length; i++) + expect(result.p[i] >= 0 && result.p[i] <= 1, true, `p[${i}] = ${result.p[i]} out of [0, 1]`); + }); + + test('solve_07: higher m → higher p(t_end)', async () => { + const r1 = solve({...DEFAULTS, m: 0.5, e0: 0.2}); + const r2 = solve({...DEFAULTS, m: 1.0, e0: 0.2}); + expect(r2.p[r2.p.length - 1] > r1.p[r1.p.length - 1], true, + 'p(t_end) with m=1.0 should exceed p(t_end) with m=0.5'); + }); + + test('solve_08: custom p0', async () => { + const result = solve({...DEFAULTS, p0: 0.9}); + expectFloat(result.p[0], 0.9, 1e-6); + }); +}); diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/tsconfig.json b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/tsconfig.json new file mode 100644 index 0000000000..b9b0997746 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/tsconfig.json @@ -0,0 +1,71 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "lib": ["ES2022", "dom"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/webpack.config.js b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/webpack.config.js new file mode 100644 index 0000000000..a06441f8ba --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/code/webpack.config.js @@ -0,0 +1,69 @@ +const path = require('path'); +const {execSync} = require('child_process'); +const packageName = path.parse(require('./package.json').name).name.toLowerCase().replace(/-/g, ''); + +function getDatagrokTools() { + const pluginPath = 'datagrok-tools/plugins/func-gen-plugin'; + try { + return require(pluginPath); + } catch (e) { + try { + const globalPath = execSync('npm root -g').toString().trim(); + return require(path.join(globalPath, pluginPath)); + } catch (globalErr) { + console.error('\n' + '='.repeat(60)); + console.error('ERROR: datagrok-tools not found!'); + console.error('To fix this, please install the tools globally by running:'); + console.error('\n npm install -g datagrok-tools\n'); + console.error('='.repeat(60) + '\n'); + process.exit(1); + } + } +} + +const FuncGeneratorPlugin = getDatagrokTools(); + +module.exports = { + cache: { + type: 'filesystem', + }, + mode: 'development', + entry: { + test: {filename: 'package-test.js', library: {type: 'var', name: `${packageName}_test`}, import: './src/package-test.ts'}, + package: './src/package.ts', + 'optimize-worker': {filename: 'optimize-worker.js', import: './src/levins/optimize-worker.ts'}, + }, + resolve: { + symlinks: false, + extensions: ['.wasm', '.mjs', '.ts', '.json', '.js', '.tsx'], + }, + module: { + rules: [ + {test: /\.tsx?$/, loader: 'ts-loader', options: {allowTsInNodeModules: true}}, + {test: /\.css$/i, use: ['style-loader', 'css-loader']}, + ], + }, + plugins: [ + new FuncGeneratorPlugin({outputPath: './src/package.g.ts'}), + ], + devtool: 'source-map', + externals: { + 'datagrok-api/dg': 'DG', + 'datagrok-api/grok': 'grok', + 'datagrok-api/ui': 'ui', + 'openchemlib/full.js': 'OCL', + 'rxjs': 'rxjs', + 'rxjs/operators': 'rxjs.operators', + 'cash-dom': '$', + 'dayjs': 'dayjs', + 'wu': 'wu', + 'exceljs': 'ExcelJS', + 'html2canvas': 'html2canvas', + }, + output: { + filename: '[name].js', + library: packageName, + libraryTarget: 'var', + path: path.resolve(__dirname, 'dist'), + }, +}; diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/levins-metapopulation-spec.md b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/levins-metapopulation-spec.md new file mode 100644 index 0000000000..37673a36b7 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/example/levins-metapopulation-spec.md @@ -0,0 +1,702 @@ +# Application Specification: Levins Metapopulation Model + +## 1. General Information + +| Field | Value | +|---|---| +| Application name | Levins Metapopulation Model | +| Package | InteractiveSciAppTest | +| Entry function | `levinsMetapopulationApp()` | +| Brief description | Interactive ODE solution for the Levins model: simulation of occupied patch fraction dynamics p(t) with support for the basic model and the extended model (rescue effect). | +| Main view | `DG.TableView` | + +--- + +## 2. Computational Tasks (Core) + +### 2.1. Task List + +| Task ID | Name | Pipeline type | Trigger | +|---|---|---|---| +| `task_primary` | Levins model ODE solution | Primary (reactive) | Any input change | +| `task_optimize` | Find optimal m by p(t_end) | Secondary (on demand) | Button `btn_optimize` | + +### 2.2. Task Description: `task_primary` + +**General characteristics:** + +| Field | Value | +|---|---| +| Name | Levins model ODE solution | +| Description | Numerical solution of ODE `dp/dt = m·p·(1−p) − e(p)·p`, returning the trajectory p(t) and the equilibrium value p* | +| Synchronicity | Synchronous | +| Execution environment | Main thread | +| Parallelization | No | +| Dependency on other tasks | No | + +**Input parameters:** + +| Parameter | Type | Units | Domain | Description | +|---|---|---|---|---| +| `p0` | `number` | dimensionless | `(0, 1]` | Initial fraction of occupied patches | +| `m` | `number` | 1/time | `> 0` | Colonization rate | +| `e0` | `number` | 1/time | `> 0` | Baseline extinction rate | +| `rescueEffect` | `boolean` | — | — | Enable rescue effect: `e(p) = e0·(1−p)` | +| `t_start` | `number` | time | `≥ 0` | Start of integration interval | +| `t_end` | `number` | time | `> t_start` | End of interval | +| `t_step` | `number` | time | `> 0, < t_end − t_start` | Grid step | +| `tolerance` | `number` | dimensionless | `> 0` | Numerical tolerance of the MRT method | + +**Output data:** + +| Parameter | Type | Description | +|---|---|---| +| `t` | `Float64Array` | Values of argument t | +| `p` | `Float64Array` | Values of p(t) | +| `p_star` | `number` | Equilibrium value: `1 − e0/m` (for the basic model) | + +**Output properties (invariants):** + +- `p(t) ∈ [0, 1]` for all `t` — the fraction of occupied patches is bounded. +- `p(0) = p0` — the initial condition is preserved. +- For the basic model with `m > e0`: `p(t) → p* = 1 − e0/m` as `t → ∞` — convergence to equilibrium. +- Higher `m` (other parameters fixed) → higher `p(t_end)` — monotonicity in colonization rate. + +**Computation implementation:** + +| Step | Implementation method | Details | Documentation | +|---|---|---|---| +| 1 | External library | `diff-grok` v1.2.0, function `mrt(task: ODEs)`. MRT is an A-stable implicit method suitable for both stiff and non-stiff ODEs. | [diff-grok README](https://github.com/datagrok-ai/diff-grok) | +| 2 | Custom method | Computation of `p_star = 1 − e0/m` (basic model); for rescue effect (`e(p) = e0·(1−p)`), no closed-form equilibrium exists — `p_star` is not computed | — | + +**ODE right-hand side reference examples:** + +| # | Mode | m | e0 | p | Hand-calculated dp/dt | Derivation | +|---|---|---|---|---|---|---| +| 1 | Base | 0.5 | 0.2 | 0.5 | `0.5·0.5·0.5 − 0.2·0.5 = 0.025` | `m·p·(1−p) − e0·p` | +| 2 | Base | 1.0 | 0.3 | 0.1 | `1.0·0.1·0.9 − 0.3·0.1 = 0.06` | `m·p·(1−p) − e0·p` | +| 3 | Base | 0.5 | 0.2 | 0.6 | `0.5·0.6·0.4 − 0.2·0.6 = 0.0` | At equilibrium `p* = 1 − e0/m = 0.6` | +| 4 | Rescue | 0.5 | 0.2 | 0.5 | `0.5·0.5·0.5 − 0.2·0.5·0.5 = 0.075` | `e = e0·(1−p) = 0.1` | +| 5 | Rescue | 0.3 | 0.5 | 0.8 | `0.3·0.8·0.2 − 0.5·0.2·0.8 = −0.032` | `e = e0·(1−p) = 0.1` | + +**Library call:** + +```typescript +import { ODEs, mrt } from 'diff-grok'; + +const task: ODEs = { + name: 'Levins', + arg: { name: 't', start: t_start, finish: t_end, step: t_step }, + initial: [p0], + func: (t, y, out) => { + const e = rescueEffect ? e0 * (1 - y[0]) : e0; + out[0] = m * y[0] * (1 - y[0]) - e * y[0]; + }, + tolerance: tolerance, + solutionColNames: ['p(t)'], +}; +const solution = mrt(task); +``` + +--- + +### 2.3. Task Description: `task_optimize` + +**General characteristics:** + +| Field | Value | +|---|---| +| Name | Find optimal m by p(t_end) | +| Description | For 10000 uniformly distributed values of m in [min, max], solves the ODE, computes p(t_end), returns m with the maximum p(t_end) | +| Synchronicity | Asynchronous | +| Execution environment | WebWorkers (parallel) | +| Parallelization | Yes — 10000 independent tasks, worker pool of size `Math.max(1, navigator.hardwareConcurrency - 2)` | +| Dependency on other tasks | Uses current values of all `task_primary` inputs except `m` | + +**Input parameters:** + +| Parameter | Type | Description | +|---|---|---| +| `m_min` | `number` | Lower bound for m search | +| `m_max` | `number` | Upper bound for m search | +| `p0, e0, rescueEffect` | `number / boolean` | Taken from current state of main UI controls (snapshot) | +| `t_start, t_end, t_step, tolerance` | `number` | Taken from current state of main UI controls (snapshot) | + +**Output data:** + +| Parameter | Type | Description | +|---|---|---| +| `m_optimal` | `number` | Value of m at which p(t_end) is maximized | +| `p_at_t_end_max` | `number` | Achieved maximum value of p(t_end) | + +**Computation implementation:** + +| Step | Implementation method | Details | Documentation | +|---|---|---|---| +| 1 | Custom method | Generate 10000 points: `m_i = m_min + i · (m_max − m_min) / 9999`, i = 0..9999 | — | +| 2 | External library in workers | `diff-grok` v1.2.0, `mrt(task)` — for each point `m_i` in a WebWorker. Passed through pipeline API: `getIvp2WebWorker(ivp)` | [diff-grok pipeline](https://github.com/datagrok-ai/diff-grok) | +| 3 | Custom method | After receiving all results: `m_optimal = m_i` where `p_end` is maximal | — | +| 4 | Datagrok API | Write `m_optimal` to `ctrl_m` via batch update (section 7) | [Datagrok JS API](https://datagrok.ai/api/js/) | + +**Parallelization strategy:** + +``` +Number of workers = Math.max(1, navigator.hardwareConcurrency - 2) + +10000 values of m_i + → worker pool + → tasks are distributed to workers as they become available (queue) + → each worker receives: { m_i, p0, e0, rescueEffect, + t_start, t_end, t_step, tolerance } + → each worker returns: { m_i, p_end } + → as each task completes: progressBar += 1/10000 + → after all 10000: find max(p_end) → m_optimal +``` + +### 2.4. Dependencies Between Tasks + +``` +task_primary — independent +task_optimize — does not depend on task_primary results; + after completion, triggers a single run of task_primary +``` + +--- + +## 3. Controls + +### 3.1. Primary Pipeline Controls + +| ID | Name (label) | Control type | Data type | Default | Min | Max | Format | Nullable | Tooltip text | Group | +|---|---|---|---|---|---|---|---|---|---|---| +| `ctrl_p0` | Initial patch fraction p₀ | `ui.input.float` | `number` | `0.5` | `0.001` | `1` | `0.000` | No | Fraction of patches occupied at time t=0. At p₀=0 the population will not recover — computation is not performed. | Initial condition | +| `ctrl_m` | Colonization rate m | `ui.input.float` | `number` | `0.5` | `0.001` | `100` | `0.000` | No | How quickly free patches are colonized from occupied ones. Units: 1/time. | Parameters | +| `ctrl_e0` | Extinction rate e₀ | `ui.input.float` | `number` | `0.2` | `0.001` | `100` | `0.000` | No | Baseline rate of local subpopulation extinction in a patch. With rescue effect — decreases as p grows. Units: 1/time. | Parameters | +| `ctrl_rescue` | Rescue effect | `ui.input.toggle` | `boolean` | `false` | — | — | — | No | If enabled, the extinction rate depends on p: e(p) = e₀·(1−p). The more occupied patches — the lower the local extinction. | Parameters | +| `ctrl_t_start` | Start t₀ | `ui.input.float` | `number` | `0` | `0` | `10000` | `0.0` | No | Simulation start time. Usually 0. | Argument | +| `ctrl_t_end` | End t_end | `ui.input.float` | `number` | `50` | `0.1` | `10000` | `0.0` | No | End time. Recommended ≥ 5/e₀ so the system reaches equilibrium. | Argument | +| `ctrl_t_step` | Step Δt | `ui.input.float` | `number` | `0.1` | `0.001` | `1000` | `0.000` | No | Numerical solution grid step. Affects chart detail but not stability (MRT is an implicit method). | Argument | +| `ctrl_tolerance` | Tolerance | `ui.input.float` | `number` | `1e-7` | `1e-12` | `1e-2` | `0.##E+0` | No | Numerical tolerance of the MRT method. Smaller — more precise, but slower. Recommended 1e-6 … 1e-9. | Solver | + +### 3.2. Secondary Task Triggers + +| ID | Name / icon | Triggers task | Tooltip text | Availability condition | +|---|---|---|---|---| +| `btn_optimize` | `ui.iconFA('search')` | `task_optimize` | Find the value of m that maximizes the fraction of occupied patches at time t_end | Always | + +### 3.3. Controls for `task_optimize` + +UI type: Datagrok dialog window. + +| ID | Name (label) | Control type | Data type | Default | Min | Max | Format | Nullable | Tooltip text | +|---|---|---|---|---|---|---|---|---|---| +| `dlg_m_min` | Minimum m | `ui.input.float` | `number` | `0.1` | `0.001` | `100` | `0.000` | No | Lower bound of the m search range. Must be less than the maximum value. | +| `dlg_m_max` | Maximum m | `ui.input.float` | `number` | `1.0` | `0.001` | `100` | `0.000` | No | Upper bound of the m search range. Must be greater than the minimum value. | + +**Dialog buttons:** + +| ID | Name | Action | Availability condition | +|---|---|---|---| +| `dlg_btn_ok` | OK | Close dialog → launch `task_optimize` | Only if there are no validation errors | +| `dlg_btn_cancel` | Cancel | Close dialog, task is not launched | Always | + +### 3.4. Other Buttons and Actions + +| ID | Name / icon | Action | Tooltip text | Availability condition | +|---|---|---|---|---| +| `btn_reset` | `ui.iconFA('undo')` | Reset all controls to default values | Reset all parameters to default values | Always | + +### 3.5. Custom UI Components + +| Component ID | Brief description | Role | UI component specification | +|---|---|---|---| +| `comp_rho_badge` | Indicator for ρ = e₀/m with color status (green / red) | Display | — | + +> **Note on `comp_rho_badge`:** displays the current value of ρ = e₀/m in real time. Green — the metapopulation persists (ρ < 1), red — heading toward extinction (ρ ≥ 1). Updated reactively when `ctrl_m` or `ctrl_e0` changes. +> +> **Tooltip:** "Ratio of extinction rate to colonization rate. ρ < 1 — metapopulation persists, ρ ≥ 1 — extinction." + +--- + +## 4. Validation + +### 4.1. Primary Pipeline Validation (`task_primary`) + +#### Complex Validation Rules + +| Rule ID | Condition (invalid) | Affected inputs (ID) | Error message | +|---|---|---|---| +| `val_01` | `p0 ≤ 0` | `ctrl_p0` | "Initial patch fraction must be greater than 0" | +| `val_02` | `p0 > 1` | `ctrl_p0` | "Initial patch fraction cannot exceed 1" | +| `val_03` | `m ≤ 0` | `ctrl_m` | "Colonization rate must be positive" | +| `val_04` | `e0 ≤ 0` | `ctrl_e0` | "Extinction rate must be positive" | +| `val_05` | `m ≤ e0` (when `rescueEffect = false`) | `ctrl_m`, `ctrl_e0` | "Colonization rate must exceed extinction rate (m > e₀). At current values, the metapopulation is heading toward extinction" | +| `val_06` | `t_end ≤ t_start` | `ctrl_t_end`, `ctrl_t_start` | "End of interval must be greater than start" | +| `val_07` | `t_step ≤ 0` | `ctrl_t_step` | "Step must be positive" | +| `val_08` | `t_step ≥ t_end − t_start` | `ctrl_t_step` | "Step must be less than the interval length" | +| `val_09` | `tolerance ≤ 0` | `ctrl_tolerance` | "Tolerance must be positive" | + +#### Validation Order + +``` +1. val_01, val_02 (single, ctrl_p0) +2. val_03 (single, ctrl_m) +3. val_04 (single, ctrl_e0) +4. val_05 (combinatorial ctrl_m + ctrl_e0 — only if val_03 and val_04 passed) +5. val_06 (combinatorial ctrl_t_start + ctrl_t_end) +6. val_07 (single, ctrl_t_step) +7. val_08 (combinatorial ctrl_t_step + ctx — only if val_06 and val_07 passed) +8. val_09 (single, ctrl_tolerance) +``` + +#### Return Map Format + +``` +Map +``` + +### 4.2. Validation for `task_optimize` + +#### Complex Validation Rules + +| Rule ID | Condition (invalid) | Affected inputs (ID) | Error message | +|---|---|---|---| +| `opt_val_01` | `m_min ≤ 0` | `dlg_m_min` | "Colonization rate must be positive" | +| `opt_val_02` | `m_max ≤ 0` | `dlg_m_max` | "Colonization rate must be positive" | +| `opt_val_03` | `m_min ≥ m_max` | `dlg_m_min`, `dlg_m_max` | "Minimum value must be less than maximum" | +| `opt_val_04` | `m_max ≤ e0` (when `rescueEffect = false`) | `dlg_m_max` | "At the current e₀, the entire m range leads to extinction (m ≤ e₀). Increase the maximum or decrease e₀" | + +> `opt_val_04` — warning, does not block OK. + +#### Validation Order + +``` +1. opt_val_01 (single) +2. opt_val_02 (single) +3. opt_val_03 (combinatorial — only if 01 and 02 passed) +4. opt_val_04 (combinatorial with external parameter e₀ — only if 01-03 passed) +``` + +--- + +## 5. Reactivity and Input Dependencies + +### 5.1. Dependency Graph + +| Source (input ID) | Target (input IDs / components) | Reaction type | Logic | +|---|---|---|---| +| `ctrl_m` | `comp_rho_badge` | Display update | Recalculate ρ = e₀ / m, update badge value and color | +| `ctrl_e0` | `comp_rho_badge` | Display update | Same | +| `ctrl_m` | `comp_rho_badge` | Availability update | If m ≤ e₀ → badge is red, otherwise green | +| `ctrl_e0` | `comp_rho_badge` | Availability update | Same | +| `ctrl_rescue` | `ctrl_e0` (label + tooltip) | Label update | If `rescue = true` → label changes to "Baseline extinction rate e₀", tooltip adds: "Effective rate: e(p) = e₀·(1−p)" | +| `ctrl_t_start` | `ctrl_t_end` | Range update | Minimum allowed value of `ctrl_t_end` = `t_start + t_step` | +| `ctrl_t_start` | `ctrl_t_step` | Range update | Maximum allowed value of `ctrl_t_step` = `t_end − t_start` | +| `ctrl_t_end` | `ctrl_t_step` | Range update | Same: `ctrl_t_step` ≤ `t_end − t_start` | +| `dlg_m_min` | `dlg_m_max` | Range update | Minimum allowed value of `dlg_m_max` = `m_min + ε` | +| `dlg_m_max` | `dlg_m_min` | Range update | Maximum allowed value of `dlg_m_min` = `m_max − ε` | +| `dlg_m_min` | `dlg_btn_ok` | Availability update | OK button is available only if there are no validation errors `opt_val_01–03` | +| `dlg_m_max` | `dlg_btn_ok` | Availability update | Same | + +### 5.2. Debounce / Throttle + +| Input ID | Strategy | Interval (ms) | +|---|---|---| +| `ctrl_p0` | debounce | 50 | +| `ctrl_m` | debounce | 50 | +| `ctrl_e0` | debounce | 50 | +| `ctrl_t_start` | debounce | 50 | +| `ctrl_t_end` | debounce | 50 | +| `ctrl_t_step` | debounce | 50 | +| `ctrl_tolerance` | debounce | 50 | +| `ctrl_rescue` | none | — | +| `dlg_m_min` | debounce | 50 | +| `dlg_m_max` | debounce | 50 | + +--- + +## 6. Behavior During Computations + +### 6.1. Primary Pipeline (`task_primary`) + +#### Control Blocking + +| Control ID | Blocked | Note | +|---|---|---| +| `ctrl_p0` | No | Computation takes < 100 ms — blocking is unnecessary | +| `ctrl_m` | No | Same | +| `ctrl_e0` | No | Same | +| `ctrl_rescue` | No | Same | +| `ctrl_t_start` | No | Same | +| `ctrl_t_end` | No | Same | +| `ctrl_t_step` | No | Same | +| `ctrl_tolerance` | No | Same | +| `btn_optimize` | No | — | +| `btn_reset` | No | — | + +#### Progress Bar + +| Field | Value | +|---|---| +| Display | No | +| Type | — | +| Cancellation support | No | + +#### Error Behavior + +Selected strategy: **Reset results + message**. + +| Strategy | Description | +|---|---| +| Reset results | Clear the p(t) chart and the p* value | +| Message | `grok.shell.error(msg)` — Datagrok platform toast notification with the error text | + +### 6.2. Secondary Pipeline (`task_optimize`) + +#### Control Blocking + +| Control ID | Blocked | Note | +|---|---|---| +| `btn_optimize` | Yes | Re-launch is not possible until completion or cancellation | +| All other controls | No | The main UI remains fully accessible | + +> Changing inputs while `task_optimize` is running triggers a `task_primary` recalculation in normal mode but does not affect the already running search — it uses the parameter snapshot from the moment OK was pressed. + +#### Progress Bar + +| Field | Value | +|---|---| +| Display | Yes | +| Type | Determinate (0–100%, +1/10000 for each completed worker) | +| Cancellation support | Yes — Cancel button terminates all active workers | + +#### Error Behavior + +Selected strategy: **Last valid + message**. + +| Strategy | Description | +|---|---| +| Last valid | The value of `ctrl_m` is not changed | +| Message | `grok.shell.error` with error text | + +--- + +## 7. Computation Blocking and Batch Update + +### 7.1. Batch Update Scenarios + +| Source (task) | Target controls (ID) | Locked pipelines | +|---|---|---| +| `task_optimize` | `ctrl_m` | Primary (blocked) | + +### 7.2. Reactivity Mode During Batch Update + +| Scenario | Reactivity mode | +|---|---| +| Writing `m_optimal` → `ctrl_m` | Primary pipeline is paused during writing, then runs once with the new value | + +--- + +## 8. Result Display + +### 8.1. Primary Pipeline Display Elements + +| ID | Type | Associated output data | Docking location | +|---|---|---|---| +| `view_p_t` | Datagrok viewer `line chart` | `task_primary.t`, `task_primary.p` | Main area | +| `view_rho_badge` | Custom HTMLElement `comp_rho_badge` | `ctrl_e0`, `ctrl_m` (displays ρ = e₀/m) | Left panel, below "Parameters" group controls | + +**Details for `view_p_t`:** + +| Property | Value | +|---|---| +| X axis | `t`, label "Time" | +| Y axis | `p(t)`, range `[0, 1]`, label "Fraction of occupied patches" | +| Series | `p(t)` — main trajectory, solid line | +| Update | Reactive — redrawn on each `task_primary` completion | + +**Color coding:** + +The `p` column of the results table receives `colorCoding` by value: + +| Range | Color | Meaning | +|---|---|---| +| `p < e₀/m` | Red | Extinction threat zone | +| `p ≥ e₀/m` | Green | Persistence zone | + +> The threshold value `e₀/m` is recalculated and updated in `colorCoding` on each change of `ctrl_m` or `ctrl_e0`. + +**Tooltip for `p` column header:** + +The `p` column header in the grid receives a tooltip explaining the color coding: + +``` +Fraction of occupied patches p(t). +Color: green — persistence zone (p ≥ e₀/m), +red — extinction threat zone (p < e₀/m). +Threshold: e₀/m = {current ρ value}. +``` + +> The tooltip text is updated reactively when `ctrl_m` or `ctrl_e0` changes. + +### 8.2. Display Elements for `task_optimize` + +Where results are displayed: toast notification + writing to the main control. + +| ID | Type | Associated output data | Placement | +|---|---|---|---| +| `view_optimize_result` | `grok.shell.info` | `task_optimize.m_optimal`, `task_optimize.p_at_t_end_max` | Datagrok platform toast notification | +| `ctrl_m` | `ui.input.float` (main UI control) | `task_optimize.m_optimal` | Left panel, "Parameters" group | + +**Toast notification text:** +``` +Optimal m = {m_optimal} +p(t_end) = {p_at_t_end_max} +``` + +**Behavior after writing the result:** + +``` +1. m_optimal → ctrl_m (batch update, section 7) +2. task_primary runs once with the new m +3. view_p_t is redrawn with the new trajectory +4. grok.shell.info is shown +``` + +--- + +## 9. Layout + +### 9.1. Control Placement + +| Area | Content | Docking | Ratio | +|---|---|---|---| +| Left panel | `ui.form` with groups separated by `ui.h2` headers | `DG.DOCK_TYPE.LEFT` | `0.2` | +| Toolbar | `btn_optimize`, `btn_reset` | — | — | +| Main area (grid) | `DG.TableView` (results table) | Default | — | +| Right area | `view_p_t` (line chart) | `DG.DOCK_TYPE.RIGHT` (relative to grid) | `0.5` | + +**Structure of `ui.form` in the left panel:** + +``` +ui.h2('Initial condition') + ctrl_p0 + +ui.h2('Parameters') + ctrl_m + ctrl_e0 + ctrl_rescue + comp_rho_badge + +ui.h2('Argument') + ctrl_t_start + ctrl_t_end + ctrl_t_step + +ui.h2('Solver') + ctrl_tolerance +``` + +### 9.2. Display Element Placement + +| Element ID | Type | Area | Note | +|---|---|---|---| +| `view_p_t` | Viewer `line chart` | Right area (dock right, ratio `0.5`) | Docked to the right of the grid, splitting space 50/50 | +| `view_optimize_result` | `grok.shell.info` | — | Datagrok platform toast, placed automatically by the platform | + +--- + +## 10. Data Lifecycle + +### 10.1. Data Input + +Primary method: manual input via `ui.form` controls (section 3). + +Initial application state: all controls are initialized with default values from section 3.1 at the time `levinsMetapopulationApp()` is called. `task_primary` runs automatically immediately after initialization. + +### 10.2. Loading from Resources + +External data loading is not supported. + +| Trigger | Resource | Format | Mapping to inputs | +|---|---|---|---| +| — | — | — | — | + +### 10.3. Results Table Lifecycle + +``` +1. Application initialization + → an empty DG.DataFrame is created with columns [t, p] + → the DataFrame is added to the TableView + +2. task_primary completion + → the DataFrame is updated: columns [t, p] are overwritten with new Float64Array + → colorCoding for column p is recalculated (threshold e₀/m) + → view_p_t is redrawn reactively + +3. task_optimize completion + → m_optimal is written to ctrl_m (batch update, section 7) + → task_primary runs once → DataFrame is updated per step 2 + → grok.shell.info is shown + +4. btn_reset press + → all controls are reset to default values + → task_primary runs → DataFrame is updated per step 2 + +5. task_primary error + → the DataFrame is cleared (columns [t, p] are zeroed out) + → view_p_t displays an empty chart + → grok.shell.error(msg) shows a toast notification with the error text +``` + +### 10.4. Data Lifecycle for `task_optimize` + +``` +1. Pressing OK in the dialog + → a snapshot of current parameters { p0, e0, rescueEffect, + t_start, t_end, t_step, tolerance } is captured + → an array of 10000 m_i values is generated + +2. Worker execution + → each worker receives { m_i, snapshot } + → each worker returns { m_i, p_end } + → intermediate results are not stored anywhere + +3. All workers complete + → m_optimal = m_i at max(p_end) is computed + → the array { m_i, p_end } is freed from memory + +4. Cancellation (progress bar Cancel) + → all active workers are terminated + → intermediate results are discarded + → ctrl_m is not changed +``` + +--- + +## 11. Error Handling Beyond Computations + +| Error type | Strategy | Notification method | +|---|---|---| +| `diff-grok` initialization error (library failed to load) | Lock `btn_optimize` and all controls, show message | `grok.shell.error`: "Failed to load the solver library. Reload the page." | +| WebWorker creation error (browser does not support or limit exceeded) | Abort `task_optimize`, do not change `ctrl_m` | `grok.shell.error`: "Failed to start parallel computations. Try again later." | +| Worker terminated with error (one or more m_i points) | Skip the point, continue remaining workers, consider only valid results | `grok.shell.warning`: "{N} out of 10000 points were not computed. Result obtained from {10000−N} points." | +| All 10000 workers terminated with error | Abort `task_optimize`, do not change `ctrl_m` | `grok.shell.error`: "Failed to compute any points. Check the parameters." | +| Invalid application state (snapshot contains invalid values) | Abort `task_optimize` before creating workers | `grok.shell.error`: "Internal error: invalid task parameters. Check inputs and retry." | +| Error updating `ctrl_m` after `task_optimize` completion | Show result via `grok.shell.info`, do not write to control | `grok.shell.warning`: "Optimal m = {m_optimal}, but the field could not be updated automatically. Enter the value manually." | + +--- + +## 12. UX + +### 12.1. Keyboard Shortcuts + +| Combination | Action | +|---|---| +| — | — | + +### 12.2. Context Menus + +| Context (element) | Menu items | +|---|---| +| — | — | + +### 12.3. Undo / Redo + +Supported: **No**. + +--- + +## 13. Testing + +### 13.1. Mathematical Verification + +Tests in this section verify that the implementation matches the mathematical model. Verification criteria are defined by the model (section 2.2: ODE, output properties, reference examples). + +#### 13.1.1. ODE Right-Hand Side Verification (formula verification) + +Verifies that the implemented ODE function produces correct `dp/dt` at specific points. Reference values are hand-calculated (see section 2.2, "ODE right-hand side reference examples"). + +| Test ID | Mode | Input `(m, e0, p)` | Expected `dp/dt` | +|---|---|---|---| +| `func_01` | Base | `(0.5, 0.2, 0.5)` | `0.025` | +| `func_02` | Base | `(1.0, 0.3, 0.1)` | `0.06` | +| `func_03` | Base (equilibrium) | `(0.5, 0.2, 0.6)` | `0.0` | +| `func_04` | Rescue | `(0.5, 0.2, 0.5)` | `0.075` | +| `func_05` | Rescue | `(0.3, 0.5, 0.8)` | `−0.032` | + +#### 13.1.2. Equilibrium Verification + +| Test ID | Description | Input data | Expected result | +|---|---|---|---| +| `eq_01` | Basic model equilibrium | `m=0.5, e0=0.2, rescue=false` | `p* = 0.6` | +| `eq_02` | p* = 0 when m ≤ e0 | `m=0.2, e0=0.5, rescue=false` | `p* = 0` | +| `eq_03` | p* = 0 when m = e0 | `m=0.5, e0=0.5, rescue=false` | `p* = 0` | +| `eq_04` | NaN with rescue effect | `m=0.5, e0=0.2, rescue=true` | `NaN` | + +#### 13.1.3. Output Property Verification (`solve`) + +Verifies that output invariants declared in section 2.2 ("Output properties") hold on actual `solve()` results. + +| Test ID | Description | Input data | Verified property | +|---|---|---|---| +| `solve_01` | Default parameters | DEFAULTS | `t.length > 0`, `p.length > 0`, `t.length = p.length` | +| `solve_02` | p values in [0, 1] | DEFAULTS | All `p[i] ∈ [0, 1]` | +| `solve_03` | p(0) = p0 | DEFAULTS | `p[0] ≈ 0.5` | +| `solve_04` | t starts at t_start | DEFAULTS | `t[0] = 0` | +| `solve_05` | Convergence to p* | `m=0.5, e0=0.2, t_end=200` | `p(t_end) ≈ p*` (tolerance 0.01) | +| `solve_06` | Rescue effect: p in [0, 1] | `m=0.3, e0=0.5, rescue=true, t_end=100` | All `p[i] ∈ [0, 1]` | +| `solve_07` | Higher m → higher p(t_end) | `m=0.5` vs `m=1.0`, `e0=0.2` | `p_end(m=1) > p_end(m=0.5)` | +| `solve_08` | Custom p0 | `p0=0.9` | `p[0] ≈ 0.9` | + +#### 13.1.4. Numerical Method Verification (MRT solver) + +Verifies that the `mrt` solver from `diff-grok` produces correct results on reference problems with known analytical solutions. + +| Test ID | Description | Reference | Expected | +|---|---|---|---| +| `mrt_01` | Non-stiff 1D: `dy/dt = 4·exp(0.8t) − 0.5y` | Chapra & Canale, p. 736 | Max absolute error < 0.1 | +| `mrt_02` | Stiff 1D: `dy/dt = −1000y + 3000 − 2000·exp(−t)` | Chapra & Canale, p. 767 | Max absolute error < 0.1 | +| `mrt_03` | Stiff 2D: van der Pol (µ=1000) | [VDPOL test set](https://archimede.uniba.it/~testset/report/vdpol.pdf) | Solver completes without divergence | + +### 13.2. Validation for task_primary + +| Test ID | Rule | Input data | Expected result | +|---|---|---|---| +| `v_01a` | val_01 | `p0=0` | Error on `ctrl_p0` | +| `v_01b` | val_01 | `p0=-1` | Error on `ctrl_p0` | +| `v_02` | val_02 | `p0=1.1` | Error on `ctrl_p0` | +| `v_p0_lo` | boundary | `p0=0.001` | No error on `ctrl_p0` | +| `v_p0_hi` | boundary | `p0=1` | No error on `ctrl_p0` | +| `v_03a` | val_03 | `m=0` | Error on `ctrl_m` | +| `v_03b` | val_03 | `m=-0.5` | Error on `ctrl_m` | +| `v_04a` | val_04 | `e0=0` | Error on `ctrl_e0` | +| `v_04b` | val_04 | `e0=-0.1` | Error on `ctrl_e0` | +| `v_05a` | val_05 | `m=0.5, e0=0.5, rescue=false` | Error on `ctrl_m` | +| `v_05b` | val_05 | `m=0.3, e0=0.5, rescue=false` | Error on `ctrl_m` | +| `v_05c` | val_05 (rescue) | `m=0.3, e0=0.5, rescue=true` | No error | +| `v_05d` | val_05 dependency | `m=0, e0=0.5` | Message val_03, not val_05 | +| `v_06a` | val_06 | `t_start=10, t_end=10` | Error on `ctrl_t_end` and `ctrl_t_start` | +| `v_06b` | val_06 | `t_start=10, t_end=5` | Error on `ctrl_t_end` | +| `v_07a` | val_07 | `t_step=0` | Error on `ctrl_t_step` | +| `v_07b` | val_07 | `t_step=-0.1` | Error on `ctrl_t_step` | +| `v_08a` | val_08 | `t_step=50 (= t_end-t_start)` | Error on `ctrl_t_step` | +| `v_08b` | val_08 | `t_step=100 (> t_end-t_start)` | Error on `ctrl_t_step` | +| `v_08c` | val_08 dependency val_06 | `t_start=10, t_end=5, t_step=100` | No error on `ctrl_t_step` | +| `v_08d` | val_08 dependency val_07 | `t_step=-1` | Message val_07, not val_08 | +| `v_09a` | val_09 | `tolerance=0` | Error on `ctrl_tolerance` | +| `v_09b` | val_09 | `tolerance=-1e-7` | Error on `ctrl_tolerance` | +| `v_def` | all defaults | DEFAULTS | `errors.size = 0` | +| `v_multi` | multiple | `p0=0, m=0, e0=0, t_step=0, tolerance=0` | ≥ 4 errors | + +### 13.3. Validation for task_optimize + +| Test ID | Rule | Input data | Expected result | +|---|---|---|---| +| `ov_01` | opt_val_01 | `m_min=0` | Error on `dlg_m_min` | +| `ov_02` | opt_val_02 | `m_max=-1` | Error on `dlg_m_max` | +| `ov_03a` | opt_val_03 | `m_min=0.5, m_max=0.5` | Error on `dlg_m_min` | +| `ov_03b` | opt_val_03 | `m_min=1.0, m_max=0.5` | Error on `dlg_m_min` | +| `ov_04a` | opt_val_04 | `m_max=0.1, e0=0.2, rescue=false` | Warning ≠ null | +| `ov_04b` | opt_val_04 (rescue) | `m_max=0.1, e0=0.2, rescue=true` | Warning = null | +| `ov_valid` | valid | `m_min=0.1, m_max=1.0, e0=0.2` | `errors.size = 0`, `warning = null` | diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/ARRAY-OPERATIONS.md b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/ARRAY-OPERATIONS.md new file mode 100644 index 0000000000..abbcc6e003 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/ARRAY-OPERATIONS.md @@ -0,0 +1,305 @@ +# Array Operations Guide + +Reference for implementing efficient array operations in Datagrok packages. + +> **End-to-end example:** typed arrays in `../example/code/src/levins/core.ts` (`Float64Array` results) and `../example/code/src/levins/optimize-worker.ts`. + +For raw data access and null handling, see `COMPUTATION-PATTERNS.md`. +For worker-specific patterns, see `WORKER-GUIDE.md`. + +## Pre-allocate and Reuse + +The core principle: allocate buffers once before the loop, reuse them across iterations. +Every `new Float32Array(n)` inside a loop is a hidden cost — allocation + eventual GC pause. + +```typescript +// Bad: allocation per iteration +for (let iter = 0; iter < maxIter; iter++) { + const temp = new Float32Array(n); // GC pressure grows with maxIter + // ... use temp ... +} + +// Good: single allocation, reused across iterations +const temp = new Float32Array(n); +for (let iter = 0; iter < maxIter; iter++) { + // ... use temp — same memory, zero allocations ... +} +``` + +Reference: `probabilistic-scoring/nelder-mead.ts` (`nelderMead`) — `centroid`, `reflectionPoint`, +`expansionPoint`, `contractionPoint` are allocated once and reused across all Nelder-Mead iterations. + +--- + +## Out-Parameter Pattern + +Write results into a caller-provided array instead of allocating and returning a new one. +This gives the caller control over allocation and enables buffer reuse. + +```typescript +function add(a: Float32Array, b: Float32Array, out: Float32Array, len: number): void { + for (let i = 0; i < len; i++) out[i] = a[i] + b[i]; +} + +const buf = new Float32Array(n); +add(x, y, buf, n); // buf = x + y +scale(buf, 2.0, buf, n); // buf = 2 * (x + y), in-place +``` + +Reference: `probabilistic-scoring/nelder-mead.ts` — `fillPoint` and `fillCentroid` write into +pre-allocated arrays, called repeatedly inside the optimization loop. + +--- + +## Scratch Buffers for Iterative Algorithms + +When an algorithm runs many iterations, declare all temporary arrays before the loop. + +```typescript +// softmax-worker.ts: Z, dZ, dW allocated once before training loop +const Z = new Array(m); +for (let i = 0; i < m; i++) Z[i] = new Float32Array(c); +const dZ = new Array(c); +for (let i = 0; i < c; i++) dZ[i] = new Float32Array(m); + +for (let iter = 0; iter < iterations; iter++) { + // Forward/backward pass writes into Z, dZ — zero allocations per iteration +} +``` + +Reference: `workers/softmax-worker.ts` (`onmessage` handler, buffer allocation before training loop). + +--- + +## Local Aliases for Inner Loops + +Store a reference to a sub-array in a local variable before the inner loop. +The primary benefit is **readability and reduced index errors**: `wBuf[k] * xBuf[k]` is +clearer than `params[i][k] * X[j][k]`, and there is less chance of mixing up `i`/`j` indices. + +> **Note on performance:** Modern V8 often hoists loop-invariant array lookups automatically +> (loop-invariant code motion), so the performance gain may be minimal. Use this pattern +> primarily for clarity in multi-level loops. + +```typescript +// Before: dense indexing, easy to confuse i/j +for (let j = 0; j < m; j++) + for (let k = 0; k < n; k++) + sum += params[i][k] * X[j][k]; + +// After: meaningful names, less index juggling +for (let j = 0; j < m; j++) { + const xBuf = X[j]; // alias, not copy + const wBuf = params[i]; + for (let k = 0; k < n; k++) + sum += wBuf[k] * xBuf[k]; +} +``` + +Reference: `workers/softmax-worker.ts` (forward propagation loop) — `xBuf`, `wBuf`, `zBuf` aliases. + +--- + +## Accumulation into Pre-allocated Output + +Allocate the output array once and accumulate contributions in-place. + +```typescript +// regression.ts: prediction = bias + sum(weight_j * feature_j) +const prediction = new Float32Array(samplesCount); +let rawData = features.byIndex(0).getRawData(); +const bias = params[featuresCount]; + +for (let i = 0; i < samplesCount; i++) + prediction[i] = bias + params[0] * rawData[i]; + +for (let j = 1; j < featuresCount; j++) { + rawData = features.byIndex(j).getRawData(); + for (let i = 0; i < samplesCount; i++) + prediction[i] += params[j] * rawData[i]; +} +``` + +Reference: `regression.ts` (`getPredictionByLinearRegression`). + +--- + +## Logical Length vs Physical Length + +Pre-allocated buffers may have a fixed physical size but a variable logical length. +Track the logical length separately and use it for all iteration bounds. + +```typescript +const properIndices = new Uint32Array(featuresCount); +let properIndicesCount = 0; + +const getProperIndices = (idx: number) => { + properIndicesCount = 0; // reset logical length + for (let i = 0; i < featuresCount; i++) { + if (featureSource[i][idx] !== featureNullVal[i]) + properIndices[properIndicesCount++] = i; + } +}; + +// Later: iterate only over valid elements +for (let i = 0; i < properIndicesCount; i++) + sum += bufferVector[properIndices[i]]; +``` + +Reference: `missing-values-imputation/knn-imputer.ts` (`KnnImputer.transform`, `getProperIndices` helper). + +--- + +## In-Place Transforms + +When the input array is no longer needed after the transform, write results directly into it. + +```typescript +function normalizeInPlace(arr: Float32Array, len: number, avg: number, stdev: number): void { + for (let i = 0; i < len; i++) arr[i] = (arr[i] - avg) / stdev; +} +``` + +Reference: `regression.ts` (`getTestDatasetForLinearRegression`). + +**Caution:** Only use in-place transforms when you own the array. Never modify arrays obtained +via `col.getRawData()` on user data — this mutates the underlying DataFrame column. + +--- + +## Bulk Copy with TypedArray.set() + +Use the built-in `set()` instead of a manual loop — engines optimize it to a memcpy-like path. + +```typescript +dst.set(src); // full copy +dst.set(src, offset); // copy into dst starting at offset +dst.set(src.subarray(start, end)); // copy a slice (subarray is a zero-copy view) + +// Clone raw column data for safe mutation +const clone = new Float32Array(col.getRawData().length); +clone.set(col.getRawData()); +``` + +**Tip:** `subarray(start, end)` returns a zero-copy view — use it to pass a logical slice +to `set()` or to functions that accept a typed array, without allocating. + +--- + +## Array Pool for Variable-Size Buffers + +When buffer sizes vary between calls, a pool recycles previously created arrays. +Interface: `acquire(minLen)` returns a buffer of at least `minLen` (contents uninitialized), +`release(arr)` returns it to the pool, `clear()` drops all pooled arrays. + +```typescript +const pool = new Float32Pool(); + +function processChunk(chunkSize: number): void { + const tmp = pool.acquire(chunkSize); + // ... compute into tmp ... + pool.release(tmp); +} + +pool.clear(); // after all work is done +``` + +Guidelines: +- **Always release** — otherwise it degrades to plain allocation. +- **Never read stale contents** — treat as uninitialized, `arr.fill(0)` if needed. +- **Scope the lifetime** — create per invocation and `clear()` when done. +- **Keep it simple** — for fixed-size buffers, plain pre-allocation is better. + +--- + +## Ring Buffer for Fixed-Length History + +When an algorithm needs a sliding window of the last N values, pre-allocate N arrays +and use a modular head index — O(1) per step, zero allocations. + +```typescript +const HIST_LEN = 5; +const history: Float64Array[] = []; +for (let i = 0; i < HIST_LEN; i++) + history[i] = new Float64Array(dim); +let head = 0; + +computeValues(history[head]); + +for (let step = 0; step < totalSteps; step++) { + const newest = history[head]; + const oldest = history[(head - (HIST_LEN - 1) + HIST_LEN) % HIST_LEN]; + + // Advance: overwrite oldest slot — O(1) + head = (head + 1) % HIST_LEN; + computeValues(history[head]); +} +``` + +**Alternative — reference shift** (O(N) per step): when consumers expect `[0]` = newest, +`[N-1]` = oldest, shift references instead. Acceptable for small N. + +```typescript +const recycled = history[HIST_LEN - 1]; +for (let j = HIST_LEN - 1; j > 0; --j) history[j] = history[j - 1]; +history[0] = recycled; +computeValues(history[0]); +``` + +Reference: `diff-grok` library, `solver-tools/ab5-method.ts` (`ab5Step`, reference shift with N=5). + +--- + +## Multi-Purpose Scratch Buffers + +The same buffer can serve different purposes at different stages within one iteration. +Each stage must fully overwrite the buffer before reading it. + +```typescript +const scratch0 = new Float64Array(dim); +const scratch1 = new Float64Array(dim); + +while (solving) { + // Stage 1: Jacobian — fills scratch0, scratch1 entirely + jacobian(t, y, f, eps, scratch0, scratch1, W); + + // Stage 2: time derivative — overwrites all elements + tDerivative(t, y, f, eps, scratch0, scratch1, hdT); + + // Stage 3: scratch0 reused as RHS for linear solve + for (let i = 0; i < dim; i++) scratch0[i] = f0[i] + hdT[i]; + luSolve(L, U, scratch0, luBuf, k1, dim); +} +``` + +For many stages, use **stage-scoped aliases**: `const rhs = scratch0;` gives semantic +context without misleading names. Both point to the same memory — zero overhead. + +> **Aliasing hazard:** Never pass the same buffer as both `src` and `dst` of a single call. +> If the function reads `src` while writing `dst`, aliasing corrupts the result. +> When unsure, use separate buffers — the cost is negligible vs a silent data corruption bug. + +Guidelines: +- **Document the reuse** with comments at each stage. +- **Never read previous-stage contents** — each stage must fully overwrite before reading. +- **Watch for aliasing** — never pass the same buffer as both source and destination. + +Reference: `diff-grok` library, `solver-tools/mrt-method.ts` (`mrtStep`, scratch buffer reuse across Jacobian/derivative/solve stages). + +--- + +## Summary + +| Pattern | When to use | Saves | +|---------|-------------|-------| +| **Pre-allocate and reuse** | Iterative algorithms | N allocations per loop | +| **Out-parameter** | Utility functions called repeatedly | 1 allocation per call | +| **Scratch buffers** | Multi-step computations in a loop | All intermediate arrays per iteration | +| **Local aliases** | Nested loops with array-of-arrays | Index errors and readability | +| **Accumulation into output** | Aggregation from multiple sources | Intermediate result arrays | +| **Logical length** | Variable-size subsets of a fixed buffer | Re-allocation on size change | +| **In-place transforms** | Input no longer needed after transform | 1 output array | +| **Bulk copy (set())** | Copying blocks between typed arrays | Loop overhead; engine-optimized | +| **Array pool** | Variable-size temporary buffers | Repeated allocation of similar arrays | +| **Ring buffer** | Sliding window / fixed-length history | O(1) advance with modular index | +| **Multi-purpose scratch** | Multi-stage algorithms | Extra buffer per stage | diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/COMPUTATION-PATTERNS.md b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/COMPUTATION-PATTERNS.md new file mode 100644 index 0000000000..0fc5cb74d0 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/COMPUTATION-PATTERNS.md @@ -0,0 +1,220 @@ +# Computation Patterns + +Reference for implementing computational methods in Datagrok packages. + +> **End-to-end example:** core `../example/code/src/levins/core.ts` + `../example/code/src/levins/model.ts`. + +For worker-based methods, see `WORKER-GUIDE.md`. +For array allocation and reuse patterns, see `ARRAY-OPERATIONS.md`. +For architectural context (core tasks, pipelines, specification structure), see `../datagrok-interactive-app-guide.md` and `../datagrok-app-specification-template.md`. + +## Raw Typed Arrays + +Access column data via `col.getRawData()` instead of per-element `col.get(i)`. This returns the underlying +typed array (`Float32Array`, `Float64Array`, `Int32Array`, `Uint32Array`) and avoids boxing/unboxing overhead on every iteration. + +**IMPORTANT:** The raw array's `.length` may be larger than the column's element count (due to internal buffer allocation). Always use `col.length` for iteration bounds, never `rawData.length`. + +```typescript +const vals: Float32Array = features.getRawData(); +const cats: Int32Array = categories.getRawData(); +const len = features.length; // use column length, NOT vals.length + +for (let i = 0; i < len; i++) { + // direct access — no per-element API calls + const value = vals[i]; + const category = cats[i]; +} +``` + +Reference: `anova/anova-tools.ts` (`FactorizedData` constructor), `missing-values-imputation/knn-imputer.ts` (`KnnImputer.transform`). + +--- + +## Missing Values Strategy + +**Before implementing any new method, define the missing values strategy.** Different methods require different approaches: + +| Strategy | When to use | Example | +|----------|-------------|---------| +| **Skip** | Aggregation, statistics | ANOVA skips rows where factor or value is null | +| **Impute before computation** | Methods that require complete data (e.g., matrix operations) | KNN imputation, mean/median fill | +| **Propagate** | Result column should reflect original nulls | Copy null sentinel to output at the same index | +| **Reject** | Method cannot handle nulls at all | Throw error if `missingValueCount > 0` | + +Document the chosen strategy in the method's JSDoc or function header. When multiple input columns are involved, specify per-column behavior (e.g., ANOVA: skip if factor OR value is null; KNN: skip feature columns with nulls at the target row but impute the target). + +--- + +## Null Handling in Loops + +Before processing, check `col.stats.missingValueCount`. If there are no missing values, skip all null checks +in the loop — this eliminates a branch per iteration and significantly speeds up computation. + +```typescript +const hasMissing = col.stats.missingValueCount > 0; + +if (hasMissing) { + const nullValue = getNullValue(col); + for (let i = 0; i < len; i++) { + if (raw[i] === nullValue) continue; + // process raw[i] + } +} else { + for (let i = 0; i < len; i++) { + // process raw[i] — no null checks needed + } +} +``` + +When nulls are present, use `getNullValue(col)` from `utils.ts` to obtain the sentinel value and compare +against it directly in loops. Do not use platform null-checking APIs in hot paths. + +| Column type | Sentinel | Notes | +|-------------|----------|-------| +| `int`, `string`, `bool` | `-2147483648` | Min 32-bit int | +| `float`, `datetime`, `qnum` | `2.6789344063684636e-34` | Special float constant | + +```typescript +import {getNullValue} from '../utils'; + +const nullValue = getNullValue(col); +const raw = col.getRawData(); + +for (let i = 0; i < col.length; i++) { + if (raw[i] === nullValue) continue; // skip missing + // process raw[i] +} +``` + +For categorical (string) columns, raw data stores integer category indices. Check for null categories separately: + +```typescript +const categoriesNull = categories.stats.missingValueCount > 0 ? getNullValue(categories) : -1; + +for (let i = 0; i < size; i++) { + if ((cats[i] === categoriesNull) || (vals[i] === featuresNull)) continue; + // process non-null pair +} +``` + +Reference: `anova/anova-tools.ts` (`FactorizedData.setStats`), `missing-values-imputation/knn-imputer.ts` (`KnnImputer.transform`). + +--- + +## Single-Pass Aggregation + +Compute all required statistics in one loop over the data. Pre-allocate output buffers as typed arrays. + +```typescript +const K = uniqueCategoryCount; +const sums = new Float64Array(K).fill(0); +const sumsOfSquares = new Float64Array(K).fill(0); +const subSampleSizes = new Int32Array(K).fill(0); + +for (let i = 0; i < size; i++) { + const cat = cats[i]; + if (vals[i] === nullValue) continue; + + sums[cat] += vals[i]; + sumsOfSquares[cat] += vals[i] ** 2; + ++subSampleSizes[cat]; +} +``` + +Reference: `anova/anova-tools.ts`, `FactorizedData.setStats()`. + +--- + +## Bool Column Handling + +Bool columns are stored as packed bit arrays. Extract individual bits via bitwise operations: + +```typescript +const raw = boolCol.getRawData(); // Uint32Array with packed bits +let catIdx = 0; +let shift = 0; +let packed = raw[0]; +const MAX_SHIFT = 8 * raw.BYTES_PER_ELEMENT - 1; + +for (let i = 0; i < size; i++) { + const bit = 1 & (packed >> shift); + // use `bit` as 0 or 1 + + ++shift; + if (shift > MAX_SHIFT) { + shift = 0; + ++catIdx; + packed = raw[catIdx]; + } +} +``` + +Reference: `anova/anova-tools.ts` (`FactorizedData.setStats`, bool branch). + +--- + +## Data Locality + +Typed arrays store elements contiguously in memory. Sequential access maximizes CPU cache utilization +and enables hardware prefetching. This is a key reason to prefer typed arrays over `number[]` or per-element API calls. + +### Why it matters + +- **Cache lines**: CPU loads data in 64-byte blocks. One cache line holds 16 `float32` or 8 `float64` values. + Sequential access means every loaded cache line is fully utilized. +- **Prefetching**: CPU detects sequential access patterns and preloads next cache lines automatically. + This hides memory latency almost entirely for linear traversals. +- **No boxing**: `number[]` stores boxed values as heap-allocated objects (pointer → header → value). + Typed arrays store raw values inline — more useful data per cache line. + +### Access pattern guidelines + +| Pattern | Cache behavior | Use when | +|---------|---------------|----------| +| Sequential typed array traversal | Optimal — prefetcher active, full cache line utilization | Aggregation, statistics, transforms | +| Multiple typed arrays in parallel (`vals[i]`, `cats[i]`) | Good — each array has its own prefetch stream | Multi-column single-pass (ANOVA, KNN distances) | +| Random access to typed array | Cache miss per access — up to 100x slower than sequential | Avoid; restructure if possible | +| `col.get(i)` in a loop | Method call + potential unboxing per element | Avoid in hot loops | + +### Column-major vs row-major + +When building matrices from multiple columns, the layout determines which access patterns are cache-friendly: + +- **Column-major** (`data[i + j * nRows]`): optimal when processing columns independently + (e.g., centering, scaling, per-feature statistics) +- **Row-major** (`data[i * nCols + j]`): optimal when accessing all features of one row + (e.g., distance computation, KNN, nearest neighbor search) + +Choose the layout that matches the method's primary access pattern. See `WORKER-GUIDE.md` +for `toFlatColumnMajor` and `toFlatRowMajor` helper functions. + +### Pre-allocate output buffers + +Allocate result arrays once before the loop to avoid repeated allocations and garbage collection: + +```typescript +// Good: single allocation +const result = new Float64Array(len); +for (let i = 0; i < len; i++) + result[i] = vals[i] * scale; + +// Bad: growing array triggers re-allocation and copying +const result: number[] = []; +for (let i = 0; i < len; i++) + result.push(vals[i] * scale); +``` + +--- + +## Module Structure + +Separate computation from UI into distinct files: + +| File pattern | Purpose | Dependencies | +|-------------|---------|-------------| +| `*-tools.ts` | Pure computation on raw data | `datagrok-api` types, `utils.ts`, math libraries | +| `*-ui.ts` or `ui.ts` | Dialog, inputs, validation, visualization | `datagrok-api` UI, computation module | +| `*-constants.ts` or `ui-constants.ts` | Enums, error messages, UI labels | None | + +The computation module must not import UI components. This keeps it testable and potentially reusable in workers. diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/PARALLEL-EXECUTION.md b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/PARALLEL-EXECUTION.md new file mode 100644 index 0000000000..516a38d73a --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/PARALLEL-EXECUTION.md @@ -0,0 +1,55 @@ +# Parallel Execution Guide + +Reference for distributing independent computations across multiple web workers. + +> **End-to-end example:** function `runOptimization()` in `../example/code/src/levins/app.ts` — worker pool with task queue, progress bar, partial error handling. + +For single-worker patterns, see `WORKER-GUIDE.md`. + +## Worker Count + +```typescript +import {MIN_WORKERS_COUNT, WORKERS_COUNT_DOWNSHIFT} from './worker-utils/worker-defs'; + +const workerCount = Math.max(MIN_WORKERS_COUNT, navigator.hardwareConcurrency - WORKERS_COUNT_DOWNSHIFT); +``` + +## Fan-out / Fan-in Pattern + +```typescript +async function runParallel( + inputs: TInput[], + workerUrl: URL, +): Promise { + const nWorkers = Math.min( + Math.max(MIN_WORKERS_COUNT, navigator.hardwareConcurrency - WORKERS_COUNT_DOWNSHIFT), + inputs.length, + ); + + // Distribute inputs round-robin + const chunks: TInput[][] = Array.from({length: nWorkers}, () => []); + for (let i = 0; i < inputs.length; i++) + chunks[i % nWorkers].push(inputs[i]); + + const promises = chunks.map((chunk) => + new Promise((resolve, reject) => { + const worker = new Worker(workerUrl); + worker.postMessage(chunk); + worker.onmessage = (e) => { + worker.terminate(); + if (e.data.success) + resolve(e.data.data); + else + reject(new Error(e.data.error)); + }; + worker.onerror = (e) => { + worker.terminate(); + reject(new Error(e.message)); + }; + }), + ); + + const results = await Promise.all(promises); + return results.flat(); +} +``` diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/WORKER-GUIDE.md b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/WORKER-GUIDE.md new file mode 100644 index 0000000000..40411e97c6 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/WORKER-GUIDE.md @@ -0,0 +1,296 @@ +# Worker Implementation Guide + +Reference for implementing in-worker computations using the `worker-utils` infrastructure. + +> **End-to-end example:** optimization worker `../example/code/src/levins/optimize-worker.ts`. +> For architectural context (ports, adapters, coordinator) see `../datagrok-interactive-app-guide.md` (sections 1.2–1.3). + +## Worker-Utils Infrastructure + +### Definitions (`worker-defs.ts`) — no dependencies, safe to import in workers + +```typescript +type RawData = Int32Array | Float32Array | Float64Array | Uint32Array; +type ColumnType = 'int' | 'float32' | 'float64' | 'string' | 'bool' | 'datetime' | 'qnum' | 'bigint'; + +interface WorkerColumnStats { + totalCount: number; + missingValueCount: number; + uniqueCount: number; + valueCount: number; + min: number; max: number; + sum: number; avg: number; + stdev: number; variance: number; + skew: number; kurt: number; + med: number; + q1: number; q2: number; q3: number; + nullValue: number; // INT_NULL (-2147483648) or FLOAT_NULL (2.6789344063684636e-34) +} + +interface WorkerColumn { + name: string; + type: ColumnType; + length: number; + rawData: RawData; + stats: WorkerColumnStats; + categories?: string[]; // only for type === 'string' +} + +interface WorkerDataFrame { + name: string; + rowCount: number; + columns: WorkerColumn[]; +} +``` + +### Transforms (`worker-transforms.ts`) — requires `datagrok-api`, main-thread only + +| Function | Signature | Direction | +|----------|-----------|-----------| +| `toWorkerColumn` | `(col: DG.Column) => WorkerColumn` | DG -> Worker | +| `toWorkerColumns` | `(columns: DG.ColumnList) => WorkerColumn[]` | DG -> Worker | +| `toWorkerDataFrame` | `(df: DG.DataFrame) => WorkerDataFrame` | DG -> Worker | +| `fromWorkerColumn` | `(wc: WorkerColumn) => DG.Column` | Worker -> DG | +| `fromWorkerDataFrame` | `(wdf: WorkerDataFrame) => DG.DataFrame` | Worker -> DG | + +### Null Sentinel Values + +| ColumnType | Sentinel | Constant | +|------------|----------|----------| +| `int`, `string`, `bool` | -2147483648 | `INT_NULL` | +| `float32`, `float64`, `datetime`, `qnum` | 2.6789344063684636e-34 | `FLOAT_NULL` | + +--- + +## Missing Values Strategy + +Before implementing any new worker-based method, define the missing values strategy (skip, impute, propagate, or reject). See `COMPUTATION-PATTERNS.md` (Missing Values Strategy section) for the full decision table and per-column behavior guidelines. + +--- + +## Working with WorkerColumn Inside a Worker + +**IMPORTANT:** The `rawData` array's `.length` may be larger than `col.length` (due to internal buffer allocation). Always use `col.length` for iteration bounds, never `rawData.length`. + +### Reading numerical data + +Check `col.stats.missingValueCount` before processing. If there are no missing values, skip all null checks +in the loop — this eliminates a branch per iteration and significantly speeds up computation. + +```typescript +// worker.ts +import {WorkerColumn} from './worker-defs'; + +onmessage = (e: MessageEvent) => { + const col: WorkerColumn = e.data; + const raw = col.rawData as Float32Array; + const n = col.length; + + if (col.stats.missingValueCount > 0) { + const nullVal = col.stats.nullValue; + for (let i = 0; i < n; i++) { + if (raw[i] === nullVal) continue; // skip missing + // process raw[i] + } + } else { + for (let i = 0; i < n; i++) { + // process raw[i] — no null checks needed + } + } +}; +``` + +### Centering / scaling using stats + +```typescript +function centerAndScale(col: WorkerColumn): Float32Array { + const raw = col.rawData as Float32Array; + const result = new Float32Array(col.length); + const nullVal = col.stats.nullValue; + const avg = col.stats.avg; + const stdev = col.stats.stdev; + + for (let i = 0; i < col.length; i++) { + if (raw[i] === nullVal) + result[i] = nullVal; + else + result[i] = (raw[i] - avg) / stdev; + } + return result; +} +``` + +### Building a feature matrix from WorkerColumn[] + +Choose the matrix layout based on the method's primary access pattern — this directly affects +CPU cache utilization: + +- **Column-major**: sequential access within each column. Optimal when columns are processed + independently (centering, scaling, per-feature statistics, WASM interop). +- **Row-major flat**: sequential access across features of each row. Optimal for distance + computation, KNN, nearest neighbor search. +- **Row-major typed**: same access pattern as row-major flat, but each row is a separate + `Float32Array`. Use as a drop-in replacement for `number[][]`. + +Avoid random access patterns — a cache miss per access can be up to 100x slower than sequential traversal. +For more details on data locality, see `COMPUTATION-PATTERNS.md` (Data Locality section). + +```typescript +// Row-major flat Float32Array: data[i * nCols + j] +// Single allocation, contiguous memory, no boxing overhead. +function toFlatRowMajor(cols: WorkerColumn[]): Float32Array { + const nRows = cols[0].length; + const nCols = cols.length; + const data = new Float32Array(nRows * nCols); + for (let j = 0; j < nCols; j++) { + const raw = cols[j].rawData; + for (let i = 0; i < nRows; i++) + data[i * nCols + j] = raw[i]; + } + return data; +} + +// Row-major Float32Array[]: data[i][j] +// Drop-in replacement for number[][] with unboxed typed rows. +function toTypedRowMajor(cols: WorkerColumn[]): Float32Array[] { + const nRows = cols[0].length; + const nCols = cols.length; + const data: Float32Array[] = new Array(nRows); + for (let i = 0; i < nRows; i++) + data[i] = new Float32Array(nCols); + for (let j = 0; j < nCols; j++) { + const raw = cols[j].rawData; + for (let i = 0; i < nRows; i++) + data[i][j] = raw[i]; + } + return data; +} + +// Column-major flat Float32Array: data[i + j * nRows] +// Optimal when columns are processed independently (WASM, matrix ops). +function toFlatColumnMajor(cols: WorkerColumn[]): Float32Array { + const nRows = cols[0].length; + const nCols = cols.length; + const data = new Float32Array(nRows * nCols); + for (let j = 0; j < nCols; j++) { + const raw = cols[j].rawData; + const offset = j * nRows; + for (let i = 0; i < nRows; i++) + data[i + offset] = raw[i]; + } + return data; +} +``` + +### Creating a result WorkerColumn + +```typescript +function makeResultColumn(name: string, data: Float32Array): WorkerColumn { + return { + name: name, + type: 'float32', + length: data.length, + rawData: data, + stats: computeStats(data), // compute in worker or leave zeros if not needed + }; +} +``` + +--- + +## Worker Lifecycle Pattern + +### Main thread (caller) + +```typescript +import {toWorkerColumns, fromWorkerColumn} from './worker-utils/worker-transforms'; +import {WorkerColumn} from './worker-utils/worker-defs'; + +async function runInWorker( + features: DG.ColumnList, components: number +): Promise { + const workerFeatures = toWorkerColumns(features); + + return new Promise((resolve, reject) => { + const worker = new Worker(new URL('./workers/my-worker.ts', import.meta.url)); + + worker.postMessage({features: workerFeatures, components}); + + worker.onmessage = (e) => { + worker.terminate(); + if (e.data.success) { + const result = e.data.data.columns as WorkerColumn[]; + resolve(result.map(fromWorkerColumn)); + } else { + reject(new Error(e.data.error)); + } + }; + + worker.onerror = (e) => { + worker.terminate(); + reject(new Error(e.message)); + }; + }); +} +``` + +### Web worker + +```typescript +import {WorkerColumn} from '../worker-utils/worker-defs'; + +interface MyWorkerInput { + features: WorkerColumn[]; + components: number; +} + +interface MyWorkerOutput { + success: true; + data: {columns: WorkerColumn[]}; +} | { + success: false; + error: string; +} + +onmessage = (e: MessageEvent) => { + try { + const {features, components} = e.data; + + // Access raw data directly: + const nRows = features[0].length; + const nCols = features.length; + + // Use stats: + for (const f of features) { + const avg = f.stats.avg; + const stdev = f.stats.stdev; + const nullVal = f.stats.nullValue; + // ... + } + + // Build result columns: + const resultCols: WorkerColumn[] = []; + for (let c = 0; c < components; c++) { + const data = new Float32Array(nRows); + // ... fill data ... + resultCols.push({ + name: `Component ${c + 1}`, + type: 'float32', + length: nRows, + rawData: data, + stats: { /* fill or leave defaults */ } as any, + }); + } + + postMessage({success: true, data: {columns: resultCols}} satisfies MyWorkerOutput); + } catch (err) { + postMessage({success: false, error: String(err)}); + } +}; +``` + +--- + +## Parallel Execution + +For distributing independent computations across multiple workers, see `PARALLEL-EXECUTION.md`. diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/datagrok-api-reference.md b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/datagrok-api-reference.md new file mode 100644 index 0000000000..310c41c97a --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/datagrok-api-reference.md @@ -0,0 +1,289 @@ +# Datagrok API Reference: Inputs and Viewers + +For interactive scientific applications. + +> **End-to-end example:** UI setup in `../example/code/src/levins/app.ts`. + +## 1. Inputs (`ui.input.*`) + +All inputs return an object inheriting from [`InputBase`](https://datagrok.ai/api/js/dg/classes/InputBase). + +UI Documentation: [Datagrok UI](https://datagrok.ai/help/develop/advanced/ui.md) | Namespace [`ui.input`](https://datagrok.ai/api/js/ui/namespaces/input/) + +### 1.1. Input Catalog + +| Method | Value Type | Description | Docs | +|---|---|---|---| +| `ui.input.int(label, options?)` | `number` | Integer | [int()](https://datagrok.ai/api/js/ui/namespaces/input/functions/int) | +| `ui.input.float(label, options?)` | `number` | Floating-point number | [float()](https://datagrok.ai/api/js/ui/namespaces/input/functions/float) | +| `ui.input.string(label, options?)` | `string` | String | [string()](https://datagrok.ai/api/js/ui/namespaces/input/functions/string) | +| `ui.input.bool(label, options?)` | `boolean` | Checkbox (true/false) | [bool()](https://datagrok.ai/api/js/ui/namespaces/input/functions/bool) | +| `ui.input.toggle(label, options?)` | `boolean` | Toggle switch | [toggle()](https://datagrok.ai/api/js/ui/namespaces/input/functions/toggle) | +| `ui.input.choice(label, options?)` | `string` | Selection from a list (dropdown) | [choice()](https://datagrok.ai/api/js/ui/namespaces/input/functions/choice) | +| `ui.input.multiChoice(label, options?)` | `string[]` | Multiple selection | [multiChoice()](https://datagrok.ai/api/js/ui/namespaces/input/functions/multiChoice) | +| `ui.input.dateTime(label, options?)` | `DateTime` | Date and time | [dateTime()](https://datagrok.ai/api/js/ui/namespaces/input/functions/dateTime) | +| `ui.input.textArea(label, options?)` | `string` | Multiline text | [textArea()](https://datagrok.ai/api/js/ui/namespaces/input/functions/textArea) | +| `ui.input.search(label, options?)` | `string` | String with search icon, Esc to clear | [search()](https://datagrok.ai/api/js/ui/namespaces/input/functions/search) | +| `ui.input.column(label, options?)` | `DG.Column` | Column selection | [column()](https://datagrok.ai/api/js/ui/namespaces/input/functions/column) | +| `ui.input.columnList(label, options?)` | `DG.Column[]` | Multiple column selection | [columnList()](https://datagrok.ai/api/js/ui/namespaces/input/functions/columnList) | +| `ui.input.markdown(label, options?)` | `string` | Markdown editor | [markdown()](https://datagrok.ai/api/js/ui/namespaces/input/functions/markdown) | + +### 1.2. Common Options (`options`) + +| Option | Type | Description | +|---|---|---| +| `value` | matches the type | Initial value | +| `items` | `string[]` | List of choices (for `choice`, `multiChoice`) | +| `nullable` | `boolean` | Whether the value can be `null` | +| `min` | `number` | Minimum value (for numeric types) | +| `max` | `number` | Maximum value (for numeric types) | +| `step` | `number` | Step (for numeric types) | +| `placeholder` | `string` | Hint text in an empty field | +| `icon` | `string \| HTMLElement` | Icon in the field (for string types) | +| `clearIcon` | `boolean` | Clear icon (for string types) | +| `escClears` | `boolean` | Esc clears the field (for string types) | +| `tooltipText` | `string` | Tooltip text displayed on hover | +| `onValueChanged` | `(value) => void` | Callback fired when the value changes (shorthand for subscribing to `.onChanged`) | + +### 1.3. Properties and Methods of [`InputBase`](https://datagrok.ai/api/js/dg/classes/InputBase) + +#### Value and State + +| Property/Method | Description | +|---|---| +| `.value` | Current value (get/set) | +| `.enabled` | Input enabled state (get/set) | +| `.root` | Root `HTMLElement` (div with label and input) | +| `.input` | `HTMLElement` of the input field itself | +| `.captionLabel` | `HTMLElement` of the label | + +#### Tooltips + +```typescript +// At creation time (preferred): +const input = ui.input.float('Rate', { tooltipText: 'Parameter description' }); + +// Or after creation: +input.setTooltip('Parameter description'); +``` + +#### Validators + +```typescript +// Adding a validator: returns null (valid) or an error string +input.addValidator((value) => { + if (value < 0) return 'Value must be non-negative'; + return null; +}); + +// Checking validity +const isValid = input.validate(); // true / false +``` + +#### Events + +| Event | Description | +|---|---| +| `.onChanged` | Value changed (by user or programmatically). Subscribe: `.onChanged.subscribe(callback)` | +| `.onInput` | Value changed by user. Subscribe: `.onInput.subscribe(callback)` | +| `.fireChanged()` | Programmatically trigger the `onChanged` event | +| `.fireInput()` | Programmatically trigger the `onInput` event | + +> **Note:** The `onValueChanged` callback in input options (section 1.2) is a convenience shorthand equivalent to subscribing to `.onChanged`. Use `onValueChanged` when setting up the callback at creation time; use `.onChanged.subscribe(...)` when subscribing later or when you need the `rxjs.Subscription` for cleanup. + +### 1.4. Binding Inputs ([`ui.bindInputs`](https://datagrok.ai/api/js/ui/functions/bindInputs)) + +```typescript +// Combining subscriptions from multiple inputs +const subs: rxjs.Subscription[] = ui.bindInputs([input1, input2, input3]); +``` + +### 1.5. Grouping Inputs into a Form ([`ui.inputs`](https://datagrok.ai/api/js/ui/functions/inputs) | [UI: Forms](https://datagrok.ai/help/develop/advanced/ui.md#forms)) + +```typescript +// Vertical form +const form = ui.inputs([ + ui.input.string('Name'), + ui.input.int('Age'), + ui.buttonsInput([ + ui.bigButton('Apply'), + ui.button('Cancel'), + ]), +]); + +// Also: ui.form([...]), ui.narrowForm([...]), ui.wideForm([...]) +``` + +## 2. Buttons and Icons + +| Method | Description | Docs | +|---|---|---| +| `ui.button(text, onClick, tooltip?)` | Standard button | [button()](https://datagrok.ai/api/js/ui/functions/button) | +| `ui.bigButton(text, onClick, tooltip?)` | Accent button (for the primary action) | [bigButton()](https://datagrok.ai/api/js/ui/functions/bigButton) | +| `ui.iconFA(name, onClick, tooltip?)` | FontAwesome icon as a button | [iconFA()](https://datagrok.ai/api/js/ui/functions/iconFA) | +| `ui.iconFAB(name, onClick, tooltip?)` | FontAwesome icon (blue) | [iconFAB()](https://datagrok.ai/api/js/ui/functions/iconFAB) | +| `ui.iconSvg(svgContent, onClick, tooltip?)` | SVG icon as a button | [iconSvg()](https://datagrok.ai/api/js/ui/functions/iconSvg) | + +## 3. Tooltips ([`ui.tooltip`](https://datagrok.ai/api/js/ui/classes/Tooltip)) + +```typescript +// Binding a tooltip to any HTMLElement +ui.tooltip.bind(element, 'Tooltip text'); + +// Showing a tooltip programmatically +ui.tooltip.show('Text', x, y); + +// Hiding a tooltip +ui.tooltip.hide(); + +// Showing a tooltip for a group of table rows +ui.tooltip.showRowGroup(dataFrame, predicate, x, y); +``` + +## 4. Viewers ([`DG.Viewer`](https://datagrok.ai/api/js/dg/classes/JsViewer)) + +Documentation: [Viewers](https://datagrok.ai/help/visualize/viewers/) | [Viewer API](https://datagrok.ai/api/js/dg/classes/JsViewer) | [UI: Viewers](https://datagrok.ai/help/develop/advanced/ui.md#viewers) + +### 4.1. Standard Viewer Catalog + +| Factory Method | TableView Method | Description | Docs | +|---|---|---|---| +| `DG.Viewer.barChart(df, options?)` | `view.barChart(options?)` | Bar chart | [Bar Chart](https://datagrok.ai/help/visualize/viewers/bar-chart.md) | +| `DG.Viewer.boxPlot(df, options?)` | `view.boxPlot(options?)` | Box plot | [Box Plot](https://datagrok.ai/help/visualize/viewers/box-plot.md) | +| `DG.Viewer.calendar(df, options?)` | `view.calendar(options?)` | Calendar | [Calendar](https://datagrok.ai/help/visualize/viewers/calendar.md) | +| `DG.Viewer.correlationPlot(df, options?)` | `view.corrPlot(options?)` | Correlation matrix | [Correlation Plot](https://datagrok.ai/help/visualize/viewers/correlation-plot.md) | +| `DG.Viewer.densityPlot(df, options?)` | `view.densityPlot(options?)` | Point density | [Density Plot](https://datagrok.ai/help/visualize/viewers/density-plot.md) | +| `DG.Viewer.filters(df, options?)` | `view.filters(options?)` | Filter set | [Filters](https://datagrok.ai/help/visualize/viewers/filters.md) | +| `DG.Viewer.form(df, options?)` | `view.form(options?)` | Form (single row) | [Form](https://datagrok.ai/help/visualize/viewers/form.md) | +| `DG.Viewer.grid(df, options?)` | `view.grid` | Table grid | [Grid](https://datagrok.ai/help/visualize/viewers/grid.md) | +| `DG.Viewer.heatMap(df, options?)` | `view.heatMap(options?)` | Heat map | [Heat Map](https://datagrok.ai/help/visualize/viewers/heat-map.md) | +| `DG.Viewer.histogram(df, options?)` | `view.histogram(options?)` | Histogram | [Histogram](https://datagrok.ai/help/visualize/viewers/histogram.md) | +| `DG.Viewer.lineChart(df, options?)` | `view.lineChart(options?)` | Line chart | [Line Chart](https://datagrok.ai/help/visualize/viewers/line-chart.md) | +| `DG.Viewer.markup(df, options?)` | `view.markup(options?)` | HTML/Markdown | [Markup](https://datagrok.ai/help/visualize/viewers/markup.md) | +| `DG.Viewer.matrixPlot(df, options?)` | `view.matrixPlot(options?)` | Matrix of plots | [Matrix Plot](https://datagrok.ai/help/visualize/viewers/matrix-plot.md) | +| `DG.Viewer.network(df, options?)` | `view.networkDiagram(options?)` | Network diagram | [Network Diagram](https://datagrok.ai/help/visualize/viewers/network-diagram.md) | +| `DG.Viewer.pcPlot(df, options?)` | `view.pcPlot(options?)` | Parallel coordinates | [PC Plot](https://datagrok.ai/help/visualize/viewers/pc-plot.md) | +| `DG.Viewer.pieChart(df, options?)` | — | Pie chart | [Pie Chart](https://datagrok.ai/help/visualize/viewers/pie-chart.md) | +| `DG.Viewer.scatterPlot(df, options?)` | `view.scatterPlot(options?)` | Scatter plot | [Scatter Plot](https://datagrok.ai/help/visualize/viewers/scatter-plot.md) | +| `DG.Viewer.scatterPlot3d(df, options?)` | `view.scatterPlot3d(options?)` | 3D scatter plot | [3D Scatter Plot](https://datagrok.ai/help/visualize/viewers/3d-scatter-plot.md) | +| `DG.Viewer.statistics(df, options?)` | `view.statistics(options?)` | Descriptive statistics | [Statistics](https://datagrok.ai/help/visualize/viewers/statistics.md) | +| `DG.Viewer.tile(df, options?)` | `view.tileViewer(options?)` | Tile view | [Tile Viewer](https://datagrok.ai/help/visualize/viewers/tile-viewer.md) | +| `DG.Viewer.treeMap(df, options?)` | `view.treeMap(options?)` | Tree map | [Tree Map](https://datagrok.ai/help/visualize/viewers/tree-map.md) | +| `DG.Viewer.trellisPlot(df, options?)` | — | Facet grid | [Trellis Plot](https://datagrok.ai/help/visualize/viewers/trellis-plot.md) | +| `DG.Viewer.wordCloud(df, options?)` | — | Word cloud | [Word Cloud](https://datagrok.ai/help/visualize/viewers/word-cloud.md) | + +Additional viewers (require data with coordinates): + +| Viewer | Description | Docs | +|---|---|---| +| `view.googleMap(options?)` | Google Maps with data overlay | [Google Map](https://datagrok.ai/help/visualize/viewers/google-map.md) | +| `DG.Viewer.fromType('Globe', df)` | 3D globe | [Globe](https://datagrok.ai/help/visualize/viewers/globe.md) | +| `view.shapeMap(options?)` | Region map | [Shape Map](https://datagrok.ai/help/visualize/viewers/shape-map.md) | + +### 4.2. Creating by Type + +```typescript +// Creating a viewer by string type +const viewer = DG.Viewer.fromType('Scatter plot', dataFrame); +``` + +### 4.3. Configuring Options + +```typescript +// At creation time +const plot = view.scatterPlot({ + x: 'height', + y: 'weight', + size: 'age', + color: 'race', +}); + +// After creation +plot.setOptions({ + showRegressionLine: true, + markerType: 'square', +}); +``` + +### 4.4. Docking to TableView ([UI: Docking](https://datagrok.ai/help/develop/advanced/ui.md#docking)) + +```typescript +const view = grok.shell.addTableView(df); + +// Docking a viewer +const chart = DG.Viewer.lineChart(df); +view.dockManager.dock(chart, 'right', null, 'Line Chart'); + +// Docking an arbitrary element +const div = ui.div([/* content */]); +const node = view.dockManager.dock(div, 'down', null, 'Panel', 0.3); + +// Docking types: 'left', 'right', 'top', 'down', 'fill' +// Last parameter is the dock ratio (0..1) +``` + +## 5. Notifications ([`grok.shell`](https://datagrok.ai/api/js/dg/classes/Shell)) + +```typescript +grok.shell.info('Informational message'); +grok.shell.warning('Warning'); +grok.shell.error('Error message'); +``` + +## 6. Dialogs ([`ui.dialog`](https://datagrok.ai/api/js/ui/functions/dialog) | [UI: Dialogs](https://datagrok.ai/help/develop/advanced/ui.md#dialogs)) + +```typescript +// Standard dialog +ui.dialog('Title') + .add(ui.inputs([ + ui.input.float('Parameter 1', {value: 1.0}), + ui.input.float('Parameter 2', {value: 2.0}), + ])) + .onOK(() => { /* handling */ }) + .show(); + +// Modal dialog +ui.dialog('Title') + .add(/* content */) + .onOK(() => { /* handling */ }) + .showModal(); +``` + +## 7. Subscriptions and Cleanup + +```typescript +// Subscribing to an event +const sub = input.onChanged.subscribe((value) => { + // handling +}); + +// Unsubscribing +sub.unsubscribe(); + +// For viewers +viewer.sub(eventId, callback); // registers a subscription +viewer.registerCleanup(cleanupFunc); // will be called on close +``` + +## 8. Progress Bar + +```typescript +const pi = DG.TaskBarProgressIndicator.create('Task description...'); +pi.update(50, 'Progress 50%'); +// ... +pi.close(); +``` + +## 9. Layouts and Containers ([UI: Layouts](https://datagrok.ai/help/develop/advanced/ui.md#layouts)) + +| Method | Description | Docs | +|---|---|---| +| `ui.div([...])` | Container | [div()](https://datagrok.ai/api/js/ui/functions/div) | +| `ui.divH([...])` | Horizontal flex container | [divH()](https://datagrok.ai/api/js/ui/functions/divH) | +| `ui.divV([...])` | Vertical flex container | [divV()](https://datagrok.ai/api/js/ui/functions/divV) | +| `ui.panel([...])` | Panel with padding | [panel()](https://datagrok.ai/api/js/ui/functions/panel) | +| `ui.box(element)` | Fixed-size container | [box()](https://datagrok.ai/api/js/ui/functions/box) | +| `ui.splitH([...])` | Horizontal splitter (resizable) | [splitH()](https://datagrok.ai/api/js/ui/functions/splitH) | +| `ui.splitV([...])` | Vertical splitter (resizable) | [splitV()](https://datagrok.ai/api/js/ui/functions/splitV) | +| `ui.tabControl({...})` | Tabs | [tabControl()](https://datagrok.ai/api/js/ui/functions/tabControl) | +| `ui.accordion()` | Accordion | [accordion()](https://datagrok.ai/api/js/ui/functions/accordion) | diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/datagrok-coding-conventions.md b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/datagrok-coding-conventions.md new file mode 100644 index 0000000000..0ff7fa7c78 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/datagrok-coding-conventions.md @@ -0,0 +1,396 @@ +# Coding Standard: Datagrok Interactive Scientific Applications + +> **End-to-end example:** application code in `../example/code/src/levins/`. + +Extracted from the `DiffStudio` package (`packages/DiffStudio`). + +## 1. File Structure + +### 1.1. Module Organization + +Each file has a single responsibility: + +- `package.ts` — entry point, package function registration via decorators. +- `app.ts` — main UI application class. +- `constants.ts` — domain constants (not UI). +- `ui-constants.ts` — UI constants: tooltips, titles, errors, links, timeouts. +- `error-utils.ts` — custom errors and error display utilities. +- `utils.ts` — general utilities. +- `model.ts` — types and model class. +- `solver-tools.ts` — wrappers over external library (core). +- `callbacks/` — pattern: base class + concrete implementations in separate files. +- `demo/` — standalone demo models in separate files. +- `tests/` — tests, separated by categories. + +### 1.2. Comment at the Beginning of a File + +Each file starts with a single-line comment describing the module's purpose: + +```typescript +// Solver of initial value problem +``` + +For files with a detailed description, a block comment is used: + +```typescript +/* Scripting tools for the Initial Value Problem (IVP) solver: + - parser of formulas defining IVP; + - JS-script generator. +*/ +``` + +### 1.3. Import Order + +Imports follow a fixed order: + +1. Datagrok API (always three lines): + +```typescript +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; +``` + +2. External libraries (`diff-grok`, `codemirror`, `dayjs`, etc.). +3. Internal package modules (`./solver-tools`, `./constants`, etc.). +4. CSS styles (`'../css/app-styles.css'`). + +Groups are separated by a blank line. + +## 2. Constants + +### 2.1. All String Literals Go Into Constants + +String literals are not used inline in the code. All messages, titles, tooltips, and keywords are extracted into enums or consts. + +### 2.2. Constants Are Grouped Into Enums by Purpose + +```typescript +/** Tooltips messages */ +export enum HINT { + HELP = 'Open help in a new tab', + OPEN = 'Open model', + // ... +}; // HINT + +/** UI titles */ +export enum TITLE { + LOAD = 'Load...', + IMPORT = 'Import...', + // ... +}; // TITLE + +/** Error messages */ +export enum ERROR_MSG { + SOLVING_FAILS = 'Solving fails', + // ... +}; +``` + +### 2.3. Enum Naming + +- Enum name — `UPPER_CASE` (e.g., `HINT`, `TITLE`, `ERROR_MSG`, `UI_TIME`). +- Enum values — `UPPER_CASE` (e.g., `HINT.OPEN`, `UI_TIME.DOCK_EDITOR_TIMEOUT`). + +### 2.4. Composite Constants Are Built from Base Ones + +```typescript +const META = `${CONTROL_TAG}meta`; + +export enum CONTROL_EXPR { + NAME = `${CONTROL_TAG}name`, + SOLVER = `${META}.solver`, + // ... +}; +``` + +### 2.5. Maps for Linking Constants + +`Map` is used for mapping between sets of constants: + +```typescript +export const MODEL_HINT = new Map([ + [TITLE.BASIC, HINT.BASIC], + [TITLE.ADV, HINT.ADV], + // ... +]); +``` + +### 2.6. Separation of Domain and UI Constants + +- `constants.ts` — domain: parser formulas, solver settings, column names. +- `ui-constants.ts` — UI: tooltips, titles, errors, links, timeouts, dock ratios. + +## 3. Types + +### 3.1. Local Types Are Defined Near Their Usage + +```typescript +/** Numerical input specification */ +export type Input = { + value: number, + annot: string | null, +}; + +/** Argument of IVP specification */ +type Arg = { + name: string, + initial: Input, + final: Input, + step: Input, +}; +``` + +### 3.2. Types Used in Multiple Modules Are Exported + +Types needed in other files are exported from the defining file and imported at the point of use. + +## 4. Classes + +### 4.1. Access Modifiers + +Fields and methods are explicitly marked with `private` or `public`: + +```typescript +export class ModelError extends Error { + private helpUrl: string; + private toHighlight: string = undefined; + + public getHelpUrl() { return this.helpUrl; } + public getToHighlight() { return this.toHighlight; } +} +``` + +### 4.2. Closing Comment for a Class + +A comment with the class name is placed after the closing brace: + +```typescript +}; // ModelError +``` + +```typescript +}; // Model +``` + +Similarly for large enums and functions: + +```typescript +}; // HINT + +} // error + +} // showModelErrorHint +``` + +### 4.3. Inheritance Pattern (Callbacks) + +The base class is in a separate file, concrete implementations are each in their own file: + +``` +callbacks/ + callback-base.ts — base class Callback + callback-tools.ts — factory function getCallback + iter-checker-callback.ts — IterCheckerCallback extends Callback + time-checker-callback.ts — TimeCheckerCallback extends Callback +``` + +## 5. Functions + +### 5.1. JSDoc Comment Before Each Function + +Every function (exported and internal) has a single-line JSDoc comment: + +```typescript +/** Return solution as a dataframe */ +function getSolutionDF(odes: ODEs, solutionArrs: Float64Array[]): DG.DataFrame { + +/** Default solver of initial value problem. */ +export function solveDefault(odes: ODEs): DG.DataFrame { + +/** Return unused IVP-file name */ +export function unusedFileName(name: string, files: string[]): string { +``` + +### 5.2. Arrow Functions for Short Utilities + +```typescript +const getMethod = (options?: Partial) => { + // ... +}; + +const strToVal = (s: string) => { + const num = Number(s); + return !isNaN(num) ? num : s === 'true' ? true : s === 'false' ? false : s; +}; +``` + +### 5.3. Comments Inside Functions Mark Logical Steps + +```typescript +// Get numerical solution +const approxSolution = method(corProb.odes); + +// Compute error +for (let i = 0; i < pointsCount; ++i) { +``` + +```typescript +// extract function values +const vx = _y[2]; + +// evaluate expressions +const v = Math.PI * dB ** 3 / 6; + +// compute output +_output[0] = vx; +``` + +## 6. Error Handling + +### 6.1. Custom Error Class + +A separate class extending `Error` is created for domain errors: + +```typescript +export class ModelError extends Error { + private helpUrl: string; + constructor(message: string, helpUrl: string) { + super(message); + this.helpUrl = helpUrl; + } +} +``` + +### 6.2. Factory Functions for Common Errors + +```typescript +/** Return ModelError corresponding to ".. is not defined" */ +export function getIsNotDefined(msg: string): ModelError { + // ... +} +``` + +### 6.3. User Notification via Datagrok Utilities + +- `grok.shell.warning(...)` — warnings. +- `grok.shell.error(...)` — errors. +- `grok.shell.info(...)` — informational messages. + +## 7. Package Function Registration + +Datagrok supports two function registration approaches: + +1. **JSDoc-style comments** (traditional) — `//name:`, `//tags: app`, `//input:`, `//output:`. Processed by `grok api` and `grok check`. +2. **Decorators** `@grok.decorators.*` (modern) — type-safe alternative used in newer packages. + +Both approaches are valid. The example below uses decorators (as in the DiffStudio package): + +```typescript +export class PackageFunctions { + @grok.decorators.app({ + name: 'Diff Studio', + description: 'Solver of ordinary differential equations systems', + browsePath: 'Compute', + }) + static async runDiffStudio(): Promise { + // ... + } + + @grok.decorators.func({}) + static solve(@grok.decorators.param({type: 'object'}) problem: ODEs): DG.DataFrame { + return solveDefault(problem); + } + + @grok.decorators.model({ + name: 'Ball flight', + description: 'Ball flight simulation', + // ... + }) + static ballFlight(/* params */) { + // ... + } +} +``` + +## 8. Formatting (ESLint) + +The configuration extends the `google` style guide. + +- **Indentation**: 2 spaces. +- **Maximum line length**: 120 characters. +- **Curly braces**: `multi-or-nest` — single-line block without braces, multi-line — with braces. +- **Brace style**: `1tbs` with `allowSingleLine: true`. +- **Unused variables**: `warn`, exceptions — `_`, `ui`, `grok`, `DG`. +- **JSDoc**: `require-jsdoc: off`, `valid-jsdoc: off` — single-line `/** */` comments are used instead. +- **Line break**: `linebreak-style: off`. + +## 9. Testing + +### 9.1. Test Organization + +Tests are separated by categories in individual files: + +- `numerical-methods-tests.ts` — solver correctness and performance. +- `features-tests.ts` — IVP format features. +- `platform-funcs-tests.ts` — platform integration. +- `pipeline-tests.ts` — end-to-end pipelines. +- `test-utils.ts` — test utilities. + +### 9.2. Framework + +`category`, `test`, `expect` from `@datagrok-libraries/test` are used: + +```typescript +import {category, expect, test} from '@datagrok-libraries/test/src/test'; + +category(`Correctness: ${name}`, () => { + corrProbs.forEach((problem) => test(problem.odes.name, async () => { + const error = getError(method, problem); + expect( + error < TINY, + true, + `The ${name} method failed to solve "${problem.odes.name}", too big error: ${error}`, + ); + }, {timeout: TIMEOUT})); +}); +``` + +### 9.3. Test Options + +- `{timeout: N}` — for tests with a time limit. +- `{benchmark: true}` — for performance tests. + +## 10. Naming + +### 10.1. Variables and Functions + +- `camelCase`: `solveDefault`, `getScriptLines`, `showModelErrorHint`. + +### 10.2. Classes and Types + +- `PascalCase`: `DiffStudio`, `ModelError`, `CallbackAction`, `ModelInfo`. + +### 10.3. Enums + +- Enum name: `UPPER_CASE` (`HINT`, `TITLE`, `ERROR_MSG`). +- Values: `UPPER_CASE` (`HINT.OPEN`, `UI_TIME.DOCK_EDITOR_TIMEOUT`). + +### 10.4. Constants Outside Enums + +- `UPPER_CASE` for primitives: `const TINY = 0.0001;` +- `camelCase` for complex objects: `const completions = [...]`, `const modelImageLink = new Map(...)`. + +### 10.5. Files + +- `kebab-case`: `solver-tools.ts`, `error-utils.ts`, `ui-constants.ts`, `ball-flight.ts`. + +## 11. CSS + +Styles are placed in a separate CSS file (`css/app-styles.css`) and imported in the modules that use them: + +```typescript +import '../css/app-styles.css'; +``` + +CSS classes are named with a package prefix: `diff-studio-hint-btns-div`, `diff-studio-highlight-text`. diff --git a/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/datagrok-document-schema.md b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/datagrok-document-schema.md new file mode 100644 index 0000000000..0726ca0383 --- /dev/null +++ b/packages/LotkaVolterraGuided/datagrok-interactive-app-guide/reference/datagrok-document-schema.md @@ -0,0 +1,146 @@ +# Document Schema: Datagrok Interactive Scientific Applications + +## Common Part (reusable, identical for all applications) + +``` +guides/ +│ +├── 1. Architecture and Philosophy +│ datagrok-interactive-app-guide.md +│ "Ports and adapters" pattern, pipelines, lifecycle, +│ subscriptions, teardown, testing. +│ +├── 2. Specification Template +│ datagrok-app-specification-template.md +│ Empty template defining the specification structure +│ for a specific application. +│ +├── 3. End-to-End Application Example +│ levins-metapopulation-spec.md +│ Completed specification for the Levins Metapopulation Model. +│ Serves as a reference for generation. +│ +└── reference/ + │ + ├── 4. Datagrok API Reference + │ datagrok-api-reference.md + │ Catalog of inputs, viewers, buttons, tooltips, dialogs, + │ layouts, notifications, subscriptions. With links to documentation. + │ + ├── 5. Coding Standard + │ datagrok-coding-conventions.md + │ File structure, constants, types, classes, functions, + │ error handling, naming, formatting, testing. + │ + └── 6. Implementation References (provided during core implementation phase) + COMPUTATION-PATTERNS.md + Working with raw data, null handling, single-pass aggregation, + data locality, bool columns, module structure. + + ARRAY-OPERATIONS.md + Typed array patterns: buffer reuse, out-parameter, + scratch buffers, ring buffer, array pool, in-place transforms. + + WORKER-GUIDE.md + Worker-utils infrastructure, WorkerColumn/WorkerDataFrame, + DG↔Worker transforms, matrix layouts, lifecycle. + + PARALLEL-EXECUTION.md + Fan-out/fan-in, distribution across workers, worker count. +``` + +## Application-Specific Part (unique for each application) + +``` +my-app-spec/ +│ +├── Application Specification (required) +│ my-app-specification.md +│ Completed specification template: core tasks, controls, +│ validation, reactivity, computation behavior, +│ rendering, layout, data lifecycle, UX. +│ +├── Method Specifications (if the application has its own methods) +│ methods/ +│ ├── method-A.md +│ │ Mathematical formulation, step-by-step algorithm, +│ │ inputs/outputs, constraints, edge cases, +│ │ literature references. +│ ├── method-B.md +│ └── ... +│ +├── UI Component Specifications (if there are custom elements) +│ ui-components/ +│ ├── component-X.md +│ │ Visual description (sketch/mockup), states, +│ │ events, styles (CSS), accessibility. +│ ├── component-Y.md +│ └── ... +│ +└── External Library Documentation (if used) + Not created, but referenced from the application specification. + Links to API reference, README, guides. +``` + +## Relationship Between Parts + +``` +┌─────────────────────────────────────────────────────┐ +│ COMMON PART │ +│ │ +│ ┌─────────────┐ ┌──────────────┐ ┌────────────┐ │ +│ │Architecture │ │ API │ │ Coding │ │ +│ │& Philosophy │ │ Reference │ │ Standard │ │ +│ └─────────────┘ └──────────────┘ └────────────┘ │ +│ ┌─────────────┐ ┌──────────────────────────────┐ │ +│ │ Specification│ │ End-to-end example │ │ +│ │ Template │ │ (specification + code) │ │ +│ └─────────────┘ └──────────────────────────────┘ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Implementation References │ │ +│ │ (computation, arrays, workers, parallelism) │ │ +│ └──────────────────────────────────────────────┘ │ +└──────────────────────────┬──────────────────────────┘ + │ + │ provided in context + │ together with + ▼ +┌─────────────────────────────────────────────────────┐ +│ APPLICATION-SPECIFIC PART │ +│ │ +│ ┌──────────────────────────────────────────────┐ │ +│ │ Application Specification │ │ +│ │ (completed template) │ │ +│ └──────┬──────────────┬───────────────┬────────┘ │ +│ │ │ │ │ +│ ▼ ▼ ▼ │ +│ ┌────────────┐ ┌─────────────┐ ┌──────────────┐ │ +│ │ Method │ │ UI Component│ │ External │ │ +│ │ Specs │ │ Specs │ │ Library │ │ +│ │ (custom) │ │ (custom) │ │ Documentation│ │ +│ └────────────┘ └─────────────┘ │ (links) │ │ +│ └──────────────┘ │ +└─────────────────────────────────────────────────────┘ + │ + ▼ + ┌─────────────────┐ + │ Generated │ + │ Application │ + │ Code │ + └─────────────────┘ +``` + +## Summary: What Is Needed to Create a New Application + +| What | Source | Created from scratch? | +|---|---|---| +| Architecture and Philosophy | Common Part | No | +| Datagrok API Reference | Common Part | No | +| Coding Standard | Common Part | No | +| Specification Template | Common Part | No | +| End-to-End Example | Common Part | No | +| Implementation References | Common Part | No | +| Application Specification | Application-Specific Part | Yes, for each application | +| Method Specifications | Application-Specific Part | Yes, if the application has its own methods | +| UI Component Specifications | Application-Specific Part | Yes, if there are custom elements | +| External Library Documentation | Application-Specific Part (links) | No, already exists | diff --git a/packages/LotkaVolterraGuided/detectors.js b/packages/LotkaVolterraGuided/detectors.js new file mode 100644 index 0000000000..a1c4b85c03 --- /dev/null +++ b/packages/LotkaVolterraGuided/detectors.js @@ -0,0 +1,9 @@ +/** + * The class contains semantic type detectors. + * Detectors are functions tagged with `DG.FUNC_TYPES.SEM_TYPE_DETECTOR`. + * See also: https://datagrok.ai/help/develop/how-to/define-semantic-type-detectors + * The class name is comprised of and the `PackageDetectors` suffix. + * Follow this naming convention to ensure that your detectors are properly loaded. + */ +class LotkaVolterraGuidedPackageDetectors extends DG.Package { +} diff --git a/packages/LotkaVolterraGuided/package-lock.json b/packages/LotkaVolterraGuided/package-lock.json new file mode 100644 index 0000000000..68c41c16e8 --- /dev/null +++ b/packages/LotkaVolterraGuided/package-lock.json @@ -0,0 +1,2436 @@ +{ + "name": "lotkavolterraguided", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lotkavolterraguided", + "version": "0.0.1", + "dependencies": { + "@datagrok-libraries/utils": "^4.6.5", + "cash-dom": "^8.1.5", + "datagrok-api": "^1.26.0", + "dayjs": "^1.11.13", + "diff-grok": "^1.2.0" + }, + "devDependencies": { + "css-loader": "^6.8.1", + "style-loader": "^3.3.3", + "ts-loader": "latest", + "typescript": "latest", + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@datagrok-libraries/chem-meta": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@datagrok-libraries/chem-meta/-/chem-meta-1.2.10.tgz", + "integrity": "sha512-05Qfw1ul3I1lipNQNBTeO8zNBQisF2bJupw7nw/SfEhohH5578SXVMUCjwMAGxkTP//rnsDcfn9pZBN+fjj//A==", + "dependencies": { + "wu": "^2.1.0" + } + }, + "node_modules/@datagrok-libraries/utils": { + "version": "4.6.14", + "resolved": "https://registry.npmjs.org/@datagrok-libraries/utils/-/utils-4.6.14.tgz", + "integrity": "sha512-3hgNM3m30tsn3mdwlXJb8z5k4RnW3KkJdIiTJaib7rojo47AyERT9ix5gzaCj9ipIMmDoI8yljyLhiKR5CBNAw==", + "dependencies": { + "cash-dom": "^8.1.1", + "datagrok-api": "^1.26.0", + "dayjs": "=1.11.10", + "fast-sha256": "^1.3.0", + "js-base64": "^3.7.5", + "rxjs": "^6.5.5", + "wu": "^2.1.0" + } + }, + "node_modules/@datagrok-libraries/utils/node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "license": "MIT" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", + "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/wu": { + "version": "2.1.44", + "resolved": "https://registry.npmjs.org/@types/wu/-/wu-2.1.44.tgz", + "integrity": "sha512-veqvAklPyeT4DJFD66iBwzUKW5zicMDwaDShIvJmDkteQhwhXBKvgydA+yrNN8FnxWUFCI5y9+a2DI5sSwMUlQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cash-dom": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/cash-dom/-/cash-dom-8.1.5.tgz", + "integrity": "sha512-/BS05CfzyHR5xT2ksKj1sDLPaOv5rSmIwoGxNgdKwUtnIuiJ5neMxVEmZxvfyJiSjGbOMD0Lwe+9v+fszDqHew==", + "license": "MIT" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-loader": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/css-loader/-/css-loader-6.11.0.tgz", + "integrity": "sha512-CTJ+AEQJjq5NzLga5pE39qdiSV56F8ywCIsqNIRF0r7BDgWsN25aazToqAFg7ZrtA/U016xudB3ffgweORxX7g==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.1.0", + "postcss": "^8.4.33", + "postcss-modules-extract-imports": "^3.1.0", + "postcss-modules-local-by-default": "^4.0.5", + "postcss-modules-scope": "^3.2.0", + "postcss-modules-values": "^4.0.0", + "postcss-value-parser": "^4.2.0", + "semver": "^7.5.4" + }, + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "@rspack/core": "0.x || 1.x", + "webpack": "^5.0.0" + }, + "peerDependenciesMeta": { + "@rspack/core": { + "optional": true + }, + "webpack": { + "optional": true + } + } + }, + "node_modules/css-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/cssesc": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/cssesc/-/cssesc-3.0.0.tgz", + "integrity": "sha512-/Tb/JcjK111nNScGob5MNtsntNM1aCNUDipB/TkwZFhyDrrE47SOx/18wF2bbjgc3ZzCSKW1T5nt5EbFoAz/Vg==", + "dev": true, + "license": "MIT", + "bin": { + "cssesc": "bin/cssesc" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/datagrok-api": { + "version": "1.26.8", + "resolved": "https://registry.npmjs.org/datagrok-api/-/datagrok-api-1.26.8.tgz", + "integrity": "sha512-MaXfheRRsk4ZJjGr+ATPcw/NGCGcIwi56QCT4QIbyiZSbedwhPeRoXYEvFvwdYCNwZtouQuWp8U6dOXSM4GLbA==", + "dependencies": { + "@babel/core": "^7.27.1", + "@datagrok-libraries/chem-meta": "^1.0.12", + "@types/react": "^18.3.11", + "@types/wu": "^2.1.44", + "cash-dom": "^8.1.5", + "dayjs": "^1.11.10", + "openchemlib": "^7.2.3", + "react": "^18.3.1", + "rxjs": "^6.5.5", + "typeahead-standalone": "4.14.1", + "ws": "^8.18.2", + "wu": "^2.1.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff-grok": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/diff-grok/-/diff-grok-1.2.0.tgz", + "integrity": "sha512-qjU07sXsLVy/Z5YTSTYwzHvFnFCZyUPd2JYpGTVSi06R6b6qBCzpa4ZEmWoybeRFproGmNlfam7JhhFMPPcjoQ==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/icss-utils": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/icss-utils/-/icss-utils-5.1.0.tgz", + "integrity": "sha512-soFhflCVWLfRNOPU3iv5Z9VUdT44xFRbzjLsEzSr5AQmgqPMTHdU3PMT1Cf1ssx8fLNJDA1juftYl+PUcv3MqA==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/nanoid": { + "version": "3.3.11", + "resolved": "https://registry.npmjs.org/nanoid/-/nanoid-3.3.11.tgz", + "integrity": "sha512-N8SpfPUnUp1bK+PMYW8qSWdl9U+wwNWI4QKxOYDy9JAro3WMX7p2OeVRF9v+347pnakNevPmiHhNmZ2HbFA76w==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "bin": { + "nanoid": "bin/nanoid.cjs" + }, + "engines": { + "node": "^10 || ^12 || ^13.7 || ^14 || >=15.0.1" + } + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "license": "MIT" + }, + "node_modules/openchemlib": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/openchemlib/-/openchemlib-7.5.0.tgz", + "integrity": "sha512-cxEmgL1Szuw5zPDX29PyuAIkokSKPkzEIc/61oPA84GqvGyjMMRrGaF4tbFCDOT4c7ULZ/qmIWk9/ERj3wOg1w==", + "license": "BSD-3-Clause" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/postcss": { + "version": "8.5.8", + "resolved": "https://registry.npmjs.org/postcss/-/postcss-8.5.8.tgz", + "integrity": "sha512-OW/rX8O/jXnm82Ey1k44pObPtdblfiuWnrd8X7GJ7emImCOstunGbXUpp7HdBrFQX6rJzn3sPT397Wp5aCwCHg==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/postcss/" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/postcss" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "nanoid": "^3.3.11", + "picocolors": "^1.1.1", + "source-map-js": "^1.2.1" + }, + "engines": { + "node": "^10 || ^12 || >=14" + } + }, + "node_modules/postcss-modules-extract-imports": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/postcss-modules-extract-imports/-/postcss-modules-extract-imports-3.1.0.tgz", + "integrity": "sha512-k3kNe0aNFQDAZGbin48pL2VNidTF0w4/eASDsxlyspobzU3wZQLOGj7L9gfRe0Jo9/4uud09DsjFNH7winGv8Q==", + "dev": true, + "license": "ISC", + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-local-by-default": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-modules-local-by-default/-/postcss-modules-local-by-default-4.2.0.tgz", + "integrity": "sha512-5kcJm/zk+GJDSfw+V/42fJ5fhjL5YbFDl8nVdXkJPLLW+Vf9mTD5Xe0wqIaDnLuL2U6cDNpTr+UQ+v2HWIBhzw==", + "dev": true, + "license": "MIT", + "dependencies": { + "icss-utils": "^5.0.0", + "postcss-selector-parser": "^7.0.0", + "postcss-value-parser": "^4.1.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-scope": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/postcss-modules-scope/-/postcss-modules-scope-3.2.1.tgz", + "integrity": "sha512-m9jZstCVaqGjTAuny8MdgE88scJnCiQSlSrOWcTQgM2t32UBe+MUmFSO5t7VMSfAf/FJKImAxBav8ooCHJXCJA==", + "dev": true, + "license": "ISC", + "dependencies": { + "postcss-selector-parser": "^7.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-modules-values": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/postcss-modules-values/-/postcss-modules-values-4.0.0.tgz", + "integrity": "sha512-RDxHkAiEGI78gS2ofyvCsu7iycRv7oqw5xMWn9iMoR0N/7mf9D50ecQqUo5BZ9Zh2vH4bCUR/ktCqbB9m8vJjQ==", + "dev": true, + "license": "ISC", + "dependencies": { + "icss-utils": "^5.0.0" + }, + "engines": { + "node": "^10 || ^12 || >= 14" + }, + "peerDependencies": { + "postcss": "^8.1.0" + } + }, + "node_modules/postcss-selector-parser": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-7.1.1.tgz", + "integrity": "sha512-orRsuYpJVw8LdAwqqLykBj9ecS5/cRHlI5+nvTo8LcCKmzDmqVORXtOIYEEQuL9D4BxtA1lm5isAqzQZCoQ6Eg==", + "dev": true, + "license": "MIT", + "dependencies": { + "cssesc": "^3.0.0", + "util-deprecate": "^1.0.2" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/postcss-value-parser": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/postcss-value-parser/-/postcss-value-parser-4.2.0.tgz", + "integrity": "sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-js": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.1.tgz", + "integrity": "sha512-UXWMKhLOwVKb728IUtQPXxfYU+usdybtUrK/8uGE8CQMvrhOpwvzDBwj0QhSL7MQc7vIsISBG8VQ8+IDQxpfQA==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/style-loader": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/style-loader/-/style-loader-3.3.4.tgz", + "integrity": "sha512-0WqXzrsMTyb8yjZJHDqwmnwRJvhALK9LfRtRc6B4UTWe8AijYLZYZ9thuJTZc2VfQWINADW/j+LiJnfy2RoC1w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 12.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.0.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/typeahead-standalone": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/typeahead-standalone/-/typeahead-standalone-4.14.1.tgz", + "integrity": "sha512-K+mqXmHferhxlyFD5blmOV9UIlazUxumyLWxO5QXnD1cjL6uQ6JGuqvjk0rHt4uz5cD2POAFxnyfkyZUD1ce7A==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/util-deprecate": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", + "integrity": "sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==", + "dev": true, + "license": "MIT" + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wu": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wu/-/wu-2.1.0.tgz", + "integrity": "sha512-j+Gdt5IUK4eoLO6mrN/ZurInHacaxr/EPCvQHf1ARq6ROdKRN/aFtc0PGdH9lnRPMg6vhJOOqIYdNhMN6uWtUg==" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/packages/LotkaVolterraGuided/package.json b/packages/LotkaVolterraGuided/package.json new file mode 100644 index 0000000000..d53a9f080e --- /dev/null +++ b/packages/LotkaVolterraGuided/package.json @@ -0,0 +1,45 @@ +{ + "name": "lotkavolterraguided", + "friendlyName": "LotkaVolterraGuided", + "version": "0.0.1", + "description": "LotkaVolterraGuided package", + "dependencies": { + "datagrok-api": "^1.26.0", + "cash-dom": "^8.1.5", + "dayjs": "^1.11.13", + "@datagrok-libraries/utils": "^4.6.5", + "diff-grok": "^1.2.0" + }, + "devDependencies": { + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4", + "css-loader": "^6.8.1", + "style-loader": "^3.3.3", + "ts-loader": "latest", + "typescript": "latest" + }, + "scripts": { + "debug-lotkavolterraguided": "webpack && grok publish", + "release-lotkavolterraguided": "webpack && grok publish --release", + "build-lotkavolterraguided": "webpack", + "build": "grok api && grok check --soft && webpack", + "test": "grok test", + "debug-lotkavolterraguided-dev": "webpack && grok publish dev", + "release-lotkavolterraguided-dev": "webpack && grok publish dev --release", + "debug-lotkavolterraguided-local": "webpack && grok publish local", + "release-lotkavolterraguided-local": "webpack && grok publish local --release", + "debug-lotkavolterraguided-release": "webpack && grok publish release", + "release-lotkavolterraguided-release": "webpack && grok publish release --release" + }, + "canEdit": [ + "Developers" + ], + "canView": [ + "All users" + ], + "repository": { + "type": "git", + "url": "https://github.com/datagrok-ai/public.git", + "directory": "packages/LotkaVolterraGuided" + } +} \ No newline at end of file diff --git a/packages/LotkaVolterraGuided/package.png b/packages/LotkaVolterraGuided/package.png new file mode 100644 index 0000000000..77aceb1bab Binary files /dev/null and b/packages/LotkaVolterraGuided/package.png differ diff --git a/packages/LotkaVolterraGuided/src/lotka-volterra/app.ts b/packages/LotkaVolterraGuided/src/lotka-volterra/app.ts new file mode 100644 index 0000000000..1eb1350e50 --- /dev/null +++ b/packages/LotkaVolterraGuided/src/lotka-volterra/app.ts @@ -0,0 +1,449 @@ +// Lotka-Volterra Predator-Prey Simulation — Application (Coordinator + UI) + +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; + +import { + DEFAULTS, RANGES, validate, solve, + LotkaVolterraParams, LotkaVolterraSolution, InputId, WorkerTask, WorkerResult, +} from './core'; + +import '../../css/lotka-volterra.css'; + +const DEBOUNCE_MS = 50; +const GRID_STEPS = 11; // 0%, 10%, ..., 100% of range +const TOTAL_GRID_POINTS = GRID_STEPS ** 4; // 14641 + +export function lotkaVolterraApp(_package: DG.Package): void { + // --- State --- + let computationsBlocked = false; + let debounceTimer: ReturnType | null = null; + const subs: {unsubscribe(): void}[] = []; + let activeWorkers: Worker[] = []; + let lineChart!: DG.Viewer; + let scatterPlot!: DG.Viewer; + + // --- Initial DataFrame --- + const initSolution = solve(DEFAULTS); + const df = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', initSolution.t), + DG.Column.fromFloat64Array('x', initSolution.x), + DG.Column.fromFloat64Array('y', initSolution.y), + ]); + df.name = 'Lotka-Volterra'; + + const view = grok.shell.addTableView(df); + view.name = 'Lotka-Volterra Predator-Prey'; + + // --- Stats panel --- + const statsPanel = ui.div([], 'lv-stats-panel'); + + function updateStatsPanel(sol: LotkaVolterraSolution): void { + statsPanel.innerHTML = ''; + const lines = [ + `Equilibrium:`, + ` x* = ${sol.xStar.toFixed(2)}, y* = ${sol.yStar.toFixed(2)}`, + `Max prey: ${sol.maxPrey.toFixed(2)}`, + `Max predators: ${sol.maxPredators.toFixed(2)}`, + `Steps: ${sol.stepCount}`, + ]; + statsPanel.innerHTML = lines.join('
'); + } + updateStatsPanel(initSolution); + + // --- Controls --- + + // Model Coefficients + const ctrlAlpha = ui.input.float('Prey birth rate \u03B1', { + value: DEFAULTS.alpha, nullable: false, + min: RANGES.alpha.min, max: RANGES.alpha.max, + tooltipText: 'Rate at which prey reproduce. Higher \u03B1 \u2192 faster prey growth in the absence of predators.', + onValueChanged: () => debouncedRun(), + }); + + const ctrlBeta = ui.input.float('Predation rate \u03B2', { + value: DEFAULTS.beta, nullable: false, + min: RANGES.beta.min, max: RANGES.beta.max, + tooltipText: 'Rate at which predators consume prey. Higher \u03B2 \u2192 more prey eaten per encounter, reducing prey population faster.', + onValueChanged: () => debouncedRun(), + }); + + const ctrlDelta = ui.input.float('Predator efficiency \u03B4', { + value: DEFAULTS.delta, nullable: false, + min: RANGES.delta.min, max: RANGES.delta.max, + tooltipText: 'Efficiency of converting consumed prey into predator growth. Higher \u03B4 \u2192 predators grow faster from each prey consumed.', + onValueChanged: () => debouncedRun(), + }); + + const ctrlGamma = ui.input.float('Predator death rate \u03B3', { + value: DEFAULTS.gamma, nullable: false, + min: RANGES.gamma.min, max: RANGES.gamma.max, + tooltipText: 'Natural death rate of predators. Higher \u03B3 \u2192 predators die off faster without sufficient prey.', + onValueChanged: () => debouncedRun(), + }); + + // Initial conditions + const ctrlX0 = ui.input.float('Initial prey x\u2080', { + value: DEFAULTS.x0, nullable: false, + min: RANGES.x0.min, max: RANGES.x0.max, + tooltipText: 'Starting prey population at time t=0.', + onValueChanged: () => debouncedRun(), + }); + + const ctrlY0 = ui.input.float('Initial predators y\u2080', { + value: DEFAULTS.y0, nullable: false, + min: RANGES.y0.min, max: RANGES.y0.max, + tooltipText: 'Starting predator population at time t=0.', + onValueChanged: () => debouncedRun(), + }); + + const ctrlT = ui.input.float('Simulation time T', { + value: DEFAULTS.T, nullable: false, + min: RANGES.T.min, max: RANGES.T.max, + tooltipText: 'Total simulation time. Longer T shows more oscillation cycles.', + onValueChanged: () => debouncedRun(), + }); + + // Set formats (block computations to avoid spurious runs from format-triggered events) + computationsBlocked = true; + ctrlAlpha.format = '0.00'; + ctrlBeta.format = '0.000'; + ctrlDelta.format = '0.000'; + ctrlGamma.format = '0.00'; + ctrlX0.format = '0.0'; + ctrlY0.format = '0.0'; + ctrlT.format = '0.0'; + computationsBlocked = false; + + // --- Input map for validators --- + const inputMap: Record = { + 'ctrl_alpha': ctrlAlpha, + 'ctrl_beta': ctrlBeta, + 'ctrl_delta': ctrlDelta, + 'ctrl_gamma': ctrlGamma, + 'ctrl_x0': ctrlX0, + 'ctrl_y0': ctrlY0, + 'ctrl_T': ctrlT, + }; + + // --- Gather current inputs --- + function getInputs(): LotkaVolterraParams { + return { + alpha: ctrlAlpha.value ?? DEFAULTS.alpha, + beta: ctrlBeta.value ?? DEFAULTS.beta, + delta: ctrlDelta.value ?? DEFAULTS.delta, + gamma: ctrlGamma.value ?? DEFAULTS.gamma, + x0: ctrlX0.value ?? DEFAULTS.x0, + y0: ctrlY0.value ?? DEFAULTS.y0, + T: ctrlT.value ?? DEFAULTS.T, + }; + } + + // --- Validators --- + function addValidators(): void { + const validatorFor = (id: InputId) => { + return () => { + const inputs = getInputs(); + const errors = validate(inputs); + return errors.get(id) ?? null; + }; + }; + + ctrlAlpha.addValidator(validatorFor('ctrl_alpha')); + ctrlBeta.addValidator(validatorFor('ctrl_beta')); + ctrlDelta.addValidator(validatorFor('ctrl_delta')); + ctrlGamma.addValidator(validatorFor('ctrl_gamma')); + ctrlX0.addValidator(validatorFor('ctrl_x0')); + ctrlY0.addValidator(validatorFor('ctrl_y0')); + ctrlT.addValidator(validatorFor('ctrl_T')); + } + addValidators(); + + // --- Primary pipeline --- + function runPrimary(): void { + if (computationsBlocked) + return; + + const inputs = getInputs(); + const errors = validate(inputs); + + // Clear previous errors on all inputs + for (const input of Object.values(inputMap)) + input.input?.classList.remove('d4-invalid'); + + if (errors.size > 0) { + errors.forEach((_msg, id) => { + const input = inputMap[id]; + if (input) + input.input?.classList.add('d4-invalid'); + }); + clearResults(); + return; + } + + try { + const result = solve(inputs); + updateDataFrame(result); + updateStatsPanel(result); + } catch (err) { + clearResults(); + const msg = err instanceof Error ? err.message : 'Computation error'; + grok.shell.error(msg); + } + } + + function debouncedRun(): void { + if (debounceTimer !== null) + clearTimeout(debounceTimer); + debounceTimer = setTimeout(() => runPrimary(), DEBOUNCE_MS); + } + + // --- Update DataFrame --- + function updateDataFrame(result: LotkaVolterraSolution): void { + const newDf = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', result.t), + DG.Column.fromFloat64Array('x', result.x), + DG.Column.fromFloat64Array('y', result.y), + ]); + newDf.name = 'Lotka-Volterra'; + view.dataFrame = newDf; + lineChart.dataFrame = newDf; + scatterPlot.dataFrame = newDf; + } + + function clearResults(): void { + const emptyDf = DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', new Float64Array(0)), + DG.Column.fromFloat64Array('x', new Float64Array(0)), + DG.Column.fromFloat64Array('y', new Float64Array(0)), + ]); + emptyDf.name = 'Lotka-Volterra'; + view.dataFrame = emptyDf; + lineChart.dataFrame = emptyDf; + scatterPlot.dataFrame = emptyDf; + } + + // --- Optimization task --- + const optimizeBtn = ui.bigButton('Optimize', () => runOptimization(), + 'Run brute-force grid search over all four model coefficients to maximize peak prey population'); + + function setOptimizeEnabled(enabled: boolean): void { + optimizeBtn.classList.toggle('lv-optimize-btn--disabled', !enabled); + } + + async function runOptimization(): Promise { + const inputs = getInputs(); + const errors = validate(inputs); + if (errors.size > 0) { + grok.shell.error('Invalid parameters. Fix inputs before optimizing.'); + return; + } + + setOptimizeEnabled(false); + + const workerCount = Math.max(1, (navigator.hardwareConcurrency ?? 4) - 2); + + // Generate grid points: 11 steps per parameter (10% of range each) + const alphaValues = linspace(RANGES.alpha.min, RANGES.alpha.max, GRID_STEPS); + const betaValues = linspace(RANGES.beta.min, RANGES.beta.max, GRID_STEPS); + const deltaValues = linspace(RANGES.delta.min, RANGES.delta.max, GRID_STEPS); + const gammaValues = linspace(RANGES.gamma.min, RANGES.gamma.max, GRID_STEPS); + + const tasks: WorkerTask[] = []; + for (const a of alphaValues) { + for (const b of betaValues) { + for (const d of deltaValues) { + for (const g of gammaValues) { + tasks.push({alpha: a, beta: b, delta: d, gamma: g, x0: inputs.x0, y0: inputs.y0, T: inputs.T}); + } + } + } + } + + const workerUrl = _package.webRoot + 'dist/optimize-worker.js'; + const nWorkers = Math.min(workerCount, tasks.length); + const chunks: WorkerTask[][] = Array.from({length: nWorkers}, () => []); + for (let i = 0; i < tasks.length; i++) + chunks[i % nWorkers].push(tasks[i]); + + activeWorkers = []; + const pi = DG.TaskBarProgressIndicator.create('Optimizing Max Prey...', {cancelable: true}); + + const resolvers = new Array<(value: WorkerResult[]) => void>(chunks.length); + const batchPromises = chunks.map((batch, i) => + new Promise((resolve, reject) => { + resolvers[i] = resolve; + let worker: Worker; + try { + worker = new Worker(workerUrl); + } catch (_err) { + reject(new Error('Failed to start parallel computations. Try again later.')); + return; + } + activeWorkers.push(worker); + + worker.onmessage = (event: MessageEvent) => { + worker.terminate(); + activeWorkers = activeWorkers.filter((w) => w !== worker); + resolve(event.data); + }; + + worker.onerror = (err) => { + worker.terminate(); + activeWorkers = activeWorkers.filter((w) => w !== worker); + reject(new Error(err.message ?? 'Worker error')); + }; + + worker.postMessage(batch); + }), + ); + + const cancelSub = pi.onCanceled.subscribe(() => { + terminateWorkers(); + for (const resolve of resolvers) + resolve([]); + }); + + const settled = await Promise.allSettled(batchPromises); + + cancelSub.unsubscribe(); + pi.close(); + terminateWorkers(); + setOptimizeEnabled(true); + + if (pi.canceled) + return; + + const results: WorkerResult[] = []; + let errorCount = 0; + for (let i = 0; i < settled.length; i++) { + const outcome = settled[i]; + if (outcome.status === 'fulfilled') { + for (const r of outcome.value) { + if (r.error) + errorCount++; + else + results.push(r); + } + } else { + errorCount += chunks[i].length; + } + } + + if (errorCount > 0 && errorCount < TOTAL_GRID_POINTS) + grok.shell.warning(`${errorCount} of ${TOTAL_GRID_POINTS} points failed. Result based on ${TOTAL_GRID_POINTS - errorCount} points.`); + + if (results.length === 0) { + grok.shell.error('Failed to compute any point. Check the parameters.'); + return; + } + + // Find optimal + let best = results[0]; + for (const r of results) { + if (r.maxPrey > best.maxPrey) + best = r; + } + + // Batch update: block primary, write optimal values, unblock and run once + try { + computationsBlocked = true; + ctrlAlpha.value = best.alpha; + ctrlBeta.value = best.beta; + ctrlDelta.value = best.delta; + ctrlGamma.value = best.gamma; + computationsBlocked = false; + runPrimary(); + } catch (_err) { + computationsBlocked = false; + grok.shell.warning(`Optimal values found but could not update sliders automatically.`); + } + } + + function terminateWorkers(): void { + for (const w of activeWorkers) + w.terminate(); + activeWorkers = []; + } + + function linspace(min: number, max: number, steps: number): number[] { + const arr: number[] = []; + for (let i = 0; i < steps; i++) + arr.push(min + i * (max - min) / (steps - 1)); + return arr; + } + + // --- Toolbar buttons --- + const resetBtn = ui.iconFA('undo', () => { + computationsBlocked = true; + ctrlAlpha.value = DEFAULTS.alpha; + ctrlBeta.value = DEFAULTS.beta; + ctrlDelta.value = DEFAULTS.delta; + ctrlGamma.value = DEFAULTS.gamma; + ctrlX0.value = DEFAULTS.x0; + ctrlY0.value = DEFAULTS.y0; + ctrlT.value = DEFAULTS.T; + computationsBlocked = false; + runPrimary(); + }, 'Reset all parameters to default values'); + + view.setRibbonPanels([[resetBtn]]); + + // --- Layout: left panel with form --- + const form = ui.form([]); + + form.append(ui.h2('Model Coefficients')); + form.append(ctrlAlpha.root); + form.append(ctrlBeta.root); + form.append(ctrlDelta.root); + form.append(ctrlGamma.root); + + form.append(ui.h2('Initial Conditions')); + form.append(ctrlX0.root); + form.append(ctrlY0.root); + form.append(ctrlT.root); + + form.append(optimizeBtn); + + form.append(ui.h2('Equilibrium & Stats')); + form.append(statsPanel); + + const dockMng = view.dockManager; + dockMng.dock(form, DG.DOCK_TYPE.LEFT, null, undefined, 0.1); + + // --- Line chart (time series) --- + lineChart = view.addViewer('Line chart', { + xColumnName: 't', + yColumnNames: ['x', 'y'], + title: 'Population Dynamics', + }); + + // --- Phase portrait (scatter plot) --- + scatterPlot = view.addViewer('Scatter plot', { + xColumnName: 'x', + yColumnName: 'y', + title: 'Phase Portrait', + markerDefaultSize: 2, + }); + + // Dock: line chart top-center, phase portrait bottom-center, grid right + const gridNode = dockMng.findNode(view.grid.root); + if (gridNode != null) { + dockMng.dock(lineChart, DG.DOCK_TYPE.LEFT, gridNode, undefined, 0.65); + const lineChartNode = dockMng.findNode(lineChart.root); + if (lineChartNode != null) + dockMng.dock(scatterPlot, DG.DOCK_TYPE.DOWN, lineChartNode, undefined, 0.5); + } + + // --- Cleanup on close --- + subs.push(grok.events.onViewRemoved.subscribe((v: any) => { + if (v === view) { + terminateWorkers(); + for (const sub of subs) + sub.unsubscribe(); + if (debounceTimer !== null) + clearTimeout(debounceTimer); + } + })); +} diff --git a/packages/LotkaVolterraGuided/src/lotka-volterra/core.ts b/packages/LotkaVolterraGuided/src/lotka-volterra/core.ts new file mode 100644 index 0000000000..18b879b915 --- /dev/null +++ b/packages/LotkaVolterraGuided/src/lotka-volterra/core.ts @@ -0,0 +1,132 @@ +// Lotka-Volterra Predator-Prey Model — Computational Core + +import {mrt} from 'diff-grok'; + +import {LotkaVolterraParams, createLotkaVolterraODE, getEquilibrium} from './model'; + +// --- Re-exports --- + +export type {LotkaVolterraParams} from './model'; +export {createLotkaVolterraODE, getEquilibrium} from './model'; + +// --- Types --- + +export interface LotkaVolterraSolution { + t: Float64Array; + x: Float64Array; + y: Float64Array; + xStar: number; + yStar: number; + maxPrey: number; + maxPredators: number; + stepCount: number; +} + +export type InputId = 'ctrl_alpha' | 'ctrl_beta' | 'ctrl_delta' | 'ctrl_gamma' | + 'ctrl_x0' | 'ctrl_y0' | 'ctrl_T'; + +export type ValidationErrors = Map; + +// --- Defaults --- + +export const DEFAULTS: LotkaVolterraParams = { + alpha: 1.0, + beta: 0.1, + delta: 0.075, + gamma: 1.5, + x0: 10, + y0: 5, + T: 100, +}; + +// --- Slider ranges --- + +export const RANGES: Record = { + alpha: {min: 0.1, max: 3.0}, + beta: {min: 0.01, max: 0.5}, + delta: {min: 0.01, max: 0.5}, + gamma: {min: 0.1, max: 3.0}, + x0: {min: 1, max: 200}, + y0: {min: 1, max: 100}, + T: {min: 10, max: 500}, +}; + +// --- Validation --- + +export function validate(inputs: LotkaVolterraParams): ValidationErrors { + const errors: ValidationErrors = new Map(); + const {alpha, beta, delta, gamma, x0, y0, T} = inputs; + + if (alpha <= 0) + errors.set('ctrl_alpha', 'Prey birth rate must be positive'); + + if (beta <= 0) + errors.set('ctrl_beta', 'Predation rate must be positive'); + + if (delta <= 0) + errors.set('ctrl_delta', 'Predator efficiency must be positive'); + + if (gamma <= 0) + errors.set('ctrl_gamma', 'Predator death rate must be positive'); + + if (x0 <= 0) + errors.set('ctrl_x0', 'Initial prey population must be positive'); + + if (y0 <= 0) + errors.set('ctrl_y0', 'Initial predator population must be positive'); + + if (T <= 0) + errors.set('ctrl_T', 'Simulation time must be positive'); + + return errors; +} + +// --- Solver --- + +export function solve(inputs: LotkaVolterraParams): LotkaVolterraSolution { + const task = createLotkaVolterraODE(inputs); + const solution = mrt(task); + + const t = solution[0]; + const x = solution[1]; + const y = solution[2]; + + const eq = getEquilibrium(inputs.alpha, inputs.beta, inputs.delta, inputs.gamma); + + let maxPrey = 0; + let maxPredators = 0; + for (let i = 0; i < x.length; i++) { + if (x[i] > maxPrey) maxPrey = x[i]; + if (y[i] > maxPredators) maxPredators = y[i]; + } + + return { + t, x, y, + xStar: eq.xStar, + yStar: eq.yStar, + maxPrey, + maxPredators, + stepCount: t.length, + }; +} + +// --- Worker message types --- + +export interface WorkerTask { + alpha: number; + beta: number; + delta: number; + gamma: number; + x0: number; + y0: number; + T: number; +} + +export interface WorkerResult { + alpha: number; + beta: number; + delta: number; + gamma: number; + maxPrey: number; + error?: string; +} diff --git a/packages/LotkaVolterraGuided/src/lotka-volterra/model.ts b/packages/LotkaVolterraGuided/src/lotka-volterra/model.ts new file mode 100644 index 0000000000..8e3fa4250e --- /dev/null +++ b/packages/LotkaVolterraGuided/src/lotka-volterra/model.ts @@ -0,0 +1,41 @@ +// Lotka-Volterra Predator-Prey Model — ODE specification + +import {ODEs} from 'diff-grok'; + +/** Parameters for the Lotka-Volterra ODE system */ +export interface LotkaVolterraParams { + alpha: number; + beta: number; + delta: number; + gamma: number; + x0: number; + y0: number; + T: number; +} + +/** Creates the ODEs specification for the Lotka-Volterra model */ +export function createLotkaVolterraODE(params: LotkaVolterraParams): ODEs { + const {alpha, beta, delta, gamma, x0, y0, T} = params; + + return { + name: 'LotkaVolterra', + arg: {name: 't', start: 0, finish: T, step: 0.05}, + initial: [x0, y0], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = alpha * y[0] - beta * y[0] * y[1]; + out[1] = delta * y[0] * y[1] - gamma * y[1]; + }, + tolerance: 1e-6, + solutionColNames: ['x(t)', 'y(t)'], + }; +} + +/** Computes the non-trivial equilibrium point (x*, y*) */ +export function getEquilibrium( + alpha: number, beta: number, delta: number, gamma: number, +): {xStar: number; yStar: number} { + return { + xStar: gamma / delta, + yStar: alpha / beta, + }; +} diff --git a/packages/LotkaVolterraGuided/src/lotka-volterra/optimize-worker.ts b/packages/LotkaVolterraGuided/src/lotka-volterra/optimize-worker.ts new file mode 100644 index 0000000000..da09faa88f --- /dev/null +++ b/packages/LotkaVolterraGuided/src/lotka-volterra/optimize-worker.ts @@ -0,0 +1,48 @@ +// Lotka-Volterra — Web Worker for optimization grid search + +import {mrt} from 'diff-grok'; + +import {createLotkaVolterraODE} from './model'; +import {WorkerTask, WorkerResult} from './core'; + +const ctx: Worker = self as unknown as Worker; + +ctx.onmessage = (event: MessageEvent) => { + const tasks = event.data; + const results: WorkerResult[] = []; + + for (const task of tasks) { + try { + const ode = createLotkaVolterraODE({ + alpha: task.alpha, + beta: task.beta, + delta: task.delta, + gamma: task.gamma, + x0: task.x0, + y0: task.y0, + T: task.T, + }); + + const solution = mrt(ode); + const xValues = solution[1]; + + let maxPrey = 0; + for (let i = 0; i < xValues.length; i++) { + if (xValues[i] > maxPrey) maxPrey = xValues[i]; + } + + results.push({alpha: task.alpha, beta: task.beta, delta: task.delta, gamma: task.gamma, maxPrey}); + } catch (err) { + results.push({ + alpha: task.alpha, + beta: task.beta, + delta: task.delta, + gamma: task.gamma, + maxPrey: -1, + error: err instanceof Error ? err.message : 'Unknown error', + }); + } + } + + ctx.postMessage(results); +}; diff --git a/packages/LotkaVolterraGuided/src/package-api.ts b/packages/LotkaVolterraGuided/src/package-api.ts new file mode 100644 index 0000000000..ad3a519c55 --- /dev/null +++ b/packages/LotkaVolterraGuided/src/package-api.ts @@ -0,0 +1,18 @@ +/** +This file is auto-generated by the grok api command. +If you notice any changes, please push them to the repository. +Do not edit this file manually. +*/ +import * as grok from 'datagrok-api/grok'; +import * as DG from 'datagrok-api/dg'; + + +export namespace funcs { + export async function info(): Promise { + return await grok.functions.call('LotkaVolterraGuided:Info', {}); + } + + export async function lotkaVolterraSimulation(): Promise { + return await grok.functions.call('LotkaVolterraGuided:LotkaVolterraSimulation', {}); + } +} diff --git a/packages/LotkaVolterraGuided/src/package-test.ts b/packages/LotkaVolterraGuided/src/package-test.ts new file mode 100644 index 0000000000..ec2814debf --- /dev/null +++ b/packages/LotkaVolterraGuided/src/package-test.ts @@ -0,0 +1,23 @@ +import { runTests, tests, TestContext , initAutoTests as initTests } from '@datagrok-libraries/utils/src/test'; +import * as DG from 'datagrok-api/dg'; + +export let _package = new DG.Package(); +export { tests }; + +import './tests/lotka-volterra-math-tests'; +import './tests/lotka-volterra-api-tests'; + +//name: test +//input: string category {optional: true} +//input: string test {optional: true} +//input: object testContext {optional: true} +//output: dataframe result +export async function test(category: string, test: string, testContext: TestContext): Promise { + const data = await runTests({ category, test, testContext }); + return DG.DataFrame.fromObjects(data)!; +} + +//name: initAutoTests +export async function initAutoTests() { + await initTests(_package, _package.getModule('package-test.js')); +} diff --git a/packages/LotkaVolterraGuided/src/package.g.ts b/packages/LotkaVolterraGuided/src/package.g.ts new file mode 100644 index 0000000000..8de619387a --- /dev/null +++ b/packages/LotkaVolterraGuided/src/package.g.ts @@ -0,0 +1 @@ +import * as DG from 'datagrok-api/dg'; diff --git a/packages/LotkaVolterraGuided/src/package.ts b/packages/LotkaVolterraGuided/src/package.ts new file mode 100644 index 0000000000..28d6de5090 --- /dev/null +++ b/packages/LotkaVolterraGuided/src/package.ts @@ -0,0 +1,20 @@ +/* Do not change these import lines to match external modules in webpack configuration */ +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; +export * from './package.g'; + +export const _package = new DG.Package(); + +import {lotkaVolterraApp} from './lotka-volterra/app'; + +//name: info +export function info() { + grok.shell.info(_package.webRoot); +} + +//name: Lotka-Volterra Simulation (Guided) +//tags: app +export function lotkaVolterraSimulation(): void { + lotkaVolterraApp(_package); +} diff --git a/packages/LotkaVolterraGuided/src/tests/lotka-volterra-api-tests.ts b/packages/LotkaVolterraGuided/src/tests/lotka-volterra-api-tests.ts new file mode 100644 index 0000000000..381c0dbc88 --- /dev/null +++ b/packages/LotkaVolterraGuided/src/tests/lotka-volterra-api-tests.ts @@ -0,0 +1,111 @@ +// Lotka-Volterra — API / Validation tests + +import {category, test, expect} from '@datagrok-libraries/utils/src/test'; + +import {DEFAULTS, validate} from '../lotka-volterra/core'; + +category('API: Validation', () => { + // --- val_01: alpha <= 0 --- + test('val_01: alpha = 0', async () => { + const errors = validate({...DEFAULTS, alpha: 0}); + expect(errors.has('ctrl_alpha'), true, 'Should reject alpha = 0'); + }); + + test('val_01: alpha = -1', async () => { + const errors = validate({...DEFAULTS, alpha: -1}); + expect(errors.has('ctrl_alpha'), true, 'Should reject alpha < 0'); + }); + + // --- val_02: beta <= 0 --- + test('val_02: beta = 0', async () => { + const errors = validate({...DEFAULTS, beta: 0}); + expect(errors.has('ctrl_beta'), true, 'Should reject beta = 0'); + }); + + test('val_02: beta = -0.1', async () => { + const errors = validate({...DEFAULTS, beta: -0.1}); + expect(errors.has('ctrl_beta'), true, 'Should reject beta < 0'); + }); + + // --- val_03: delta <= 0 --- + test('val_03: delta = 0', async () => { + const errors = validate({...DEFAULTS, delta: 0}); + expect(errors.has('ctrl_delta'), true, 'Should reject delta = 0'); + }); + + test('val_03: delta = -0.05', async () => { + const errors = validate({...DEFAULTS, delta: -0.05}); + expect(errors.has('ctrl_delta'), true, 'Should reject delta < 0'); + }); + + // --- val_04: gamma <= 0 --- + test('val_04: gamma = 0', async () => { + const errors = validate({...DEFAULTS, gamma: 0}); + expect(errors.has('ctrl_gamma'), true, 'Should reject gamma = 0'); + }); + + test('val_04: gamma = -1', async () => { + const errors = validate({...DEFAULTS, gamma: -1}); + expect(errors.has('ctrl_gamma'), true, 'Should reject gamma < 0'); + }); + + // --- val_05: x0 <= 0 --- + test('val_05: x0 = 0', async () => { + const errors = validate({...DEFAULTS, x0: 0}); + expect(errors.has('ctrl_x0'), true, 'Should reject x0 = 0'); + }); + + test('val_05: x0 = -5', async () => { + const errors = validate({...DEFAULTS, x0: -5}); + expect(errors.has('ctrl_x0'), true, 'Should reject x0 < 0'); + }); + + // --- val_06: y0 <= 0 --- + test('val_06: y0 = 0', async () => { + const errors = validate({...DEFAULTS, y0: 0}); + expect(errors.has('ctrl_y0'), true, 'Should reject y0 = 0'); + }); + + test('val_06: y0 = -5', async () => { + const errors = validate({...DEFAULTS, y0: -5}); + expect(errors.has('ctrl_y0'), true, 'Should reject y0 < 0'); + }); + + // --- val_07: T <= 0 --- + test('val_07: T = 0', async () => { + const errors = validate({...DEFAULTS, T: 0}); + expect(errors.has('ctrl_T'), true, 'Should reject T = 0'); + }); + + test('val_07: T = -10', async () => { + const errors = validate({...DEFAULTS, T: -10}); + expect(errors.has('ctrl_T'), true, 'Should reject T < 0'); + }); + + // --- valid defaults --- + test('Defaults pass validation', async () => { + const errors = validate(DEFAULTS); + expect(errors.size, 0, 'Default parameters should be valid'); + }); + + // --- valid boundary values --- + test('alpha = 0.1 (valid boundary)', async () => { + const errors = validate({...DEFAULTS, alpha: 0.1}); + expect(errors.has('ctrl_alpha'), false, 'Should accept alpha = 0.1'); + }); + + test('x0 = 1 (valid boundary)', async () => { + const errors = validate({...DEFAULTS, x0: 1}); + expect(errors.has('ctrl_x0'), false, 'Should accept x0 = 1'); + }); + + // --- multiple errors --- + test('Multiple simultaneous errors', async () => { + const errors = validate({...DEFAULTS, alpha: 0, beta: 0, x0: 0, T: 0}); + expect(errors.size >= 4, true, 'Should report multiple errors'); + expect(errors.has('ctrl_alpha'), true); + expect(errors.has('ctrl_beta'), true); + expect(errors.has('ctrl_x0'), true); + expect(errors.has('ctrl_T'), true); + }); +}); diff --git a/packages/LotkaVolterraGuided/src/tests/lotka-volterra-math-tests.ts b/packages/LotkaVolterraGuided/src/tests/lotka-volterra-math-tests.ts new file mode 100644 index 0000000000..6ee0c94393 --- /dev/null +++ b/packages/LotkaVolterraGuided/src/tests/lotka-volterra-math-tests.ts @@ -0,0 +1,198 @@ +// Lotka-Volterra — Math tests + +import {category, test, expect, expectFloat} from '@datagrok-libraries/utils/src/test'; +import {mrt, ODEs} from 'diff-grok'; + +import {createLotkaVolterraODE, LotkaVolterraParams} from '../lotka-volterra/model'; +import {DEFAULTS, solve, getEquilibrium} from '../lotka-volterra/core'; + +// -- Helpers -- + +/** Evaluates the ODE right-hand side at given (x, y) and returns [dx/dt, dy/dt] */ +function evalFunc(params: LotkaVolterraParams, x: number, y: number): [number, number] { + const ode = createLotkaVolterraODE(params); + const state = new Float64Array([x, y]); + const out = new Float64Array(2); + ode.func(0, state, out); + return [out[0], out[1]]; +} + +// -- ODE right-hand side verification -- + +const BASE: LotkaVolterraParams = { + alpha: 1.0, beta: 0.1, delta: 0.075, gamma: 1.5, + x0: 10, y0: 5, T: 100, +}; + +category('Math: LV func', () => { + // dx/dt = 1.0·10 − 0.1·10·5 = 10 − 5 = 5.0 + // dy/dt = 0.075·10·5 − 1.5·5 = 3.75 − 7.5 = −3.75 + test('func_01: default params, (x=10, y=5)', async () => { + const [dx, dy] = evalFunc(BASE, 10, 5); + expectFloat(dx, 5.0, 1e-12); + expectFloat(dy, -3.75, 1e-12); + }); + + // At equilibrium x*=γ/δ=20, y*=α/β=10: dx/dt=0, dy/dt=0 + test('func_02: equilibrium (x*=20, y*=10)', async () => { + const [dx, dy] = evalFunc(BASE, 20, 10); + expectFloat(dx, 0.0, 1e-10); + expectFloat(dy, 0.0, 1e-10); + }); + + // dx/dt = 1.0·30 − 0.1·30·4 = 30 − 12 = 18 + // dy/dt = 0.075·30·4 − 1.5·4 = 9 − 6 = 3.0 + test('func_03: (x=30, y=4)', async () => { + const [dx, dy] = evalFunc(BASE, 30, 4); + expectFloat(dx, 18.0, 1e-12); + expectFloat(dy, 3.0, 1e-12); + }); + + // α=2.0, β=0.5, δ=0.3, γ=1.0 + // dx/dt = 2.0·5 − 0.5·5·2 = 10 − 5 = 5.0 + // dy/dt = 0.3·5·2 − 1.0·2 = 3 − 2 = 1.0 + test('func_04: different params', async () => { + const params: LotkaVolterraParams = {...BASE, alpha: 2.0, beta: 0.5, delta: 0.3, gamma: 1.0}; + const [dx, dy] = evalFunc(params, 5, 2); + expectFloat(dx, 5.0, 1e-12); + expectFloat(dy, 1.0, 1e-12); + }); + + // α=0.5, β=0.02, δ=0.01, γ=0.3 + // dx/dt = 0.5·50 − 0.02·50·10 = 25 − 10 = 15.0 + // dy/dt = 0.01·50·10 − 0.3·10 = 5 − 3 = 2.0 + test('func_05: another param set', async () => { + const params: LotkaVolterraParams = {...BASE, alpha: 0.5, beta: 0.02, delta: 0.01, gamma: 0.3}; + const [dx, dy] = evalFunc(params, 50, 10); + expectFloat(dx, 15.0, 1e-12); + expectFloat(dy, 2.0, 1e-12); + }); +}); + +// -- Equilibrium verification -- + +category('Math: LV Equilibrium', () => { + test('eq_01: default params x*=20, y*=10', async () => { + const eq = getEquilibrium(1.0, 0.1, 0.075, 1.5); + expectFloat(eq.xStar, 20.0, 1e-10); + expectFloat(eq.yStar, 10.0, 1e-10); + }); + + test('eq_02: α=2.0, β=0.5, δ=0.3, γ=1.0 → x*=10/3, y*=4', async () => { + const eq = getEquilibrium(2.0, 0.5, 0.3, 1.0); + expectFloat(eq.xStar, 10 / 3, 1e-10); + expectFloat(eq.yStar, 4.0, 1e-10); + }); +}); + +// -- Output property verification -- + +category('Math: LV Solve properties', () => { + test('solve_01: default parameters produce non-empty arrays of equal length', async () => { + const result = solve(DEFAULTS); + expect(result.t.length > 0, true, 't should be non-empty'); + expect(result.x.length > 0, true, 'x should be non-empty'); + expect(result.y.length > 0, true, 'y should be non-empty'); + expect(result.t.length, result.x.length, 't and x should have equal length'); + expect(result.t.length, result.y.length, 't and y should have equal length'); + }); + + test('solve_02: x,y values are non-negative', async () => { + const result = solve(DEFAULTS); + for (let i = 0; i < result.x.length; i++) { + expect(result.x[i] >= -0.01, true, `x[${i}] = ${result.x[i]} is negative`); + expect(result.y[i] >= -0.01, true, `y[${i}] = ${result.y[i]} is negative`); + } + }); + + test('solve_03: initial conditions preserved', async () => { + const result = solve(DEFAULTS); + expectFloat(result.x[0], DEFAULTS.x0, 1e-6); + expectFloat(result.y[0], DEFAULTS.y0, 1e-6); + }); + + test('solve_04: t starts at 0', async () => { + const result = solve(DEFAULTS); + expectFloat(result.t[0], 0, 1e-12); + }); + + test('solve_05: equilibrium start → stays near equilibrium', async () => { + const eq = getEquilibrium(DEFAULTS.alpha, DEFAULTS.beta, DEFAULTS.delta, DEFAULTS.gamma); + const params = {...DEFAULTS, x0: eq.xStar, y0: eq.yStar, T: 50}; + const result = solve(params); + const lastX = result.x[result.x.length - 1]; + const lastY = result.y[result.y.length - 1]; + expectFloat(lastX, eq.xStar, 1.0); + expectFloat(lastY, eq.yStar, 1.0); + }); + + test('solve_06: summary stats computed correctly', async () => { + const result = solve(DEFAULTS); + expect(result.maxPrey > 0, true, 'maxPrey should be positive'); + expect(result.maxPredators > 0, true, 'maxPredators should be positive'); + expect(result.stepCount, result.t.length, 'stepCount should match t.length'); + expect(result.maxPrey >= DEFAULTS.x0, true, 'maxPrey should be at least x0'); + }); + + test('solve_07: custom initial conditions', async () => { + const result = solve({...DEFAULTS, x0: 40, y0: 15}); + expectFloat(result.x[0], 40, 1e-6); + expectFloat(result.y[0], 15, 1e-6); + }); +}); + +// -- MRT solver verification -- + +category('Math: MRT solver', () => { + test('Non-stiff 1D: dy/dt = 4\u00B7exp(0.8t) \u2212 0.5y', async () => { + // Reference: Chapra & Canale, p. 736 + const odes: ODEs = { + name: 'Non-stiff 1D', + arg: {name: 't', start: 0, finish: 4, step: 0.01}, + initial: [2], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = 4 * Math.exp(0.8 * _t) - 0.5 * y[0]; + }, + tolerance: 1e-6, + solutionColNames: ['y'], + }; + + const exact = (t: number) => + (4 / 1.3) * (Math.exp(0.8 * t) - Math.exp(-0.5 * t)) + 2 * Math.exp(-0.5 * t); + + const solution = mrt(odes); + const tArr = solution[0]; + const yArr = solution[1]; + let maxError = 0; + for (let i = 0; i < tArr.length; i++) + maxError = Math.max(maxError, Math.abs(exact(tArr[i]) - yArr[i])); + + expect(maxError < 0.1, true, `Max error ${maxError} exceeds 0.1`); + }); + + test('Stiff 1D: dy/dt = \u22121000y + 3000 \u2212 2000\u00B7exp(\u2212t)', async () => { + // Reference: Chapra & Canale, p. 767 + const odes: ODEs = { + name: 'Stiff 1D', + arg: {name: 't', start: 0, finish: 4, step: 0.01}, + initial: [0], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = -1000 * y[0] + 3000 - 2000 * Math.exp(-_t); + }, + tolerance: 5e-7, + solutionColNames: ['y'], + }; + + const exact = (t: number) => + 3 - 0.998 * Math.exp(-1000 * t) - 2.002 * Math.exp(-t); + + const solution = mrt(odes); + const tArr = solution[0]; + const yArr = solution[1]; + let maxError = 0; + for (let i = 0; i < tArr.length; i++) + maxError = Math.max(maxError, Math.abs(exact(tArr[i]) - yArr[i])); + + expect(maxError < 0.1, true, `Max error ${maxError} exceeds 0.1`); + }); +}); diff --git a/packages/LotkaVolterraGuided/tsconfig.json b/packages/LotkaVolterraGuided/tsconfig.json new file mode 100644 index 0000000000..b9b0997746 --- /dev/null +++ b/packages/LotkaVolterraGuided/tsconfig.json @@ -0,0 +1,71 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "lib": ["ES2022", "dom"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/packages/LotkaVolterraGuided/webpack.config.js b/packages/LotkaVolterraGuided/webpack.config.js new file mode 100644 index 0000000000..50780d95f8 --- /dev/null +++ b/packages/LotkaVolterraGuided/webpack.config.js @@ -0,0 +1,69 @@ +const path = require('path'); +const {execSync} = require('child_process'); +const packageName = path.parse(require('./package.json').name).name.toLowerCase().replace(/-/g, ''); + +function getDatagrokTools() { + const pluginPath = 'datagrok-tools/plugins/func-gen-plugin'; + try { + return require(pluginPath); + } catch (e) { + try { + const globalPath = execSync('npm root -g').toString().trim(); + return require(path.join(globalPath, pluginPath)); + } catch (globalErr) { + console.error('\n' + '='.repeat(60)); + console.error('ERROR: datagrok-tools not found!'); + console.error('To fix this, please install the tools globally by running:'); + console.error('\n npm install -g datagrok-tools\n'); + console.error('='.repeat(60) + '\n'); + process.exit(1); + } + } +} + +const FuncGeneratorPlugin = getDatagrokTools(); + +module.exports = { + cache: { + type: 'filesystem', + }, + mode: 'development', + entry: { + test: {filename: 'package-test.js', library: {type: 'var', name: `${packageName}_test`}, import: './src/package-test.ts'}, + package: './src/package.ts', + 'optimize-worker': {filename: 'optimize-worker.js', import: './src/lotka-volterra/optimize-worker.ts'}, + }, + resolve: { + symlinks: false, + extensions: ['.wasm', '.mjs', '.ts', '.json', '.js', '.tsx'], + }, + module: { + rules: [ + {test: /\.tsx?$/, loader: 'ts-loader', options: {allowTsInNodeModules: true}}, + {test: /\.css$/i, use: ['style-loader', 'css-loader']}, + ], + }, + plugins: [ + new FuncGeneratorPlugin({outputPath: './src/package.g.ts'}), + ], + devtool: 'source-map', + externals: { + 'datagrok-api/dg': 'DG', + 'datagrok-api/grok': 'grok', + 'datagrok-api/ui': 'ui', + 'openchemlib/full.js': 'OCL', + 'rxjs': 'rxjs', + 'rxjs/operators': 'rxjs.operators', + 'cash-dom': '$', + 'dayjs': 'dayjs', + 'wu': 'wu', + 'exceljs': 'ExcelJS', + 'html2canvas': 'html2canvas', + }, + output: { + filename: '[name].js', + library: packageName, + libraryTarget: 'var', + path: path.resolve(__dirname, 'dist'), + }, +}; diff --git a/packages/LotkaVolterraNonGuided/.gitignore b/packages/LotkaVolterraNonGuided/.gitignore new file mode 100644 index 0000000000..fb3a960466 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/.gitignore @@ -0,0 +1,33 @@ +# Developer keys +upload.keys.json + +# Dependency directories +node_modules/ + +# Webpack outputs +dist/ + +# IDEs +# VS Code (https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore) +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.history/ + +# Intellij IDEA/WebStorm (https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore) +.idea/inspectionProfiles/ +.idea/**/compiler.xml +.idea/**/encodings.xml +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/codeStyles/ + +**/*.d.ts +# Emitted *.js files +src/**/*.js diff --git a/packages/LotkaVolterraNonGuided/.npmignore b/packages/LotkaVolterraNonGuided/.npmignore new file mode 100644 index 0000000000..3b8fe98436 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/.npmignore @@ -0,0 +1,30 @@ +# Developer keys +upload.keys.json + +# Dependency directories +node_modules/ + +# IDEs +# VS Code (https://github.com/github/gitignore/blob/master/Global/VisualStudioCode.gitignore) +.vscode/* +!.vscode/settings.json +!.vscode/tasks.json +!.vscode/launch.json +!.vscode/extensions.json +*.code-workspace +.history/ + +# Intellij IDEA/WebStorm (https://github.com/github/gitignore/blob/master/Global/JetBrains.gitignore) +.idea/inspectionProfiles/ +.idea/**/compiler.xml +.idea/**/encodings.xml +.idea/**/workspace.xml +.idea/**/tasks.xml +.idea/**/usage.statistics.xml +.idea/**/dictionaries +.idea/**/shelf +.idea/codeStyles/ + + +**/*.d.ts +!src/package-api.d.ts diff --git a/packages/LotkaVolterraNonGuided/CHANGELOG.md b/packages/LotkaVolterraNonGuided/CHANGELOG.md new file mode 100644 index 0000000000..8e70f70013 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/CHANGELOG.md @@ -0,0 +1,3 @@ +# LotkaVolterraNonGuided changelog + +## 0.0.1 (2026-03-11) \ No newline at end of file diff --git a/packages/LotkaVolterraNonGuided/README.md b/packages/LotkaVolterraNonGuided/README.md new file mode 100644 index 0000000000..8a52c3612f --- /dev/null +++ b/packages/LotkaVolterraNonGuided/README.md @@ -0,0 +1,3 @@ +# LotkaVolterraNonGuided + +`LotkaVolterraNonGuided` is a [package](https://datagrok.ai/help/develop/develop#packages) for the [Datagrok](https://datagrok.ai) platform diff --git a/packages/LotkaVolterraNonGuided/detectors.js b/packages/LotkaVolterraNonGuided/detectors.js new file mode 100644 index 0000000000..a10aa5b1b5 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/detectors.js @@ -0,0 +1,9 @@ +/** + * The class contains semantic type detectors. + * Detectors are functions tagged with `DG.FUNC_TYPES.SEM_TYPE_DETECTOR`. + * See also: https://datagrok.ai/help/develop/how-to/define-semantic-type-detectors + * The class name is comprised of and the `PackageDetectors` suffix. + * Follow this naming convention to ensure that your detectors are properly loaded. + */ +class LotkaVolterraNonGuidedPackageDetectors extends DG.Package { +} diff --git a/packages/LotkaVolterraNonGuided/package-lock.json b/packages/LotkaVolterraNonGuided/package-lock.json new file mode 100644 index 0000000000..420d54d5cc --- /dev/null +++ b/packages/LotkaVolterraNonGuided/package-lock.json @@ -0,0 +1,2192 @@ +{ + "name": "lotkavolterranonguided", + "version": "0.0.1", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "lotkavolterranonguided", + "version": "0.0.1", + "dependencies": { + "@datagrok-libraries/utils": "^4.6.5", + "cash-dom": "^8.1.5", + "datagrok-api": "^1.26.0", + "dayjs": "^1.11.13", + "diff-grok": "^1.2.0" + }, + "devDependencies": { + "ts-loader": "latest", + "typescript": "latest", + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.29.0.tgz", + "integrity": "sha512-9NhCeYjq9+3uxgdtp20LSiJXJvN0FeCtNGpJxuMFZ1Kv3cWUNb6DOhJwUvcVCzKGR66cw4njwM6hrJLqgOwbcw==", + "license": "MIT", + "dependencies": { + "@babel/helper-validator-identifier": "^7.28.5", + "js-tokens": "^4.0.0", + "picocolors": "^1.1.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.29.0.tgz", + "integrity": "sha512-T1NCJqT/j9+cn8fvkt7jtwbLBfLC/1y1c7NtCeXFRgzGTsafi68MRv8yzkYSapBnFA6L3U2VSc02ciDzoAJhJg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.29.0.tgz", + "integrity": "sha512-CGOfOJqWjg2qW/Mb6zNsDm+u5vFQ8DxXfbM09z69p5Z6+mE1ikP2jUXw+j42Pf1XTYED2Rni5f95npYeuwMDQA==", + "license": "MIT", + "peer": true, + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-compilation-targets": "^7.28.6", + "@babel/helper-module-transforms": "^7.28.6", + "@babel/helpers": "^7.28.6", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/traverse": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/remapping": "^2.3.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/generator": { + "version": "7.29.1", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.29.1.tgz", + "integrity": "sha512-qsaF+9Qcm2Qv8SRIMMscAvG4O3lJ0F1GuMo5HR/Bp02LopNgnZBC/EkbevHFeGs4ls/oPz9v+Bsmzbkbe+0dUw==", + "license": "MIT", + "dependencies": { + "@babel/parser": "^7.29.0", + "@babel/types": "^7.29.0", + "@jridgewell/gen-mapping": "^0.3.12", + "@jridgewell/trace-mapping": "^0.3.28", + "jsesc": "^3.0.2" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.28.6.tgz", + "integrity": "sha512-JYtls3hqi15fcx5GaSNL7SCTJ2MNmjrkHXg4FSpOA/grxK8KwyZ5bubHsCq8FXCkua6xhuaaBit+3b7+VZRfcA==", + "license": "MIT", + "dependencies": { + "@babel/compat-data": "^7.28.6", + "@babel/helper-validator-option": "^7.27.1", + "browserslist": "^4.24.0", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-globals": { + "version": "7.28.0", + "resolved": "https://registry.npmjs.org/@babel/helper-globals/-/helper-globals-7.28.0.tgz", + "integrity": "sha512-+W6cISkXFa1jXsDEdYA8HeevQT/FULhxzR99pxphltZcVaugps53THCeiWA8SguxxpSp3gKPiuYfSWopkLQ4hw==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.28.6.tgz", + "integrity": "sha512-l5XkZK7r7wa9LucGw9LwZyyCUscb4x37JWTPz7swwFE/0FMQAGpiWUZn8u9DzkSBWEcK25jmvubfpw2dnAMdbw==", + "license": "MIT", + "dependencies": { + "@babel/traverse": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.28.6.tgz", + "integrity": "sha512-67oXFAYr2cDLDVGLXTEABjdBJZ6drElUSI7WKp70NrpyISso3plG9SAGEF6y7zbha/wOzUByWWTJvEDVNIUGcA==", + "license": "MIT", + "dependencies": { + "@babel/helper-module-imports": "^7.28.6", + "@babel/helper-validator-identifier": "^7.28.5", + "@babel/traverse": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.27.1.tgz", + "integrity": "sha512-qMlSxKbpRlAridDExk92nSobyDdpPijUq2DW6oDnUqd0iOGxmQjyqhMIihI9+zv4LPyZdRje2cavWPbCbWm3eA==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.28.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.28.5.tgz", + "integrity": "sha512-qSs4ifwzKJSV39ucNjsvc6WVHs6b7S03sOh2OcHF9UHfVPqWWALUsNUVzhSBiItjRZoLHx7nIarVjqKVusUZ1Q==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.27.1", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.27.1.tgz", + "integrity": "sha512-YvjJow9FxbhFFKDSuFnVCe2WxXk1zWc22fFePVNEaWJEu8IrZVlda6N0uHwzZrUM1il7NC9Mlp4MaJYbYd9JSg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.28.6.tgz", + "integrity": "sha512-xOBvwq86HHdB7WUDTfKfT/Vuxh7gElQ+Sfti2Cy6yIWNW05P8iUslOVcZ4/sKbE+/jQaukQAdz/gf3724kYdqw==", + "license": "MIT", + "dependencies": { + "@babel/template": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/parser": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.29.0.tgz", + "integrity": "sha512-IyDgFV5GeDUVX4YdF/3CPULtVGSXXMLh1xVIgdCgxApktqnQV0r7/8Nqthg+8YLGaAtdyIlo2qIdZrbCv4+7ww==", + "license": "MIT", + "dependencies": { + "@babel/types": "^7.29.0" + }, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/template": { + "version": "7.28.6", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.28.6.tgz", + "integrity": "sha512-YA6Ma2KsCdGb+WC6UpBVFJGXL58MDA6oyONbjyF/+5sBgxY/dwkhLogbMT2GXXyU84/IhRw/2D1Os1B/giz+BQ==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.28.6", + "@babel/parser": "^7.28.6", + "@babel/types": "^7.28.6" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.29.0.tgz", + "integrity": "sha512-4HPiQr0X7+waHfyXPZpWPfWL/J7dcN1mx9gL6WdQVMbPnF3+ZhSMs8tCxN7oHddJE9fhNE7+lxdnlyemKfJRuA==", + "license": "MIT", + "dependencies": { + "@babel/code-frame": "^7.29.0", + "@babel/generator": "^7.29.0", + "@babel/helper-globals": "^7.28.0", + "@babel/parser": "^7.29.0", + "@babel/template": "^7.28.6", + "@babel/types": "^7.29.0", + "debug": "^4.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/types": { + "version": "7.29.0", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.29.0.tgz", + "integrity": "sha512-LwdZHpScM4Qz8Xw2iKSzS+cfglZzJGvofQICy7W7v4caru4EaAmyUuO6BGrbyQ2mYV11W0U8j5mBhd14dd3B0A==", + "license": "MIT", + "dependencies": { + "@babel/helper-string-parser": "^7.27.1", + "@babel/helper-validator-identifier": "^7.28.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@datagrok-libraries/chem-meta": { + "version": "1.2.10", + "resolved": "https://registry.npmjs.org/@datagrok-libraries/chem-meta/-/chem-meta-1.2.10.tgz", + "integrity": "sha512-05Qfw1ul3I1lipNQNBTeO8zNBQisF2bJupw7nw/SfEhohH5578SXVMUCjwMAGxkTP//rnsDcfn9pZBN+fjj//A==", + "dependencies": { + "wu": "^2.1.0" + } + }, + "node_modules/@datagrok-libraries/utils": { + "version": "4.6.14", + "resolved": "https://registry.npmjs.org/@datagrok-libraries/utils/-/utils-4.6.14.tgz", + "integrity": "sha512-3hgNM3m30tsn3mdwlXJb8z5k4RnW3KkJdIiTJaib7rojo47AyERT9ix5gzaCj9ipIMmDoI8yljyLhiKR5CBNAw==", + "dependencies": { + "cash-dom": "^8.1.1", + "datagrok-api": "^1.26.0", + "dayjs": "=1.11.10", + "fast-sha256": "^1.3.0", + "js-base64": "^3.7.5", + "rxjs": "^6.5.5", + "wu": "^2.1.0" + } + }, + "node_modules/@datagrok-libraries/utils/node_modules/dayjs": { + "version": "1.11.10", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.10.tgz", + "integrity": "sha512-vjAczensTgRcqDERK0SR2XMwsF/tSvnvlv6VcF2GIhg6Sx4yOIt/irsr1RDJsKiIyBzJDpCoXiWWq28MqH2cnQ==", + "license": "MIT" + }, + "node_modules/@discoveryjs/json-ext": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@discoveryjs/json-ext/-/json-ext-0.5.7.tgz", + "integrity": "sha512-dBVuXR082gk3jsFp7Rd/JI4kytwGHecnCoTtXFb7DB6CNHp4rg5k1bhg0nWdLGLnOV71lmDzGQaLMy8iPLY0pw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/@jridgewell/gen-mapping": { + "version": "0.3.13", + "resolved": "https://registry.npmjs.org/@jridgewell/gen-mapping/-/gen-mapping-0.3.13.tgz", + "integrity": "sha512-2kkt/7niJ6MgEPxF0bYdQ6etZaA+fQvDcLKckhy1yIQOzaoKjBBjSj63/aLVjYE3qhRt5dvM+uUyfCg6UKCBbA==", + "license": "MIT", + "dependencies": { + "@jridgewell/sourcemap-codec": "^1.5.0", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/remapping": { + "version": "2.3.5", + "resolved": "https://registry.npmjs.org/@jridgewell/remapping/-/remapping-2.3.5.tgz", + "integrity": "sha512-LI9u/+laYG4Ds1TDKSJW2YPrIlcVYOwi2fUC6xB43lueCjgxV4lffOCZCtYFiH6TNOX+tQKXx97T4IKHbhyHEQ==", + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + } + }, + "node_modules/@jridgewell/resolve-uri": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/@jridgewell/resolve-uri/-/resolve-uri-3.1.2.tgz", + "integrity": "sha512-bRISgCIjP20/tbWSPWMEi54QVPRZExkuD9lJL+UIxUKtwVJA8wW1Trb1jMs1RFXo1CBTNZ/5hpC9QvmKWdopKw==", + "license": "MIT", + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@jridgewell/source-map": { + "version": "0.3.11", + "resolved": "https://registry.npmjs.org/@jridgewell/source-map/-/source-map-0.3.11.tgz", + "integrity": "sha512-ZMp1V8ZFcPG5dIWnQLr3NSI1MiCU7UETdS/A0G8V/XWHvJv3ZsFqutJn1Y5RPmAPX6F3BiE397OqveU/9NCuIA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25" + } + }, + "node_modules/@jridgewell/sourcemap-codec": { + "version": "1.5.5", + "resolved": "https://registry.npmjs.org/@jridgewell/sourcemap-codec/-/sourcemap-codec-1.5.5.tgz", + "integrity": "sha512-cYQ9310grqxueWbl+WuIUIaiUaDcj7WOq5fVhEljNVgRfOUhY9fy2zTvfoqWsnebh8Sl70VScFbICvJnLKB0Og==", + "license": "MIT" + }, + "node_modules/@jridgewell/trace-mapping": { + "version": "0.3.31", + "resolved": "https://registry.npmjs.org/@jridgewell/trace-mapping/-/trace-mapping-0.3.31.tgz", + "integrity": "sha512-zzNR+SdQSDJzc8joaeP8QQoCQr8NuYx2dIIytl1QeBEZHJ9uW6hebsrYgbz8hJwUQao3TWCMtmfV8Nu1twOLAw==", + "license": "MIT", + "dependencies": { + "@jridgewell/resolve-uri": "^3.1.0", + "@jridgewell/sourcemap-codec": "^1.4.14" + } + }, + "node_modules/@types/eslint": { + "version": "9.6.1", + "resolved": "https://registry.npmjs.org/@types/eslint/-/eslint-9.6.1.tgz", + "integrity": "sha512-FXx2pKgId/WyYo2jXw63kk7/+TY7u7AziEJxJAnSFzHlqTAS3Ync6SvgYAN/k4/PQpnnVuzoMuVnByKK2qp0ag==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/estree": "*", + "@types/json-schema": "*" + } + }, + "node_modules/@types/eslint-scope": { + "version": "3.7.7", + "resolved": "https://registry.npmjs.org/@types/eslint-scope/-/eslint-scope-3.7.7.tgz", + "integrity": "sha512-MzMFlSLBqNF2gcHWO0G1vP/YQyfvrxZ0bF+u7mzUdZ1/xK4A4sru+nraZz5i3iEIk1l1uyicaDVTB4QbbEkAYg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/eslint": "*", + "@types/estree": "*" + } + }, + "node_modules/@types/estree": { + "version": "1.0.8", + "resolved": "https://registry.npmjs.org/@types/estree/-/estree-1.0.8.tgz", + "integrity": "sha512-dWHzHa2WqEXI/O1E9OjrocMTKJl2mSrEolh1Iomrv6U+JuNwaHXsXx9bLu5gG7BUWFIN0skIQJQ/L1rIex4X6w==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/json-schema": { + "version": "7.0.15", + "resolved": "https://registry.npmjs.org/@types/json-schema/-/json-schema-7.0.15.tgz", + "integrity": "sha512-5+fP8P8MFNC+AyZCDxrB2pkZFPGzqQWUzpSeuuVLvm8VMcorNYavBqoFcxK8bQz4Qsbn4oUEEem4wDLfcysGHA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@types/node": { + "version": "25.4.0", + "resolved": "https://registry.npmjs.org/@types/node/-/node-25.4.0.tgz", + "integrity": "sha512-9wLpoeWuBlcbBpOY3XmzSTG3oscB6xjBEEtn+pYXTfhyXhIxC5FsBer2KTopBlvKEiW9l13po9fq+SJY/5lkhw==", + "dev": true, + "license": "MIT", + "dependencies": { + "undici-types": "~7.18.0" + } + }, + "node_modules/@types/prop-types": { + "version": "15.7.15", + "resolved": "https://registry.npmjs.org/@types/prop-types/-/prop-types-15.7.15.tgz", + "integrity": "sha512-F6bEyamV9jKGAFBEmlQnesRPGOQqS2+Uwi0Em15xenOxHaf2hv6L8YCVn3rPdPJOiJfPiCnLIRyvwVaqMY3MIw==", + "license": "MIT" + }, + "node_modules/@types/react": { + "version": "18.3.28", + "resolved": "https://registry.npmjs.org/@types/react/-/react-18.3.28.tgz", + "integrity": "sha512-z9VXpC7MWrhfWipitjNdgCauoMLRdIILQsAEV+ZesIzBq/oUlxk0m3ApZuMFCXdnS4U7KrI+l3WRUEGQ8K1QKw==", + "license": "MIT", + "dependencies": { + "@types/prop-types": "*", + "csstype": "^3.2.2" + } + }, + "node_modules/@types/wu": { + "version": "2.1.44", + "resolved": "https://registry.npmjs.org/@types/wu/-/wu-2.1.44.tgz", + "integrity": "sha512-veqvAklPyeT4DJFD66iBwzUKW5zicMDwaDShIvJmDkteQhwhXBKvgydA+yrNN8FnxWUFCI5y9+a2DI5sSwMUlQ==", + "license": "MIT" + }, + "node_modules/@webassemblyjs/ast": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ast/-/ast-1.14.1.tgz", + "integrity": "sha512-nuBEDgQfm1ccRp/8bCQrx1frohyufl4JlbMMZ4P1wpeOfDhF6FQkxZJ1b/e+PLwr6X1Nhw6OLme5usuBWYBvuQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/helper-numbers": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2" + } + }, + "node_modules/@webassemblyjs/floating-point-hex-parser": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/floating-point-hex-parser/-/floating-point-hex-parser-1.13.2.tgz", + "integrity": "sha512-6oXyTOzbKxGH4steLbLNOu71Oj+C8Lg34n6CqRvqfS2O71BxY6ByfMDRhBytzknj9yGUPVJ1qIKhRlAwO1AovA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-api-error": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-api-error/-/helper-api-error-1.13.2.tgz", + "integrity": "sha512-U56GMYxy4ZQCbDZd6JuvvNV/WFildOjsaWD3Tzzvmw/mas3cXzRJPMjP83JqEsgSbyrmaGjBfDtV7KDXV9UzFQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-buffer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-buffer/-/helper-buffer-1.14.1.tgz", + "integrity": "sha512-jyH7wtcHiKssDtFPRB+iQdxlDf96m0E39yb0k5uJVhFGleZFoNw1c4aeIcVUPPbXUVJ94wwnMOAqUHyzoEPVMA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-numbers": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-numbers/-/helper-numbers-1.13.2.tgz", + "integrity": "sha512-FE8aCmS5Q6eQYcV3gI35O4J789wlQA+7JrqTTpJqn5emA4U2hvwJmvFRC0HODS+3Ye6WioDklgd6scJ3+PLnEA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/floating-point-hex-parser": "1.13.2", + "@webassemblyjs/helper-api-error": "1.13.2", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/helper-wasm-bytecode": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-bytecode/-/helper-wasm-bytecode-1.13.2.tgz", + "integrity": "sha512-3QbLKy93F0EAIXLh0ogEVR6rOubA9AoZ+WRYhNbFyuB70j3dRdwH9g+qXhLAO0kiYGlg3TxDV+I4rQTr/YNXkA==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/helper-wasm-section": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/helper-wasm-section/-/helper-wasm-section-1.14.1.tgz", + "integrity": "sha512-ds5mXEqTJ6oxRoqjhWDU83OgzAYjwsCV8Lo/N+oRsNDmx/ZDpqalmrtgOMkHwxsG0iI//3BwWAErYRHtgn0dZw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/wasm-gen": "1.14.1" + } + }, + "node_modules/@webassemblyjs/ieee754": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/ieee754/-/ieee754-1.13.2.tgz", + "integrity": "sha512-4LtOzh58S/5lX4ITKxnAK2USuNEvpdVV9AlgGQb8rJDHaLeHciwG4zlGr0j/SNWlr7x3vO1lDEsuePvtcDNCkw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@xtuc/ieee754": "^1.2.0" + } + }, + "node_modules/@webassemblyjs/leb128": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/leb128/-/leb128-1.13.2.tgz", + "integrity": "sha512-Lde1oNoIdzVzdkNEAWZ1dZ5orIbff80YPdHx20mrHwHrVNNTjNr8E3xz9BdpcGqRQbAEa+fkrCb+fRFTl/6sQw==", + "dev": true, + "license": "Apache-2.0", + "dependencies": { + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webassemblyjs/utf8": { + "version": "1.13.2", + "resolved": "https://registry.npmjs.org/@webassemblyjs/utf8/-/utf8-1.13.2.tgz", + "integrity": "sha512-3NQWGjKTASY1xV5m7Hr0iPeXD9+RDobLll3T9d2AO+g3my8xy5peVyjSag4I50mR1bBSN/Ct12lo+R9tJk0NZQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/@webassemblyjs/wasm-edit": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-edit/-/wasm-edit-1.14.1.tgz", + "integrity": "sha512-RNJUIQH/J8iA/1NzlE4N7KtyZNHi3w7at7hDjvRNm5rcUXa00z1vRz3glZoULfJ5mpvYhLybmVcwcjGrC1pRrQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/helper-wasm-section": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-opt": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1", + "@webassemblyjs/wast-printer": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-gen": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-gen/-/wasm-gen-1.14.1.tgz", + "integrity": "sha512-AmomSIjP8ZbfGQhumkNvgC33AY7qtMCXnN6bL2u2Js4gVCg8fp735aEiMSBbDR7UQIj90n4wKAFUSEd0QN2Ukg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wasm-opt": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-opt/-/wasm-opt-1.14.1.tgz", + "integrity": "sha512-PTcKLUNvBqnY2U6E5bdOQcSM+oVP/PmrDY9NzowJjislEjwP/C4an2303MCVS2Mg9d3AJpIGdUFIQQWbPds0Sw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-buffer": "1.14.1", + "@webassemblyjs/wasm-gen": "1.14.1", + "@webassemblyjs/wasm-parser": "1.14.1" + } + }, + "node_modules/@webassemblyjs/wasm-parser": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wasm-parser/-/wasm-parser-1.14.1.tgz", + "integrity": "sha512-JLBl+KZ0R5qB7mCnud/yyX08jWFw5MsoalJ1pQ4EdFlgj9VdXKGuENGsiCIjegI1W7p91rUlcB/LB5yRJKNTcQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@webassemblyjs/helper-api-error": "1.13.2", + "@webassemblyjs/helper-wasm-bytecode": "1.13.2", + "@webassemblyjs/ieee754": "1.13.2", + "@webassemblyjs/leb128": "1.13.2", + "@webassemblyjs/utf8": "1.13.2" + } + }, + "node_modules/@webassemblyjs/wast-printer": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/@webassemblyjs/wast-printer/-/wast-printer-1.14.1.tgz", + "integrity": "sha512-kPSSXE6De1XOR820C90RIo2ogvZG+c3KiHzqUoO/F34Y2shGzesfqv7o57xrxovZJH/MetF5UjroJ/R/3isoiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "@webassemblyjs/ast": "1.14.1", + "@xtuc/long": "4.2.2" + } + }, + "node_modules/@webpack-cli/configtest": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/@webpack-cli/configtest/-/configtest-2.1.1.tgz", + "integrity": "sha512-wy0mglZpDSiSS0XHrVR+BAdId2+yxPSoJW8fsna3ZpYSlufjvxnP4YbKTCBZnNIcGN4r6ZPXV55X4mYExOfLmw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/info": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/@webpack-cli/info/-/info-2.0.2.tgz", + "integrity": "sha512-zLHQdI/Qs1UyT5UBdWNqsARasIA+AaF8t+4u2aS2nEpBQh2mWIVb8qAklq0eUENnC5mOItrIB4LiS9xMtph18A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + } + }, + "node_modules/@webpack-cli/serve": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@webpack-cli/serve/-/serve-2.0.5.tgz", + "integrity": "sha512-lqaoKnRYBdo1UgDX8uF24AfGMifWK19TxPmM5FHc2vAGxrJ/qtyUyFBWoY1tISZdelsQ5fBcOusifo5o5wSJxQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14.15.0" + }, + "peerDependencies": { + "webpack": "5.x.x", + "webpack-cli": "5.x.x" + }, + "peerDependenciesMeta": { + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/@xtuc/ieee754": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/@xtuc/ieee754/-/ieee754-1.2.0.tgz", + "integrity": "sha512-DX8nKgqcGwsc0eJSqYt5lwP4DH5FlHnmuWWBRy7X0NcaGR0ZtuyeESgMwTYVEtxmsNGY+qit4QYT/MIYTOTPeA==", + "dev": true, + "license": "BSD-3-Clause" + }, + "node_modules/@xtuc/long": { + "version": "4.2.2", + "resolved": "https://registry.npmjs.org/@xtuc/long/-/long-4.2.2.tgz", + "integrity": "sha512-NuHqBY1PB/D8xU6s/thBgOAiAP7HOYDQ32+BFZILJ8ivkUkAHQnWfn6WhL79Owj1qmUnoN/YPhktdIoucipkAQ==", + "dev": true, + "license": "Apache-2.0" + }, + "node_modules/acorn": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/acorn/-/acorn-8.16.0.tgz", + "integrity": "sha512-UVJyE9MttOsBQIDKw1skb9nAwQuR5wuGD3+82K6JgJlm/Y+KI92oNsMNGZCYdDsVtRHSak0pcV5Dno5+4jh9sw==", + "dev": true, + "license": "MIT", + "peer": true, + "bin": { + "acorn": "bin/acorn" + }, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/acorn-import-phases": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/acorn-import-phases/-/acorn-import-phases-1.0.4.tgz", + "integrity": "sha512-wKmbr/DDiIXzEOiWrTTUcDm24kQ2vGfZQvM2fwg2vXqR5uW6aapr7ObPtj1th32b9u90/Pf4AItvdTh42fBmVQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + }, + "peerDependencies": { + "acorn": "^8.14.0" + } + }, + "node_modules/ajv": { + "version": "8.18.0", + "resolved": "https://registry.npmjs.org/ajv/-/ajv-8.18.0.tgz", + "integrity": "sha512-PlXPeEWMXMZ7sPYOHqmDyCJzcfNrUr3fGNKtezX14ykXOEIvyK81d+qydx89KY5O71FKMPaQ2vBfBFI5NHR63A==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "fast-deep-equal": "^3.1.3", + "fast-uri": "^3.0.1", + "json-schema-traverse": "^1.0.0", + "require-from-string": "^2.0.2" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/epoberezkin" + } + }, + "node_modules/ajv-formats": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz", + "integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ajv": "^8.0.0" + }, + "peerDependencies": { + "ajv": "^8.0.0" + }, + "peerDependenciesMeta": { + "ajv": { + "optional": true + } + } + }, + "node_modules/ajv-keywords": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz", + "integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==", + "dev": true, + "license": "MIT", + "dependencies": { + "fast-deep-equal": "^3.1.3" + }, + "peerDependencies": { + "ajv": "^8.8.2" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/baseline-browser-mapping": { + "version": "2.10.0", + "resolved": "https://registry.npmjs.org/baseline-browser-mapping/-/baseline-browser-mapping-2.10.0.tgz", + "integrity": "sha512-lIyg0szRfYbiy67j9KN8IyeD7q7hcmqnJ1ddWmNt19ItGpNN64mnllmxUNFIOdOm6by97jlL6wfpTTJrmnjWAA==", + "license": "Apache-2.0", + "bin": { + "baseline-browser-mapping": "dist/cli.cjs" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", + "dev": true, + "license": "MIT", + "dependencies": { + "fill-range": "^7.1.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browserslist": { + "version": "4.28.1", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.28.1.tgz", + "integrity": "sha512-ZC5Bd0LgJXgwGqUknZY/vkUQ04r8NXnJZ3yYi4vDmSiZmC/pdSN0NbNRPxZpbtO4uAfDUAFffO8IZoM3Gj8IkA==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "peer": true, + "dependencies": { + "baseline-browser-mapping": "^2.9.0", + "caniuse-lite": "^1.0.30001759", + "electron-to-chromium": "^1.5.263", + "node-releases": "^2.0.27", + "update-browserslist-db": "^1.2.0" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, + "node_modules/buffer-from": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz", + "integrity": "sha512-E+XQCRwSbaaiChtv6k6Dwgc+bx+Bs6vuKJHHl5kox/BaKbhiXzqQOwK4cO22yElGp2OCmjwVhT3HmxgyPGnJfQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/caniuse-lite": { + "version": "1.0.30001777", + "resolved": "https://registry.npmjs.org/caniuse-lite/-/caniuse-lite-1.0.30001777.tgz", + "integrity": "sha512-tmN+fJxroPndC74efCdp12j+0rk0RHwV5Jwa1zWaFVyw2ZxAuPeG8ZgWC3Wz7uSjT3qMRQ5XHZ4COgQmsCMJAQ==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/caniuse-lite" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "CC-BY-4.0" + }, + "node_modules/cash-dom": { + "version": "8.1.5", + "resolved": "https://registry.npmjs.org/cash-dom/-/cash-dom-8.1.5.tgz", + "integrity": "sha512-/BS05CfzyHR5xT2ksKj1sDLPaOv5rSmIwoGxNgdKwUtnIuiJ5neMxVEmZxvfyJiSjGbOMD0Lwe+9v+fszDqHew==", + "license": "MIT" + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "license": "MIT", + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chrome-trace-event": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/chrome-trace-event/-/chrome-trace-event-1.0.4.tgz", + "integrity": "sha512-rNjApaLzuwaOTjCiT8lSDdGN1APCiqkChLMJxJPWLunPAt5fy8xgU9/jNOchV84wfIxrA0lRQB7oCT8jrn/wrQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.0" + } + }, + "node_modules/clone-deep": { + "version": "4.0.1", + "resolved": "https://registry.npmjs.org/clone-deep/-/clone-deep-4.0.1.tgz", + "integrity": "sha512-neHB9xuzh/wk0dIHweyAXv2aPGZIVk3pLMe+/RNzINf17fe0OG96QroktYAUm7SM1PBnzTabaLboqqxDyMU+SQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-plain-object": "^2.0.4", + "kind-of": "^6.0.2", + "shallow-clone": "^3.0.0" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true, + "license": "MIT" + }, + "node_modules/colorette": { + "version": "2.0.20", + "resolved": "https://registry.npmjs.org/colorette/-/colorette-2.0.20.tgz", + "integrity": "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/commander": { + "version": "2.20.3", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.20.3.tgz", + "integrity": "sha512-GpVkmM8vF2vQUkj2LvZmD35JxeJOLCwJ9cUkugyk2nuhbv3+mJvpLYYt+0+USMxE+oj+ey/lJEnhZw75x/OMcQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "license": "MIT" + }, + "node_modules/cross-spawn": { + "version": "7.0.6", + "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.6.tgz", + "integrity": "sha512-uV2QOWP2nWzsy2aMp8aRibhi9dlzF5Hgh5SHaB9OiTGEyDTiJJyx0uy51QXdyWbtAHNua4XJzUKca3OzKUd3vA==", + "dev": true, + "license": "MIT", + "dependencies": { + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/csstype": { + "version": "3.2.3", + "resolved": "https://registry.npmjs.org/csstype/-/csstype-3.2.3.tgz", + "integrity": "sha512-z1HGKcYy2xA8AGQfwrn0PAy+PB7X/GSj3UVJW9qKyn43xWa+gl5nXmU4qqLMRzWVLFC8KusUX8T/0kCiOYpAIQ==", + "license": "MIT" + }, + "node_modules/datagrok-api": { + "version": "1.26.8", + "resolved": "https://registry.npmjs.org/datagrok-api/-/datagrok-api-1.26.8.tgz", + "integrity": "sha512-MaXfheRRsk4ZJjGr+ATPcw/NGCGcIwi56QCT4QIbyiZSbedwhPeRoXYEvFvwdYCNwZtouQuWp8U6dOXSM4GLbA==", + "dependencies": { + "@babel/core": "^7.27.1", + "@datagrok-libraries/chem-meta": "^1.0.12", + "@types/react": "^18.3.11", + "@types/wu": "^2.1.44", + "cash-dom": "^8.1.5", + "dayjs": "^1.11.10", + "openchemlib": "^7.2.3", + "react": "^18.3.1", + "rxjs": "^6.5.5", + "typeahead-standalone": "4.14.1", + "ws": "^8.18.2", + "wu": "^2.1.0" + } + }, + "node_modules/dayjs": { + "version": "1.11.19", + "resolved": "https://registry.npmjs.org/dayjs/-/dayjs-1.11.19.tgz", + "integrity": "sha512-t5EcLVS6QPBNqM2z8fakk/NKel+Xzshgt8FFKAn+qwlD1pzZWxh0nVCrvFK7ZDb6XucZeF9z8C7CBWTRIVApAw==", + "license": "MIT" + }, + "node_modules/debug": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.4.3.tgz", + "integrity": "sha512-RGwwWnwQvkVfavKVt22FGLw+xYSdzARwm0ru6DhTVA3umU5hZc28V3kO4stgYryrTlLpuvgI9GiijltAjNbcqA==", + "license": "MIT", + "dependencies": { + "ms": "^2.1.3" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/diff-grok": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/diff-grok/-/diff-grok-1.2.0.tgz", + "integrity": "sha512-qjU07sXsLVy/Z5YTSTYwzHvFnFCZyUPd2JYpGTVSi06R6b6qBCzpa4ZEmWoybeRFproGmNlfam7JhhFMPPcjoQ==", + "license": "MIT" + }, + "node_modules/electron-to-chromium": { + "version": "1.5.307", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.5.307.tgz", + "integrity": "sha512-5z3uFKBWjiNR44nFcYdkcXjKMbg5KXNdciu7mhTPo9tB7NbqSNP2sSnGR+fqknZSCwKkBN+oxiiajWs4dT6ORg==", + "license": "ISC" + }, + "node_modules/enhanced-resolve": { + "version": "5.20.0", + "resolved": "https://registry.npmjs.org/enhanced-resolve/-/enhanced-resolve-5.20.0.tgz", + "integrity": "sha512-/ce7+jQ1PQ6rVXwe+jKEg5hW5ciicHwIQUagZkp6IufBoY3YDgdTTY1azVs0qoRgVmvsNB+rbjLJxDAeHHtwsQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "graceful-fs": "^4.2.4", + "tapable": "^2.3.0" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/envinfo": { + "version": "7.21.0", + "resolved": "https://registry.npmjs.org/envinfo/-/envinfo-7.21.0.tgz", + "integrity": "sha512-Lw7I8Zp5YKHFCXL7+Dz95g4CcbMEpgvqZNNq3AmlT5XAV6CgAAk6gyAMqn2zjw08K9BHfcNuKrMiCPLByGafow==", + "dev": true, + "license": "MIT", + "bin": { + "envinfo": "dist/cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/es-module-lexer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/es-module-lexer/-/es-module-lexer-2.0.0.tgz", + "integrity": "sha512-5POEcUuZybH7IdmGsD8wlf0AI55wMecM9rVBTI/qEAy2c1kTOm3DjFYjrBdI2K3BaJjJYfYFeRtM0t9ssnRuxw==", + "dev": true, + "license": "MIT" + }, + "node_modules/escalade": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", + "integrity": "sha512-WUj2qlxaQtO4g6Pq5c29GTcWGDyd8itL8zTlipgECz3JesAiiOKotd8JU6otB3PACgG6xkJUyVhboMS+bje/jA==", + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/eslint-scope": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/eslint-scope/-/eslint-scope-5.1.1.tgz", + "integrity": "sha512-2NxwbF/hZ0KpepYN0cNbo+FN6XoK7GaHlQhgx/hIZl6Va0bF45RQOOwhLIy8lQDbuCiadSLCBnH2CFYquit5bw==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "esrecurse": "^4.3.0", + "estraverse": "^4.1.1" + }, + "engines": { + "node": ">=8.0.0" + } + }, + "node_modules/esrecurse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/esrecurse/-/esrecurse-4.3.0.tgz", + "integrity": "sha512-KmfKL3b6G+RXvP8N1vr3Tq1kL/oCFgn2NYXEtqP8/L3pKapUA4G8cFVaoF3SU323CD4XypR/ffioHmkti6/Tag==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "estraverse": "^5.2.0" + }, + "engines": { + "node": ">=4.0" + } + }, + "node_modules/esrecurse/node_modules/estraverse": { + "version": "5.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-5.3.0.tgz", + "integrity": "sha512-MMdARuVEQziNTeJD8DgMqmhwR11BRQ/cBP+pLtYdSTnf3MIO8fFeiINEbX36ZdNlfU/7A9f3gUw49B3oQsvwBA==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/estraverse": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/estraverse/-/estraverse-4.3.0.tgz", + "integrity": "sha512-39nnKffWz8xN1BU/2c79n9nB9HDzo0niYUqx6xyqUnyoAnQyyWpOTdZEeiCch8BBu515t4wp9ZmgVfVhn9EBpw==", + "dev": true, + "license": "BSD-2-Clause", + "engines": { + "node": ">=4.0" + } + }, + "node_modules/events": { + "version": "3.3.0", + "resolved": "https://registry.npmjs.org/events/-/events-3.3.0.tgz", + "integrity": "sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.8.x" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", + "dev": true, + "license": "MIT" + }, + "node_modules/fast-sha256": { + "version": "1.3.0", + "resolved": "https://registry.npmjs.org/fast-sha256/-/fast-sha256-1.3.0.tgz", + "integrity": "sha512-n11RGP/lrWEFI/bWdygLxhI+pVeo1ZYIVwvvPkW7azl/rOy+F3HYRZ2K5zeE9mmkhQppyv9sQFx0JM9UabnpPQ==", + "license": "Unlicense" + }, + "node_modules/fast-uri": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/fast-uri/-/fast-uri-3.1.0.tgz", + "integrity": "sha512-iPeeDKJSWf4IEOasVVrknXpaBV0IApz/gp7S2bb7Z4Lljbl2MGJRqInZiUrQwV16cpzw/D3S5j5Julj/gT52AA==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fastify" + }, + { + "type": "opencollective", + "url": "https://opencollective.com/fastify" + } + ], + "license": "BSD-3-Clause" + }, + "node_modules/fastest-levenshtein": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/fastest-levenshtein/-/fastest-levenshtein-1.0.16.tgz", + "integrity": "sha512-eRnCtTTtGZFpQCwhJiUOuxPQWRXVKYDn0b2PeHfXL6/Zi53SLAzAHfVhVWK2AryC/WH05kGfxhFIPvTF0SXQzg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 4.9.1" + } + }, + "node_modules/fill-range": { + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", + "dev": true, + "license": "MIT", + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/find-up": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-4.1.0.tgz", + "integrity": "sha512-PpOwAdQ/YlXQ2vj8a3h8IipDuYRi3wceVQQGYWxNINccq40Anw7BlsEXCMbt1Zt+OLA6Fq9suIpIWD0OsnISlw==", + "dev": true, + "license": "MIT", + "dependencies": { + "locate-path": "^5.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "license": "BSD-3-Clause", + "bin": { + "flat": "cli.js" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "dev": true, + "license": "MIT", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "license": "MIT", + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/glob-to-regexp": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/glob-to-regexp/-/glob-to-regexp-0.4.1.tgz", + "integrity": "sha512-lkX1HJXwyMcprw/5YUZc2s7DrpAiHB21/V+E1rHUrVNokkvB6bqMzT0VfV6/86ZNabt1k14YOIaT7nDvOX3Iiw==", + "dev": true, + "license": "BSD-2-Clause" + }, + "node_modules/graceful-fs": { + "version": "4.2.11", + "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.11.tgz", + "integrity": "sha512-RbJ5/jmFcNNCcDV5o9eTnBLJ/HszWV0P73bc+Ff4nS/rJj+YaS6IGyiOL0VoBYX+l1Wrl3k63h/KrH+nhJ0XvQ==", + "dev": true, + "license": "ISC" + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/hasown": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.2.tgz", + "integrity": "sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/import-local": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/import-local/-/import-local-3.2.0.tgz", + "integrity": "sha512-2SPlun1JUPWoM6t3F0dw0FkCF/jWY8kttcY4f599GLTSjh2OCuuhdTkJQsEcZzBqbXZGKMK2OqW1oZsjtf/gQA==", + "dev": true, + "license": "MIT", + "dependencies": { + "pkg-dir": "^4.2.0", + "resolve-cwd": "^3.0.0" + }, + "bin": { + "import-local-fixture": "fixtures/cli.js" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/interpret": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/interpret/-/interpret-3.1.1.tgz", + "integrity": "sha512-6xwYfHbajpoF0xLW+iwLkhwgvLoZDfjYfoFNu8ftMoXINzwuymNLd9u/KmwtdT2GbR+/Cz66otEGEVVUHX9QLQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/is-core-module": { + "version": "2.16.1", + "resolved": "https://registry.npmjs.org/is-core-module/-/is-core-module-2.16.1.tgz", + "integrity": "sha512-UfoeMA6fIJ8wTYFEUjelnaGI67v6+N7qXJEvQuIGa99l4xsCruSYOVSQ0uPANn4dAzm8lkYPaKLrrijLq7x23w==", + "dev": true, + "license": "MIT", + "dependencies": { + "hasown": "^2.0.2" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-object": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/is-plain-object/-/is-plain-object-2.0.4.tgz", + "integrity": "sha512-h5PpgXkWitc38BBMYawTYMWJHFZJVnBquFE57xFpjB8pJFiF6gZ+bU+WyI/yqXiFR5mdLsgYNaPe8uao6Uv9Og==", + "dev": true, + "license": "MIT", + "dependencies": { + "isobject": "^3.0.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/isexe": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", + "integrity": "sha512-RHxMLp9lnKHGHRng9QFhRCMbYAcVpn69smSGcq3f36xjgVVWThj4qqLbTLlq7Ssj8B+fIQ1EuCEGI2lKsyQeIw==", + "dev": true, + "license": "ISC" + }, + "node_modules/isobject": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/isobject/-/isobject-3.0.1.tgz", + "integrity": "sha512-WhB9zCku7EGTj/HQQRz5aUQEUeoQZH2bWcltRErOpymJ4boYE6wL9Tbr23krRPSZ+C5zqNSrSw+Cc7sZZ4b7vg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/jest-worker": { + "version": "27.5.1", + "resolved": "https://registry.npmjs.org/jest-worker/-/jest-worker-27.5.1.tgz", + "integrity": "sha512-7vuh85V5cdDofPyxn58nrPjBktZo0u9x1g8WtjQol+jZDaE+fhN+cIvTj11GndBnMnyfrUOG1sZQxCdjKh+DKg==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/node": "*", + "merge-stream": "^2.0.0", + "supports-color": "^8.0.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/jest-worker/node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/js-base64": { + "version": "3.7.8", + "resolved": "https://registry.npmjs.org/js-base64/-/js-base64-3.7.8.tgz", + "integrity": "sha512-hNngCeKxIUQiEUN3GPJOkz4wF/YvdUdbNL9hsBcMQTkKzboD7T/q3OYOuuPZLUE6dBxSGpwhk5mwuDud7JVAow==", + "license": "BSD-3-Clause" + }, + "node_modules/js-tokens": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/js-tokens/-/js-tokens-4.0.0.tgz", + "integrity": "sha512-RdJUflcE3cUzKiMqQgsCu06FPu9UdIJO0beYbPhHN4k6apgJtifcoCtT9bcxOpYBtpD2kCM6Sbzg4CausW/PKQ==", + "license": "MIT" + }, + "node_modules/jsesc": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-3.1.0.tgz", + "integrity": "sha512-/sM3dO2FOzXjKQhJuo0Q173wf2KOo8t4I8vHy6lF9poUp7bKT0/NHE8fPX23PwfhnykfqnC2xRxOnVw5XuGIaA==", + "license": "MIT", + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true, + "license": "MIT" + }, + "node_modules/json-schema-traverse": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-1.0.0.tgz", + "integrity": "sha512-NM8/P9n3XjXhIZn1lLhkFaACTOURQXjWhV4BA/RnOv8xvgqtqpAX9IO4mRQxSx1Rlo4tqzeqb0sOlruaOy3dug==", + "dev": true, + "license": "MIT" + }, + "node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "license": "MIT", + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/kind-of": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/kind-of/-/kind-of-6.0.3.tgz", + "integrity": "sha512-dcS1ul+9tmeD95T+x28/ehLgd9mENa3LsvDTtzm3vyBEO7RPptvAD+t44WVXaUjTBRcrpFeFlC8WCruUR456hw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/loader-runner": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/loader-runner/-/loader-runner-4.3.1.tgz", + "integrity": "sha512-IWqP2SCPhyVFTBtRcgMHdzlf9ul25NwaFx4wCEH/KjAXuuHY4yNjvPXsBokp8jCB936PyWRaPKUNh8NvylLp2Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6.11.5" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/locate-path": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-5.0.0.tgz", + "integrity": "sha512-t7hw9pI+WvuwNJXwk5zVHpyhIqzg2qTlklJOf0mVxGSbe3Fp2VieZcduNYjaLDoy6p9uGpQEGWG87WpMKlNq8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-locate": "^4.1.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/loose-envify": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/loose-envify/-/loose-envify-1.4.0.tgz", + "integrity": "sha512-lyuxPGr/Wfhrlem2CL/UcnUc1zcqKAImBDzukY7Y5F/yQiNdko6+fRLevlw1HgMySw7f611UIY408EtxRSoK3Q==", + "license": "MIT", + "dependencies": { + "js-tokens": "^3.0.0 || ^4.0.0" + }, + "bin": { + "loose-envify": "cli.js" + } + }, + "node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "license": "ISC", + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/merge-stream": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", + "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==", + "dev": true, + "license": "MIT" + }, + "node_modules/micromatch": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/micromatch/-/micromatch-4.0.8.tgz", + "integrity": "sha512-PXwfBhYu0hBCPw8Dn0E+WDYb7af3dSLVWKi3HGv84IdF4TyFoC0ysxFd0Goxw7nSv4T/PzEJQxsYsEiFCKo2BA==", + "dev": true, + "license": "MIT", + "dependencies": { + "braces": "^3.0.3", + "picomatch": "^2.3.1" + }, + "engines": { + "node": ">=8.6" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dev": true, + "license": "MIT", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "license": "MIT" + }, + "node_modules/neo-async": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", + "dev": true, + "license": "MIT" + }, + "node_modules/node-releases": { + "version": "2.0.36", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.36.tgz", + "integrity": "sha512-TdC8FSgHz8Mwtw9g5L4gR/Sh9XhSP/0DEkQxfEFXOpiul5IiHgHan2VhYYb6agDSfp4KuvltmGApc8HMgUrIkA==", + "license": "MIT" + }, + "node_modules/openchemlib": { + "version": "7.5.0", + "resolved": "https://registry.npmjs.org/openchemlib/-/openchemlib-7.5.0.tgz", + "integrity": "sha512-cxEmgL1Szuw5zPDX29PyuAIkokSKPkzEIc/61oPA84GqvGyjMMRrGaF4tbFCDOT4c7ULZ/qmIWk9/ERj3wOg1w==", + "license": "BSD-3-Clause" + }, + "node_modules/p-limit": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-2.3.0.tgz", + "integrity": "sha512-//88mFWSJx8lxCzwdAABTJL2MyWB12+eIY7MDL2SqLmAkeKU9qxRvWuSyTjm3FUmpBEMuFfckAIqEaVGUDxb6w==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-try": "^2.0.0" + }, + "engines": { + "node": ">=6" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-4.1.0.tgz", + "integrity": "sha512-R79ZZ/0wAxKGu3oYMlz8jy/kbhsNrS7SKZ7PxEHBgJ5+F2mtFW2fK2cOtBh1cHYkQsbzFV7I+EoRKe6Yt0oK7A==", + "dev": true, + "license": "MIT", + "dependencies": { + "p-limit": "^2.2.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/p-try": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/p-try/-/p-try-2.2.0.tgz", + "integrity": "sha512-R4nPAVTAU0B9D35/Gk3uJf/7XYbQcyohSKdvAxIRSNghFl4e71hVoGnBNQz9cWaXxO2I10KTC+3jMdvvoKw6dQ==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-key": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.1.tgz", + "integrity": "sha512-ojmeN0qd+y0jszEtoY48r0Peq5dwMEkIlCOu6Q5f41lfkswXuKtYrhgoTpLnyIcHm24Uhqx+5Tqm2InSwLhE6Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/path-parse": { + "version": "1.0.7", + "resolved": "https://registry.npmjs.org/path-parse/-/path-parse-1.0.7.tgz", + "integrity": "sha512-LDJzPVEEEPR+y48z93A0Ed0yXb8pAByGWo/k5YYdYgpY2/2EsOsksJrq7lOHxryrVOn1ejG6oAp8ahvOIQD8sw==", + "dev": true, + "license": "MIT" + }, + "node_modules/picocolors": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/picocolors/-/picocolors-1.1.1.tgz", + "integrity": "sha512-xceH2snhtb5M9liqDsmEw56le376mTZkEX/jEb/RxNFyegNul7eNslCXP9FDj/Lcu0X8KEyMceP2ntpaHrDEVA==", + "license": "ISC" + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/pkg-dir": { + "version": "4.2.0", + "resolved": "https://registry.npmjs.org/pkg-dir/-/pkg-dir-4.2.0.tgz", + "integrity": "sha512-HRDzbaKjC+AOWVXxAU/x54COGeIv9eb+6CkDSQoNTt4XyWoIJvuPsXizxu/Fr23EiekbtZwmh1IcIG/l/a10GQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "find-up": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/react": { + "version": "18.3.1", + "resolved": "https://registry.npmjs.org/react/-/react-18.3.1.tgz", + "integrity": "sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==", + "license": "MIT", + "dependencies": { + "loose-envify": "^1.1.0" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/rechoir": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/rechoir/-/rechoir-0.8.0.tgz", + "integrity": "sha512-/vxpCXddiX8NGfGO/mTafwjq4aFa/71pvamip0++IQk3zG8cbCj0fifNPrjjF1XMXUne91jL9OoxmdykoEtifQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve": "^1.20.0" + }, + "engines": { + "node": ">= 10.13.0" + } + }, + "node_modules/require-from-string": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz", + "integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/resolve": { + "version": "1.22.11", + "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.11.tgz", + "integrity": "sha512-RfqAvLnMl313r7c9oclB1HhUEAezcpLjz95wFH4LVuhk9JF/r22qmVP9AMmOU4vMX7Q8pN8jwNg/CSpdFnMjTQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-core-module": "^2.16.1", + "path-parse": "^1.0.7", + "supports-preserve-symlinks-flag": "^1.0.0" + }, + "bin": { + "resolve": "bin/resolve" + }, + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/resolve-cwd": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/resolve-cwd/-/resolve-cwd-3.0.0.tgz", + "integrity": "sha512-OrZaX2Mb+rJCpH/6CpSqt9xFVpN++x01XnN2ie9g6P5/3xelLAkXWVADpdz1IHD/KFfEXyE6V0U01OQ3UO2rEg==", + "dev": true, + "license": "MIT", + "dependencies": { + "resolve-from": "^5.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/resolve-from": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/resolve-from/-/resolve-from-5.0.0.tgz", + "integrity": "sha512-qYg9KP24dD5qka9J47d0aVky0N+b4fTU89LN9iDnjB5waksiC49rvMB0PrUJQGoTmH50XPiqOvAjDfaijGxYZw==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/rxjs": { + "version": "6.6.7", + "resolved": "https://registry.npmjs.org/rxjs/-/rxjs-6.6.7.tgz", + "integrity": "sha512-hTdwr+7yYNIT5n4AMYp85KA6yw2Va0FLa3Rguvbpa4W3I5xynaBZo41cM3XM+4Q6fRMj3sBYIR1VAmZMXYJvRQ==", + "license": "Apache-2.0", + "dependencies": { + "tslib": "^1.9.0" + }, + "engines": { + "npm": ">=2.0.0" + } + }, + "node_modules/schema-utils": { + "version": "4.3.3", + "resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-4.3.3.tgz", + "integrity": "sha512-eflK8wEtyOE6+hsaRVPxvUKYCpRgzLqDTb8krvAsRIwOGlHoSgYLgBXoubGgLd2fT41/OUYdb48v4k4WWHQurA==", + "dev": true, + "license": "MIT", + "dependencies": { + "@types/json-schema": "^7.0.9", + "ajv": "^8.9.0", + "ajv-formats": "^2.1.1", + "ajv-keywords": "^5.1.0" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/shallow-clone": { + "version": "3.0.1", + "resolved": "https://registry.npmjs.org/shallow-clone/-/shallow-clone-3.0.1.tgz", + "integrity": "sha512-/6KqX+GVUdqPuPPd2LxDDxzX6CAbjJehAAOKlNpqqUpAqPM6HeL8f+o3a+JsyGjn2lv0WY8UsTgUJjU9Ok55NA==", + "dev": true, + "license": "MIT", + "dependencies": { + "kind-of": "^6.0.2" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-command": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", + "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", + "dev": true, + "license": "MIT", + "dependencies": { + "shebang-regex": "^3.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/shebang-regex": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", + "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=8" + } + }, + "node_modules/source-map": { + "version": "0.7.6", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.7.6.tgz", + "integrity": "sha512-i5uvt8C3ikiWeNZSVZNWcfZPItFQOsYTUAOkcUPGd8DqDy1uOUikjt5dG+uRlwyvR108Fb9DOd4GvXfT0N2/uQ==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">= 12" + } + }, + "node_modules/source-map-support": { + "version": "0.5.21", + "resolved": "https://registry.npmjs.org/source-map-support/-/source-map-support-0.5.21.tgz", + "integrity": "sha512-uBHU3L3czsIyYXKX88fdrGovxdSCoTGDRZ6SYXtSRxLZUzHg5P/66Ht6uoUlHu9EZod+inXhKo3qQgwXUT/y1w==", + "dev": true, + "license": "MIT", + "dependencies": { + "buffer-from": "^1.0.0", + "source-map": "^0.6.0" + } + }, + "node_modules/source-map-support/node_modules/source-map": { + "version": "0.6.1", + "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", + "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", + "dev": true, + "license": "BSD-3-Clause", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "license": "MIT", + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/supports-preserve-symlinks-flag": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/supports-preserve-symlinks-flag/-/supports-preserve-symlinks-flag-1.0.0.tgz", + "integrity": "sha512-ot0WnXS9fgdkgIcePe6RHNk1WA8+muPa6cSjeR3V8K27q9BB1rTE3R1p7Hv0z1ZyAc8s6Vvv8DIyWf681MAt0w==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/tapable": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/tapable/-/tapable-2.3.0.tgz", + "integrity": "sha512-g9ljZiwki/LfxmQADO3dEY1CbpmXT5Hm2fJ+QaGKwSXUylMybePR7/67YW7jOrrvjEgL1Fmz5kzyAjWVWLlucg==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=6" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + } + }, + "node_modules/terser": { + "version": "5.46.0", + "resolved": "https://registry.npmjs.org/terser/-/terser-5.46.0.tgz", + "integrity": "sha512-jTwoImyr/QbOWFFso3YoU3ik0jBBDJ6JTOQiy/J2YxVJdZCc+5u7skhNwiOR3FQIygFqVUPHl7qbbxtjW2K3Qg==", + "dev": true, + "license": "BSD-2-Clause", + "dependencies": { + "@jridgewell/source-map": "^0.3.3", + "acorn": "^8.15.0", + "commander": "^2.20.0", + "source-map-support": "~0.5.20" + }, + "bin": { + "terser": "bin/terser" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/terser-webpack-plugin": { + "version": "5.4.0", + "resolved": "https://registry.npmjs.org/terser-webpack-plugin/-/terser-webpack-plugin-5.4.0.tgz", + "integrity": "sha512-Bn5vxm48flOIfkdl5CaD2+1CiUVbonWQ3KQPyP7/EuIl9Gbzq/gQFOzaMFUEgVjB1396tcK0SG8XcNJ/2kDH8g==", + "dev": true, + "license": "MIT", + "dependencies": { + "@jridgewell/trace-mapping": "^0.3.25", + "jest-worker": "^27.4.5", + "schema-utils": "^4.3.0", + "terser": "^5.31.1" + }, + "engines": { + "node": ">= 10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "^5.1.0" + }, + "peerDependenciesMeta": { + "@swc/core": { + "optional": true + }, + "esbuild": { + "optional": true + }, + "uglify-js": { + "optional": true + } + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/ts-loader": { + "version": "9.5.4", + "resolved": "https://registry.npmjs.org/ts-loader/-/ts-loader-9.5.4.tgz", + "integrity": "sha512-nCz0rEwunlTZiy6rXFByQU1kVVpCIgUpc/psFiKVrUwrizdnIbRFu8w7bxhUF0X613DYwT4XzrZHpVyMe758hQ==", + "dev": true, + "license": "MIT", + "dependencies": { + "chalk": "^4.1.0", + "enhanced-resolve": "^5.0.0", + "micromatch": "^4.0.0", + "semver": "^7.3.4", + "source-map": "^0.7.4" + }, + "engines": { + "node": ">=12.0.0" + }, + "peerDependencies": { + "typescript": "*", + "webpack": "^5.0.0" + } + }, + "node_modules/ts-loader/node_modules/semver": { + "version": "7.7.4", + "resolved": "https://registry.npmjs.org/semver/-/semver-7.7.4.tgz", + "integrity": "sha512-vFKC2IEtQnVhpT78h1Yp8wzwrf8CM+MzKMHGJZfBtzhZNycRFnXsHk6E5TxIkkMsgNS7mdX3AGB7x2QM2di4lA==", + "dev": true, + "license": "ISC", + "bin": { + "semver": "bin/semver.js" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/tslib": { + "version": "1.14.1", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-1.14.1.tgz", + "integrity": "sha512-Xni35NKzjgMrwevysHTCArtLDpPvye8zV/0E4EyYn43P7/7qvQwPh9BGkHewbMulVntbigmcT7rdX3BNo9wRJg==", + "license": "0BSD" + }, + "node_modules/typeahead-standalone": { + "version": "4.14.1", + "resolved": "https://registry.npmjs.org/typeahead-standalone/-/typeahead-standalone-4.14.1.tgz", + "integrity": "sha512-K+mqXmHferhxlyFD5blmOV9UIlazUxumyLWxO5QXnD1cjL6uQ6JGuqvjk0rHt4uz5cD2POAFxnyfkyZUD1ce7A==", + "license": "MIT" + }, + "node_modules/typescript": { + "version": "5.9.3", + "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.9.3.tgz", + "integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==", + "dev": true, + "license": "Apache-2.0", + "peer": true, + "bin": { + "tsc": "bin/tsc", + "tsserver": "bin/tsserver" + }, + "engines": { + "node": ">=14.17" + } + }, + "node_modules/undici-types": { + "version": "7.18.2", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-7.18.2.tgz", + "integrity": "sha512-AsuCzffGHJybSaRrmr5eHr81mwJU3kjw6M+uprWvCXiNeN9SOGwQ3Jn8jb8m3Z6izVgknn1R0FTCEAP2QrLY/w==", + "dev": true, + "license": "MIT" + }, + "node_modules/update-browserslist-db": { + "version": "1.2.3", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.2.3.tgz", + "integrity": "sha512-Js0m9cx+qOgDxo0eMiFGEueWztz+d4+M3rGlmKPT+T4IS/jP4ylw3Nwpu6cpTTP8R1MAC1kF4VbdLt3ARf209w==", + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "license": "MIT", + "dependencies": { + "escalade": "^3.2.0", + "picocolors": "^1.1.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, + "node_modules/watchpack": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/watchpack/-/watchpack-2.5.1.tgz", + "integrity": "sha512-Zn5uXdcFNIA1+1Ei5McRd+iRzfhENPCe7LeABkJtNulSxjma+l7ltNx55BWZkRlwRnpOgHqxnjyaDgJnNXnqzg==", + "dev": true, + "license": "MIT", + "dependencies": { + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.1.2" + }, + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/webpack": { + "version": "5.105.4", + "resolved": "https://registry.npmjs.org/webpack/-/webpack-5.105.4.tgz", + "integrity": "sha512-jTywjboN9aHxFlToqb0K0Zs9SbBoW4zRUlGzI2tYNxVYcEi/IPpn+Xi4ye5jTLvX2YeLuic/IvxNot+Q1jMoOw==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@types/eslint-scope": "^3.7.7", + "@types/estree": "^1.0.8", + "@types/json-schema": "^7.0.15", + "@webassemblyjs/ast": "^1.14.1", + "@webassemblyjs/wasm-edit": "^1.14.1", + "@webassemblyjs/wasm-parser": "^1.14.1", + "acorn": "^8.16.0", + "acorn-import-phases": "^1.0.3", + "browserslist": "^4.28.1", + "chrome-trace-event": "^1.0.2", + "enhanced-resolve": "^5.20.0", + "es-module-lexer": "^2.0.0", + "eslint-scope": "5.1.1", + "events": "^3.2.0", + "glob-to-regexp": "^0.4.1", + "graceful-fs": "^4.2.11", + "json-parse-even-better-errors": "^2.3.1", + "loader-runner": "^4.3.1", + "mime-types": "^2.1.27", + "neo-async": "^2.6.2", + "schema-utils": "^4.3.3", + "tapable": "^2.3.0", + "terser-webpack-plugin": "^5.3.17", + "watchpack": "^2.5.1", + "webpack-sources": "^3.3.4" + }, + "bin": { + "webpack": "bin/webpack.js" + }, + "engines": { + "node": ">=10.13.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependenciesMeta": { + "webpack-cli": { + "optional": true + } + } + }, + "node_modules/webpack-cli": { + "version": "5.1.4", + "resolved": "https://registry.npmjs.org/webpack-cli/-/webpack-cli-5.1.4.tgz", + "integrity": "sha512-pIDJHIEI9LR0yxHXQ+Qh95k2EvXpWzZ5l+d+jIo+RdSm9MiHfzazIxwwni/p7+x4eJZuvG1AJwgC4TNQ7NRgsg==", + "dev": true, + "license": "MIT", + "peer": true, + "dependencies": { + "@discoveryjs/json-ext": "^0.5.0", + "@webpack-cli/configtest": "^2.1.1", + "@webpack-cli/info": "^2.0.2", + "@webpack-cli/serve": "^2.0.5", + "colorette": "^2.0.14", + "commander": "^10.0.1", + "cross-spawn": "^7.0.3", + "envinfo": "^7.7.3", + "fastest-levenshtein": "^1.0.12", + "import-local": "^3.0.2", + "interpret": "^3.1.1", + "rechoir": "^0.8.0", + "webpack-merge": "^5.7.3" + }, + "bin": { + "webpack-cli": "bin/cli.js" + }, + "engines": { + "node": ">=14.15.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/webpack" + }, + "peerDependencies": { + "webpack": "5.x.x" + }, + "peerDependenciesMeta": { + "@webpack-cli/generators": { + "optional": true + }, + "webpack-bundle-analyzer": { + "optional": true + }, + "webpack-dev-server": { + "optional": true + } + } + }, + "node_modules/webpack-cli/node_modules/commander": { + "version": "10.0.1", + "resolved": "https://registry.npmjs.org/commander/-/commander-10.0.1.tgz", + "integrity": "sha512-y4Mg2tXshplEbSGzx7amzPwKKOCGuoSRP/CjEdwwk0FOGlUbq6lKuoyDZTNZkmxHdJtp54hdfY/JUrdL7Xfdug==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=14" + } + }, + "node_modules/webpack-merge": { + "version": "5.10.0", + "resolved": "https://registry.npmjs.org/webpack-merge/-/webpack-merge-5.10.0.tgz", + "integrity": "sha512-+4zXKdx7UnO+1jaN4l2lHVD+mFvnlZQP/6ljaJVb4SZiwIKeUnrT5l0gkT8z+n4hKpC+jpOv6O9R+gLtag7pSA==", + "dev": true, + "license": "MIT", + "dependencies": { + "clone-deep": "^4.0.1", + "flat": "^5.0.2", + "wildcard": "^2.0.0" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/webpack-sources": { + "version": "3.3.4", + "resolved": "https://registry.npmjs.org/webpack-sources/-/webpack-sources-3.3.4.tgz", + "integrity": "sha512-7tP1PdV4vF+lYPnkMR0jMY5/la2ub5Fc/8VQrrU+lXkiM6C4TjVfGw7iKfyhnTQOsD+6Q/iKw0eFciziRgD58Q==", + "dev": true, + "license": "MIT", + "engines": { + "node": ">=10.13.0" + } + }, + "node_modules/which": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/which/-/which-2.0.2.tgz", + "integrity": "sha512-BLI3Tl1TW3Pvl70l3yq3Y64i+awpwXqsGBYWkkqMtnbXgrMD+yj7rhW0kuEDxzJaYXGjEW5ogapKNMEKNMjibA==", + "dev": true, + "license": "ISC", + "dependencies": { + "isexe": "^2.0.0" + }, + "bin": { + "node-which": "bin/node-which" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/wildcard": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/wildcard/-/wildcard-2.0.1.tgz", + "integrity": "sha512-CC1bOL87PIWSBhDcTrdeLo6eGT7mCFtrg0uIJtqJUFyK+eJnzl8A1niH56uu7KMa5XFrtiV+AQuHO3n7DsHnLQ==", + "dev": true, + "license": "MIT" + }, + "node_modules/ws": { + "version": "8.19.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.19.0.tgz", + "integrity": "sha512-blAT2mjOEIi0ZzruJfIhb3nps74PRWTCz1IjglWEEpQl5XS/UNama6u2/rjFkDDouqr4L67ry+1aGIALViWjDg==", + "license": "MIT", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/wu": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/wu/-/wu-2.1.0.tgz", + "integrity": "sha512-j+Gdt5IUK4eoLO6mrN/ZurInHacaxr/EPCvQHf1ARq6ROdKRN/aFtc0PGdH9lnRPMg6vhJOOqIYdNhMN6uWtUg==" + }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "license": "ISC" + } + } +} diff --git a/packages/LotkaVolterraNonGuided/package.json b/packages/LotkaVolterraNonGuided/package.json new file mode 100644 index 0000000000..598f57ee49 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/package.json @@ -0,0 +1,43 @@ +{ + "name": "lotkavolterranonguided", + "friendlyName": "LotkaVolterraNonGuided", + "version": "0.0.1", + "description": "LotkaVolterraNonGuided package", + "dependencies": { + "@datagrok-libraries/utils": "^4.6.5", + "cash-dom": "^8.1.5", + "datagrok-api": "^1.26.0", + "dayjs": "^1.11.13", + "diff-grok": "^1.2.0" + }, + "devDependencies": { + "ts-loader": "latest", + "typescript": "latest", + "webpack": "^5.95.0", + "webpack-cli": "^5.1.4" + }, + "scripts": { + "debug-lotkavolterranonguided": "webpack && grok publish", + "release-lotkavolterranonguided": "webpack && grok publish --release", + "build-lotkavolterranonguided": "webpack", + "build": "grok api && grok check --soft && webpack", + "test": "grok test", + "debug-lotkavolterranonguided-dev": "webpack && grok publish dev", + "release-lotkavolterranonguided-dev": "webpack && grok publish dev --release", + "debug-lotkavolterranonguided-local": "webpack && grok publish local", + "release-lotkavolterranonguided-local": "webpack && grok publish local --release", + "debug-lotkavolterranonguided-release": "webpack && grok publish release", + "release-lotkavolterranonguided-release": "webpack && grok publish release --release" + }, + "canEdit": [ + "Developers" + ], + "canView": [ + "All users" + ], + "repository": { + "type": "git", + "url": "https://github.com/datagrok-ai/public.git", + "directory": "packages/LotkaVolterraNonGuided" + } +} diff --git a/packages/LotkaVolterraNonGuided/package.png b/packages/LotkaVolterraNonGuided/package.png new file mode 100644 index 0000000000..77aceb1bab Binary files /dev/null and b/packages/LotkaVolterraNonGuided/package.png differ diff --git a/packages/LotkaVolterraNonGuided/src/app.ts b/packages/LotkaVolterraNonGuided/src/app.ts new file mode 100644 index 0000000000..42cf2ab776 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/src/app.ts @@ -0,0 +1,487 @@ +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; +import {mrt, ODEs} from 'diff-grok'; + +// ── Types ───────────────────────────────────────────────────────────────────── + +interface LVParams { + alpha: number; // prey birth rate + beta: number; // predation rate + delta: number; // predator growth efficiency + gamma: number; // predator death rate + x0: number; // initial prey + y0: number; // initial predators + T: number; // simulation time +} + +// ── Solver ──────────────────────────────────────────────────────────────────── + +function solveLV(p: LVParams): {t: Float64Array; x: Float64Array; y: Float64Array} { + const step = Math.max(0.05, p.T / 2000); + const odes: ODEs = { + name: 'LotkaVolterra', + arg: {name: 't', start: 0, finish: p.T, step}, + initial: [p.x0, p.y0], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = p.alpha * y[0] - p.beta * y[0] * y[1]; + out[1] = p.delta * y[0] * y[1] - p.gamma * y[1]; + }, + tolerance: 1e-6, + solutionColNames: ['prey', 'predators'], + }; + const sol = mrt(odes); + return {t: sol[0], x: sol[1], y: sol[2]}; +} + +// ── DataFrame factories ──────────────────────────────────────────────────────── + +function makeTimeSeriesDf(t: Float64Array, x: Float64Array, y: Float64Array): DG.DataFrame { + return DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', t.slice()), + DG.Column.fromFloat64Array('prey', x.slice()), + DG.Column.fromFloat64Array('predators', y.slice()), + ]); +} + +function makePhaseDf(x: Float64Array, y: Float64Array): DG.DataFrame { + const n = x.length; + const labels = new Array(n).fill('trajectory'); + labels[0] = 'start'; + return DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('prey', x.slice()), + DG.Column.fromFloat64Array('predators', y.slice()), + DG.Column.fromStrings('point', labels), + ]); +} + +function makeTableDf(t: Float64Array, x: Float64Array, y: Float64Array): DG.DataFrame { + const n = t.length; + const stride = Math.max(1, Math.floor(n / 200)); + const rows = Math.ceil(n / stride); + const st = new Float64Array(rows); + const sx = new Float64Array(rows); + const sy = new Float64Array(rows); + for (let r = 0; r < rows; r++) { + const i = Math.min(r * stride, n - 1); + st[r] = t[i]; sx[r] = x[i]; sy[r] = y[i]; + } + return DG.DataFrame.fromColumns([ + DG.Column.fromFloat64Array('t', st), + DG.Column.fromFloat64Array('x (prey)', sx), + DG.Column.fromFloat64Array('y (pred)', sy), + ]); +} + +// ── Slider definitions ───────────────────────────────────────────────────────── + +interface SliderDef { + key: keyof LVParams; + label: string; + min: number; max: number; step: number; decimals: number; + tooltip: string; +} + +const SLIDER_DEFS: SliderDef[] = [ + { + key: 'alpha', label: 'α — prey birth rate', + min: 0.1, max: 3.0, step: 0.01, decimals: 2, + tooltip: 'Prey birth rate (α): how fast prey reproduce without predators.\n' + + 'Higher α → larger oscillation amplitude, higher y* = α/β equilibrium.', + }, + { + key: 'beta', label: 'β — predation rate', + min: 0.01, max: 0.5, step: 0.001, decimals: 3, + tooltip: 'Predation rate (β): likelihood of a predator catching prey per encounter.\n' + + 'Higher β → prey equilibrium x* = γ/δ unchanged but oscillations shift.', + }, + { + key: 'delta', label: 'δ — predator efficiency', + min: 0.01, max: 0.5, step: 0.001, decimals: 3, + tooltip: 'Predator growth efficiency (δ): fraction of eaten prey converted to new predators.\n' + + 'Higher δ → lower prey equilibrium x* = γ/δ, more predators.', + }, + { + key: 'gamma', label: 'γ — predator death rate', + min: 0.1, max: 3.0, step: 0.01, decimals: 2, + tooltip: 'Predator death rate (γ): natural mortality of predators.\n' + + 'Higher γ → fewer predators, higher prey equilibrium x* = γ/δ.', + }, + { + key: 'x0', label: 'x₀ — initial prey', + min: 1, max: 100, step: 1, decimals: 0, + tooltip: 'Initial prey population (x₀): starting point on the phase portrait.\n' + + 'Near x* = γ/δ → small oscillations; far away → large swings.', + }, + { + key: 'y0', label: 'y₀ — initial predators', + min: 1, max: 50, step: 1, decimals: 0, + tooltip: 'Initial predator population (y₀): starting point on the phase portrait.\n' + + 'Near y* = α/β → small oscillations; far away → large swings.', + }, + { + key: 'T', label: 'T — simulation time', + min: 10, max: 500, step: 5, decimals: 0, + tooltip: 'Total simulation time T. Increase to observe more oscillation cycles.\n' + + 'Lotka-Volterra is conservative: cycle period ≈ 2π / √(α·γ).', + }, +]; + +// ── Slider widget factory ────────────────────────────────────────────────────── + +function makeSlider( + def: SliderDef, + value: number, + onChange: (v: number) => void, +): {root: HTMLElement; setValue: (v: number) => void} { + const valSpan = document.createElement('span'); + valSpan.className = 'lv-val'; + valSpan.textContent = value.toFixed(def.decimals); + + const labelRow = document.createElement('div'); + labelRow.className = 'lv-label-row'; + labelRow.appendChild(document.createTextNode(def.label)); + labelRow.appendChild(valSpan); + + const range = document.createElement('input'); + range.type = 'range'; + range.className = 'lv-range'; + range.min = String(def.min); + range.max = String(def.max); + range.step = String(def.step); + range.value = String(value); + range.addEventListener('input', () => { + const v = parseFloat(range.value); + valSpan.textContent = v.toFixed(def.decimals); + onChange(v); + }); + + const root = document.createElement('div'); + root.className = 'lv-slider-row'; + root.title = def.tooltip; + root.appendChild(labelRow); + root.appendChild(range); + + return { + root, + setValue(v: number) { + range.value = String(v); + valSpan.textContent = v.toFixed(def.decimals); + }, + }; +} + +// ── CSS ──────────────────────────────────────────────────────────────────────── + +const CSS = ` +.lv-root { + display: flex; height: 100%; width: 100%; overflow: hidden; font-size: 12px; box-sizing: border-box; +} +.lv-left { + width: 274px; min-width: 274px; display: flex; flex-direction: column; gap: 6px; + padding: 10px 10px 10px 12px; overflow-y: auto; border-right: 1px solid var(--grey-2,#e0e0e0); +} +.lv-panel-title { + font-size: 13px; font-weight: 600; color: var(--grey-8,#222); margin-bottom: 2px; +} +.lv-group-label { + font-size: 11px; font-weight: 600; color: var(--blue-2,#1565c0); text-transform: uppercase; + letter-spacing: 0.05em; margin-top: 4px; margin-bottom: 2px; + border-bottom: 1px solid var(--grey-2,#e0e0e0); padding-bottom: 2px; +} +.lv-slider-row { padding: 3px 0; cursor: default; } +.lv-label-row { display: flex; justify-content: space-between; margin-bottom: 2px; color: var(--grey-6,#444); } +.lv-val { font-weight: 700; color: var(--blue-1,#1976d2); min-width: 38px; text-align: right; } +.lv-range { width: 100%; cursor: pointer; accent-color: var(--blue-1,#1976d2); } +.lv-stats-block { border-top: 1px solid var(--grey-2,#e0e0e0); padding-top: 6px; } +.lv-stats-title { font-weight: 600; color: var(--grey-7,#333); margin-bottom: 3px; } +.lv-stats-line { color: var(--grey-6,#555); line-height: 1.6; font-family: monospace; font-size: 11.5px; } +.lv-progress-wrap { + display: none; height: 18px; background: var(--grey-1,#f0f0f0); border-radius: 9px; + overflow: hidden; position: relative; margin-top: 2px; +} +.lv-progress-bar { height: 100%; background: var(--blue-1,#1976d2); transition: width 0.08s linear; width: 0; } +.lv-progress-pct { + position: absolute; right: 8px; top: 0; line-height: 18px; font-size: 11px; font-weight: 600; + color: var(--grey-7,#333); +} +.lv-center { + flex: 1; min-width: 0; display: flex; flex-direction: column; +} +.lv-chart-section { + flex: 1; min-height: 0; display: flex; flex-direction: column; +} +.lv-section-title { + font-size: 11px; font-weight: 600; color: var(--grey-6,#555); text-transform: uppercase; + letter-spacing: 0.06em; padding: 4px 10px 0; flex-shrink: 0; +} +.lv-chart-wrap { + flex: 1; min-height: 0; position: relative; overflow: hidden; +} +.lv-chart-wrap > * { position: absolute !important; inset: 0 !important; width: 100% !important; height: 100% !important; } +.lv-divider { height: 1px; background: var(--grey-2,#e0e0e0); flex-shrink: 0; } +.lv-right { + width: 230px; min-width: 230px; border-left: 1px solid var(--grey-2,#e0e0e0); + display: flex; flex-direction: column; overflow: hidden; +} +.lv-right-title { + font-size: 11px; font-weight: 600; color: var(--grey-6,#555); text-transform: uppercase; + letter-spacing: 0.06em; padding: 4px 10px 0; flex-shrink: 0; +} +.lv-grid-wrap { flex: 1; min-height: 0; overflow: hidden; } +.lv-grid-wrap > * { width: 100% !important; height: 100% !important; } +`; + +// ── App entry point ──────────────────────────────────────────────────────────── + +export function runLotkaVolterra(): void { + // ── State ────────────────────────────────────────────────────────────────── + const params: LVParams = {alpha: 1.0, beta: 0.1, delta: 0.075, gamma: 1.5, x0: 10, y0: 5, T: 100}; + + // ── Initial solve ────────────────────────────────────────────────────────── + let sol = solveLV(params); + let tsDf = makeTimeSeriesDf(sol.t, sol.x, sol.y); + let phaseDf = makePhaseDf(sol.x, sol.y); + let tableDf = makeTableDf(sol.t, sol.x, sol.y); + + // ── Viewers ──────────────────────────────────────────────────────────────── + const lineChart = DG.Viewer.fromType('Line chart', tsDf, {xColumnName: 't'}); + const phaseScatter = DG.Viewer.fromType('Scatter plot', phaseDf, { + xColumnName: 'prey', + yColumnName: 'predators', + colorColumnName: 'point', + }); + const gridViewer = DG.Viewer.fromType('Grid', tableDf); + + // ── Stats ────────────────────────────────────────────────────────────────── + const equilLine = document.createElement('div'); + equilLine.className = 'lv-stats-line'; + const summaryLine = document.createElement('div'); + summaryLine.className = 'lv-stats-line'; + + function updateStats(): void { + const xStar = params.gamma / params.delta; + const yStar = params.alpha / params.beta; + equilLine.textContent = `x* = ${xStar.toFixed(2)}, y* = ${yStar.toFixed(2)}`; + let maxX = -Infinity; + let maxY = -Infinity; + for (let i = 0; i < sol.x.length; i++) { if (sol.x[i] > maxX) maxX = sol.x[i]; } + for (let i = 0; i < sol.y.length; i++) { if (sol.y[i] > maxY) maxY = sol.y[i]; } + summaryLine.textContent = `max prey = ${maxX.toFixed(1)}\nmax pred = ${maxY.toFixed(1)}\nsteps = ${sol.t.length}`; + } + updateStats(); + + // ── Debounced solver update ──────────────────────────────────────────────── + let debounceTimer = 0; + function scheduleUpdate(): void { + clearTimeout(debounceTimer); + debounceTimer = window.setTimeout(() => { + try { + sol = solveLV(params); + tsDf = makeTimeSeriesDf(sol.t, sol.x, sol.y); + phaseDf = makePhaseDf(sol.x, sol.y); + tableDf = makeTableDf(sol.t, sol.x, sol.y); + lineChart.dataFrame = tsDf; + phaseScatter.dataFrame = phaseDf; + gridViewer.dataFrame = tableDf; + updateStats(); + } catch (err) { + console.error('[LotkaVolterra] solver error:', err); + } + }, 60); + } + + // ── Sliders ──────────────────────────────────────────────────────────────── + const sliderRefs = new Map void}>(); + const modelGroup = document.createElement('div'); + modelGroup.className = 'lv-group-label'; + modelGroup.textContent = 'Model Coefficients'; + const initGroup = document.createElement('div'); + initGroup.className = 'lv-group-label'; + initGroup.textContent = 'Initial Conditions'; + + const slidersDiv = document.createElement('div'); + slidersDiv.appendChild(modelGroup); + for (const def of SLIDER_DEFS.slice(0, 4)) { + const {root, setValue} = makeSlider(def, params[def.key], (v) => { + (params as unknown as Record)[def.key] = v; + scheduleUpdate(); + }); + sliderRefs.set(def.key, {setValue}); + slidersDiv.appendChild(root); + } + slidersDiv.appendChild(initGroup); + for (const def of SLIDER_DEFS.slice(4)) { + const {root, setValue} = makeSlider(def, params[def.key], (v) => { + (params as unknown as Record)[def.key] = v; + scheduleUpdate(); + }); + sliderRefs.set(def.key, {setValue}); + slidersDiv.appendChild(root); + } + + // ── Equilibrium & stats blocks ───────────────────────────────────────────── + const equilBlock = document.createElement('div'); + equilBlock.className = 'lv-stats-block'; + const equilTitle = document.createElement('div'); + equilTitle.className = 'lv-stats-title'; + equilTitle.textContent = 'Equilibrium'; + equilBlock.appendChild(equilTitle); + equilBlock.appendChild(equilLine); + + const summaryBlock = document.createElement('div'); + summaryBlock.className = 'lv-stats-block'; + const summaryTitle = document.createElement('div'); + summaryTitle.className = 'lv-stats-title'; + summaryTitle.textContent = 'Summary'; + summaryBlock.appendChild(summaryTitle); + summaryBlock.appendChild(summaryLine); + + // ── Optimizer button & progress bar ─────────────────────────────────────── + const progressWrap = document.createElement('div'); + progressWrap.className = 'lv-progress-wrap'; + const progressBar = document.createElement('div'); + progressBar.className = 'lv-progress-bar'; + const progressPct = document.createElement('span'); + progressPct.className = 'lv-progress-pct'; + progressPct.textContent = '0%'; + progressWrap.appendChild(progressBar); + progressWrap.appendChild(progressPct); + + let worker: Worker | null = null; + const optimizeBtn = ui.bigButton('Optimize Max Prey', () => { + if (worker) { + worker.terminate(); + worker = null; + optimizeBtn.textContent = 'Optimize Max Prey'; + progressWrap.style.display = 'none'; + return; + } + + progressWrap.style.display = 'block'; + progressBar.style.width = '0%'; + progressPct.textContent = '0%'; + optimizeBtn.textContent = 'Cancel'; + + worker = new Worker(new URL('./optimizer.worker.ts', import.meta.url)); + + worker.postMessage({ + alphaRange: [0.1, 3.0] as [number, number], + betaRange: [0.01, 0.5] as [number, number], + deltaRange: [0.01, 0.5] as [number, number], + gammaRange: [0.1, 3.0] as [number, number], + x0: params.x0, + y0: params.y0, + T: Math.min(params.T, 100), + }); + + worker.onmessage = (ev: MessageEvent) => { + if (ev.data.type === 'progress') { + const pct = Math.round(ev.data.progress * 100); + progressBar.style.width = `${pct}%`; + progressPct.textContent = `${pct}%`; + } else if (ev.data.type === 'result') { + const {alpha, beta, delta, gamma} = ev.data; + params.alpha = alpha; params.beta = beta; + params.delta = delta; params.gamma = gamma; + sliderRefs.get('alpha')!.setValue(alpha); + sliderRefs.get('beta')!.setValue(beta); + sliderRefs.get('delta')!.setValue(delta); + sliderRefs.get('gamma')!.setValue(gamma); + scheduleUpdate(); + worker = null; + optimizeBtn.textContent = 'Optimize Max Prey'; + progressWrap.style.display = 'none'; + } + }; + + worker.onerror = (ev) => { + console.error('[LotkaVolterra] worker error:', ev); + worker = null; + optimizeBtn.textContent = 'Optimize Max Prey'; + progressWrap.style.display = 'none'; + }; + }); + + // ── Left panel ───────────────────────────────────────────────────────────── + const leftPanel = document.createElement('div'); + leftPanel.className = 'lv-left'; + + const panelTitle = document.createElement('div'); + panelTitle.className = 'lv-panel-title'; + panelTitle.textContent = 'Parameters'; + + leftPanel.appendChild(panelTitle); + leftPanel.appendChild(slidersDiv); + leftPanel.appendChild(equilBlock); + leftPanel.appendChild(summaryBlock); + leftPanel.appendChild(progressWrap); + leftPanel.appendChild(optimizeBtn); + + // ── Center panel ─────────────────────────────────────────────────────────── + const tsTitle = document.createElement('div'); + tsTitle.className = 'lv-section-title'; + tsTitle.textContent = 'Population Dynamics'; + const tsWrap = document.createElement('div'); + tsWrap.className = 'lv-chart-wrap'; + tsWrap.appendChild(lineChart.root); + const tsSection = document.createElement('div'); + tsSection.className = 'lv-chart-section'; + tsSection.appendChild(tsTitle); + tsSection.appendChild(tsWrap); + + const divider = document.createElement('div'); + divider.className = 'lv-divider'; + + const phaseTitle = document.createElement('div'); + phaseTitle.className = 'lv-section-title'; + phaseTitle.textContent = 'Phase Portrait (● = start)'; + const phaseWrap = document.createElement('div'); + phaseWrap.className = 'lv-chart-wrap'; + phaseWrap.appendChild(phaseScatter.root); + const phaseSection = document.createElement('div'); + phaseSection.className = 'lv-chart-section'; + phaseSection.appendChild(phaseTitle); + phaseSection.appendChild(phaseWrap); + + const centerPanel = document.createElement('div'); + centerPanel.className = 'lv-center'; + centerPanel.appendChild(tsSection); + centerPanel.appendChild(divider); + centerPanel.appendChild(phaseSection); + + // ── Right panel ──────────────────────────────────────────────────────────── + const rightTitle = document.createElement('div'); + rightTitle.className = 'lv-right-title'; + rightTitle.textContent = 'Data (sampled)'; + const gridWrap = document.createElement('div'); + gridWrap.className = 'lv-grid-wrap'; + gridWrap.appendChild(gridViewer.root); + + const rightPanel = document.createElement('div'); + rightPanel.className = 'lv-right'; + rightPanel.appendChild(rightTitle); + rightPanel.appendChild(gridWrap); + + // ── Root ─────────────────────────────────────────────────────────────────── + const appRoot = document.createElement('div'); + appRoot.className = 'lv-root'; + appRoot.appendChild(leftPanel); + appRoot.appendChild(centerPanel); + appRoot.appendChild(rightPanel); + + // Inject CSS once + if (!document.getElementById('lv-styles')) { + const style = document.createElement('style'); + style.id = 'lv-styles'; + style.textContent = CSS; + document.head.appendChild(style); + } + + // ── Open view ────────────────────────────────────────────────────────────── + const view = grok.shell.newView('Lotka–Volterra', [appRoot]); + view.root.style.cssText = 'padding:0;overflow:hidden;display:flex;height:100%;'; + + // Clean up worker if view is closed + (view as any).onClosed?.subscribe(() => { if (worker) { worker.terminate(); worker = null; } }); +} diff --git a/packages/LotkaVolterraNonGuided/src/optimizer.worker.ts b/packages/LotkaVolterraNonGuided/src/optimizer.worker.ts new file mode 100644 index 0000000000..def4a23258 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/src/optimizer.worker.ts @@ -0,0 +1,91 @@ +// Web Worker: brute-force grid search for Lotka-Volterra "max prey" optimization. +// Uses the MRT solver from diff-grok (same method as the main simulation). +import {mrt, ODEs} from 'diff-grok'; + +function maxPreyMRT( + alpha: number, beta: number, delta: number, gamma: number, + x0: number, y0: number, T: number, +): number { + try { + const odes: ODEs = { + name: 'LV', + arg: {name: 't', start: 0, finish: T, step: Math.max(0.1, T / 500)}, + initial: [x0, y0], + func: (_t: number, y: Float64Array, out: Float64Array) => { + out[0] = alpha * y[0] - beta * y[0] * y[1]; + out[1] = delta * y[0] * y[1] - gamma * y[1]; + }, + tolerance: 1e-4, + solutionColNames: ['x', 'y'], + }; + const sol = mrt(odes); + const x = sol[1]; + let maxX = 0; + for (let i = 0; i < x.length; i++) { + if (!isFinite(x[i]) || x[i] > 1e8) return 0; + if (x[i] > maxX) maxX = x[i]; + } + return maxX; + } catch { + return 0; + } +} + +(self as any).onmessage = function(e: MessageEvent): void { + const {alphaRange, betaRange, deltaRange, gammaRange, x0, y0, T} = e.data as { + alphaRange: [number, number]; + betaRange: [number, number]; + deltaRange: [number, number]; + gammaRange: [number, number]; + x0: number; y0: number; T: number; + }; + + // 10% step = 10 intervals → 11 sample values per parameter + const STEPS = 10; + const aStep = (alphaRange[1] - alphaRange[0]) / STEPS; + const bStep = (betaRange[1] - betaRange[0]) / STEPS; + const dStep = (deltaRange[1] - deltaRange[0]) / STEPS; + const gStep = (gammaRange[1] - gammaRange[0]) / STEPS; + + let bestAlpha = alphaRange[0]; + let bestBeta = betaRange[0]; + let bestDelta = deltaRange[0]; + let bestGamma = gammaRange[0]; + let bestMax = 0; + + const total = (STEPS + 1) ** 4; + let done = 0; + + for (let ai = 0; ai <= STEPS; ai++) { + const alpha = alphaRange[0] + ai * aStep; + for (let bi = 0; bi <= STEPS; bi++) { + const beta = betaRange[0] + bi * bStep; + for (let di = 0; di <= STEPS; di++) { + const delta = deltaRange[0] + di * dStep; + for (let gi = 0; gi <= STEPS; gi++) { + const gamma = gammaRange[0] + gi * gStep; + const maxPrey = maxPreyMRT(alpha, beta, delta, gamma, x0, y0, T); + if (maxPrey > bestMax) { + bestMax = maxPrey; + bestAlpha = alpha; + bestBeta = beta; + bestDelta = delta; + bestGamma = gamma; + } + done++; + } + } + // ~121 progress posts total (one per alpha×beta pair) + (self as any).postMessage({type: 'progress', progress: done / total}); + } + } + + (self as any).postMessage({ + type: 'result', + alpha: bestAlpha, + beta: bestBeta, + delta: bestDelta, + gamma: bestGamma, + maxPrey: bestMax, + }); +}; diff --git a/packages/LotkaVolterraNonGuided/src/package-api.ts b/packages/LotkaVolterraNonGuided/src/package-api.ts new file mode 100644 index 0000000000..1908887585 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/src/package-api.ts @@ -0,0 +1,18 @@ +/** +This file is auto-generated by the grok api command. +If you notice any changes, please push them to the repository. +Do not edit this file manually. +*/ +import * as grok from 'datagrok-api/grok'; +import * as DG from 'datagrok-api/dg'; + + +export namespace funcs { + export async function lotkaVolterra(): Promise { + return await grok.functions.call('LotkaVolterraNonGuided:LotkaVolterra', {}); + } + + export async function info(): Promise { + return await grok.functions.call('LotkaVolterraNonGuided:Info', {}); + } +} diff --git a/packages/LotkaVolterraNonGuided/src/package-test.ts b/packages/LotkaVolterraNonGuided/src/package-test.ts new file mode 100644 index 0000000000..f41e9e4a66 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/src/package-test.ts @@ -0,0 +1,20 @@ +import { runTests, tests, TestContext , initAutoTests as initTests } from '@datagrok-libraries/utils/src/test'; +import * as DG from 'datagrok-api/dg'; + +export let _package = new DG.Package(); +export { tests }; + +//name: test +//input: string category {optional: true} +//input: string test {optional: true} +//input: object testContext {optional: true} +//output: dataframe result +export async function test(category: string, test: string, testContext: TestContext): Promise { + const data = await runTests({ category, test, testContext }); + return DG.DataFrame.fromObjects(data)!; +} + +//name: initAutoTests +export async function initAutoTests() { + await initTests(_package, _package.getModule('package-test.js')); +} diff --git a/packages/LotkaVolterraNonGuided/src/package.g.ts b/packages/LotkaVolterraNonGuided/src/package.g.ts new file mode 100644 index 0000000000..8de619387a --- /dev/null +++ b/packages/LotkaVolterraNonGuided/src/package.g.ts @@ -0,0 +1 @@ +import * as DG from 'datagrok-api/dg'; diff --git a/packages/LotkaVolterraNonGuided/src/package.ts b/packages/LotkaVolterraNonGuided/src/package.ts new file mode 100644 index 0000000000..b2e625a623 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/src/package.ts @@ -0,0 +1,19 @@ +/* Do not change these import lines to match external modules in webpack configuration */ +import * as grok from 'datagrok-api/grok'; +import * as ui from 'datagrok-api/ui'; +import * as DG from 'datagrok-api/dg'; +export * from './package.g'; +import {runLotkaVolterra} from './app'; + +export const _package = new DG.Package(); + +//name: Lotka-Volterra (non-Guided) +//tags: app +export function LotkaVolterra(): void { + runLotkaVolterra(); +} + +//name: info +export function info() { + grok.shell.info(_package.webRoot); +} diff --git a/packages/LotkaVolterraNonGuided/tsconfig.json b/packages/LotkaVolterraNonGuided/tsconfig.json new file mode 100644 index 0000000000..b9b0997746 --- /dev/null +++ b/packages/LotkaVolterraNonGuided/tsconfig.json @@ -0,0 +1,71 @@ +{ + "compilerOptions": { + /* Visit https://aka.ms/tsconfig.json to read more about this file */ + + /* Basic Options */ + // "incremental": true, /* Enable incremental compilation */ + "target": "es6", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ + "module": "es2020", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ + "lib": ["ES2022", "dom"], /* Specify library files to be included in the compilation. */ + // "allowJs": true, /* Allow javascript files to be compiled. */ + // "checkJs": true, /* Report errors in .js files. */ + // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', 'react', 'react-jsx' or 'react-jsxdev'. */ + // "declaration": true, /* Generates corresponding '.d.ts' file. */ + // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ + "sourceMap": true, /* Generates corresponding '.map' file. */ + // "outFile": "./", /* Concatenate and emit output to single file. */ + // "outDir": "./", /* Redirect output structure to the directory. */ + // "rootDir": "./", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ + // "composite": true, /* Enable project compilation */ + // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ + // "removeComments": true, /* Do not emit comments to output. */ + // "noEmit": true, /* Do not emit outputs. */ + // "importHelpers": true, /* Import emit helpers from 'tslib'. */ + // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ + // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ + + /* Strict Type-Checking Options */ + "strict": true, /* Enable all strict type-checking options. */ + // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ + // "strictNullChecks": true, /* Enable strict null checks. */ + // "strictFunctionTypes": true, /* Enable strict checking of function types. */ + // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ + // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ + // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ + // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ + + /* Additional Checks */ + // "noUnusedLocals": true, /* Report errors on unused locals. */ + // "noUnusedParameters": true, /* Report errors on unused parameters. */ + // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ + // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ + // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ + // "noPropertyAccessFromIndexSignature": true, /* Require undeclared properties from index signatures to use element accesses. */ + + /* Module Resolution Options */ + "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ + // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ + // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ + // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ + // "typeRoots": [], /* List of folders to include type definitions from. */ + // "types": [], /* Type declaration files to be included in compilation. */ + // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ + "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ + // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ + // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ + + /* Source Map Options */ + // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ + // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ + // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ + // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ + + /* Experimental Options */ + "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ + "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ + + /* Advanced Options */ + "skipLibCheck": true, /* Skip type checking of declaration files. */ + "forceConsistentCasingInFileNames": true, /* Disallow inconsistently-cased references to the same file. */ + } +} diff --git a/packages/LotkaVolterraNonGuided/webpack.config.js b/packages/LotkaVolterraNonGuided/webpack.config.js new file mode 100644 index 0000000000..1813a31b7e --- /dev/null +++ b/packages/LotkaVolterraNonGuided/webpack.config.js @@ -0,0 +1,67 @@ +const path = require('path'); +const {execSync} = require('child_process'); +const packageName = path.parse(require('./package.json').name).name.toLowerCase().replace(/-/g, ''); + +function getDatagrokTools() { + const pluginPath = 'datagrok-tools/plugins/func-gen-plugin'; + try { + return require(pluginPath); + } catch (e) { + try { + const globalPath = execSync('npm root -g').toString().trim(); + return require(path.join(globalPath, pluginPath)); + } catch (globalErr) { + console.error('\n' + '='.repeat(60)); + console.error('ERROR: datagrok-tools not found!'); + console.error('To fix this, please install the tools globally by running:'); + console.error('\n npm install -g datagrok-tools\n'); + console.error('='.repeat(60) + '\n'); + process.exit(1); + } + } +} + +const FuncGeneratorPlugin = getDatagrokTools(); + +module.exports = { + cache: { + type: 'filesystem', + }, + mode: 'development', + entry: { + test: {filename: 'package-test.js', library: {type: 'var', name: `${packageName}_test`}, import: './src/package-test.ts'}, + package: './src/package.ts', + }, + resolve: { + symlinks: false, + extensions: ['.wasm', '.mjs', '.ts', '.json', '.js', '.tsx'], + }, + module: { + rules: [ + {test: /\.tsx?$/, loader: 'ts-loader', options: {allowTsInNodeModules: true}}, + ], + }, + plugins: [ + new FuncGeneratorPlugin({outputPath: './src/package.g.ts'}), + ], + devtool: 'source-map', + externals: { + 'datagrok-api/dg': 'DG', + 'datagrok-api/grok': 'grok', + 'datagrok-api/ui': 'ui', + 'openchemlib/full.js': 'OCL', + 'rxjs': 'rxjs', + 'rxjs/operators': 'rxjs.operators', + 'cash-dom': '$', + 'dayjs': 'dayjs', + 'wu': 'wu', + 'exceljs': 'ExcelJS', + 'html2canvas': 'html2canvas', + }, + output: { + filename: '[name].js', + library: packageName, + libraryTarget: 'var', + path: path.resolve(__dirname, 'dist'), + }, +};