docstr: interpret output of model
  
  Args:
      out: output as produced by ``forward()``
      reduction: "none" for no averaging over batches, "sum" for summing attentions, "mean" for
          normalizing by encode lengths
      attention_prediction_horizon: which prediction horizon to use for attention
  
  Returns:
      interpretations that can be plotted with ``plot_interpretation()``
docstr_tok: ["interpret","output","of","model","Args",":","out",":","output","as","produced","by","``","forward","(",")","``","reduction",":","``","none","''","for","no","averaging","over","batches",",","``","sum","''","for","summing","attentions",",","``","mean","''","for","normalizing","by","encode","lengths","attention_prediction_horizon",":","which","prediction","horizon","to","use","for","attention","Returns",":","interpretations","that","can","be","plotted","with","``","plot_interpretation","(",")","``"]
code: def interpret_output(
      self,
      out: Dict[str, torch.Tensor],
      reduction: str = "none",
      attention_prediction_horizon: int = 0,
  ) -> Dict[str, torch.Tensor]:
      """
      interpret output of model
  
      Args:
          out: output as produced by ``forward()``
          reduction: "none" for no averaging over batches, "sum" for summing attentions, "mean" for
              normalizing by encode lengths
          attention_prediction_horizon: which prediction horizon to use for attention
  
      Returns:
          interpretations that can be plotted with ``plot_interpretation()``
      """
      # take attention and concatenate if a list to proper attention object
      batch_size = len(out["decoder_attention"])
      if isinstance(out["decoder_attention"], (list, tuple)):
          # start with decoder attention
          # assume issue is in last dimension, we need to find max
          max_last_dimension = max(
              x.size(-1) for x in out["decoder_attention"]
          )
          first_elm = out["decoder_attention"][0]
          # create new attention tensor into which we will scatter
          decoder_attention = torch.full(
              (batch_size, *first_elm.shape[:-1], max_last_dimension),
              float("nan"),
              dtype=first_elm.dtype,
              device=first_elm.device,
          )
          # scatter into tensor
          for idx, x in enumerate(out["decoder_attention"]):
              decoder_length = out["decoder_lengths"][idx]
              decoder_attention[idx, :, :, :decoder_length] = x[
                  ..., :decoder_length
              ]
      else:
          decoder_attention = out["decoder_attention"].clone()
          decoder_mask = create_mask(
              out["decoder_attention"].size(1), out["decoder_lengths"]
          )
          decoder_attention[
              decoder_mask[..., None, None].expand_as(decoder_attention)
          ] = float("nan")
  
      if isinstance(out["encoder_attention"], (list, tuple)):
          # same game for encoder attention
          # create new attention tensor into which we will scatter
          first_elm = out["encoder_attention"][0]
          encoder_attention = torch.full(
              (
                  batch_size,
                  *first_elm.shape[:-1],
                  self.hparams.max_encoder_length,
              ),
              float("nan"),
              dtype=first_elm.dtype,
              device=first_elm.device,
          )
          # scatter into tensor
          for idx, x in enumerate(out["encoder_attention"]):
              encoder_length = out["encoder_lengths"][idx]
              encoder_attention[
                  idx,
                  :,
                  :,
                  self.hparams.max_encoder_length - encoder_length :,
              ] = x[..., :encoder_length]
      else:
          # roll encoder attention (so start last encoder value is on the right)
          encoder_attention = out["encoder_attention"].clone()
          shifts = encoder_attention.size(3) - out["encoder_lengths"]
          new_index = (
              torch.arange(
                  encoder_attention.size(3),
                  device=encoder_attention.device,
              )[None, None, None].expand_as(encoder_attention)
              - shifts[:, None, None, None]
          ) % encoder_attention.size(3)
          encoder_attention = torch.gather(
              encoder_attention, dim=3, index=new_index
          )
          # expand encoder_attentiont to full size
          if (
              encoder_attention.size(-1)
              < self.hparams.max_encoder_length
          ):
              encoder_attention = torch.concat(
                  [
                      torch.full(
                          (
                              *encoder_attention.shape[:-1],
                              self.hparams.max_encoder_length
                              - out["encoder_lengths"].max(),
                          ),
                          float("nan"),
                          dtype=encoder_attention.dtype,
                          device=encoder_attention.device,
                      ),
                      encoder_attention,
                  ],
                  dim=-1,
              )
  
      # combine attention vector
      attention = torch.concat(
          [encoder_attention, decoder_attention], dim=-1
      )
      attention[attention < 1e-5] = float("nan")
  
      # histogram of decode and encode lengths
      encoder_length_histogram = integer_histogram(
          out["encoder_lengths"],
          min=0,
          max=self.hparams.max_encoder_length,
      )
      decoder_length_histogram = integer_histogram(
          out["decoder_lengths"],
          min=1,
          max=out["decoder_variables"].size(1),
      )
  
      # mask where decoder and encoder where not applied when averaging variable selection weights
      encoder_variables = out["encoder_variables"].squeeze(-2).clone()
      encode_mask = create_mask(
          encoder_variables.size(1), out["encoder_lengths"]
      )
      encoder_variables = encoder_variables.masked_fill(
          encode_mask.unsqueeze(-1), 0.0
      ).sum(dim=1)
      encoder_variables /= (
          out["encoder_lengths"]
          .where(
              out["encoder_lengths"] > 0,
              torch.ones_like(out["encoder_lengths"]),
          )
          .unsqueeze(-1)
      )
  
      decoder_variables = out["decoder_variables"].squeeze(-2).clone()
      decode_mask = create_mask(
          decoder_variables.size(1), out["decoder_lengths"]
      )
      decoder_variables = decoder_variables.masked_fill(
          decode_mask.unsqueeze(-1), 0.0
      ).sum(dim=1)
      decoder_variables /= out["decoder_lengths"].unsqueeze(-1)
  
      # static variables need no masking
      static_variables = out["static_variables"].squeeze(1)
      # attention is batch x time x heads x time_to_attend
      # average over heads + only keep prediction attention and attention on observed timesteps
      attention = masked_op(
          attention[
              :,
              attention_prediction_horizon,
              :,
              : self.hparams.max_encoder_length
              + attention_prediction_horizon,
          ],
          op="mean",
          dim=1,
      )
  
      if reduction != "none":  # if to average over batches
          static_variables = static_variables.sum(dim=0)
          encoder_variables = encoder_variables.sum(dim=0)
          decoder_variables = decoder_variables.sum(dim=0)
  
          attention = masked_op(attention, dim=0, op=reduction)
      else:
          attention = attention / masked_op(
              attention, dim=1, op="sum"
          ).unsqueeze(
              -1
          )  # renormalize
  
      interpretation = dict(
          attention=attention.masked_fill(torch.isnan(attention), 0.0),
          static_variables=static_variables,
          encoder_variables=encoder_variables,
          decoder_variables=decoder_variables,
          encoder_length_histogram=encoder_length_histogram,
          decoder_length_histogram=decoder_length_histogram,
      )
      return interpretation
  
imports: from copy import copy
  from typing import Dict, List, Tuple, Union
  from matplotlib import pyplot
  import numpy
  import torch
  from torch import nn
  from torchmetrics import Metric
  from pytorch_forecasting.data import TimeSeriesDataSet
  from pytorch_forecasting.data.encoders import NaNLabelEncoder
  from pytorch_forecasting.metrics import MAE, MAPE, MASE, RMSE, SMAPE, MultiHorizonMetric, MultiLoss, QuantileLoss
  from pytorch_forecasting.models.base_model import BaseModelWithCovariates
  from pytorch_forecasting.models.nn import LSTM, MultiEmbedding
  from pytorch_forecasting.models.temporal_fusion_transformer.sub_modules import AddNorm, GateAddNorm, GatedLinearUnit, GatedResidualNetwork, InterpretableMultiHeadAttention, VariableSelectionNetwork
  from pytorch_forecasting.utils import create_mask, detach, integer_histogram, masked_op, padded_stack, to_list
label_desc: Use image node_id 10 for calling the TemporalFusionTransformer obj's underlying member method code with example usage: obj.interpret_output(out, reduction, attention_prediction_horizon) and returns: interpretation