///|
fn reliability_noise(
value : Json,
state : Ref[Int],
amplitude : Double,
) -> Json raise TopologyError {
match value {
Array(values) =>
values
.map(fn(v) raise TopologyError { reliability_noise(v, state, amplitude) })
.to_json()
Number(v, ..) =>
(v + amplitude * (research_next(state, 65536).to_double() / 32768.0 - 1.0)).to_json()
_ =>
raise TopologyError(
"jitter requires numeric coordinates or scalar fields",
)
}
}
///|
/// Finite-grid simultaneous Hoeffding bands for an IID mean landscape.
fn reliability_mean_band(f : Map[String, Json]) -> Json raise TopologyError {
if research_string(f, "assumption", "") != "iid_independent_samples" {
raise TopologyError(
"mean_band requires explicit iid_independent_samples assumption",
)
}
let cases : Array[Json] = @json.from_json(required_field(f, "inputs")) catch {
_ => raise TopologyError("inputs must be independent analysis requests")
}
if cases.length() < 2 || cases.length() > 64 {
raise TopologyError("mean band requires 2..64 independent samples")
}
let start = research_number(f, "start", 0.0)
let end = number_field(f, "end")
let confidence = research_number(f, "confidence", 0.95)
if !finite(end) || start >= end || confidence <= 0.0 || confidence >= 1.0 {
raise TopologyError("invalid confidence or range")
}
let dimension = integer_field(f, "dimension", 1)
let vectors : Array[Array[Double]] = []
let mut total = 0
let mut kind = ""
let excluded : Array[Int] = []
for request in cases {
let a = research_request(request)
if a.filtration.cutoff != end {
raise TopologyError("mean band inputs require common cutoff=end")
}
if kind != "" && kind != a.filtration.kind {
raise TopologyError("mean band requires a common complex kind")
}
kind = a.filtration.kind
for cell in a.filtration.cells {
if cell.value < start {
raise TopologyError(
"mean band start must bound all filtration values from below",
)
}
}
total += a.filtration.cells.length()
if total > 64000 {
raise TopologyError("mean band exceeds 64000 cells")
}
vectors.push(research_vector(a, dimension, start, end, 31))
excluded.push(
a.intervals
.filter(fn(i) { i.dimension == dimension && i.death is None })
.length(),
)
}
let mean = Array::make(31, 0.0)
for row in vectors {
for j = 0; j < 31; j = j + 1 {
mean[j] += row[j] / cases.length().to_double()
}
}
let bound = (end - start) / 2.0
let epsilon = bound *
(@math.ln(62.0 / (1.0 - confidence)) / (2.0 * cases.length().to_double())).sqrt()
let grid = landscape_grid(start, end, 31)
let rows : Array[Json] = Array::makei(31, fn(j) {
{
"scale": grid[j],
"mean": mean[j],
"lower": maximum(0.0, mean[j] - epsilon),
"upper": if mean[j] + epsilon > bound {
bound
} else {
mean[j] + epsilon
},
}
})
{
"kind": "reliability",
"request": f.to_json(),
"mode": "mean_band",
"rows": rows,
"sample_count": cases.length(),
"confidence": confidence,
"epsilon": epsilon,
"dimension": dimension,
"censored_excluded_per_sample": excluded,
"grid_simultaneous": true,
"vacuous": epsilon >= bound,
"interpretation": "Hoeffding plus union bound over 31 fixed grid points for the population mean first finite landscape under IID independent sampling units and common observation conditions; not confidence for individual intervals, whole continuous curves, or overlapping windows",
}
}
///|
/// Seeded sensitivity and resampling envelopes; frequencies are not probabilities.
pub fn reliability_study(text : String) -> Json raise TopologyError {
let f = research_object(text, "reliability", [
"mode", "input", "inputs", "start", "end", "confidence", "assumption", "dimension",
"trials", "seed", "sizes", "thresholds", "jitter", "block_length", "match_tolerance",
])
let mode = research_string(f, "mode", "subsample_points")
if mode == "mean_band" {
return reliability_mean_band(f)
}
if !["subsample_points", "bootstrap_points", "block_series", "jitter"].contains(
mode,
) {
raise TopologyError("unknown reliability mode")
}
let original = batch_object(required_field(f, "input"), "reliability input")
let input_kind = batch_string(original, "kind")
let trials = integer_field(f, "trials", 16)
let seed = integer_field(f, "seed", 1)
let dimension = integer_field(f, "dimension", 1)
let amplitude = research_number(f, "jitter", 0.0)
if trials < 2 ||
trials > 64 ||
seed < 0 ||
seed > 65535 ||
amplitude < 0.0 ||
amplitude > 1.0e6 {
raise TopologyError(
"reliability requires 2..64 trials, 16-bit seed and bounded nonnegative jitter",
)
}
let thresholds = research_numbers(f, "thresholds", [
number_field(original, "threshold"),
])
let base = research_request(original.to_json())
let n = if input_kind == "points" || input_kind == "points_sparse" {
rows_field(original, "points").length()
} else {
base.filtration.vertex_count
}
let sizes = research_numbers(f, "sizes", [n.to_double()])
if thresholds.length() < 1 ||
thresholds.length() > 8 ||
sizes.length() < 1 ||
sizes.length() > 8 ||
thresholds.length() * sizes.length() * trials > 96 {
raise TopologyError("reliability requires <=96 resampled cases")
}
if mode == "subsample_points" || mode == "bootstrap_points" {
if (input_kind != "points" && input_kind != "points_sparse") ||
n < 2 ||
n > 128 {
raise TopologyError("point resampling requires 2..128 original points")
}
for size in sizes {
if size < 2.0 || size > n.to_double() || size != @math.floor(size) {
raise TopologyError(
"resampling sizes must be integers in 2..original count",
)
}
}
} else if sizes.length() != 1 || sizes[0] != n.to_double() {
raise TopologyError("sizes apply only to point resampling")
}
if mode == "block_series" && input_kind != "series" {
raise TopologyError("block resampling requires a series")
}
let state = Ref(seed)
let rows : Array[Json] = []
let features : Array[Json] = []
let envelopes : Array[Json] = []
let mut total = 0
for cutoff in thresholds {
let baseline_request = original.copy()
baseline_request.set("threshold", cutoff.to_json())
let baseline = research_request(baseline_request.to_json())
let start = research_number(
f,
"start",
if cutoff > 0.0 {
0.0
} else {
cutoff - 1.0
},
)
if start >= cutoff {
raise TopologyError("reliability start must be below every cutoff")
}
let reference = research_vector(baseline, dimension, start, cutoff, 31)
let tolerance = research_number(
f,
"match_tolerance",
maximum(1.0e-12, (cutoff - start) * 0.1),
)
if tolerance < 0.0 {
raise TopologyError("negative matching tolerance")
}
for size in sizes {
let support = Array::make(baseline.intervals.length(), 0)
let vectors : Array[Array[Double]] = []
for trial = 0; trial < trials; trial = trial + 1 {
let request = baseline_request.copy()
let indices : Array[Int] = []
if mode == "subsample_points" || mode == "bootstrap_points" {
let points = rows_field(original, "points")
let order = Array::makei(n, fn(i) { i })
for i = 0; i < size.to_int(); i = i + 1 {
if mode == "bootstrap_points" {
indices.push(research_next(state, n))
} else {
let j = i + research_next(state, n - i)
let old = order[i]
order[i] = order[j]
order[j] = old
indices.push(order[i])
}
}
request.set("points", indices.map(fn(i) { points[i] }).to_json())
} else if mode == "block_series" {
let series : Array[Double] = @json.from_json(
required_field(original, "series"),
) catch {
_ => raise TopologyError("invalid series")
}
let length = integer_field(f, "block_length", 4)
if length < 1 || length > series.length() || series.length() > 512 {
raise TopologyError(
"block_length must fit a series of <=512 samples",
)
}
while indices.length() < series.length() {
let begin = research_next(state, series.length() - length + 1)
for j = 0
j < length && indices.length() < series.length()
j = j + 1 {
indices.push(begin + j)
}
}
request.set("series", indices.map(fn(i) { series[i] }).to_json())
} else {
let field = match input_kind {
"points" | "points_sparse" => "points"
"grid" => "grid"
"volume" => "volume"
"series" => "series"
_ => raise TopologyError("jitter does not support this input")
}
request.set(
field,
reliability_noise(required_field(original, field), state, amplitude),
)
}
let result = analyze_json(
request.to_json().stringify(),
representatives=false,
)
total += result.filtration.cells.length()
if total > 64000 {
raise TopologyError("reliability exceeds 64000 resampled cells")
}
let vector = research_vector(result, dimension, start, cutoff, 31)
vectors.push(vector)
for m in interval_matching(baseline, result, dimension~) {
if m.left is Some(id) &&
m.right is Some(_) &&
m.cost is Some(cost) &&
cost <= tolerance {
support[id] += 1
}
}
rows.push({
"cutoff": cutoff,
"size": size,
"trial": trial,
"landscape_rms_change": research_l2(reference, vector),
"bottleneck": optional_number(
compare_analyses(baseline, result, dimension~),
),
"censored_count": result.intervals
.filter(fn(i) { i.dimension == dimension && i.death is None })
.length(),
"source_indices": indices,
})
}
for i, interval in baseline.intervals {
if interval.dimension == dimension {
features.push({
"cutoff": cutoff,
"size": size,
"interval_index": i,
"birth": interval.birth,
"death": optional_number(interval.death),
"retained": support[i],
"trials": trials,
"support_frequency": support[i].to_double() / trials.to_double(),
"match_tolerance": tolerance,
})
}
}
let grid = landscape_grid(start, cutoff, 31)
for j = 0; j < 31; j = j + 1 {
let column = vectors.map(fn(v) { v[j] })
envelopes.push({
"cutoff": cutoff,
"size": size,
"scale": grid[j],
"q025": research_quantile(column, 0.025),
"median": research_quantile(column, 0.5),
"q975": research_quantile(column, 0.975),
})
}
}
}
{
"kind": "reliability",
"request": f.to_json(),
"mode": mode,
"seed": seed,
"dimension": dimension,
"rows": rows,
"features": features,
"envelopes": envelopes,
"baseline": report_json(base),
"interpretation": "seeded empirical sensitivity experiment; pointwise resampling percentiles and matching support are not coverage guarantees, probabilities that a feature is real, or confidence sets; block resampling synthesizes block joins and assumes locally stationary dependence for interpretation; censored-count mismatch yields null distance",
}
}