1#![allow(clippy::cast_precision_loss)] pub use crate::singularity_types::*;
5
6use crate::index::{AnnIndex, IndexBackend, IndexStats};
7use crate::singularity_cache::{CacheMetrics, CacheMetricsSnapshot};
8use crate::singularity_retrieval::RetrievalConfig;
9use crate::singularity_state::NamespaceState;
10use csm_core_lib::error::{MemoryError, Result};
11use csm_core_lib::hyperdim::Hypervector;
12use std::collections::HashMap;
13use std::sync::Arc;
14use tracing::instrument;
15
16pub struct Singularity<H: Hypervector = csm_core_lib::hyperdim::HVec10240> {
17 pub config: SingularityConfig,
18 pub namespaces: HashMap<String, NamespaceState<H>>,
19 pub(crate) _retrieval_config: RetrievalConfig,
20 pub cache_metrics: Arc<CacheMetrics>,
21}
22
23#[allow(dead_code)]
25pub type BinarySingularity = Singularity<csm_core_lib::BHVec10240>;
26
27impl<H: Hypervector + 'static> Singularity<H> {
28 pub fn new(config: SingularityConfig) -> Self {
29 Self::new_with_metrics(config, Arc::new(CacheMetrics::default()))
30 }
31
32 pub fn new_with_metrics(config: SingularityConfig, cache_metrics: Arc<CacheMetrics>) -> Self {
33 Self {
34 config,
35 namespaces: HashMap::new(),
36 _retrieval_config: RetrievalConfig::default(),
37 cache_metrics,
38 }
39 }
40
41 pub fn with_config(config: SingularityConfig) -> Self {
42 Self::new(config)
43 }
44
45 pub fn with_config_and_backend(config: SingularityConfig, backend: IndexBackend) -> Self {
46 let mut cfg = config;
47 cfg.index_backend = backend;
48 Self::new(cfg)
49 }
50
51 pub fn with_config_backend_and_metrics(
52 config: SingularityConfig,
53 backend: IndexBackend,
54 cache_metrics: Arc<CacheMetrics>,
55 ) -> Self {
56 let mut cfg = config;
57 cfg.index_backend = backend;
58 Self::new_with_metrics(cfg, cache_metrics)
59 }
60
61 fn create_index(&self) -> Result<Box<dyn AnnIndex<H>>> {
62 crate::index::create_index(&self.config.index_backend)
63 }
64
65 pub fn get_namespace(&self, ns: &str) -> Option<&NamespaceState<H>> {
66 self.namespaces.get(ns)
67 }
68
69 pub fn ensure_namespace(&mut self, ns: &str) -> Result<&mut NamespaceState<H>> {
74 if !self.namespaces.contains_key(ns) {
75 let index = self.create_index()?;
76 self.namespaces.insert(
77 ns.to_string(),
78 NamespaceState::new(&self.config, index, Arc::clone(&self.cache_metrics)),
79 );
80 }
81 self.namespaces
83 .get_mut(ns)
84 .ok_or_else(|| MemoryError::NotFound {
85 entity: "Namespace".to_string(),
86 id: ns.to_string(),
87 })
88 }
89
90 pub fn get_namespace_mut(&mut self, ns: &str) -> Result<&mut NamespaceState<H>> {
97 self.ensure_namespace(ns)
98 }
99
100 #[instrument(skip(self, concept))]
101 pub fn inject(&mut self, ns: &str, concept: Concept<H>) -> Result<()> {
102 self.evict_oldest_if_needed(ns);
103 let id = concept.id.clone();
104 let vector = concept.vector;
105
106 let ns_state = self.ensure_namespace(ns)?;
107
108 ns_state.index.insert(id.clone(), &vector)?;
110
111 if let Some(_old) = ns_state.concepts.insert(id.clone(), concept) {
112 if let Some(pos) = ns_state.id_to_index.get(&id) {
113 ns_state.concept_vectors[*pos] = vector;
114 }
115 self.invalidate_cache(ns);
116 } else {
117 let pos = ns_state.concept_vectors.len();
118 ns_state.concept_vectors.push(vector);
119 ns_state.concept_indices.push(id.clone());
120 ns_state.id_to_index.insert(id, pos);
121 }
122
123 Ok(())
124 }
125
126 pub fn update(&mut self, ns: &str, id: &str, vector: H) -> Result<()> {
127 let ns_state = self.ensure_namespace(ns)?;
128 if let Some(concept) = ns_state.concepts.get_mut(id) {
129 concept.vector = vector;
130 concept.modified_at = unix_now_secs();
131 if let Some(pos) = ns_state.id_to_index.get(id) {
132 ns_state.concept_vectors[*pos] = vector;
133 }
134 ns_state.index.insert(id.to_string(), &vector)?;
135 self.invalidate_cache(ns);
136 Ok(())
137 } else {
138 Err(MemoryError::NotFound {
139 entity: "Concept".to_string(),
140 id: id.to_string(),
141 })
142 }
143 }
144
145 pub fn delete(&mut self, ns: &str, id: &str) -> Result<()> {
146 let ns_state = self.ensure_namespace(ns)?;
147 if ns_state.concepts.remove(id).is_some() {
148 ns_state.associations.remove(id);
149 for neighbors in ns_state.associations.values_mut() {
150 neighbors.remove(id);
151 }
152 if let Some(pos) = ns_state.id_to_index.remove(id) {
153 let _ = ns_state.concept_vectors.swap_remove(pos);
154 ns_state.concept_indices.swap_remove(pos);
155 if pos < ns_state.concept_indices.len() {
156 let moved_id = &ns_state.concept_indices[pos];
157 ns_state.id_to_index.insert(moved_id.clone(), pos);
158 }
159 }
160 ns_state.index.delete(id)?;
161 self.invalidate_cache(ns);
162 Ok(())
163 } else {
164 Err(MemoryError::NotFound {
165 entity: "Concept".to_string(),
166 id: id.to_string(),
167 })
168 }
169 }
170
171 pub fn clear(&mut self, ns: &str) {
172 if let Some(ns_state) = self.namespaces.get_mut(ns) {
173 ns_state.concepts.clear();
174 ns_state.associations.clear();
175 ns_state.concept_vectors.clear();
176 ns_state.concept_indices.clear();
177 ns_state.id_to_index.clear();
178 let _ = ns_state.index.rebuild(&HashMap::new());
179 self.invalidate_cache(ns);
180 }
181 }
182
183 pub fn get(&self, ns: &str, id: &str) -> Option<&Concept<H>> {
184 self.get_namespace(ns).and_then(|n| n.concepts.get(id))
185 }
186
187 pub fn associate(&mut self, ns: &str, from: &str, to: &str, strength: f32) -> Result<()> {
188 if !strength.is_finite() {
190 return Err(MemoryError::InvalidInput {
191 field: "strength".to_string(),
192 reason: "association strength must be finite".to_string(),
193 });
194 }
195 if !(0.0..=1.0).contains(&strength) {
196 return Err(MemoryError::InvalidInput {
197 field: "strength".to_string(),
198 reason: format!("association strength must be in [0.0, 1.0], got {strength}"),
199 });
200 }
201
202 let max_assoc = self.config.max_associations_per_concept;
204
205 let ns_state = self.ensure_namespace(ns)?;
206 if !ns_state.concepts.contains_key(from) {
207 return Err(MemoryError::NotFound {
208 entity: "Concept".to_string(),
209 id: from.to_string(),
210 });
211 }
212 if !ns_state.concepts.contains_key(to) {
213 return Err(MemoryError::NotFound {
214 entity: "Concept".to_string(),
215 id: to.to_string(),
216 });
217 }
218
219 let neighbors = ns_state.associations.entry(from.to_string()).or_default();
220 neighbors.insert(to.to_string(), (strength, unix_now_secs()));
221
222 if let Some(limit) = max_assoc {
224 while neighbors.len() > limit {
225 if let Some(weakest) = neighbors
226 .iter()
227 .min_by(|a, b| {
228 a.1.0
229 .partial_cmp(&b.1.0)
230 .unwrap_or(std::cmp::Ordering::Equal)
231 })
232 .map(|(k, _)| k.clone())
233 {
234 neighbors.remove(&weakest);
235 } else {
236 break;
237 }
238 }
239 }
240
241 Ok(())
242 }
243
244 pub fn disassociate(&mut self, ns: &str, from: &str, to: &str) -> Result<()> {
245 let ns_state = self.ensure_namespace(ns)?;
246 if let Some(neighbors) = ns_state.associations.get_mut(from) {
247 neighbors.remove(to);
248 }
249 Ok(())
250 }
251
252 pub fn get_associations(&self, ns: &str, id: &str) -> Vec<(String, f32)> {
253 self.get_associations_with_decay(ns, id, DecayCurve::None)
254 }
255
256 pub fn get_associations_with_decay(
258 &self,
259 ns: &str,
260 id: &str,
261 curve: DecayCurve,
262 ) -> Vec<(String, f32)> {
263 let now = unix_now_secs();
264 self.get_namespace(ns)
265 .and_then(|n| n.associations.get(id))
266 .map(|m| {
267 m.iter()
268 .map(|(k, (strength, created_at))| {
269 let elapsed = now.saturating_sub(*created_at);
270 (k.clone(), curve.apply(*strength, elapsed))
271 })
272 .collect::<Vec<_>>()
273 })
274 .unwrap_or_default()
275 }
276
277 pub fn incoming_associations(&self, ns: &str, id: &str) -> Vec<(String, f32)> {
278 self.incoming_associations_with_decay(ns, id, DecayCurve::None)
279 }
280
281 pub fn incoming_associations_with_decay(
283 &self,
284 ns: &str,
285 id: &str,
286 curve: DecayCurve,
287 ) -> Vec<(String, f32)> {
288 let now = unix_now_secs();
289 let mut incoming = Vec::new();
290 if let Some(ns_state) = self.get_namespace(ns) {
291 for (from_id, neighbors) in &ns_state.associations {
292 if let Some((strength, created_at)) = neighbors.get(id) {
293 let elapsed = now.saturating_sub(*created_at);
294 incoming.push((from_id.clone(), curve.apply(*strength, elapsed)));
295 }
296 }
297 }
298 incoming
299 .sort_unstable_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(std::cmp::Ordering::Equal));
300 incoming
301 }
302
303 pub fn all_concepts(&self, ns: &str) -> Vec<Concept<H>> {
304 self.get_namespace(ns)
305 .map(|n| n.concepts.values().cloned().collect())
306 .unwrap_or_default()
307 }
308
309 pub fn all_associations(&self, ns: &str) -> Vec<(String, String, f32)> {
310 self.all_associations_with_decay(ns, DecayCurve::None)
311 }
312
313 pub fn all_associations_with_decay(
315 &self,
316 ns: &str,
317 curve: DecayCurve,
318 ) -> Vec<(String, String, f32)> {
319 let now = unix_now_secs();
320 let mut all = Vec::new();
321 if let Some(ns_state) = self.get_namespace(ns) {
322 for (from, neighbors) in &ns_state.associations {
323 for (to, (strength, created_at)) in neighbors {
324 let elapsed = now.saturating_sub(*created_at);
325 all.push((from.clone(), to.clone(), curve.apply(*strength, elapsed)));
326 }
327 }
328 }
329 all
330 }
331
332 pub fn len(&self, ns: &str) -> usize {
333 self.get_namespace(ns).map_or(0, |n| n.concepts.len())
334 }
335
336 pub fn is_empty(&self, ns: &str) -> bool {
337 self.get_namespace(ns).is_none_or(|n| n.concepts.is_empty())
338 }
339
340 pub fn cache_metrics_snapshot(&self, ns: &str) -> CacheMetricsSnapshot {
341 self.get_namespace(ns)
342 .map_or(CacheMetricsSnapshot::default(), |n| {
343 n.cache_metrics.snapshot()
344 })
345 }
346
347 fn evict_oldest_if_needed(&mut self, ns: &str) {
348 let Some(limit) = self.config.max_concepts else {
349 return;
350 };
351
352 while self.len(ns) >= limit {
353 let oldest = {
354 let Some(ns_state) = self.get_namespace(ns) else {
355 break;
356 };
357 ns_state
358 .concepts
359 .values()
360 .min_by_key(|c| c.created_at)
361 .map(|c| c.id.clone())
362 };
363
364 if let Some(id) = oldest {
365 let _ = self.delete(ns, &id);
366 } else {
367 break;
368 }
369 }
370 }
371
372 pub fn invalidate_cache(&self, ns: &str) {
373 if let Some(ns_state) = self.get_namespace(ns) {
374 if let Ok(mut cache) = ns_state.query_cache.write() {
375 cache.clear();
376 }
377 }
378 }
379
380 pub fn index_stats(&self, ns: &str) -> IndexStats {
381 self.get_namespace(ns)
382 .map(|n| n.index.stats())
383 .unwrap_or_default()
384 }
385
386 pub const fn retrieval_config(&self) -> &RetrievalConfig {
387 &self._retrieval_config
388 }
389}
390
391#[cfg(not(target_arch = "wasm32"))]
393pub fn unix_now_ns() -> u64 {
394 let nanos = std::time::SystemTime::now()
395 .duration_since(std::time::UNIX_EPOCH)
396 .unwrap_or_default()
397 .as_nanos();
398 u64::try_from(nanos).unwrap_or(u64::MAX)
399}
400
401#[cfg(target_arch = "wasm32")]
403pub fn unix_now_ns() -> u64 {
404 (js_sys::Date::new_0().get_time() * 1_000_000.0) as u64
405}
406
407pub fn similarity_cache_key<H: Hypervector>(query: &H, top_k: usize) -> u64 {
408 use std::hash::{Hash, Hasher};
409 let mut s = std::collections::hash_map::DefaultHasher::new();
410 query.hash(&mut s);
413 top_k.hash(&mut s);
414 s.finish()
415}