///|
/// Symmetric H0/H1 bottleneck distances for one bounded batch study.
/// None means no finite match because alive interval counts differ.
pub struct BatchDistanceMatrix {
names : Array[String]
kinds : Array[String]
cutoffs : Array[Double]
h0 : Array[Array[Double?]]
h1 : Array[Array[Double?]]
conditions_aligned : Array[Array[Bool]]
} derive(@debug.Debug)
///|
pub extend BatchDistanceMatrix with @debug.Debug::{to_repr}
///|
/// Compute every unordered pair once. Agreement on kind and cutoff is only
/// a necessary metadata check; scale units and data meaning remain user choices.
pub fn batch_distance_matrix(
plan : BatchPlan,
analyses : Array[Analysis],
) -> BatchDistanceMatrix raise TopologyError {
let n = plan.cases.length()
if n < 1 || n > 8 || analyses.length() != n {
raise TopologyError(
"batch matrix requires 1..8 matching cases and analyses",
)
}
let seen : Map[String, Bool] = Map([])
for case in plan.cases {
batch_name(case.name)
if seen.contains(case.name) {
raise TopologyError("duplicate batch matrix case name: " + case.name)
}
seen[case.name] = true
}
let names = plan.cases.map(fn(case) { case.name })
let kinds = analyses.map(fn(result) { result.filtration.kind })
let cutoffs = analyses.map(fn(result) { result.filtration.cutoff })
let h0 = Array::makei(n, fn(_) { Array::make(n, None) })
let h1 = Array::makei(n, fn(_) { Array::make(n, None) })
let conditions_aligned = Array::makei(n, fn(_) { Array::make(n, false) })
for i = 0; i < n; i = i + 1 {
h0[i][i] = Some(0.0)
h1[i][i] = Some(0.0)
conditions_aligned[i][i] = true
for j = i + 1; j < n; j = j + 1 {
let d0 = compare_analyses(analyses[i], analyses[j], dimension=0)
let d1 = compare_analyses(analyses[i], analyses[j], dimension=1)
h0[i][j] = d0
h0[j][i] = d0
h1[i][j] = d1
h1[j][i] = d1
let aligned = kinds[i] == kinds[j] && cutoffs[i] == cutoffs[j]
conditions_aligned[i][j] = aligned
conditions_aligned[j][i] = aligned
}
}
{ names, kinds, cutoffs, h0, h1, conditions_aligned, }
}
///|
fn matrix_value(value : Double?) -> Json {
match value {
Some(number) => number.to_json()
None => Json::null()
}
}
///|
/// Portable matrix schema; null is deliberately distinct from a zero distance.
pub fn batch_matrix_json(matrix : BatchDistanceMatrix) -> Json {
let h0 : Array[Array[Json]] = []
let h1 : Array[Array[Json]] = []
for row in matrix.h0 {
h0.push(row.map(matrix_value))
}
for row in matrix.h1 {
h1.push(row.map(matrix_value))
}
{
"schema_version": 1,
"metric": "bottleneck",
"names": matrix.names,
"kinds": matrix.kinds,
"cutoffs": matrix.cutoffs,
"h0": h0,
"h1": h1,
"conditions_aligned": matrix.conditions_aligned,
"null_meaning": "unequal counts of classes alive at cutoff; no finite match",
"comparison_note": "matching kind and cutoff is necessary but not sufficient; verify units, grayscale transforms and data meaning before interpreting distances",
}
}
///|
/// Square CSV with blanks for null distances. Batch names are CSV-safe ASCII.
pub fn batch_matrix_csv(
matrix : BatchDistanceMatrix,
dimension : Int,
) -> String raise TopologyError {
if dimension != 0 && dimension != 1 {
raise TopologyError("matrix dimension must be 0 or 1")
}
let rows = if dimension == 0 { matrix.h0 } else { matrix.h1 }
let mut csv = "case"
for name in matrix.names {
csv += "," + name
}
csv += "\n"
for i, name in matrix.names {
csv += name
for value in rows[i] {
let field = match value {
Some(number) => number.to_string()
None => ""
}
csv += "," + field
}
csv += "\n"
}
csv
}
///|
fn matrix_max(rows : Array[Array[Double?]]) -> Double {
let mut greatest = 0.0
for row in rows {
for value in row {
if value is Some(number) && number > greatest {
greatest = number
}
}
}
greatest
}
///|
fn matrix_table(
matrix : BatchDistanceMatrix,
rows : Array[Array[Double?]],
dimension : Int,
) -> String {
let greatest = matrix_max(rows)
let mut html = ""
}
///|
fn matrix_html_escape(value : String) -> String {
value
.replace_all(old="&", new="&")
.replace_all(old="<", new="<")
.replace_all(old=">", new=">")
.replace_all(old="\"", new=""")
}
///|
/// Static offline heatmap with no JavaScript or network dependency.
pub fn batch_matrix_html(matrix : BatchDistanceMatrix) -> String {
let mut html =
#|MoonTopoLens · 批量距离热图MoonTopoLens · 批量距离热图
同一批案例的 H0/H1 瓶颈距离;每个无序案例对只计算一次。点击行标题查看案例报告。
输入条件
橙框表示两侧复形类型或截止值不同。相同类型和截止值也不保证尺度单位、灰度变换和数据含义可比。
破折号表示截止时存活类数量不同,因而没有有限匹配;它不是距离 0。颜色只按本维度的有限距离归一化,用于浏览,数值以 CSV/JSON 为准。
"
html += matrix_table(matrix, matrix.h0, 0)
html += matrix_table(matrix, matrix.h1, 1)
html +
""
}