diff --git a/configs/model/multimodal.yaml b/configs/model/multimodal.yaml index 41af91e2..8a90fdfa 100644 --- a/configs/model/multimodal.yaml +++ b/configs/model/multimodal.yaml @@ -43,6 +43,19 @@ pv_encoder: kdim: 40 pv_id_embed_dim: 20 +#-------------------------------------------- +# Sensor encoder settings +#-------------------------------------------- + +sensor_encoder: + _target_: pvnet.models.multimodal.site_encoders.encoders.SingleAttentionNetwork + _partial_: True + num_sites: 123 + out_features: 40 + num_heads: 4 + kdim: 40 + pv_id_embed_dim: 20 + #-------------------------------------------- # Tabular network settings #-------------------------------------------- diff --git a/pvnet/models/base_model.py b/pvnet/models/base_model.py index d1d667a2..5b85e205 100644 --- a/pvnet/models/base_model.py +++ b/pvnet/models/base_model.py @@ -68,6 +68,11 @@ def make_clean_data_config(input_path, output_path, placeholder="PLACEHOLDER"): d["pv_filename"] = f"{placeholder}.netcdf" d["pv_metadata_filename"] = f"{placeholder}.csv" + if "sensor" in config["input_data"]: + # If not empty - i.e. if used + if config["input_data"][source][f"{source}_filename"] != "": + config["input_data"][source][f"{source}_filename"] = f"{placeholder}.nc" + with open(output_path, "w") as outfile: yaml.dump(config, outfile, default_flow_style=False) @@ -237,6 +242,7 @@ def __init__( forecast_minutes: int, optimizer: AbstractOptimizer, output_quantiles: Optional[list[float]] = None, + target_key: BatchKey = BatchKey.gsp, ): """Abtstract base class for PVNet submodels. @@ -246,10 +252,12 @@ def __init__( optimizer (AbstractOptimizer): Optimizer output_quantiles: A list of float (0.0, 1.0) quantiles to predict values for. If set to None the output is a single value. + target_key: BatchKey of the target variable """ super().__init__() self._optimizer = optimizer + self._target_key = target_key # Model must have lr to allow tuning # This setting is only used when lr is tuned with callback @@ -424,7 +432,7 @@ def _training_accumulate_log(self, batch, batch_idx, losses, y_hat): def training_step(self, batch, batch_idx): """Run training step""" y_hat = self(batch) - y = batch[BatchKey.gsp][:, -self.forecast_len_30 :, 0] + y = batch[self._target_key][:, -self.forecast_len_30 :, 0] losses = self._calculate_common_losses(y, y_hat) losses = {f"{k}/train": v for k, v in losses.items()} @@ -440,7 +448,7 @@ def training_step(self, batch, batch_idx): def validation_step(self, batch: dict, batch_idx): """Run validation step""" y_hat = self(batch) - y = batch[BatchKey.gsp][:, -self.forecast_len_30 :, 0] + y = batch[self._target_key][:, -self.forecast_len_30 :, 0] losses = self._calculate_common_losses(y, y_hat) losses.update(self._calculate_val_losses(y, y_hat)) @@ -484,7 +492,7 @@ def validation_step(self, batch: dict, batch_idx): def test_step(self, batch, batch_idx): """Run test step""" y_hat = self(batch) - y = batch[BatchKey.gsp][:, -self.forecast_len_30 :, 0] + y = batch[self._target_key][:, -self.forecast_len_30 :, 0] losses = self._calculate_common_losses(y, y_hat) losses.update(self._calculate_val_losses(y, y_hat)) diff --git a/pvnet/models/multimodal/multimodal.py b/pvnet/models/multimodal/multimodal.py index 562f9bdc..d57676a4 100644 --- a/pvnet/models/multimodal/multimodal.py +++ b/pvnet/models/multimodal/multimodal.py @@ -42,9 +42,11 @@ def __init__( nwp_encoder: Optional[AbstractNWPSatelliteEncoder] = None, sat_encoder: Optional[AbstractNWPSatelliteEncoder] = None, pv_encoder: Optional[AbstractPVSitesEncoder] = None, + sensor_encoder: Optional[AbstractPVSitesEncoder] = None, # TODO Change to SensorEncoder add_image_embedding_channel: bool = False, include_gsp_yield_history: bool = True, include_sun: bool = True, + include_gsp: bool = True, embedding_dim: Optional[int] = 16, forecast_minutes: int = 30, history_minutes: int = 60, @@ -53,6 +55,7 @@ def __init__( nwp_forecast_minutes: Optional[int] = None, nwp_history_minutes: Optional[int] = None, pv_history_minutes: Optional[int] = None, + sensor_history_minutes: Optional[int] = None, optimizer: AbstractOptimizer = pvnet.optimizers.Adam(), ): """Neural network which combines information from different sources. @@ -100,6 +103,8 @@ def __init__( self.include_nwp = nwp_encoder is not None self.include_pv = pv_encoder is not None self.include_sun = include_sun + self.include_gsp = include_gsp + self.include_sensor = sensor_encoder is not None self.embedding_dim = embedding_dim self.add_image_embedding_channel = add_image_embedding_channel @@ -159,6 +164,18 @@ def __init__( # Update num features fusion_input_features += self.pv_encoder.out_features + if self.include_sensor: + if sensor_history_minutes is None: + sensor_history_minutes = history_minutes + + self.sensor_encoder = sensor_encoder( + sequence_length=sensor_history_minutes // 30 + 1, + # Sensors are currently resampled to 30min + ) + + # Update num features + fusion_input_features += self.sensor_encoder.out_features + if self.embedding_dim: self.embed = nn.Embedding(num_embeddings=318, embedding_dim=embedding_dim) @@ -227,6 +244,11 @@ def forward(self, x): id_embedding = self.embed(id) modes["id"] = id_embedding + # *********************** Sensor Data ************************************ + # add sensor yield history + if self.include_sensor: + modes["sensor"] = self.sensor_encoder(x) + if self.include_sun: sun = torch.cat( (x[BatchKey.gsp_solar_azimuth], x[BatchKey.gsp_solar_elevation]), dim=1 diff --git a/pvnet/models/multimodal/site_encoders/encoders.py b/pvnet/models/multimodal/site_encoders/encoders.py index c7951b86..2999fbd4 100644 --- a/pvnet/models/multimodal/site_encoders/encoders.py +++ b/pvnet/models/multimodal/site_encoders/encoders.py @@ -125,6 +125,8 @@ def __init__( n_kv_res_blocks: int = 2, kv_res_block_layers: int = 2, use_pv_id_in_value: bool = False, + use_gsp_embedding: bool = True, + sensor_embed_dim: int = 123, ): """A simple attention-based model with a single multihead attention layer @@ -142,6 +144,7 @@ def __init__( the key and value encoders. use_pv_id_in_value: Whether to use a PV ID embedding in network used to produce the value for the attention layer. + use_gsp_embedding: Whether to use embedding of the GSP ID """ super().__init__(sequence_length, num_sites, out_features)