Skip to article frontmatterSkip to article content
Site not loading correctly?

This may be due to an incorrect BASE_URL configuration. See the MyST Documentation for reference.

Estimate the Faraday Rotation of a GSLC


Faraday rotation in the ionosphere scales with the square of a SAR signal’s wavelength (λ2\lambda^2). Therefore, ionospheric impacts to polarization rotation are much more pronounced in NISAR’s ~25cm L-band data than in Sentinel-1’s ~5cm C-band data.

This notebook loads NISAR L-band GSLC data and performs a Faraday rotation estimation using the circular-basis method described in equation 14 of F. J. Meyer and J. B. Nicoll’s, “Prediction, detection, and correction of Faraday rotation in full-polarimetric L-band SAR data”.


Overview

  1. Prerequisites

  2. Search for data

  3. Load a single GSLC with HTTPS

  4. Define functions to perform a Faraday rotation estimation over an optionally multilooked GSLC

  5. Estimate Faraday rotation on the multi-looked subset GSLC

  6. Plot the Faraday rotation over a basemap

  7. Summary

  8. Resources and references


1. Prerequisites

  • Rough Notebook Time Estimate: 10 minutes


Use asf_search to find GCOV data.

2a. Perform an asf_search.search() to identify your desired product URLs

import os
import asf_search as asf
from datetime import datetime
from getpass import getpass

import warnings
warnings.filterwarnings(
    "ignore",
    message="Parsing dates involving a day of month without a year specified",
)

session = asf.ASFSession()

start_date = datetime(2026, 1, 16)
end_date = datetime(2026, 1, 18)
area_of_interest = "POINT(447.0504 24.5906)" # POINT or POLYGON as WKT (well-known-text)
pattern = r'^(?!.*QA_STATS).*'

opts=asf.ASFSearchOptions(**{
    "maxResults": 250,
    "intersectsWith": area_of_interest,
    "flightDirection": "DESCENDING",
    "start": start_date,
    "end": end_date,
    "processingLevel": [
        "GSLC"
    ],
    "dataset": [
        "NISAR"
    ],
    "productionConfiguration": [
        "PR"
    ],
    'session': session,
})

response = asf.search(opts=opts)
hdf5_urls = response.find_urls(extension='.h5', pattern=pattern, directAccess=False)
print(f"Found {len(hdf5_urls)} GSLC products:")
hdf5_urls

2c. Provide your Earthdata Login (EDL) Bearer Token

Both HTTPS and S3 access require an EDL Bearer Token

Image of the Earthdata Login Generate Token web page

View or generate a Bearer Token in “Generate Token” tab of the Profile page in your Earthdata Login account: https://urs.earthdata.nasa.gov/profile

from getpass import getpass

token = getpass("Enter your EDL Bearer Token")

3. Load a single GSLC with HTTPS

3a. Lazily load a GSLC dataset with xarray

%%time
import asf_search as asf
import fsspec
import rioxarray
import xarray as xr

group_path = "/science/LSAR/GSLC/grids/frequencyA"

fs = fsspec.filesystem(
    "http",
    headers = {"Authorization": f"Bearer {token}"},
    block_size = 16 * 512 * 512,
)

f = fs.open(hdf5_urls[0], "rb")

dt = xr.open_datatree(
    f,
    engine="h5netcdf",
    decode_timedelta=False,
    phony_dims="access",
    chunks="auto",
    group=group_path
)

display(dt)

3b. Create a spatial subset

import rioxarray

ds = dt.to_dataset()
projection = ds.projection.attrs['epsg_code'].item()
ds = ds.rio.write_crs(projection) # write the project to the HHHH data for easy lat/lon subsetting

# subset the data
subset_ds = ds.rio.clip_box(
    minx=446.5857, miny=24.7935,
    maxx=446.7342, maxy=24.9244, 
    crs="EPSG:4326"
)
subset_ds

4. Define functions to perform a Faraday rotation estimation over an optionally multilooked GSLC

Uses the circular-basis method for Faraday rotation estimation described in equation 14 of F. J. Meyer and J. B. Nicoll’s, “Prediction, detection, and correction of Faraday rotation in full-polarimetric L-band SAR data”

import numpy as np
import xarray as xr

def multilook_mean(da, ly=1, lx=1, x_coord="xCoordinates", y_coord="yCoordinates"):
    """
    Spatially multilook an `xarray.DataArray` by taking the mean over
    pixel windows (ly x lx).

    da : (xarray.Dataset or xarray.DataArray) 2D DataArray to multilook
    ly : (int) Number of pixels to average together in the y direction
    lx : (int) Number of pixels to average together in the x direction

    returns: Multilooked xarray.DataArray
    """
    return da.coarsen(
        {y_coord: ly, x_coord: lx},
        boundary="trim"
    ).mean()

