Skip to main content

optirs_core/neuromorphic/
event_driven.rs

1// Event-Driven Optimization Algorithms
2//
3// This module implements event-driven optimization algorithms that process
4// updates asynchronously based on neuromorphic events, designed for
5// neuromorphic computing platforms with event-based architectures.
6
7use super::{
8    to_generic_or, EventPriority, MembraneDynamicsConfig, NeuromorphicEvent, NeuromorphicMetrics,
9    STDPConfig,
10};
11use crate::error::{OptimError, Result};
12use scirs2_core::ndarray::{Array1, Array2};
13use scirs2_core::numeric::Float;
14use std::cmp::Reverse;
15use std::collections::{BTreeMap, BinaryHeap, HashMap, HashSet, VecDeque};
16use std::fmt::Debug;
17use std::time::{Duration, Instant};
18
19// --- Pure-Rust varint helpers for event (de)serialization (F57) --------
20//
21// LEB128 unsigned varints, with zigzag encoding for signed values. Used by
22// `EventCompressionEngine` to produce real, decodable bytes for events
23// instead of fixed-size placeholder buffers.
24
25fn write_uvarint(buf: &mut Vec<u8>, mut value: u64) {
26    loop {
27        let mut byte = (value & 0x7f) as u8;
28        value >>= 7;
29        if value != 0 {
30            byte |= 0x80;
31        }
32        buf.push(byte);
33        if value == 0 {
34            break;
35        }
36    }
37}
38
39fn read_uvarint(buf: &[u8], pos: &mut usize) -> Option<u64> {
40    let mut result: u64 = 0;
41    let mut shift: u32 = 0;
42    loop {
43        let byte = *buf.get(*pos)?;
44        *pos += 1;
45        result |= ((byte & 0x7f) as u64) << shift;
46        if byte & 0x80 == 0 {
47            break;
48        }
49        shift += 7;
50        if shift >= 64 {
51            return None;
52        }
53    }
54    Some(result)
55}
56
57fn zigzag_encode(value: i64) -> u64 {
58    ((value << 1) ^ (value >> 63)) as u64
59}
60
61fn zigzag_decode(value: u64) -> i64 {
62    ((value >> 1) as i64) ^ -((value & 1) as i64)
63}
64
65fn write_ivarint(buf: &mut Vec<u8>, value: i64) {
66    write_uvarint(buf, zigzag_encode(value));
67}
68
69fn read_ivarint(buf: &[u8], pos: &mut usize) -> Option<i64> {
70    read_uvarint(buf, pos).map(zigzag_decode)
71}
72
73/// Fixed-point scale used to convert floating-point event fields
74/// (timestamps, neuron ids treated as integers, values, energy costs) to
75/// integers before varint encoding. Millisecond timestamps are kept to
76/// microsecond resolution.
77const EVENT_FIXED_POINT_SCALE: f64 = 1000.0;
78
79fn decode_event_type(byte: u8) -> Result<EventType> {
80    match byte {
81        0 => Ok(EventType::Spike),
82        1 => Ok(EventType::WeightUpdate),
83        2 => Ok(EventType::ThresholdCrossing),
84        3 => Ok(EventType::PlasticityEvent),
85        4 => Ok(EventType::ExternalStimulus),
86        5 => Ok(EventType::TimerEvent),
87        6 => Ok(EventType::ErrorEvent),
88        7 => Ok(EventType::HomeostaticEvent),
89        8 => Ok(EventType::SynchronizationEvent),
90        9 => Ok(EventType::EnergyEvent),
91        other => Err(OptimError::InvalidConfig(format!(
92            "unknown encoded EventType discriminant: {other}"
93        ))),
94    }
95}
96
97fn decode_priority(byte: u8) -> Result<EventPriority> {
98    match byte {
99        0 => Ok(EventPriority::Low),
100        1 => Ok(EventPriority::Normal),
101        2 => Ok(EventPriority::High),
102        3 => Ok(EventPriority::Critical),
103        4 => Ok(EventPriority::RealTime),
104        other => Err(OptimError::InvalidConfig(format!(
105            "unknown encoded EventPriority discriminant: {other}"
106        ))),
107    }
108}
109
110fn truncated_bytes_err() -> crate::error::OptimError {
111    OptimError::InvalidConfig("truncated compressed event bytes".to_string())
112}
113
114/// Event types for neuromorphic computing
115#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
116pub enum EventType {
117    /// Spike event from a neuron
118    Spike,
119
120    /// Synaptic weight update event
121    WeightUpdate,
122
123    /// Membrane potential threshold crossing
124    ThresholdCrossing,
125
126    /// Plasticity-triggered event
127    PlasticityEvent,
128
129    /// External stimulus event
130    ExternalStimulus,
131
132    /// Timer-based event
133    TimerEvent,
134
135    /// Error backpropagation event
136    ErrorEvent,
137
138    /// Homeostatic adaptation event
139    HomeostaticEvent,
140
141    /// Population synchronization event
142    SynchronizationEvent,
143
144    /// Energy budget event
145    EnergyEvent,
146}
147
148/// Event-driven optimization configuration
149#[derive(Debug, Clone)]
150pub struct EventDrivenConfig<T: Float + Debug + Send + Sync + 'static> {
151    /// Maximum event queue size
152    pub max_queue_size: usize,
153
154    /// Event processing timeout (ms)
155    pub processing_timeout: T,
156
157    /// Enable event priority scheduling
158    pub priority_scheduling: bool,
159
160    /// Event filtering threshold
161    pub event_threshold: T,
162
163    /// Enable event batching
164    pub event_batching: bool,
165
166    /// Batch size for event processing
167    pub batch_size: usize,
168
169    /// Enable temporal event correlation
170    pub temporal_correlation: bool,
171
172    /// Temporal correlation window (ms)
173    pub correlation_window: T,
174
175    /// Enable adaptive event handling
176    pub adaptive_handling: bool,
177
178    /// Event rate limits (events/second)
179    pub rate_limits: HashMap<EventType, T>,
180
181    /// Enable event compression
182    pub event_compression: bool,
183
184    /// Compression algorithm
185    pub compression_algorithm: EventCompressionAlgorithm,
186
187    /// Enable distributed event processing
188    pub distributed_processing: bool,
189
190    /// Load balancing strategy
191    pub load_balancing: LoadBalancingStrategy,
192}
193
194/// Event compression algorithms
195#[derive(Debug, Clone, Copy)]
196pub enum EventCompressionAlgorithm {
197    /// No compression
198    None,
199
200    /// Delta encoding
201    DeltaEncoding,
202
203    /// Huffman encoding
204    HuffmanEncoding,
205
206    /// Run-length encoding
207    RunLengthEncoding,
208
209    /// Sparse encoding
210    SparseEncoding,
211
212    /// Predictive encoding
213    PredictiveEncoding,
214}
215
216/// Load balancing strategies for distributed event processing
217#[derive(Debug, Clone, Copy)]
218pub enum LoadBalancingStrategy {
219    /// Round-robin distribution
220    RoundRobin,
221
222    /// Event type-based partitioning
223    TypeBased,
224
225    /// Load-aware distribution
226    LoadAware,
227
228    /// Locality-aware distribution
229    LocalityAware,
230
231    /// Dynamic load balancing
232    Dynamic,
233}
234
235impl<T: Float + Debug + Send + Sync + 'static> Default for EventDrivenConfig<T> {
236    fn default() -> Self {
237        let mut rate_limits = HashMap::new();
238        rate_limits.insert(
239            EventType::Spike,
240            T::from(1000.0).unwrap_or_else(|| T::zero()),
241        );
242        rate_limits.insert(
243            EventType::WeightUpdate,
244            T::from(100.0).unwrap_or_else(|| T::zero()),
245        );
246        rate_limits.insert(
247            EventType::PlasticityEvent,
248            T::from(50.0).unwrap_or_else(|| T::zero()),
249        );
250
251        Self {
252            max_queue_size: 10000,
253            processing_timeout: T::from(1.0).unwrap_or_else(|| T::zero()),
254            priority_scheduling: true,
255            event_threshold: T::from(0.001).unwrap_or_else(|| T::zero()),
256            event_batching: true,
257            batch_size: 32,
258            temporal_correlation: true,
259            correlation_window: T::from(10.0).unwrap_or_else(|| T::zero()),
260            adaptive_handling: true,
261            rate_limits,
262            event_compression: false,
263            compression_algorithm: EventCompressionAlgorithm::None,
264            distributed_processing: false,
265            load_balancing: LoadBalancingStrategy::RoundRobin,
266        }
267    }
268}
269
270/// Priority queue entry for event scheduling
271#[derive(Debug, Clone)]
272struct PriorityEventEntry<T: Float + Debug + Send + Sync + 'static> {
273    event: NeuromorphicEvent<T>,
274    insertion_time: Instant,
275}
276
277impl<T: Float + Debug + Send + Sync + 'static> PartialEq for PriorityEventEntry<T> {
278    fn eq(&self, other: &Self) -> bool {
279        self.event.priority == other.event.priority
280    }
281}
282
283impl<T: Float + Debug + Send + Sync + 'static> Eq for PriorityEventEntry<T> {}
284
285impl<T: Float + Debug + Send + Sync + 'static> PartialOrd for PriorityEventEntry<T> {
286    fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
287        Some(self.cmp(other))
288    }
289}
290
291impl<T: Float + Debug + Send + Sync + 'static> Ord for PriorityEventEntry<T> {
292    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
293        // `BinaryHeap` is a max-heap: `pop()` returns the "greatest"
294        // element. We want higher `EventPriority` to pop first, so
295        // compare priorities directly rather than reversed (F55: the
296        // previous `other.priority.cmp(&self.priority)` inverted this,
297        // so pop() returned the LOWEST-priority event first). Ties break
298        // FIFO: the earlier `insertion_time` must compare as "greater" so
299        // it pops first, which is exactly what wrapping both sides in
300        // `Reverse` gives us.
301        self.event
302            .priority
303            .cmp(&other.event.priority)
304            .then_with(|| Reverse(self.insertion_time).cmp(&Reverse(other.insertion_time)))
305    }
306}
307
308/// Event-driven optimizer
309pub struct EventDrivenOptimizer<T: Float + Debug + Send + Sync + 'static> {
310    /// Configuration
311    config: EventDrivenConfig<T>,
312
313    /// STDP configuration
314    stdp_config: STDPConfig<T>,
315
316    /// Membrane dynamics configuration
317    membrane_config: MembraneDynamicsConfig<T>,
318
319    /// Event queue with priority scheduling
320    event_queue: BinaryHeap<PriorityEventEntry<T>>,
321
322    /// Event processing statistics
323    event_stats: HashMap<EventType, EventStatistics<T>>,
324
325    /// Current system state
326    system_state: SystemState<T>,
327
328    /// Event handlers
329    event_handlers: HashMap<EventType, Box<dyn EventHandler<T>>>,
330
331    /// Temporal correlation tracker
332    correlation_tracker: TemporalCorrelationTracker<T>,
333
334    /// Event rate limiter
335    rate_limiter: EventRateLimiter<T>,
336
337    /// Performance metrics
338    metrics: NeuromorphicMetrics<T>,
339
340    /// Distributed processing coordinator
341    distributed_coordinator: Option<DistributedEventCoordinator<T>>,
342
343    /// Reference (uncompressed) codec, used to measure how many bytes each
344    /// event would occupy without compression so the achieved compression
345    /// ratio reported by [`EventDrivenOptimizer::compression_ratio`] is a
346    /// real measurement rather than an estimate.
347    compression_engine: EventCompressionEngine<T>,
348
349    /// Compressed event storage, one FIFO chain per [`EventPriority`] (F57).
350    /// Used instead of `event_queue` while `config.event_compression` is
351    /// enabled: enqueued events are compressed to bytes immediately and only
352    /// reconstructed when they are actually processed.
353    compressed_chains: BTreeMap<EventPriority, CompressedEventChain<T>>,
354
355    /// Cumulative uncompressed size of every event that entered the
356    /// compressed queue.
357    compression_raw_bytes: usize,
358
359    /// Cumulative compressed size of every event that entered the compressed
360    /// queue.
361    compression_compressed_bytes: usize,
362
363    /// Adaptive handler
364    adaptive_handler: AdaptiveEventHandler<T>,
365}
366
367/// Event processing statistics
368#[derive(Debug, Clone)]
369pub struct EventStatistics<T: Float + Debug + Send + Sync + 'static> {
370    /// Total events processed
371    pub total_processed: usize,
372
373    /// Average processing time (ms)
374    pub avg_processing_time: T,
375
376    /// Event rate (events/second)
377    pub event_rate: T,
378
379    /// Queue wait time (ms)
380    pub avg_queue_wait_time: T,
381
382    /// Error count
383    pub error_count: usize,
384
385    /// Last update time
386    pub last_update: Instant,
387}
388
389/// System state for event-driven optimization
390#[derive(Debug, Clone)]
391pub struct SystemState<T: Float + Debug + Send + Sync + 'static> {
392    /// Current membrane potentials
393    pub membrane_potentials: Array1<T>,
394
395    /// Synaptic weights
396    pub synaptic_weights: Array2<T>,
397
398    /// Last spike times
399    pub last_spike_times: Array1<T>,
400
401    /// Refractory states
402    pub refractory_until: Array1<T>,
403
404    /// Current simulation time
405    pub current_time: T,
406
407    /// Active neurons
408    pub active_neurons: HashSet<usize>,
409
410    /// Pending weight updates
411    pub pending_updates: HashMap<(usize, usize), T>,
412}
413
414/// Event handler trait
415trait EventHandler<T: Float + Debug + Send + Sync + 'static>: Send + Sync {
416    fn handle_event(
417        &mut self,
418        event: &NeuromorphicEvent<T>,
419        state: &mut SystemState<T>,
420    ) -> Result<()>;
421}
422
423/// Spike event handler
424struct SpikeEventHandler<T: Float + Debug + Send + Sync + 'static> {
425    stdp_config: STDPConfig<T>,
426    membrane_config: MembraneDynamicsConfig<T>,
427}
428
429impl<T: Float + Debug + Send + Sync + 'static> EventHandler<T> for SpikeEventHandler<T> {
430    fn handle_event(
431        &mut self,
432        event: &NeuromorphicEvent<T>,
433        state: &mut SystemState<T>,
434    ) -> Result<()> {
435        let neuron_id = event.source_neuron;
436
437        // Generate spike
438        if neuron_id < state.membrane_potentials.len() {
439            // Reset membrane potential
440            state.membrane_potentials[neuron_id] = self.membrane_config.reset_potential;
441
442            // Set refractory period
443            state.refractory_until[neuron_id] =
444                state.current_time + self.membrane_config.refractory_period;
445
446            // Update last spike time
447            state.last_spike_times[neuron_id] = state.current_time;
448
449            // Add to active neurons
450            state.active_neurons.insert(neuron_id);
451
452            // Trigger STDP updates for connected synapses
453            self.trigger_stdp_updates(neuron_id, state)?;
454        }
455
456        Ok(())
457    }
458}
459
460impl<T: Float + Debug + Send + Sync + 'static> SpikeEventHandler<T> {
461    fn trigger_stdp_updates(&self, post_neuron: usize, state: &mut SystemState<T>) -> Result<()> {
462        let long_ago = to_generic_or(-1000.0, T::zero());
463        let now = state.current_time;
464
465        for other_neuron in 0..state.last_spike_times.len() {
466            if other_neuron == post_neuron {
467                continue;
468            }
469            let other_spike_time = state.last_spike_times[other_neuron];
470            if other_spike_time <= long_ago {
471                continue; // no valid spike history for `other_neuron` yet
472            }
473
474            // `other_neuron` fired before `post_neuron` (now): it is PRE,
475            // dt = t_post - t_pre > 0 => potentiation (LTP) on
476            // other_neuron -> post_neuron.
477            let dt_ltp = now - other_spike_time;
478            let ltp = self.compute_stdp_weight_change(dt_ltp);
479            Self::accumulate_pending(state, (other_neuron, post_neuron), ltp);
480
481            // `post_neuron` is firing NOW, arriving after
482            // `other_neuron`'s last spike: from `other_neuron`'s
483            // perspective as POST, this is a PRE spike arriving late,
484            // dt = t_post - t_pre = other_spike_time - now < 0 =>
485            // depression (LTD) on post_neuron -> other_neuron. This is
486            // the presynaptic-trace side of STDP that was previously
487            // unreachable (F50): `dt` computed only from "post's own time
488            // minus pre's last (necessarily past) spike time" is always
489            // >= 0, so LTD never fired.
490            let dt_ltd = other_spike_time - now;
491            let ltd = self.compute_stdp_weight_change(dt_ltd);
492            Self::accumulate_pending(state, (post_neuron, other_neuron), ltd);
493        }
494
495        Ok(())
496    }
497
498    /// Accumulate a weight delta into `pending_updates` (F56): the
499    /// previous `insert(...)` overwrote any existing pending update for
500    /// the same `(pre, post)` pair instead of summing contributions from
501    /// multiple presynaptic partners within the same batch.
502    fn accumulate_pending(state: &mut SystemState<T>, key: (usize, usize), delta: T) {
503        let entry = state.pending_updates.entry(key).or_insert_with(T::zero);
504        *entry = *entry + delta;
505    }
506
507    fn compute_stdp_weight_change(&self, dt: T) -> T {
508        if dt > T::zero() {
509            // Post-before-pre: LTP
510            let exp_arg = -dt / self.stdp_config.tau_pot;
511            self.stdp_config.learning_rate_pot * exp_arg.exp()
512        } else {
513            // Pre-before-post: LTD
514            let exp_arg = dt / self.stdp_config.tau_dep;
515            -self.stdp_config.learning_rate_dep * exp_arg.exp()
516        }
517    }
518}
519
520/// Weight update event handler
521struct WeightUpdateEventHandler<T: Float + Debug + Send + Sync + 'static> {
522    stdp_config: STDPConfig<T>,
523}
524
525impl<T: Float + Debug + Send + Sync + 'static> EventHandler<T> for WeightUpdateEventHandler<T> {
526    fn handle_event(
527        &mut self,
528        event: &NeuromorphicEvent<T>,
529        state: &mut SystemState<T>,
530    ) -> Result<()> {
531        let source = event.source_neuron;
532
533        if let Some(target) = event.target_neuron {
534            if source < state.synaptic_weights.nrows() && target < state.synaptic_weights.ncols() {
535                // Apply weight update
536                let current_weight = state.synaptic_weights[[source, target]];
537                let new_weight = (current_weight + event.value)
538                    .max(self.stdp_config.weight_min)
539                    .min(self.stdp_config.weight_max);
540
541                state.synaptic_weights[[source, target]] = new_weight;
542            }
543        }
544
545        Ok(())
546    }
547}
548
549/// Temporal correlation tracker
550struct TemporalCorrelationTracker<T: Float + Debug + Send + Sync + 'static> {
551    correlation_window: T,
552    event_history: VecDeque<(T, EventType, usize)>,
553    correlation_patterns: HashMap<(EventType, EventType), T>,
554}
555
556impl<T: Float + Debug + Send + Sync + 'static + std::ops::AddAssign> TemporalCorrelationTracker<T> {
557    fn new(correlation_window: T) -> Self {
558        Self {
559            correlation_window,
560            event_history: VecDeque::new(),
561            correlation_patterns: HashMap::new(),
562        }
563    }
564
565    fn add_event(&mut self, time: T, event_type: EventType, neuron_id: usize) {
566        // Add new event
567        self.event_history.push_back((time, event_type, neuron_id));
568
569        // Remove old events outside correlation window
570        while let Some(&(old_time, _, _)) = self.event_history.front() {
571            if time - old_time > self.correlation_window {
572                self.event_history.pop_front();
573            } else {
574                break;
575            }
576        }
577
578        // Update correlation patterns
579        self.update_correlations(time, event_type);
580    }
581
582    fn update_correlations(&mut self, current_time: T, current_event: EventType) {
583        for &(event_time, event_type_, _) in &self.event_history {
584            if current_time - event_time <= self.correlation_window {
585                let correlation_key = (event_type_, current_event);
586                let time_diff = current_time - event_time;
587                let correlation_strength = (-time_diff / self.correlation_window).exp();
588
589                *self
590                    .correlation_patterns
591                    .entry(correlation_key)
592                    .or_insert(T::zero()) += correlation_strength;
593            }
594        }
595    }
596
597    /// Measured co-occurrence strength between two event types.
598    pub(crate) fn get_correlation(&self, event1: EventType, event2: EventType) -> T {
599        self.correlation_patterns
600            .get(&(event1, event2))
601            .copied()
602            .unwrap_or(T::zero())
603    }
604}
605
606/// Event rate limiter
607struct EventRateLimiter<T: Float + Debug + Send + Sync + 'static> {
608    rate_limits: HashMap<EventType, T>,
609    event_counts: HashMap<EventType, usize>,
610    last_reset: Instant,
611    reset_interval: Duration,
612}
613
614impl<T: Float + Debug + Send + Sync + 'static> EventRateLimiter<T> {
615    fn new(rate_limits: HashMap<EventType, T>) -> Self {
616        Self {
617            rate_limits,
618            event_counts: HashMap::new(),
619            last_reset: Instant::now(),
620            reset_interval: Duration::from_secs(1),
621        }
622    }
623
624    fn can_process(&mut self, event_type: EventType) -> bool {
625        // Reset counters if interval elapsed
626        if self.last_reset.elapsed() >= self.reset_interval {
627            self.event_counts.clear();
628            self.last_reset = Instant::now();
629        }
630
631        if let Some(&limit) = self.rate_limits.get(&event_type) {
632            let current_count = self.event_counts.get(&event_type).copied().unwrap_or(0);
633            if T::from(current_count).unwrap_or_else(|| T::zero()) < limit {
634                *self.event_counts.entry(event_type).or_insert(0) += 1;
635                true
636            } else {
637                false
638            }
639        } else {
640            true
641        }
642    }
643}
644
645/// Event compression engine
646struct EventCompressionEngine<T: Float + Debug + Send + Sync + 'static> {
647    algorithm: EventCompressionAlgorithm,
648    compression_buffer: Vec<u8>,
649    decompression_buffer: Vec<u8>,
650    /// Last event's (fixed-point-scaled) fields, used as the delta baseline
651    /// by [`Self::delta_encode_event`]/[`Self::delta_decode_event`] (F57).
652    /// `None` until the first event has been compressed.
653    last_event_fields: Option<(i64, i64, Option<i64>, i64, i64)>,
654    _phantom: std::marker::PhantomData<T>,
655}
656
657/// Convert a float event field to a fixed-point integer at
658/// [`EVENT_FIXED_POINT_SCALE`] resolution, saturating instead of panicking
659/// on out-of-range or non-finite values.
660fn field_to_fixed<T: Float>(value: T) -> i64 {
661    let scaled = value.to_f64().unwrap_or(0.0) * EVENT_FIXED_POINT_SCALE;
662    if !scaled.is_finite() {
663        0
664    } else {
665        scaled.clamp(i64::MIN as f64, i64::MAX as f64) as i64
666    }
667}
668
669fn fixed_to_field<T: Float>(fixed: i64) -> T {
670    to_generic_or(fixed as f64 / EVENT_FIXED_POINT_SCALE, T::zero())
671}
672
673impl<T: Float + Debug + Send + Sync + 'static> EventCompressionEngine<T> {
674    fn new(algorithm: EventCompressionAlgorithm) -> Self {
675        Self {
676            algorithm,
677            compression_buffer: Vec::new(),
678            decompression_buffer: Vec::new(),
679            last_event_fields: None,
680            _phantom: std::marker::PhantomData,
681        }
682    }
683
684    fn compress_event(&mut self, event: &NeuromorphicEvent<T>) -> Result<Vec<u8>> {
685        let compressed = match self.algorithm {
686            EventCompressionAlgorithm::None => {
687                // No compression, serialize directly
688                self.serialize_event(event)
689            }
690            EventCompressionAlgorithm::DeltaEncoding => self.delta_encode_event(event),
691            EventCompressionAlgorithm::SparseEncoding => self.sparse_encode_event(event),
692            _ => {
693                // Fallback to no compression
694                self.serialize_event(event)
695            }
696        }?;
697        self.compression_buffer.clear();
698        self.compression_buffer.extend_from_slice(&compressed);
699        Ok(compressed)
700    }
701
702    /// Reconstruct a [`NeuromorphicEvent`] from bytes produced by
703    /// [`Self::compress_event`], using the same algorithm and delta-baseline
704    /// state. Must be called in the same order events were compressed when
705    /// `DeltaEncoding` is in use, since each event's baseline is the
706    /// previously *decoded* one.
707    fn decompress_event(&mut self, bytes: &[u8]) -> Result<NeuromorphicEvent<T>> {
708        self.decompression_buffer.clear();
709        self.decompression_buffer.extend_from_slice(bytes);
710        match self.algorithm {
711            EventCompressionAlgorithm::DeltaEncoding => self.delta_decode_event(bytes),
712            EventCompressionAlgorithm::SparseEncoding => self.sparse_decode_event(bytes),
713            _ => self.deserialize_event(bytes),
714        }
715    }
716
717    /// Full-fidelity, self-contained encoding of every event field as
718    /// LEB128 varints (unsigned for the always-non-negative fields,
719    /// zigzag for `value`/`energy_cost` which may be negative). This is
720    /// both `EventCompressionAlgorithm::None`'s wire format and the
721    /// decoding target every other algorithm falls back to.
722    fn serialize_event(&self, event: &NeuromorphicEvent<T>) -> Result<Vec<u8>> {
723        let mut data = Vec::new();
724        data.push(event.event_type as u8);
725        data.push(event.priority as u8);
726        write_uvarint(&mut data, event.source_neuron as u64);
727        match event.target_neuron {
728            Some(target) => {
729                data.push(1);
730                write_uvarint(&mut data, target as u64);
731            }
732            None => data.push(0),
733        }
734        write_ivarint(&mut data, field_to_fixed(event.timestamp));
735        write_ivarint(&mut data, field_to_fixed(event.value));
736        write_ivarint(&mut data, field_to_fixed(event.energy_cost));
737        Ok(data)
738    }
739
740    fn deserialize_event(&self, bytes: &[u8]) -> Result<NeuromorphicEvent<T>> {
741        let mut pos = 0usize;
742        let event_type = decode_event_type(*bytes.first().ok_or_else(truncated_bytes_err)?)?;
743        pos += 1;
744        let priority = decode_priority(*bytes.get(pos).ok_or_else(truncated_bytes_err)?)?;
745        pos += 1;
746        let source_neuron = read_uvarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)? as usize;
747        let has_target = *bytes.get(pos).ok_or_else(truncated_bytes_err)?;
748        pos += 1;
749        let target_neuron = if has_target == 1 {
750            Some(read_uvarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)? as usize)
751        } else {
752            None
753        };
754        let timestamp =
755            fixed_to_field(read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?);
756        let value = fixed_to_field(read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?);
757        let energy_cost =
758            fixed_to_field(read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?);
759        Ok(NeuromorphicEvent {
760            event_type,
761            timestamp,
762            source_neuron,
763            target_neuron,
764            value,
765            energy_cost,
766            priority,
767        })
768    }
769
770    /// Delta encoding (F57): every numeric field is written as its
771    /// varint-encoded difference from the previous compressed event's
772    /// corresponding field, rather than its absolute value. For a stream of
773    /// similar consecutive events (the common neuromorphic case — spikes
774    /// from nearby neurons close together in time) small deltas take far
775    /// fewer varint bytes than the absolute fixed-point values. The first
776    /// event in a stream has no baseline and is encoded as full deltas from
777    /// zero, which is exactly `serialize_event`'s absolute encoding.
778    fn delta_encode_event(&mut self, event: &NeuromorphicEvent<T>) -> Result<Vec<u8>> {
779        let ts = field_to_fixed(event.timestamp);
780        let src = event.source_neuron as i64;
781        let tgt = event.target_neuron.map(|t| t as i64);
782        let val = field_to_fixed(event.value);
783        let energy = field_to_fixed(event.energy_cost);
784
785        let (base_ts, base_src, base_tgt, base_val, base_energy) =
786            self.last_event_fields.unwrap_or((0, 0, None, 0, 0));
787
788        let mut data = Vec::new();
789        data.push(event.event_type as u8);
790        data.push(event.priority as u8);
791        write_ivarint(&mut data, src - base_src);
792        match (tgt, base_tgt) {
793            (Some(t), Some(b)) => {
794                data.push(1);
795                write_ivarint(&mut data, t - b);
796            }
797            (Some(t), None) => {
798                data.push(2); // "present, no prior baseline": encode absolute
799                write_ivarint(&mut data, t);
800            }
801            (None, _) => data.push(0),
802        }
803        write_ivarint(&mut data, ts - base_ts);
804        write_ivarint(&mut data, val - base_val);
805        write_ivarint(&mut data, energy - base_energy);
806
807        self.last_event_fields = Some((ts, src, tgt, val, energy));
808        Ok(data)
809    }
810
811    fn delta_decode_event(&mut self, bytes: &[u8]) -> Result<NeuromorphicEvent<T>> {
812        let mut pos = 0usize;
813        let event_type = decode_event_type(*bytes.first().ok_or_else(truncated_bytes_err)?)?;
814        pos += 1;
815        let priority = decode_priority(*bytes.get(pos).ok_or_else(truncated_bytes_err)?)?;
816        pos += 1;
817
818        let (base_ts, base_src, base_tgt, base_val, base_energy) =
819            self.last_event_fields.unwrap_or((0, 0, None, 0, 0));
820
821        let src = base_src + read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?;
822        let target_tag = *bytes.get(pos).ok_or_else(truncated_bytes_err)?;
823        pos += 1;
824        let tgt = match target_tag {
825            0 => None,
826            1 => {
827                let base = base_tgt.ok_or_else(|| {
828                    OptimError::InvalidConfig(
829                        "delta-encoded target references a missing baseline".to_string(),
830                    )
831                })?;
832                Some(base + read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?)
833            }
834            2 => Some(read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?),
835            other => {
836                return Err(OptimError::InvalidConfig(format!(
837                    "invalid delta-encoded target tag: {other}"
838                )))
839            }
840        };
841        let ts = base_ts + read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?;
842        let val = base_val + read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?;
843        let energy = base_energy + read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?;
844
845        self.last_event_fields = Some((ts, src, tgt, val, energy));
846
847        if src < 0 {
848            return Err(OptimError::InvalidConfig(
849                "delta-decoded source_neuron underflowed".to_string(),
850            ));
851        }
852        Ok(NeuromorphicEvent {
853            event_type,
854            timestamp: fixed_to_field(ts),
855            source_neuron: src as usize,
856            target_neuron: match tgt {
857                Some(t) if t >= 0 => Some(t as usize),
858                Some(_) => {
859                    return Err(OptimError::InvalidConfig(
860                        "delta-decoded target_neuron underflowed".to_string(),
861                    ))
862                }
863                None => None,
864            },
865            value: fixed_to_field(val),
866            energy_cost: fixed_to_field(energy),
867            priority,
868        })
869    }
870
871    /// Sparse encoding (F57): most events carry a zero/default `value` and
872    /// `energy_cost`, and most events have no `target_neuron` (broadcast
873    /// events). A leading bitmask flags which optional fields are present,
874    /// so all-zero/absent fields cost a single bit each instead of a full
875    /// varint.
876    fn sparse_encode_event(&mut self, event: &NeuromorphicEvent<T>) -> Result<Vec<u8>> {
877        let has_target = event.target_neuron.is_some();
878        let has_value = event.value != T::zero();
879        let has_energy = event.energy_cost != T::zero();
880
881        let mut mask = 0u8;
882        if has_target {
883            mask |= 0b001;
884        }
885        if has_value {
886            mask |= 0b010;
887        }
888        if has_energy {
889            mask |= 0b100;
890        }
891
892        let mut data = Vec::new();
893        data.push(event.event_type as u8);
894        data.push(event.priority as u8);
895        data.push(mask);
896        write_uvarint(&mut data, event.source_neuron as u64);
897        write_ivarint(&mut data, field_to_fixed(event.timestamp));
898        if let Some(target) = event.target_neuron {
899            write_uvarint(&mut data, target as u64);
900        }
901        if has_value {
902            write_ivarint(&mut data, field_to_fixed(event.value));
903        }
904        if has_energy {
905            write_ivarint(&mut data, field_to_fixed(event.energy_cost));
906        }
907        Ok(data)
908    }
909
910    fn sparse_decode_event(&mut self, bytes: &[u8]) -> Result<NeuromorphicEvent<T>> {
911        let mut pos = 0usize;
912        let event_type = decode_event_type(*bytes.first().ok_or_else(truncated_bytes_err)?)?;
913        pos += 1;
914        let priority = decode_priority(*bytes.get(pos).ok_or_else(truncated_bytes_err)?)?;
915        pos += 1;
916        let mask = *bytes.get(pos).ok_or_else(truncated_bytes_err)?;
917        pos += 1;
918        let source_neuron = read_uvarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)? as usize;
919        let timestamp =
920            fixed_to_field(read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?);
921        let target_neuron = if mask & 0b001 != 0 {
922            Some(read_uvarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)? as usize)
923        } else {
924            None
925        };
926        let value = if mask & 0b010 != 0 {
927            fixed_to_field(read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?)
928        } else {
929            T::zero()
930        };
931        let energy_cost = if mask & 0b100 != 0 {
932            fixed_to_field(read_ivarint(bytes, &mut pos).ok_or_else(truncated_bytes_err)?)
933        } else {
934            T::zero()
935        };
936        Ok(NeuromorphicEvent {
937            event_type,
938            timestamp,
939            source_neuron,
940            target_neuron,
941            value,
942            energy_cost,
943            priority,
944        })
945    }
946}
947
948/// A FIFO chain of *compressed* event frames for a single priority level.
949///
950/// F57 (wiring): compressed storage has to be decoded in the same order it
951/// was encoded, because the delta/predictive codecs carry a running baseline
952/// (`EventCompressionEngine::last_event_fields`) from one frame to the next.
953/// The event scheduler, on the other hand, must still honour
954/// [`EventPriority`]. Keeping one independent chain per priority level
955/// satisfies both constraints at once: inside a chain the order is strictly
956/// FIFO (so the delta baseline chain stays intact, matching the FIFO
957/// tie-break the uncompressed [`BinaryHeap`] path uses), and the scheduler
958/// just picks the highest-priority non-empty chain.
959///
960/// Two engines are held because the encoder necessarily runs ahead of the
961/// decoder by `frames.len()` events, so they cannot share one baseline.
962struct CompressedEventChain<T: Float + Debug + Send + Sync + 'static> {
963    /// Encoder state; advanced by [`Self::push`].
964    encoder: EventCompressionEngine<T>,
965    /// Decoder state; advanced by [`Self::pop`], trailing the encoder.
966    decoder: EventCompressionEngine<T>,
967    /// Compressed frames, oldest first.
968    frames: VecDeque<Vec<u8>>,
969    /// Bytes currently held by `frames`.
970    stored_bytes: usize,
971}
972
973impl<T: Float + Debug + Send + Sync + 'static> CompressedEventChain<T> {
974    fn new(algorithm: EventCompressionAlgorithm) -> Self {
975        Self {
976            encoder: EventCompressionEngine::new(algorithm),
977            decoder: EventCompressionEngine::new(algorithm),
978            frames: VecDeque::new(),
979            stored_bytes: 0,
980        }
981    }
982
983    /// Compress `event` and append the resulting frame. Returns the number
984    /// of bytes the frame occupies.
985    fn push(&mut self, event: &NeuromorphicEvent<T>) -> Result<usize> {
986        let frame = self.encoder.compress_event(event)?;
987        let frame_len = frame.len();
988        self.stored_bytes += frame_len;
989        self.frames.push_back(frame);
990        Ok(frame_len)
991    }
992
993    /// Decode and remove the oldest frame, reconstructing the event.
994    fn pop(&mut self) -> Result<Option<NeuromorphicEvent<T>>> {
995        match self.frames.pop_front() {
996            Some(frame) => {
997                self.stored_bytes = self.stored_bytes.saturating_sub(frame.len());
998                Ok(Some(self.decoder.decompress_event(&frame)?))
999            }
1000            None => Ok(None),
1001        }
1002    }
1003
1004    fn len(&self) -> usize {
1005        self.frames.len()
1006    }
1007
1008    fn is_empty(&self) -> bool {
1009        self.frames.is_empty()
1010    }
1011
1012    /// Drop every pending frame. The codec baselines are reset as well:
1013    /// discarding frames breaks the delta chain, so the encoder and decoder
1014    /// must both restart from "no baseline" to stay consistent.
1015    fn clear(&mut self) {
1016        self.frames.clear();
1017        self.stored_bytes = 0;
1018        self.encoder.last_event_fields = None;
1019        self.decoder.last_event_fields = None;
1020    }
1021}
1022
1023/// Adaptive event handler
1024struct AdaptiveEventHandler<T: Float + Debug + Send + Sync + 'static> {
1025    performance_history: VecDeque<T>,
1026    current_strategy: AdaptationStrategy,
1027}
1028
1029#[derive(Debug, Clone, Copy)]
1030enum AdaptationStrategy {
1031    Conservative,
1032    Balanced,
1033    Aggressive,
1034}
1035
1036impl<T: Float + Debug + Send + Sync + 'static + std::iter::Sum> AdaptiveEventHandler<T> {
1037    fn new() -> Self {
1038        Self {
1039            performance_history: VecDeque::new(),
1040            current_strategy: AdaptationStrategy::Balanced,
1041        }
1042    }
1043
1044    fn adapt_processing(&mut self, current_performance: T) {
1045        self.performance_history.push_back(current_performance);
1046
1047        if self.performance_history.len() > 100 {
1048            self.performance_history.pop_front();
1049        }
1050
1051        if self.performance_history.len() >= 10 {
1052            let recent_avg = self
1053                .performance_history
1054                .iter()
1055                .rev()
1056                .take(10)
1057                .cloned()
1058                .sum::<T>()
1059                / T::from(10).unwrap_or_else(|| T::zero());
1060            let older_avg = if self.performance_history.len() >= 20 {
1061                self.performance_history
1062                    .iter()
1063                    .rev()
1064                    .skip(10)
1065                    .take(10)
1066                    .cloned()
1067                    .sum::<T>()
1068                    / T::from(10).unwrap_or_else(|| T::zero())
1069            } else {
1070                recent_avg
1071            };
1072
1073            let performance_change = recent_avg - older_avg;
1074
1075            self.current_strategy =
1076                if performance_change > T::from(0.1).unwrap_or_else(|| T::zero()) {
1077                    AdaptationStrategy::Aggressive
1078                } else if performance_change < T::from(-0.1).unwrap_or_else(|| T::zero()) {
1079                    AdaptationStrategy::Conservative
1080                } else {
1081                    AdaptationStrategy::Balanced
1082                };
1083        }
1084    }
1085
1086    fn get_adaptation_factor(&self) -> T {
1087        match self.current_strategy {
1088            AdaptationStrategy::Conservative => T::from(0.5).unwrap_or_else(|| T::zero()),
1089            AdaptationStrategy::Balanced => T::one(),
1090            AdaptationStrategy::Aggressive => T::from(1.5).unwrap_or_else(|| T::zero()),
1091        }
1092    }
1093}
1094
1095/// Distributed event coordinator
1096struct DistributedEventCoordinator<T: Float + Debug + Send + Sync + 'static> {
1097    load_balancing: LoadBalancingStrategy,
1098    worker_loads: HashMap<usize, T>,
1099    current_worker: usize,
1100    total_workers: usize,
1101}
1102
1103impl<T: Float + Debug + Send + Sync + 'static> DistributedEventCoordinator<T> {
1104    fn new(strategy: LoadBalancingStrategy, num_workers: usize) -> Self {
1105        Self {
1106            load_balancing: strategy,
1107            worker_loads: HashMap::new(),
1108            current_worker: 0,
1109            total_workers: num_workers,
1110        }
1111    }
1112
1113    fn assign_worker(&mut self, event: &NeuromorphicEvent<T>) -> usize {
1114        match self.load_balancing {
1115            LoadBalancingStrategy::RoundRobin => {
1116                let worker = self.current_worker;
1117                self.current_worker = (self.current_worker + 1) % self.total_workers;
1118                worker
1119            }
1120            LoadBalancingStrategy::TypeBased => {
1121                // Hash event type to worker
1122                (event.event_type as usize) % self.total_workers
1123            }
1124            LoadBalancingStrategy::LoadAware => {
1125                // Find worker with minimum load
1126                self.worker_loads
1127                    .iter()
1128                    .min_by(|a, b| a.1.partial_cmp(b.1).unwrap_or(std::cmp::Ordering::Equal))
1129                    .map(|(&worker_id, _)| worker_id)
1130                    .unwrap_or(0)
1131            }
1132            _ => 0,
1133        }
1134    }
1135
1136    fn update_worker_load(&mut self, worker_id: usize, load: T) {
1137        self.worker_loads.insert(worker_id, load);
1138    }
1139
1140    /// Load currently recorded for `worker_id`.
1141    fn worker_load(&self, worker_id: usize) -> Option<T> {
1142        self.worker_loads.get(&worker_id).copied()
1143    }
1144
1145    /// All recorded worker loads, ascending by worker id.
1146    fn loads(&self) -> Vec<(usize, T)> {
1147        let mut loads: Vec<(usize, T)> = self
1148            .worker_loads
1149            .iter()
1150            .map(|(&worker, &load)| (worker, load))
1151            .collect();
1152        loads.sort_by_key(|(worker, _)| *worker);
1153        loads
1154    }
1155}
1156
1157impl<
1158        T: Float
1159            + Debug
1160            + Send
1161            + Sync
1162            + 'static
1163            + std::iter::Sum
1164            + scirs2_core::ndarray::ScalarOperand
1165            + std::ops::AddAssign,
1166    > EventDrivenOptimizer<T>
1167{
1168    /// Create a new event-driven optimizer
1169    pub fn new(
1170        config: EventDrivenConfig<T>,
1171        stdp_config: STDPConfig<T>,
1172        membrane_config: MembraneDynamicsConfig<T>,
1173        num_neurons: usize,
1174    ) -> Self {
1175        let mut optimizer = Self {
1176            config: config.clone(),
1177            stdp_config: stdp_config.clone(),
1178            membrane_config: membrane_config.clone(),
1179            event_queue: BinaryHeap::new(),
1180            event_stats: HashMap::new(),
1181            system_state: SystemState {
1182                membrane_potentials: Array1::from_elem(
1183                    num_neurons,
1184                    membrane_config.resting_potential,
1185                ),
1186                synaptic_weights: Array2::ones((num_neurons, num_neurons))
1187                    * T::from(0.1).unwrap_or_else(|| T::zero()),
1188                last_spike_times: Array1::from_elem(
1189                    num_neurons,
1190                    T::from(-1000.0).unwrap_or_else(|| T::zero()),
1191                ),
1192                refractory_until: Array1::zeros(num_neurons),
1193                current_time: T::zero(),
1194                active_neurons: HashSet::new(),
1195                pending_updates: HashMap::new(),
1196            },
1197            event_handlers: HashMap::new(),
1198            correlation_tracker: TemporalCorrelationTracker::new(config.correlation_window),
1199            rate_limiter: EventRateLimiter::new(config.rate_limits.clone()),
1200            metrics: NeuromorphicMetrics::default(),
1201            distributed_coordinator: if config.distributed_processing {
1202                Some(DistributedEventCoordinator::new(config.load_balancing, 4))
1203            } else {
1204                None
1205            },
1206            compression_engine: EventCompressionEngine::new(config.compression_algorithm),
1207            compressed_chains: BTreeMap::new(),
1208            compression_raw_bytes: 0,
1209            compression_compressed_bytes: 0,
1210            adaptive_handler: AdaptiveEventHandler::new(),
1211        };
1212
1213        // Register default event handlers
1214        optimizer.register_default_handlers();
1215
1216        optimizer
1217    }
1218
1219    /// Register default event handlers
1220    fn register_default_handlers(&mut self) {
1221        let spike_handler = Box::new(SpikeEventHandler {
1222            stdp_config: self.stdp_config.clone(),
1223            membrane_config: self.membrane_config.clone(),
1224        });
1225
1226        let weight_handler = Box::new(WeightUpdateEventHandler {
1227            stdp_config: self.stdp_config.clone(),
1228        });
1229
1230        self.event_handlers.insert(EventType::Spike, spike_handler);
1231        self.event_handlers
1232            .insert(EventType::WeightUpdate, weight_handler);
1233    }
1234
1235    /// Add event to the processing queue.
1236    ///
1237    /// With `config.event_compression` enabled the event is compressed to
1238    /// bytes *here* and only those bytes are retained (F57): the live queue
1239    /// really does store compressed frames instead of whole events, and the
1240    /// event is reconstructed lazily in `Self::pop_next_event` when it is
1241    /// about to be processed.
1242    pub fn enqueue_event(&mut self, event: NeuromorphicEvent<T>) -> Result<()> {
1243        // Check rate limits
1244        if !self.rate_limiter.can_process(event.event_type) {
1245            return Err(OptimError::InvalidConfig("Rate limit exceeded".to_string()));
1246        }
1247
1248        // Check queue capacity (both stores count towards the budget, so the
1249        // capacity check keeps working when compression is enabled).
1250        if self.get_queue_size() >= self.config.max_queue_size {
1251            return Err(OptimError::InvalidConfig("Event queue full".to_string()));
1252        }
1253
1254        // Extract event fields before moving
1255        let timestamp = event.timestamp;
1256        let event_type = event.event_type;
1257        let source_neuron = event.source_neuron;
1258
1259        if self.config.event_compression {
1260            let raw_bytes = self.compression_engine.serialize_event(&event)?.len();
1261            let algorithm = self.config.compression_algorithm;
1262            let chain = self
1263                .compressed_chains
1264                .entry(event.priority)
1265                .or_insert_with(|| CompressedEventChain::new(algorithm));
1266            let compressed_bytes = chain.push(&event)?;
1267            self.compression_raw_bytes += raw_bytes;
1268            self.compression_compressed_bytes += compressed_bytes;
1269        } else {
1270            self.event_queue.push(PriorityEventEntry {
1271                event,
1272                insertion_time: Instant::now(),
1273            });
1274        }
1275
1276        // Update correlation tracking
1277        if self.config.temporal_correlation {
1278            self.correlation_tracker
1279                .add_event(timestamp, event_type, source_neuron);
1280        }
1281
1282        Ok(())
1283    }
1284
1285    /// Take the next event to process, highest priority first.
1286    ///
1287    /// Compressed frames are drained before the in-memory heap. The two
1288    /// stores only ever hold events simultaneously if
1289    /// `config.event_compression` was toggled while events were queued; in
1290    /// that case the compressed frames (which were necessarily enqueued
1291    /// while compression was on) are the older ones, so draining them first
1292    /// preserves arrival order across the switch.
1293    fn pop_next_event(&mut self) -> Result<Option<NeuromorphicEvent<T>>> {
1294        // `BTreeMap` iterates in ascending key order and `EventPriority`
1295        // derives `Ord` lowest-first, so scan in reverse for the
1296        // highest-priority non-empty chain.
1297        let highest = self
1298            .compressed_chains
1299            .iter()
1300            .rev()
1301            .find(|(_, chain)| !chain.is_empty())
1302            .map(|(&priority, _)| priority);
1303        if let Some(priority) = highest {
1304            if let Some(chain) = self.compressed_chains.get_mut(&priority) {
1305                return chain.pop();
1306            }
1307        }
1308        Ok(self.event_queue.pop().map(|entry| entry.event))
1309    }
1310
1311    /// Whether any event (compressed or not) is still waiting.
1312    fn has_pending_events(&self) -> bool {
1313        !self.event_queue.is_empty()
1314            || self
1315                .compressed_chains
1316                .values()
1317                .any(|chain| !chain.is_empty())
1318    }
1319
1320    /// Process events from the queue
1321    pub fn process_events(&mut self) -> Result<usize> {
1322        let mut processed_count = 0;
1323        let start_time = Instant::now();
1324        let timeout =
1325            Duration::from_millis(self.config.processing_timeout.to_u64().unwrap_or(1000));
1326
1327        while self.has_pending_events() && start_time.elapsed() < timeout {
1328            if self.config.event_batching {
1329                let batch_size = self.adaptive_batch_size().min(self.get_queue_size());
1330                let batch_processed = self.process_event_batch(batch_size)?;
1331                if batch_processed == 0 {
1332                    break;
1333                }
1334                processed_count += batch_processed;
1335            } else if let Some(event) = self.pop_next_event()? {
1336                self.assign_event_to_worker(&event);
1337                self.process_single_event(&event)?;
1338                processed_count += 1;
1339            } else {
1340                break;
1341            }
1342        }
1343
1344        // Apply pending weight updates
1345        self.apply_pending_updates()?;
1346
1347        // Update adaptive processing. An interval the monotonic clock reports
1348        // as exactly zero carries no rate information at all, so no sample is
1349        // recorded for it: the previous code divided by that zero (and
1350        // `expect`ed the conversion), poisoning `performance_history` with
1351        // `inf` whenever a batch completed inside one clock tick.
1352        let elapsed_secs = start_time.elapsed().as_secs_f64();
1353        if elapsed_secs > 0.0 {
1354            let processing_rate = to_generic_or(processed_count as f64 / elapsed_secs, T::zero());
1355            self.adaptive_handler.adapt_processing(processing_rate);
1356        }
1357
1358        Ok(processed_count)
1359    }
1360
1361    /// Process a batch of events
1362    fn process_event_batch(&mut self, batch_size: usize) -> Result<usize> {
1363        let mut batch_events = Vec::with_capacity(batch_size);
1364
1365        // Collect batch events
1366        for _ in 0..batch_size {
1367            match self.pop_next_event()? {
1368                Some(event) => batch_events.push(event),
1369                None => break,
1370            }
1371        }
1372
1373        // Process batch
1374        for event in &batch_events {
1375            self.assign_event_to_worker(event);
1376            self.process_single_event(event)?;
1377        }
1378
1379        Ok(batch_events.len())
1380    }
1381
1382    /// Process a single event
1383    fn process_single_event(&mut self, event: &NeuromorphicEvent<T>) -> Result<()> {
1384        let start_time = Instant::now();
1385
1386        // Find appropriate handler
1387        if let Some(handler) = self.event_handlers.get_mut(&event.event_type) {
1388            handler.handle_event(event, &mut self.system_state)?;
1389        } else {
1390            // Default handling
1391            self.default_event_handling(event)?;
1392        }
1393
1394        // Update statistics
1395        let processing_time = start_time.elapsed().as_nanos() as f64 / 1_000_000.0;
1396        self.update_event_statistics(
1397            event.event_type,
1398            T::from(processing_time).unwrap_or_else(|| T::zero()),
1399        );
1400
1401        // Update energy consumption
1402        self.metrics.energy_consumption += event.energy_cost;
1403
1404        Ok(())
1405    }
1406
1407    /// Default event handling
1408    fn default_event_handling(&mut self, event: &NeuromorphicEvent<T>) -> Result<()> {
1409        match event.event_type {
1410            EventType::ExternalStimulus
1411                // Apply external stimulus to neuron
1412                if event.source_neuron < self.system_state.membrane_potentials.len() => {
1413                    self.system_state.membrane_potentials[event.source_neuron] += event.value;
1414                }
1415            EventType::TimerEvent => {
1416                // Update system time
1417                self.system_state.current_time = event.timestamp;
1418            }
1419            _ => {
1420                // Ignore unknown events
1421            }
1422        }
1423
1424        Ok(())
1425    }
1426
1427    /// Apply pending weight updates
1428    fn apply_pending_updates(&mut self) -> Result<()> {
1429        for ((pre, post), weight_change) in self.system_state.pending_updates.drain() {
1430            if pre < self.system_state.synaptic_weights.nrows()
1431                && post < self.system_state.synaptic_weights.ncols()
1432            {
1433                let current_weight = self.system_state.synaptic_weights[[pre, post]];
1434                let new_weight = (current_weight + weight_change)
1435                    .max(self.stdp_config.weight_min)
1436                    .min(self.stdp_config.weight_max);
1437
1438                self.system_state.synaptic_weights[[pre, post]] = new_weight;
1439            }
1440        }
1441
1442        Ok(())
1443    }
1444
1445    /// Update event processing statistics
1446    fn update_event_statistics(&mut self, event_type: EventType, processing_time: T) {
1447        let stats = self
1448            .event_stats
1449            .entry(event_type)
1450            .or_insert_with(|| EventStatistics {
1451                total_processed: 0,
1452                avg_processing_time: T::zero(),
1453                event_rate: T::zero(),
1454                avg_queue_wait_time: T::zero(),
1455                error_count: 0,
1456                last_update: Instant::now(),
1457            });
1458
1459        stats.total_processed += 1;
1460
1461        // Update average processing _time using exponential moving average
1462        let alpha = T::from(0.1).unwrap_or_else(|| T::zero());
1463        stats.avg_processing_time =
1464            stats.avg_processing_time * (T::one() - alpha) + processing_time * alpha;
1465
1466        // Update event rate
1467        let time_since_last = stats.last_update.elapsed().as_secs_f64();
1468        if time_since_last > 0.0 {
1469            let current_rate = T::one() / T::from(time_since_last).unwrap_or_else(|| T::zero());
1470            stats.event_rate = stats.event_rate * (T::one() - alpha) + current_rate * alpha;
1471        }
1472
1473        stats.last_update = Instant::now();
1474    }
1475
1476    /// Get event processing statistics
1477    pub fn get_event_statistics(&self) -> &HashMap<EventType, EventStatistics<T>> {
1478        &self.event_stats
1479    }
1480
1481    /// Get current system state
1482    pub fn get_system_state(&self) -> &SystemState<T> {
1483        &self.system_state
1484    }
1485
1486    /// Get current metrics
1487    pub fn get_metrics(&self) -> &NeuromorphicMetrics<T> {
1488        &self.metrics
1489    }
1490
1491    /// Clear event queue, including any compressed frames still pending.
1492    pub fn clear_event_queue(&mut self) {
1493        self.event_queue.clear();
1494        for chain in self.compressed_chains.values_mut() {
1495            chain.clear();
1496        }
1497    }
1498
1499    /// Get queue size: events waiting in the in-memory priority queue plus
1500    /// events waiting as compressed frames.
1501    pub fn get_queue_size(&self) -> usize {
1502        self.event_queue.len()
1503            + self
1504                .compressed_chains
1505                .values()
1506                .map(|chain| chain.len())
1507                .sum::<usize>()
1508    }
1509
1510    /// Bytes currently occupied by the compressed event queue (F57). Zero
1511    /// while `config.event_compression` is disabled, since nothing is stored
1512    /// in compressed form then.
1513    pub fn compressed_queue_bytes(&self) -> usize {
1514        self.compressed_chains
1515            .values()
1516            .map(|chain| chain.stored_bytes)
1517            .sum()
1518    }
1519
1520    /// Cumulative `(uncompressed_bytes, compressed_bytes)` measured across
1521    /// every event that has entered the compressed queue since construction.
1522    /// The uncompressed figure is the size the same event occupies under the
1523    /// reference (`EventCompressionAlgorithm::None`) codec, so the pair is a
1524    /// direct measurement of what compression actually achieved.
1525    pub fn compression_statistics(&self) -> (usize, usize) {
1526        (
1527            self.compression_raw_bytes,
1528            self.compression_compressed_bytes,
1529        )
1530    }
1531
1532    /// Achieved compression ratio (`compressed / uncompressed`); `None` until
1533    /// at least one event has been compressed.
1534    pub fn compression_ratio(&self) -> Option<f64> {
1535        if self.compression_raw_bytes == 0 {
1536            None
1537        } else {
1538            Some(self.compression_compressed_bytes as f64 / self.compression_raw_bytes as f64)
1539        }
1540    }
1541
1542    /// The most recent *measured* event-processing rate (events per second)
1543    /// recorded by [`Self::process_events`], or `None` if no interval long
1544    /// enough to be measurable has been observed yet. Always finite: the
1545    /// previous implementation divided the processed count by an integer
1546    /// millisecond count, which is zero for any batch finishing inside one
1547    /// clock tick, feeding `inf`/`NaN` into the adaptive handler.
1548    pub fn last_measured_event_rate(&self) -> Option<T> {
1549        self.adaptive_handler.performance_history.back().copied()
1550    }
1551
1552    /// The adaptation factor currently chosen by the adaptive event handler
1553    /// from the measured event-processing rate history (0.5 while throughput
1554    /// is degrading, 1.0 while it is stable, 1.5 while it is improving).
1555    pub fn current_adaptation_factor(&self) -> T {
1556        self.adaptive_handler.get_adaptation_factor()
1557    }
1558
1559    /// Effective per-iteration batch size: the configured `batch_size` scaled
1560    /// by [`Self::current_adaptation_factor`], so a system whose measured
1561    /// throughput is improving takes larger bites and one that is degrading
1562    /// takes smaller ones. Never zero.
1563    pub fn adaptive_batch_size(&self) -> usize {
1564        let factor = self.current_adaptation_factor().to_f64().unwrap_or(1.0);
1565        let scaled = (self.config.batch_size as f64 * factor).round();
1566        if scaled.is_finite() && scaled >= 1.0 {
1567            scaled as usize
1568        } else {
1569            self.config.batch_size.max(1)
1570        }
1571    }
1572
1573    /// Route an event to a worker under the configured load-balancing policy
1574    /// and record the resulting load, when distributed processing is enabled.
1575    ///
1576    /// The coordinator used to be constructed from `distributed_processing` and
1577    /// then never consulted, so `LoadBalancingStrategy` selected nothing and no
1578    /// worker load was ever tracked.
1579    fn assign_event_to_worker(&mut self, event: &NeuromorphicEvent<T>) {
1580        let Some(coordinator) = self.distributed_coordinator.as_mut() else {
1581            return;
1582        };
1583        let worker = coordinator.assign_worker(event);
1584        let current = coordinator.worker_load(worker).unwrap_or_else(T::zero);
1585        coordinator.update_worker_load(worker, current + T::one());
1586    }
1587
1588    /// Measured temporal correlation between two event types, as accumulated by
1589    /// the correlation tracker over the configured correlation window.
1590    ///
1591    /// The tracker genuinely computes and stores these strengths; before this
1592    /// accessor existed there was no way to read any of them back.
1593    pub fn event_correlation(&self, first: EventType, second: EventType) -> T {
1594        self.correlation_tracker.get_correlation(first, second)
1595    }
1596
1597    /// Per-worker event counts recorded by the distributed coordinator, or
1598    /// `None` when distributed processing is disabled.
1599    pub fn worker_loads(&self) -> Option<Vec<(usize, T)>> {
1600        self.distributed_coordinator
1601            .as_ref()
1602            .map(|coordinator| coordinator.loads())
1603    }
1604
1605    /// Enable distributed processing
1606    pub fn enable_distributed_processing(&mut self, num_workers: usize) {
1607        self.distributed_coordinator = Some(DistributedEventCoordinator::new(
1608            self.config.load_balancing,
1609            num_workers,
1610        ));
1611        self.config.distributed_processing = true;
1612    }
1613
1614    /// Disable distributed processing
1615    pub fn disable_distributed_processing(&mut self) {
1616        self.distributed_coordinator = None;
1617        self.config.distributed_processing = false;
1618    }
1619}
1620
1621impl<T: Float + Debug + Send + Sync + 'static> Default for EventStatistics<T> {
1622    fn default() -> Self {
1623        Self {
1624            total_processed: 0,
1625            avg_processing_time: T::zero(),
1626            event_rate: T::zero(),
1627            avg_queue_wait_time: T::zero(),
1628            error_count: 0,
1629            last_update: Instant::now(),
1630        }
1631    }
1632}
1633
1634/// Regression tests for the F57 *wiring* (compressed live queue), kept in
1635/// their own file so `event_driven.rs` stays under the 2000-line policy.
1636#[cfg(test)]
1637#[path = "event_driven_compression_tests.rs"]
1638mod compression_wiring_tests;
1639
1640#[cfg(test)]
1641mod f57_compression_tests {
1642    use super::*;
1643
1644    fn sample_events() -> Vec<NeuromorphicEvent<f64>> {
1645        vec![
1646            NeuromorphicEvent {
1647                event_type: EventType::Spike,
1648                timestamp: 12.5,
1649                source_neuron: 3,
1650                target_neuron: Some(7),
1651                value: 1.0,
1652                energy_cost: 0.05,
1653                priority: EventPriority::High,
1654            },
1655            NeuromorphicEvent {
1656                event_type: EventType::WeightUpdate,
1657                timestamp: 12.6,
1658                source_neuron: 4,
1659                target_neuron: Some(8),
1660                value: -0.25,
1661                energy_cost: 0.02,
1662                priority: EventPriority::Normal,
1663            },
1664            NeuromorphicEvent {
1665                event_type: EventType::TimerEvent,
1666                timestamp: 100.0,
1667                source_neuron: 0,
1668                target_neuron: None,
1669                value: 0.0,
1670                energy_cost: 0.0,
1671                priority: EventPriority::Low,
1672            },
1673            NeuromorphicEvent {
1674                event_type: EventType::EnergyEvent,
1675                timestamp: 0.0,
1676                source_neuron: 42,
1677                target_neuron: Some(1),
1678                value: 999.75,
1679                energy_cost: -3.5,
1680                priority: EventPriority::RealTime,
1681            },
1682        ]
1683    }
1684
1685    fn assert_events_close(a: &NeuromorphicEvent<f64>, b: &NeuromorphicEvent<f64>) {
1686        assert_eq!(a.event_type, b.event_type);
1687        assert_eq!(a.priority, b.priority);
1688        assert_eq!(a.source_neuron, b.source_neuron);
1689        assert_eq!(a.target_neuron, b.target_neuron);
1690        assert!(
1691            (a.timestamp - b.timestamp).abs() < 1e-3,
1692            "timestamp mismatch: {} vs {}",
1693            a.timestamp,
1694            b.timestamp
1695        );
1696        assert!(
1697            (a.value - b.value).abs() < 1e-3,
1698            "value mismatch: {} vs {}",
1699            a.value,
1700            b.value
1701        );
1702        assert!(
1703            (a.energy_cost - b.energy_cost).abs() < 1e-3,
1704            "energy_cost mismatch: {} vs {}",
1705            a.energy_cost,
1706            b.energy_cost
1707        );
1708    }
1709
1710    /// F57: `EventCompressionAlgorithm::None` must round-trip every field,
1711    /// and must not be the fixed-size all-zero buffer the previous
1712    /// delta/sparse stubs returned regardless of algorithm choice.
1713    #[test]
1714    fn none_algorithm_round_trips_all_fields() {
1715        let mut engine = EventCompressionEngine::<f64>::new(EventCompressionAlgorithm::None);
1716        for event in sample_events() {
1717            let bytes = engine.compress_event(&event).expect("compress");
1718            let decoded = engine.decompress_event(&bytes).expect("decompress");
1719            assert_events_close(&event, &decoded);
1720        }
1721    }
1722
1723    /// F57: delta encoding must round-trip a whole stream of events in
1724    /// order (each event's baseline is the previously *decoded* event),
1725    /// including the target-neuron presence/absence transitions.
1726    #[test]
1727    fn delta_encoding_round_trips_event_stream() {
1728        let mut encoder =
1729            EventCompressionEngine::<f64>::new(EventCompressionAlgorithm::DeltaEncoding);
1730        let mut decoder =
1731            EventCompressionEngine::<f64>::new(EventCompressionAlgorithm::DeltaEncoding);
1732
1733        for event in sample_events() {
1734            let bytes = encoder.compress_event(&event).expect("compress");
1735            let decoded = decoder.decompress_event(&bytes).expect("decompress");
1736            assert_events_close(&event, &decoded);
1737        }
1738    }
1739
1740    /// F57: delta-encoded bytes for a stream of *similar* consecutive
1741    /// events (the case delta encoding exists to exploit) must be smaller
1742    /// than the same events' absolute (`None`) encoding — otherwise the
1743    /// "compression" is not actually compressing anything.
1744    #[test]
1745    fn delta_encoding_is_smaller_for_similar_events() {
1746        let mut none_engine = EventCompressionEngine::<f64>::new(EventCompressionAlgorithm::None);
1747        let mut delta_engine =
1748            EventCompressionEngine::<f64>::new(EventCompressionAlgorithm::DeltaEncoding);
1749
1750        let mut none_total = 0usize;
1751        let mut delta_total = 0usize;
1752        for i in 0..20 {
1753            let event = NeuromorphicEvent {
1754                event_type: EventType::Spike,
1755                timestamp: 1000.0 + i as f64 * 0.1,
1756                source_neuron: 50 + (i % 3),
1757                target_neuron: Some(100 + (i % 2)),
1758                value: 0.5,
1759                energy_cost: 0.01,
1760                priority: EventPriority::Normal,
1761            };
1762            none_total += none_engine.compress_event(&event).expect("compress").len();
1763            delta_total += delta_engine.compress_event(&event).expect("compress").len();
1764        }
1765        assert!(
1766            delta_total < none_total,
1767            "delta encoding was not smaller for a similar-event stream: \
1768             delta={delta_total} bytes, none={none_total} bytes"
1769        );
1770    }
1771
1772    /// F57: sparse encoding must round-trip events with all-default
1773    /// optional fields (no target, zero value/energy) as well as events
1774    /// that set every optional field.
1775    #[test]
1776    fn sparse_encoding_round_trips_default_and_full_events() {
1777        let mut engine =
1778            EventCompressionEngine::<f64>::new(EventCompressionAlgorithm::SparseEncoding);
1779        for event in sample_events() {
1780            let bytes = engine.compress_event(&event).expect("compress");
1781            let decoded = engine.decompress_event(&bytes).expect("decompress");
1782            assert_events_close(&event, &decoded);
1783        }
1784    }
1785
1786    /// F57: decoding truncated/corrupt bytes must return an honest `Err`,
1787    /// never panic (the varint readers use `?`/`ok_or_else` throughout,
1788    /// never indexing or unwrapping directly).
1789    #[test]
1790    fn decoding_truncated_bytes_is_an_error_not_a_panic() {
1791        let mut engine = EventCompressionEngine::<f64>::new(EventCompressionAlgorithm::None);
1792        let event = sample_events().remove(0);
1793        let bytes = engine.compress_event(&event).expect("compress");
1794
1795        for len in 0..bytes.len() {
1796            let mut fresh_engine =
1797                EventCompressionEngine::<f64>::new(EventCompressionAlgorithm::None);
1798            let result = fresh_engine.decompress_event(&bytes[..len]);
1799            assert!(
1800                result.is_err(),
1801                "truncating to {len} bytes should be a decode error, got {result:?}"
1802            );
1803        }
1804    }
1805}