Processing#

easysnowdata.processing#

Pure processing functions (design contract §2.6): xarray in, xarray out, no I/O.

  • masks — Sentinel-2 SCL and HLS Fmask masks, nodata masking

  • optical — baseline harmonization, metadata-driven scale/offset, the UDM2 mask decoder

  • sar — dB conversion, border-noise removal, slope/aspect and the local incidence angle

  • snow — snow-product class tables and the SNODAS flat-file reader

  • wateryear — vectorized water-year helpers

  • categorical — CF flag attributes for categorical products (the contract’s replacement for class_info dicts)

  • contract — the output contract (§2.5): CRS via both accessors, dim names, nodata policy, provenance attrs

add_water_year_coords

Attach water_year and dowy coordinates along dim.

station_hemispheres

"southern" where the latitude is negative, else "northern".

apply_fmask

Mask obj to NaN where Fmask flags are set (fmask defaults to obj["Fmask"]).

apply_scl_mask

Mask obj to NaN where the SCL class is in remove (scl defaults to obj["scl"]).

day_of_water_year

Day of the water year, 1-indexed (1 October = 1 in the northern hemisphere).

db_to_linear

10 ** (x / 10).

decode_udm2

Decode a PlanetScope UDM2 mask into named variables.

flag_mask

Boolean mask that is True where the array holds any of meanings.

flags

Read CF flag attributes into a DataFrame with value, meaning, color.

fmask_aerosol_level

The aerosol level 0–3 encoded in Fmask bits 6–7.

fmask_bit

The value (0/1) of bit in each Fmask pixel.

fmask_mask

Boolean mask that is True where a pixel is kept.

from_class_info

Convert the legacy {value: {"name": ..., "color": ...}} dict to flag lists.

harmonize_s2_baseline

Undo the post-2022-01-25 Sentinel-2 L2A offset so all dates share one baseline.

linear_to_db

10 log10(x); non-positive values become NaN.

local_incidence_angle

Local incidence angle from a DEM and the ellipsoidal incidence angle.

look_azimuth

Radar look azimuth in degrees from the platform heading.

mask_nodata

Replace the nodata value with NaN (converting integers to float).

ndsi_flag_attrs

CF attributes for an NDSI byte: percentages plus the sentinel meanings.

parse_snodas_header

Parse a NOHRSC .txt header into a dict.

remove_border_noise

Mask falsely low backscatter (border noise) in linear-power data.

scale_offset

Apply value * scale + offset and return float data.

scl_mask

Boolean mask that is True where a pixel is kept (its SCL class is not in remove).

set_flags

Attach CF flag attributes to a categorical DataArray.

slope_aspect

Slope and aspect of a projected DEM, by central differences.

udm1_bit

The value (0/1) of one UDM1 bit ("blackfill", "cloud_udm1", …).

water_year

The water year of each time as an integer (calendar year the WY ends in for the north).

water_year_start

The first day of the water year containing each time (same container type as the input).

easysnowdata.processing.categorical#

CF flag attributes for categorical products.

The design contract (§2.5) forbids Python objects in .attrs: a categorical variable carries flag_values (list of ints), flag_meanings (one space-separated string, blanks inside a meaning become underscores) and flag_colors (space-separated hex strings). These helpers write and read that form; easysnowdata.plotting turns it into colormaps and legends.

meaning_key

Normalise a class name to a CF flag_meanings token (blanks → underscores).

easysnowdata.processing.contract#

Output-contract helpers (design contract §2.5), shared by every theme loader.

Pure functions: they rename dimensions, write the CRS with both the rio and odc accessors, mask nodata the way the contract prescribes (categorical keeps the sentinel with rio.nodata set; continuous is NaN-masked with encoded_nodata preserved) and stamp the standard provenance attributes (source, source_url, product_id, data_citation, license, easysnowdata_version). No I/O.

chunks_policy

Give a loader's chunks= the one meaning every loader shares.

geographic_dims

Rename spatial dims to the contract: latitude/longitude for a geographic CRS, y/x for a projected one.

