Skip to content

Collocation and SPARE-ICE updates - #431

Open
olemke wants to merge 23 commits into
atmtools:masterfrom
olemke:apply-lorena-collocation-updates
Open

Collocation and SPARE-ICE updates#431
olemke wants to merge 23 commits into
atmtools:masterfrom
olemke:apply-lorena-collocation-updates

Conversation

@olemke

@olemke olemke commented Nov 20, 2024

Copy link
Copy Markdown
Member

This PR consolidates a series of updates to the collocation machinery and its satellite data pipeline, many of which were contributed by Lorena Kowalczyk as part of her Master's thesis on SPARE-ICE. The collocator now combines all matching secondary files into a single dataset per primary file (instead of processing only the first match), tolerates NaT time values, and sorts inputs by time before collocating. The PR also bundles fixes to the FileSet/NetCDF4 layer that these workflows depend on: symlinked directories are now discovered and read correctly, integer dtypes survive NetCDF4 round-trips, subgroups are written in an order that older readers can parse, and decompression uses collision-free temporary file names. SPARE-ICE retrieval benefits from corrected elevation-map indexing, compatibility with models saved by older scikit-learn versions, and updated validation plots. Along the way, the collocation and SPARE-ICE code was modernized to be compatible with Python 3.14.

Changes

  • typhon/collocations/collocator.py: Combine all overlapping secondary files via xr.concat before collocating, mark the combined files in the __file variable, return early when no file matches are found, and sort input datasets by time to avoid xarray stacking problems
  • typhon/collocations/collocator.py: Replace pd.Timestamp(values.min().item()) with pd.to_datetime(...).min() so datasets containing NaT are handled, cast interval output to timedelta64[ns], and drop the MultiIndex explicitly before storing collocation output
  • typhon/files/fileset.py: Switch find() from a semi-open to a closed interval (end inclusive) to match collocate(), allow match() to skip file-not-found errors via skip_file_errors, and glob directories without a trailing slash plus an isdir() filter so symlinked directories are found; use unique (secrets.token_hex) decompression targets to avoid name clashes between processes and replace the private tempfile._get_candidate_names with secrets
  • typhon/files/handlers/common.py: Write NetCDF4 subgroups before their parent groups so dimensions are declared locally, and unwrap fully-unmasked MaskedArrays before handing them to xarray to preserve integer dtypes (e.g. scnline, channel coordinates); skip identity renames when writing groups so variables already named without a group prefix are left untouched; add a read_from_list() helper for concatenating multiple files; use Dataset.sizes instead of the deprecated Dataset.dims
  • typhon/files/handlers/cloudsat.py: Derive file start/end times from the filename (year/day-of-year) and the UTC_start/Profile_time fields, making get_info work for both R04 and R05 data, and expose scnline as a 1-based coordinate
  • typhon/files/handlers/tovs.py: Degrade gracefully when AVHRR packed-pixel interpolation fails (log a warning and return None instead of aborting), report the affected file in the error message, and skip broken MHS files that lack the scnline dimension
  • typhon/retrieval/common.py: Restore SPARE-ICE models saved with old scikit-learn versions — remap un-prefixed sklearn module names (e.g. sklearn.tree.treesklearn.tree._classes), fall back to n_features_ when n_features_in_ is None, backfill the missing_go_to_left node field, decode structured dtypes in their dictionary representation (numpy ≥ 2), and filter constructor params against the class signature; exclude _repr_* attributes when serializing models
  • typhon/retrieval/spareice/common.py: Correct elevation-grid cell indexing (offset to -90/-180 origin), zero the elevation column via .loc, replace -inf IWP values without in-place mutation, convert collocations with to_xarray() before retrieval, and rework the validation plots (PDF output, confusion-matrix heatmap with feature-importance panel, framing/spine and axis-limit fixes)
  • typhon/geographical.py: Guard against division by zero in gridded_mean so empty cells return 0 instead of nan, and additionally return the mean of the squared values (useful for variance estimates)
  • typhon/tests/...: Add tests for symlinked directory discovery/reading, locally-declared subgroup dimensions, and int-coordinate dtype round-trips; update test time ranges for the closed-interval find() semantics

Breaking Changes

⚠️ FileSet.find() now treats end as inclusive (closed interval) instead of semi-open. Code that relied on the previous exclusive-end behavior will see one additional file at the interval boundary. gridded_mean() now returns three values (mean, mean of squares, count) instead of two.

