1use crate::error::{SignalError, SignalResult};
13use scirs2_core::numeric::Complex64;
14use std::f64::consts::PI;
15
16pub trait ArrayGeometry: Send + Sync {
22 fn steering_vector(&self, angle_rad: f64, wavelength: f64) -> SignalResult<Vec<Complex64>>;
30
31 fn n_elements(&self) -> usize;
33
34 fn element_positions(&self) -> Vec<(f64, f64)>;
36
37 fn steering_vector_normalized(&self, angle_rad: f64) -> SignalResult<Vec<Complex64>> {
41 self.steering_vector(angle_rad, 1.0)
42 }
43}
44
45#[derive(Debug, Clone)]
56pub struct UniformLinearArray {
57 n_elements: usize,
59 element_spacing: f64,
61}
62
63impl UniformLinearArray {
64 pub fn new(n_elements: usize, element_spacing: f64) -> SignalResult<Self> {
71 if n_elements < 2 {
72 return Err(SignalError::ValueError(
73 "ULA requires at least 2 elements".to_string(),
74 ));
75 }
76 if element_spacing <= 0.0 {
77 return Err(SignalError::ValueError(
78 "Element spacing must be positive".to_string(),
79 ));
80 }
81 Ok(Self {
82 n_elements,
83 element_spacing,
84 })
85 }
86
87 pub fn element_spacing(&self) -> f64 {
89 self.element_spacing
90 }
91
92 pub fn steering_matrix(
94 &self,
95 angles_rad: &[f64],
96 wavelength: f64,
97 ) -> SignalResult<Vec<Vec<Complex64>>> {
98 angles_rad
99 .iter()
100 .map(|&theta| self.steering_vector(theta, wavelength))
101 .collect()
102 }
103}
104
105impl ArrayGeometry for UniformLinearArray {
106 fn steering_vector(&self, angle_rad: f64, wavelength: f64) -> SignalResult<Vec<Complex64>> {
107 if wavelength <= 0.0 {
108 return Err(SignalError::ValueError(
109 "Wavelength must be positive".to_string(),
110 ));
111 }
112 let phase_increment = -2.0 * PI * self.element_spacing * angle_rad.sin() / wavelength;
113 let sv = (0..self.n_elements)
114 .map(|m| {
115 let phase = phase_increment * m as f64;
116 Complex64::new(phase.cos(), phase.sin())
117 })
118 .collect();
119 Ok(sv)
120 }
121
122 fn n_elements(&self) -> usize {
123 self.n_elements
124 }
125
126 fn element_positions(&self) -> Vec<(f64, f64)> {
127 (0..self.n_elements)
128 .map(|m| (self.element_spacing * m as f64, 0.0))
129 .collect()
130 }
131}
132
133#[derive(Debug, Clone)]
145pub struct UniformCircularArray {
146 n_elements: usize,
148 radius: f64,
150}
151
152impl UniformCircularArray {
153 pub fn new(n_elements: usize, radius: f64) -> SignalResult<Self> {
160 if n_elements < 3 {
161 return Err(SignalError::ValueError(
162 "UCA requires at least 3 elements".to_string(),
163 ));
164 }
165 if radius <= 0.0 {
166 return Err(SignalError::ValueError(
167 "Radius must be positive".to_string(),
168 ));
169 }
170 Ok(Self { n_elements, radius })
171 }
172
173 pub fn radius(&self) -> f64 {
175 self.radius
176 }
177}
178
179impl ArrayGeometry for UniformCircularArray {
180 fn steering_vector(&self, angle_rad: f64, wavelength: f64) -> SignalResult<Vec<Complex64>> {
181 if wavelength <= 0.0 {
182 return Err(SignalError::ValueError(
183 "Wavelength must be positive".to_string(),
184 ));
185 }
186 let n = self.n_elements;
187 let sv = (0..n)
188 .map(|m| {
189 let element_angle = 2.0 * PI * m as f64 / n as f64;
190 let phase = 2.0 * PI * self.radius * (angle_rad - element_angle).cos() / wavelength;
191 Complex64::new(phase.cos(), phase.sin())
192 })
193 .collect();
194 Ok(sv)
195 }
196
197 fn n_elements(&self) -> usize {
198 self.n_elements
199 }
200
201 fn element_positions(&self) -> Vec<(f64, f64)> {
202 let n = self.n_elements;
203 (0..n)
204 .map(|m| {
205 let angle = 2.0 * PI * m as f64 / n as f64;
206 (self.radius * angle.cos(), self.radius * angle.sin())
207 })
208 .collect()
209 }
210}
211
212#[derive(Debug, Clone)]
226pub struct ArbitraryArray {
227 positions: Vec<(f64, f64)>,
229}
230
231impl ArbitraryArray {
232 pub fn new(positions: Vec<(f64, f64)>) -> SignalResult<Self> {
238 if positions.len() < 2 {
239 return Err(SignalError::ValueError(
240 "Arbitrary array requires at least 2 elements".to_string(),
241 ));
242 }
243 Ok(Self { positions })
244 }
245
246 pub fn steering_vector_2d(
250 &self,
251 azimuth_rad: f64,
252 elevation_rad: f64,
253 wavelength: f64,
254 ) -> SignalResult<Vec<Complex64>> {
255 if wavelength <= 0.0 {
256 return Err(SignalError::ValueError(
257 "Wavelength must be positive".to_string(),
258 ));
259 }
260 let cos_el = elevation_rad.cos();
261 let kx = 2.0 * PI * azimuth_rad.cos() * cos_el / wavelength;
262 let ky = 2.0 * PI * azimuth_rad.sin() * cos_el / wavelength;
263
264 let sv = self
265 .positions
266 .iter()
267 .map(|&(x, y)| {
268 let phase = -(kx * x + ky * y);
269 Complex64::new(phase.cos(), phase.sin())
270 })
271 .collect();
272 Ok(sv)
273 }
274}
275
276impl ArrayGeometry for ArbitraryArray {
277 fn steering_vector(&self, angle_rad: f64, wavelength: f64) -> SignalResult<Vec<Complex64>> {
278 if wavelength <= 0.0 {
279 return Err(SignalError::ValueError(
280 "Wavelength must be positive".to_string(),
281 ));
282 }
283 let sv = self
285 .positions
286 .iter()
287 .map(|&(x, _y)| {
288 let phase = -2.0 * PI * x * angle_rad.sin() / wavelength;
289 Complex64::new(phase.cos(), phase.sin())
290 })
291 .collect();
292 Ok(sv)
293 }
294
295 fn n_elements(&self) -> usize {
296 self.positions.len()
297 }
298
299 fn element_positions(&self) -> Vec<(f64, f64)> {
300 self.positions.clone()
301 }
302}
303
304#[derive(Debug, Clone)]
310pub struct ArrayManifoldData {
311 pub steering_vectors: Vec<Vec<Complex64>>,
313 pub scan_angles: Vec<f64>,
315 pub n_elements: usize,
317}
318
319impl ArrayManifoldData {
320 pub fn compute(
328 array: &dyn ArrayGeometry,
329 scan_angles_rad: &[f64],
330 wavelength: f64,
331 ) -> SignalResult<Self> {
332 if scan_angles_rad.is_empty() {
333 return Err(SignalError::ValueError(
334 "Scan angles must not be empty".to_string(),
335 ));
336 }
337 let mut steering_vectors = Vec::with_capacity(scan_angles_rad.len());
338 for &angle in scan_angles_rad {
339 steering_vectors.push(array.steering_vector(angle, wavelength)?);
340 }
341 Ok(Self {
342 steering_vectors,
343 scan_angles: scan_angles_rad.to_vec(),
344 n_elements: array.n_elements(),
345 })
346 }
347}
348
349pub fn scan_angles_degrees(
361 start_deg: f64,
362 end_deg: f64,
363 n_points: usize,
364) -> SignalResult<Vec<f64>> {
365 if n_points == 0 {
366 return Err(SignalError::ValueError(
367 "Number of scan points must be positive".to_string(),
368 ));
369 }
370 if n_points == 1 {
371 return Ok(vec![start_deg.to_radians()]);
372 }
373 let step = (end_deg - start_deg) / (n_points - 1) as f64;
374 Ok((0..n_points)
375 .map(|i| (start_deg + step * i as f64).to_radians())
376 .collect())
377}
378
379pub fn steering_vector_ula(
387 n_elements: usize,
388 angle_rad: f64,
389 element_spacing: f64,
390) -> SignalResult<Vec<Complex64>> {
391 if n_elements == 0 {
392 return Err(SignalError::ValueError(
393 "Number of elements must be positive".to_string(),
394 ));
395 }
396 if element_spacing <= 0.0 {
397 return Err(SignalError::ValueError(
398 "Element spacing must be positive".to_string(),
399 ));
400 }
401 let phase_increment = -2.0 * PI * element_spacing * angle_rad.sin();
402 let sv = (0..n_elements)
403 .map(|m| {
404 let phase = phase_increment * m as f64;
405 Complex64::new(phase.cos(), phase.sin())
406 })
407 .collect();
408 Ok(sv)
409}
410
411pub fn steering_vectors_ula(
413 n_elements: usize,
414 angles_rad: &[f64],
415 element_spacing: f64,
416) -> SignalResult<Vec<Vec<Complex64>>> {
417 if angles_rad.is_empty() {
418 return Err(SignalError::ValueError(
419 "Angle list must not be empty".to_string(),
420 ));
421 }
422 angles_rad
423 .iter()
424 .map(|&angle| steering_vector_ula(n_elements, angle, element_spacing))
425 .collect()
426}
427
428pub fn estimate_covariance(signals: &[Vec<Complex64>]) -> SignalResult<Vec<Vec<Complex64>>> {
436 if signals.is_empty() {
437 return Err(SignalError::ValueError(
438 "Signal matrix must not be empty".to_string(),
439 ));
440 }
441 let n_elements = signals.len();
442 let n_snapshots = signals[0].len();
443 if n_snapshots == 0 {
444 return Err(SignalError::ValueError(
445 "Number of snapshots must be positive".to_string(),
446 ));
447 }
448 for (idx, sig) in signals.iter().enumerate() {
449 if sig.len() != n_snapshots {
450 return Err(SignalError::DimensionMismatch(format!(
451 "Element {} has {} snapshots, expected {}",
452 idx,
453 sig.len(),
454 n_snapshots
455 )));
456 }
457 }
458
459 let mut cov = vec![vec![Complex64::new(0.0, 0.0); n_elements]; n_elements];
460 for i in 0..n_elements {
461 for j in 0..n_elements {
462 let mut sum = Complex64::new(0.0, 0.0);
463 for k in 0..n_snapshots {
464 sum += signals[i][k] * signals[j][k].conj();
465 }
466 cov[i][j] = sum / n_snapshots as f64;
467 }
468 }
469 Ok(cov)
470}
471
472pub fn estimate_covariance_real(signals: &[Vec<f64>]) -> SignalResult<Vec<Vec<Complex64>>> {
474 let complex_signals: Vec<Vec<Complex64>> = signals
475 .iter()
476 .map(|ch| ch.iter().map(|&x| Complex64::new(x, 0.0)).collect())
477 .collect();
478 estimate_covariance(&complex_signals)
479}
480
481pub(crate) fn invert_hermitian_matrix(
487 matrix: &[Vec<Complex64>],
488) -> SignalResult<Vec<Vec<Complex64>>> {
489 let n = matrix.len();
490 if n == 0 {
491 return Err(SignalError::ValueError(
492 "Matrix must not be empty".to_string(),
493 ));
494 }
495
496 let mut aug: Vec<Vec<Complex64>> = Vec::with_capacity(n);
497 for i in 0..n {
498 if matrix[i].len() != n {
499 return Err(SignalError::DimensionMismatch(format!(
500 "Matrix row {} has length {}, expected {}",
501 i,
502 matrix[i].len(),
503 n
504 )));
505 }
506 let mut row = Vec::with_capacity(2 * n);
507 row.extend_from_slice(&matrix[i]);
508 for j in 0..n {
509 if i == j {
510 row.push(Complex64::new(1.0, 0.0));
511 } else {
512 row.push(Complex64::new(0.0, 0.0));
513 }
514 }
515 aug.push(row);
516 }
517
518 for col in 0..n {
519 let mut max_row = col;
520 let mut max_val = aug[col][col].norm();
521 for row in (col + 1)..n {
522 let val = aug[row][col].norm();
523 if val > max_val {
524 max_val = val;
525 max_row = row;
526 }
527 }
528 if max_val < 1e-14 {
529 return Err(SignalError::ComputationError(
530 "Matrix is singular or near-singular".to_string(),
531 ));
532 }
533 aug.swap(col, max_row);
534
535 let pivot = aug[col][col];
536 let pivot_inv = Complex64::new(1.0, 0.0) / pivot;
537 for j in 0..(2 * n) {
538 aug[col][j] = aug[col][j] * pivot_inv;
539 }
540
541 for row in 0..n {
542 if row == col {
543 continue;
544 }
545 let factor = aug[row][col];
546 for j in 0..(2 * n) {
547 aug[row][j] = aug[row][j] - factor * aug[col][j];
548 }
549 }
550 }
551
552 let mut inverse = Vec::with_capacity(n);
553 for i in 0..n {
554 inverse.push(aug[i][n..(2 * n)].to_vec());
555 }
556 Ok(inverse)
557}
558
559pub(crate) fn mat_vec_mul(matrix: &[Vec<Complex64>], vec: &[Complex64]) -> Vec<Complex64> {
561 let m = matrix.len();
562 let mut result = vec![Complex64::new(0.0, 0.0); m];
563 for i in 0..m {
564 for (j, &v) in vec.iter().enumerate() {
565 result[i] += matrix[i][j] * v;
566 }
567 }
568 result
569}
570
571pub(crate) fn inner_product_conj(a: &[Complex64], b: &[Complex64]) -> Complex64 {
573 a.iter()
574 .zip(b.iter())
575 .fold(Complex64::new(0.0, 0.0), |acc, (&ai, &bi)| {
576 acc + ai.conj() * bi
577 })
578}
579
580#[cfg(test)]
585mod tests {
586 use super::*;
587 use approx::assert_relative_eq;
588
589 #[test]
590 fn test_ula_steering_vector_broadside() {
591 let ula = UniformLinearArray::new(4, 0.5).expect("should create ULA");
592 let sv = ula
593 .steering_vector(0.0, 1.0)
594 .expect("should compute steering vector");
595 assert_eq!(sv.len(), 4);
596 for s in &sv {
597 assert_relative_eq!(s.re, 1.0, epsilon = 1e-12);
598 assert_relative_eq!(s.im, 0.0, epsilon = 1e-12);
599 }
600 }
601
602 #[test]
603 fn test_ula_steering_vector_unit_norm() {
604 let ula = UniformLinearArray::new(8, 0.5).expect("should create ULA");
605 let sv = ula
606 .steering_vector(0.3, 1.0)
607 .expect("should compute steering vector");
608 for s in &sv {
609 assert_relative_eq!(s.norm(), 1.0, epsilon = 1e-12);
610 }
611 }
612
613 #[test]
614 fn test_ula_element_positions() {
615 let ula = UniformLinearArray::new(4, 0.5).expect("should create ULA");
616 let pos = ula.element_positions();
617 assert_eq!(pos.len(), 4);
618 assert_relative_eq!(pos[0].0, 0.0, epsilon = 1e-12);
619 assert_relative_eq!(pos[1].0, 0.5, epsilon = 1e-12);
620 assert_relative_eq!(pos[3].0, 1.5, epsilon = 1e-12);
621 }
622
623 #[test]
624 fn test_ula_validation() {
625 assert!(UniformLinearArray::new(0, 0.5).is_err());
626 assert!(UniformLinearArray::new(1, 0.5).is_err());
627 assert!(UniformLinearArray::new(4, 0.0).is_err());
628 assert!(UniformLinearArray::new(4, -0.5).is_err());
629 }
630
631 #[test]
632 fn test_uca_steering_vector() {
633 let uca = UniformCircularArray::new(8, 1.0).expect("should create UCA");
634 let sv = uca
635 .steering_vector(0.0, 1.0)
636 .expect("should compute steering vector");
637 assert_eq!(sv.len(), 8);
638 for s in &sv {
639 assert_relative_eq!(s.norm(), 1.0, epsilon = 1e-12);
640 }
641 }
642
643 #[test]
644 fn test_uca_symmetry() {
645 let uca = UniformCircularArray::new(8, 1.0).expect("should create UCA");
646 let sv = uca
648 .steering_vector(0.0, 1.0)
649 .expect("should compute steering vector");
650 assert_relative_eq!(sv[1].norm(), sv[7].norm(), epsilon = 1e-10);
652 assert_relative_eq!((sv[1] * sv[7].conj()).im.abs(), 0.0, epsilon = 0.1);
654 }
655
656 #[test]
657 fn test_uca_validation() {
658 assert!(UniformCircularArray::new(2, 1.0).is_err());
659 assert!(UniformCircularArray::new(4, 0.0).is_err());
660 }
661
662 #[test]
663 fn test_arbitrary_array() {
664 let positions = vec![(0.0, 0.0), (0.5, 0.0), (1.0, 0.0), (1.5, 0.0)];
666 let arr = ArbitraryArray::new(positions).expect("should create arbitrary array");
667 assert_eq!(arr.n_elements(), 4);
668
669 let sv = arr
670 .steering_vector(0.0, 1.0)
671 .expect("should compute steering vector");
672 for s in &sv {
674 assert_relative_eq!(s.re, 1.0, epsilon = 1e-12);
675 assert_relative_eq!(s.im, 0.0, epsilon = 1e-12);
676 }
677 }
678
679 #[test]
680 fn test_arbitrary_array_validation() {
681 assert!(ArbitraryArray::new(vec![(0.0, 0.0)]).is_err());
682 }
683
684 #[test]
685 fn test_array_manifold() {
686 let ula = UniformLinearArray::new(4, 0.5).expect("should create ULA");
687 let angles = scan_angles_degrees(-90.0, 90.0, 181).expect("should create angles");
688 let manifold =
689 ArrayManifoldData::compute(&ula, &angles, 1.0).expect("should compute manifold");
690 assert_eq!(manifold.steering_vectors.len(), 181);
691 assert_eq!(manifold.n_elements, 4);
692 }
693
694 #[test]
695 fn test_scan_angles_degrees() {
696 let angles = scan_angles_degrees(-90.0, 90.0, 181).expect("should create angles");
697 assert_eq!(angles.len(), 181);
698 assert_relative_eq!(angles[0], -PI / 2.0, epsilon = 1e-10);
699 assert_relative_eq!(angles[180], PI / 2.0, epsilon = 1e-10);
700 }
701
702 #[test]
703 fn test_scan_angles_single() {
704 let angles = scan_angles_degrees(30.0, 30.0, 1).expect("should create single angle");
705 assert_eq!(angles.len(), 1);
706 assert_relative_eq!(angles[0], 30.0_f64.to_radians(), epsilon = 1e-10);
707 }
708
709 #[test]
710 fn test_scan_angles_validation() {
711 assert!(scan_angles_degrees(-90.0, 90.0, 0).is_err());
712 }
713
714 #[test]
715 fn test_covariance_hermitian() {
716 let signals = vec![
717 vec![Complex64::new(1.0, 0.5), Complex64::new(0.3, -0.2)],
718 vec![Complex64::new(-0.5, 0.1), Complex64::new(0.8, 0.4)],
719 ];
720 let cov = estimate_covariance(&signals).expect("should compute covariance");
721 for i in 0..cov.len() {
722 for j in 0..cov.len() {
723 assert_relative_eq!(cov[i][j].re, cov[j][i].re, epsilon = 1e-12);
724 assert_relative_eq!(cov[i][j].im, -cov[j][i].im, epsilon = 1e-12);
725 }
726 }
727 }
728
729 #[test]
730 fn test_covariance_real() {
731 let signals = vec![vec![1.0, 2.0, 3.0], vec![4.0, 5.0, 6.0]];
732 let cov = estimate_covariance_real(&signals).expect("should compute covariance");
733 assert_eq!(cov.len(), 2);
734 for row in &cov {
735 for &val in row {
736 assert!(val.im.abs() < 1e-12);
737 }
738 }
739 }
740
741 #[test]
742 fn test_covariance_validation() {
743 assert!(estimate_covariance(&[]).is_err());
744 assert!(estimate_covariance(&[vec![]]).is_err());
745 }
746
747 #[test]
748 fn test_invert_identity() {
749 let n = 3;
750 let mut identity = vec![vec![Complex64::new(0.0, 0.0); n]; n];
751 for i in 0..n {
752 identity[i][i] = Complex64::new(1.0, 0.0);
753 }
754 let inv = invert_hermitian_matrix(&identity).expect("should invert");
755 for i in 0..n {
756 for j in 0..n {
757 if i == j {
758 assert_relative_eq!(inv[i][j].re, 1.0, epsilon = 1e-10);
759 } else {
760 assert!(inv[i][j].norm() < 1e-10);
761 }
762 }
763 }
764 }
765}