class_docstr: Bayesian online change point detection algorithm described by
  `Adams & MacKay (2007) <https://arxiv.org/abs/0710.3742>`__.
  At a high level, this algorithm models the observed data using Bayesian conjugate priors. If an observed value
  deviates too much from the current posterior distribution, it is likely a change point, and we should start
  modeling the time series from that point forwards with a freshly initialized Bayesian conjugate prior.
  
  The ``get_anomaly_score()`` method returns a z-score corresponding to the probability of each point being
  a change point. The ``forecast()`` method returns the predicted values (and standard error) of the underlying
  piecewise model on the relevant data.
class_docstr_tok: ["Bayesian","online","change","point","detection","algorithm","described","by","`","Adams","&","MacKay","(","2007",")","<","https",":","\/\/arxiv.org\/abs\/0710.3742",">","`","__",".","At","a","high","level",",","this","algorithm","models","the","observed","data","using","Bayesian","conjugate","priors",".","If","an","observed","value","deviates","too","much","from","the","current","posterior","distribution",",","it","is","likely","a","change","point",",","and","we","should","start","modeling","the","time","series","from","that","point","forwards","with","a","freshly","initialized","Bayesian","conjugate","prior",".","The","``","get_anomaly_score","(",")","``","method","returns","a","z-score","corresponding","to","the","probability","of","each","point","being","a","change","point",".","The","``","forecast","(",")","``","method","returns","the","predicted","values","(","and","standard","error",")","of","the","underlying","piecewise","model","on","the","relevant","data","."]
docstr: Updates the BOCPD model's internal state using the time series values provided.
  
  :param time_series: time series whose values we are using to update the internal state of the model
  :return: anomaly score associated with each point (based on the probability of it being a change point)
code: def update(self, time_series: TimeSeries):
      """
      Updates the BOCPD model's internal state using the time series values provided.
  
      :param time_series: time series whose values we are using to update the internal state of the model
      :return: anomaly score associated with each point (based on the probability of it being a change point)
      """
      # Only update on the portion of the time series after the last training timestamp
      time_stamps = time_series.time_stamps
      if self.last_train_time is not None:
          _, time_series = time_series.bisect(
              self.last_train_time, t_in_left=True
          )
  
      # Update the training data accumulated so far
      if self.train_data is None:
          self.train_data = time_series
      else:
          self.train_data = self.train_data + time_series
  
      # Align the time series & expand the array storing the full posterior distribution of run lengths
      time_series = time_series.align()
      n_seen, T = self.n_seen, len(time_series)
      self.full_run_length_posterior = scipy.sparse.block_diag(
          (
              self.full_run_length_posterior,
              scipy.sparse.dok_matrix((T, T), dtype=float),
          ),
          format="dok",
      )
  
      # Compute the minimum log likelihood threshold that we consider.
      min_ll = (
          -np.inf
          if self.min_likelihood is None or self.min_likelihood <= 0
          else np.log(self.min_likelihood)
      )
      if self.change_kind is ChangeKind.TrendChange:
          min_ll = min_ll * time_series.dim
      min_ll = min_ll + np.log(self.cp_prior)
  
      # Iterate over the time series
      for i, (t, x) in enumerate(
          tqdm(time_series, desc="BOCPD Update", disable=(T == 0))
      ):
          # Update posterior beams
          for post in self.posterior_beam:
              post.update((t, x))
  
          # Calculate posterior probability that this is change point with
          # P_changepoint = \sum_{r_{t-1}} P(r_{t-1}, x_{1:t-1}) * P(x_t) * cp_prior
          # After the updates, post.logp = log P(r_t, x_{1:t})
          #                              = log P(r_{t-1}, x_1{1:t-1}) + log P(x_t) - log(1 - cp_prior)
          # So we can just add log(cp_prior) - log(1 - cp_prior) to each of the logp's
          if len(self.posterior_beam) == 0:
              cp_logp = 0
          else:
              cp_delta = np.log(self.cp_prior) - np.log1p(
                  -self.cp_prior
              )
              cp_logp = logsumexp(
                  [post.logp + cp_delta for post in self.posterior_beam]
              )
          self.posterior_beam.append(
              self._create_posterior(logp=cp_logp)
          )
  
          # P(x_{1:t}) = \sum_{r_t} P(r_t, x_{1:t})
          evidence = logsumexp(
              [post.logp for post in self.posterior_beam]
          )
  
          # P(r_t) = P(r_t, x_{1:t}) / P(x_{1:t})
          run_length_dist_0 = {
              post.run_length: post.logp - evidence
              for post in self.posterior_beam
          }
  
          # Remove posterior beam candidates whose run length probability is too low
          run_length_dist, to_remove = {}, {}
          for r, logp in run_length_dist_0.items():
              if (
                  logp < min_ll and r > 2
              ):  # allow at least 2 updates for each change point hypothesis
                  to_remove[r] = logp
              else:
                  run_length_dist[r] = logp
  
          # Re-normalize all remaining probabilities to sum to 1
          self.posterior_beam = [
              post
              for post in self.posterior_beam
              if post.run_length not in to_remove
          ]
          if len(to_remove) > 0:
              excess_p = np.exp(
                  logsumexp(list(to_remove.values()))
              )  # log P[to_remove]
              for post in self.posterior_beam:
                  post.logp -= np.log1p(-excess_p)
                  run_length_dist[post.run_length] -= np.log1p(
                      -excess_p
                  )
  
          # Update the full posterior distribution of run-length at each time, up to the desired lag
          run_length_dist = [
              (r, logp) for r, logp in run_length_dist.items()
          ]
          if len(run_length_dist) > 0:
              all_r, all_logp_r = zip(*run_length_dist)
              self.full_run_length_posterior[
                  n_seen + i, all_r
              ] = np.exp(all_logp_r)
  
          # Add this timestamp to the list of timestamps we've trained on
          self.train_timestamps.append(t)
  
      # Update the predictive model if there is any new data
      if len(time_series) > 0:
          if self.lag is None:
              n = len(self.train_timestamps)
          else:
              n = T + self.lag
          self._update_model(self.train_timestamps[-n:])
  
      # Return the anomaly scores
      return self._get_anom_scores(time_stamps)
  
imports: import bisect
  import copy
  from enum import Enum
  import logging
  from typing import List, Tuple, Union
  import warnings
  import numpy
  import pandas
  import scipy.sparse
  from scipy.special import logsumexp
  from scipy.stats import norm
  from tqdm import tqdm
  from merlion.models.anomaly.base import NoCalibrationDetectorConfig, DetectorBase
  from merlion.models.anomaly.forecast_based.base import ForecastingDetectorBase
  from merlion.models.forecast.base import ForecasterConfig
  from merlion.plot import Figure
  from merlion.post_process.threshold import AggregateAlarms
  from merlion.utils.conj_priors import ConjPrior, MVNormInvWishart, BayesianMVLinReg
  from merlion.utils.time_series import TimeSeries, UnivariateTimeSeries, to_pd_datetime
label_desc: Use image node_id 18 for calling the BOCPD obj's underlying member method code with example usage: obj.update(time_series) and returns: self