Linear Scaling - Implementation

How LS is computed in src/bias_correction.py.

For the theory, see Methodology > Linear Scaling. This page is the algorithm view.

Entry point

The LS step lives inside the lseqm() function in src/bias_correction.py. It runs as the first stage before EQM.

# Conceptual (simplified) - actual code is interleaved with EQM logic
imerg_mean = imerg_dekad_data.mean(dim='time')

if _use_native_cpc:
    cpc_native_mean = cpc_native_dekad.mean(dim='time')
    cpc_mean = cpc_native_mean.interp(
        lat=imerg_dekad_data.lat, lon=imerg_dekad_data.lon, method='linear'
    )
    cpc_mean = cpc_mean.fillna(
        cpc_native_mean.reindex(
            lat=imerg_dekad_data.lat, lon=imerg_dekad_data.lon, method='nearest'
        )
    )
else:
    cpc_mean = cpc_dekad_data.mean(dim='time')

ls_scale_factor = xr.where(imerg_mean != 0, cpc_mean / imerg_mean, 1)
ls_corrected_precip = imerg_dekad_data * ls_scale_factor

Two paths through LS

Path Triggered by What it does
Smooth (BCSD) cpc_native_ds is passed CPC mean is computed at 0.5 deg, bilinearly interpolated to the IMERG 0.1 deg grid. Eliminates the 5x5 block boundary artefact you get from CPC’s nearest-neighbour downscaling.
Per-pixel cpc_native_ds=None CPC mean is computed directly on the 0.1 deg grid. Faster, but the downscaled CPC grid has visible block edges.

The smooth path is the default in the notebooks (nb02 passes cpc_native_ds=cpc_ds_native).

Edge handling on small AOIs

The reindex(method='nearest') fillna at line 8 (in the snippet above) is the one that matters. interp(method='nearest') returns NaN outside the convex hull of source cell centres; reindex(method='nearest') is a pure label lookup with no convex-hull restriction.

This was a real bug surfaced by Bali: the OLD code used interp(method='nearest') for the fallback, which meant the AOI-edge rows came back NaN whenever the CPC native cells did not fully bracket the AOI bbox. See the Changelog for the full story.

Cost

  • Memory: one full IMERG dekad slice + one CPC dekad slice, both 0.1 deg, both reduced to a per-cell mean. For Indonesia (171 x 461) that is two arrays of about 0.3 MB each. Negligible.
  • Time: per dekad, dominated by the imerg.mean('time') reduction. On Bali, sub-second.

Where the output goes

save_corrected_precip(ls_corrected_precip, ..., method_abbr='ls', folder=ls_corrected_precip_path) writes a per-dekad NetCDF under data/output/corrected_ls/. The land-sea mask is applied at write time.

Back to top