Skip to main content

difi/
partitions.rs

1//! Partition creation and summary.
2//!
3//! Partitions divide observations into time windows for analysis.
4//! Each partition is defined by a start and end night (inclusive).
5
6use crate::error::{Error, Result};
7
8/// A partition defining a time window of observations.
9#[derive(Debug, Clone)]
10pub struct Partition {
11    pub id: u64,
12    pub start_night: i64,
13    pub end_night: i64,
14}
15
16/// Summary statistics for a partition.
17#[derive(Debug, Clone)]
18pub struct PartitionSummary {
19    pub id: u64,
20    pub start_night: i64,
21    pub end_night: i64,
22    pub observations: i64,
23    pub findable: Option<i64>,
24    pub found: Option<i64>,
25    pub completeness: Option<f64>,
26    pub pure_known: Option<i64>,
27    pub pure_unknown: Option<i64>,
28    pub contaminated: Option<i64>,
29    pub mixed: Option<i64>,
30}
31
32/// Create a single partition spanning all given nights.
33pub fn create_single(nights: &[i64]) -> Result<Partition> {
34    if nights.is_empty() {
35        return Err(Error::InvalidInput(
36            "Cannot create partition from empty nights".to_string(),
37        ));
38    }
39    let min_night = *nights.iter().min().unwrap();
40    let max_night = *nights.iter().max().unwrap();
41    Ok(Partition {
42        id: 0,
43        start_night: min_night,
44        end_night: max_night,
45    })
46}
47
48/// Create non-overlapping or sliding linking windows.
49///
50/// If `detection_window` is None, returns a single partition spanning all nights.
51/// If `sliding` is true, windows slide by one night with a ramp-up from `min_nights`.
52/// If `sliding` is false, windows are non-overlapping blocks of `detection_window` nights.
53pub fn create_linking_windows(
54    nights: &[i64],
55    detection_window: Option<i64>,
56    min_nights: Option<i64>,
57    sliding: bool,
58) -> Result<Vec<Partition>> {
59    if nights.is_empty() {
60        return Err(Error::InvalidInput(
61            "Cannot create partitions from empty nights".to_string(),
62        ));
63    }
64    let min_night = *nights.iter().min().unwrap();
65    let max_night = *nights.iter().max().unwrap();
66
67    let detection_window = match detection_window {
68        None => return Ok(vec![create_single(nights)?]),
69        Some(dw) => {
70            if dw >= (max_night - min_night + 1) {
71                max_night - min_night + 1
72            } else {
73                dw
74            }
75        }
76    };
77
78    let min_nights = min_nights.unwrap_or(detection_window);
79    if detection_window < min_nights {
80        return Err(Error::InvalidInput(
81            "Detection window must be >= min_nights".to_string(),
82        ));
83    }
84
85    let mut partitions = Vec::new();
86
87    if sliding {
88        let mut i: u64 = 0;
89        let mut start_night = min_night;
90        let mut end_night = start_night + min_nights - 1;
91        loop {
92            if end_night > max_night {
93                break;
94            }
95            partitions.push(Partition {
96                id: i,
97                start_night,
98                end_night,
99            });
100            i += 1;
101            end_night += 1;
102            if end_night - detection_window == start_night {
103                start_night += 1;
104            }
105        }
106    } else {
107        let mut i: u64 = 0;
108        let mut start = min_night;
109        while start <= max_night {
110            let end = (start + detection_window - 1).min(max_night);
111            partitions.push(Partition {
112                id: i,
113                start_night: start,
114                end_night: end,
115            });
116            i += 1;
117            start += detection_window;
118        }
119    }
120
121    Ok(partitions)
122}
123
124/// Create partition summaries by counting observations per partition.
125///
126/// Uses a pre-sorted night index for O(log n) lookups per partition.
127pub fn create_summaries(
128    obs_nights: &[i64],
129    partitions: &[Partition],
130    night_sorted_indices: &[usize],
131) -> Vec<PartitionSummary> {
132    partitions
133        .iter()
134        .map(|p| {
135            let lo = night_sorted_indices.partition_point(|&i| obs_nights[i] < p.start_night);
136            let hi = night_sorted_indices.partition_point(|&i| obs_nights[i] <= p.end_night);
137            let num_obs = (hi - lo) as i64;
138
139            PartitionSummary {
140                id: p.id,
141                start_night: p.start_night,
142                end_night: p.end_night,
143                observations: num_obs,
144                findable: None,
145                found: None,
146                completeness: None,
147                pure_known: None,
148                pure_unknown: None,
149                contaminated: None,
150                mixed: None,
151            }
152        })
153        .collect()
154}
155
156#[cfg(test)]
157mod tests {
158    use super::*;
159
160    #[test]
161    fn test_create_single() {
162        let nights = vec![5, 3, 7, 1, 9];
163        let p = create_single(&nights).unwrap();
164        assert_eq!(p.start_night, 1);
165        assert_eq!(p.end_night, 9);
166    }
167
168    #[test]
169    fn test_create_single_empty() {
170        assert!(create_single(&[]).is_err());
171    }
172
173    #[test]
174    fn test_create_linking_windows_non_overlapping() {
175        let nights: Vec<i64> = (0..10).collect();
176        let partitions = create_linking_windows(&nights, Some(3), None, false).unwrap();
177        assert_eq!(partitions.len(), 4); // [0,2], [3,5], [6,8], [9,9]
178        assert_eq!(partitions[0].start_night, 0);
179        assert_eq!(partitions[0].end_night, 2);
180        assert_eq!(partitions[3].start_night, 9);
181        assert_eq!(partitions[3].end_night, 9);
182    }
183
184    #[test]
185    fn test_create_linking_windows_none() {
186        let nights = vec![1, 5, 10];
187        let partitions = create_linking_windows(&nights, None, None, false).unwrap();
188        assert_eq!(partitions.len(), 1);
189        assert_eq!(partitions[0].start_night, 1);
190        assert_eq!(partitions[0].end_night, 10);
191    }
192
193    #[test]
194    fn test_create_linking_windows_sliding() {
195        let nights: Vec<i64> = (0..10).collect();
196        let partitions = create_linking_windows(&nights, Some(5), Some(3), true).unwrap();
197        // Ramp-up: windows start at length 3, grow to 5, then slide
198        assert!(!partitions.is_empty());
199        // First window: [0, 2] (length 3 = min_nights)
200        assert_eq!(partitions[0].start_night, 0);
201        assert_eq!(partitions[0].end_night, 2);
202        // Windows should grow until detection_window is reached
203        assert!(partitions.last().unwrap().end_night <= 9);
204    }
205
206    #[test]
207    fn test_create_linking_windows_empty() {
208        assert!(create_linking_windows(&[], Some(3), None, false).is_err());
209    }
210
211    #[test]
212    fn test_create_linking_windows_window_too_small() {
213        let nights: Vec<i64> = (0..10).collect();
214        assert!(create_linking_windows(&nights, Some(2), Some(5), false).is_err());
215    }
216
217    #[test]
218    fn test_create_linking_windows_larger_than_range() {
219        let nights: Vec<i64> = (0..5).collect();
220        let partitions = create_linking_windows(&nights, Some(100), None, false).unwrap();
221        assert_eq!(partitions.len(), 1);
222        assert_eq!(partitions[0].start_night, 0);
223        assert_eq!(partitions[0].end_night, 4);
224    }
225
226    #[test]
227    fn test_create_summaries() {
228        let nights = vec![1, 1, 2, 2, 2, 3];
229        let partitions = vec![
230            Partition {
231                id: 0,
232                start_night: 1,
233                end_night: 2,
234            },
235            Partition {
236                id: 1,
237                start_night: 3,
238                end_night: 3,
239            },
240        ];
241        let sorted = crate::types::compute_night_sorted_indices(&nights);
242        let summaries = create_summaries(&nights, &partitions, &sorted);
243        assert_eq!(summaries.len(), 2);
244        assert_eq!(summaries[0].observations, 5); // nights 1,1,2,2,2
245        assert_eq!(summaries[1].observations, 1); // night 3
246    }
247}