text: import numpy as np
  
  from base import HiddenMarkovChain
  
  
  class HiddenMarkovChain_Simulation(HiddenMarkovChain):
      def run(self, length: int) -> (list, list):
          assert length >= 0, "The chain needs to be a non-negative number."
          s_history = [0] * (length + 1)
          o_history = [0] * (length + 1)
          
          prb = self.pi.values
          obs = prb @ self.E.values
          s_history[0] = np.random.choice(self.states, p=prb.flatten())
          o_history[0] = np.random.choice(self.observables, p=obs.flatten())
          
          for t in range(1, length + 1):
              prb = prb @ self.T.values
              obs = prb @ self.E.values
              s_history[t] = np.random.choice(self.states, p=prb.flatten())
              o_history[t] = np.random.choice(self.observables, p=obs.flatten())
          
          return o_history, s_history
  
  
  class HiddenMarkovChain_Uncover(HiddenMarkovChain_Simulation):
      def _alphas(self, observations: list) -> np.ndarray:
          alphas = np.zeros((len(observations), len(self.states)))
          alphas[0, :] = self.pi.values * self.E[observations[0]].T
          for t in range(1, len(observations)):
              alphas[t, :] = (alphas[t - 1, :].reshape(1, -1) @ self.T.values) \
                           * self.E[observations[t]].T
          return alphas
      
      def _betas(self, observations: list) -> np.ndarray:
          betas = np.zeros((len(observations), len(self.states)))
          betas[-1, :] = 1
          for t in range(len(observations) - 2, -1, -1):
              betas[t, :] = (self.T.values @ (self.E[observations[t + 1]] \
                          * betas[t + 1, :].reshape(-1, 1))).reshape(1, -1)
          return betas
      
      def uncover(self, observations: list) -> list:
          alphas = self._alphas(observations)
          betas = self._betas(observations)
          maxargs = (alphas * betas).argmax(axis=1)
          return list(map(lambda x: self.states[x], maxargs))
          
  
  class HiddenMarkovLayer(HiddenMarkovChain_Uncover):
      def _digammas(self, observations: list) -> np.ndarray:
          L, N = len(observations), len(self.states)
          digammas = np.zeros((L - 1, N, N))
  
          alphas = self._alphas(observations)
          betas = self._betas(observations)
          # score = self.score(observations)
          score = alphas[-1, :].sum()
          for t in range(L - 1):
              P1 = (alphas[t, :].reshape(-1, 1) * self.T.values)
              P2 = self.E[observations[t + 1]].T * betas[t + 1].reshape(1, -1)
              digammas[t, :, :] = P1 * P2 / score
          return digammas  
  