Skip to content

Factorized backprojection - #346

Open
bhawkins wants to merge 250 commits into
isce-framework:developfrom
bhawkins:factorized_bp
Open

Factorized backprojection#346
bhawkins wants to merge 250 commits into
isce-framework:developfrom
bhawkins:factorized_bp

Conversation

@bhawkins

@bhawkins bhawkins commented Jul 31, 2026

Copy link
Copy Markdown
Contributor

This PR adds a factorized backprojection (FBP) algorithm for SAR focusing as a faster alternative to the existing direct backprojection method.

Algorithm Design

The basic idea is from Yegulalp 1999 where local, low-resolution polar grids can efficiently store partial sums of subapertures. As in other papers, the idea is extended hierarchically, so that multiple local grids can be combined together into a higher resolution grid. This process can be repeated in successive stages until the desired resolution is met. In the limit one achieves a factor of $O(N / log(N))$ reduction in the number of sums per output pixel (where $N$ is the number of pulses in the synthetic aperture). In practice there's a fair amount of overhead, but the speedup is still significant.

The FBP algorithm is usually described for the spotlight case, and the one here has a few modifications for the stripmap case relevant to NISAR. First, the azimuth extent of the local grid is extended based on the duration of the subaperture, since the beam is sliding. Second, the azimuth resolution of the local grid is not allowed to exceed the L/2 limit. The radar travel time between transmit and receive ("bistatic" correction) is accounted for both in the initial back projection stage and in the selection of grid coordinates. The latter is necessary to maintain a baseband signal in azimuth.

I'm not sure I'd call this a "fast factorized" (FFBP) algorithm. In the interest of maximal accuracy, it does full 2D interpolation and makes no assumptions about the trajectory or nesting of coordinate grids. Non-uniform FFT algorithms are used for interpolation in order to maintain high accuracy with small kernels. The zero-padded transforms do occupy a fair amount of memory, though, which may be a bottleneck. Care has been taken to ensure that only the "active" subimages reside in memory at any given time.

You might notice a few new functions that are not used in the NISAR RSLC workflow. These have been used for processing UAVSAR data from circular tracks (CSAR).

Key Features

  • Factorized backprojection algorithm: Multi-stage approach (initial → middle → final) that reduces computational complexity compared to direct backprojection
  • 2D NFFT implementation: New CPU and GPU implementations of 2D Non-uniform FFT (NFFT2d) required by the FBP algorithm
  • GPU acceleration: CUDA implementations for all FBP stages and NFFT2d operations
  • Bug fixes in interp1d and interp2d
  • Python interface: New azcomp_bp and azcomp_fbp functions exposed via isce3.focus.azcomp_bp module
  • Configuration support: Added FBP parameters to focus workflow configuration schema

Usage

The algorithm is highly configurable. You can specify the number of stages, the size of each stage, and the interpolation parameters for each stage (NFFT kernel size and oversampling ratio in both dimensions). To use a two-stage algorithm, the RSLC configuration file should look like

runconfig:
    groups:
        processing:
            azcomp:
                factorization:
                - size: 128

which means sum groups of 128 pulses into polar grids before summing those into the output image. Similarly,

# runconfig.groups.processing.azcomp
                factorization:
                - size: 64
                - size: 2

means a three-stage algorithm where the initial grids comprise 64 pulses each, these get merged together two at a time, and then the merged grids get summed into the output image. The default factorization: [{"size": 1}] is understood to mean the original direct backprojection algorithm.

In my testing I've found the two-stage n=128 algorithm is a 10x speedup of the azcomp algorithm on a g6e instance in AWS EC2.

I included a script that can generate animations like the following from the FBP debug file:

factors.mp4

bhawkins and others added 2 commits August 3, 2026 15:47
Fixes linker errors with CUDA 13+ where __global__ kernel symbols
from the main library weren't visible when template code in the
Python bindings module instantiated templates that called those
kernels.

