Skip to main content

evlib/ev_formats/
aer_reader.rs

1// Address Event Representation (AER) format reader
2// Implements comprehensive AER format parsing for neuromorphic vision systems
3// Implements comprehensive AER format parsing for neuromorphic vision systems
4//
5// AER Format Specification:
6// - 18-bit structure per event: 1 bit polarity + 9 bits x + 9 bits y
7// - No explicit timestamps (real-time processing format)
8// - Polarity: 0 = CD OFF (negative), 1 = CD ON (positive)
9// - Coordinates: 9-bit values (0-511 range)
10// - Common in neuromorphic sensors like GenX320
11//
12// References:
13// - https://docs.prophesee.ai/stable/data/encoding_formats/aer.html
14// - GenX320 sensor documentation
15// - jAER project specifications
16use crate::ev_formats::dataframe_builder::EventDataFrameBuilder;
17use crate::ev_formats::{EventFormat, LoadConfig};
18use polars::prelude::*;
19use std::fs::File;
20use std::io::Read;
21use std::path::Path;
22/// Configuration for AER reader
23#[derive(Debug, Clone)]
24pub struct AerConfig {
25    /// Endianness of the AER data (true = big endian, false = little endian)
26    pub big_endian: bool,
27    /// Validate coordinate bounds (0-511 for 9-bit coordinates)
28    pub validate_coordinates: bool,
29    /// Maximum allowed x coordinate (default 511 for 9-bit)
30    pub max_x: u16,
31    /// Maximum allowed y coordinate (default 511 for 9-bit)
32    pub max_y: u16,
33    /// Skip invalid events instead of failing
34    pub skip_invalid_events: bool,
35    /// Generate timestamps based on event order
36    pub generate_timestamps: bool,
37    /// Timestamp generation mode
38    pub timestamp_mode: TimestampMode,
39    /// Starting timestamp for generated timestamps
40    pub start_timestamp: f64,
41    /// Time increment between events (seconds)
42    pub time_increment: f64,
43    /// Maximum number of events to read (None = all)
44    pub max_events: Option<usize>,
45    /// Bytes per event (2 or 4, depending on storage format)
46    pub bytes_per_event: usize,
47}
48/// Timestamp generation modes for AER data
49#[derive(Debug, Clone, PartialEq)]
50pub enum TimestampMode {
51    /// Sequential timestamps with fixed increment
52    Sequential,
53    /// Uniform distribution over time window
54    Uniform,
55    /// Exponential distribution (for spike-like patterns)
56    Exponential,
57    /// User-provided timestamps
58    Custom(Vec<f64>),
59}
60/// AER-specific errors
61#[derive(Debug)]
62pub enum AerError {
63    Io(std::io::Error),
64    InvalidFileSize(u64, usize),
65    InvalidCoordinate(u16, u16, u16, u16),
66    InvalidEventData(usize, Vec<u8>),
67    InsufficientData(usize, usize),
68    InvalidBytesPerEvent(usize),
69    EmptyFile,
70    ValidationFailed(String),
71}
72impl std::fmt::Display for AerError {
73    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
74        match self {
75            AerError::Io(e) => write!(f, "I/O error: {e}"),
76            AerError::InvalidFileSize(size, expected) => write!(
77                f,
78                "Invalid file size: {size} bytes, expected multiple of {expected}"
79            ),
80            AerError::InvalidCoordinate(x, y, max_x, max_y) => write!(
81                f,
82                "Invalid coordinate: x={x}, y={y}, max_x={max_x}, max_y={max_y}"
83            ),
84            AerError::InvalidEventData(offset, data) => {
85                write!(f, "Invalid event data at byte {offset}: {data:02X?}")
86            }
87            AerError::InsufficientData(expected, actual) => write!(
88                f,
89                "Insufficient data: expected {expected} bytes, got {actual}"
90            ),
91            AerError::InvalidBytesPerEvent(bytes) => {
92                write!(f, "Invalid bytes per event: {bytes}, expected 2 or 4")
93            }
94            AerError::EmptyFile => write!(f, "File is empty"),
95            AerError::ValidationFailed(msg) => write!(f, "Event validation failed: {msg}"),
96        }
97    }
98}
99impl std::error::Error for AerError {
100    fn source(&self) -> Option<&(dyn std::error::Error + 'static)> {
101        match self {
102            AerError::Io(e) => Some(e),
103            _ => None,
104        }
105    }
106}
107impl From<std::io::Error> for AerError {
108    fn from(error: std::io::Error) -> Self {
109        AerError::Io(error)
110    }
111}
112/// Result type for AER operations
113pub type AerResult<T> = Result<T, AerError>;
114/// Metadata about AER file
115#[derive(Debug, Clone)]
116pub struct AerMetadata {
117    /// File size in bytes
118    pub file_size: u64,
119    /// Number of events in file
120    pub event_count: usize,
121    /// Bytes per event (2 or 4)
122    pub bytes_per_event: usize,
123    /// Detected endianness
124    pub endianness: String,
125    /// Coordinate bounds found in data
126    pub coordinate_bounds: Option<(u16, u16, u16, u16)>, // min_x, min_y, max_x, max_y
127    /// Timestamp range (if generated)
128    pub timestamp_range: Option<(f64, f64)>,
129    /// Polarity distribution (positive_count, negative_count)
130    pub polarity_distribution: Option<(usize, usize)>,
131}
132impl Default for AerConfig {
133    fn default() -> Self {
134        Self {
135            big_endian: false,
136            validate_coordinates: true,
137            max_x: 511, // 9-bit maximum
138            max_y: 511, // 9-bit maximum
139            skip_invalid_events: false,
140            generate_timestamps: true,
141            timestamp_mode: TimestampMode::Sequential,
142            start_timestamp: 0.0,
143            time_increment: 1e-6, // 1 microsecond
144            max_events: None,
145            bytes_per_event: 4, // Common format: 32-bit words
146        }
147    }
148}
149impl AerConfig {
150    /// Create a new AER configuration with default values
151    pub fn new() -> Self {
152        Self::default()
153    }
154    /// Set endianness
155    pub fn with_endianness(mut self, big_endian: bool) -> Self {
156        self.big_endian = big_endian;
157        self
158    }
159    /// Set coordinate bounds
160    pub fn with_coordinate_bounds(mut self, max_x: u16, max_y: u16) -> Self {
161        self.max_x = max_x;
162        self.max_y = max_y;
163        self
164    }
165    /// Set timestamp generation parameters
166    pub fn with_timestamp_generation(
167        mut self,
168        generate: bool,
169        mode: TimestampMode,
170        start: f64,
171        increment: f64,
172    ) -> Self {
173        self.generate_timestamps = generate;
174        self.timestamp_mode = mode;
175        self.start_timestamp = start;
176        self.time_increment = increment;
177        self
178    }
179    /// Set bytes per event
180    pub fn with_bytes_per_event(mut self, bytes: usize) -> Self {
181        self.bytes_per_event = bytes;
182        self
183    }
184    /// Enable validation with error skipping
185    pub fn with_validation(mut self, validate: bool, skip_invalid: bool) -> Self {
186        self.validate_coordinates = validate;
187        self.skip_invalid_events = skip_invalid;
188        self
189    }
190    /// Set maximum events to read
191    pub fn with_max_events(mut self, max_events: Option<usize>) -> Self {
192        self.max_events = max_events;
193        self
194    }
195}
196/// AER format reader
197pub struct AerReader {
198    config: AerConfig,
199}
200impl AerReader {
201    /// Create a new AER reader with default configuration
202    pub fn new() -> Self {
203        Self {
204            config: AerConfig::default(),
205        }
206    }
207    /// Create a new AER reader with custom configuration
208    pub fn with_config(config: AerConfig) -> Self {
209        Self { config }
210    }
211    /// Read AER events from a file
212    pub fn read_file<P: AsRef<Path>>(&self, path: P) -> AerResult<(DataFrame, AerMetadata)> {
213        let mut file = File::open(path.as_ref())?;
214        let file_size = file.metadata()?.len();
215        if file_size == 0 {
216            return Err(AerError::EmptyFile);
217        }
218        // Validate file size
219        if file_size % self.config.bytes_per_event as u64 != 0 {
220            return Err(AerError::InvalidFileSize(
221                file_size,
222                self.config.bytes_per_event,
223            ));
224        }
225        let expected_event_count = (file_size / self.config.bytes_per_event as u64) as usize;
226        let event_count = match self.config.max_events {
227            Some(max) => expected_event_count.min(max),
228            None => expected_event_count,
229        };
230        // Read binary data
231        let mut buffer = vec![0u8; event_count * self.config.bytes_per_event];
232        file.read_exact(&mut buffer)?;
233        // Parse events
234        let (events, metadata) = self.parse_events(&buffer, file_size)?;
235        Ok((events, metadata))
236    }
237    /// Parse AER events from binary data
238    fn parse_events(&self, data: &[u8], file_size: u64) -> AerResult<(DataFrame, AerMetadata)> {
239        if self.config.bytes_per_event != 2 && self.config.bytes_per_event != 4 {
240            return Err(AerError::InvalidBytesPerEvent(self.config.bytes_per_event));
241        }
242        {
243            let event_count = data.len() / self.config.bytes_per_event;
244            let mut builder = EventDataFrameBuilder::new(EventFormat::AER, event_count);
245            // Statistics for metadata
246            let mut min_x = u16::MAX;
247            let mut min_y = u16::MAX;
248            let mut max_x = 0u16;
249            let mut max_y = 0u16;
250            let mut positive_count = 0;
251            let mut negative_count = 0;
252            let mut valid_events = 0;
253            // Collect parsed events for timestamp generation
254            let mut parsed_events = Vec::with_capacity(event_count);
255            // Process events
256            for i in 0..event_count {
257                let offset = i * self.config.bytes_per_event;
258                match self
259                    .parse_single_event(&data[offset..offset + self.config.bytes_per_event], i)
260                {
261                    Ok((x, y, t, polarity)) => {
262                        // Update statistics
263                        min_x = min_x.min(x);
264                        min_y = min_y.min(y);
265                        max_x = max_x.max(x);
266                        max_y = max_y.max(y);
267                        if polarity {
268                            positive_count += 1;
269                        } else {
270                            negative_count += 1;
271                        }
272                        parsed_events.push((x, y, t, polarity));
273                        valid_events += 1;
274                    }
275                    Err(e) => {
276                        if self.config.skip_invalid_events {
277                            continue;
278                        } else {
279                            return Err(e);
280                        }
281                    }
282                }
283            }
284            // Generate timestamps if requested
285            if self.config.generate_timestamps {
286                self.generate_timestamps_for_parsed_events(&mut parsed_events)?;
287            }
288            // Add events to builder.
289            // Generated timestamps are f64 seconds. Converting to microseconds with
290            // a truncating cast would turn e.g. 1.001 s into 1_000_999 us (because
291            // 1.001 * 1e6 is 1000999.999... in f64) instead of the intended
292            // 1_001_000 us. Round the seconds -> microseconds product here
293            // (round-half-up) and feed exact integer microseconds to the builder.
294            // This rounding is AER-local; the shared builder's seconds heuristic is
295            // left untouched for other readers.
296            for (x, y, timestamp, polarity) in &parsed_events {
297                let microseconds = (*timestamp * 1_000_000.0).round() as i64;
298                builder.add_event_microseconds(*x, *y, microseconds, *polarity);
299            }
300            let events = builder.build().map_err(|e| {
301                AerError::Io(std::io::Error::new(
302                    std::io::ErrorKind::InvalidData,
303                    format!("Failed to build DataFrame: {}", e),
304                ))
305            })?;
306            let timestamp_range = if !parsed_events.is_empty() {
307                Some((parsed_events[0].2, parsed_events[parsed_events.len() - 1].2))
308            } else {
309                None
310            };
311            let coordinate_bounds = if valid_events > 0 {
312                Some((min_x, min_y, max_x, max_y))
313            } else {
314                None
315            };
316            let metadata = AerMetadata {
317                file_size,
318                event_count: valid_events,
319                bytes_per_event: self.config.bytes_per_event,
320                endianness: if self.config.big_endian {
321                    "big".to_string()
322                } else {
323                    "little".to_string()
324                },
325                coordinate_bounds,
326                timestamp_range,
327                polarity_distribution: Some((positive_count, negative_count)),
328            };
329            Ok((events, metadata))
330        }
331    }
332    /// Parse a single AER event from binary data
333    fn parse_single_event(
334        &self,
335        data: &[u8],
336        _event_index: usize,
337    ) -> AerResult<(u16, u16, f64, bool)> {
338        if data.len() < self.config.bytes_per_event {
339            return Err(AerError::InsufficientData(
340                self.config.bytes_per_event,
341                data.len(),
342            ));
343        }
344        let raw_event = match self.config.bytes_per_event {
345            2 => {
346                // 16-bit format (direct 18-bit event, but only 16 bits used)
347                if self.config.big_endian {
348                    u16::from_be_bytes([data[0], data[1]]) as u32
349                } else {
350                    u16::from_le_bytes([data[0], data[1]]) as u32
351                }
352            }
353            4 => {
354                // 32-bit format (18-bit event in 32-bit word)
355                if self.config.big_endian {
356                    u32::from_be_bytes([data[0], data[1], data[2], data[3]])
357                } else {
358                    u32::from_le_bytes([data[0], data[1], data[2], data[3]])
359                }
360            }
361            _ => {
362                return Err(AerError::InvalidBytesPerEvent(self.config.bytes_per_event));
363            }
364        };
365        // Extract 18-bit AER structure: 1 bit polarity + 9 bits x + 9 bits y
366        let polarity_bit = (raw_event & 0x1) as u8;
367        let x = ((raw_event >> 1) & 0x1FF) as u16; // 9 bits for x coordinate
368        let y = ((raw_event >> 10) & 0x1FF) as u16; // 9 bits for y coordinate
369                                                    // Convert polarity bit to boolean
370        let polarity = polarity_bit == 1;
371        // Validate coordinates if requested
372        if self.config.validate_coordinates && (x > self.config.max_x || y > self.config.max_y) {
373            return Err(AerError::InvalidCoordinate(
374                x,
375                y,
376                self.config.max_x,
377                self.config.max_y,
378            ));
379        }
380        // Return tuple with placeholder timestamp (will be generated later if needed)
381        Ok((x, y, 0.0, polarity))
382    }
383    /// Generate timestamps for parsed events based on configuration
384    fn generate_timestamps_for_parsed_events(
385        &self,
386        events: &mut [(u16, u16, f64, bool)],
387    ) -> AerResult<()> {
388        if events.is_empty() {
389            return Ok(());
390        }
391        match &self.config.timestamp_mode {
392            TimestampMode::Sequential => {
393                for (i, event) in events.iter_mut().enumerate() {
394                    event.2 = self.config.start_timestamp + (i as f64 * self.config.time_increment);
395                }
396            }
397            TimestampMode::Uniform => {
398                let total_time = events.len() as f64 * self.config.time_increment;
399                let event_count = events.len();
400                for (i, event) in events.iter_mut().enumerate() {
401                    event.2 =
402                        self.config.start_timestamp + (i as f64 / event_count as f64) * total_time;
403                }
404            }
405            TimestampMode::Exponential => {
406                // Generate exponentially distributed timestamps
407                let mut current_time = self.config.start_timestamp;
408                let lambda = 1.0 / self.config.time_increment; // Rate parameter
409                for event in events.iter_mut() {
410                    // Simple exponential distribution approximation
411                    let u: f64 = (fastrand::f64() + 1e-10).ln(); // Add small value to avoid log(0)
412                    let interval = -u / lambda;
413                    current_time += interval;
414                    event.2 = current_time;
415                }
416            }
417            TimestampMode::Custom(timestamps) => {
418                if timestamps.len() != events.len() {
419                    let timestamp_count = timestamps.len();
420                    let event_count = events.len();
421                    return Err(AerError::ValidationFailed(format!(
422                        "Custom timestamp count ({timestamp_count}) doesn't match event count ({event_count})"
423                    )));
424                }
425                for (event, &timestamp) in events.iter_mut().zip(timestamps.iter()) {
426                    event.2 = timestamp;
427                }
428            }
429        }
430        Ok(())
431    }
432    /// Read AER events with LoadConfig filtering
433    pub fn read_with_config<P: AsRef<Path>>(
434        &self,
435        path: P,
436        load_config: &LoadConfig,
437    ) -> AerResult<DataFrame> {
438        let (events, _metadata) = self.read_file(path)?;
439        {
440            // Apply filtering and sorting using DataFrame operations
441            let mut df = events.lazy();
442            // Apply LoadConfig filters (this would need to be implemented for DataFrame)
443            // For now, just return the DataFrame as-is
444            // TODO: Implement LoadConfig filtering for DataFrames
445            if load_config.sort {
446                df = df.sort(["t"], Default::default());
447            }
448            df.collect().map_err(|e| {
449                AerError::Io(std::io::Error::new(
450                    std::io::ErrorKind::InvalidData,
451                    format!("Failed to process DataFrame: {}", e),
452                ))
453            })
454        }
455    }
456    /// Get configuration
457    pub fn config(&self) -> &AerConfig {
458        &self.config
459    }
460}
461impl Default for AerReader {
462    fn default() -> Self {
463        Self::new()
464    }
465}
466/// Convenience function to read AER file with default configuration
467pub fn read_aer_file<P: AsRef<Path>>(path: P) -> AerResult<(DataFrame, AerMetadata)> {
468    let reader = AerReader::new();
469    reader.read_file(path)
470}
471/// Convenience function to read AER file with custom configuration
472pub fn read_aer_file_with_config<P: AsRef<Path>>(
473    path: P,
474    config: AerConfig,
475) -> AerResult<(DataFrame, AerMetadata)> {
476    let reader = AerReader::with_config(config);
477    reader.read_file(path)
478}
479/// Detect if a file is likely to be AER format
480pub fn is_aer_format<P: AsRef<Path>>(path: P) -> bool {
481    let file = match File::open(path.as_ref()) {
482        Ok(f) => f,
483        Err(_) => return false,
484    };
485    let file_size = match file.metadata() {
486        Ok(m) => m.len(),
487        Err(_) => return false,
488    };
489    // Check if file size is consistent with AER format
490    if file_size % 4 != 0 && file_size % 2 != 0 {
491        return false;
492    }
493    // Try to read a few events and validate
494    let bytes_to_read = std::cmp::min(32, file_size as usize);
495    let mut buffer = vec![0u8; bytes_to_read];
496    let mut file = file;
497    if file.read_exact(&mut buffer).is_err() {
498        return false;
499    }
500    // Check if the data looks like valid AER events
501    let config = AerConfig::default();
502    let reader = AerReader::with_config(config);
503    for i in 0..8 {
504        let offset = i * 4;
505        if offset + 4 > buffer.len() {
506            break;
507        }
508        if reader
509            .parse_single_event(&buffer[offset..offset + 4], i)
510            .is_err()
511        {
512            return false;
513        }
514    }
515    true
516}
517#[cfg(test)]
518mod tests {
519    use super::*;
520    use std::io::Write;
521    use tempfile::NamedTempFile;
522
523    /// Lightweight event row extracted from a Polars DataFrame for assertion convenience.
524    #[derive(Debug, Clone, Copy)]
525    struct EventRow {
526        t: f64, // seconds
527        x: u16,
528        y: u16,
529        polarity: i8, // 0/1 for AER (Text-like encoding)
530    }
531
532    fn dataframe_to_rows(df: &polars::prelude::DataFrame) -> Vec<EventRow> {
533        let x = df.column("x").unwrap().i16().unwrap();
534        let y = df.column("y").unwrap().i16().unwrap();
535        let t = df.column("t").unwrap().duration().unwrap();
536        let p = df.column("polarity").unwrap().i8().unwrap();
537        (0..df.height())
538            .map(|i| EventRow {
539                t: t.get(i).unwrap() as f64 / 1_000_000.0,
540                x: x.get(i).unwrap() as u16,
541                y: y.get(i).unwrap() as u16,
542                polarity: p.get(i).unwrap(),
543            })
544            .collect()
545    }
546
547    #[test]
548    fn test_aer_config_default() {
549        let config = AerConfig::default();
550        assert!(!config.big_endian);
551        assert!(config.validate_coordinates);
552        assert_eq!(config.max_x, 511);
553        assert_eq!(config.max_y, 511);
554        assert_eq!(config.bytes_per_event, 4);
555        assert!(config.generate_timestamps);
556        assert_eq!(config.timestamp_mode, TimestampMode::Sequential);
557    }
558    #[test]
559    fn test_aer_config_builder() {
560        let config = AerConfig::new()
561            .with_endianness(true)
562            .with_coordinate_bounds(1023, 1023)
563            .with_bytes_per_event(2)
564            .with_validation(true, true);
565        assert!(config.big_endian);
566        assert_eq!(config.max_x, 1023);
567        assert_eq!(config.max_y, 1023);
568        assert_eq!(config.bytes_per_event, 2);
569        assert!(config.skip_invalid_events);
570    }
571    #[test]
572    fn test_parse_18bit_aer_event() {
573        let config = AerConfig::default();
574        let reader = AerReader::with_config(config);
575        // Create a test event: polarity=1, x=100, y=200
576        // Raw: (200 << 10) | (100 << 1) | 1 = 204800 + 200 + 1 = 205001
577        let raw_event = 205001u32;
578        let data = raw_event.to_le_bytes();
579        let event = reader.parse_single_event(&data, 0).unwrap();
580        assert_eq!(event.0, 100); // x
581        assert_eq!(event.1, 200); // y
582        assert!(event.3); // polarity bit set
583    }
584    #[test]
585    fn test_parse_negative_polarity() {
586        let config = AerConfig::default();
587        let reader = AerReader::with_config(config);
588        // Create a test event: polarity=0, x=50, y=75
589        // Raw: (75 << 10) | (50 << 1) = 76800 + 100 = 76900
590        let raw_event = 76900u32;
591        let data = raw_event.to_le_bytes();
592        let event = reader.parse_single_event(&data, 0).unwrap();
593        assert_eq!(event.0, 50); // x
594        assert_eq!(event.1, 75); // y
595        assert!(!event.3); // polarity bit clear
596    }
597    #[test]
598    fn test_coordinate_validation() {
599        let config = AerConfig::default().with_coordinate_bounds(100, 100);
600        let reader = AerReader::with_config(config);
601        // Create an event with coordinates exceeding bounds
602        let raw_event = (200u32 << 10) | (150u32 << 1) | 1; // x=150, y=200
603        let data = raw_event.to_le_bytes();
604        let result = reader.parse_single_event(&data, 0);
605        assert!(result.is_err());
606        assert!(matches!(
607            result.unwrap_err(),
608            AerError::InvalidCoordinate(_, _, _, _)
609        ));
610    }
611    #[test]
612    fn test_skip_invalid_events() {
613        let config = AerConfig::default()
614            .with_coordinate_bounds(100, 100)
615            .with_validation(true, true); // Skip invalid events
616        let reader = AerReader::with_config(config);
617        // Create test data with mix of valid and invalid events
618        let mut data = Vec::new();
619        // Valid event: x=50, y=75, polarity=1
620        let valid_event = (75u32 << 10) | (50u32 << 1) | 1;
621        data.extend_from_slice(&valid_event.to_le_bytes());
622        // Invalid event: x=150, y=200, polarity=0 (exceeds bounds)
623        let invalid_event = (200u32 << 10) | (150u32 << 1);
624        data.extend_from_slice(&invalid_event.to_le_bytes());
625        // Another valid event: x=25, y=30, polarity=0
626        let valid_event2 = (30u32 << 10) | (25u32 << 1);
627        data.extend_from_slice(&valid_event2.to_le_bytes());
628        let (events, metadata) = reader.parse_events(&data, data.len() as u64).unwrap();
629        assert_eq!(events.height(), 2); // Only valid events should be included
630        assert_eq!(metadata.event_count, 2);
631        let rows = dataframe_to_rows(&events);
632        assert_eq!(rows[0].x, 50);
633        assert_eq!(rows[0].y, 75);
634        assert_eq!(rows[1].x, 25);
635        assert_eq!(rows[1].y, 30);
636    }
637    #[test]
638    fn test_timestamp_generation_sequential() {
639        let config = AerConfig::default().with_timestamp_generation(
640            true,
641            TimestampMode::Sequential,
642            1.0,
643            0.001,
644        );
645        let reader = AerReader::with_config(config);
646        // Create test data
647        let events_data = vec![
648            ((100u32 << 10) | (50u32 << 1) | 1).to_le_bytes(),
649            ((200u32 << 10) | (150u32 << 1)).to_le_bytes(),
650            ((300u32 << 10) | (250u32 << 1) | 1).to_le_bytes(),
651        ];
652        let data: Vec<u8> = events_data.into_iter().flatten().collect();
653        let (events, _) = reader.parse_events(&data, data.len() as u64).unwrap();
654        assert_eq!(events.height(), 3);
655        // Timestamps are stored as Duration(microseconds) via a rounding
656        // seconds -> microseconds conversion, so assert on the exact decoded
657        // microsecond integers. Note 1.001 s rounds to 1_001_000 us; a truncating
658        // conversion would instead give 1_000_999 us because 1.001 * 1e6 is not
659        // exactly representable in f64 (1000999.999...).
660        let t = events.column("t").unwrap().duration().unwrap();
661        assert_eq!(t.get(0).unwrap(), 1_000_000); // start 1.0 s
662        assert_eq!(t.get(1).unwrap(), 1_001_000); // 1.001 s (rounded)
663        assert_eq!(t.get(2).unwrap(), 1_002_000); // 1.002 s
664    }
665    #[test]
666    fn test_timestamp_generation_uniform() {
667        let config = AerConfig::default().with_timestamp_generation(
668            true,
669            TimestampMode::Uniform,
670            0.0,
671            0.003,
672        );
673        let reader = AerReader::with_config(config);
674        // Create test data for 3 events
675        let events_data = vec![
676            ((100u32 << 10) | (50u32 << 1) | 1).to_le_bytes(),
677            ((200u32 << 10) | (150u32 << 1)).to_le_bytes(),
678            ((300u32 << 10) | (250u32 << 1) | 1).to_le_bytes(),
679        ];
680        let data: Vec<u8> = events_data.into_iter().flatten().collect();
681        let (events, _) = reader.parse_events(&data, data.len() as u64).unwrap();
682        assert_eq!(events.height(), 3);
683        let rows = dataframe_to_rows(&events);
684        assert_eq!(rows[0].t, 0.0);
685        assert_eq!(rows[1].t, 0.003);
686        assert_eq!(rows[2].t, 0.006);
687    }
688    #[test]
689    fn test_big_endian_parsing() {
690        let config = AerConfig::default().with_endianness(true);
691        let reader = AerReader::with_config(config);
692        // Create a test event: polarity=1, x=100, y=200
693        let raw_event = 205001u32;
694        let data = raw_event.to_be_bytes(); // Big endian
695        let event = reader.parse_single_event(&data, 0).unwrap();
696        assert_eq!(event.0, 100); // x
697        assert_eq!(event.1, 200); // y
698        assert!(event.3); // polarity bit set
699    }
700    #[test]
701    fn test_16bit_format() {
702        let config = AerConfig::default().with_bytes_per_event(2);
703        let reader = AerReader::with_config(config);
704        // Create a test event in 16-bit format
705        // Since we're using 16-bit, we need to fit polarity + x + y into 16 bits
706        // Let's use: 1 bit polarity + 7 bits x + 8 bits y
707        let raw_event = (75u16 << 8) | (50u16 << 1) | 1; // polarity=1, x=50, y=75
708        let data = raw_event.to_le_bytes();
709        let event = reader.parse_single_event(&data, 0).unwrap();
710        // With 16-bit format, coordinates will be different due to bit layout
711        assert!(event.3); // polarity bit set
712    }
713    #[test]
714    fn test_read_aer_file() {
715        let config = AerConfig::default();
716        let reader = AerReader::with_config(config);
717        // Create temporary file with test data
718        let mut temp_file = NamedTempFile::new().unwrap();
719        // Write test events
720        let events_data = vec![
721            ((100u32 << 10) | (50u32 << 1) | 1).to_le_bytes(),
722            ((200u32 << 10) | (150u32 << 1)).to_le_bytes(),
723            ((300u32 << 10) | (250u32 << 1) | 1).to_le_bytes(),
724        ];
725        for event_bytes in events_data {
726            temp_file.write_all(&event_bytes).unwrap();
727        }
728        let (events, metadata) = reader.read_file(temp_file.path()).unwrap();
729        assert_eq!(events.height(), 3);
730        assert_eq!(metadata.event_count, 3);
731        assert_eq!(metadata.bytes_per_event, 4);
732        assert_eq!(metadata.file_size, 12);
733        // Check first event
734        let rows = dataframe_to_rows(&events);
735        assert_eq!(rows[0].x, 50);
736        assert_eq!(rows[0].y, 100);
737        assert_eq!(rows[0].polarity, 1); // AER stores positive polarity as 1
738                                         // Check metadata
739        assert!(metadata.coordinate_bounds.is_some());
740        assert!(metadata.polarity_distribution.is_some());
741        let (pos_count, neg_count) = metadata.polarity_distribution.unwrap();
742        assert_eq!(pos_count, 2);
743        assert_eq!(neg_count, 1);
744    }
745    #[test]
746    fn test_empty_file() {
747        let config = AerConfig::default();
748        let reader = AerReader::with_config(config);
749        let temp_file = NamedTempFile::new().unwrap();
750        let result = reader.read_file(temp_file.path());
751        assert!(result.is_err());
752        assert!(matches!(result.unwrap_err(), AerError::EmptyFile));
753    }
754    #[test]
755    fn test_invalid_file_size() {
756        let config = AerConfig::default();
757        let reader = AerReader::with_config(config);
758        let mut temp_file = NamedTempFile::new().unwrap();
759        temp_file.write_all(&[1, 2, 3]).unwrap(); // 3 bytes, not divisible by 4
760        let result = reader.read_file(temp_file.path());
761        assert!(result.is_err());
762        assert!(matches!(
763            result.unwrap_err(),
764            AerError::InvalidFileSize(_, _)
765        ));
766    }
767    #[test]
768    fn test_custom_timestamps() {
769        let custom_timestamps = vec![0.1, 0.5, 1.2];
770        let config = AerConfig::default().with_timestamp_generation(
771            true,
772            TimestampMode::Custom(custom_timestamps.clone()),
773            0.0,
774            0.001,
775        );
776        let reader = AerReader::with_config(config);
777        // Create test data
778        let events_data = vec![
779            ((100u32 << 10) | (50u32 << 1) | 1).to_le_bytes(),
780            ((200u32 << 10) | (150u32 << 1)).to_le_bytes(),
781            ((300u32 << 10) | (250u32 << 1) | 1).to_le_bytes(),
782        ];
783        let data: Vec<u8> = events_data.into_iter().flatten().collect();
784        let (events, _) = reader.parse_events(&data, data.len() as u64).unwrap();
785        assert_eq!(events.height(), 3);
786        let rows = dataframe_to_rows(&events);
787        assert_eq!(rows[0].t, 0.1);
788        assert_eq!(rows[1].t, 0.5);
789        assert_eq!(rows[2].t, 1.2);
790    }
791    #[test]
792    fn test_is_aer_format() {
793        // Create a temporary file with valid AER data
794        let mut temp_file = NamedTempFile::new().unwrap();
795        let events_data = vec![
796            ((100u32 << 10) | (50u32 << 1) | 1).to_le_bytes(),
797            ((200u32 << 10) | (150u32 << 1)).to_le_bytes(),
798        ];
799        for event_bytes in events_data {
800            temp_file.write_all(&event_bytes).unwrap();
801        }
802        assert!(is_aer_format(temp_file.path()));
803    }
804    #[test]
805    fn test_max_events_limit() {
806        let config = AerConfig::default().with_max_events(Some(2));
807        let reader = AerReader::with_config(config);
808        let mut temp_file = NamedTempFile::new().unwrap();
809        // Write 3 events but limit to 2
810        let events_data = vec![
811            ((100u32 << 10) | (50u32 << 1) | 1).to_le_bytes(),
812            ((200u32 << 10) | (150u32 << 1)).to_le_bytes(),
813            ((300u32 << 10) | (250u32 << 1) | 1).to_le_bytes(),
814        ];
815        for event_bytes in events_data {
816            temp_file.write_all(&event_bytes).unwrap();
817        }
818        let (events, metadata) = reader.read_file(temp_file.path()).unwrap();
819        assert_eq!(events.height(), 2);
820        assert_eq!(metadata.event_count, 2);
821    }
822}