Cloud regime analysis - #417
Conversation
…ysis Only the obs-comparison path with pre-computed ISCCP labels had ever run; that path returns early before reaching most of this code. Everything else raised on first call. Crashes: - ot_library was undefined in _get_ref_cluster_labels (it is a parameter of cloud_regime_analysis and was never passed down). Thread it through opts. - cluster_labels_b was undefined in _plot_baseline_column, so baseline mode always failed. Use the reference labels already in plot_data. - _preprocess_data looked field_name up in ALL_VARS, but for observations field_name is the obs variable (e.g. n_pctaudist) and is not a key there. Carry product_name on VariableNames instead. - The obs column transposed its histogram for pcolormesh but the test and baseline columns did not. That raises for non-square histograms and silently plots transposed for square ones (ISCCP is 7x7). _calculate_weighted_mean_xr now returns (ht, tau) explicitly. - Baseline plotting averaged the raw reference data while the labels came from preprocessed data, so the two disagreed on time range and binning. Return the processed reference alongside the labels. Portability: - Replace the hardcoded personal regrid weights path with a documented regrid_weights_file argument; skip unstructured cases with a clear warning when it is absent. - Replace n_jobs = 36 with the num_procs value the script already loads. Correctness: - emd_method now defaults to "exact", matching the docstring. It had defaulted to sinkhorn, which the same docstring calls experimental and not recommended. - The RFO map counted NaN points as "not in this cluster" while the scalar total excluded them, so the two disagreed whenever land/ocean masking was on. - Year ranges are now per-case rather than case[0] applied to every case. Cleanup: - create_land_mask built the land point list, discarded it, then re-derived the mask by O(n_points x n_land) coordinate matching under numba. Record the result directly; _find_land_points is no longer needed. - Drop the dead plot_data['weights'] and its expensive stack, unused imports and locals, a leftover "identical to your original" comment, and a comment that described removing a panel as setting its ticks. - Fix the stacking() closure that sized arrays from the enclosing mat rather than its own argument, and assorted docstring typos. lib/adf_variable_defaults.yaml: - Restore '$\mu$g/m3' for BC/POM/SO4/SOA/DUST/SeaSalt. The branch had doubled the backslash, which mathtext cannot parse (ValueError at draw time). - Add obs_var_name for CLMODIS and CLD_MISR, matching FISCCP1_COSP. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
The obs_ht_var/obs_tau_var pairs were swapped for all three products. The obs files label their own axes, and the corrected assignment is what makes the valid bin counts match the supplied cluster centers exactly: ISCCP levtau "Optical thickness levels" (6) x levpc "Pressure levels" (7) = 42 MISR tau "cloud optical depth" (7 valid) x cth "altitude" (15 valid) = 105 MODIS COT optical thickness (7) x PRES pressure (7) = 49 These never ran before because _preprocess_data raised KeyError first. Skip the obs-comparison bin trimming for observational input. Trimming exists to bring model histograms onto the binning the centers were built on; the obs files already match, so trimming them dropped a real bin. Add a units check in compute_cluster_labels. Euclidean distance is not scale invariant, and MODIS_obs_data.nc stores cloud fraction (~0.5 per profile) while its centers are percent (~60), a factor of ~109. That silently assigned every profile to a single cluster instead of failing. It now warns. ISCCP and MISR obs are already on the same scale as their centers and do not trigger it. Verified against the real ADF_obs files: with these fixes the ISCCP obs clustering path reproduces the stored euclidean_cluster_labels exactly (100% agreement over the compared points). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
…re path Found by an actual model-vs-model run (CESM2 amip vs amip-p4K, ISCCP). Observation scaling now goes through the standard ADF mechanism. CRA loads the whole obs Dataset via load_dataset (it needs the stored cluster labels as well as the histogram), which bypasses the obs_scale_factor/obs_add_offset handling that load_da applies, so apply it explicitly. Add obs_scale_factor: 100 to CLMODIS: MODIS_CLD_HISTO is a cloud fraction while CAM's CLMODIS and the MODIS cluster centers are percent. With this, MODIS labels computed from the obs histograms reproduce the file's stored euclidean_cluster_labels exactly; before it was a factor of ~109 off and every profile collapsed into one cluster. ISCCP and MISR obs are already percent and are left alone. In baseline mode the first column was titled "Observation CR n, RFO = x%" while reporting the baseline's RFO, duplicating the neighbouring column. There are no observation labels in that mode, so show the centers without an RFO and title them by product. _calculate_rfo returned a bare None on its guard path while all three call sites unpack two values, turning a reported problem into a TypeError. Return the tuple shape and skip the affected map with a warning. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
|
Using Claude Code to bring this PR up to current main branch. Claude found several issues to fix, and now more of the code has been tested (model-vs-model) (MISR hasn't been validated still, but I'll probably try to do that with new runs soon.) Here's Claude's comment on the audit and fixes: Going in, only one code path had ever executed: obs comparison with pre-computed ISCCP labels, which returns early before reaching most of the script. Everything else raised on first call. Details below for reviewers. Crashes fixed
On the transpose: this does not affect native CAM output, which is Observation metadata was wrong
These never ran before because the MODIS units
Fixed through the existing ADF mechanism: Unrelated regression revertedThe branch's Portability and cleanup
Verification
Not covered — worth knowing before merge
|
- num_procs: use adf.num_procs (resolved int) rather than the raw config value from get_basic_info, which can be None or "*". - Years: use adf.climo_yrs for both test cases and the baseline. The raw start_year/end_year config entries are optional and often None, which silently disabled time subsetting. - Multi-case: write to plot_location[case_idx] and register figures under their own case name. Every case previously wrote to plot_location[0], and the CR-centers filename carried no case name, so cases in a multi-case run overwrote each other. - Figure extension now comes from diag_basic_info's plot_type. - obs_data_loc: check before Path(), which raised an uncaught TypeError and killed the whole ADF run when the setting was absent. - Remove the LANDFRAC branch of apply_land_ocean_mask. Callers always pass a single-variable DataArray, so it was unreachable, and reaching it raised AttributeError. - create_land_mask now returns a (lat, lon) DataArray so xarray broadcasts by dimension name, replacing _make_mask_broadcastable, which inserted dummy axes positionally and assumed lat/lon were the trailing dims. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Follow-ups to @nusbaume's review of the original cloud regime PR: - dask and joblib were unconditional module-level imports, so the script failed to import in the stock ADF environment. joblib now imports inside the POT branch that uses it, alongside `ot`. The only use of dask was one dask.config.set call that silenced a chunking PerformanceWarning while mutating global config for the rest of the ADF run; dropped. - Add pot and joblib to env/conda_environment.yaml so the documented Wasserstein option works out of the box. numba needs no entry: xesmf already requires it transitively, and the njit shim covers its absence. - Add the Davis & Medeiros (2024) DOI to the docstring. - Register cloud_regime_analysis in config_cam_baseline_example.yaml, commented out, with its COSP prerequisites and kwargs syntax. - Replace warnings.warn with print for user-facing messages, matching the rest of the ADF, and normalize the ERROR:/WARNING: prefixes. The numba shim keeps warnings.warn, where it is the right mechanism. Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
- Route the load_reference_data trace through adf.debug_log instead of printing on every run. The scale-factor message stays a print, since it reports an actual transformation of the data rather than a trace. - Remove leftover commented-out code and the redundant float() around a value np.round already handles. - Cache the point-in-polygon land mask on the lat/lon grid. It depends only on the grid, but create_land_mask runs once per case per variable and each call costs O(n_points x n_polygons). Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Figure filenames must have the stem "{var}_{season}_{plot_type}_Mean":
template_mean_diag.html builds its links as
plot_page_{var}_{season}_{plot_type}_Mean.html rather than using the
filename registered through add_website_data. The old names left the
FISCCP1_COSP entry on the index page pointing at a page that was never
generated, so the figures were unreachable from the website even though
both the plot pages and the image assets existed.
The regime number now occupies the "season" slot (CR1..CRk, and Centers
for the cluster-center figure) with non_season=True, the same way QBO
uses QBOts/QBOamp. The cluster-center figure is also named for the
variable rather than the data product so it groups with that variable's
RFO maps.
Verified with create_html: true -- 8 plot pages, 8 assets, and no broken
internal links anywhere in the generated site.
Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
This PR is supposed to replace #251.
It is a substantial refactoring of Isaac's original script, but accomplishes the same thing: plots of the cloud regimes and maps of frequency of occurrence of cloud regimes. This works with ISCCP, MODIS, and MISR cloud regimes. There are a number of options available, and they should be set using the argument dictionary approach in the configuration YAML file.
I've done testing for ISCCP histograms, but haven't repeated for MODIS and MISR so far.
There are quite a few helper functions included. I have tried to organize them into the categories:
_validate_user_inputs_get_ref_cluster_labels_preprocess_datacompute_cluster_labelsprecomputed_clusters_compute_distances_pot_compute_distances_wasserstein_calculate_rfoload_reference_dataload_cluster_centers_plot_map_configure_map_axes_add_colorbar_add_colorbar2plot_rfo_mapsplot_hists_baselineplot_hists_obs_prepare_plot_data_plot_cloud_regimes_plot_observation_column_plot_baseline_column_plot_test_case_column_create_colormap_configure_axes_configure_modis_axes_configure_misr_axes_configure_isccp_axes_add_figure_labels_save_figurespatial_subsettemporal_subsetselect_valid_tau_height_calculate_weighted_mean_xrapply_land_ocean_mask_make_mask_broadcastablecreate_land_mask_find_land_pointsmake_se_regridderregrid_se_data_bilinearThis can probably be simplified more, but I thought it was worth getting it into ADF now, and modifying it incrementally as possible. In particular, the land/ocean masking and the regridding should probably be removed for more generalized versions of those functions that can be used across ADF scripts.