def faraday_rotation_circular_basis(hh, hv, vh, vv, 
                                    ly=10, lx=10, # multilook window
                                    x_coord="xCoordinates", y_coord="yCoordinates"):
    """
    Estimate Faraday rotation angle using the circular-basis method:

    [Z11 Z12]   [1  j] [M'hh M'vh] [1  j]
    [Z21 Z22] = [j  1] [M'hv M'vv] [j  1]

    from which we derive:

    Ω_hat = 1/4 * arg(Z12 * conj(Z21))

    M′: measured scattering matrices (linear basis)
    Ω_hat: Faraday Rotation angle estimate
    Z: circular basis (Z12: RL, Z21: LR)
    arg: argument of complex number (phase in the complex plane)

    References: 
    - F. J. Meyer and J. B. Nicoll, “Prediction, detection, and correction of Faraday rotation in full-polarimetric
    L-band SAR data,” IEEE Trans. Geosci. Remote Sens., vol. 46, no. 10, pp. 3076–3086, Oct. 2008.
    https://topex.ucsd.edu/pub/sandwell/Venus_InSAR/atmosphere/meyer_nicoll_TGRS46_10_ionosphere_Polarimetry.pdf
    - S. H. Bickel and R. H. T. Bates, “Effects of magneto-ionic propagation on the
    polarization scattering matrix,” Proc. IEEE, vol. 53, no. 8, pp. 1089–1091, Aug. 1965

    hh, hv, vh, vv : (xarray.Dataset or xarray.DataArray) Complex-valued, linearly polarized image DataArrays
    ly : (int) Number of looks in y for multilooking
    lx : (int) Number of looks in x for multilooking
    x_coord: (string) DataArrays' x coordinate variable name
    y_coord: (sring) DataArrays' y coordinate variable name

    Returns: omega (xarray.DataArray) Estimated Faraday rotation angle in radians
    """
    
    #     Z = T * M' * T
    #
    #     T = [[1, j],
    #          [j, 1]]
    #
    # [[Z11 Z12], =  [[1, j],      [[hh, vh],    [[1, j],
    # [Z21 Z22]]  =  [j, 1]]   *   [hv, vv]]  *  [j, 1]]
    
    T = 1j
    # calculate the circular-basis off-diagonal terms 
    z12 = (T * hh) - vh + hv + (T * vv)
    z21 = (T * hh) + vh - hv + (T * vv)

    # multilook the cross-product of the off-diagonal terms
    phase_product = z12 * np.conj(z21)

    # multilook the cross-product
    phase_product_ml = multilook_mean(
        phase_product,
        ly=ly, lx=lx,
        x_coord=x_coord, y_coord=y_coord
    )

    # P = Z12 * conj(Z21)
    # Ω_hat = 1/4 * arg(P)
    # and
    # Z=x+iy
    # cosθ = x/hyp, sinθ = y/hyp
    # θ = arctan(y/x)
    # ∴
    # arg(P)=atan2(ℑ(P),ℜ(P))
    # Ω_hat = 1/4 * atan2(ℑ(P),ℜ(P))
    omega_rad = 0.25 * xr.apply_ufunc(
        np.arctan2, # np.arctan2 avoids quadrant ambiguity
        phase_product_ml.imag, # y-coord
        phase_product_ml.real, # x-coord
        dask="parallelized",
        output_dtypes=[np.float64],
    )
    omega_rad.name = "faraday_rotation_rad"
    omega_rad.attrs["units"] = "radian"

    # Coherence-based quality metric
    # P = Z12 * conj(Z21)
    # quality = |P| / sqrt(|Z12|^2 * |Z21|^2)
    #
    # quality ≈ 1: stable phase difference, reliable FR estimate
    # quality ≈ 0: random/noisy phase difference, unreliable FR estimate
    z12_pow_ml = multilook_mean(
        np.abs(z12) ** 2,
        ly=ly, lx=lx,
        x_coord=x_coord, y_coord=y_coord
    )

    z21_pow_ml = multilook_mean(
        np.abs(z21) ** 2,
        ly=ly, lx=lx,
        x_coord=x_coord, y_coord=y_coord
    )

    quality = np.abs(phase_product_ml) / np.sqrt(z12_pow_ml * z21_pow_ml)
    quality = quality.clip(0, 1)
    quality.name = "faraday_rotation_quality"
    quality.attrs["units"] = "unitless"

    if hasattr(hh, "rio") and hh.rio.crs is not None:
        omega_rad = omega_rad.rio.write_crs(hh.rio.crs, inplace=False)
        quality = quality.rio.write_crs(hh.rio.crs, inplace=False)

    return omega_rad, quality

