text: # -*- coding: utf-8 -*-
  __author__ = ["Matthew Middlehurst", "Patrick Schäfer"]
  __all__ = ["SFA"]
  
  import math
  import sys
  import warnings
  
  import numpy as np
  import pandas as pd
  from joblib import Parallel, delayed
  from numba import njit, types, NumbaTypeSafetyWarning
  from numba.typed import Dict
  from sklearn.feature_selection import f_classif
  from sklearn.preprocessing import KBinsDiscretizer
  from sklearn.tree import DecisionTreeClassifier
  
  from sktime.transformations.base import _PanelToPanelTransformer
  from sktime.utils.validation.panel import check_X
  
  # The binning methods to use: equi-depth, equi-width, information gain or kmeans
  binning_methods = {"equi-depth", "equi-width", "information-gain", "kmeans"}
  
  # TODO remove imag-part from dc-component component
  
  
  class SFA(_PanelToPanelTransformer):
      """SFA (Symbolic Fourier Approximation) Transformer, as described in
  
      @inproceedings{schafer2012sfa,
        title={SFA: a symbolic fourier approximation and index for similarity
        search in high dimensional datasets},
        author={Sch{\"a}fer, Patrick and H{\"o}gqvist, Mikael},
        booktitle={Proceedings of the 15th International Conference on
        Extending Database Technology},
        pages={516--527},
        year={2012},
        organization={ACM}
      }
  
      Overview: for each series:
          run a sliding window across the series
          for each window
              shorten the series with DFT
              discretise the shortened series into bins set by MFC
              form a word from these discrete values
      by default SFA produces a single word per series (window_size=0)
      if a window is used, it forms a histogram of counts of words.
  
      Parameters
      ----------
          word_length:         int, default = 8
              length of word to shorten window to (using PAA)
  
          alphabet_size:       int, default = 4
              number of values to discretise each value to
  
          window_size:         int, default = 12
              size of window for sliding. Input series
              length for whole series transform
  
          norm:                boolean, default = False
              mean normalise words by dropping first fourier coefficient
  
          binning_method:      {"equi-depth", "equi-width", "information-gain", "kmeans"},
                               default="equi-depth"
              the binning method used to derive the breakpoints.
  
          anova:               boolean, default = False
              If True, the Fourier coefficient selection is done via a one-way
              ANOVA test. If False, the first Fourier coefficients are selected.
              Only applicable if labels are given
  
          bigrams:             boolean, default = False
              whether to create bigrams of SFA words
  
          skip_grams:          boolean, default = False
              whether to create skip-grams of SFA words
  
          remove_repeat_words: boolean, default = False
              whether to use numerosity reduction (default False)
  
          levels:              int, default = 1
              Number of spatial pyramid levels
  
          save_words:          boolean, default = False
              whether to save the words generated for each series (default False)
  
          return_pandas_data_series:          boolean, default = False
              set to true to return Pandas Series as a result of transform.
              setting to true reduces speed significantly but is required for
              automatic test.
  
          n_jobs:              int, optional, default = 1
              The number of jobs to run in parallel for both `transform`.
              ``-1`` means using all processors.
  
      Attributes
      ----------
          words: []
          breakpoints: = []
          num_insts = 0
          num_atts = 0
      """
  
      _tags = {"univariate-only": True}
  
      def __init__(
          self,
          word_length=8,
          alphabet_size=4,
          window_size=12,
          norm=False,
          binning_method="equi-depth",
          anova=False,
          bigrams=False,
          skip_grams=False,
          remove_repeat_words=False,
          levels=1,
          lower_bounding=True,
          save_words=False,
          keep_binning_dft=False,
          return_pandas_data_series=False,
          use_fallback_dft=False,
          typed_dict=False,
          n_jobs=1,
      ):
          self.words = []
          self.breakpoints = []
  
          # we cannot select more than window_size many letters in a word
          offset = 2 if norm else 0
          self.dft_length = window_size - offset if anova is True else word_length
          # make dft_length an even number (same number of reals and imags)
          self.dft_length = self.dft_length + self.dft_length % 2
  
          self.support = np.array(list(range(word_length)))
  
          self.word_length = word_length
          self.alphabet_size = alphabet_size
          self.window_size = window_size
          self.lower_bounding = lower_bounding
          self.inverse_sqrt_win_size = (
              1.0 / math.sqrt(window_size) if not lower_bounding else 1.0
          )
  
          self.norm = norm
          self.remove_repeat_words = remove_repeat_words
  
          self.save_words = save_words
          self.keep_binning_dft = keep_binning_dft
          self.binning_dft = None
  
          self.levels = levels
          self.binning_method = binning_method
          self.anova = anova
  
          self.bigrams = bigrams
          self.skip_grams = skip_grams
  
          self.return_pandas_data_series = return_pandas_data_series
          self.use_fallback_dft = (
              use_fallback_dft if word_length < window_size - offset else True
          )
          self.typed_dict = typed_dict
  
          self.n_jobs = n_jobs
  
          self.n_instances = 0
          self.series_length = 0
          self.letter_bits = 0
          self.letter_max = 0
          self.word_bits = 0
          self.max_bits = 0
          self.level_bits = 0
          self.level_max = 0
  
          super(SFA, self).__init__()
  
      def fit(self, X, y=None):
          """Calculate word breakpoints using _mcb
  
          Parameters
          ----------
          X: nested pandas DataFrame of shape [n_instances, 1]
              Nested dataframe with univariate time-series in cells.
          y: array-like, shape = [n_samples] or [n_samples, n_outputs]
              The class labels.
  
          Returns
          -------
          self: object
          """
  
          if self.alphabet_size < 2:
              raise ValueError("Alphabet size must be an integer greater than 2")
  
          if self.word_length < 1:
              raise ValueError("Word length must be an integer greater than 1")
  
          if self.binning_method == "information-gain" and y is None:
              raise ValueError(
                  "Class values must be provided for information gain binning"
              )
  
          if self.binning_method not in binning_methods:
              raise TypeError("binning_method must be one of: ", binning_methods)
  
          self.letter_bits = math.ceil(math.log2(self.alphabet_size))
          self.letter_max = pow(2, self.letter_bits) - 1
          self.word_bits = self.word_length * self.letter_bits
          self.max_bits = (
              self.word_bits * 2 if self.bigrams or self.skip_grams else self.word_bits
          )
  
          if self.typed_dict and self.max_bits > 64:
              raise ValueError(
                  "Typed Dictionaries can only handle 64 bit words. "
                  "ceil(log2(alphabet_size)) * word_length must be less than or equal "
                  "to 64"
              )
  
          if self.typed_dict and self.levels > 15:
              raise ValueError(
                  "Typed Dictionaries can only handle 15 levels "
                  "(this is way to many anyway)."
              )
  
          X = check_X(X, enforce_univariate=True, coerce_to_numpy=True)
          X = X.squeeze(1)
  
          if self.levels > 1:
              quadrants = 0
              for i in range(self.levels):
                  quadrants += pow(2, i)
              self.level_bits = math.ceil(math.log2(quadrants))
              self.level_max = pow(2, self.level_bits) - 1
  
          self.n_instances, self.series_length = X.shape
          self.breakpoints = self._binning(X, y)
  
          self._is_fitted = True
          return self
  
      def transform(self, X, y=None):
          self.check_is_fitted()
          X = check_X(X, enforce_univariate=True, coerce_to_numpy=True)
          X = X.squeeze(1)
  
          if self.typed_dict:
              warnings.simplefilter("ignore", category=NumbaTypeSafetyWarning)
  
          transform = Parallel(n_jobs=self.n_jobs)(
              delayed(self._transform_case)(
                  X[i, :],
                  supplied_dft=self.binning_dft[i] if self.keep_binning_dft else None,
              )
              for i in range(X.shape[0])
          )
  
          dim, words = zip(*transform)
          if self.save_words:
              self.words = list(words)
          bags = pd.DataFrame() if self.return_pandas_data_series else [None]
          bags[0] = list(dim)
  
          return bags
  
      def _transform_case(self, X, supplied_dft=None):
          if supplied_dft is None:
              dfts = self._mft(X)
          else:
              dfts = supplied_dft
  
          if self.typed_dict:
              bag = (
                  Dict.empty(
                      key_type=types.UniTuple(types.int64, 2), value_type=types.uint32
                  )
                  if self.levels > 1
                  else Dict.empty(key_type=types.int64, value_type=types.uint32)
              )
          else:
              bag = {}
  
          last_word = -1
          repeat_words = 0
          words = (
              np.zeros(dfts.shape[0], dtype=np.int64)
              if self.word_bits <= 64
              else [0 for _ in range(dfts.shape[0])]
          )
  
          for window in range(dfts.shape[0]):
              word_raw = (
                  SFA._create_word(
                      dfts[window],
                      self.word_length,
                      self.alphabet_size,
                      self.breakpoints,
                      self.letter_bits,
                  )
                  if self.word_bits <= 64
                  else self._create_word_large(
                      dfts[window],
                  )
              )
              words[window] = word_raw
  
              repeat_word = (
                  self._add_to_pyramid(
                      bag, word_raw, last_word, window - int(repeat_words / 2)
                  )
                  if self.levels > 1
                  else self._add_to_bag(bag, word_raw, last_word)
              )
  
              if repeat_word:
                  repeat_words += 1
              else:
                  last_word = word_raw
                  repeat_words = 0
  
              if self.bigrams:
                  if window - self.window_size >= 0:
                      bigram = self._create_bigram_words(
                          word_raw, words[window - self.window_size]
                      )
  
                      if self.levels > 1:
                          if self.typed_dict:
                              bigram = (bigram, -1)
                          else:
                              bigram = (bigram << self.level_bits) | 0
                      bag[bigram] = bag.get(bigram, 0) + 1
  
              if self.skip_grams:
                  # creates skip-grams, skipping every (s-1)-th word in-between
                  for s in range(2, 4):
                      if window - s * self.window_size >= 0:
                          skip_gram = self._create_bigram_words(
                              word_raw, words[window - s * self.window_size]
                          )
  
                          if self.levels > 1:
                              if self.typed_dict:
                                  skip_gram = (skip_gram, -1)
                              else:
                                  skip_gram = (skip_gram << self.level_bits) | 0
                          bag[skip_gram] = bag.get(skip_gram, 0) + 1
  
          return [
              pd.Series(bag) if self.return_pandas_data_series else bag,
              words if self.save_words else [],
          ]
  
      def _binning(self, X, y=None):
          num_windows_per_inst = math.ceil(self.series_length / self.window_size)
          dft = np.array(
              [
                  self._binning_dft(X[i, :], num_windows_per_inst)
                  for i in range(self.n_instances)
              ]
          )
          if self.keep_binning_dft:
              self.binning_dft = dft
          dft = dft.reshape(len(X) * num_windows_per_inst, self.dft_length)
  
          if y is not None:
              y = np.repeat(y, num_windows_per_inst)
  
          if self.anova and y is not None:
              non_constant = np.where(
                  ~np.isclose(dft.var(axis=0), np.zeros_like(dft.shape[1]))
              )[0]
  
              # select word-length many indices with best f-score
              if self.word_length <= non_constant.size:
                  f, _ = f_classif(dft[:, non_constant], y)
                  self.support = non_constant[np.argsort(-f)][: self.word_length]
  
              # sort remaining indices
              # self.support = np.sort(self.support)
  
              # select the Fourier coefficients with highest f-score
              dft = dft[:, self.support]
              self.dft_length = np.max(self.support) + 1
              self.dft_length = self.dft_length + self.dft_length % 2  # even
  
          if self.binning_method == "information-gain":
              return self._igb(dft, y)
          elif self.binning_method == "kmeans":
              return self._KBinsDiscretizer(dft)
          else:
              return self._mcb(dft)
  
      def _KBinsDiscretizer(self, dft):
          encoder = KBinsDiscretizer(
              n_bins=self.alphabet_size, strategy=self.binning_method
          )
          encoder.fit(dft)
          breaks = encoder.bin_edges_
          breakpoints = np.zeros((self.word_length, self.alphabet_size))
  
          for letter in range(self.word_length):
              for bp in range(1, len(breaks[letter]) - 1):
                  breakpoints[letter][bp - 1] = breaks[letter][bp]
  
          breakpoints[:, self.alphabet_size - 1] = sys.float_info.max
          return breakpoints
  
      def _mcb(self, dft):
          num_windows_per_inst = math.ceil(self.series_length / self.window_size)
          total_num_windows = int(self.n_instances * num_windows_per_inst)
          breakpoints = np.zeros((self.word_length, self.alphabet_size))
  
          for letter in range(self.word_length):
              res = [round(dft[i][letter] * 100) / 100 for i in range(total_num_windows)]
              column = np.sort(np.array(res))
  
              bin_index = 0
  
              # use equi-depth binning
              if self.binning_method == "equi-depth":
                  target_bin_depth = total_num_windows / self.alphabet_size
  
                  for bp in range(self.alphabet_size - 1):
                      bin_index += target_bin_depth
                      breakpoints[letter][bp] = column[int(bin_index)]
  
              # use equi-width binning aka equi-frequency binning
              elif self.binning_method == "equi-width":
                  target_bin_width = (column[-1] - column[0]) / self.alphabet_size
  
                  for bp in range(self.alphabet_size - 1):
                      breakpoints[letter][bp] = (bp + 1) * target_bin_width + column[0]
  
          breakpoints[:, self.alphabet_size - 1] = sys.float_info.max
          return breakpoints
  
      def _igb(self, dft, y):
          breakpoints = np.zeros((self.word_length, self.alphabet_size))
          clf = DecisionTreeClassifier(
              criterion="entropy",
              max_depth=np.log2(self.alphabet_size),
              max_leaf_nodes=self.alphabet_size,
              random_state=1,
          )
  
          for i in range(self.word_length):
              clf.fit(dft[:, i][:, None], y)
              threshold = clf.tree_.threshold[clf.tree_.children_left != -1]
              for bp in range(len(threshold)):
                  breakpoints[i][bp] = threshold[bp]
              for bp in range(len(threshold), self.alphabet_size):
                  breakpoints[i][bp] = sys.float_info.max
  
          return np.sort(breakpoints, axis=1)
  
      def _binning_dft(self, series, num_windows_per_inst):
          # Splits individual time series into windows and returns the DFT for
          # each
          split = np.split(
              series,
              np.linspace(
                  self.window_size,
                  self.window_size * (num_windows_per_inst - 1),
                  num_windows_per_inst - 1,
                  dtype=np.int_,
              ),
          )
          start = self.series_length - self.window_size
          split[-1] = series[start : self.series_length]
  
          result = np.zeros((len(split), self.dft_length), dtype=np.float64)
  
          for i, row in enumerate(split):
              result[i] = (
                  self._discrete_fourier_transform(
                      row,
                      self.dft_length,
                      self.norm,
                      self.inverse_sqrt_win_size,
                      self.lower_bounding,
                  )
                  if self.use_fallback_dft
                  else self._fast_fourier_transform(row)
              )
  
          return result
  
      def _fast_fourier_transform(self, series):
          """Performs a discrete fourier transform using the fast fourier
          transform
          if self.norm is True, then the first term of the DFT is ignored
  
          Input
          -------
          X : The training input samples.  array-like or sparse matrix of
          shape = [n_samps, num_atts]
  
          Returns
          -------
          1D array of fourier term, real_0,imag_0, real_1, imag_1 etc, length
          num_atts or
          num_atts-2 if if self.norm is True
          """
          # first two are real and imaginary parts
          start = 2 if self.norm else 0
  
          s = np.std(series)
          std = s if s > 1e-8 else 1
  
          X_fft = np.fft.rfft(series)
          reals = np.real(X_fft)
          imags = np.imag(X_fft)
  
          length = start + self.dft_length
          dft = np.empty((length,), dtype=reals.dtype)
          dft[0::2] = reals[: np.uint32(length / 2)]
          dft[1::2] = imags[: np.uint32(length / 2)]
          if self.lower_bounding:
              dft[1::2] = dft[1::2] * -1  # lower bounding
          dft *= self.inverse_sqrt_win_size / std
          return dft[start:]
  
      @staticmethod
      @njit(fastmath=True, cache=True)
      def _discrete_fourier_transform(
          series,
          dft_length,
          norm,
          inverse_sqrt_win_size,
          lower_bounding,
          apply_normalising_factor=True,
          cut_start_if_norm=True,
      ):
          """Performs a discrete fourier transform using standard O(n^2)
          transform
          if self.norm is True, then the first term of the DFT is ignored
  
          Input
          -------
          X : The training input samples.  array-like or sparse matrix of
          shape = [n_samps, num_atts]
  
          Returns
          -------
          1D array of fourier term, real_0,imag_0, real_1, imag_1 etc, length
          num_atts or
          num_atts-2 if if self.norm is True
          """
          start = 2 if norm else 0
          output_length = start + dft_length
  
          if cut_start_if_norm:
              c = int(start / 2)
          else:
              c = 0
              start = 0
  
          dft = np.zeros(output_length - start)
          for i in range(c, int(output_length / 2)):
              for n in range(len(series)):
                  dft[(i - c) * 2] += series[n] * math.cos(
                      2 * math.pi * n * i / len(series)
                  )
                  dft[(i - c) * 2 + 1] += -series[n] * math.sin(
                      2 * math.pi * n * i / len(series)
                  )
  
          if apply_normalising_factor:
              if lower_bounding:
                  dft[1::2] = dft[1::2] * -1  # lower bounding
  
              std = np.std(series)
              if std == 0:
                  std = 1
              dft *= inverse_sqrt_win_size / std
  
          return dft
  
      def _mft(self, series):
          """
  
          :param series:
          :return:
          """
          start_offset = 2 if self.norm else 0
          length = self.dft_length + start_offset + self.dft_length % 2
          end = max(1, len(series) - self.window_size + 1)
  
          phis = SFA._get_phis(self.window_size, length)
          stds = SFA._calc_incremental_mean_std(series, end, self.window_size)
          transformed = np.zeros((end, length))
  
          # first run with dft
          if self.use_fallback_dft:
              mft_data = self._discrete_fourier_transform(
                  series[0 : self.window_size],
                  self.dft_length,
                  self.norm,
                  self.inverse_sqrt_win_size,
                  self.lower_bounding,
                  apply_normalising_factor=False,
                  cut_start_if_norm=False,
              )
          else:
              X_fft = np.fft.rfft(series[: self.window_size])
              reals = np.real(X_fft)
              imags = np.imag(X_fft)
              mft_data = np.empty((length,), dtype=reals.dtype)
              mft_data[0::2] = reals[: np.uint32(length / 2)]
              mft_data[1::2] = imags[: np.uint32(length / 2)]
  
          transformed[0] = mft_data * self.inverse_sqrt_win_size / stds[0]
  
          # other runs using mft
          # moved to external method to use njit
          SFA._iterate_mft(
              series,
              mft_data,
              phis,
              self.window_size,
              stds,
              transformed,
              self.inverse_sqrt_win_size,
          )
  
          if self.lower_bounding:
              transformed[:, 1::2] = transformed[:, 1::2] * -1  # lower bounding
  
          return (
              transformed[:, start_offset:][:, self.support]
              if self.anova
              else transformed[:, start_offset:]
          )
  
      @staticmethod
      @njit(fastmath=True, cache=True)
      def _get_phis(window_size, length):
          phis = np.zeros(length)
          for i in range(int(length / 2)):
              phis[i * 2] += math.cos(2 * math.pi * (-i) / window_size)
              phis[i * 2 + 1] += -math.sin(2 * math.pi * (-i) / window_size)
          return phis
  
      @staticmethod
      @njit(fastmath=True, cache=True)
      def _iterate_mft(
          series, mft_data, phis, window_size, stds, transformed, inverse_sqrt_win_size
      ):
          for i in range(1, len(transformed)):
              for n in range(0, len(mft_data), 2):
                  # only compute needed indices
                  real = mft_data[n] + series[i + window_size - 1] - series[i - 1]
                  imag = mft_data[n + 1]
                  mft_data[n] = real * phis[n] - imag * phis[n + 1]
                  mft_data[n + 1] = real * phis[n + 1] + phis[n] * imag
  
              normalising_factor = inverse_sqrt_win_size / stds[i]
              transformed[i] = mft_data * normalising_factor
  
      def _shorten_bags(self, word_len):
          if self.save_words is False:
              raise ValueError(
                  "Words from transform must be saved using save_word to shorten bags."
              )
  
          if word_len > self.word_length:
              word_len = self.word_length
  
          if self.typed_dict:
              warnings.simplefilter("ignore", category=NumbaTypeSafetyWarning)
  
          dim = Parallel(n_jobs=self.n_jobs)(
              delayed(self._shorten_case)(word_len, i) for i in range(len(self.words))
          )
  
          new_bags = pd.DataFrame() if self.return_pandas_data_series else [None]
          new_bags[0] = list(dim)
  
          return new_bags
  
      def _shorten_case(self, word_len, i):
          if self.typed_dict:
              new_bag = (
                  Dict.empty(
                      key_type=types.Tuple((types.int64, types.int16)),
                      value_type=types.uint32,
                  )
                  if self.levels > 1
                  else Dict.empty(key_type=types.int64, value_type=types.uint32)
              )
          else:
              new_bag = {}
  
          last_word = -1
          repeat_words = 0
  
          for window, word in enumerate(self.words[i]):
              new_word = self._shorten_words(word, word_len)
  
              repeat_word = (
                  self._add_to_pyramid(
                      new_bag, new_word, last_word, window - int(repeat_words / 2)
                  )
                  if self.levels > 1
                  else self._add_to_bag(new_bag, new_word, last_word)
              )
  
              if repeat_word:
                  repeat_words += 1
              else:
                  last_word = new_word
                  repeat_words = 0
  
              if self.bigrams:
                  if window - self.window_size >= 0:
                      bigram = self._create_bigram_words(
                          new_word,
                          self._shorten_words(
                              self.words[i][window - self.window_size], word_len
                          ),
                      )
  
                      if self.levels > 1:
                          if self.typed_dict:
                              bigram = (bigram, -1)
                          else:
                              bigram = (bigram << self.level_bits) | 0
                      new_bag[bigram] = new_bag.get(bigram, 0) + 1
  
              if self.skip_grams:
                  # creates skip-grams, skipping every (s-1)-th word in-between
                  for s in range(2, 4):
                      if window - s * self.window_size >= 0:
                          skip_gram = self._create_bigram_words(
                              new_word,
                              self._shorten_words(
                                  self.words[i][window - s * self.window_size],
                                  word_len,
                              ),
                          )
  
                          if self.levels > 1:
                              if self.typed_dict:
                                  skip_gram = (skip_gram, -1)
                              else:
                                  skip_gram = (skip_gram << self.level_bits) | 0
                          new_bag[skip_gram] = new_bag.get(skip_gram, 0) + 1
  
          return pd.Series(new_bag) if self.return_pandas_data_series else new_bag
  
      def _add_to_bag(self, bag, word, last_word):
          if self.remove_repeat_words and word == last_word:
              return True
  
          # store the histogram of word counts
          bag[word] = bag.get(word, 0) + 1
  
          return False
  
      def _add_to_pyramid(self, bag, word, last_word, window_ind):
          if self.remove_repeat_words and word == last_word:
              return True
  
          start = 0
          for i in range(self.levels):
              if self.typed_dict:
                  new_word, num_quadrants = SFA._add_level_typed(
                      word, start, i, window_ind, self.window_size, self.series_length
                  )
              else:
                  new_word, num_quadrants = (
                      SFA._add_level(
                          word,
                          start,
                          i,
                          window_ind,
                          self.window_size,
                          self.series_length,
                          self.level_bits,
                      )
                      if self.word_bits + self.level_bits <= 64
                      else self._add_level_large(
                          word,
                          start,
                          i,
                          window_ind,
                      )
                  )
              bag[new_word] = bag.get(new_word, 0) + num_quadrants
              start += num_quadrants
  
          return False
  
      @staticmethod
      @njit(fastmath=True, cache=True)
      def _add_level(
          word, start, level, window_ind, window_size, series_length, level_bits
      ):
          num_quadrants = pow(2, level)
          quadrant = start + int(
              (window_ind + int(window_size / 2)) / int(series_length / num_quadrants)
          )
          return (word << level_bits) | quadrant, num_quadrants
  
      def _add_level_large(self, word, start, level, window_ind):
          num_quadrants = pow(2, level)
          quadrant = start + int(
              (window_ind + int(self.window_size / 2))
              / int(self.series_length / num_quadrants)
          )
          return (word << self.level_bits) | quadrant, num_quadrants
  
      @staticmethod
      @njit(fastmath=True, cache=True)
      def _add_level_typed(word, start, level, window_ind, window_size, series_length):
          num_quadrants = pow(2, level)
          quadrant = start + int(
              (window_ind + int(window_size / 2)) / int(series_length / num_quadrants)
          )
          return (word, quadrant), num_quadrants
  
      @staticmethod
      @njit(fastmath=True, cache=True)
      def _create_word(dft, word_length, alphabet_size, breakpoints, letter_bits):
          word = np.int64(0)
          for i in range(word_length):
              for bp in range(alphabet_size):
                  if dft[i] <= breakpoints[i][bp]:
                      word = (word << letter_bits) | bp
                      break
  
          return word
  
      def _create_word_large(self, dft):
          word = 0
          for i in range(self.word_length):
              for bp in range(self.alphabet_size):
                  if dft[i] <= self.breakpoints[i][bp]:
                      word = (word << self.letter_bits) | bp
                      break
  
          return word
  
      @staticmethod
      @njit(fastmath=True, cache=True)
      def _calc_incremental_mean_std(series, end, window_size):
          stds = np.zeros(end)
          window = series[0:window_size]
          series_sum = np.sum(window)
          square_sum = np.sum(np.multiply(window, window))
  
          r_window_length = 1 / window_size
          mean = series_sum * r_window_length
          buf = math.sqrt(square_sum * r_window_length - mean * mean)
          stds[0] = buf if buf > 1e-8 else 1
  
          for w in range(1, end):
              series_sum += series[w + window_size - 1] - series[w - 1]
              mean = series_sum * r_window_length
              square_sum += (
                  series[w + window_size - 1] * series[w + window_size - 1]
                  - series[w - 1] * series[w - 1]
              )
              buf = math.sqrt(square_sum * r_window_length - mean * mean)
              stds[w] = buf if buf > 1e-8 else 1
  
          return stds
  
      def _create_bigram_words(self, word_raw, word):
          return (
              SFA._create_bigram_word(
                  word,
                  word_raw,
                  self.word_bits,
              )
              if self.max_bits <= 64
              else self._create_bigram_word_large(
                  word,
                  word_raw,
              )
          )
  
      @staticmethod
      @njit(fastmath=True, cache=True)
      def _create_bigram_word(word, other_word, word_bits):
          return (word << word_bits) | other_word
  
      def _create_bigram_word_large(self, word, other_word):
          return (word << self.word_bits) | other_word
  
      def _shorten_words(self, word, word_len):
          return (
              SFA._shorten_word(word, self.word_length - word_len, self.letter_bits)
              if self.word_bits <= 64
              else self._shorten_word_large(word, self.word_length - word_len)
          )
  
      @staticmethod
      @njit(fastmath=True, cache=True)
      def _shorten_word(word, amount, letter_bits):
          # shorten a word by set amount of letters
          return word >> amount * letter_bits
  
      def _shorten_word_large(self, word, amount):
          # shorten a word by set amount of letters
          return word >> amount * self.letter_bits
  
      def bag_to_string(self, bag):
          s = "{"
          for word, value in bag.items():
              s += "{0}: {1}, ".format(
                  self.word_list_typed(word) if self.typed_dict else self.word_list(word),
                  value,
              )
          s = s[:-2]
          return s + "}"
  
      def word_list(self, word):
          # list of input integers to obtain current word
  
          letters = []
          word_bits = self.word_bits + self.level_bits
          shift = word_bits - self.letter_bits
  
          for _ in range(self.word_length, 0, -1):
              letters.append(word >> shift & self.letter_max)
              shift -= self.letter_bits
  
          if word.bit_length() > self.word_bits + self.level_bits:
              bigram_letters = []
              shift = self.word_bits + word_bits - self.letter_bits
              for _ in range(self.word_length, 0, -1):
                  bigram_letters.append(word >> shift & self.letter_max)
                  shift -= self.letter_bits
  
              letters = (bigram_letters, letters)
  
          if self.levels > 1:
              level = word >> 0 & self.level_max
              letters = (letters, level)
  
          return letters
  
      def word_list_typed(self, word):
          # list of input integers to obtain current word
  
          letters = []
          shift = self.word_bits - self.letter_bits
  
          if self.levels > 1:
              level = word[1]
              word = word[0]
  
          for _ in range(self.word_length, 0, -1):
              letters.append(word >> shift & self.letter_max)
              shift -= self.letter_bits
  
          if word.bit_length() > self.word_bits:
              bigram_letters = []
              shift = self.word_bits + self.word_bits - self.letter_bits
              for _ in range(self.word_length, 0, -1):
                  bigram_letters.append(word >> shift & self.letter_max)
                  shift -= self.letter_bits
  
              letters = (bigram_letters, letters)
  
          if self.levels > 1:
              letters = (letters, level)
  
          return letters
  