fastsar 0.1.0__tar.gz

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 (61) hide show
  1. fastsar-0.1.0/CHANGELOG.md +123 -0
  2. fastsar-0.1.0/CONTRIBUTING.md +75 -0
  3. fastsar-0.1.0/LICENSE +21 -0
  4. fastsar-0.1.0/MANIFEST.in +4 -0
  5. fastsar-0.1.0/PKG-INFO +121 -0
  6. fastsar-0.1.0/README.md +80 -0
  7. fastsar-0.1.0/examples/chain.py +76 -0
  8. fastsar-0.1.0/examples/form_capella_stripmap.py +64 -0
  9. fastsar-0.1.0/examples/form_umbra.py +33 -0
  10. fastsar-0.1.0/fastsar/__init__.py +16 -0
  11. fastsar-0.1.0/fastsar/_build.py +59 -0
  12. fastsar-0.1.0/fastsar/api.py +487 -0
  13. fastsar-0.1.0/fastsar/autofocus.py +136 -0
  14. fastsar-0.1.0/fastsar/bp.py +356 -0
  15. fastsar-0.1.0/fastsar/burst.py +265 -0
  16. fastsar-0.1.0/fastsar/cphd.py +212 -0
  17. fastsar-0.1.0/fastsar/exact.py +596 -0
  18. fastsar-0.1.0/fastsar/ffbp.py +516 -0
  19. fastsar-0.1.0/fastsar/ffbp2.py +924 -0
  20. fastsar-0.1.0/fastsar/ffbp_cpu.cpp +363 -0
  21. fastsar-0.1.0/fastsar/ffbp_cpu.py +291 -0
  22. fastsar-0.1.0/fastsar/ffbp_cuda.py +803 -0
  23. fastsar-0.1.0/fastsar/io.py +280 -0
  24. fastsar-0.1.0/fastsar/lowfp.py +44 -0
  25. fastsar-0.1.0/fastsar/memory.py +164 -0
  26. fastsar-0.1.0/fastsar/pallas_ffbp.py +648 -0
  27. fastsar-0.1.0/fastsar/patches.py +774 -0
  28. fastsar-0.1.0/fastsar/pfa2.py +386 -0
  29. fastsar-0.1.0/fastsar/products.py +420 -0
  30. fastsar-0.1.0/fastsar/quality.py +99 -0
  31. fastsar-0.1.0/fastsar/sim.py +344 -0
  32. fastsar-0.1.0/fastsar/stripmap.py +417 -0
  33. fastsar-0.1.0/fastsar.egg-info/PKG-INFO +121 -0
  34. fastsar-0.1.0/fastsar.egg-info/SOURCES.txt +59 -0
  35. fastsar-0.1.0/fastsar.egg-info/dependency_links.txt +1 -0
  36. fastsar-0.1.0/fastsar.egg-info/requires.txt +21 -0
  37. fastsar-0.1.0/fastsar.egg-info/top_level.txt +1 -0
  38. fastsar-0.1.0/pyproject.toml +48 -0
  39. fastsar-0.1.0/setup.cfg +4 -0
  40. fastsar-0.1.0/tests/run_all.py +140 -0
  41. fastsar-0.1.0/tests/test_accuracy.py +70 -0
  42. fastsar-0.1.0/tests/test_api.py +48 -0
  43. fastsar-0.1.0/tests/test_autofocus.py +64 -0
  44. fastsar-0.1.0/tests/test_bp.py +114 -0
  45. fastsar-0.1.0/tests/test_burst.py +167 -0
  46. fastsar-0.1.0/tests/test_chain.py +216 -0
  47. fastsar-0.1.0/tests/test_cphd.py +222 -0
  48. fastsar-0.1.0/tests/test_exact.py +170 -0
  49. fastsar-0.1.0/tests/test_ffbp_cpu.py +34 -0
  50. fastsar-0.1.0/tests/test_ffbp_cuda.py +126 -0
  51. fastsar-0.1.0/tests/test_insar.py +66 -0
  52. fastsar-0.1.0/tests/test_io.py +45 -0
  53. fastsar-0.1.0/tests/test_pallas_e2e_tpu.py +55 -0
  54. fastsar-0.1.0/tests/test_pallas_final.py +63 -0
  55. fastsar-0.1.0/tests/test_pallas_fused.py +118 -0
  56. fastsar-0.1.0/tests/test_patches.py +250 -0
  57. fastsar-0.1.0/tests/test_planning.py +297 -0
  58. fastsar-0.1.0/tests/test_products.py +52 -0
  59. fastsar-0.1.0/tests/test_stripmap.py +79 -0
  60. fastsar-0.1.0/tests/test_units.py +436 -0
  61. fastsar-0.1.0/tests/test_wide_angle.py +39 -0
