use std::sync::{Arc, Mutex};
use g_math::fixed_point::FixedPoint;
use crate::hyperbolic_geometry::HyperbolicPoint;
use crate::klein::{self, KleinPoint};
use crate::metric_tree::{CachedNormPoint, HyperbolicMetric, MetricVpTree};
use crate::store::{Store, StoreError};
use crate::tensor_network::HyperbolicTensorNetwork;
struct Anchor {
path: String,
dim: usize,
site: KleinPoint,
gamma: FixedPoint,
}
pub struct SemanticDisk {
anchors: Vec<Anchor>,
cache: Mutex<Option<(u64, Arc<MetricVpTree<CachedNormPoint>>)>>,
}
impl SemanticDisk {
pub fn build(spec: &[(&str, usize)]) -> Result<Self, StoreError> {
if spec.is_empty() {
return Err(StoreError::InvalidOperation(
"semantic disk spec must name at least one concept".to_string(),
));
}
let mut pairs: Vec<(String, usize)> = spec
.iter()
.map(|(p, d)| (normalize_concept_path(p), *d))
.collect();
pairs.sort();
for w in pairs.windows(2) {
if w[0].0 == w[1].0 {
return Err(StoreError::InvalidOperation(format!(
"duplicate concept path in spec: {}",
w[0].0
)));
}
}
{
let mut dims: Vec<usize> = pairs.iter().map(|(_, d)| *d).collect();
dims.sort_unstable();
if dims.windows(2).any(|w| w[0] == w[1]) {
return Err(StoreError::InvalidOperation(
"duplicate affinity dim in spec: each concept needs its own dimension"
.to_string(),
));
}
}
let taxonomy = Store::new();
for (path, _) in &pairs {
for ancestor in ancestors_of(path) {
if !taxonomy.exists(&ancestor) {
taxonomy.put(&ancestor, ancestor.as_bytes())?;
}
}
if !taxonomy.exists(path) {
taxonomy.put(path, path.as_bytes())?;
}
}
let one = FixedPoint::from_int(1);
let mut anchors = Vec::with_capacity(pairs.len());
for (path, dim) in pairs {
let point = taxonomy.position_fixed(&path)?;
let site = klein::poincare_to_klein(&point);
let radicand = if site.weight > crate::constants::small_epsilon() {
site.weight
} else {
crate::constants::small_epsilon()
};
let gamma = one / radicand.sqrt();
anchors.push(Anchor { path, dim, site, gamma });
}
Ok(Self { anchors, cache: Mutex::new(None) })
}
pub fn concepts(&self) -> Vec<&str> {
self.anchors.iter().map(|a| a.path.as_str()).collect()
}
pub fn position_of(&self, store: &Store, key: &str) -> Result<Option<Vec<f64>>, StoreError> {
Ok(self
.derive_from_coords(&store.get_semantic(key)?)
.map(|p| p.coords().iter().map(|c| c.to_f64()).collect()))
}
pub fn concept_of(&self, store: &Store, key: &str) -> Result<Option<String>, StoreError> {
Ok(self
.derive_from_coords(&store.get_semantic(key)?)
.and_then(|p| self.classify_point(&p)))
}
pub fn nearest(
&self,
store: &Store,
key: &str,
k: usize,
) -> Result<Vec<(String, f64)>, StoreError> {
let Some(query) = self.derive_from_coords(&store.get_semantic(key)?) else {
return Err(StoreError::InvalidOperation(format!(
"{} has no concept position (no positive affinity on any mapped dim)",
key
)));
};
let index = self.index(store)?;
Ok(index
.knn(&CachedNormPoint::new(query), k + 1, &HyperbolicMetric)
.into_iter()
.filter(|(id, _)| id != key)
.take(k)
.map(|(id, d)| (id, d.to_f64()))
.collect())
}
pub fn nearest_to_weights(
&self,
store: &Store,
weights: &[f64],
k: usize,
) -> Result<Vec<(String, f64)>, StoreError> {
if weights.len() != self.anchors.len() {
return Err(StoreError::InvalidOperation(format!(
"expected {} weights (one per mapped concept), got {}",
self.anchors.len(),
weights.len()
)));
}
let fixed: Vec<FixedPoint> = weights.iter().map(|w| FixedPoint::from_f64(*w)).collect();
let Some(query) = self.derive_from_weights(&fixed) else {
return Err(StoreError::InvalidOperation(
"no positive weight supplied — the query has no concept position".to_string(),
));
};
let index = self.index(store)?;
Ok(index
.knn(&CachedNormPoint::new(query), k, &HyperbolicMetric)
.into_iter()
.map(|(id, d)| (id, d.to_f64()))
.collect())
}
pub fn classify_trajectory(
&self,
sample_dim_start: usize,
samples: &[(u64, Vec<f64>)],
) -> Vec<(u64, String)> {
samples
.iter()
.filter_map(|(epoch, values)| {
let weights: Vec<FixedPoint> = self
.anchors
.iter()
.map(|a| {
a.dim
.checked_sub(sample_dim_start)
.and_then(|i| values.get(i))
.map_or(FixedPoint::from_int(0), |v| FixedPoint::from_f64(*v))
})
.collect();
let point = self.derive_from_weights(&weights)?;
self.classify_point(&point).map(|c| (*epoch, c))
})
.collect()
}
fn derive_from_coords(&self, coords: &[u8]) -> Option<HyperbolicPoint> {
if coords.is_empty() {
return None;
}
let weights: Vec<FixedPoint> = self
.anchors
.iter()
.map(|a| {
HyperbolicTensorNetwork::decode_semantic_slice(coords, &(a.dim..a.dim + 1))[0]
})
.collect();
self.derive_from_weights(&weights)
}
fn derive_from_weights(&self, weights: &[FixedPoint]) -> Option<HyperbolicPoint> {
let zero = FixedPoint::from_int(0);
let one = FixedPoint::from_int(1);
let mut denom = zero;
let mut numer: Option<g_math::fixed_point::FixedVector> = None;
for (a, w) in self.anchors.iter().zip(weights) {
if *w <= zero {
continue;
}
let coeff = *w * a.gamma;
let dim = a.site.dimension();
let acc = numer.get_or_insert_with(|| g_math::fixed_point::FixedVector::new(dim));
for i in 0..dim {
acc[i] += a.site.coords[i] * coeff;
}
denom += coeff;
}
let numer = numer?;
if denom <= zero {
return None;
}
let inv = one / denom;
let dim = numer.len();
let mut coords = g_math::fixed_point::FixedVector::new(dim);
for i in 0..dim {
coords[i] = numer[i] * inv;
}
Some(klein::klein_to_poincare(&KleinPoint::new(coords)))
}
fn classify_point(&self, point: &HyperbolicPoint) -> Option<String> {
let query = klein::poincare_to_klein(point);
let one = FixedPoint::from_int(1);
let mut best: Option<(usize, FixedPoint)> = None;
for (i, a) in self.anchors.iter().enumerate() {
let score = (one - query.coords.dot(&a.site.coords)) * a.gamma;
let better = match &best {
None => true,
Some((_, incumbent)) => score < *incumbent,
};
if better {
best = Some((i, score));
}
}
best.map(|(i, _)| self.anchors[i].path.clone())
}
fn index(&self, store: &Store) -> Result<Arc<MetricVpTree<CachedNormPoint>>, StoreError> {
let epoch = store.semantic_epoch();
{
let cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
if let Some((tagged, tree)) = cache.as_ref() {
if *tagged == epoch {
return Ok(Arc::clone(tree));
}
}
}
let build_epoch = store.semantic_epoch();
let mut keys = store.list("/")?;
keys.sort();
let entries: Vec<(String, CachedNormPoint)> = keys
.into_iter()
.filter_map(|key| {
let coords = store.get_semantic(&key).ok()?;
let point = self.derive_from_coords(&coords)?;
Some((key, CachedNormPoint::new(point)))
})
.collect();
let tree = Arc::new(MetricVpTree::build(entries, &HyperbolicMetric));
let mut cache = self.cache.lock().unwrap_or_else(|e| e.into_inner());
*cache = Some((build_epoch, Arc::clone(&tree)));
Ok(tree)
}
}
fn normalize_concept_path(path: &str) -> String {
let mut p = if path.starts_with('/') {
path.to_string()
} else {
format!("/{}", path)
};
while p.len() > 1 && p.ends_with('/') {
p.pop();
}
p
}
fn ancestors_of(path: &str) -> Vec<String> {
let mut out = Vec::new();
let mut idx = 1;
while let Some(next) = path[idx..].find('/') {
out.push(path[..idx + next].to_string());
idx += next + 1;
}
out
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn path_helpers() {
assert_eq!(normalize_concept_path("trauma"), "/trauma");
assert_eq!(normalize_concept_path("/a/b/"), "/a/b");
assert_eq!(ancestors_of("/a"), Vec::<String>::new());
assert_eq!(ancestors_of("/a/b/c"), vec!["/a".to_string(), "/a/b".to_string()]);
}
#[test]
fn build_rejects_bad_specs() {
assert!(SemanticDisk::build(&[]).is_err());
assert!(SemanticDisk::build(&[("/a", 16), ("/a", 17)]).is_err());
assert!(SemanticDisk::build(&[("/a", 16), ("/b", 16)]).is_err());
}
#[test]
fn anchors_are_sorted_and_embedded() {
let disk = SemanticDisk::build(&[("/zeta", 18), ("/alpha", 16), ("/mid", 17)]).unwrap();
assert_eq!(disk.concepts(), vec!["/alpha", "/mid", "/zeta"]);
for w in disk.anchors.windows(2) {
assert!(w[0].site.coords[0] != w[1].site.coords[0]
|| w[0].site.coords[1] != w[1].site.coords[1]);
}
}
}