From c8c115b1fab37fa0fe486976f9c60c77039cbc97 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:11:49 +0000 Subject: [PATCH 1/2] fix(frontend): restore build and metadata tests Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- frontend/src/api/client.ts | 1 + frontend/src/api/types.ts | 26 +++++++++- .../src/components/PipelineStages.test.ts | 6 +-- .../src/components/differentialTable.test.ts | 4 +- frontend/src/components/differentialTable.ts | 49 ++++++++++++++++--- frontend/src/views/StudyView.tsx | 4 +- test/unit/test_numeric_policy.jl | 4 +- 7 files changed, 76 insertions(+), 18 deletions(-) diff --git a/frontend/src/api/client.ts b/frontend/src/api/client.ts index 98a97087..5b58aaa7 100644 --- a/frontend/src/api/client.ts +++ b/frontend/src/api/client.ts @@ -23,6 +23,7 @@ import type { ReferenceTreeDoc, ReferenceTreeSummary, ReferenceTreeDetail, AlignmentQC, TrimSettings, CladeCumulusSourcesResponse, CladeCumulusRequest, CladeCumulusResponse, CladeMoveValidationRequest, CladeMoveValidationResponse, + CladisticsSummary, CladisticsDetail, CladisticsCreateRequest, } from './types' import type { FigureDoc } from '../figure/types' diff --git a/frontend/src/api/types.ts b/frontend/src/api/types.ts index e668807d..593f7dc2 100644 --- a/frontend/src/api/types.ts +++ b/frontend/src/api/types.ts @@ -423,7 +423,10 @@ export interface DifferentialRow { prevalence: number } -export type DifferentialMethod = 'nb_glm' | 'clr_welch' | 'zinb' | 'hurdle_nb' +export type DifferentialMethod = + | 'nb_glm' | 'clr_welch' | 'zinb' | 'hurdle_nb' + | 'multinomial' | 'dirichlet_multinomial' + | 'ancom_bc' | 'aldex2' | 'songbird' /** Which row field holds the effect size, and what to call it. */ export interface DifferentialEffect { @@ -453,6 +456,16 @@ export interface DifferentialResult { replacement_delta?: number /** Null uses GBM's estimated concentration; positive values override it. */ bayesian_alpha?: number | null + /** multinomial and dirichlet_multinomial only. */ + mn_reference_taxon?: string | null + /** multinomial and dirichlet_multinomial only. */ + mn_penalty?: number + /** dirichlet_multinomial only. */ + dm_overdispersion_method?: 'mle' | 'mom' + /** Multinomial optimiser settings. */ + tolerance?: number + max_iter?: number + max_seconds?: number /** aldex2 only. */ mc_samples?: number /** aldex2 only. */ @@ -462,6 +475,17 @@ export interface DifferentialResult { n_taxa: number; n_tested: number; n_failed: number; n_boundary: number; n_filtered: number /** Safety note for two-part models. */ model_warning?: string + /** Diagnostics returned by the joint multinomial models. */ + model?: { + reference_taxon: string + penalty: number + n_parameters: number + iterations: number + converged: boolean + log_likelihood: number + runtime_seconds: number + n_taxa_fitted: number + } /** clr_welch only. */ zero_replacement?: { method: string diff --git a/frontend/src/components/PipelineStages.test.ts b/frontend/src/components/PipelineStages.test.ts index 0037ce55..2c46743a 100644 --- a/frontend/src/components/PipelineStages.test.ts +++ b/frontend/src/components/PipelineStages.test.ts @@ -24,11 +24,9 @@ test('advanced zero handling exposes both methods, bounded delta and nullable al expect(CONFIG_DESCRIPTIONS['analysis.differential.bayesian_alpha']).toContain('no posterior draws') }) -test('differential method enum lists all five compositional and count models', () => { +test('differential method enum lists exactly the methods accepted by the backend', () => { expect(CONFIG_TYPES['analysis.differential.method']).toEqual({ kind: 'enum', - options: ['nb_glm', 'clr_welch', 'ancom_bc', 'aldex2', 'songbird'], + options: ['nb_glm', 'clr_welch', 'zinb', 'hurdle_nb'], }) - expect(CONFIG_TYPES['analysis.differential.mc_samples']).toEqual({ kind: 'int' }) - expect(CONFIG_DESCRIPTIONS['analysis.differential.mc_samples']).toContain('Dirichlet') }) diff --git a/frontend/src/components/differentialTable.test.ts b/frontend/src/components/differentialTable.test.ts index 2bae9b2c..0090f3d1 100644 --- a/frontend/src/components/differentialTable.test.ts +++ b/frontend/src/components/differentialTable.test.ts @@ -99,7 +99,7 @@ describe('multinomial results', () => { test('the reference taxon is exported with an empty statistic set, never a substitute', () => { const lines = differentialCsv(mn).trimEnd().split('\r\n') - expect(lines[4].startsWith('Bacteroides,reference,0,0,,,,,,1,')).toBe(true) + expect(lines[4].startsWith('Bacteroides,reference,0,0,,,,,,,1,')).toBe(true) expect(effectValue(mn, mn.rows[1])).toBe(1.6) }) }) @@ -163,7 +163,7 @@ describe('advanced compositional methods (ancom_bc, aldex2, songbird)', () => { expect(effectValue(ax, ax.rows[0])).toBe(1.85) expect(configLine(ax)).toBe('method aldex2, mc_samples 128, seed 123, min_prevalence 0.05; estimate is an ALDEx2 standardized CLR effect size, not a fold change') const csvRows = differentialCsv(ax).trimEnd().split('\r\n') - expect(csvRows[4]).toBe('Bacteroides,ok,,1.85,,5.545,2.9e-8,5.8e-8,,1,') + expect(csvRows[4]).toBe('Bacteroides,ok,,1.85,,5.545,2.9e-8,5.8e-8,,,1,') }) test('songbird reports CLR-centred multinomial differential metadata', () => { diff --git a/frontend/src/components/differentialTable.ts b/frontend/src/components/differentialTable.ts index 2743553e..fd4b3980 100644 --- a/frontend/src/components/differentialTable.ts +++ b/frontend/src/components/differentialTable.ts @@ -46,13 +46,48 @@ export function formatStat(value: number | null): string { */ export function configLine(result: DifferentialResult): string { const c = result.config - if (c.method !== 'clr_welch') - return `method ${c.method}, offset ${c.offset}, min_prevalence ${c.min_prevalence}` - const replacement = c.replacement_method ?? 'bayesian_multiplicative' - const alpha = c.bayesian_alpha === null || c.bayesian_alpha === undefined - ? 'estimated' : String(c.bayesian_alpha) - return `method clr_welch, replacement_method ${replacement}, replacement_delta ${c.replacement_delta}, ` + - `bayesian_alpha ${alpha}, min_prevalence ${c.min_prevalence}; estimate is a CLR difference, not a fold change` + + if (c.method === 'multinomial' || c.method === 'dirichlet_multinomial') { + const settings = [`method ${c.method}`] + if (c.method === 'dirichlet_multinomial' && c.dm_overdispersion_method) + settings.push(`overdispersion ${c.dm_overdispersion_method}`) + if (c.mn_reference_taxon) settings.push(`reference ${c.mn_reference_taxon}`) + if (c.mn_penalty !== undefined) settings.push(`penalty ${c.mn_penalty}`) + settings.push(`min_prevalence ${c.min_prevalence}`) + return `${settings.join(', ')}; estimates are log2 fold changes against the reference taxon` + } + + if (c.method === 'clr_welch') { + const replacement = c.replacement_method ?? 'bayesian_multiplicative' + const settings = [ + `method clr_welch`, + `replacement_method ${replacement}`, + `replacement_delta ${c.replacement_delta}`, + ] + // CZM has no Bayesian concentration parameter; do not claim one was used. + if (replacement === 'bayesian_multiplicative') { + const alpha = c.bayesian_alpha === null || c.bayesian_alpha === undefined + ? 'estimated' : String(c.bayesian_alpha) + settings.push(`bayesian_alpha ${alpha}`) + } + settings.push(`min_prevalence ${c.min_prevalence}`) + return `${settings.join(', ')}; estimate is a CLR difference, not a fold change` + } + + if (c.method === 'ancom_bc') + return `method ancom_bc, min_prevalence ${c.min_prevalence}; effect is bias-corrected log2 fold change (Lin & Peddada 2020)` + + if (c.method === 'aldex2') + return `method aldex2, mc_samples ${c.mc_samples}, seed ${c.seed}, min_prevalence ${c.min_prevalence}; estimate is an ALDEx2 standardized CLR effect size, not a fold change` + + if (c.method === 'songbird') + return `method songbird, min_prevalence ${c.min_prevalence}; estimate is a CLR-centred multinomial differential, not an absolute fold change` + + const settings = [`method ${c.method}`] + if (c.offset !== undefined) settings.push(`offset ${c.offset}`) + if (c.method === 'nb_glm' && c.dispersion !== undefined) settings.push(`dispersion ${c.dispersion}`) + settings.push(`min_prevalence ${c.min_prevalence}`) + return settings.join(', ') } /** The effect size of one row, from the field the result names in `effect`. */ diff --git a/frontend/src/views/StudyView.tsx b/frontend/src/views/StudyView.tsx index 89f32231..553d948d 100644 --- a/frontend/src/views/StudyView.tsx +++ b/frontend/src/views/StudyView.tsx @@ -15,7 +15,7 @@ import { ConfigAccordion } from '../components/ConfigAccordion' import { TreesPanel } from '../components/TreesPanel' import { PlacementPanel } from '../components/PlacementPanel' import { EvidenceModeToggle } from '../components/EvidenceModeToggle' -import { CladeCumulusPanel } from '../components/CladeCumulusPanel' +import { CladeCumulusPanel as EvidenceCladeCumulusPanel } from '../components/CladeCumulusPanel' import { ReportPanel } from '../components/ReportPanel' import { useNavRefresh } from '../hooks/useNavRefresh' import { CladeCumulusPanel } from '../cladistics/CladeCumulusPanel' @@ -304,7 +304,7 @@ export function StudyView() { {view === 'trees' && <> - {evidenceMode && } + {evidenceMode && } setTreesVersion(v => v + 1)} /> } diff --git a/test/unit/test_numeric_policy.jl b/test/unit/test_numeric_policy.jl index 2f64ff8c..5c2cdf5b 100644 --- a/test/unit/test_numeric_policy.jl +++ b/test/unit/test_numeric_policy.jl @@ -51,8 +51,8 @@ end tight = numeric_policy(mode=:exact_counts, max_denominator_bits=32) @test_throws ResourceLimitError exact_fraction(1, big(2)^40; policy=tight) @test_throws ResourceLimitError exact_fraction(big(2)^40, 1; policy=tight) - # Each addend fits the 32-bit budget on its own; their sum 1/2^31 + 1/3 needs - # 33 bits, so the limit must fire on the running total, not only on inputs. + # Each addend fits the 32-bit budget on its own; their reduced sum has a + # 62-bit denominator, so the limit must fire on the running total. @test_throws ResourceLimitError exact_rational_sum( [big(1)//big(2)^31, big(1)//(big(2)^31 - 1)]; policy=tight) end From fd342911d5b89af6dc114ef943d6e8537782b035 Mon Sep 17 00:00:00 2001 From: hyperpolymath <6759885+hyperpolymath@users.noreply.github.com> Date: Fri, 23 Oct 2026 00:17:39 +0000 Subject: [PATCH 2/2] test(stats): verify 1024-bit references independently Co-authored-by: arena-agent <297053741+arena-agent@users.noreply.github.com> --- test/references/check_high_precision_references.py | 2 +- test/unit/test_high_precision.jl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/test/references/check_high_precision_references.py b/test/references/check_high_precision_references.py index 3d644c8f..c63ffa23 100644 --- a/test/references/check_high_precision_references.py +++ b/test/references/check_high_precision_references.py @@ -14,7 +14,7 @@ import re import sys -PRECISION = 220 +PRECISION = 460 def references() -> dict[str, str]: diff --git a/test/unit/test_high_precision.jl b/test/unit/test_high_precision.jl index 1b903e08..9f4c9e96 100644 --- a/test/unit/test_high_precision.jl +++ b/test/unit/test_high_precision.jl @@ -10,9 +10,9 @@ const HP_LABELS = ["A", "B"] const HP_FEATURES = ["f1", "f2", "f3"] const REF_LN2 = "0.6931471805599453094172321214581765680755001343602552541206800094933936219696947156058633269964186875420014810205706857336855202357581305570326707516350759619307275708283714351903070386238916734711233501153644979552391204751726815749320651555247341395258829504530070953263666426541042391578149520437404303855008019441706416715186447128399681717845469570262716310645461502572074024816377733896385506952606683411372738737229289564935470257626520988596932019650586" -const REF_LN3 = "1.098612288668109691395245236922525704647490557822749451734694333637494293218608966873615754813732088787970029065957865742368004225930519821052801870767277410603162769183381367179373698844360959903742570316795911521145592" -const REF_TWO_THIRDS = "0.6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666667" -const REF_H_1234 = "1.279854225833667467182185762149429928131354186921692886392239591382930699742075074568049151649059984163575294301928203177013603961545520253317776056233522504371230806742168835499680746549033237086147384098562106321046048" +const REF_LN3 = "1.098612288668109691395245236922525704647490557822749451734694333637494293218608966873615754813732088787970029065957865742368004225930519821052801870767277410603162769183381367179373698844360959903742570316795911521145591917750671347054940166775580222203170252946897560690106521505642868138036317373298577782366991654792131818149020030103823630122248652748198225991097452490896458053467008845965085748444119018857087647494867079613085829411602166121184001409826" +const REF_TWO_THIRDS = "0.6666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666666667" +const REF_H_1234 = "1.279854225833667467182185762149429928131354186921692886392239591382930699742075074568049151649059984163575294301928203177013603961545520253317776056233522504371230806742168835499680746549033237086147384098562106321046048112868339308649672681189135461287570216937288791515535715833594380020178552966375097993963900928200120047744717834903206362014300458629735272704036751340598445797090470318251133760850603018005627582297102594277585911107434593371009711254916" function test_bigfloat_close(actual, reference, bits, ulps=16) setprecision(BigFloat, bits) do