6. Plot the Faraday rotation over a basemap

“Faraday rotation (FR) angles of less than 55^\circ are generally considered acceptable for many commonly used SAR parameter extraction methods. Higher levels of FR can introduce significant errors in SAR image interpretation and analysis, particularly for polarimetric decompositions that rely on channel ratios.”

Meyer, F. J., and J. B. Nicoll (2008), “Prediction, detection, and correction of Faraday rotation in full-polarimetric L-band SAR data,” IEEE Transactions on Geoscience and Remote Sensing, 46(10), 3076–3086

%%time

import numpy as np
import panel as pn
import holoviews as hv
import hvplot.xarray  # noqa

hv.extension("bokeh")
pn.extension()

omega_rad, quality = faraday_rotation_circular_basis(
    subset_ds.HH,
    subset_ds.HV,
    subset_ds.VH,
    subset_ds.VV,
    ly=10, # adjust multilooking window height
    lx=10, # adjust multilooking window width
)

omega_deg = np.degrees(omega_rad)
omega_deg.name = "faraday_rotation_deg"
omega_deg.attrs["units"] = "degree"

quality_thresh = 0.3 # Set quality threshold for masking 
omega_deg_masked = omega_deg.where(quality >= quality_thresh)
omega_deg_masked.name = "faraday_rotation_deg_quality_masked"
omega_deg_masked.attrs["units"] = "degree"
omega_deg_masked.attrs["quality_threshold"] = quality_thresh
omega_deg_masked

omega_web = omega_deg_masked.rio.reproject("EPSG:3857")
quality_web = quality.rio.reproject_match(omega_web)
omega_web = omega_web.compute()
quality_web = quality_web.compute()

print(f"FR min/max: {float(omega_web.min())}, {float(omega_web.max())}")
mean_abs_deg = float(np.abs(omega_deg_masked).mean(skipna=True))
print(f"Mean abs FR: {mean_abs_deg}")
median_abs_deg = float(np.nanmedian(np.abs(omega_deg_masked).compute()))
print(f"Median abs FR: {median_abs_deg}")
std_deg = float(omega_deg_masked.std(skipna=True))
print(f"Std FR: {std_deg}")
print(f"Quality min/max: {float(quality_web.min())}, {float(quality_web.max())}")
print(f"Quality valid pixels: {int(quality_web.count())}")

xmin = float(omega_web.x.min())
xmax = float(omega_web.x.max())
ymin = float(omega_web.y.min())
ymax = float(omega_web.y.max())

alpha = pn.widgets.FloatSlider(name="FR alpha", start=0.0, end=1.0, step=0.05, value=1.0)
show_fr = pn.widgets.Toggle(name="Show Faraday rotation", value=True)
show_quality = pn.widgets.Toggle(name="Show quality", value=False)

tiles = hv.element.tiles.EsriImagery().opts(
    xlim=(xmin, xmax),
    ylim=(ymin, ymax),
    frame_width=700,
    frame_height=500,
    active_tools=["wheel_zoom"],
)

@pn.depends(alpha, show_fr, show_quality)
def view(alpha, show_fr, show_quality):
    layers = tiles

    if show_fr:
        fr = omega_web.hvplot.image(
            x="x",
            y="y",
            cmap="RdBu_r",
            clim=(-5, 5),
            frame_width=700,
            frame_height=500,
            title="Faraday Rotation",
        ).opts(
            alpha=alpha,
            xlim=(xmin, xmax),
            ylim=(ymin, ymax),
        )
        layers = layers * fr

    if show_quality:
        q = quality_web.hvplot.image(
            x="x",
            y="y",
            cmap="viridis",
            clim=(0, 1),
            colorbar=True, 
            frame_width=700,
            frame_height=500,
            title="Faraday Rotation Quality",
        ).opts(
            alpha=1.0,
            xlim=(xmin, xmax),
            ylim=(ymin, ymax),
        )
        layers = layers * q

    return layers

pn.Column(show_fr, alpha, show_quality, view)

7. Summary

You now have the tools and knowledge that you need to search GSLC data with asf_search, stream it with ffspec and xarray, and estimate Faraday rotation caused by passage of the signal through the ionosphere.