class_docstr: Compare various KF EnKF versions.
  
  If the model is linear and Gaussian the EnKF sample mean/cov and marginal
  likelihood converges to that of a KF in the large ensemble limit.
  This class tests that they are the same. It does that by implementing a
  one-step KF. It also does some simple checks on the KF, to make sure we didn't
  just replicate misunderstanding in the EnKF.
  
  This class also checks that various flavors of the EnKF are the same.
docstr: Check that the KF and EnKF solutions are the same.
docstr_tok: ["Check","that","the","KF","and","EnKF","solutions","are","the","same","."]
code: def test_kf_vs_enkf(
      self,
      noise_level,
      n_states,
      n_observations,
      perturbed_observations,
  ):
      """Check that the KF and EnKF solutions are the same."""
      # Tests pass with n_ensemble = 1e7. The KF vs. EnKF tolerance is
      # proportional to 1 / sqrt(n_ensemble), so this shows good agreement.
      n_ensemble = int(1e4) if NUMPY_MODE else int(1e5)
  
      salt = str(noise_level) + str(n_states) + str(n_observations)
      seed_stream = test_util.test_seed_stream(salt)
      dtype = tf.float64
      predict_kwargs = {}
      update_kwargs = {}
      log_marginal_likelihood_kwargs = {
          "perturbed_observations": perturbed_observations,
      }
  
      linear_model_params = self._get_linear_model_params(
          noise_level=noise_level,
          n_states=n_states,
          n_observations=n_observations,
          seed_stream=seed_stream,
          dtype=dtype,
      )
  
      # Ensure that our observation comes from a state that ~ prior.
      prior_dist = mvn_tril.MultivariateNormalTriL(
          loc=linear_model_params.prior_mean,
          scale_tril=tf.linalg.cholesky(linear_model_params.prior_cov),
      )
      true_state = prior_dist.sample(seed=seed_stream())
      observation = tf.linalg.matvec(
          linear_model_params.observation_mat, true_state
      )
  
      kf_soln = self._kalman_filter_solve(
          observation, linear_model_params
      )
  
      enkf_params = self._get_enkf_params(
          n_ensemble,
          linear_model_params,
          prior_dist,
          seed_stream,
          dtype,
      )
      enkf_soln = self._enkf_solve(
          observation,
          enkf_params,
          predict_kwargs,
          update_kwargs,
          log_marginal_likelihood_kwargs,
          seed_stream,
      )
  
      # In the low noise limit, the spectral norm of the posterior covariance is
      # bounded by reconstruction_tol**2.
      # http://screen/96UV8kiXMvp8QSM
      reconstruction_tol = noise_level / tf.reduce_min(
          tf.linalg.svd(
              linear_model_params.observation_mat, compute_uv=False
          )
      )
  
      # Evaluate at the same time, so both use the same randomness!
      # Do not use anything that was not evaluated here!
      (
          true_state,
          reconstruction_tol,
          kf_soln,
          enkf_soln,
      ) = self.evaluate(
          [true_state, reconstruction_tol, kf_soln, enkf_soln]
      )
  
      max_updated_scale = self.evaluate(
          tf.sqrt(
              tf.reduce_max(
                  tf.linalg.svd(
                      kf_soln["updated_cov"], compute_uv=False
                  )
              )
          )
      )
  
      if noise_level < 0.2 and n_states == n_observations:
          # Check that the theoretical error bound is obeyed.
          # We use max_updated_scale below to check reconstruction error, but
          # without this check here, it's possible that max_updated_scale is large
          # due to some error in the kalman filter...which would invalidate checks
          # below.
          slop = 2.0 + 5 * noise_level
          self.assertLess(max_updated_scale, slop * reconstruction_tol)
  
      # The KF should reconstruct the correct value up to 5 stddevs.
      # The relevant stddev is that of a χ² random variable.
      reconstruction_error = np.linalg.norm(
          kf_soln["updated_mean"] - true_state, axis=-1
      )
      self.assertLess(
          reconstruction_error,
          5 * np.sqrt(2 * n_states) * max_updated_scale,
      )
  
      # We know the EnKF converges at rate 1 / Sqrt(n_ensemble). The factor in
      # front is set empirically.
      tol_scale = 1 / np.sqrt(n_ensemble)  # 1 / Sqrt(1e5) ~= 0.003
      self.assertAllCloseNested(
          kf_soln, enkf_soln, atol=20 * tol_scale, rtol=50 * tol_scale
      )
  
imports: import collections
  from absl.testing import parameterized
  import numpy
  import tensorflow.compat.v2
  from tensorflow_probability.python.distributions import joint_distribution_named
  from tensorflow_probability.python.distributions import mvn_diag
  from tensorflow_probability.python.distributions import mvn_tril
  from tensorflow_probability.python.distributions import poisson
  from tensorflow_probability.python.experimental.sequential import ensemble_kalman_filter
  from tensorflow_probability.python.internal import test_combinations
  from tensorflow_probability.python.internal import test_util
  from tensorflow_probability.python.stats import sample_stats
label_desc: Use image node_id 6 for calling the ComparingMethodsTest obj's underlying member method code with example usage: obj.test_kf_vs_enkf(noise_level, n_states, n_observations, perturbed_observations) without return types