sarpyx 0.1.5__py3-none-any.whl → 0.1.6__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (48) hide show
  1. docs/examples/advanced/batch_processing.py +1 -1
  2. docs/examples/advanced/custom_processing_chains.py +1 -1
  3. docs/examples/advanced/performance_optimization.py +1 -1
  4. docs/examples/basic/snap_integration.py +1 -1
  5. docs/examples/intermediate/quality_assessment.py +1 -1
  6. outputs/baseline/20260205-234828/__init__.py +33 -0
  7. outputs/baseline/20260205-234828/main.py +493 -0
  8. outputs/final/20260205-234851/__init__.py +33 -0
  9. outputs/final/20260205-234851/main.py +493 -0
  10. sarpyx/__init__.py +2 -2
  11. sarpyx/algorithms/__init__.py +2 -2
  12. sarpyx/cli/__init__.py +1 -1
  13. sarpyx/cli/focus.py +3 -5
  14. sarpyx/cli/main.py +106 -7
  15. sarpyx/cli/shipdet.py +1 -1
  16. sarpyx/cli/worldsar.py +549 -0
  17. sarpyx/processor/__init__.py +1 -1
  18. sarpyx/processor/core/decode.py +43 -8
  19. sarpyx/processor/core/focus.py +104 -57
  20. sarpyx/science/__init__.py +1 -1
  21. sarpyx/sla/__init__.py +8 -0
  22. sarpyx/sla/metrics.py +101 -0
  23. sarpyx/{snap → snapflow}/__init__.py +1 -1
  24. sarpyx/snapflow/engine.py +6165 -0
  25. sarpyx/{snap → snapflow}/op.py +0 -1
  26. sarpyx/utils/__init__.py +1 -1
  27. sarpyx/utils/geos.py +652 -0
  28. sarpyx/utils/grid.py +285 -0
  29. sarpyx/utils/io.py +77 -9
  30. sarpyx/utils/meta.py +55 -0
  31. sarpyx/utils/nisar_utils.py +652 -0
  32. sarpyx/utils/rfigen.py +108 -0
  33. sarpyx/utils/wkt_utils.py +109 -0
  34. sarpyx/utils/zarr_utils.py +55 -37
  35. {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/METADATA +9 -5
  36. {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/RECORD +41 -32
  37. {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/WHEEL +1 -1
  38. sarpyx-0.1.6.dist-info/licenses/LICENSE +201 -0
  39. sarpyx-0.1.6.dist-info/top_level.txt +4 -0
  40. tests/test_zarr_compat.py +35 -0
  41. sarpyx/processor/core/decode_v0.py +0 -0
  42. sarpyx/processor/core/decode_v1.py +0 -849
  43. sarpyx/processor/core/focus_old.py +0 -1550
  44. sarpyx/processor/core/focus_v1.py +0 -1566
  45. sarpyx/processor/core/focus_v2.py +0 -1625
  46. sarpyx/snap/engine.py +0 -633
  47. sarpyx-0.1.5.dist-info/top_level.txt +0 -2
  48. {sarpyx-0.1.5.dist-info → sarpyx-0.1.6.dist-info}/entry_points.txt +0 -0
@@ -35,7 +35,7 @@ from ..utils.viz import dump
35
35
 
36
36
 
37
37
  # ---------- Global settings ----------
38
- environ['OMP_NUM_THREADS'] = '12' # Set OpenMP threads for parallel processing
38
+ environ['OMP_NUM_THREADS'] = '16' # Set OpenMP threads for parallel processing
39
39
  __VTIMING__ = False
40
40
 
41
41
 
@@ -529,20 +529,17 @@ class CoarseRDA:
529
529
  else:
530
530
  raise ValueError(f'Backend {self._backend} not supported')
531
531
 
532
- # Verify dimensions are preserved
533
- expected_shape = (self.len_az_line, self.len_range_line)
534
- if self.radar_data.shape != expected_shape:
535
- raise RuntimeError(f'FFT changed radar data shape from {expected_shape} to {self.radar_data.shape}')
536
-
532
+ # Note: Range dimension is padded for linear convolution, so shape will differ from original
537
533
  if self._verbose:
538
534
  print(f'FFT output data shape: {self.radar_data.shape}')
539
535
  print('- FFT performed successfully!')
540
536
  print_memory()
541
537
 
542
538
  def _fft2d_numpy_efficient(self) -> None:
543
- """Perform memory-efficient 2D FFT using NumPy backend preserving original dimensions.
539
+ """Perform memory-efficient 2D FFT using NumPy backend with zero-padding for linear convolution.
544
540
 
545
541
  Uses in-place operations and memory cleanup for better efficiency.
542
+ Pads along range axis to enable linear convolution instead of circular convolution.
546
543
  """
547
544
  # Store original shape for verification
548
545
  original_shape = self.radar_data.shape
@@ -555,51 +552,96 @@ class CoarseRDA:
555
552
  print('Making data contiguous...')
556
553
  self.radar_data = np.ascontiguousarray(self.radar_data)
557
554
 
558
- # FFT each range line (axis=1) - EXACT SAME as original
555
+ # Calculate padding for linear convolution along range axis
556
+ len_range_line = self.radar_data.shape[1]
557
+ required_fft_size = len_range_line + self.num_tx_vals - 1
558
+ # Round up to next power of 2 for FFT efficiency
559
+ range_fft_size = int(2**np.ceil(np.log2(required_fft_size)))
560
+
561
+ # Safety check: ensure FFT size is at least the required size
562
+ assert range_fft_size >= required_fft_size, \
563
+ f'FFT size {range_fft_size} is smaller than required {required_fft_size}'
564
+
559
565
  if self._verbose:
560
- print(f'Performing FFT along range dimension (axis=1)...')
566
+ print(f'Range line length: {len_range_line}')
567
+ print(f'TX replica length: {self.num_tx_vals}')
568
+ print(f'Required FFT size for linear convolution: {required_fft_size}')
569
+ print(f'Padded FFT size (power of 2): {range_fft_size}')
570
+
571
+ # Zero-pad along range axis to enable linear convolution
572
+ pad_width = range_fft_size - len_range_line
573
+ assert pad_width >= 0, f'Negative padding width {pad_width} calculated'
561
574
 
562
- # Use same approach as original - no dtype changes
563
- self.radar_data = np.fft.fft(self.radar_data, axis=1)
575
+ self.radar_data = np.pad(self.radar_data, ((0, 0), (0, pad_width)), mode='constant', constant_values=0)
576
+
577
+ if self._verbose:
578
+ print(f'Padded radar data shape: {self.radar_data.shape}')
579
+
580
+ # FFT each range line (axis=1) with explicit size for linear convolution
581
+ if self._verbose:
582
+ print(f'Performing FFT along range dimension (axis=1) with size {range_fft_size}...')
583
+
584
+ self.radar_data = np.fft.fft(self.radar_data, n=range_fft_size, axis=1)
564
585
 
565
586
  if self._verbose:
566
587
  print(f'First FFT along range dimension completed, shape: {self.radar_data.shape}')
567
588
  print_memory()
568
589
 
569
- # FFT each azimuth line (axis=0) with fftshift - EXACT SAME as original
590
+ # FFT each azimuth line (axis=0) with fftshift
570
591
  if self._verbose:
571
592
  print(f'Performing FFT along azimuth dimension (axis=0) with fftshift...')
572
593
 
573
- # Use same approach as original
574
594
  self.radar_data = np.fft.fftshift(np.fft.fft(self.radar_data, axis=0), axes=0)
575
595
 
576
596
  if self._verbose:
577
597
  print(f'Second FFT along azimuth dimension completed, shape: {self.radar_data.shape}')
578
598
  print_memory()
579
-
580
- # Verify shape preservation
581
- assert self.radar_data.shape == original_shape, \
582
- f'FFT changed shape from {original_shape} to {self.radar_data.shape}'
583
599
 
584
600
  def _fft2d_torch_efficient(self) -> None:
585
- """Perform memory-efficient 2D FFT using PyTorch backend preserving dimensions.
601
+ """Perform memory-efficient 2D FFT using PyTorch backend with zero-padding for linear convolution.
586
602
 
587
603
  Uses in-place operations where possible.
604
+ Pads along range axis to enable linear convolution instead of circular convolution.
588
605
  """
589
606
  original_shape = self.radar_data.shape
590
607
 
591
608
  if self._verbose:
592
- print('Performing memory-efficient PyTorch FFT...')
609
+ print('Performing memory-efficient PyTorch FFT with linear convolution padding...')
593
610
  print_memory()
594
611
 
595
- # FFT each range line (axis=1) - in-place when possible
612
+ # Calculate padding for linear convolution along range axis
613
+ len_range_line = self.radar_data.shape[1]
614
+ required_fft_size = len_range_line + self.num_tx_vals - 1
615
+ # Round up to next power of 2 for FFT efficiency
616
+ range_fft_size = int(2**np.ceil(np.log2(required_fft_size)))
617
+
618
+ # Safety check: ensure FFT size is at least the required size
619
+ assert range_fft_size >= required_fft_size, \
620
+ f'FFT size {range_fft_size} is smaller than required {required_fft_size}'
621
+
622
+ if self._verbose:
623
+ print(f'Range line length: {len_range_line}')
624
+ print(f'TX replica length: {self.num_tx_vals}')
625
+ print(f'Required FFT size for linear convolution: {required_fft_size}')
626
+ print(f'Padded FFT size (power of 2): {range_fft_size}')
627
+
628
+ # Zero-pad along range axis
629
+ pad_width = range_fft_size - len_range_line
630
+ assert pad_width >= 0, f'Negative padding width {pad_width} calculated'
631
+
632
+ self.radar_data = torch.nn.functional.pad(self.radar_data, (0, pad_width), mode='constant', value=0)
633
+
634
+ if self._verbose:
635
+ print(f'Padded radar data shape: {self.radar_data.shape}')
636
+
637
+ # FFT each range line (axis=1) with explicit size - in-place when possible
596
638
  if self._memory_efficient:
597
- temp = torch.fft.fft(self.radar_data, dim=1)
639
+ temp = torch.fft.fft(self.radar_data, n=range_fft_size, dim=1)
598
640
  self.radar_data.copy_(temp)
599
641
  del temp
600
642
  torch.cuda.empty_cache() if torch.cuda.is_available() else None
601
643
  else:
602
- self.radar_data = torch.fft.fft(self.radar_data, dim=1)
644
+ self.radar_data = torch.fft.fft(self.radar_data, n=range_fft_size, dim=1)
603
645
 
604
646
  # FFT each azimuth line (axis=0) with fftshift
605
647
  if self._memory_efficient:
@@ -616,21 +658,29 @@ class CoarseRDA:
616
658
  torch.fft.fft(self.radar_data, dim=0),
617
659
  dim=0
618
660
  )
619
-
620
- # Verify shape preservation
621
- assert self.radar_data.shape == original_shape, \
622
- f'Torch FFT changed shape from {original_shape} to {self.radar_data.shape}'
623
661
 
624
662
  @flush_mem
625
663
  @timing_decorator
626
664
  def ifft_range(self) -> None:
627
- """Perform memory-efficient inverse FFT along range dimension."""
665
+ """Perform memory-efficient inverse FFT along range dimension and trim to original size."""
628
666
  if self._backend == 'numpy':
629
- # Use EXACT SAME approach as original
667
+ # Inverse FFT along range with the padded size
630
668
  self.radar_data = np.fft.ifftshift(np.fft.ifft(self.radar_data, axis=1), axes=1)
669
+
670
+ # Trim back to original range dimension (linear convolution complete)
671
+ self.radar_data = self.radar_data[:, :self.len_range_line]
672
+
673
+ if self._verbose:
674
+ print(f'Trimmed radar data back to original range dimension: {self.radar_data.shape}')
631
675
  elif self._backend == 'torch':
632
676
  self.radar_data = torch.fft.ifft(self.radar_data, dim=1)
633
677
  self.radar_data = torch.fft.ifftshift(self.radar_data, dim=1)
678
+
679
+ # Trim back to original range dimension
680
+ self.radar_data = self.radar_data[:, :self.len_range_line]
681
+
682
+ if self._verbose:
683
+ print(f'Trimmed radar data back to original range dimension: {self.radar_data.shape}')
634
684
  else:
635
685
  raise ValueError(f'Unsupported backend: {self._backend}')
636
686
 
@@ -1379,10 +1429,10 @@ class CoarseRDA:
1379
1429
  print(f'Raw radar data shape: {self.raw_data.shape}')
1380
1430
  print_memory()
1381
1431
  # ------------------------------------------------------------------------
1382
- # Step 1: 2D FFT transformation (preserves dimensions)
1432
+ # Step 1: 2D FFT transformation (pads range dimension for linear convolution)
1383
1433
  self.fft2d()
1384
- assert self.radar_data.shape == initial_shape, \
1385
- f'FFT changed data shape from {initial_shape} to {self.radar_data.shape}'
1434
+ if self._verbose:
1435
+ print(f'FFT completed, radar data shape (padded): {self.radar_data.shape}')
1386
1436
  # ------------------------------------------------------------------------
1387
1437
 
1388
1438
  # Step 2: Range compression
@@ -1414,7 +1464,7 @@ class CoarseRDA:
1414
1464
  """Perform memory-efficient range compression step.
1415
1465
 
1416
1466
  This method applies the range compression filter to compress the radar
1417
- signal in the range dimension while preserving data dimensions.
1467
+ signal in the range dimension. Range dimension is padded for linear convolution.
1418
1468
 
1419
1469
  Raises:
1420
1470
  RuntimeError: If data dimensions change unexpectedly during processing.
@@ -1432,14 +1482,14 @@ class CoarseRDA:
1432
1482
  original_w = initial_shape[1]
1433
1483
 
1434
1484
  if self._verbose:
1435
- print(f'Processing with original_w={original_w}')
1485
+ print(f'Processing with padded range dimension={original_w}')
1436
1486
 
1437
1487
  # Perform range compression
1438
1488
  self._perform_range_compression_efficient(w_pad, original_w)
1439
1489
 
1440
- # Verify dimensions are preserved
1441
- assert self.radar_data.shape == initial_shape, \
1442
- f'Range compression changed data shape from {initial_shape} to {self.radar_data.shape}'
1490
+ # Verify azimuth dimension is preserved (range may be padded)
1491
+ assert self.radar_data.shape[0] == initial_shape[0], \
1492
+ f'Range compression changed azimuth dimension from {initial_shape[0]} to {self.radar_data.shape[0]}'
1443
1493
 
1444
1494
  if self._verbose:
1445
1495
  print(f'Range compression completed successfully!')
@@ -1452,7 +1502,7 @@ class CoarseRDA:
1452
1502
  """Perform memory-efficient Range Cell Migration Correction.
1453
1503
 
1454
1504
  This method applies the RCMC filter to correct for range cell migration
1455
- effects and performs inverse FFT in the range dimension.
1505
+ effects and performs inverse FFT in the range dimension (trimming back to original size).
1456
1506
 
1457
1507
  Raises:
1458
1508
  RuntimeError: If data dimensions change unexpectedly during processing.
@@ -1462,15 +1512,13 @@ class CoarseRDA:
1462
1512
  print(f'Input radar data shape: {self.radar_data.shape}')
1463
1513
  print_memory()
1464
1514
 
1465
- # Store initial shape for verification
1466
- initial_shape = self.radar_data.shape
1467
-
1468
- # Perform RCMC
1515
+ # Perform RCMC (includes ifft_range which trims to original size)
1469
1516
  self._perform_rcmc_efficient()
1470
1517
 
1471
- # Verify dimensions are preserved
1472
- assert self.radar_data.shape == initial_shape, \
1473
- f'RCMC changed data shape from {initial_shape} to {self.radar_data.shape}'
1518
+ # After RCMC and ifft_range, data should be back to original dimensions
1519
+ expected_shape = (self.len_az_line, self.len_range_line)
1520
+ assert self.radar_data.shape == expected_shape, \
1521
+ f'RCMC resulted in unexpected shape {self.radar_data.shape}, expected {expected_shape}'
1474
1522
 
1475
1523
  if self._verbose:
1476
1524
  print(f'RCMC completed successfully!')
@@ -1495,6 +1543,11 @@ class CoarseRDA:
1495
1543
 
1496
1544
  # Store initial shape for verification
1497
1545
  initial_shape = self.radar_data.shape
1546
+ expected_shape = (self.len_az_line, self.len_range_line)
1547
+
1548
+ # Verify we have original dimensions after RCMC
1549
+ assert initial_shape == expected_shape, \
1550
+ f'Unexpected input shape {initial_shape}, expected {expected_shape}'
1498
1551
 
1499
1552
  # Perform azimuth compression
1500
1553
  self._perform_azimuth_compression_efficient()
@@ -1509,43 +1562,37 @@ class CoarseRDA:
1509
1562
  print_memory()
1510
1563
 
1511
1564
  def _perform_range_compression_efficient(self, w_pad: int, original_w: int) -> None:
1512
- """Perform memory-efficient range compression step while preserving data dimensions.
1565
+ """Perform memory-efficient range compression step with padded dimensions.
1513
1566
 
1514
1567
  Args:
1515
- w_pad: Width padding for FFT length optimization.
1516
- original_w: Original width (for verification).
1568
+ w_pad: Width padding (unused, kept for interface compatibility).
1569
+ original_w: Original padded width from FFT.
1517
1570
 
1518
1571
  Raises:
1519
1572
  ValueError: If array shapes are incompatible.
1520
- AssertionError: If dimensions change unexpectedly.
1521
1573
  """
1522
1574
  if self._verbose:
1523
1575
  print(f'Starting memory-efficient range compression...')
1524
- print(f'Radar data shape: {self.radar_data.shape}')
1576
+ print(f'Radar data shape (padded): {self.radar_data.shape}')
1525
1577
  print_memory()
1526
1578
 
1527
1579
  # Store original shape for verification
1528
1580
  original_shape = self.radar_data.shape
1529
- expected_shape = (self.len_az_line, self.len_range_line)
1530
-
1531
- # Verify we still have expected dimensions
1532
- assert original_shape == expected_shape, \
1533
- f'Unexpected radar data shape: {original_shape}, expected: {expected_shape}'
1534
1581
 
1535
- # Get range filter with matching dimensions
1582
+ # Get range filter with matching padded dimensions
1536
1583
  range_filter = self.get_range_filter()
1537
1584
 
1538
1585
  if self._verbose:
1539
1586
  print(f'Range filter shape: {range_filter.shape}')
1540
1587
  print(f'Applying range compression filter...')
1541
1588
 
1542
- # Apply range compression filter - USE SAME METHOD AS ORIGINAL
1589
+ # Apply range compression filter
1543
1590
  self.radar_data = multiply(self.radar_data, range_filter)
1544
1591
 
1545
1592
  # Cleanup filter
1546
1593
  cleanup_variables(range_filter)
1547
1594
 
1548
- # Verify dimensions are preserved
1595
+ # Verify dimensions are preserved during multiplication
1549
1596
  assert self.radar_data.shape == original_shape, \
1550
1597
  f'Range compression changed data shape from {original_shape} to {self.radar_data.shape}'
1551
1598
 
@@ -6,4 +6,4 @@ and processing applications.
6
6
 
7
7
  __all__ = []
8
8
 
9
- __version__ = '0.1.0'
9
+ __version__ = '0.1.6'
sarpyx/sla/__init__.py CHANGED
@@ -6,12 +6,20 @@ including handler utilities and analysis tools.
6
6
  """
7
7
 
8
8
  from .core import SubLookAnalysis, Handler
9
+ from . import metrics
10
+ from .metrics import enl, interlook_coherence, dispersion_ratio, phase_variance, stack_metrics
9
11
  # Import utility functions if they exist in utilis.py
10
12
  # from .utilis import delete, unzip, delProd, command_line, iterNodes
11
13
 
12
14
  __all__ = [
13
15
  'SubLookAnalysis',
14
16
  'Handler',
17
+ 'metrics',
18
+ 'enl',
19
+ 'interlook_coherence',
20
+ 'dispersion_ratio',
21
+ 'phase_variance',
22
+ 'stack_metrics',
15
23
  # Uncomment these when utilis.py functions are properly imported
16
24
  # 'delete',
17
25
  # 'unzip',
sarpyx/sla/metrics.py ADDED
@@ -0,0 +1,101 @@
1
+ """Sub-look metrics.
2
+
3
+ Example:
4
+ from srp.sarpyx.sla.metrics import stack_metrics
5
+ """
6
+
7
+ from __future__ import annotations
8
+
9
+ import numpy as np
10
+
11
+
12
+ def _intensity(x: np.ndarray) -> np.ndarray:
13
+ return np.abs(x) ** 2 if np.iscomplexobj(x) else np.asarray(x)
14
+
15
+
16
+ def enl(x: np.ndarray, axis=None, eps: float = 1e-12) -> np.ndarray:
17
+ """Computes equivalent number of looks (ENL).
18
+
19
+ Args:
20
+ x (np.ndarray): Intensity or complex samples.
21
+ axis: Axis of looks.
22
+ eps (float): Small value to avoid division by zero.
23
+
24
+ Returns:
25
+ np.ndarray: ENL estimate.
26
+ """
27
+ i = _intensity(x)
28
+ m = np.mean(i, axis=axis)
29
+ v = np.var(i, axis=axis)
30
+ return (m * m) / (v + eps)
31
+
32
+
33
+ def dispersion_ratio(x: np.ndarray, axis=None, eps: float = 1e-12) -> np.ndarray:
34
+ """Computes dispersion ratio (normalized variance).
35
+
36
+ Args:
37
+ x (np.ndarray): Intensity or complex samples.
38
+ axis: Axis of looks.
39
+ eps (float): Small value to avoid division by zero.
40
+
41
+ Returns:
42
+ np.ndarray: Dispersion ratio.
43
+ """
44
+ i = _intensity(x)
45
+ m = np.mean(i, axis=axis)
46
+ v = np.var(i, axis=axis)
47
+ return v / (m * m + eps)
48
+
49
+
50
+ def interlook_coherence(a: np.ndarray, b: np.ndarray, axis=None, eps: float = 1e-12) -> np.ndarray:
51
+ """Computes inter-look coherence between two complex looks.
52
+
53
+ Args:
54
+ a (np.ndarray): First complex look.
55
+ b (np.ndarray): Second complex look.
56
+ axis: Averaging axis.
57
+ eps (float): Small value to avoid division by zero.
58
+
59
+ Returns:
60
+ np.ndarray: Coherence magnitude in [0, 1].
61
+ """
62
+ num = np.abs(np.mean(a * np.conj(b), axis=axis))
63
+ den = np.sqrt(np.mean(np.abs(a) ** 2, axis=axis) * np.mean(np.abs(b) ** 2, axis=axis))
64
+ return num / (den + eps)
65
+
66
+
67
+ def phase_variance(x: np.ndarray, axis=None) -> np.ndarray:
68
+ """Computes circular phase variance.
69
+
70
+ Args:
71
+ x (np.ndarray): Complex samples.
72
+ axis: Axis of looks.
73
+
74
+ Returns:
75
+ np.ndarray: Circular phase variance in [0, 1].
76
+ """
77
+ ph = np.angle(x)
78
+ return 1.0 - np.abs(np.mean(np.exp(1j * ph), axis=axis))
79
+
80
+
81
+ def stack_metrics(stack: np.ndarray, look_axis: int = 0, pair=(0, 1), eps: float = 1e-12):
82
+ """Computes all metrics from a sub-look stack.
83
+
84
+ Args:
85
+ stack (np.ndarray): Complex sub-look stack.
86
+ look_axis (int): Axis of looks.
87
+ pair (tuple[int, int]): Look indices for coherence.
88
+ eps (float): Small value to avoid division by zero.
89
+
90
+ Returns:
91
+ tuple[np.ndarray, np.ndarray, np.ndarray, np.ndarray]: ENL, coherence,
92
+ dispersion ratio, phase variance.
93
+ """
94
+ s = np.moveaxis(stack, look_axis, 0)
95
+ a, b = s[pair[0]], s[pair[1]]
96
+ return (
97
+ enl(s, axis=0, eps=eps),
98
+ interlook_coherence(a, b, axis=0, eps=eps),
99
+ dispersion_ratio(s, axis=0, eps=eps),
100
+ phase_variance(s, axis=0),
101
+ )
@@ -6,4 +6,4 @@ for SAR data processing workflows.
6
6
 
7
7
  __all__ = []
8
8
 
9
- __version__ = '0.1.0'
9
+ __version__ = '0.1.6'