Skip to content

Add Cross-Unet model for photovoltaic power forecasting#1833

Open
ipe-zhangyz wants to merge 8 commits into
NVIDIA:mainfrom
ipe-zhangyz:feature/cross-unet
Open

Add Cross-Unet model for photovoltaic power forecasting#1833
ipe-zhangyz wants to merge 8 commits into
NVIDIA:mainfrom
ipe-zhangyz:feature/cross-unet

Conversation

@ipe-zhangyz

Copy link
Copy Markdown

PhysicsNeMo Pull Request

Description

Summary

This PR adds Cross-Unet, a Transformer-based architecture for photovoltaic (PV) power forecasting, featuring multi-scale temporal encoding, correlation-aware channel attention, and hierarchical cross-attention decoding to fuse historical generation data with weather forecasts, as an experimental model in PhysicsNeMo.
The model is adapted from the paper "Rethinking the use of deep learning methods for photovoltaic power forecasting" (Nat Commun (2026), Zhang Y. et al.), ported to PhysicsNeMo with explicit typed kwargs and integration with PhysicsNeMo training primitives.

What is added

  • Model: physicsnemo/experimental/models/pv_power/ — Cross-Unet implementation including cross-attention, embedding, encoder/decoder components
  • Training example: examples/weather/pv_power_cross_unet/ — synthetic-data training script (train_cross_unet.py), real-data training script (train_real_cross_unet.py), Hydra configs, and bilingual usage documentation
  • Tests: test/experimental/models/pv_power/cross_unet/test_cross_unet.py — unit tests covering forward pass, state channels, and edge cases; test/examples/weather/test_pv_power_cross_unet_real_data.py — real-data workflow tests
  • CHANGELOG: updated per contribution guidelines

Key design

  • Cross-Unet constructs channel-correlation representations from historical weather channels and historical targets, then applies cross-attention in a U-Net encoder-decoder to produce multi-step power forecasts. It supports configurable sequence length, prediction horizon, and attention variants (two_stage, etc.)

Checklist

Dependencies

Review Process

All PRs are reviewed by the PhysicsNeMo team before merging.

Depending on which files are changed, GitHub may automatically assign a maintainer for review.

We are also testing AI-based code review tools (e.g., Greptile), which may add automated comments with a confidence score.
This score reflects the AI’s assessment of merge readiness and is not a qualitative judgment of your work, nor is
it an indication that the PR will be accepted / rejected.

AI-generated feedback should be reviewed critically for usefulness.
You are not required to respond to every AI comment, but they are intended to help both authors and reviewers.
Please react to Greptile comments with 👍 or 👎 to provide feedback on their accuracy.

@copy-pr-bot

copy-pr-bot Bot commented Jul 13, 2026

Copy link
Copy Markdown

This pull request requires additional validation before any workflows can run on NVIDIA's runners.

Pull request vetters can view their responsibilities here.

Contributors can view more details about this message here.

@greptile-apps

greptile-apps Bot commented Jul 13, 2026

Copy link
Copy Markdown
Contributor

Greptile Summary

This PR adds an experimental CrossUnet workflow for PV power forecasting. The main changes are:

  • New physicsnemo.experimental.models.pv_power model package.
  • CrossUnet attention, embedding, encoder, and decoder components.
  • Synthetic and real-data training scripts with Hydra configs.
  • English and Chinese usage documentation for the example.
  • Unit tests and real-data workflow tests.

Important Files Changed

Filename Overview
physicsnemo/experimental/models/pv_power/cross_unet.py Adds the CrossUnet model, metadata, shape validation, correlation construction, and forecast path.
physicsnemo/experimental/models/pv_power/encoder_decoder.py Adds the encoder and decoder stack used by CrossUnet, including segment merging and bottleneck fusion.
examples/weather/pv_power_cross_unet/train_real_cross_unet.py Adds CSV loading, split/window construction, training, prediction, evaluation, checkpointing, and metrics output.
examples/weather/pv_power_cross_unet/train_cross_unet.py Adds a synthetic-data training loop using PhysicsNeMo logging and checkpoint helpers.
test/experimental/models/pv_power/cross_unet/test_cross_unet.py Adds construction, forward, validation, and regression tests for CrossUnet.
test/examples/weather/test_pv_power_cross_unet_real_data.py Adds tests for real CSV loading, split alignment, metrics, scalers, and horizon presets.

