Skip to content

Fix cluster matching for "partial" secondary inputs - #2417

Open
gavinevans wants to merge 4 commits into
metoppv:masterfrom
gavinevans:mobt_783_partial_realization_clustering
Open

Fix cluster matching for "partial" secondary inputs#2417
gavinevans wants to merge 4 commits into
metoppv:masterfrom
gavinevans:mobt_783_partial_realization_clustering

Conversation

@gavinevans

@gavinevans gavinevans commented Jul 24, 2026

Copy link
Copy Markdown
Contributor

Related issues: #2259, #2305, #2332, #2340, #2359, #2361

Description
This PR consists of 3 commits to address issues with the current clustering logic.

Commit 1: This addresses the primary goal because I noticed that it was possible for secondary inputs (e.g. UKV), that only have enough realizations to cover a subset of the total number of clusters, to match to different clusters at different forecast periods e.g. see the output below. This wasn't the intention but was arising from the fact that the call to RealizationToClusterMatcher was inside the loop over forecast period in _process_partial_realization_inputs. This was unlike the methodology in _process_full_realization_inputs where RealizationToClusterMatcher was called prior to the for loop over forecast period, so that the realization to cluster matching used all forecast periods. This commit therefore makes _process_partial_realization_inputs more like _process_full_realization_inputs, so that secondary inputs with a "partial" set of realizations are matched to the clusters using all the forecast periods.

json.loads(ccube.attributes["secondary_input_realizations_to_clusters"])["uk_det"]
{'10': [{'realization': 3, 'forecast_periods': [3600]}, {'realization': 1, 'forecast_periods': [21600]}], '11': [{'realization': 2, 'forecast_periods': [3600]}], '13': [{'realization': 0, 'forecast_periods': [3600]}], '15': [{'realization': 1, 'forecast_periods': [3600]}], '0': [{'realization': 2, 'forecast_periods': [21600]}], '5': [{'realization': 3, 'forecast_periods': [21600]}], '17': [{'realization': 0, 'forecast_periods': [21600]}]}

Commit 2: This addresses an issue that was found in testing commit 1, where it was possible for a secondary input to have an inconsistent number of realizations at different forecast periods. This led to an inability to merge the secondary inputs across forecast periods in _process_partial_realization_inputs. This hadn't been an issue previously because different "partial" forecast sources were being matched independently at each forecast period.

iris.exceptions.MergeError: failed to merge into a single cube.
  cube.shape differs: (4, 970, 1042) != (3, 970, 1042)

To fix this issue, I've modified the _categorise_secondary_inputs method, which was previously only checking the number of realizations present on the first cube. This has been extended, so that if the number of realizations changes across forecast periods within the secondary input then the secondary input will be truncated, so as to avoid a possible pulsing if a forecast source is missing particular forecast periods.

Commit 3: This commit does some refactoring to centralise some of the code present in _process_partial_realization_inputs and _process_full_realization_inputs and avoid duplication where possible.

Testing:

  • Ran tests and they passed OK
  • Added new tests for the new feature(s)

… the secondary forecast source are fewer than the number of clusters.
…izations in the secondary inputs changed over time, so a merge would fail. This has now been handled by curtailing any secondary input if there is a change in the number of realizations.

@mo-jbeaver mo-jbeaver left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added some comments, mostly for areas that might need more explanations. Tests all ran successfully.

Cross-fp avg MSE (new code):
- vs cluster A: (4 + 7225)/2 = 3614.5
- vs cluster B: (7744 + 25)/2 = 3884.5
=> new code assigns to cluster A at both fps

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should a small note be added that for MSE, lower is better?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added.

"""Partial inputs must assign the same cluster at every lead time.

A partial secondary input (fewer realizations than n_clusters) was previously
matched independently per forecast period, allowing the same realization to be

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

forecast period (fp) might be a helpful addition

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added (fp) given that that's used in lots of variable names later on.

Comment on lines 711 to 718
@@ -705,8 +717,31 @@ def _categorise_secondary_inputs(
)
continue # No valid forecast periods after filtering

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to move this section to below the lines:
valid_fp_cube_pairs = [ (fp, cube) for fp, cube in fp_cube_pairs if fp in primary_fps ] valid_fp_cube_pairs = sorted(valid_fp_cube_pairs, key=lambda x: x[0])

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes, I've moved the if not valid_fp_cube_pairs block up a few lines to be next to where the valid_fp_cube_pairs variable is defined.

Comment on lines +2445 to +2448
fp_6_data = result.extract(iris.Constraint(forecast_period=6 * 3600)).data
assert np.allclose(fp_6_data, 106.0, atol=5.0), (
"fp=6 should fall back to clustered primary when secondary is truncated"
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Could an additional comment be added here to explain this section.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added an additional comment to explain the meaning of the result.

)


def test_clusterandmatch_full_input_inconsistent_realization_counts_truncates():

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test seems very similar to the previous one. Could more descriptions be added to both to explain the differences between them.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've parameterised these tests, as you're right that they were very similar. The unit test description has been extended.

Comment on lines +1012 to +1013
Updates replaced_realizations, cluster_sources, and
secondary_input_realizations_to_clusters in-place. Called from the

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe mention what these variables are, attributes, dimensions ect.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've added a bit more detail to the docstring.

candidate_name: Name of the secondary input.
fps: Forecast period values in seconds to extract.
cubes: CubeList containing all input cubes.
target_grid_cube: Target grid cube for optional regridding.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Is the regridding optional?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've tried to clarify that the target_grid_cube variable can be optional and updated the type hints and docstrings to reflect this.

@mo-jbeaver mo-jbeaver assigned gavinevans and unassigned mo-jbeaver Jul 27, 2026
@gavinevans gavinevans assigned mo-jbeaver and unassigned gavinevans Jul 27, 2026
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.

2 participants