olemke added 8 commits August 11, 2026 11:39
Improvements and fixes made by Lorena during her Masters' thesis.

Collocator (typhon/collocations/collocator.py):
- Extend `collocate_dataset` to handle multiple secondary files per
  primary: collect all secondaries matched to a primary, concatenate
  them along the time dimension, renumber the time index, and join
  their file paths with "::" in the `__file` attribute. The first
  secondary's file attributes are taken as representative for the
  combined group.
- Return early from `collocate_dataset` when no matches are found, and
  propagate `skip_file_errors` through `FileSet.match`.
- Sort the primary and secondary datasets by time at the start of
  `collocate` so unsorted input files cannot break xarray's stack/sel.
- Switch the common-period computation to `pd.to_datetime` so NaT
  values introduced by concatenating secondaries no longer break the
  min/max calculation.
- Cast the `interval` DataArray to `timedelta64[ns]` and fix the
  "tempoerally" typo in a comment.

Collocations common (typhon/collocations/common.py):
- Use `np.int64` explicitly for the row counters in
  `_rows_for_secondaries` so results stay consistent across platforms.

FileSet (typhon/files/fileset.py):
- Treat the `find` search interval as closed (no longer subtract a
  microsecond from `end`) so end-time files are included consistently
  with `collocate`'s common-period computation.
- Add `skip_file_errors` to `match` and search the secondary fileset
  over an `max_interval`-extended window while keeping the primary
  search on the original [start, end].
- Append the original file name (plus a temp suffix) to the decompress
  target path so decompressed HDF4/CloudSat files keep a recognisable
  name in the temp directory.

CloudSat handler (typhon/files/handlers/cloudsat.py):
- Rewrite `get_info` to work for both R04 and R05 granules: parse year
  and DOY from the filename and combine `UTC_start` with the last
  `Profile_time` value instead of relying on the `start_time`/`end_time`
  global attributes.
- Promote `scnline` to a coordinate (numbered from 1) after reading.
- Update the 2C-ICE documentation link to the current CloudSat DPC
  page.

NetCDF4 handler (typhon/files/handlers/common.py):
- Add `read_from_list` helper that reads a list of FileInfo objects
  and concatenates the resulting datasets along a caller-supplied
  common dimension.

AVHRR GAC handler (typhon/files/handlers/tovs.py):
- Pass `file_info` into `_interpolate_packed_pixels` so the "too many
  NaNs" error message can name the affected file, and let the granule
  be read as None when the interpolation raises.

RetrievalProduct (typhon/retrieval/common.py):
- Exclude attributes starting with `_repr_` (e.g. `_repr_html_`) from
  being deep-copied into the product dict.

SPAREICE (typhon/retrieval/spareice/common.py):
- Mask negative elevations via a `.loc` assignment to avoid
  chained-assignment warnings.
- Convert the input collocations to an xarray Dataset before pulling
  lat/lon/time/scnpos, and make the retrieval `index` 1-based to match
  `scnline`.
- Add `no_files_error` to `retrieve_from_collocations` and pass it
  through to `Collocations.map`. Fix `Timer` instantiation
  (`Timer().start()` rather than the classmethod form) and simplify the
  elapsed-time log line.
- Rework the SPARE-ICE report plots: enable the top/right axes spines
  for a framed look, save figures as PDF (instead of PNG) with
  `bbox_inches='tight'`, drop the per-experiment subdirectory, draw a
  zero line and set xlim on the bias/mfe plots, and stop hard-coding a
  y-range for the bias plot. Rewrite `_report_ice_cloud` to build the
  confusion matrix with seaborn `heatmap` (labels=[1,0], row-normalised
  with NaN-safe denominators), keeping the previous implementation as
  `_report_ice_cloud_old`.
Collocation end time is now inclusive instead of exclusive.
The secondary dataset sort guard was checking `isinstance(primary,
tuple)` in both branches due to a copy-paste bug, so the secondary
dataset was never sorted when the primary was not a tuple. Use
`isinstance(secondary, tuple)` for the secondary check.

Also drop a stray `print(collocated)` debug statement left inside the
`collocate` docstring example.
Clean up several leftover print() debug statements added during
development:

- collocations/common.py: drop "default read_mode is collapse" print
  issued whenever Collocations.read() is called with the default
  read_mode.
- files/fileset.py: drop print(file_info) on every read in
  _call_map_function.
- retrieval/spareice/common.py: drop print of ice_cloud
  feature_importances_ in _report_ice_cloud.