The main CUDA library already has CUDA_SEPARABLE_COMPILATION enabled,
but the Python bindings module didn't. This enables the -rdc flag for
nvcc, allowing device code symbols to be visible across translation
units during device link time.
CUDA 13.0 changed nvcc defaults so __global__ kernels get hidden ELF
visibility and __global__ function-template stubs get internal linkage.
This breaks the pybind module link: template code in the bindings
launches detail:: kernels defined in libisce3-cuda, and those symbols
are no longer resolvable across the shared-library boundary.

Restore the pre-13 behavior on the isce3-cuda target via the documented
nvcc opt-out flags (available since CUDA 12.8), guarded on compiler
version so older toolchains are unaffected.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
Comment thread cxx/isce3/cuda/focus/Backproject.h Outdated
bhawkins and others added 9 commits August 5, 2026 23:54
Change accumulatePolarImagesToRadarGrid and mergePolarImages to use
std::vector<const NFFT2dResult<float>*> instead of std::vector<NFFT2dResult<float>>
to match the GPU implementation and avoid unnecessary copies.

Updated Python bindings to accept py::sequence and convert to vector
of pointers, consistent with the GPU bindings.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@bhawkins

bhawkins commented Aug 6, 2026

Copy link
Copy Markdown
Contributor Author

All the CI failures are in test.cxx.isce3.io.raster.raster with

[ RUN      ] RasterTest.addRasterToVRT
ERROR 3: Failed to read scanline 100.
ERROR 1: IReadBlock failed at X offset 0, Y offset 100: Failed to read scanline 100.
In isce3::io::Raster::getSetValue() - error in RasterIO.
/home/runner/work/isce3/isce3/tests/cxx/isce3/io/raster/raster.cpp:258: Failure
Expected equality of these values:
  std::isfinite(val)
    Which is: false
  true

[  FAILED  ] RasterTest.addRasterToVRT (55 ms)

Seems unrelated to the PR and the test passes locally.

@bhawkins bhawkins left a comment

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Comments from today's review meeting.

Comment thread python/packages/isce3/focus/serialization.py
Comment thread python/packages/isce3/focus/azcomp_bp.py Outdated
Comment thread python/packages/isce3/focus/azcomp_bp.py Outdated
Comment thread python/packages/isce3/focus/azcomp_bp.py Outdated
Comment thread python/packages/isce3/focus/azcomp_bp.py Outdated
bhawkins and others added 5 commits August 6, 2026 20:53
Documented all functions and classes in serialization.py with NumPy format
docstrings including parameter types, return values, and attribute descriptions.

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
@bhawkins

bhawkins commented Aug 9, 2026

Copy link
Copy Markdown
Contributor Author

Fix for the CI failure is in #348

@hfattahi hfattahi added this to the R05.03.0 milestone Aug 11, 2026
@bhawkins

bhawkins commented Sep 2, 2026

Copy link
Copy Markdown
Contributor Author

I just pushed a change making FBP the default algorithm in order to simplify PCM/SDS testing. @hfattahi @jshimada47

@jshimada47

Copy link
Copy Markdown
Contributor

I'm getting a unit test failure while doing the build on dev-5 in test.python.pkg.nisar.workflows.focus:

253/280 Test #248: test.python.pkg.nisar.workflows.focus ...............................***Failed   23.63 sec
============================= test session starts ==============================
platform linux -- Python 3.12.11, pytest-8.4.1, pluggy-1.5.0
rootdir: /src
configfile: pyproject.toml
collected 3 items

../../../../../../src/tests/python/packages/nisar/workflows/focus.py .F. [100%]

=================================== FAILURES ===================================
__________________________ test_focus[factorization1] __________________________

factorization = [<nisar.workflows.focus.Struct object at 0x7eff92c080b0>]

    @pytest.mark.parametrize("factorization", (direct_bp, factorized_bp_64))
    def test_focus(factorization):
        cfg = get_test_cfg()
        cfg.runconfig.groups.processing.azcomp.factorization = factorization

>       focus.focus(cfg)

