Changelog¶
Unreleased¶
New Features¶
-
groupsupport inrange_probability— the stat now honours thegroupfield (month,season,year) like the other ops, computing probabilities within each group and returning a group dimension. Previouslygroupwas accepted and silently discarded: the call returned the ungrouped all-record probability, but the pipeline still appended the group to the output variable name, so the result looked grouped and was not. Denominators are computed once per variable and shared across all ranges defined on it. -
groupsupport inpcount— the stat now honours thegroupfield, reporting the percentage of valid samples within each group and returning a group dimension. The denominator is the number of timesteps belonging to the group, not the length of the whole record, so a variable with no gaps reads 100 in every group.
Bug Fixes¶
statdirwith more than one function raisedMergeError— every stat names its output after the variable it reduced, sofuncs: [mean, max]produced two datasets that both containedhsand the merge of the two sector results failed with conflicting values for variable 'hs'. The stat was therefore usable only with the single function it exists to avoid. Delegated outputs are now suffixed with the function that produced them (hs_mean,hs_max), including when only one function is requested, so a name never depends on the length offuncs. The pipeline no longer appends_statdiron top of a name that already carries the name of the delegated stat, which would have givenhs_mean_statdir_direc; the written names arehs_mean_direcandhs_max_direc— identical to those of the equivalent per-function calls withnsectorset, and they resolve throughattributes.ymllike any other output. Single-functionstatdiroutput previously written ashs_statdirwill be written ashs_mean_direc; when appending to an existing store the old variable is not removed automatically.nsectoron astatdircall binned the data twice —nsectoris a pipeline-level field, so it was consumed by the pipeline's own sectoriser and never reached the stat, which then applied its own default of 4 sectors inside each of the outer ones. The documented eight-sectorstatdirexample failed with conflicting sizes for dimension 'direction'. The sector settings are now forwarded to stats that sectorise internally instead of being applied around them, sonsector: 8yields the eight sectors asked for.statdiracceptsgroupagain — it forwardedgroupto its delegated stats through**kwargsbut declared nogroupparameter of its own, so the new signature-based guard classed it as unable to group and rejected the call.groupis now an explicit parameter passed through to every delegated stat, and a call naming a stat that cannot honour it (funcs: [mean, rpv]withgroup: month) fails up front, naming the offender, rather than silently returning an ungrouped result for it.output.chunkswas silently ignored — the pipeline read the output chunking asself.config.output.__dict__.get("chunks", {}), butOutputConfigdeclared nochunksfield, and Pydantic had already dropped the key while validating the config. A user who setchunks:underoutput:got the default chunking of the finalised dataset — typically a single chunk per variable — with no warning, however large the grid.chunksis now a declareddict[str, int]field, read by normal attribute access, and reachesfinalise, which applies it to the dimensions present in the output and clears the stale chunk encoding inherited from the source.- Grouped variables were written with no attributes at all — the pipeline appends the
group to the output variable name, so a
group: monthcall onmeanwriteshs_mean_month. Attribute resolution split that name on_and read the last token as the statistic, foundmonthin neither thestatsnor thedata_varsmap, and gave up: the variable was delivered with nounits, nostandard_nameand nolong_name, while its ungrouped siblinghs_meanwas fully described. The group suffix — and the_direcsuffix a directionally sectorised call appends after it — are now stripped before the statistic and the parent variable are looked up, sohs_mean_monthandhs_mean_month_direcboth resolve throughhs_meanand carryunits: m. A grouped variable additionally has itslong_nameprefixed with the matching adjective, taken from an explicit mapping rather than built by string arithmetic (month→ monthly,season→ seasonal,year→ annual; the previous, unreachable attempt at this would have produced "seasonly"). Only a trailing group token counts as a group, so a parent variable whose own name contains a group word is not mangled. The override that keeps a probability or a percentage dimensionless still applies on top, sohs_gt_1p5_range_probability_monthremainsunits: "1". Values are unchanged, but grouped fields already written carry no metadata and will not gain any until they are rewritten or re-consolidated. countno longer inherits the units of its parent variable —hs_countwas written withunits: malthough it is a number of samples.countis now declared in thestat_unitssection ofattributes.ymlas"1", the CF/UDUNITS spelling of a dimensionless quantity, alongsidedist, whose joint histograms are raw counts and had exactly the same problem. Values are unchanged; only metadata is affected.- Grouped
pcountoutput was the ungrouped value —pcountacceptedgroupand silently discarded it, yet the pipeline still appended the group to the output variable name, so agroup: monthconfig wrote the whole-record coverage under a name likehs_pcount_month, with nomonthdimension. Now that the stat honoursgroupthe name matches the data. Any groupedpcountfield produced before this change is wrong and should be regenerated. - Requesting
groupon a stat that cannot honour it is now an error —rpv,hmoanddistribution3_timestepabsorbedgroupinto**kwargsand ignored it while the pipeline appended it to the output variable name, producing an ungrouped result wearing a grouped name. Such a call now raises before any data is loaded, naming the stat and listing the stats that do support grouping. Support is determined by inspecting the stat's signature for an explicitgroupparameter, so it cannot drift out of sync with the code. This is a behaviour change callers may notice: a config that previously ran to completion with a meaninglessgroupon one of these stats will now fail, and the fix is to drop thegroupfield from that call. Grouping is deliberately not implemented for the three — a per-group extreme-value fit uses a fraction of the samples and is not statistically defensible, and grouping is not well defined for a Welch spectrum or for a histogram accumulated over windows that do not align with time groups. rpvtimestep derivation no longer assumes nanosecond time — the sampling interval was computed asfloat(np.diff(time)[0]) / 3.6e12, which raisesTypeErrorunder NumPy 2 for anydatetime64resolution other than nanoseconds (and would silently mis-scale if it did not). It now goes throughpd.Timedelta, matching the fix already applied inexceedance.uorbno longer returns a value outside the water column — the evaluation point is a height above the seabed,z_bed, and withreference: surfaceit is derived asdepth - z, soz: 15in 3 m of water asked for a point 12 m below the bed. The transfer functioncosh(k z_bed)/sinh(k h)is even inz_bed, so that point silently returned the value for its mirror image 12 m above the bed — well clear of the surface and larger than the true surface value (Hs = 2 m, Tm02 = 9 s in 5 m of water gave 2.04 m/s against a true surface value of 1.46 m/s). The same applied toreference: bedwithzgreater than the depth. Points withz_bed < 0orz_bed > hare now NaN, tested per timestep and per cell so the mask follows the bathymetry; the seabed (z_bed = 0) and the still-water surface (z_bed = h) remain valid. Values inside the water column are unchanged. Anyuorb_sfc_*field already computed at a fixed depth below the surface is affected wherever the water is shallower than that depth and should be regenerated.- Statistics no longer inherit the units of their parent variable —
hs_pcountwas written withunits: malthough it is a percentage, andhs_gt_1p5_range_probabilitycarriedunits: mplus thestandard_nameandlong_nameof a wave height although it is a dimensionless fraction in [0, 1].attributes.ymlgains astat_unitssection declaring the units of statistics that do not preserve those of their parent (pcount: "%", and"1"forprobability,exceedanceandnonexceedance), merged from themetadataconfig like the other sections. The key is matched against any_-separated token of the output variable name, so grouped names such ashs_gt_1p5_range_probability_monthresolve too, and names inherited from the parent are dropped rather than left describing the wrong quantity.pcountandrange_probabilitynow also set correct attributes at the source, the latter with along_namespelling out the range (probability of hs >= 1.5 m). Values are unchanged; only metadata is affected. - Durational exceedance is now actually grouped —
exceedance/nonexceedancewith bothdurationandgroupset previously computed the ungrouped result and broadcast it across the group dimension, giving every month the same value. Runs are now identified on the continuous record and then grouped, so events are neither truncated at group boundaries nor spliced across the gaps a grouper introduces (amonthgrouper places every January back to back). - Runs touching the ends of the record are no longer missed — run detection used
scipy.signal.find_peaks, which only reports plateaus bounded by lower values on both sides, so a qualifying spell at the very start or end of the record was dropped (a record entirely above the threshold returned a probability of 0.0). Detection is now based on run boundaries and includes them. Durational exceedance values may increase slightly as a result. - Timestep derivation no longer assumes nanosecond time — the sampling interval was
computed as
float(np.diff(time)[0]) / 3.6e12, which raisesTypeErrorunder NumPy 2 for anydatetime64resolution other than nanoseconds (and would silently mis-scale if it did not). It now goes throughpd.Timedelta.
Breaking Changes¶
- Unknown fields under
outputare now rejected —OutputConfiginherited Pydantic's default of dropping keys it does not know, which is howoutput.chunkscame to be written in configs, accepted by validation and never applied. Any misspelling had the same failure mode:chunk:forchunks:, orconsolidated:forconsolidate:, left the option unset while the run reported success. The model now forbids extras, so such a config fails validation naming the offending key. A config carrying a stray key underoutputthat used to run will now fail, and the fix is to correct or remove the key — the setting it was meant to express was never in force. - Fractional exceedance thresholds are named with
p—exceedance/nonexceedanceoutput variables now write the decimal point asp, so a threshold of 1.5 giveshs_1p5rather thanhs_1.5, keeping the name usable as an identifier and by attribute access. Integral thresholds are unchanged (2.0still giveshs_2). Configs using fractional thresholds will write new variable names; when appending to an existing store the old dotted variables are not removed automatically. - Default
range_probabilitylabels are named withp— adata_rangesentry with nolabelbuilt its output name from the raw bounds, so a fractional bound producedhs_1.5_to_max, a name that is not a valid identifier and is awkward in path-like contexts. The decimal point is now written asp(hs_1p5_to_max), matchingexceedance; the formatting is a single helper shared by both ops so the two cannot drift apart. Integral bounds are unchanged (hs_1_to_2), and entries that setlabelexplicitly are unaffected. Configs relying on the default label with a fractional bound will write new variable names; when appending to an existing store the old dotted variables are not removed automatically.
2.3.0 (2026-06-26)¶
New Features¶
updiroutput upload — theupdirfield on the output config is now implemented. After writing, the output (Zarr store or NetCDF file) is uploaded to the given remote directory under its basename via fsspec, so it works forgs://,s3://, and local targets (e.g.outfile: ./scratch/hs.zarr+updir: gs://bucket/stats→gs://bucket/stats/hs.zarr). Useful for deployments that compute to fast local disk and then publish the result. Skipped whenoutfileis already remote.- Inline config via
$CONFIG—gridstats runcan now read its YAML config from the$CONFIGenvironment variable instead of a path argument, for deployment tools that inject the config inline (e.g. Argo on k8s). The path argument takes precedence when both are present. hmosampling frequency control — thehmostat accepts an explicitfs(Hz) parameter to override the sampling frequency. When omitted,fsis now inferred from the mean sampling interval over the whole record (rounded to 3 decimal places) rather than the first interval alone, making it robust to small timing jitter in model output whose nominal cadence wanders by a few percent. Numeric (seconds) anddatetime64time coordinates are both supported.
2.2.0 (2026-05-04)¶
New Features¶
-
modestat — new generic histogram-based mode stat. Returns the centre of the most-occupied bin along the reduction dimension. Accepts explicitbinsedges and an optionalweight_varfor energy- or Hs-weighted modes. Supports the samegroupparameter as all other aggregations. Particularly suited for ordinal data such as the Douglas Sea Scale — pass half-integer edges (e.g.[-0.5, 0.5, ..., 9.5]) to get one bin per integer degree. Dask-compatible viaapply_ufuncwithvectorize=True. -
Output masking — new
mask:block on the output config applies a spatial mask to all output variables before writing. Two types are supported:notnull(keep points where a source variable is non-null) andthreshold(keep points satisfying a numerical condition). Both accept an optionalisel:dict to reduce the source variable to a 2-D slice before the mask is computed, and xarray broadcasting ensures the mask is applied correctly across any extra dimensions (time, quantile, direction).
Documentation¶
- Douglas scales — corrected scale descriptions in the
douglas_seadocs table (e.g. "Rippled" → "Calm (rippled)", "Wavelets" → "Smooth") and added a full degree/description/range table to thedouglas_swelldocs, including a note on degree 9. - Crossing seas — rewrote the
crossing_seasdocs section: added a direction convention warning (all inputs must use the same coming-from or going-to convention), guidance on threshold choices (Li 2016 defaults and operational variants), and the full Li (2016) reference with DOI.
Bug Fixes¶
- Douglas sea scale — fixed an off-by-one error where all degrees were shifted one lower than the correct Douglas classification (e.g. a 3 m sea was reported as degree 4 "moderate" instead of degree 5 "rough"). Degree 9 (phenomenal, Hs > 14 m) was also never assigned.
- Crossing seas — changed
hs_thresholddefault from0.0to0.5m. A zero threshold allowed the function to flag crossing seas in near-calm conditions where partition directions are undefined and noisy. The Li (2016) criterion is meaningful only when both systems are energetically significant;0.5m is the standard operational floor.
Improvements¶
- Douglas sea scale — replaced the 9-iteration
xr.whereloop with a singlexr.apply_ufunc(np.digitize, …)call. Produces a flat dask graph (one task per chunk instead of nine) and runs ~1.5× faster on eager arrays; gains are larger under dask where the stacked task graph was a scheduling bottleneck. - Douglas swell scale —
DOUGLAS_SWELL_BINSreplaced withDOUGLAS_SWELL_INTERVALS, a degree-keyed dict ofpd.Intervalpairs matching the original onstats definition. Degree 9 ("confused") is documented as a crossing-seas condition that cannot be assigned from height/wavelength thresholds alone; it was also unreachable in the original (pd.Interval(inf, inf)is never satisfied by any finite value). - Douglas swell scale — replaced the 9-iteration
xr.whereloop withxr.apply_ufunc(_classify_swell)backed by twonp.digitizecalls and a 3×3 LUT. Eager compute cost is unchanged (2D classification has similar work to the loop), but the dask task graph shrinks ~7× (54 → 8 tasks per chunk), reducing graph construction and scheduler overhead on large multi-chunk hindcasts.
2.1.0 (2026-05-01)¶
New Features¶
- Derived variables — new top-level
gridstats.derivedsubpackage computes secondary variables (wind speed/direction, current speed/direction, wave parameters, wave orbital velocity, sky conditions) directly from source data before stats are applied. - Modal direction — new
modal_directionstat implemented as a vectorised ufunc. - Zarr append / consolidate — new
appendandconsolidateoutput options enable parallel Zarr writes across tiled or distributed runs. - Custom dataset metadata — new
global_attrsfield on the output config block lets you inject arbitrary CF-compliant global attributes into the output dataset.
Bug Fixes¶
- Derived variables were silently dropped when
data_varswas set on a call; this is now fixed so derived variables survive variable selection.
Improvements¶
- Info-level logging added for loader calls and derived variable definitions, making it easier to trace pipeline execution.
- Docs restructured: configuration, loaders, and derived variables each have their own section with per-topic pages; MathJax support added for equation rendering.
2.0.0 (2026-03-25)¶
A near-complete rewrite that renamed the library from onstats to gridstats and replaced the legacy architecture with a modern, well-typed stack.
Breaking Changes¶
- Library renamed from
onstatstogridstats; all imports must be updated. - Configuration schema rewritten using Pydantic v2 — existing YAML configs require migration (see the Configuration docs).
slice_dictreplaced by explicitsel/iselfields on source config.- Source config now uses a discriminated union: set
urlpathfor xarray orcatalog+dataset_idfor intake — the oldtype:field is gone. - CLI commands changed:
gridstats run config.ymlandgridstats list-stats(powered by Typer instead of the old argparse CLI).
New Features¶
- Pipeline orchestrator — new
Pipelineclass handles loading, stat dispatch, tiling, directional sectorisation, and output in a single object. - Registry with entry-point plugins —
@register_stat/@register_loaderdecorators; third-party packages can contribute stats or loaders via thegridstats.stats/gridstats.loadersentry-point groups. - Spatial tiling —
tiles:on any call keeps peak memory bounded for large grids. - flox integration —
use_floxper-call toggle (defaultTrue); automatically disabled forquantileto avoid ~2× memory overhead. - open_kwargs on
XarraySourceConfig— pass arbitrary kwargs toxarray.open_dataset. - CF attributes —
gridstats/attributes.ymlmaps variable names to CF standard_name / long_name / units; applied automatically on output. - Packaging migrated to
pyproject.toml; mkdocs documentation site published to GitHub Pages; GitHub Actions CI/CD added.
1.2.0 (2022-06-26)¶
New Features¶
- CF attribute support — variable attributes (standard_name, long_name, units) can now be declared in config and are written to the output dataset.
- Global attributes — pipeline-level
global_attrsfor dataset-level metadata.
Bug Fixes¶
- Fixed crash with non-uniform chunk sizes during output writing.
1.1.0 (2022-05-17)¶
New Features¶
- Probability stats — new module with exceedance and non-exceedance probability
functions (
pexc,pcount). - Allow transposing, fixing dtype, and setting chunks on output variables.
- Dask cluster can now be enabled or disabled individually per call.
chunkscan be specified when opening datasets from an intake catalog.
Bug Fixes¶
- Fixed chunking when applying a land mask.
- Fixed bug in distribution function output.
Internal Changes¶
- Removed deprecated KMZ stats code.
1.0.0 (2021-11-25)¶
New Features¶
- Return period values (RPV) — new
rpvstats module supporting groupby time aggregations. - Distribution stats — histogram / empirical distribution functions.
- Count function — count non-NaN observations.
- Directional sectorisation —
nsector/ directional bin support for computing stats per direction sector. - Stepwise decorator — compute stats in time chunks to bound peak memory on large datasets.
- Dask cluster — explicit cluster setup with configurable worker count (including
all/halfshortcuts). - Logging throughout the pipeline.
- Refactored to a CLI entry point.
0.1.0 (2021-06-18)¶
First release on PyPI.