use serde::{Deserialize, Serialize};
use crate::linalg::jacobi_eigenvalues;
#[derive(Clone, Debug, Serialize, Deserialize)]
pub struct RhymeScheme {
pub pattern: Vec<usize>,
pub adjacency: Vec<Vec<f64>>,
}
impl RhymeScheme {
pub fn new(pattern: Vec<usize>) -> Self {
let n = pattern.len();
let mut adj = vec![vec![0.0; n]; n];
for i in 0..n {
for j in (i + 1)..n {
if pattern[i] == pattern[j] {
adj[i][j] = 1.0;
adj[j][i] = 1.0;
}
}
}
Self { pattern, adjacency: adj }
}
pub fn from_str(s: &str) -> Self {
let pattern: Vec<usize> = s.chars()
.filter(|c| c.is_ascii_alphabetic())
.map(|c| (c as u8 - b'A') as usize)
.collect();
Self::new(pattern)
}
pub fn line_count(&self) -> usize {
self.pattern.len()
}
pub fn rhyme_class_count(&self) -> usize {
let mut classes: Vec<usize> = self.pattern.clone();
classes.sort();
classes.dedup();
classes.len()
}
pub fn rhyme_pair_count(&self) -> usize {
let mut count = 0;
let n = self.pattern.len();
for i in 0..n {
for j in (i + 1)..n {
if self.pattern[i] == self.pattern[j] {
count += 1;
}
}
}
count
}
pub fn laplacian(&self) -> Vec<Vec<f64>> {
let n = self.line_count();
let mut lap = vec![vec![0.0; n]; n];
for i in 0..n {
let deg: f64 = self.adjacency[i].iter().sum();
lap[i][i] = deg;
for j in 0..n {
if i != j {
lap[i][j] = -self.adjacency[i][j];
}
}
}
lap
}
pub fn spectral_radius(&self) -> f64 {
let eigs = jacobi_eigenvalues(&self.adjacency, 500);
eigs.iter().map(|e| e.abs()).fold(0.0_f64, f64::max)
}
pub fn eigenvalues(&self) -> Vec<f64> {
jacobi_eigenvalues(&self.adjacency, 500)
}
pub fn fiedler_value(&self) -> f64 {
let lap = self.laplacian();
let eigs = jacobi_eigenvalues(&lap, 500);
eigs.get(1).copied().unwrap_or(0.0)
}
pub fn classify(&self) -> RhymeType {
let s: String = self.pattern.iter()
.map(|&c| (b'A' + c as u8) as char)
.collect();
if self.rhyme_pair_count() == 0 {
RhymeType::FreeVerse
} else if s == "ABAB" {
RhymeType::Alternating
} else if s == "AABB" {
RhymeType::Coupled
} else if s == "ABBA" {
RhymeType::Enclosed
} else {
RhymeType::Other(s)
}
}
pub fn spectral_distance(&self, other: &RhymeScheme) -> f64 {
let eigs_a = self.eigenvalues();
let eigs_b = other.eigenvalues();
let max_len = eigs_a.len().max(eigs_b.len());
let mut sum_sq = 0.0;
for i in 0..max_len {
let a = eigs_a.get(i).copied().unwrap_or(0.0);
let b = eigs_b.get(i).copied().unwrap_or(0.0);
sum_sq += (a - b) * (a - b);
}
sum_sq.sqrt()
}
}
#[derive(Clone, Debug, PartialEq, Serialize, Deserialize)]
pub enum RhymeType {
Alternating, Coupled, Enclosed, FreeVerse, Other(String),
}
pub fn classify_by_spectrum(scheme: &RhymeScheme) -> RhymeType {
scheme.classify()
}
pub fn common_schemes() -> Vec<RhymeScheme> {
vec![
RhymeScheme::from_str("ABAB"),
RhymeScheme::from_str("AABB"),
RhymeScheme::from_str("ABBA"),
RhymeScheme::from_str("ABCD"), ]
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_abab_construction() {
let scheme = RhymeScheme::from_str("ABAB");
assert_eq!(scheme.line_count(), 4);
assert_eq!(scheme.rhyme_class_count(), 2);
assert_eq!(scheme.rhyme_pair_count(), 2);
}
#[test]
fn test_aabb_construction() {
let scheme = RhymeScheme::from_str("AABB");
assert_eq!(scheme.rhyme_pair_count(), 2);
}
#[test]
fn test_abba_construction() {
let scheme = RhymeScheme::from_str("ABBA");
assert_eq!(scheme.rhyme_pair_count(), 2);
}
#[test]
fn test_free_verse_no_rhymes() {
let scheme = RhymeScheme::from_str("ABCD");
assert_eq!(scheme.rhyme_pair_count(), 0);
}
#[test]
fn test_spectral_radius_abab() {
let scheme = RhymeScheme::from_str("ABAB");
let r = scheme.spectral_radius();
assert!(r > 0.0, "ABAB spectral radius should be > 0");
}
#[test]
fn test_spectral_radii_distinct() {
let schemes = common_schemes();
let radii: Vec<f64> = schemes.iter().map(|s| s.spectral_radius()).collect();
let abab_r = radii[0];
let aabb_r = radii[1];
let _abba_r = radii[2];
let free_r = radii[3];
assert!(free_r < 0.01, "Free verse should have near-zero spectral radius, got {}", free_r);
assert!(abab_r > 0.5, "ABAB should have meaningful spectral radius");
assert!(aabb_r > 0.5, "AABB should have meaningful spectral radius");
}
#[test]
fn test_abab_vs_aabb_different_spectra() {
let abab = RhymeScheme::from_str("ABAB");
let aabb = RhymeScheme::from_str("AABB");
let fiedler_abab = abab.fiedler_value();
let fiedler_aabb = aabb.fiedler_value();
assert_ne!(abab.classify(), aabb.classify());
let free = RhymeScheme::from_str("ABCD");
let dist_abab_free = abab.spectral_distance(&free);
assert!(dist_abab_free > 0.01, "Rhyming vs free verse should differ spectrally");
}
#[test]
fn test_classify_abab() {
let scheme = RhymeScheme::from_str("ABAB");
assert_eq!(scheme.classify(), RhymeType::Alternating);
}
#[test]
fn test_classify_aabb() {
let scheme = RhymeScheme::from_str("AABB");
assert_eq!(scheme.classify(), RhymeType::Coupled);
}
#[test]
fn test_classify_abba() {
let scheme = RhymeScheme::from_str("ABBA");
assert_eq!(scheme.classify(), RhymeType::Enclosed);
}
#[test]
fn test_classify_free_verse() {
let scheme = RhymeScheme::from_str("ABCD");
assert_eq!(scheme.classify(), RhymeType::FreeVerse);
}
#[test]
fn test_rhyme_laplacian_rows_sum_zero() {
let scheme = RhymeScheme::from_str("ABAB");
let lap = scheme.laplacian();
for row in &lap {
let sum: f64 = row.iter().sum();
assert!(sum.abs() < 1e-9, "Row sum should be 0, got {}", sum);
}
}
#[test]
fn test_spectral_classification_by_spectrum() {
let abab = RhymeScheme::from_str("ABAB");
let result = classify_by_spectrum(&abab);
assert_eq!(result, RhymeType::Alternating);
}
#[test]
fn test_rhyme_scheme_serde() {
let scheme = RhymeScheme::from_str("ABBA");
let json = serde_json::to_string(&scheme).unwrap();
let back: RhymeScheme = serde_json::from_str(&json).unwrap();
assert_eq!(back.pattern, scheme.pattern);
}
}