nale/align/bounded/structs/
bound.rs

1use crate::util::PrintMe;
2use std::fmt::{Display, Formatter};
3use std::iter::{Rev, Zip};
4use std::ops::RangeInclusive;
5
6#[derive(Clone)]
7pub struct CloudBound {
8    pub left_target_idx: usize,
9    pub left_profile_idx: usize,
10    pub right_target_idx: usize,
11    pub right_profile_idx: usize,
12}
13
14impl Default for CloudBound {
15    fn default() -> Self {
16        // set the default to a simple invalid bound
17        // (the left bound is on the right, and vice versa)
18        CloudBound {
19            left_target_idx: 0,
20            left_profile_idx: 1,
21            right_target_idx: 1,
22            right_profile_idx: 0,
23        }
24    }
25}
26
27impl Display for CloudBound {
28    fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
29        write!(
30            f,
31            "{},{} : {},{}",
32            self.left_target_idx,
33            self.left_profile_idx,
34            self.right_target_idx,
35            self.right_profile_idx
36        )
37    }
38}
39
40impl PrintMe for CloudBound {
41    fn print(&self) {
42        println!("{}", self);
43    }
44}
45
46impl CloudBound {
47    pub fn was_pruned(&self) -> bool {
48        self.left_target_idx < self.right_target_idx
49    }
50
51    pub fn anti_diagonal_idx(&self) -> usize {
52        self.left_target_idx + self.left_profile_idx
53    }
54
55    pub fn anti_diagonal_cell_zip(&self) -> Zip<Rev<RangeInclusive<usize>>, RangeInclusive<usize>> {
56        let target_range = (self.right_target_idx..=self.left_target_idx).rev();
57        let profile_range = self.left_profile_idx..=self.right_profile_idx;
58        target_range.zip(profile_range)
59    }
60}
61
62#[derive(Default, Clone)]
63pub struct CloudBoundGroup {
64    pub bounds: Vec<CloudBound>,
65    pub target_length: usize,
66    pub profile_length: usize,
67    pub size: usize,
68    pub min_anti_diagonal_idx: usize,
69    pub max_anti_diagonal_idx: usize,
70}
71
72impl CloudBoundGroup {
73    pub fn new(target_length: usize, profile_length: usize) -> Self {
74        let size = target_length + profile_length + 1;
75        Self {
76            bounds: vec![CloudBound::default(); size],
77            target_length,
78            profile_length,
79            size,
80            min_anti_diagonal_idx: size,
81            max_anti_diagonal_idx: 0,
82        }
83    }
84
85    pub fn resize(&mut self, new_size: usize) {
86        self.bounds.resize(new_size, CloudBound::default());
87        self.size = new_size;
88    }
89
90    pub fn reuse(&mut self, target_length: usize, profile_length: usize) {
91        let new_size = target_length + profile_length + 1;
92        if new_size > self.size {
93            self.resize(new_size);
94        }
95
96        self.min_anti_diagonal_idx = new_size;
97        self.max_anti_diagonal_idx = 0;
98
99        self.target_length = target_length;
100        self.profile_length = profile_length;
101
102        // TODO: think about this
103        for bound in self.bounds.iter_mut() {
104            bound.left_target_idx = 0;
105            bound.left_profile_idx = 1;
106            bound.right_target_idx = 1;
107            bound.right_profile_idx = 0;
108        }
109    }
110
111    pub fn set(
112        &mut self,
113        // TODO: remove this
114        anti_diagonal_idx: usize,
115        left_target_idx: usize,
116        left_profile_idx: usize,
117        right_target_idx: usize,
118        right_profile_idx: usize,
119    ) {
120        // TODO: I don't think there's much point to setting these here
121        self.min_anti_diagonal_idx = self.min_anti_diagonal_idx.min(anti_diagonal_idx);
122        self.max_anti_diagonal_idx = self.max_anti_diagonal_idx.max(anti_diagonal_idx);
123
124        debug_assert_eq!(
125            left_target_idx + left_profile_idx,
126            right_target_idx + right_profile_idx
127        );
128
129        let bound = &mut self.bounds[anti_diagonal_idx];
130        bound.left_target_idx = left_target_idx;
131        bound.left_profile_idx = left_profile_idx;
132        bound.right_target_idx = right_target_idx;
133        bound.right_profile_idx = right_profile_idx;
134    }
135
136    pub fn get(&self, idx: usize) -> &CloudBound {
137        &self.bounds[idx]
138    }
139
140    pub fn get_mut(&mut self, idx: usize) -> &mut CloudBound {
141        &mut self.bounds[idx]
142    }
143
144    pub fn get_first(&self) -> &CloudBound {
145        self.get(self.min_anti_diagonal_idx)
146    }
147
148    pub fn get_last(&self) -> &CloudBound {
149        self.get(self.max_anti_diagonal_idx)
150    }
151
152    pub fn valid(&self) -> bool {
153        for idx in self.min_anti_diagonal_idx..=self.max_anti_diagonal_idx {
154            let bound = &self.bounds[idx];
155            if bound.was_pruned() {
156                return false;
157            }
158        }
159        true
160    }
161
162    /// Get the total number of cells that exist within the cloud boundaries.
163    pub fn cloud_size(&self) -> usize {
164        let mut cloud_size = 0usize;
165        for bound in self.bounds[self.min_anti_diagonal_idx..=self.max_anti_diagonal_idx].iter() {
166            cloud_size += bound.left_target_idx - bound.right_target_idx;
167        }
168        cloud_size
169    }
170
171    /// Get the number of anti-diagonals defined in the cloud.
172    pub fn num_anti_diagonals(&self) -> usize {
173        self.max_anti_diagonal_idx - self.min_anti_diagonal_idx + 1
174    }
175
176    /// This removes all of the protruding regions in the cloud that are unreachable from a traceback
177    /// that traverses the entire cloud.
178    ///
179    pub fn trim_wings(&mut self) {
180        for anti_diagonal_idx in self.min_anti_diagonal_idx + 1..=self.max_anti_diagonal_idx {
181            let previous_bound = self.get(anti_diagonal_idx - 1);
182            let current_bound = self.get(anti_diagonal_idx);
183
184            // right wings identifiable from a forward pass look like:
185            //            c
186            //         c  c
187            //   c  c  c  c
188            //   c  c  c  c
189            //   c  c  c  c  c  c
190            //   c  c  c  c  c  c
191            //
192            let right_distance = previous_bound
193                .right_target_idx
194                .saturating_sub(current_bound.right_target_idx);
195
196            // left wings identifiable from a forward pass look like:
197            //   c  c  c  c  c  c
198            //   c  c  c  c  c  c
199            //   c  c  c  c  c  c
200            //         c  c  c  c
201            //      c  c  c  c  c
202            //   c  c  c  c  c  c
203            //
204            let left_distance =
205                (previous_bound.left_profile_idx).saturating_sub(current_bound.left_profile_idx);
206
207            self.set(
208                anti_diagonal_idx,
209                current_bound.left_target_idx - left_distance,
210                current_bound.left_profile_idx + left_distance,
211                current_bound.right_target_idx + right_distance,
212                current_bound.right_profile_idx - right_distance,
213            )
214        }
215
216        for anti_diagonal_idx in (self.min_anti_diagonal_idx..self.max_anti_diagonal_idx).rev() {
217            let previous_bound = self.get(anti_diagonal_idx + 1);
218            let current_bound = self.get(anti_diagonal_idx);
219
220            // right wings identifiable from a backward pass look like:
221            //   c  c  c  c  c  c
222            //   c  c  c  c  c
223            //   c  c  c  c
224            //   c  c  c  c  c  c
225            //   c  c  c  c  c  c
226            //   c  c  c  c  c  c
227            //
228            let right_distance = current_bound
229                .right_profile_idx
230                .saturating_sub(previous_bound.right_profile_idx);
231
232            // left wings identifiable from a backward pass look like:
233            //   c  c  c  c  c  c
234            //   c  c  c  c  c  c
235            //         c  c  c  c
236            //         c  c  c  c
237            //         c  c
238            //         c
239            //
240            let left_distance =
241                (current_bound.left_target_idx).saturating_sub(previous_bound.left_target_idx);
242
243            self.set(
244                anti_diagonal_idx,
245                current_bound.left_target_idx - left_distance,
246                current_bound.left_profile_idx + left_distance,
247                current_bound.right_target_idx + right_distance,
248                current_bound.right_profile_idx - right_distance,
249            )
250        }
251    }
252
253    /// Add a square block of bounds to the group with side length of `size`.
254    ///
255    /// This is currently intended for debugging.
256    pub fn bound_block(&mut self, target_start: usize, profile_start: usize, size: usize) {
257        let idx = target_start + profile_start;
258        for i in 0..size {
259            self.set(
260                idx + i,
261                target_start + i,
262                profile_start,
263                target_start,
264                profile_start + i,
265            );
266        }
267        let idx = target_start + profile_start + size;
268        for i in 0..=size {
269            self.set(
270                idx + i,
271                target_start + size,
272                profile_start + i,
273                target_start + i,
274                profile_start + size,
275            );
276        }
277    }
278
279    pub fn bounds(&self) -> &[CloudBound] {
280        &self.bounds[self.min_anti_diagonal_idx..=self.max_anti_diagonal_idx]
281    }
282
283    pub fn join_bounds(forward_bounds: &mut CloudBoundGroup, backward_bounds: &CloudBoundGroup) {
284        // first check if the forward & backward bounds happened not to intersect
285        if forward_bounds.max_anti_diagonal_idx < backward_bounds.min_anti_diagonal_idx {
286            // TODO: **this is going to need to be rewritten**
287            //       I think a better idea is going to be to interpolate two lines:
288            //         1. one between the highest point on the forward & backward bounds
289            //         2. one between the lowest point on the forward & backward bounds
290            //
291            // if they do not intersect, we need to interpolate
292            //
293            // we are going to do that by:
294            //   1. selecting a "central point" on both the last
295            //      forward bound and the first backward bound
296            //   2. solving for the linear equation that defines
297            //      the line between those two points
298            //   3. computing the average anti-diagonal length
299            //      across the anti-diagonals in both bound groups
300            //   4. fill in between the bound groups with anti-diagonals
301            //      of that average length that are roughly centered
302            //      on the line
303            //
304            let last_forward = forward_bounds.get_last();
305            let forward_x = ((last_forward.right_profile_idx as f32
306                + last_forward.left_profile_idx as f32)
307                / 2.0)
308                .floor();
309            let forward_y = last_forward.anti_diagonal_idx() as f32 - forward_x;
310
311            let first_backward = backward_bounds.get_first();
312            let backward_x = ((first_backward.right_profile_idx as f32
313                + first_backward.left_profile_idx as f32)
314                / 2.0)
315                .ceil();
316            let backward_y = first_backward.anti_diagonal_idx() as f32 - backward_x;
317
318            let slope = (backward_y - forward_y) / (backward_x - forward_x);
319            let intercept = forward_y - slope * forward_x;
320            let line_equation = |x: f32| slope * x + intercept;
321
322            let cloud_size_sum = forward_bounds.cloud_size() + backward_bounds.cloud_size();
323            let num_anti_diagonals =
324                forward_bounds.num_anti_diagonals() + backward_bounds.num_anti_diagonals();
325            // integer division is truncated, which is probably what we want
326            let avg_anti_diagonal_length = cloud_size_sum / num_anti_diagonals;
327
328            // I have elected to make this a closure since it's convenient
329            // to have it capture the kine_equation closure, and I don't think
330            // we'll ever call this from outside of this function
331            let bound_fn = |center_profile_idx: usize| {
332                let center_target_idx = line_equation(center_profile_idx as f32).round() as usize;
333                let anti_diagonal_idx = center_profile_idx + center_target_idx;
334
335                // note: if the avg_anti_diagonal_length is even, this
336                //       will cause us to fill with +1 of that length
337                let left_profile_idx = center_profile_idx - avg_anti_diagonal_length / 2;
338                let right_profile_idx = center_profile_idx + avg_anti_diagonal_length / 2;
339                let left_target_idx = anti_diagonal_idx - left_profile_idx;
340                let right_target_idx = anti_diagonal_idx - right_profile_idx;
341                CloudBound {
342                    left_target_idx,
343                    left_profile_idx,
344                    right_target_idx,
345                    right_profile_idx,
346                }
347            };
348
349            let first_bound = bound_fn(forward_x as usize);
350            forward_bounds.set(
351                first_bound.anti_diagonal_idx() + 1,
352                first_bound.left_target_idx,
353                first_bound.left_profile_idx + 1,
354                first_bound.right_target_idx + 1,
355                first_bound.right_profile_idx,
356            );
357
358            // fill in the missing bounds using the line equation:
359            //   - iterate across the profile indices that span the missing bounds
360            //   - we'll think of the profile index as the x value in the line equation
361            //   - using the profile index as the input to the line equation, we can get
362            //     a corresponding target index (y value)
363            //   - those coordinates (profile index, target index) will be the center cell
364            //     along the current anti-diagonal that we are filling
365            //   - then we can just extend that bound out from the center to produce an
366            //     anti-diagonal equal to the average anti-diagonal length
367            let profile_start = forward_x as usize + 1;
368            let profile_end = backward_x as usize - 1;
369            for center_profile_idx in profile_start..=profile_end {
370                let bound = bound_fn(center_profile_idx);
371
372                forward_bounds.set(
373                    bound.anti_diagonal_idx(),
374                    bound.left_target_idx,
375                    bound.left_profile_idx,
376                    bound.right_target_idx,
377                    bound.right_profile_idx,
378                );
379
380                forward_bounds.set(
381                    bound.anti_diagonal_idx() + 1,
382                    bound.left_target_idx,
383                    bound.left_profile_idx + 1,
384                    bound.right_target_idx + 1,
385                    bound.right_profile_idx,
386                );
387            }
388
389            // finally just tack on the backward bounds
390            for anti_diagonal_idx in
391                backward_bounds.min_anti_diagonal_idx..=backward_bounds.max_anti_diagonal_idx
392            {
393                let bound = backward_bounds.get(anti_diagonal_idx);
394                forward_bounds.set(
395                    anti_diagonal_idx,
396                    bound.left_target_idx,
397                    bound.left_profile_idx,
398                    bound.right_target_idx,
399                    bound.right_profile_idx,
400                )
401            }
402        } else {
403            // if they do intersect, we can join them by taking
404            // the longest anti-diagonal at each index
405            let start_idx = forward_bounds
406                .min_anti_diagonal_idx
407                .min(backward_bounds.min_anti_diagonal_idx);
408
409            let end_idx = forward_bounds
410                .max_anti_diagonal_idx
411                .max(backward_bounds.max_anti_diagonal_idx);
412
413            forward_bounds.min_anti_diagonal_idx = start_idx;
414            forward_bounds.max_anti_diagonal_idx = end_idx;
415            let forward_slice = &mut forward_bounds.bounds[start_idx..=end_idx];
416            let backward_slice = &backward_bounds.bounds[start_idx..=end_idx];
417
418            for (forward_bound, backward_bound) in forward_slice.iter_mut().zip(backward_slice) {
419                if forward_bound.was_pruned() {
420                    // if there's no valid forward bound, just take the backward bound
421                    forward_bound.left_target_idx = backward_bound.left_target_idx;
422                    forward_bound.left_profile_idx = backward_bound.left_profile_idx;
423                    forward_bound.right_target_idx = backward_bound.right_target_idx;
424                    forward_bound.right_profile_idx = backward_bound.right_profile_idx;
425                } else if backward_bound.was_pruned() {
426                    // if there's no valid backward bound, we can do nothing since we
427                    // are consuming the forward bounds
428                    continue;
429                }
430
431                debug_assert_eq!(
432                    forward_bound.anti_diagonal_idx(),
433                    backward_bound.anti_diagonal_idx()
434                );
435
436                // otherwise we have two valid bounds and we can compare them
437                forward_bound.left_target_idx = forward_bound
438                    .left_target_idx
439                    .max(backward_bound.left_target_idx);
440
441                forward_bound.left_profile_idx = forward_bound
442                    .left_profile_idx
443                    .min(backward_bound.left_profile_idx);
444
445                forward_bound.right_target_idx = forward_bound
446                    .right_target_idx
447                    .min(backward_bound.right_target_idx);
448
449                forward_bound.right_profile_idx = forward_bound
450                    .right_profile_idx
451                    .max(backward_bound.right_profile_idx);
452            }
453        }
454    }
455}