Skip to main content

plato_midi_bridge/
style.rs

1/// 109-dimensional musical style vector.
2use crate::EisensteinLattice;
3
4#[derive(Debug, Clone)]
5pub struct StyleVector {
6    pub dims: [f64; 109],
7}
8
9impl StyleVector {
10    pub fn new(data: &[f64]) -> Self {
11        let mut dims = [0.0; 109];
12        let n = data.len().min(109);
13        dims[..n].copy_from_slice(&data[..n]);
14        StyleVector { dims }
15    }
16
17    /// Cosine similarity
18    pub fn cosine_similarity(&self, other: &StyleVector) -> f64 {
19        let dot: f64 = self.dims.iter().zip(other.dims.iter()).map(|(a, b)| a * b).sum();
20        let norm1: f64 = self.dims.iter().map(|a| a * a).sum::<f64>().sqrt();
21        let norm2: f64 = other.dims.iter().map(|a| a * a).sum::<f64>().sqrt();
22        if norm1 == 0.0 || norm2 == 0.0 { return 0.0; }
23        (dot / (norm1 * norm2)).clamp(0.0, 1.0)
24    }
25
26    /// Euclidean distance
27    pub fn euclidean_distance(&self, other: &StyleVector) -> f64 {
28        self.dims.iter().zip(other.dims.iter())
29            .map(|(a, b)| (a - b) * (a - b))
30            .sum::<f64>()
31            .sqrt()
32    }
33
34    /// Reduce to 5-dim style primitives using positions 0-108:
35    /// pitch_complexity: avg of dims 0-47 (first 48 pitch bins)
36    /// timing_expressiveness: dims 48-55 (timing-related features)
37    /// velocity_energy: dims 56-63 (velocity-related)
38    /// articulation_clarity: dims 64-71 (articulation)
39    /// timbral_breadth: dims 72-79 (timbre/register)
40    pub fn to_5d(&self) -> [f64; 5] {
41        let pitch = self.dims[..48].iter().sum::<f64>() / 48.0 * 12.0;
42        let timing = self.dims[48..56].iter().sum::<f64>() / 8.0 * 100.0;
43        let velocity = self.dims[56..64].iter().sum::<f64>() / 8.0;
44        let articulation = 1.0 - (self.dims[64..72].iter().sum::<f64>() / 8.0);
45        let timbre = self.dims[72..80].iter().sum::<f64>() / 8.0;
46        [pitch, timing, velocity, articulation, timbre]
47    }
48
49    /// Reduce to 12-dim Eisenstein coupling vector
50    pub fn to_12d(&self) -> [f64; 12] {
51        let mut coupling = [0.0; 12];
52        for i in 0..12 {
53            coupling[i] = self.dims[i.min(108)];
54        }
55        coupling
56    }
57}
58
59#[cfg(test)]
60mod tests {
61    use super::*;
62    use crate::EisensteinLattice;
63
64    #[test]
65    fn test_style_vector_creation() {
66        let data = vec![0.5; 109];
67        let v = StyleVector::new(&data);
68        assert!((v.dims[0] - 0.5).abs() < 1e-10);
69    }
70
71    #[test]
72    fn test_cosine_similarity_identical() {
73        let data: Vec<f64> = (0..109).map(|i| (i as f64) / 109.0).collect();
74        let v1 = StyleVector::new(&data);
75        let v2 = StyleVector::new(&data);
76        assert!((v1.cosine_similarity(&v2) - 1.0).abs() < 1e-10);
77    }
78
79    #[test]
80    fn test_5d_reduction() {
81        let data: Vec<f64> = (0..109).map(|i| (i as f64) / 50.0).collect();
82        let v = StyleVector::new(&data);
83        let d5 = v.to_5d();
84        assert_eq!(d5.len(), 5);
85        for val in &d5 {
86            assert!(val.is_finite());
87        }
88    }
89
90    #[test]
91    fn test_eisenstein_chamber_from_style() {
92        let data: Vec<f64> = (0..109).map(|i| if i < 12 { (i+1) as f64 / 12.0 } else { 0.0 }).collect();
93        let v = StyleVector::new(&data);
94        let coupling = v.to_12d();
95        let chamber = EisensteinLattice::chamber(&coupling);
96        assert!(chamber < 12);
97    }
98
99    #[test]
100    fn test_orthogonal_vectors_distant() {
101        let v1 = StyleVector::new(&[1.0; 109]);
102        let v2 = StyleVector::new(&[0.0; 109]);
103        let sim = v1.cosine_similarity(&v2);
104        assert!((sim - 0.0).abs() < 1e-10);
105    }
106}