write_crs

Write crs so that both obj.rio.crs and obj.odc.crs read it.

mask_continuous

NaN-mask a continuous variable and keep the encoded nodata value.

set_categorical_nodata

Keep the source sentinel and record it as rio.nodata (and attrs['nodata']).

provenance

The standard provenance attrs for a catalog product loaded via source.

apply_variables

Apply the catalog Variable definitions present in ds.

finalize

Bring a loaded object onto the output contract.

finalize_frame

Bring a loaded GeoDataFrame onto the output contract (§2.5).

easysnowdata.processing.masks#

Quality masks: Sentinel-2 SCL classes, HLS Fmask bits, nodata.

easysnowdata.processing.optical#

Optical processing: Sentinel-2 baseline harmonization, metadata-driven scale/offset, and the PlanetScope UDM2 mask decoder. Pure functions.

Band arithmetic is deliberately not wrapped here. A normalized difference is one line of xarray, and hiding it behind ndsi(ds) obscured which bands were being used and what happened to negative reflectance:

ndsi_da = (s2_ds["green"] - s2_ds["swir16"]) / (s2_ds["green"] + s2_ds["swir16"])
rgb_da = s2_ds[["red", "green", "blue"]].to_array("band").clip(0, 0.3) / 0.3
rgb_da.isel(time=0).plot.imshow(rgb="band")

The gallery examples spell these out every time they are used.

easysnowdata.processing.sar#

SAR backscatter helpers: dB conversion and Sentinel-1 border-noise removal.

easysnowdata.processing.snow#

Snow-product processing: class tables and the SNODAS flat-file reader.

Pure functions (design contract §2.6). The class tables are the MODIS and VIIRS sentinel values that share a byte with the NDSI percentage. Thresholding that byte is left to the user, in the open, because the choice of threshold and of what to do with cloud, night and water is the analysis:

ndsi_da = snow_ds["CGF_NDSI_Snow_Cover"]
valid_da = ndsi_da <= 100                       # 200+ are the sentinels below
binary_da = (ndsi_da >= 40).where(valid_da)     # NaN where the pixel says nothing

repair_fill_values

Put the upstream 256/265 fill values back to 255 and return uint8.

snodas_members

Group a SNODAS day's tar into {stem: {"dat": ..., "txt": ...}}.

snodas_array

Build a georeferenced DataArray from one SNODAS .dat and its header.

easysnowdata.processing.wateryear#

Vectorized water-year helpers.

This is the single copy of these functions (§12 Q16): the scalar utils.datetime_to_WY/datetime_to_DOWY that used to be applied with pd.Index.map and the global_snow_networks utils/utils.py implementations are both reconciled here. The names wy_start/wy_end from that repo are water_year_bounds(), wy_date_range is water_year_range(), wy_length is water_year_length() and add_wy_coords is add_water_year_coords(); all of them also take a hemisphere, which the originals did not.

A northern-hemisphere water year starts 1 October and is named for the calendar year in which it ends (WY 2021 = 2020-10-01 … 2021-09-30); a southern-hemisphere one starts 1 April and is named for the year it starts. For aggregation prefer the pandas anchored offsets: swe_ds.resample(time="YS-OCT").max(). Day-of-water-year has no pandas primitive, hence day_of_water_year() and add_water_year_coords().

The two namings line up: water year N is, in both hemispheres, the snow season that peaks in calendar year N. The southern convention is the one the Andes SWE reanalysis uses (Cortés and Margulis 2017; its Dryad record, doi:10.5061/dryad.ngf1vhj0s: “WY1985 corresponds to April 1st, 1985 through March 31st, 1986”).

A (station, time) Dataset can mix hemispheres. hemisphere="auto" in add_water_year_coords() gives each station its own water year from its latitude (station_hemispheres()).

water_year_bounds

First and last day of water year year.

water_year_range

Daily DatetimeIndex spanning water year year (365 or 366 days).

water_year_length

Number of days in water year year — 366 when 29 February falls inside it.