music21_rs/alpha/analysis/
aligner.rs1use super::hasher::{HashReference, Hasher, NoteHash};
5use crate::{
6 defaults::FloatType,
7 error::{Error, Result},
8 stream::Stream,
9};
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
14#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
15pub enum ChangeOp {
16 Insertion,
18 Deletion,
20 Substitution,
22 NoChange,
24}
25
26impl ChangeOp {
27 pub fn color(self) -> Option<&'static str> {
29 match self {
30 Self::Insertion => Some("green"),
31 Self::Deletion => Some("red"),
32 Self::Substitution => Some("purple"),
33 Self::NoChange => None,
34 }
35 }
36
37 fn from_index(index: usize) -> Self {
38 match index {
39 0 => Self::Insertion,
40 1 => Self::Deletion,
41 2 => Self::Substitution,
42 _ => Self::NoChange,
43 }
44 }
45}
46
47#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50#[cfg_attr(feature = "serde", derive(serde::Serialize, serde::Deserialize))]
51pub struct Change {
52 pub target: Option<HashReference>,
54 pub source: Option<HashReference>,
56 pub op: ChangeOp,
58}
59
60#[derive(Clone, Debug)]
66pub struct StreamAligner {
67 pub hasher: Hasher,
70 pub hashed_target: Vec<NoteHash>,
72 pub hashed_source: Vec<NoteHash>,
74 pub distance_matrix: Vec<Vec<i64>>,
77 pub changes: Vec<Change>,
79 pub similarity_score: FloatType,
81}
82
83impl Default for StreamAligner {
84 fn default() -> Self {
85 Self::new()
86 }
87}
88
89impl StreamAligner {
90 pub fn new() -> Self {
93 Self::with_hasher(Hasher {
94 hash_offset: false,
95 include_reference: true,
96 ..Hasher::default()
97 })
98 }
99
100 pub fn with_hasher(hasher: Hasher) -> Self {
102 Self {
103 hasher,
104 hashed_target: Vec::new(),
105 hashed_source: Vec::new(),
106 distance_matrix: Vec::new(),
107 changes: Vec::new(),
108 similarity_score: 0.0,
109 }
110 }
111
112 pub fn align(&mut self, target: &Stream, source: &Stream) -> Result<()> {
118 let target = self.hasher.hash_stream(target)?;
119 let source = self.hasher.hash_stream(source)?;
120 self.align_hashed(target, source)
121 }
122
123 pub fn align_hashed(&mut self, target: Vec<NoteHash>, source: Vec<NoteHash>) -> Result<()> {
130 if target.is_empty() {
131 return Err(Error::Analysis(
132 "Cannot perform alignment with empty target stream.".to_string(),
133 ));
134 }
135 if source.is_empty() {
136 return Err(Error::Analysis(
137 "Cannot perform alignment with empty source stream.".to_string(),
138 ));
139 }
140 self.hashed_target = target;
141 self.hashed_source = source;
142 self.populate_distance_matrix();
143 self.calculate_changes()
144 }
145
146 fn populate_distance_matrix(&mut self) {
147 let (n, m) = (self.hashed_target.len(), self.hashed_source.len());
148 let cost = self.hashed_source[0].keys.len() as i64;
151 let mut matrix = vec![vec![0_i64; m + 1]; n + 1];
152 for i in 1..=n {
153 matrix[i][0] = matrix[i - 1][0] + cost;
154 }
155 for j in 1..=m {
156 matrix[0][j] = matrix[0][j - 1] + cost;
157 }
158 for i in 1..=n {
159 for j in 1..=m {
160 let substitution =
161 substitution_cost(&self.hashed_target[i - 1], &self.hashed_source[j - 1]);
162 matrix[i][j] = (matrix[i - 1][j] + cost)
163 .min(matrix[i][j - 1] + cost)
164 .min(matrix[i - 1][j - 1] + substitution);
165 }
166 }
167 self.distance_matrix = matrix;
168 }
169
170 fn op_at(&self, i: usize, j: usize) -> Result<ChangeOp> {
174 let matrix = &self.distance_matrix;
175 let up = (i >= 1).then(|| matrix[i - 1][j]);
176 let left = (j >= 1).then(|| matrix[i][j - 1]);
177 let diagonal = (i >= 1 && j >= 1).then(|| matrix[i - 1][j - 1]);
178 let Some(up) = up else {
179 if left.is_none() {
180 return Err(Error::Analysis(
181 "No movement possible from the origin".to_string(),
182 ));
183 }
184 return Ok(ChangeOp::Deletion);
185 };
186 let Some(left) = left else {
187 return Ok(ChangeOp::Insertion);
188 };
189 let diagonal = diagonal.unwrap_or(i64::MAX);
190 let moves = [up, left, diagonal];
191 let (index, cheapest) =
192 moves
193 .iter()
194 .enumerate()
195 .fold((0, i64::MAX), |best, (index, cost)| {
196 if *cost < best.1 { (index, *cost) } else { best }
197 });
198 if matrix[i][j] == cheapest {
199 Ok(ChangeOp::NoChange)
200 } else {
201 Ok(ChangeOp::from_index(index))
202 }
203 }
204
205 fn calculate_changes(&mut self) -> Result<()> {
206 let (mut i, mut j) = (self.hashed_target.len(), self.hashed_source.len());
207 let (n, m) = (i, j);
208 let before = |index: usize, count: usize| if index == 0 { count - 1 } else { index - 1 };
211 let mut changes = Vec::new();
212 while i != 0 || j != 0 {
213 let op = self.op_at(i, j)?;
214 changes.push(Change {
215 target: self.hashed_target[before(i, n)].reference,
216 source: self.hashed_source[before(j, m)].reference,
217 op,
218 });
219 match op {
220 ChangeOp::Insertion => i -= 1,
221 ChangeOp::Deletion => j -= 1,
222 ChangeOp::Substitution | ChangeOp::NoChange => {
223 i -= 1;
224 j -= 1;
225 }
226 }
227 }
228 changes.reverse();
229 let unchanged = changes
230 .iter()
231 .filter(|change| change.op == ChangeOp::NoChange)
232 .count();
233 self.similarity_score = unchanged as FloatType / changes.len() as FloatType;
234 self.changes = changes;
235 Ok(())
236 }
237
238 pub fn changes_count(&self, op: ChangeOp) -> usize {
240 self.changes.iter().filter(|change| change.op == op).count()
241 }
242}
243
244fn substitution_cost(target: &NoteHash, source: &NoteHash) -> i64 {
247 let same = target
248 .values
249 .iter()
250 .zip(&source.values)
251 .filter(|(a, b)| a == b)
252 .count();
253 if same == target.values.len() && target.values.len() == source.values.len() {
254 return 0;
255 }
256 (target.keys.len() - same) as i64
257}
258
259#[cfg(test)]
260mod tests {
261 use super::*;
262 use crate::note::Note;
263
264 fn line(names: &[&str]) -> Result<Stream> {
265 let mut stream = Stream::new();
266 for name in names {
267 stream.push(Note::from_name(name)?);
268 }
269 Ok(stream)
270 }
271
272 #[test]
273 fn streams_are_aligned_as_music21_aligns_them() -> Result<()> {
274 let mut aligner = StreamAligner::new();
276 aligner.align(&line(&["C4", "D#4", "C4"])?, &line(&["C4", "D-4", "C4"])?)?;
277 assert!((aligner.similarity_score - 2.0 / 3.0).abs() < 1e-12);
278 let mut aligner = StreamAligner::new();
279 aligner.align(
280 &line(&["C4", "D4", "E4", "F4"])?,
281 &line(&["C4", "D4", "E4"])?,
282 )?;
283 assert!((aligner.similarity_score - 0.75).abs() < 1e-12);
284 Ok(())
285 }
286}