class_docstr: Temporal Markov Random Field with Gaussian factors for initial, transition,
  and observation distributions. This adapts [1] to parallelize over time to
  achieve O(log(time)) parallel complexity, however it differs in that it
  tracks the log normalizer to ensure :meth:`log_prob` is differentiable.
  
  The event_shape of this distribution includes time on the left::
  
      event_shape = (num_steps,) + observation_dist.event_shape
  
  This distribution supports any combination of homogeneous/heterogeneous
  time dependency of ``transition_dist`` and ``observation_dist``. However,
  because time is included in this distribution's event_shape, the
  homogeneous+homogeneous case will have a broadcastable event_shape with
  ``num_steps = 1``, allowing :meth:`log_prob` to work with arbitrary length
  data::
  
      event_shape = (1, obs_dim)  # homogeneous + homogeneous case
  
  This class should be compatible with
  :class:`pyro.distributions.hmm.GaussianMRF` , but additionally supports
  funsor :mod:`~funsor.adjoint` algorithms.
  
  **References:**
  
  [1] Simo Sarkka, Angel F. Garcia-Fernandez (2019)
      "Temporal Parallelization of Bayesian Filters and Smoothers"
      https://arxiv.org/pdf/1905.13002.pdf
  
  :ivar int hidden_dim: The dimension of the hidden state.
  :ivar int obs_dim: The dimension of the observed state.
  :param ~torch.distributions.MultivariateNormal initial_dist: A distribution
      over initial states. This should have batch_shape broadcastable to
      ``self.batch_shape``.  This should have event_shape ``(hidden_dim,)``.
  :param ~torch.distributions.MultivariateNormal transition_dist: A joint
      distribution factor over a pair of successive time steps. This should
      have batch_shape broadcastable to ``self.batch_shape + (num_steps,)``.
      This should have event_shape ``(hidden_dim + hidden_dim,)`` (old+new).
  :param ~torch.distributions.MultivariateNormal observation_dist: A joint
      distribution factor over a hidden and an observed state. This should
      have batch_shape broadcastable to ``self.batch_shape + (num_steps,)``.
      This should have event_shape ``(hidden_dim + obs_dim,)``.
code: def __init__(
      self,
      initial_dist,
      transition_dist,
      observation_dist,
      validate_args=None,
  ):
      assert isinstance(
          initial_dist, torch.distributions.MultivariateNormal
      )
      assert isinstance(
          transition_dist, torch.distributions.MultivariateNormal
      )
      assert isinstance(
          observation_dist, torch.distributions.MultivariateNormal
      )
      hidden_dim = initial_dist.event_shape[0]
      assert transition_dist.event_shape[0] == hidden_dim + hidden_dim
      obs_dim = observation_dist.event_shape[0] - hidden_dim
      shape = broadcast_shape(
          initial_dist.batch_shape + (1,),
          transition_dist.batch_shape,
          observation_dist.batch_shape,
      )
      batch_shape, time_shape = shape[:-1], shape[-1:]
      event_shape = time_shape + (obs_dim,)
  
      # Convert distributions to funsors.
      init = dist_to_funsor(initial_dist)(value="state")
      trans = mvn_to_funsor(
          transition_dist,
          ("time",),
          OrderedDict(
              [
                  ("state", Reals[hidden_dim]),
                  ("state(time=1)", Reals[hidden_dim]),
              ]
          ),
      )
      obs = mvn_to_funsor(
          observation_dist,
          ("time",),
          OrderedDict(
              [
                  ("state(time=1)", Reals[hidden_dim]),
                  ("value", Reals[obs_dim]),
              ]
          ),
      )
  
      # Construct the joint funsor.
      # Compare with pyro.distributions.hmm.GaussianMRF.log_prob().
      with lazy:
          time = Variable("time", Bint[time_shape[0]])
          value = Variable("value", Reals[time_shape[0], obs_dim])
          logp_oh = trans + obs(value=value["time"])
          logp_oh = MarkovProduct(
              ops.logaddexp,
              ops.add,
              logp_oh,
              time,
              {"state": "state(time=1)"},
          )
          logp_oh += init
          logp_oh = logp_oh.reduce(
              ops.logaddexp, frozenset({"state", "state(time=1)"})
          )
          logp_h = trans + obs.reduce(ops.logaddexp, "value")
          logp_h = MarkovProduct(
              ops.logaddexp,
              ops.add,
              logp_h,
              time,
              {"state": "state(time=1)"},
          )
          logp_h += init
          logp_h = logp_h.reduce(
              ops.logaddexp, frozenset({"state", "state(time=1)"})
          )
          funsor_dist = logp_oh - logp_h
  
      dtype = "real"
      super(GaussianMRF, self).__init__(
          funsor_dist, batch_shape, event_shape, dtype, validate_args
      )
      self.hidden_dim = hidden_dim
      self.obs_dim = obs_dim
  
imports: from collections import OrderedDict
  import torch
  import funsor.ops
  from funsor.domains import Bint, Reals
  from funsor.interpretations import eager, lazy, moment_matching
  from funsor.pyro.convert import dist_to_funsor, funsor_to_cat_and_mvn, funsor_to_tensor, matrix_and_mvn_to_funsor, mvn_to_funsor, tensor_to_funsor
  from funsor.pyro.distribution import FunsorDistribution
  from funsor.sum_product import MarkovProduct, naive_sequential_sum_product, sequential_sum_product
  from funsor.terms import Variable
  from funsor.util import broadcast_shape
label_desc: Use image node_id 1 to create a new GaussianMRF object from inherited base classes: FunsorDistribution with example: obj = GaussianMRF(initial_dist, transition_dist, observation_dist, validate_args)