FileSet's decompress target names previously relied on the private
`tempfile._get_candidate_names()` generator (an underscore-prefixed
stdlib API that may change across Python versions). Use the public
`secrets.token_hex(8)` instead, which is stable and produces a random
filename suffix of the same shape.

Applied at both decompress call sites in fileset.py
(_get_info_via_handler and FileSet.read).
The interpolation fallback in AVHRR_GAC_HDF.read caught every exception
(including KeyboardInterrupt and unrelated bugs) and silently returned
None, masking real failures. Narrow it to `except ValueError` (the only
exception raised by _interpolate_packed_pixels) and log the skipped
file via a module-level logger.warning.
@olemke
olemke force-pushed the apply-lorena-collocation-updates branch from b3fa04f to c5ffad5 Compare August 11, 2026 10:16
olemke added 15 commits August 11, 2026 14:46
_tree_from_dict referenced the deprecated `n_features_` coefficient
key, which was renamed to `n_features_in_` in scikit-learn 1.0.
Update the deserialization to match the new attribute name so
RetrievalProduct trees reconstruct correctly with modern sklearn.
Replace typhon.utils.to_array with direct numpy handling when
rebuilding decision trees from stored coefficients. n_features_in_
and n_outputs_ are passed through unchanged, while n_classes_ is
coerced with np.atleast_1d/np.asarray to satisfy sklearn's
expectation of an integer array.
Trees trained with scikit-learn <1.3 lack the missing_go_to_left
field in their node ndarray dtype. Newer sklearn versions reject
this in Tree.__setstate__ with an incompatible-dtype error, making
trained SPAREICE products unloadable. Rebuild the nodes array with
the expected dtype (missing_go_to_left defaulting to 0, matching
pre-1.3 behaviour) before calling __setstate__.
Replace `dataset.dims.keys()` with `dataset.sizes.keys()` in
common._xarray_rename_fields and AAPP_HDF._test_coords to avoid
the FutureWarning about the upcoming change in `Dataset.dims`
return type.
Remove the MultiIndex coordinates before assigning a plain integer
coordinate, since a MultiIndex cannot be stored to a file.
netCDF4 returns unmasked variables as MaskedArrays. Passing these
directly to xarray promotes int types to float (NaN fill), which
turned int64 coordinates (e.g. scnline, scnpos, channel) of SPARE-ICE
collocation files into float64 on newer xarray/numpy stacks. Unwrap
fully unmasked arrays so the original dtype is kept.

Add a regression test verifying int64 coordinate dtypes survive a
write/read roundtrip.
Subgroups only inherit dimensions when the parent group declares
them first, so older readers that look at locally-defined dimensions
(group.dimensions) fail to map variables in the subgroups.

Sort groups by nesting depth so deeper subgroups are written first
and declare their dimensions locally. Add a regression test.
The helper that samples the elevation map computed cell indices
from the wrong origin and flipped the latitude direction. Use
lat + 90 (instead of 90 - lat) for south→north ordering at the
(-90, -180) origin.
fsspec's LocalFileSystem reports symlinks to directories as type
other rather than directory, so the trailing-slash glob pattern
in _get_matching_dirs skipped them entirely. Drop the trailing slash
and filter with isdir(), which follows symlinks. Add a regression
test for MHS files read through a symlinked placeholder directory.
Broken MHS AAPP files that lack the scnline dimension crashed the
reader. Return None and log a warning instead.
Also compute the mean of the squared data in each grid cell and return
it alongside the mean and the number of points.
Only rename variables, coordinates and dimensions whose names still
contain the group prefix, and use rename_dims for dimensions.
The bundled standard.json was trained with scikit-learn <1.0 and
failed to deserialize with modern versions. Remap the pre-1.0
module paths (sklearn.preprocessing.data, ...) in _import_class,
drop constructor params removed from the estimator signatures,
and fall back to the old n_features_ key when n_features_in_ is
missing or None. Also parse structured dtypes stored in their
dictionary representation, which numpy >=2 emits for aligned
dtypes such as decision tree nodes.
The inplace Series.replace silently does nothing under pandas 3.0
Copy-on-Write, leaving log10(0) = -inf in the iwp column. Clearsky
pixels (iwp=0) were then never NaN, so the training dataset
preparation found an empty clearsky subset and crashed.

Assign the replaced Series back to the column, which works across
all pandas versions.
@olemke
olemke force-pushed the apply-lorena-collocation-updates branch from f52e2d1 to 0a259dc Compare August 14, 2026 12:48
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant