eryon_actors/operators/
observer.rs

1/*
2    Appellation: observer <module>
3    Contrib: @FL03
4*/
5use crate::core::rstmt::prelude::{HarmonicFunction, LPR, PitchMod, Triad, Triads};
6use crate::ctx::ActorContext;
7use crate::drivers::RawDriver;
8use crate::mem::TopoLedger;
9use crate::{Actor, VNode};
10use rshyper::EdgeId;
11use scsys::Id;
12
13use num_traits::{Float, FromPrimitive, NumAssign};
14
15/// a type alias for a [`VecDeque`](alloc::collections::VecDeque) of observations, where each observation
16/// consists of a leading pitch region (LPR), a triad, and its context triads.
17pub(crate) type Observations = alloc::collections::VecDeque<(LPR, Triads, Triads)>;
18/// A type alias for a mapping of triads to their harmonic functions.
19pub(crate) type TonalRegions = std::collections::HashMap<[usize; 3], String>;
20
21/// Observer actors monitor the system without directly intervening.
22/// They collect data, analyze patterns, and build context but do not
23/// initiate transformations.
24#[derive(Clone, Debug)]
25pub struct Observer<T = f32> {
26    pub(crate) id: Id,
27    /// Threshold for accepting transformations (0.0 - 1.0)
28    pub(crate) threshold: T,
29    /// Recent observations of transformations and their context
30    pub(crate) observations: Observations,
31    /// Known tonal regions mapped by their center triads
32    pub(crate) tonal_regions: TonalRegions,
33    /// Initialized flag
34    pub(crate) initialized: bool,
35}
36
37impl<T> Observer<T> {
38    /// Create a new observer
39    pub fn new() -> Self
40    where
41        T: FromPrimitive,
42    {
43        let id: Id;
44        #[cfg(feature = "rand")]
45        {
46            id = Id::<u128>::random().map(|id| id as usize);
47        }
48        #[cfg(not(feature = "rand"))]
49        {
50            id = Id::atomic();
51        }
52        Observer {
53            id,
54            threshold: T::from_f32(0.95).unwrap(),
55            observations: Observations::new(),
56            tonal_regions: TonalRegions::new(),
57            initialized: false,
58        }
59    }
60    /// returns a copy of the observer's ID
61    pub const fn id(&self) -> Id {
62        self.id
63    }
64    /// returns true if the observer is initialized
65    pub const fn is_initialized(&self) -> bool {
66        self.initialized
67    }
68    /// returns true if the observer is not initialized
69    pub const fn is_uninitialized(&self) -> bool {
70        !self.initialized
71    }
72    /// returns a reference to the observation threshold
73    pub const fn threshold(&self) -> &T {
74        &self.threshold
75    }
76    /// returns a mutable reference to the observation threshold
77    pub const fn threshold_mut(&mut self) -> &mut T {
78        &mut self.threshold
79    }
80    /// returns an immutable reference to the observations
81    pub const fn observations(&self) -> &Observations {
82        &self.observations
83    }
84    /// returns a mutable reference to the observations
85    pub const fn observations_mut(&mut self) -> &mut Observations {
86        &mut self.observations
87    }
88    /// returns an immutable reference to the tonal regions
89    pub const fn tonal_regions(&self) -> &TonalRegions {
90        &self.tonal_regions
91    }
92    /// returns a mutable reference to the tonal regions
93    pub const fn tonal_regions_mut(&mut self) -> &mut TonalRegions {
94        &mut self.tonal_regions
95    }
96    /// update the current id and return a mutable reference to the observer
97    pub fn set_id(&mut self, id: Id) -> &mut Self {
98        self.id = id;
99        self
100    }
101    /// update the current threshold and return a mutable reference to the observer
102    pub fn set_threshold(&mut self, threshold: T) -> &mut Self
103    where
104        T: Float,
105    {
106        self.threshold = threshold.max(T::zero()).min(T::one());
107        self
108    }
109    /// update the current observations and return a mutable reference to the observer
110    pub fn set_observations(&mut self, observations: Observations) -> &mut Self {
111        self.observations = observations;
112        self
113    }
114    /// update the current tonal regions and return a mutable reference to the observer
115    pub fn set_tonal_regions(&mut self, tonal_regions: TonalRegions) -> &mut Self {
116        self.tonal_regions = tonal_regions;
117        self
118    }
119    /// consumes the current instance to toggle the initialized flag
120    pub fn initialized(self) -> Self {
121        Self {
122            initialized: !self.initialized,
123            ..self
124        }
125    }
126    /// consumes the current instance to create another with the given ID
127    pub fn with_id(self, id: Id) -> Self {
128        Self { id, ..self }
129    }
130    /// consumes the current instance to create another with the given observations
131    pub fn with_observations(self, observations: Observations) -> Self {
132        Self {
133            observations,
134            ..self
135        }
136    }
137    /// consumes the current instance to create another with the given threshold
138    pub fn with_threshold(self, threshold: T) -> Self
139    where
140        T: Float,
141    {
142        Self {
143            threshold: threshold.max(T::zero()).min(T::one()),
144            ..self
145        }
146    }
147    /// consumes the current instance to create another with the given tonal regions
148    pub fn with_tonal_regions(self, tonal_regions: TonalRegions) -> Self {
149        Self {
150            tonal_regions,
151            ..self
152        }
153    }
154    /// set the initialized flag to true
155    pub fn initialize(self) -> Self {
156        Self {
157            initialized: true,
158            ..self
159        }
160    }
161    /// Get the number of transformations observed
162    pub fn observation_count(&self) -> usize {
163        self.observations.len()
164    }
165
166    /// Analyze recent transformations for patterns
167    pub fn analyze_patterns(&self, memory: &TopoLedger<T>) -> Vec<(Vec<LPR>, T)>
168    where
169        T: Float + FromPrimitive,
170    {
171        let mut patterns = Vec::new();
172
173        // Extract recent LPRs
174        let lpr_sequence: Vec<_> = self.observations.iter().map(|(lpr, _, _)| *lpr).collect();
175
176        if lpr_sequence.len() < 3 {
177            return patterns;
178        }
179
180        // Look for repeated subsequences
181        for window_size in 2..=lpr_sequence.len() / 2 {
182            for start in 0..=lpr_sequence.len() - window_size {
183                let pattern = lpr_sequence[start..start + window_size].to_vec();
184                let mut count = 0;
185
186                // Count occurrences in memory
187                for mem_pattern in memory.patterns() {
188                    if mem_pattern.sequence_len() >= pattern.len() {
189                        for i in 0..=mem_pattern.sequence_len() - pattern.len() {
190                            let mut matches = true;
191                            for j in 0..pattern.len() {
192                                // Convert usize back to LPR for comparison
193                                let mem_lpr = match mem_pattern[i + j] {
194                                    0 => LPR::Leading,
195                                    1 => LPR::Parallel,
196                                    2 => LPR::Relative,
197                                    _ => continue,
198                                };
199
200                                if mem_lpr != pattern[j] {
201                                    matches = false;
202                                    break;
203                                }
204                            }
205
206                            if matches {
207                                count += 1;
208                            }
209                        }
210                    }
211                }
212
213                // If pattern occurs multiple times
214                if count > 1 {
215                    let importance = T::from_usize(count).unwrap()
216                        / T::from_usize(memory.patterns().len()).unwrap();
217                    if &importance >= self.threshold() {
218                        patterns.push((pattern, importance));
219                    }
220                }
221            }
222        }
223
224        // Sort by importance
225        patterns.sort_by(|a, b| b.1.partial_cmp(&a.1).unwrap_or(core::cmp::Ordering::Equal));
226        patterns
227    }
228    /// Analyze a pattern and derive semantic meaning
229    fn analyze_pattern(&self, pattern: &[usize]) -> Vec<String> {
230        if pattern.is_empty() {
231            return Vec::new();
232        }
233
234        let mut meanings = Vec::new();
235
236        // Common cadential patterns
237        if pattern == &[2, 1] {
238            // Relative -> Parallel
239            meanings.push("Cadential".to_string());
240        } else if pattern == &[0, 1] {
241            // Leading -> Parallel
242            meanings.push("Deceptive".to_string());
243        } else if pattern == &[0, 0] {
244            // Leading -> Leading
245            meanings.push("Sequential".to_string());
246        } else if pattern == &[1, 1, 1] {
247            // Parallel -> Parallel -> Parallel
248            meanings.push("Modal Mixture".to_string());
249        } else if pattern == &[2, 2] {
250            // Relative -> Relative
251            meanings.push("Common-tone Modulation".to_string());
252        }
253
254        // Pattern structure analysis
255        let mut has_symmetry = false;
256        if pattern.len() >= 4 {
257            // Check for symmetrical patterns (palindromes)
258            let mut is_palindrome = true;
259            for i in 0..pattern.len() / 2 {
260                if pattern[i] != pattern[pattern.len() - 1 - i] {
261                    is_palindrome = false;
262                    break;
263                }
264            }
265            has_symmetry = is_palindrome;
266        }
267
268        if has_symmetry {
269            meanings.push("Symmetrical".to_string());
270        }
271
272        // Check for cycles
273        if pattern.len() >= 3 {
274            'cycle_check: for cycle_len in 1..=pattern.len() / 2 {
275                if pattern.len() % cycle_len != 0 {
276                    continue;
277                }
278
279                let mut is_cycle = true;
280                for i in 0..pattern.len() - cycle_len {
281                    if pattern[i] != pattern[i + cycle_len] {
282                        is_cycle = false;
283                        break;
284                    }
285                }
286
287                if is_cycle {
288                    meanings.push(format!("Cyclic-{}", cycle_len));
289                    break 'cycle_check;
290                }
291            }
292        }
293
294        // Analyze transformation distribution
295        let mut counts = [0, 0, 0]; // L, P, R counts
296        for &p in pattern {
297            if p < 3 {
298                counts[p] += 1;
299            }
300        }
301
302        // Characterize by dominant transformation type
303        let total = pattern.len();
304        if counts[0] > total / 2 {
305            meanings.push("Leading-dominated".to_string());
306        } else if counts[1] > total / 2 {
307            meanings.push("Parallel-dominated".to_string());
308        } else if counts[2] > total / 2 {
309            meanings.push("Relative-dominated".to_string());
310        } else if counts[0] == counts[1] && counts[1] == counts[2] && counts[0] > 0 {
311            meanings.push("Balanced".to_string());
312        }
313
314        meanings
315    }
316
317    /// Identify tonal region containing a triad
318    fn identify_tonal_region(&self, triad: &Triad) -> Option<String> {
319        // Simple version - just check if this exact triad is a region center
320        self.tonal_regions().get(&triad.notes()).cloned()
321    }
322
323    /// Analyze critical points in relation to triad
324    pub fn analyze_critical_points<D>(&self, vnode: &VNode<D>) -> Vec<(String, T)>
325    where
326        D: RawDriver<Triad>,
327        T: Float + FromPrimitive,
328    {
329        let mut analysis = Vec::new();
330
331        // Get current triad and critical points
332        let triad = vnode.headspace();
333        let critical_points = vnode.critical_points();
334
335        for (name, point) in critical_points {
336            // Calculate distance from current triad root to critical point
337            // using pitch class distance
338            let root = triad.root();
339            let distance = (((*point as isize) - (root as isize)).abs().pmod()) as usize;
340
341            // Normalize distance (0.0 = closest, 1.0 = furthest)
342            let normalized_distance = T::from_usize(distance).unwrap() / T::from_usize(6).unwrap(); // 6 is max distance in pitch class space
343
344            // Create relationship descriptor
345            let relationship = match distance {
346                0 => "At",
347                1 => "Adjacent to",
348                2 => "Near",
349                _ => "Distant from",
350            };
351
352            analysis.push((
353                format!("{} {}", relationship, name),
354                T::one() - normalized_distance,
355            ));
356        }
357
358        analysis
359    }
360}
361
362impl<D, T> Actor<D, T> for Observer<T>
363where
364    D: RawDriver<Triad>,
365    T: core::iter::Sum + Float + FromPrimitive + NumAssign,
366{
367    fn kind(&self) -> &'static str {
368        super::OperatorKind::Observer.as_ref()
369    }
370
371    fn initialize(&mut self) -> crate::Result<()> {
372        if !self.initialized {
373            self.observations.clear();
374            self.initialized = true;
375        }
376        Ok(())
377    }
378
379    fn process_transform(
380        &mut self,
381        transform: LPR,
382        plant: &mut D,
383        _memory: &mut TopoLedger<T>,
384    ) -> crate::Result<bool> {
385        // Record the observation with from and to triad classes
386        let from_class = plant.headspace().class();
387        // Apply transform temporarily to get resulting class
388        let to_triad = plant.headspace().transform(transform);
389        let to_class = to_triad.class();
390
391        // Store the observation
392        self.observations
393            .push_back((transform, from_class, to_class));
394
395        // Keep observation queue from growing too large
396        if self.observation_count() > 50 {
397            self.observations_mut().pop_front();
398        }
399
400        // Observer doesn't modify transformations, just watches
401        Ok(true) // Always allow transformation
402    }
403
404    fn process_message(
405        &mut self,
406        _source: EdgeId,
407        _message: &[u8],
408        _memory: &mut TopoLedger<T>,
409    ) -> crate::Result<()> {
410        // Observers don't process messages, they just observe
411        Ok(())
412    }
413
414    fn on_activate(&mut self, _plant: &D, _memory: &mut TopoLedger<T>) -> crate::Result<()> {
415        self.observations.clear();
416        Ok(())
417    }
418
419    fn on_deactivate(&mut self, _plant: &D, memory: &mut TopoLedger<T>) -> crate::Result<()> {
420        // Before deactivation, record patterns
421        for i in 2..=4 {
422            // Look for patterns of length 2-4
423            if self.observation_count() >= i {
424                // Get the most recent i transformations
425                let recent: Vec<usize> = self
426                    .observations
427                    .iter()
428                    .rev()
429                    .take(i)
430                    .map(|(lpr, _, _)| match lpr {
431                        LPR::Leading => 0,
432                        LPR::Parallel => 1,
433                        LPR::Relative => 2,
434                    })
435                    .collect();
436
437                // Record the pattern with medium importance
438                memory.record_pattern(&recent);
439            }
440        }
441
442        Ok(())
443    }
444
445    fn resource_requirements(&self) -> (usize, usize) {
446        // Observer uses minimal resources (memory, compute)
447        (50, 10)
448    }
449
450    fn allows_pattern_sharing(&self) -> bool {
451        true // Observer allows pattern sharing
452    }
453
454    fn contextualize(&self, plant: &D, memory: &TopoLedger<T>) -> crate::Result<ActorContext<T>> {
455        let triad = plant.headspace();
456
457        // Calculate stability based on memory relationships
458        let stability = if memory.count_features() > 0 {
459            memory.get_stability_for(&triad.notes())
460        } else {
461            // Default stability for major/minor triads
462            match triad.class() {
463                crate::nrt::Triads::Major => T::from_f32(0.8).unwrap(),
464                crate::nrt::Triads::Minor => T::from_f32(0.7).unwrap(),
465                _ => T::from_f32(0.5).unwrap(),
466            }
467        };
468
469        // Determine harmonic function
470        // Using C as tonic reference
471        let harmonic_function = match triad.root().pmod() {
472            0 => Some(HarmonicFunction::Tonic),        // C
473            7 => Some(HarmonicFunction::Dominant),     // G
474            5 => Some(HarmonicFunction::Subdominant),  // F
475            9 => Some(HarmonicFunction::Submediant),   // A
476            2 => Some(HarmonicFunction::Supertonic),   // D
477            11 => Some(HarmonicFunction::LeadingTone), // B
478            4 => Some(HarmonicFunction::Mediant),      // E
479            _ => None,
480        };
481
482        // Generate semantic tags
483        let mut semantic_tags = Vec::new();
484
485        // Add tonal region tag if recognized
486        if let Some(region) = self.identify_tonal_region(triad) {
487            semantic_tags.push(format!("Region:{}", region));
488        }
489
490        // Try to find patterns in recent transformations
491        if let Some(recent_pattern) = memory.get_recent_pattern(5) {
492            // Add pattern semantic analysis
493            let pattern_meanings = self.analyze_pattern(&recent_pattern);
494            semantic_tags.extend(pattern_meanings);
495        }
496
497        // Add descriptor based on stability
498        if stability > T::from_f32(0.8).unwrap() {
499            semantic_tags.push("Stable".to_string());
500        } else if stability < T::from_f32(0.4).unwrap() {
501            semantic_tags.push("Unstable".to_string());
502        }
503
504        // Create context
505        Ok(ActorContext {
506            harmonic_function,
507            metric_position: None, // Observer doesn't track metrics
508            semantic_tags,
509            stability,
510        })
511    }
512}
513
514impl<T> Default for Observer<T>
515where
516    T: Default + FromPrimitive,
517{
518    fn default() -> Self {
519        Observer::new()
520    }
521}
522
523impl<T> PartialEq<Observer<T>> for Observer<T> {
524    fn eq(&self, other: &Observer<T>) -> bool {
525        self.id() == other.id()
526    }
527}
528
529impl<T> PartialEq<Observer<T>> for &Observer<T> {
530    fn eq(&self, other: &Observer<T>) -> bool {
531        self.id() == other.id()
532    }
533}
534
535impl<T> PartialEq<Observer<T>> for &mut Observer<T> {
536    fn eq(&self, other: &Observer<T>) -> bool {
537        self.id() == other.id()
538    }
539}
540
541impl<'a, T> PartialEq<&'a Observer<T>> for Observer<T> {
542    fn eq(&self, other: &&'a Observer<T>) -> bool {
543        self.id() == other.id()
544    }
545}
546
547impl<'a, T> PartialEq<&'a mut Observer<T>> for Observer<T> {
548    fn eq(&self, other: &&'a mut Observer<T>) -> bool {
549        self.id() == other.id()
550    }
551}
552
553impl<T> Eq for Observer<T> {}
554
555impl<T> core::hash::Hash for Observer<T> {
556    fn hash<H: core::hash::Hasher>(&self, state: &mut H) {
557        self.id().hash(state);
558        // self.threshold().hash(state);
559        self.observations().hash(state);
560        self.is_initialized().hash(state);
561    }
562}