///|
/// Validate coordinates and compute Euclidean distances in any dimension <= 32.
/// Limits avoid overflow and accidental quadratic allocation on untrusted input.
pub fn distance_matrix(
points : Array[Array[Double]],
) -> Array[Array[Double]] raise TopologyError {
let n = points.length()
if n > 128 {
raise TopologyError("at most 128 points are supported")
}
if n == 0 {
return []
}
let dimension = points[0].length()
if dimension < 1 || dimension > 32 {
raise TopologyError("point dimension must be between 1 and 32")
}
for point in points {
if point.length() != dimension {
raise TopologyError("inconsistent point dimensions")
}
for x in point {
if !finite(x) || x.abs() > 1.0e100 {
raise TopologyError(
"coordinates must be finite with absolute value <= 1.0e100",
)
}
}
}
let matrix = Array::makei(n, fn(_) { Array::make(n, 0.0) })
for i = 0; i < n; i = i + 1 {
for j = i + 1; j < n; j = j + 1 {
let mut d = 0.0
for k = 0; k < dimension; k = k + 1 {
d = @math.hypot(d, points[i][k] - points[j][k])
}
matrix[i][j] = d
matrix[j][i] = d
}
}
matrix
}
///|
/// Symmetric nonnegative dissimilarities are accepted without a triangle axiom.
/// Exact symmetry is required, rather than silently changing user data.
pub fn validate_matrix(
matrix : Array[Array[Double]],
) -> Unit raise TopologyError {
let n = matrix.length()
if n > 128 {
raise TopologyError("at most 128 matrix rows are supported")
}
for row in matrix {
if row.length() != n {
raise TopologyError("distance matrix must be square")
}
}
for i = 0; i < n; i = i + 1 {
if matrix[i][i] != 0.0 {
raise TopologyError("matrix diagonal must be zero")
}
for j = 0; j < n; j = j + 1 {
let d = matrix[i][j]
if !finite(d) || d < 0.0 || d > 1.0e100 {
raise TopologyError(
"distances must be finite, nonnegative and <= 1.0e100",
)
}
if d != matrix[j][i] {
raise TopologyError("distance matrix must be symmetric")
}
}
}
}
///|
/// Generate a deterministic 2D circle for teaching and reproducible examples.
pub fn circle_points(
count : Int,
radius : Double,
) -> Array[Array[Double]] raise TopologyError {
if count < 3 ||
count > 128 ||
!finite(radius) ||
radius <= 0.0 ||
radius > 1.0e100 {
raise TopologyError(
"circle requires 3..128 points and positive finite radius <= 1.0e100",
)
}
Array::makei(count, fn(i) {
let angle = 2.0 * @math.PI * i.to_double() / count.to_double()
[radius * @math.cos(angle), radius * @math.sin(angle)]
})
}