/src/tests/python/packages/nisar/workflows/focus.py:45:
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _
/bld/install/opt/conda/lib/python3.12/site-packages/nisar/workflows/focus.py:1873: in focus
    slc = SLC(output_slc_path, mode="w", product=product,
/bld/install/opt/conda/lib/python3.12/site-packages/nisar/products/writers/SLC.py:353: in __init__
    super().__init__(*args, **kw)
/opt/conda/lib/python3.12/site-packages/h5py/_hl/files.py:564: in __init__
    fid = make_fid(name, mode, userblock_size, fapl, fcpl, swmr=swmr)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/opt/conda/lib/python3.12/site-packages/h5py/_hl/files.py:244: in make_fid
    fid = h5f.create(name, h5f.ACC_TRUNC, fapl=fapl, fcpl=fcpl)
          ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
h5py/_objects.pyx:56: in h5py._objects.with_phil.wrapper
    ???
h5py/_objects.pyx:57: in h5py._objects.with_phil.wrapper
    ???
_ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _ _

>   ???
E   OSError: Unable to synchronously create file (unable to truncate a file which is already open)

h5py/h5f.pyx:122: OSError
------------------------------ Captured log call -------------------------------
WARNING  focus:focus.py:331 No DEM given, using ref height 0.0 (m).
WARNING  focus:focus.py:441 Desired Doppler LUT start time is 1.0 seconds before ephemeris start. Consider adjusting ephemeris_crop_pad or providing more orbit/attitude data.
WARNING  focus:focus.py:445 Desired Doppler LUT end time is 1.0 seconds after ephemeris end. Consider adjusting ephemeris_crop_pad or providing more orbit/attitude data.
=============================== warnings summary ===============================
tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
tests/python/packages/nisar/workflows/focus.py::test_focus[factorization1]
  /bld/install/opt/conda/lib/python3.12/site-packages/nisar/products/readers/Base/Identification.py:238: UserWarning: Could not find isJointObservation in product identification.
    warn("Could not find isJointObservation in product identification.")

tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
tests/python/packages/nisar/workflows/focus.py::test_focus[factorization1]
  /bld/install/opt/conda/lib/python3.12/site-packages/nisar/products/readers/Base/Identification.py:247: UserWarning: Could not find hasInputDataException in product identification.  Assuming there are no anomalies.
    warn("Could not find hasInputDataException in product "

tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
  /bld/install/opt/conda/lib/python3.12/site-packages/nisar/products/readers/Raw/Raw.py:466: UserWarning: Missing dataset /science/LSAR/RRSD/highRateTelemetry/txH/rxH/QFSP0/CHIRP_CORRELATOR_I1_H01 in /src/tests/data/focus/REE_L0B_out17.h5. Detailed error -> 'Unable to synchronously open object (component not found)'
    warn(

tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
  /bld/install/opt/conda/lib/python3.12/site-packages/nisar/antenna/rx_channel_imbalance_helpers.py:212: UserWarning: No LNA CAL to represent RX! Use BYPASS Cal instead!
    warn('No LNA CAL to represent RX! Use BYPASS Cal instead!')

tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
  /bld/install/opt/conda/lib/python3.12/site-packages/nisar/antenna/rx_channel_imbalance_helpers.py:217: UserWarning: No LNA or BYPASS CAL! LNA mean will be all unity. The results will be invalid!
    warn('No LNA or BYPASS CAL! LNA mean will be all unity. '

tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
  /bld/install/opt/conda/lib/python3.12/site-packages/nisar/antenna/rx_channel_imbalance_helpers.py:496: UserWarning: All values are zero for HH-pol Caltone! They are set to untiy. Result may be invalid!
    warn(f'All values are zero for {msg}! They are set to untiy. '

tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
  /bld/install/opt/conda/lib/python3.12/site-packages/nisar/antenna/rx_channel_imbalance_helpers.py:500: UserWarning: Some values are zero for HH-pol Caltone!
    warn(f'Some values are zero for {msg}!')

tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
  /bld/install/opt/conda/lib/python3.12/site-packages/nisar/antenna/rx_channel_imbalance_helpers.py:48: UserWarning: The size of LNA-CALTONE ratio is 1 instead of 12!
    warn('The size of LNA-CALTONE ratio is '

tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
  /bld/install/opt/conda/lib/python3.12/site-packages/nisar/products/readers/instrument/instrument_parser.py:347: MissingInstrumentFieldWarning: HPOL/channelAdjustment/
    return self._channel_adjustment_factors('rx', pol)

tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
  /bld/install/opt/conda/lib/python3.12/site-packages/nisar/products/readers/instrument/instrument_parser.py:310: MissingInstrumentFieldWarning: HPOL/channelAdjustment/
    return self._channel_adjustment_factors('tx', pol)

tests/python/packages/nisar/workflows/focus.py::test_schema
  <unknown>:1: SyntaxWarning: invalid escape sequence '\d'

-- Docs: https://docs.pytest.org/en/stable/how-to/capture-warnings.html
==================================== PASSES ====================================
__________________________ test_focus[factorization0] __________________________
----------------------------- Captured stdout call -----------------------------
{
  "magnitude": 0.00012279492511879653,
  "phase": -3.09582781791687,
  "azimuth": {
    "ISLR": -15.777904987335205,
    "PSLR": -17.766033411026,
    "resolution": 1.203125,
    "index": 64.0,
    "offset": 0.0007591966132167727,
    "phase ramp": -0.006026060786098242
  },
"range": {
    "ISLR": -13.799757957458496,
    "PSLR": -16.547774076461792,
    "resolution": 1.15625,
    "index": 64.0,
    "offset": 4.2870595962085645e-10,
    "phase ramp": -0.015377550385892391
  }
}
------------------------------ Captured log call -------------------------------
WARNING  focus:focus.py:331 No DEM given, using ref height 0.0 (m).
WARNING  focus:focus.py:441 Desired Doppler LUT start time is 1.0 seconds before ephemeris start. Consider adjusting ephemeris_crop_pad or providing more orbit/attitude data.
WARNING  focus:focus.py:445 Desired Doppler LUT end time is 1.0 seconds after ephemeris end. Consider adjusting ephemeris_crop_pad or providing more orbit/attitude data.
WARNING  SLCWriter:SLC.py:333 Could not determine coverage indicator given frame_polygon=<POLYGON EMPTY> and image_polygon=<POLYGON Z ((-54.584 3.173 0, -54.583 3.174 -7.081e-10, -54.582 3.174 0, -54...>
WARNING  SLCWriter:SLC.py:771 Input L0B has undesired 'units' attribute on diagnosticModeFlag dataset.  Will omit from output RSLC.
WARNING  focus:focus.py:2188 No noise-only range lines within the specified pulse interval. Skip noise estimation and set noise equivalent backscatter to zero.
=========================== short test summary info ============================
FAILED ../../../../../../src/tests/python/packages/nisar/workflows/focus.py::test_focus[factorization1]
PASSED ../../../../../../src/tests/python/packages/nisar/workflows/focus.py::test_focus[factorization0]
PASSED ../../../../../../src/tests/python/packages/nisar/workflows/focus.py::test_schema
================== 1 failed, 2 passed, 13 warnings in 23.14s ===================
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: No reference epoch provided. Using first date time from XML file as orbit reference epoch.
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: cube height: 20
journal:
 -- cube length: 12
 -- cube width: 12
journal: EPSG: 32621
journal: estimating the ground-track velocity using rdr2geo
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: No reference epoch provided. Using first date time from XML file as orbit reference epoch.
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification
journal: hasInputDataException not found at /science/LSAR/identification

        Start 249: test.python.pkg.nisar.workflows.gen_doppler_range_product
254/280 Test #249: test.python.pkg.nisar.workflows.gen_doppler_range_product ...........   Passed    3.58 sec

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

GPU Enable GPU builds for this PR

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants