Chem: SAR Matrix implementation - #3940
Conversation
| * subset (e.g. R1 and R7), not the array order. | ||
| */ | ||
| linkRGroupFragments(cores: string[], fragmentColumns: string[][], attachIdx: number[]): string[] { | ||
| const size = cores.length; |
There was a problem hiding this comment.
this is quite an overcomplicated way to link fragments. look for example in how its done in markush enumerator. you can just add a cyclization points to the corresponding molecules and then canonicalize smiles. will be much faster and result in same thing
| checkCurrentView(table); | ||
| const view = grok.shell.tv as DG.TableView; | ||
| const viewer = view.addViewer('SAR Matrix Viewer'); | ||
| viewer.setOptions({ |
There was a problem hiding this comment.
why two different operations? options can be passed from the addViewer call in second param
| @@ -231,9 +232,6 @@ export class PackageFunctions { | |||
| DG.ObjectHandler.register(new MpoProfileHandler()); | |||
| } | |||
|
|
|||
| @grok.decorators.autostart() | |||
| static async initChemAutostart(): Promise<void> { } | |||
|
|
|||
There was a problem hiding this comment.
make sure to check prs for such stuff)))
|
A. The fallback path is uncapped, and it's the path big clusters take. assembleSinglePositionMatrix applies none of MAX_MATRIX_ROWS/MAX_COLUMNS_PER_POSITION: rows = every series in the cluster, columns = every distinct substituent. And decomposeCluster returns null for molIdx.length > 300 — so exactly the large clusters get routed to the unbounded builder. A 1000-molecule cluster with ~400 cores × ~600 substituents = 240 000 cell objects, and then: computeMatrixConfidence → looPredictSlice copies the entire cells grid for every observed cell . That's O(observed × R × C) — hundreds of millions of object spreads. B. activityDirection is ignored by the initial ranking. SarMatrixParams has no direction field; runSarMatrix ends with rankMatrices(..., params.scaling === SCALING_METHODS.MINUS_LG) . The viewer's higherIsBetter getter — the one that honors "Higher is better"/"Lower is better" — is only consulted when the user touches the Rank by dropdown. So for a pre-computed pIC50 column on scaling: none + "Higher is better", the matrix list is sorted backwards on first render, while cardScore renders the correct direction — the card text and the card order disagree. The uncommitted working-tree change flipping the default scaling from -lg to none makes this the common case. C. H is invisible. referenceValue and topValues both skip empty values (if (v)), and selectActivePositions counts only non-empty distinct values — yet isCleanSubstituent explicitly documents '' as "H at this position". Consequences: unsubstituted (H) can never be a column, never be the reference, and a position that varies H↔Me reads as having 1 distinct value and is dropped as inactive. Since the reference gates every cell (r.values[other] === refValues[other]), if H is the majority state at R2 the entire H-at-R2 population is excluded from the R1 group. This is the most likely explanation for "why is my matrix so empty" in practice, and H is the single most important reference point in real SAR. D. Uncontrolled positions leak into the fit. heldAtRef only pins active positions , but MAX_POSITIONS = 2 means a molecule with 3+ varying R-groups contributes cells while its 3rd position varies freely. The virtual cell in the same row, meanwhile, is assembled with all non-active positions at their global reference (linkVirtualCellStructures uses Object.keys(refValues)). So the observed cells and the predicted structures aren't the same chemical series, and the Free-Wilson row/column effects are fit across compounds that differ at an unmodelled position. Either pin every decomposed position, or drop records that vary at an inactive one. E. linkVirtualStructures runs on multi-position matrices too. In runSarMatrix it targets any virtual cell with smiles === null — including multi-position cells whose in-assembly link failed. It calls mmpLinkFragments, which does F. Make-list append is schema-fragile. addAnalogToMakeList finds a table by name and does a positional rows.addNew(...) . The activity column name embeds this.scaling (Predicted activity (-lg)), so changing scaling between adds gives a mismatched schema; and any unrelated user table named SAR virtual analogs gets appended to. G. Confidence mutates on the failure path. computeMatrixConfidence stamps cell.fit on cells before the pairs.length < MIN_CV_POINTS early return . So the chip says "Too few observations to cross-validate" while individual cells still render a red "non-additive" flag derived from a 1–3-point fit. H. Clustering isn't permutation-stable. keyOf uses scaffolds[s.members[0].molIdx] on the premise that "all members of a single-cut series share the same ring system". That's false when a substituent contains a ring — a Murcko scaffold covers all ring systems, so Me vs. phenyl at the same site gives different scaffolds. members[0] is Map-insertion order, i.e. row order, so reordering the input table changes the clustering. Methodologically questionable J. "SAR transfer" direction is an artifact. Pearson r is symmetric; a is simply whichever series gatherSeries emitted first (matrix index → position → row order). The UI states "a change learned on A should carry to B" and groups nav cards by transferSourceKey(t.a) — all of which is iteration order, not chemistry. K. No multiple-testing control on a pairwise scan. computeAllTransfers tests every pair of (matrix, position, row) — easily thousands of hypotheses — with a floor of r ≥ 0.7 on as few as 3 points, where r = 0.7 has p ≈ 0.3. This will manufacture confident-looking cross-series transfers from noise. Raise MIN_COMMON, or gate on a permutation/FDR threshold. L. foldMatch depends on arbitrary column order. It walks consecutive shared substituents, and columns are in frequency order — so the statistic changes if a substituent happens to appear one more time. And it's labelled "Fold-change match" while operating on raw differences: that's only a fold change on a lg/-lg scale, i.e. not under the new default scaling: none. M. Ranking compares absolute potency, not SAR interest. discontinuityScore (max within-row range) and preferredScore (best column mean) are unnormalized activity magnitudes, so a series of uniformly potent compounds outranks a series with a genuine 100× cliff. Normalizing the spread by the dataset's activity SD would rank what the mode claims to rank.
|
# Conflicts: # packages/Chem/src/package-api.ts
Render the SAR matrix in a virtualized DG.Grid instead of an eager HTML table. The table drew an RDKit canvas per cell up front and rebuilt the whole DOM on every parent-DataFrame move, so navigating the source table triggered a molecule-render storm. The grid paints only the cells in the viewport via onCellRender, keeps the R-group substituents in the (natively frozen) column header band and pins the core column left, so both references stay visible while scrolling. Selection and current-row changes now repaint through invalidate(), with the rings drawn inside the cell paint, instead of rebuilding the DOM. Supporting changes that make that path cheap and correct: a byte-budgeted LRU cache of rendered depictions, memoized per-core alignment templates, a direct drawImage blit in place of a getImageData/putImageData readback, precomputed header state, a single merged debounce for the two host-grid streams, explicit grid release on re-render and detach, and re-reading theme colors per render. The dead buildMatrixTable path is removed; the transfer pane keeps its (bounded) table rendering. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
decomposeCluster parsed molecules on the main thread twice over: once per molecule to take a heavy-atom-count median for the coverage check, and once per matched record to canonicalize a core, even though the cores collapse to at most a dozen distinct values. On a real profile this accounted for a large share of the main-thread RDKit time that looked like rendering. Memoize both by their input string. This is exact — no sampling, no threshold changes — and preserves the mol.delete() discipline in the finally blocks. The caches are capped and cleared wholesale so they cannot grow across a session. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
…linker linkVirtualStructures falls back to mmpLinkFragments for virtual cells that assembly left unlinked, but mmpLinkFragments only substitutes [*:1]. A core carrying more than one attachment point therefore came back with unresolved [*:2] dummies — a structurally wrong molecule that then reached the context panel, the export and the make-list. Restrict the fallback to single-cut matrices. The discriminator is refValues, not positions: positions holds only the varying positions, so a core decomposed at R1/R2 that varies at R1 alone still carries a [*:2]; only the single-cut assembler leaves refValues empty. Cells that multi-attachment linking could not resolve stay null, which every consumer already handles. Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Addition of Structure-Activity relationship matrix