pub fn canonical_n_harmonics(
basis_kinds: &[String],
raw_n_harmonics: &[i64],
decoder_widths: &[i64],
) -> Vec<i64> {
basis_kinds
.iter()
.zip(raw_n_harmonics)
.zip(decoder_widths)
.map(|((bk, &h), &width)| {
let kind = canon_name(bk);
if matches!(kind.as_str(), "periodic" | "periodic_spline" | "circle") && h <= 0 {
((width - 1) / 2).max(1)
} else {
h
}
})
.collect()
}
pub fn canon_name(name: &str) -> String {
name.trim().to_ascii_lowercase().replace('-', "_")
}
pub fn canonical_assignment_kind(kind: &str) -> Result<&'static str, String> {
match kind {
"softmax" => Ok("softmax"),
"ibp_map" => Ok("ibp_map"),
"threshold_gate" => Ok("threshold_gate"),
"topk" => Ok("topk"),
_ => Err(format!(
"assignment={kind:?} is not a recognized assignment kind; expected one of \
['ibp_map', 'softmax', 'threshold_gate', 'topk']"
)),
}
}
fn basis_alias_to_kind(normalized: &str) -> Option<&'static str> {
match normalized {
"circle" | "periodic" | "periodic_spline" => Some("periodic"),
"sphere" => Some("sphere"),
"torus" => Some("torus"),
"linear" | "linear_rank1" | "affine" => Some("linear"),
"linear_block" | "flat_block" => Some("linear_block"),
"euclidean" | "euclidean_patch" | "euclidean_quadratic_patch" => Some("euclidean"),
"duchon" => Some("duchon"),
"poincare" | "hyperbolic" | "poincare_patch" => Some("poincare"),
"cylinder" => Some("cylinder"),
"mobius" | "mobius_band" => Some("mobius"),
"auto" => Some("auto"),
_ => None,
}
}
pub fn canonical_basis_kind(name: &str) -> String {
let normalized = canon_name(name);
basis_alias_to_kind(&normalized).map_or(normalized, str::to_string)
}
pub fn basis_kind_for_topology(name: &str) -> String {
let normalized = canon_name(name);
basis_alias_to_kind(&normalized).map_or_else(|| name.to_string(), str::to_string)
}
pub fn basis_to_topology(basis: &str) -> String {
match canon_name(basis).as_str() {
"periodic" | "periodic_spline" | "circle" => "circle".to_string(),
"sphere" => "sphere".to_string(),
"torus" => "torus".to_string(),
"linear" | "linear_rank1" | "affine" => "linear".to_string(),
"linear_block" | "flat_block" => "linear_block".to_string(),
"duchon" | "euclidean" | "euclidean_patch" | "euclidean_quadratic_patch" => {
"euclidean".to_string()
}
"poincare" | "hyperbolic" | "poincare_patch" => "poincare".to_string(),
"cylinder" => "cylinder".to_string(),
"mobius" | "mobius_band" => "mobius".to_string(),
"auto" => "auto".to_string(),
_ => basis.to_string(),
}
}
pub fn canonical_topology(name: &str) -> String {
basis_to_topology(&canonical_basis_kind(name))
}
pub fn coordinate_periods_for_basis(
basis: &str,
latent_dim: usize,
) -> Result<Vec<Option<f64>>, String> {
let kind = canonical_basis_kind(basis);
match kind.as_str() {
"periodic" | "torus" => Ok(vec![Some(1.0); latent_dim]),
"cylinder" if latent_dim == 2 => Ok(vec![Some(1.0), None]),
"sphere" if latent_dim == 2 => Ok(vec![None, Some(std::f64::consts::TAU)]),
"mobius" if latent_dim == 2 => Ok(vec![Some(2.0), None]),
"cylinder" | "sphere" | "mobius" => Err(format!(
"{kind} atoms require latent dimension 2; got {latent_dim}"
)),
_ => Ok(vec![None; latent_dim]),
}
}
pub fn flat_block_assignment(gating: &str) -> Result<&'static str, String> {
match gating {
"norm_selection" => Ok("ibp_map"),
"separate_gate" => Ok("threshold_gate"),
_ => Err(format!(
"flat_block gating={gating:?} is not recognized; expected one of \
['norm_selection', 'separate_gate']"
)),
}
}
pub fn topologies_for_bases(bases: &[String]) -> Vec<String> {
bases.iter().map(|b| basis_to_topology(b)).collect()
}
pub fn topology_for_bases(bases: &[String]) -> Option<String> {
let per_atom = topologies_for_bases(bases);
let first = per_atom.first()?;
if per_atom.iter().all(|t| t == first) {
Some(first.clone())
} else {
Some("mixed".to_string())
}
}