Reviews (1): Last reviewed commit: "refined cross_unet scripts and docs" | Re-trigger Greptile

Comment on lines +263 to +269
if use_bottleneck_in_decoder and dec_seg_num < out_seg_num:
raise ValueError(
f"pred_len={pred_len} (decoder segments={dec_seg_num}) is shorter "
f"than the encoder bottleneck (segments={out_seg_num}) implied by "
f"seq_len={seq_len}, seg_len={seg_len}, e_layers={e_layers}. "
f"Either increase pred_len, decrease e_layers, or decrease seg_len."
)

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.

P1 Decoder Grid Gets Truncated

When use_bottleneck_in_decoder=True and the decoder has more segments than the encoder bottleneck, the first decoder layer truncates the decoder input to the bottleneck length. The documented 1d and 7d horizons reach this state, so the model can return fewer timesteps than pred_len, and training or evaluation then fails or compares mismatched forecast lengths.

Suggested change
if use_bottleneck_in_decoder and dec_seg_num < out_seg_num:
raise ValueError(
f"pred_len={pred_len} (decoder segments={dec_seg_num}) is shorter "
f"than the encoder bottleneck (segments={out_seg_num}) implied by "
f"seq_len={seq_len}, seg_len={seg_len}, e_layers={e_layers}. "
f"Either increase pred_len, decrease e_layers, or decrease seg_len."
)
if use_bottleneck_in_decoder and dec_seg_num != out_seg_num:
raise ValueError(
f"pred_len={pred_len} (decoder segments={dec_seg_num}) must match "
f"the encoder bottleneck (segments={out_seg_num}) implied by "
f"seq_len={seq_len}, seg_len={seg_len}, e_layers={e_layers} when "
f"use_bottleneck_in_decoder=True. Adjust pred_len/seg_len/e_layers "
f"or disable the bottleneck decoder path."
)

frame = _load_generic_csv(cfg)
if len(frame) < 2 * cfg.seq_len + max(cfg.seq_len, cfg.pred_len):
raise ValueError(
f"{cfg.data_file} has {len(frame)} usable rows; at least "

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.

P1 Split Windows Can Be Empty

This check only verifies that the full CSV can hold one 2 * seq_len + max(seq_len, pred_len) window, but validation and test use 10% splits. For the documented 7d horizon, a file with 2,017 rows passes here while the valid/test splits are only about 200 rows, so RealPVDataset later raises Split ... has no usable windows instead of running the documented workflow.

Comment on lines +551 to +553
saved_data_cfg = checkpoint["data_config"]
data_cfg = RealPVDataConfig(
data_file=Path(to_absolute_path(str(saved_data_cfg["data_file"]))),

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.

P2 Checkpoint Pins Old CSV Path

Prediction rebuilds data_cfg from the absolute data_file saved during training, ignoring the data_file override used for the predict or evaluate run. If a user moves the CSV or passes a new copy with the same columns, the workflow still tries to read the original path and fails with FileNotFoundError, or evaluates the old file instead of the requested one.

- Add full Apache-2.0 license headers to real_data.yaml and test_pv_power_cross_unet_real_data.py
- Add docstrings to ArrayScaler methods and RealPVDataset properties
- Add docstring to main() in train_cross_unet.py
- Fix markdown line-length and multiple-blank-lines in CROSS_UNET_USAGE.md and CROSS_UNET_USAGE.zh.md
- Bug 2: After splitting, verify each val/test partition has at least
  pred_len rows so RealPVDataset raises a clear error early rather than
  failing later with a cryptic 'no usable windows' message.
- Bug 3: In _run_prediction, prefer cfg.data_file when it resolves to an
  existing file, falling back to the checkpoint-saved path only when the
  config still holds the placeholder default. This lets users redirect
  predict/evaluate to a new CSV without editing the checkpoint.
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