feat: add support for output-only diagnostic variables - #698
Conversation
Sir-Sloth-The-Lazy
left a comment
There was a problem hiding this comment.
Thanks for taking this on! The formatting/mypy points are quick fixes, but I think the core mechanism needs a rethink before we go further. Because boundary_states is the ground truth in every phase, overwriting the predicted diagnostic channels with it means the model never learns those variables (zero loss) and eval metrics leak the truth. Issue #684's intent is output-only variables, predicted and trained on, but not fed back as input which usually means the predictor's output dimension exceeds its input state dimension and the AR feedback slices the diagnostics off. Could you align on that design (probably with @joeloskarsson or @sadamov ) before iterating?
| output_clamping: OutputClamping = dataclasses.field( | ||
| default_factory=OutputClamping | ||
| ) | ||
| # Adding the new Field: |
There was a problem hiding this comment.
the comment does not describe the code, could you please write a comment which explain a future reader, what this line means ?
| default_factory=OutputClamping | ||
| ) | ||
| # Adding the new Field: | ||
| diagnostic_vars: list[str] = dataclasses.field(default_factory=list) |
| def __init__( | ||
| self, predictor: StepPredictor, datastore: BaseDatastore | ||
| self, predictor: StepPredictor, datastore: BaseDatastore, | ||
| diagnostic_indices: list[int] = None |
There was a problem hiding this comment.
(1)This will fail the mypy pre-commit hook,the annotation needs to be list[int] | None = None. (2) This new parameter isn't added to the init numpydoc Parameters section; It would be appreciated, if you do that.
| self, predictor: StepPredictor, datastore: BaseDatastore, | ||
| diagnostic_indices: list[int] = None | ||
| ) -> None: | ||
|
|
There was a problem hiding this comment.
This is a trailing whitespace, please remove it
| super().__init__() | ||
| self.predictor = predictor | ||
|
|
||
| self.diagnostic_indices = diagnostic_indices |
There was a problem hiding this comment.
Also, the indices are stored without validation, no bounds check against num_state_vars. This can cause a problem
| if self.diagnostic_indices is not None: | ||
| new_state[:, :, self.diagnostic_indices] = boundary_state[:, :, self.diagnostic_indices] |
There was a problem hiding this comment.
This is the core concern. boundary_state is boundary_states[:, i], and ForecasterModule.common_step passes target_states as boundary_states in all phases (train/val/test). So this line sets the returned prediction's diagnostic channels equal to the ground truth. Consequences:
- Training: the loss on those channels is identically zero --> the model gets no gradient and never learns to predict diagnostics.
- Val/test: the "prediction" for diagnostics is the target --> data leakage / artificially perfect scores.
new_statealso becomesprev_state, so ground-truth diagnostics are fed back into the predictor input each step, the opposite of "output-only," and it leaks truth into the ordinary state predictions too.
I think this needs a different design: diagnostics should be predicted (and included in the loss) but excluded from the AR feedback, which generally means the predictor outputs more channels than it takes as input, rather than overwriting predictions with truth.
| else: | ||
| print(f"Warning: Diagnostic variable {var} not found in state features! Available: {feature_names}") |
There was a problem hiding this comment.
The PR description says this validates that requested variables exist, but a missing variable just prints and continues, producing a silently-incomplete diagnostic_indices. I'd raise a config error here instead so misconfiguration fails loudly. Also, the codebase uses warnings.warn, not print.
| else: | ||
| print(f"Warning: Diagnostic variable {var} not found in state features! Available: {feature_names}") | ||
|
|
||
| forecaster = ARForecaster(predictor, datastore,diagnostic_indices=diagnostic_indices if diagnostic_indices else None) |
There was a problem hiding this comment.
Note this mapping logic only runs here in main(),load_forecaster_module_from_checkpoint constructs ARForecaster too, so a reloaded checkpoint silently loses its diagnostic config (gets None). Might be cleaner to resolve names --> indices inside the forecaster/datastore so both construction sites share it.
| # Adding the line: | ||
| diagnostic_vars: | ||
| - precipitation_rate |
There was a problem hiding this comment.
This datastore's state features are u100m, v100m, r2m, t2m there's no precipitation_rate in it. So with this config the lookup misses, prints the warning, and diagnostic_indices ends up empty --> the feature is silently disabled. The example meant to demonstrate the feature actually exercises the not-found path. It also highlights the underlying gap: the datastore has no diagnostic-category data, which is what #684 says needs to exist first.
|
@Sir-Sloth-The-Lazy Thanks for the thorough review agreed on all points, especially the core one. I implemented diagnostics as a subset of existing state indices, masked with boundary_state (= target_states) at each AR step the same mechanism used for spatial boundary conditions, misapplied to feature indices. Since boundary_state is ground truth in every phase, this zeroes the training loss on those channels and leaks truth into eval metrics, exactly as you described. It also feeds true diagnostic values back into prev_state, which is the opposite of output-only. Fix, not patch: rather than fix this mechanism in place, I'll rework it so the predictor's output width exceeds its input width: grid_output_dim = num_state_vars + num_diagnostic_vars. The AR loop will only feed the first num_state_vars channels back as prev_state/prev_prev_state; the remaining diagnostic channels are real predictions, included in the loss, never overwritten with truth. This also means diagnostic needs to be a datastore-level category (parallel to state/forcing/static, per #684), not a TrainingConfig field agreed that's the wrong home for it. And you're right that today there's no diagnostic data in the datastore layer at all, which is why the example config's precipitation_rate just hits the not-found path. @sadamov @joeloskarsson before I rebuild this, could you confirm the design above (extra output channels beyond state width, sliced off before AR feedback) matches what you had in mind for #684? Want to align once rather than iterate blind again. If that's confirmed, I'll close this PR and split the rework into smaller, reviewable pieces: Datastore support for a real diagnostic category (+ a diagnostic variable in tests/dummy_datastore.py so it's actually testable) |
|
Thanks @palakbhati, and thanks @Sir-Sloth-The-Lazy for the review. Agreed the current mechanism can't stand: overwriting the predicted channels with Design choices:
I'd close this PR and split into: (1) datastore category + a diagnostic var in @joeloskarsson @leifdenby What do you think? Let's wait for their input as well. |
I have implemented support for the diagnostic data category as discussed. This allows users to designate certain variables as "output-only," preventing them from being fed back into the autoregressive loop as input for subsequent steps.
Summary of changes:
Configuration Schema: Updated NeuralLAMConfig in config.py to include a diagnostic_vars field, allowing users to specify output-only variables directly in the model-side config.yaml.
Autoregressive Logic: Modified ARForecaster.forward in autoregressive.py to filter these variables during the unrolling process. By setting the diagnostic indices to the ground truth (boundary_state) at each step, we ensure the model treats them as output-only and does not propagate them autoregressively.
Dynamic Mapping: Updated train_model.py to dynamically map variable names provided in the config to their corresponding indices in the datastore, with validation to ensure the requested variables exist.
I have verified the implementation using the current training pipeline, and it successfully handles the configuration loading and variable filtering.
Issue Link
Closes #684
Type of change
Checklist before requesting a review
Checklist for reviewers
Each PR comes with its own improvements and flaws. The reviewer should check the following:
Author checklist after completed review
reflecting type of change (add section where missing):
Checklist for assignee
Once the PR is ready to be merged, squash commits and merge the PR.