@@ -0,0 +1,123 @@
1
+ # Changelog
2
+
3
+ The format follows [Keep a Changelog](https://keepachangelog.com/en/1.1.0/).
4
+
5
+ ## 0.1.0 (2026-10-09)
6
+
7
+ First release.
8
+
9
+ ### Added
10
+
11
+ - `fastsar.ExactFormer`: exact backprojection onto the `form_image` grid, set up once per geometry, with CUDA and
12
+ C++ kernels (float64 tile centers, a third-order expansion within each tile, the tile size chosen from the
13
+ expansion's predicted error, cubic or linear interpolation of cropped range profiles);
14
+ `form_image(..., algorithm='bp')` with `interp`, `upsample` and `center`.
15
+ - `fastsar.form_cphd`: one call from a CPHD file to an image; spotlight or moving beam chosen from the scene
16
+ reference point, ground-plane grid at the scene reference height, SICD optional.
17
+ - `products.geolocate` and `products.locate`: latitude, longitude and height of `form_cphd` pixels on the image
18
+ plane, at a height or on a DEM, and the inverse; `products.read_dem` for DEM GeoTIFFs.
19
+ - `products.geocode_image`: a `form_cphd` image, or data from it, on a north-up map grid (UTM, any rasterio CRS,
20
+ or latitude/longitude) with terrain correction; `write_geotiff` writes complex and multi-band data.
21
+ - `products.write_sicd(path, out)` and `products.sicd_meta`: SICD from `form_cphd`'s output with its geometry
22
+ (PLANE grid, aperture polynomial), whose projection matches `geolocate`.
23
+ - `form_cphd(..., autofocus=True)` for spotlight collections; the result also carries the band, the spatial
24
+ bandwidths, the windows and the autofocus phase estimate.
25
+ - `sim.write_cphd`, `sim.to_ecf`: simulated collections as CPHD 1.0.1 files placed on the Earth.
26
+ - `ref=` on `ImageFormer` and `form_image`: per-pulse reference ranges other than `|ant|` (bistatic half paths,
27
+ vendor reference points). Polar format re-references the samples to `|ant|`.
28
+ - `ImageFormer.stage(S)`: the checks, scaling and upload of a history ahead of formation on JAX and TPU.
29
+ - `fastsar.MemoryWarning` when a former falls back to a slower path for lack of memory (smaller first-level
30
+ groups, CPU pulse blocks, CUDA streaming, a GPU window of mosaic range profiles), stating the memory full speed
31
+ needs; `ImageFormer.memory()`. A TPU that cannot hold the history raises `MemoryError`. `MemoryWarning`
32
+ subclasses `UserWarning`, so `python -W error` or pytest's `filterwarnings = error` turns a fallback into an
33
+ exception.
34
+ - Stripmap and sliding spotlight mosaics apply the azimuth window in the final stage of factorized backprojection
35
+ (C++, CUDA and JAX); range profiles are computed once per mosaic; the next patches are prepared on worker threads.
36
+ - `patches.gate_bins`, `patches.choose_buckets` and `patch_history(gate_len=)`: JAX and TPU mosaics plan the pulse
37
+ counts and range gates of their compiled programs for the whole mosaic.
38
+ - The CUDA former streams a host phase history whose device planes exceed half the free GPU memory through the
39
+ first level; the CPU former reads a complex64 history in place and sizes its first-level groups from the memory
40
+ available.
41
+ - JAX and TPU formers cache compiled programs and the plan's device arrays by plan signature (up to 32).
42
+ - Polar format keeps its compiled program and geometry arrays per collection geometry (the last four), and applies
43
+ the window, the spectral weighting and the scaling on the device.
44
+ - `ExactFormer` on CUDA halves its pulses per chunk on an out-of-memory error instead of failing (with a
45
+ `MemoryWarning`).
46
+ - Environment variables `FASTSAR_SHARED_PROFILES`, `FASTSAR_WEIGHT_TERMS`, `FASTSAR_WEIGHT_GRAD`,
47
+ `FASTSAR_MOSAIC_PREFETCH`, `FASTSAR_COMPILE_PATCHES`, `FASTSAR_PULSE_SLACK`, `FASTSAR_CPU_GROUP_GB`,
48
+ `FASTSAR_CUDA_GROUP`, `FASTSAR_TPU_GROUP`, `FASTSAR_CUDA_STREAM` and `FASTSAR_TIMING`
49
+ ([performance.md](docs/performance.md#environment-variables)); numeric values out of range raise `ValueError`.
50
+ - `examples/chain.py`.
51
+
52
+ ### Changed
53
+
54
+ - `read_cphd`'s local frame takes z along the ellipsoid normal at the scene reference point instead of the
55
+ geocentric radial (up to 0.19 degrees apart). `form_cphd`'s ground plane is now horizontal at the scene, as
56
+ documented; its comparisons with vendor images in `docs/real-data.md` were measured before this change.
57
+ - `read_cphd`'s meta carries the collection start, collector and core name.
58
+ - `form_cphd` references a spotlight history to |ant - c|, the range to the grid center, so its images change.
59
+ - JAX and TPU mosaics widen range gates onto shared lengths, so their images differ slightly from before.
60
+ - `FASTSAR_MOSAIC_PREFETCH` defaults to up to 4 worker threads on a GPU or TPU (1 before; still 1 on the CPU).
61
+ - The JAX program cache holds 32 programs instead of 16.
62
+ - CUDA float32 histories are no longer scaled to their peak.
63
+ - The CUDA former windows and checks a host history on the GPU as its row blocks are uploaded, without a full
64
+ complex copy on the device (host-side windowing cost a 4-vCPU instance more than the formation); `ExactFormer`
65
+ scans for NaN and inf on the GPU.
66
+ - Importing FastSAR sets `XLA_PYTHON_CLIENT_PREALLOCATE=false` unless already set, so that JAX does not take 75% of
67
+ a GPU's memory that the CUDA formers share.
68
+ - CPU first-level groups follow the available memory (MemAvailable), so they vary on a busy host.
69
+ - `form_image`, `ImageFormer`, `backproject`, `patches.form_mosaic`, `io.read_cphd` and `form_cphd` check their
70
+ inputs: a phase history that is not 2-D or not complex, has NaN or inf samples, fewer than 2 pulses or a pulse
71
+ count different from the antenna path; antenna positions not [pulses, 3]; pixel counts that are not positive
72
+ integers, non-positive spacings, axes that are not orthonormal; unknown backends, and `cuda` or `tpu` on a
73
+ machine without one, raise `ValueError` or `TypeError` with the reason. complex128 and non-contiguous histories
74
+ are converted.
75
+
76
+ ### Fixed
77
+
78
+ - An all-zero phase history gave a NaN image on the JAX and TPU paths and with polar format, and a
79
+ ZeroDivisionError on the CPU path; it now gives a zero image.
80
+ - `api.final_weights` gave weight zero to the final subapertures centered beyond the collection, which hold the
81
+ decimation filters' tails of the edge pulses: a unit `aperture_weight` changed the image by -32 dB. They now
82
+ take the weight of the nearest pulse, and a unit weight leaves the image unchanged.
83
+ - `ffbp2.decimator_fir` differed from the column of `ffbp.decimator` by up to 3e-16; it is now equal bit for bit.
84
+ - `backproject` with a `ref` shorter than the pulse count read past its end in the C++ kernel.
85
+ - `patches.fill_gaps` divided by zero for a single position or a platform at rest.
86
+ - `sim.simulate_brute` held a [pulses, samples, scatterers] array (5.8 GB for the dropped-pulse case of
87
+ `tests/test_patches.py`); it now sums in blocks of scatterers.
88
+ - A failure while the mosaic prefetches the next patch now shuts its thread down.
89
+
90
+ ### Tests
91
+
92
+ - `tests/run_all.py` runs every test script with a timeout and a memory limit and prints a summary.
93
+ - `tests/test_units.py`: decimation kernels, the JAX program cache, the C++ input paths and group sizes, aperture
94
+ weights, shared range profiles, pulse spans, gap filling, mosaic prefetch, input checks, `ref=`.
95
+ - `tests/test_cphd.py`: `read_cphd` and `form_cphd` end to end on simulated spotlight and stripmap collections
96
+ through a stand-in for sarpy's CPHD reader.
97
+ - `tests/test_chain.py`: a simulated CPHD through geolocation, map GeoTIFFs, SICD and autofocus.
98
+ - `tests/test_planning.py`: bucket planning, the CUDA and TPU memory models, the TPU out-of-memory retry, the
99
+ program cache under concurrent formers, and environment variable checks.
100
+ - `tests/test_accuracy.py`: the tile-size and oversampling figures of `docs/algorithms.md`.
101
+
102
+ ### Documentation
103
+
104
+ - Processing-chain page and API reference; README reduced to a landing page with a supported-data table.
105
+ - Documentation matches the geolocation, map GeoTIFF, SICD and autofocus calls for `form_cphd` output; each fact
106
+ stated on one page.
107
+ - Shorter README with figures; the long-form material moved to `docs/`.
108
+ - Contributing guide, issue and pull request templates, and package metadata for PyPI-style tools.
109
+
110
+ ### Initial functionality
111
+
112
+ - Spotlight image formation by factorized backprojection with kernels for x86 CPUs (C++/OpenMP), Nvidia GPUs
113
+ (CUDA through CuPy) and Cloud TPUs (Pallas), and the plain JAX program; `form_image` and `ImageFormer`.
114
+ - Final tile size chosen from the predicted error of the collection geometry.
115
+ - Polar format with the planar-wavefront displacement correction.
116
+ - Exact backprojection onto arbitrary points, monostatic or bistatic, with per-pulse reference ranges.
117
+ - CPHD reader: per-pulse frequency grids, moving reference points, channels and polarizations, SICD pixel-grid
118
+ alignment, troposphere delay, Capella phase sign, a warning for delay windows beyond the unambiguous range.
119
+ - Phase gradient autofocus.
120
+ - Stripmap focusing (range-Doppler, omega-k, backprojection), patch mosaics for long apertures and arbitrary
121
+ tracks, ScanSAR and TOPS burst modes.
122
+ - Products: multilook, interferogram, coherence, Pauli decomposition, range-Doppler projection and geocoding,
123
+ GeoTIFF and SICD output.
@@ -0,0 +1,75 @@
1
+ # Contributing to FastSAR
2
+
3
+ Bug reports, questions and pull requests are welcome. Please open an issue first for a change that touches a
4
+ kernel or the public API, so the approach can be agreed before the code is written.
5
+
6
+ ## Development setup
7
+
8
+ ```bash
9
+ git clone https://github.com/saulpingerman/FastSAR
10
+ cd FastSAR
11
+ pip install -e ".[io,test]" # add cuda on a machine with an Nvidia GPU: ".[cuda,io,test]"
12
+ ```
13
+
14
+ or `uv sync`, which installs the versions pinned in `uv.lock`. The CPU backend needs `g++` with OpenMP. Some
15
+ tests need packages that are not dependencies of the library: `finufft` (`tests/test_insar.py`), `rasterio`
16
+ (`products.write_geotiff`, `read_dem` and map projections other than latitude/longitude) and `sarkit` (optional
17
+ consistency checks in `tests/test_chain.py`).
18
+
19
+ ## Running the tests
20
+
21
+ The tests are plain scripts, not pytest functions. Each prints its measurements, stops with an `AssertionError`
22
+ when a check fails, and exits 0 when it passes. Run them from the repository root:
23
+
24
+ ```bash
25
+ python tests/run_all.py # every script below, with a timeout and a memory limit, and a summary
26
+ python tests/test_api.py # every backend this machine has against the JAX program, and polar format
27
+ python tests/test_ffbp_cpu.py # C++ kernels against the dense JAX image, two and three levels
28
+ python tests/test_ffbp_cuda.py # CUDA kernels against the dense JAX image (needs a GPU)
29
+ python tests/test_pallas_fused.py # TPU level kernel in interpret mode (runs on a CPU)
30
+ python tests/test_pallas_final.py # TPU final-stage kernel in interpret mode (runs on a CPU)
31
+ python tests/test_pallas_e2e_tpu.py # TPU kernels end to end in interpret mode (runs on a CPU)
32
+ python tests/test_autofocus.py cpu # phase gradient autofocus; the argument is the backend (default jax)
33
+ python tests/test_stripmap.py # stripmap omega-k and RDA against float64 backprojection
34
+ python tests/test_patches.py # patch mosaics for stripmap and non-linear tracks
35
+ python tests/test_burst.py # ScanSAR and TOPS burst focusing
36
+ python tests/test_products.py # layover projection, geocoding, multilooking, Pauli
37
+ python tests/test_bp.py # exact backprojection: backends, bistatic, orbital range, moving reference
38
+ python tests/test_io.py # CPHD helpers: frequency resampling, re-referencing, geodetic conversions
39
+ python tests/test_wide_angle.py # 10 to 360 degree apertures against exact backprojection
40
+ python tests/test_insar.py # change detection and interferometric height (needs finufft)
41
+ python tests/test_chain.py # simulated CPHD to geolocation, map GeoTIFFs, SICD and autofocus (needs sarpy, rasterio)
42
+ python tests/test_exact.py # ExactFormer: 1 to 600 km and a wide near-range grid against backprojection, validation
43
+ python tests/test_units.py # filters, program cache, aperture weights, mosaic pieces, input checks
44
+ python tests/test_cphd.py # read_cphd and form_cphd on simulated collections (stand-in CPHD reader)
45
+ python tests/test_planning.py # bucket planning, memory models, TPU out-of-memory retry, program cache
46
+ python tests/test_accuracy.py # tile-size and oversampling errors against exact backprojection
47
+ ```
48
+
49
+ `run_all.py` reports SKIP for a script that needs hardware this machine lacks (`test_ffbp_cuda.py` without a GPU);
50
+ `--require cuda,tpu` makes that a failure. A script whose test package is missing fails; install them with
51
+ `pip install -e ".[io,test]"`. It stops a script that runs past `-t` seconds (default 600) or holds more than
52
+ `--max-rss-gb` of memory (default 6 GB, 12 GB on a TPU host, whose runtime holds about 5 GB; or `FASTSAR_TEST_MAX_GB`), and exits non-zero if any script fails. `-v` prints
53
+ every script's output.
54
+
55
+ `test_autofocus.py` is the only script that takes a backend argument. The others pick the backends this machine
56
+ has (`fastsar.available_backends()`) or use the CPU. The tests use small simulated scenes and take seconds to a few
57
+ minutes each. For the CPU kernels, set `OMP_NUM_THREADS` to the number of physical cores.
58
+
59
+ `test_ffbp_cpu.py`, `test_ffbp_cuda.py` and the three `test_pallas_*.py` scripts compare each error with a limit
60
+ set about 5 dB above the value measured when the limit was set, and exit with a list of the failed cases.
61
+
62
+ ## Pull requests
63
+
64
+ - Keep a pull request to one change, and state in the description which tests ran and on which devices.
65
+ - A change to a kernel should report the error against the JAX program or exact backprojection before and after,
66
+ as the tests print it.
67
+ - A change that adds a result to the documentation should say what data it comes from. Results from simulated
68
+ data are labeled as test results.
69
+ - Do not commit data files (CPHD, SICD, NITF, `.npy`) or build artifacts.
70
+
71
+ ## Reporting a bug
72
+
73
+ Use the bug report template. Include the FastSAR commit, the backend, the device, the Python, JAX and CuPy
74
+ versions, and if possible a script that reproduces the problem on a simulated scene (`fastsar.sim`), since radar
75
+ data files are often large or not shareable.
fastsar-0.1.0/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Paul Singerman
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
@@ -0,0 +1,4 @@
1
+ include CHANGELOG.md CONTRIBUTING.md
2
+ graft tests
3
+ graft examples
4
+ global-exclude __pycache__ *.py[cod]
fastsar-0.1.0/PKG-INFO ADDED
@@ -0,0 +1,121 @@
1
+ Metadata-Version: 2.4
2
+ Name: fastsar
3
+ Version: 0.1.0
4
+ Summary: Synthetic aperture radar image formation on x86 CPUs, Nvidia GPUs and Cloud TPUs
5
+ Author: Paul Singerman
6
+ License-Expression: MIT
7
+ Project-URL: Homepage, https://github.com/saulpingerman/FastSAR
8
+ Project-URL: Documentation, https://github.com/saulpingerman/FastSAR/tree/main/docs
9
+ Project-URL: Source, https://github.com/saulpingerman/FastSAR
10
+ Project-URL: Issues, https://github.com/saulpingerman/FastSAR/issues
11
+ Project-URL: Changelog, https://github.com/saulpingerman/FastSAR/blob/main/CHANGELOG.md
12
+ Keywords: synthetic aperture radar,SAR,backprojection,image formation,CPHD,SICD,GPU,TPU
13
+ Classifier: Development Status :: 3 - Alpha
14
+ Classifier: Intended Audience :: Science/Research
15
+ Classifier: Operating System :: POSIX :: Linux
16
+ Classifier: Programming Language :: Python :: 3
17
+ Classifier: Programming Language :: Python :: 3 :: Only
18
+ Classifier: Programming Language :: C++
19
+ Classifier: Topic :: Scientific/Engineering
20
+ Classifier: Topic :: Scientific/Engineering :: Image Processing
21
+ Requires-Python: >=3.10
22
+ Description-Content-Type: text/markdown
23
+ License-File: LICENSE
24
+ Requires-Dist: numpy>=2.2.6
25
+ Requires-Dist: scipy>=1.15.3
26
+ Requires-Dist: jax>=0.6.2
27
+ Provides-Extra: cuda
28
+ Requires-Dist: cupy-cuda12x; extra == "cuda"
29
+ Provides-Extra: io
30
+ Requires-Dist: sarpy; extra == "io"
31
+ Provides-Extra: tpu
32
+ Requires-Dist: jax[tpu]; extra == "tpu"
33
+ Provides-Extra: geo
34
+ Requires-Dist: rasterio; extra == "geo"
35
+ Provides-Extra: test
36
+ Requires-Dist: sarpy; extra == "test"
37
+ Requires-Dist: sarkit; extra == "test"
38
+ Requires-Dist: rasterio; extra == "test"
39
+ Requires-Dist: finufft; extra == "test"
40
+ Dynamic: license-file
41
+
42
+ # FastSAR
43
+
44
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/LICENSE)
45
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/pyproject.toml)
46
+
47
+ FastSAR forms synthetic aperture radar (SAR) images on x86 CPUs, Nvidia GPUs and Google Cloud TPUs from Python. It
48
+ reads frequency-domain CPHD files, forms spotlight, stripmap and sliding spotlight images by factorized
49
+ backprojection, and provides autofocus, interferometry, geolocation, map GeoTIFFs and SICD output.
50
+
51
+ ![FastSAR image of the Panama Canal's Pacific entrance from an Umbra spotlight collection](https://raw.githubusercontent.com/saulpingerman/FastSAR/v0.1.0/docs/images/hero_panama.jpg)
52
+
53
+ *Panama Canal, Pacific entrance (Umbra open data, 2023-07-18): 12,207 by 8,808 pixels in 3.8 s on an Nvidia L4
54
+ (warm former, transfers included; float32, -59.9 dB against float64 exact backprojection), downsampled. A
55
+ c4d-highmem-16 CPU instance (16 vCPUs, 8 cores) takes 11.3 s. ISCE3, the fastest open-source code that forms this
56
+ collection, would take an estimated 4.8 h on that CPU and 27 min on the L4.*
57
+
58
+ ![Cost per 1000 images against error for FastSAR and five open-source implementations](https://raw.githubusercontent.com/saulpingerman/FastSAR/v0.1.0/docs/images/teaser.png)
59
+
60
+ *Cost per 1000 Panama images (October 2026 prices) against error over the lock, port and ship regions, relative to
61
+ the float64 reference ([performance](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/docs/performance.md), [comparison](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/docs/comparison.md)).*
62
+
63
+ ## Install
64
+
65
+ ```bash
66
+ pip install fastsar # CPU (and JAX on whatever device it has)
67
+ pip install "fastsar[io]" # io: sarpy for CPHD and SICD
68
+ pip install "fastsar[cuda]" # cuda: CuPy for Nvidia GPUs
69
+ pip install "fastsar[tpu]" # tpu: jax[tpu] for Cloud TPUs
70
+ pip install "fastsar[geo]" # geo: rasterio for GeoTIFFs and DEMs
71
+ pip install "fastsar[cuda,io,geo]" # extras combine
72
+ ```
73
+
74
+ From a clone: `pip install -e ".[cuda,io]"` or `uv sync`. The CPU kernels need `g++` with OpenMP and compile on
75
+ first use; the float16 CUDA kernel needs `nvcc`. On the CPU set
76
+ `OMP_NUM_THREADS=<physical cores> OMP_PLACES=cores OMP_PROC_BIND=close`.
77
+
78
+ ## Quickstart
79
+
80
+ ```python
81
+ import fastsar
82
+ from fastsar import products
83
+
84
+ out = fastsar.form_cphd('scene_CPHD.cphd') # mode, grid and window from the file
85
+ img = out['image'] # complex64 [nx, ny] on a ground plane at the scene height
86
+ lat, lon, h = products.geolocate(out, 100, 200) # pixel (100, 200) on the ground
87
+ products.write_geotiff('amp.tif', **products.geocode_image(out)) # amplitude on a north-up UTM grid
88
+ products.write_sicd('scene.nitf', out) # complex image with its geometry
89
+ ```
90
+
91
+ [`examples/chain.py`](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/examples/chain.py) runs this chain from the command line (`--simulate` needs no data);
92
+ [`form_umbra.py`](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/examples/form_umbra.py) and [`form_capella_stripmap.py`](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/examples/form_capella_stripmap.py) form
93
+ vendor grids.
94
+
95
+ ## Supported data, modes and devices
96
+
97
+ | Data | Modes | Status |
98
+ |---|---|---|
99
+ | Capella CPHD | stripmap, spotlight, sliding spotlight | checked against vendor SICDs; a 2022 dynamic stripmap is correct near the center only |
100
+ | Umbra CPHD | spotlight | three open-data collections on the vendor's grid |
101
+ | ICEYE CPHD | dwell spotlight | formed (28 GB history); not compared with the vendor image |
102
+ | TOA-domain CPHD, raw Level-0 | | not supported |
103
+ | Simulated echoes | stripmap, ScanSAR, TOPS, any track | `fastsar.stripmap`, `burst`, `patches` |
104
+
105
+ | | CPU (`cpu`) | Nvidia GPU (`cuda`) | Cloud TPU (`tpu`) | Any JAX device (`jax`) |
106
+ |---|---|---|---|---|
107
+ | Factorized backprojection | C++/OpenMP, AVX-512 or AVX2 | CUDA via CuPy | Pallas | reference program |
108
+ | Precision | float32 | float32, float16 | three-pass (default), single-pass | float32, float16 |
109
+ | Exact backprojection (`ExactFormer`) | C++/OpenMP, float32 with float64 tile centers | CUDA, the same | falls back to `jax` | float32, float64 block centers |
110
+
111
+ Polar format (`algorithm='pfa'`) runs as a JAX program on any device.
112
+
113
+ ## Documentation
114
+
115
+ [docs/README.md](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/docs/README.md) lists every page; start with the [processing chain](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/docs/processing-chain.md).
116
+ Measurement records are in [sar-accel-study](https://github.com/saulpingerman/sar-accel-study). A citation file
117
+ will come with the preprint.
118
+
119
+ ## Contributing and license
120
+
121
+ [CONTRIBUTING.md](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/CONTRIBUTING.md) explains how to run the tests. MIT license ([LICENSE](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/LICENSE)).
@@ -0,0 +1,80 @@
1
+ # FastSAR
2
+
3
+ [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/LICENSE)
4
+ [![Python 3.10+](https://img.shields.io/badge/python-3.10%2B-blue.svg)](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/pyproject.toml)
5
+
6
+ FastSAR forms synthetic aperture radar (SAR) images on x86 CPUs, Nvidia GPUs and Google Cloud TPUs from Python. It
7
+ reads frequency-domain CPHD files, forms spotlight, stripmap and sliding spotlight images by factorized
8
+ backprojection, and provides autofocus, interferometry, geolocation, map GeoTIFFs and SICD output.
9
+
10
+ ![FastSAR image of the Panama Canal's Pacific entrance from an Umbra spotlight collection](https://raw.githubusercontent.com/saulpingerman/FastSAR/v0.1.0/docs/images/hero_panama.jpg)
11
+
12
+ *Panama Canal, Pacific entrance (Umbra open data, 2023-07-18): 12,207 by 8,808 pixels in 3.8 s on an Nvidia L4
13
+ (warm former, transfers included; float32, -59.9 dB against float64 exact backprojection), downsampled. A
14
+ c4d-highmem-16 CPU instance (16 vCPUs, 8 cores) takes 11.3 s. ISCE3, the fastest open-source code that forms this
15
+ collection, would take an estimated 4.8 h on that CPU and 27 min on the L4.*
16
+
17
+ ![Cost per 1000 images against error for FastSAR and five open-source implementations](https://raw.githubusercontent.com/saulpingerman/FastSAR/v0.1.0/docs/images/teaser.png)
18
+
19
+ *Cost per 1000 Panama images (October 2026 prices) against error over the lock, port and ship regions, relative to
20
+ the float64 reference ([performance](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/docs/performance.md), [comparison](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/docs/comparison.md)).*
21
+
22
+ ## Install
23
+
24
+ ```bash
25
+ pip install fastsar # CPU (and JAX on whatever device it has)
26
+ pip install "fastsar[io]" # io: sarpy for CPHD and SICD
27
+ pip install "fastsar[cuda]" # cuda: CuPy for Nvidia GPUs
28
+ pip install "fastsar[tpu]" # tpu: jax[tpu] for Cloud TPUs
29
+ pip install "fastsar[geo]" # geo: rasterio for GeoTIFFs and DEMs
30
+ pip install "fastsar[cuda,io,geo]" # extras combine
31
+ ```
32
+
33
+ From a clone: `pip install -e ".[cuda,io]"` or `uv sync`. The CPU kernels need `g++` with OpenMP and compile on
34
+ first use; the float16 CUDA kernel needs `nvcc`. On the CPU set
35
+ `OMP_NUM_THREADS=<physical cores> OMP_PLACES=cores OMP_PROC_BIND=close`.
36
+
37
+ ## Quickstart
38
+
39
+ ```python
40
+ import fastsar
41
+ from fastsar import products
42
+
43
+ out = fastsar.form_cphd('scene_CPHD.cphd') # mode, grid and window from the file
44
+ img = out['image'] # complex64 [nx, ny] on a ground plane at the scene height
45
+ lat, lon, h = products.geolocate(out, 100, 200) # pixel (100, 200) on the ground
46
+ products.write_geotiff('amp.tif', **products.geocode_image(out)) # amplitude on a north-up UTM grid
47
+ products.write_sicd('scene.nitf', out) # complex image with its geometry
48
+ ```
49
+
50
+ [`examples/chain.py`](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/examples/chain.py) runs this chain from the command line (`--simulate` needs no data);
51
+ [`form_umbra.py`](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/examples/form_umbra.py) and [`form_capella_stripmap.py`](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/examples/form_capella_stripmap.py) form
52
+ vendor grids.
53
+
54
+ ## Supported data, modes and devices
55
+
56
+ | Data | Modes | Status |
57
+ |---|---|---|
58
+ | Capella CPHD | stripmap, spotlight, sliding spotlight | checked against vendor SICDs; a 2022 dynamic stripmap is correct near the center only |
59
+ | Umbra CPHD | spotlight | three open-data collections on the vendor's grid |
60
+ | ICEYE CPHD | dwell spotlight | formed (28 GB history); not compared with the vendor image |
61
+ | TOA-domain CPHD, raw Level-0 | | not supported |
62
+ | Simulated echoes | stripmap, ScanSAR, TOPS, any track | `fastsar.stripmap`, `burst`, `patches` |
63
+
64
+ | | CPU (`cpu`) | Nvidia GPU (`cuda`) | Cloud TPU (`tpu`) | Any JAX device (`jax`) |
65
+ |---|---|---|---|---|
66
+ | Factorized backprojection | C++/OpenMP, AVX-512 or AVX2 | CUDA via CuPy | Pallas | reference program |
67
+ | Precision | float32 | float32, float16 | three-pass (default), single-pass | float32, float16 |
68
+ | Exact backprojection (`ExactFormer`) | C++/OpenMP, float32 with float64 tile centers | CUDA, the same | falls back to `jax` | float32, float64 block centers |
69
+
70
+ Polar format (`algorithm='pfa'`) runs as a JAX program on any device.
71
+
72
+ ## Documentation
73
+
74
+ [docs/README.md](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/docs/README.md) lists every page; start with the [processing chain](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/docs/processing-chain.md).
75
+ Measurement records are in [sar-accel-study](https://github.com/saulpingerman/sar-accel-study). A citation file
76
+ will come with the preprint.
77
+
78
+ ## Contributing and license
79
+
80
+ [CONTRIBUTING.md](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/CONTRIBUTING.md) explains how to run the tests. MIT license ([LICENSE](https://github.com/saulpingerman/FastSAR/blob/v0.1.0/LICENSE)).
@@ -0,0 +1,76 @@
1
+ """From a CPHD file to products: a complex SICD with its geometry, a map-projected amplitude GeoTIFF (dB, UTM) and a
2
+ PNG quicklook of it.
3
+
4
+ python examples/chain.py scene_CPHD.cphd out [--sicd scene_SICD.ntf] [--dem dem.tif --geoid 31.5] [--autofocus]
5
+ python examples/chain.py --simulate out # writes a small simulated collection to out_sim.cphd first
6
+
7
+ Writes out.nitf (SICD), out_db.tif and out_db.png. --looks sets the multilooking (along track, across track) before
8
+ detection; --dem a DEM GeoTIFF for terrain correction, with --geoid the geoid's height above the ellipsoid at the
9
+ scene when the DEM gives heights above the geoid (without --dem the map lies at the image center's height).
10
+ """
11
+ import argparse, time
12
+
13
+ import numpy as np
14
+
15
+ import fastsar
16
+ from fastsar import products, sim
17
+
18
+
19
+ def simulate(path):
20
+ """A 100 m spotlight scene at 6 km (0.5 m resolution): a 3 x 3 grid of point targets over weak clutter."""
21
+ rng = np.random.default_rng(0)
22
+ col = sim.make_collect(res=0.5, scene=100.0, r0=6e3, graze_deg=35.0)
23
+ g = np.linspace(-30.0, 30.0, 3)
24
+ pts = np.stack([*np.meshgrid(g, g), np.zeros((3, 3))], -1).reshape(-1, 3)
25
+ cl = np.concatenate([rng.uniform(-45, 45, (3000, 2)), np.zeros((3000, 1))], 1)
26
+ pos = np.concatenate([pts, cl])
27
+ amp = np.concatenate([np.full(9, 30.0), 0.5 * (rng.standard_normal(3000) + 1j * rng.standard_normal(3000))])
28
+ S = sum(sim.simulate_brute(col, pos[i:i + 50], amp[i:i + 50]) for i in range(0, len(pos), 50))
29
+ sim.write_cphd(path, col, S.astype(np.complex64), lat=40.7934, lon=-77.86, height=351.0, heading=30.0)
30
+
31
+
32
+ def main():
33
+ ap = argparse.ArgumentParser()
34
+ ap.add_argument('cphd', nargs='?'); ap.add_argument('out')
35
+ ap.add_argument('--simulate', action='store_true'); ap.add_argument('--sicd')
36
+ ap.add_argument('--dem'); ap.add_argument('--geoid', type=float, default=0.0)
37
+ ap.add_argument('--autofocus', action='store_true'); ap.add_argument('--backend', default='auto')
38
+ ap.add_argument('--looks', type=int, nargs=2, default=(2, 2)); ap.add_argument('--spacing', type=float)
39
+ a = ap.parse_args()
40
+ if a.simulate:
41
+ a.cphd = a.out + '_sim.cphd'
42
+ simulate(a.cphd)
43
+ t = time.perf_counter()
44
+ out = fastsar.form_cphd(a.cphd, sicd=a.sicd, backend=a.backend, autofocus=a.autofocus)
45
+ img = out['image']
46
+ print(f'{out["mode"]} image {img.shape}, {out["spx"]:.3f} x {out["spy"]:.3f} m pixels, '
47
+ f'{time.perf_counter() - t:.1f} s')
48
+ for n in out['notes']:
49
+ print(' ' + n)
50
+ nx, ny = img.shape
51
+ lat, lon, h = products.geolocate(out, [0, 0, nx - 1, nx - 1, nx / 2], [0, ny - 1, 0, ny - 1, ny / 2])
52
+ print('corners and center (lat, lon, height on the image plane):')
53
+ for v in zip(lat, lon, h):
54
+ print(' %.6f %.6f %.1f' % v)
55
+
56
+ products.write_sicd(a.out + '.nitf', out)
57
+ dem = products.read_dem(a.dem, offset=a.geoid) if a.dem else None
58
+ power = products.multilook(img, *a.looks)
59
+ geo = products.geocode_image(out, power, spacing=a.spacing, height=dem)
60
+ geo['data'] = products.to_db(geo['data'])
61
+ products.write_geotiff(a.out + '_db.tif', **geo)
62
+ print(f'wrote {a.out}.nitf and {a.out}_db.tif ({geo["crs"]}, {geo["data"].shape[1]} x {geo["data"].shape[0]})')
63
+ try:
64
+ import matplotlib
65
+ matplotlib.use('Agg')
66
+ import matplotlib.pyplot as plt
67
+ db = geo['data']
68
+ top = np.nanpercentile(db, 99.5)
69
+ plt.imsave(a.out + '_db.png', np.nan_to_num(db, nan=top - 40), cmap='gray', vmin=top - 40, vmax=top)
70
+ print(f'wrote {a.out}_db.png (north up)')
71
+ except ImportError:
72
+ print('no matplotlib: PNG skipped')
73
+
74
+
75
+ if __name__ == '__main__':
76
+ main()
@@ -0,0 +1,64 @@
1
+ """Form a crop of a Capella stripmap collection from its CPHD with the patch mosaic, sample it on the pixels of the
2
+ vendor's SICD (a range / zero-Doppler grid) and compare the two amplitude images.
3
+
4
+ python examples/form_capella_stripmap.py scene_CPHD.cphd scene_SICD.ntf out [--size 512] [--backend cpu]
5
+
6
+ The scene reference point of a stripmap CPHD moves with the beam; read_cphd re-references the phase history to the
7
+ mid-aperture point, and each pulse's own point gives the beam center. The beam's extent along track is the azimuth
8
+ bandwidth the vendor processed (SICD Grid.Col.ImpRespBW).
9
+ """
10
+ import argparse, time
11
+
12
+ import numpy as np
13
+
14
+ import fastsar
15
+ from fastsar import io, patches, products
16
+
17
+ C = 299792458.0
18
+
19
+
20
+ def main():
21
+ ap = argparse.ArgumentParser()
22
+ ap.add_argument('cphd'); ap.add_argument('sicd'); ap.add_argument('out')
23
+ ap.add_argument('--size', type=int, default=512); ap.add_argument('--backend', default='cpu')
24
+ ap.add_argument('--spacing', type=float, default=0.35)
25
+ a = ap.parse_args()
26
+ from sarpy.io.complex.converter import open_complex
27
+ from sarpy.io.phase_history.converter import open_phase_history
28
+ col, meta = io.read_cphd(a.cphd, meta=True)
29
+ S, ant, f0, df = col['S'], col['ant'], col['fmin'], col['df']
30
+ P, K = S.shape
31
+ srp = io.ecf_to_local(open_phase_history(a.cphd).read_pvp_variable('SRPPos', 0), meta)
32
+ lo, hi = meta['pulses'] # the pulses read_cphd kept
33
+ srp = srp[lo:hi]
34
+ rd = open_complex(a.sicd)
35
+ sm = rd.sicd_meta
36
+ dsin = float(sm.Grid.Col.ImpRespBW) * C / (f0 + K / 2 * df) / 2 # processed spread of sin(look angle)
37
+ d = np.gradient(ant, axis=0)
38
+ d /= np.linalg.norm(d, axis=1, keepdims=True)
39
+ s_c = ((srp - ant) * d).sum(1) / np.linalg.norm(srp - ant, axis=1) # beam center of each pulse
40
+
41
+ def beam(idx, pts):
42
+ w = np.asarray(pts)[None] - ant[idx][:, None]
43
+ return ((w * d[idx][:, None]).sum(-1) / np.linalg.norm(w, axis=-1) - s_c[idx][:, None]) / (dsin / 2)
44
+
45
+ n = a.size
46
+ r0, c0 = int(sm.ImageData.NumRows) // 2 - n // 2, int(sm.ImageData.NumCols) // 2 - n // 2
47
+ rr, cc = np.meshgrid(np.arange(r0, r0 + n), np.arange(c0, c0 + n), indexing='ij')
48
+ pts = io.sicd_points(sm, rr, cc, meta)
49
+ e1, e2 = np.array([1.0, 0, 0]), np.array([0, 1.0, 0])
50
+ lo, hi = pts.reshape(-1, 3).min(0), pts.reshape(-1, 3).max(0)
51
+ origin = np.array([lo[0] - 5, lo[1] - 5, pts[..., 2].mean()])
52
+ nx, ny = int((hi[0] - lo[0] + 10) / a.spacing), int((hi[1] - lo[1] + 10) / a.spacing)
53
+ t = time.perf_counter()
54
+ img = patches.form_mosaic(dict(S=S, fmin=f0, df=df, ref=meta['ref'], band=(f0, f0 + K * df)), ant, origin, nx, ny,
55
+ a.spacing, a.spacing, e1, e2, beam=beam, backend=a.backend)
56
+ print(f'{nx} x {ny} ground pixels formed in {time.perf_counter() - t:.0f} s')
57
+ ours = products.sample(img, np.stack([(pts - origin) @ e1 / a.spacing, (pts - origin) @ e2 / a.spacing], -1))
58
+ v = rd[r0:r0 + n, c0:c0 + n]
59
+ print(f'amplitude correlation with the vendor image: {np.corrcoef(np.abs(ours).ravel(), np.abs(v).ravel())[0, 1]:.3f}')
60
+ np.save(a.out + '.npy', ours)
61
+
62
+
63
+ if __name__ == '__main__':
64
+ main()
@@ -0,0 +1,33 @@
1
+ """Form an Umbra spotlight image on the vendor's grid and save it as a complex64 .npy and an amplitude PNG.
2
+
3
+ python examples/form_umbra.py scene_CPHD.cphd scene_SICD.nitf out [--backend cpu] [--precision float32]
4
+ """
5
+ import argparse, time
6
+
7
+ import numpy as np
8
+
9
+ import fastsar
10
+
11
+
12
+ def main():
13
+ ap = argparse.ArgumentParser()
14
+ ap.add_argument('cphd'); ap.add_argument('sicd'); ap.add_argument('out')
15
+ ap.add_argument('--backend', default='auto'); ap.add_argument('--precision', default='float32')
16
+ ap.add_argument('--algorithm', default='ffbp')
17
+ a = ap.parse_args()
18
+ col = fastsar.io.read_cphd(a.cphd, sicd=a.sicd)
19
+ print('pulses, samples', col['S'].shape, 'grid', col['nx'], col['ny'], 'backends', fastsar.available_backends())
20
+ t = time.perf_counter()
21
+ img = fastsar.form_image(**col, algorithm=a.algorithm, backend=a.backend, precision=a.precision)
22
+ print(f'formed in {time.perf_counter() - t:.1f} s (includes compilation on the first call)')
23
+ np.save(a.out + '.npy', img)
24
+ try:
25
+ import matplotlib; matplotlib.use('Agg'); import matplotlib.pyplot as plt
26
+ db = 20 * np.log10(np.abs(img) + 1e-12); top = np.percentile(db, 99.9)
27
+ plt.imsave(a.out + '.png', np.clip(db.T, top - 45, top), cmap='gray', vmin=top - 45, vmax=top, origin='lower')
28
+ except ImportError:
29
+ pass
30
+
31
+
32
+ if __name__ == '__main__':
33
+ main()
@@ -0,0 +1,16 @@
1
+ """Fast spotlight SAR image formation: factorized backprojection with kernels for Cloud TPUs (Pallas), Nvidia GPUs
2
+ (CUDA) and x86 CPUs (C++/OpenMP), and polar format with its geometric resampling. See fastsar.api.form_image."""
3
+ import os as _os
4
+
5
+ # JAX takes 75% of a GPU's memory when it first runs there unless told otherwise; FastSAR's CUDA kernels (CuPy) and
6
+ # its JAX programs share the GPU, so JAX allocates as it goes (a setting the user made before importing JAX stands);
7
+ # JAX keeps what it has allocated, and the CuPy formers shrink their working sets to what is left
8
+ _os.environ.setdefault('XLA_PYTHON_CLIENT_PREALLOCATE', 'false')
9
+ from .api import form_image, available_backends, ImageFormer # noqa: F401
10
+ from . import io, autofocus, stripmap, burst, patches, quality, products # noqa: F401
11
+ from .bp import backproject, plane_points # noqa: F401
12
+ from .exact import ExactFormer # noqa: F401
13
+ from .cphd import form_cphd # noqa: F401
14
+ from .memory import MemoryWarning # noqa: F401
15
+
16
+ __version__ = '0.1.0'