docs: def get_bottom_up_likelihood(tree, character, frequencies, sf, kappa=None, is_marginal=True, model=F81):
      """
      Calculates the bottom-up likelihood for the given tree.
      The likelihood for each node is stored in the corresponding feature,
      given by get_personalised_feature_name(feature, BU_LH).
  
      :param model: model of character evolution
      :type model: str
      :param is_marginal: whether the likelihood reconstruction is marginal (true) or joint (false)
      :type is_marginal: bool
      :param tree: tree of interest
      :type tree: ete3.Tree
      :param character: character for which the likelihood is calculated
      :type character: str
      :param frequencies: array of state frequencies \pi_i
      :type frequencies: numpy.array
      :param sf: scaling factor
      :type sf: float
      :return: log likelihood
      :rtype: float
      """
      lh_sf_feature = get_personalized_feature_name(character, BU_LH_SF)
      lh_feature = get_personalized_feature_name(character, BU_LH)
      lh_joint_state_feature = get_personalized_feature_name(character, BU_LH_JOINT_STATES)
      allowed_state_feature = get_personalized_feature_name(character, ALLOWED_STATES)
  
      get_pij = get_pij_method(model, frequencies, kappa)
      for node in tree.traverse('postorder'):
          likelihood_array = np.ones(len(frequencies), dtype=np.float64) * getattr(node, allowed_state_feature)
          factors = 0
          for child in node.children:
              child_likelihoods = get_pij(child.dist * sf) * getattr(child, lh_feature)
              if is_marginal:
                  child_likelihoods = child_likelihoods.sum(axis=1)
              else:
                  child_states = child_likelihoods.argmax(axis=1)
                  child.add_feature(lh_joint_state_feature, child_states)
                  child_likelihoods = child_likelihoods.max(axis=1)
  
              factors += rescale(child_likelihoods, fraction_of_limit=len(node.children))
              likelihood_array *= child_likelihoods
  
          if np.all(likelihood_array == 0):
              return -np.inf
  
          factors += rescale(likelihood_array, fraction_of_limit=len(node.up.children) if not node.is_root() else 1)
          node.add_feature(lh_feature, likelihood_array)
          node.add_feature(lh_sf_feature, factors + sum(getattr(_, lh_sf_feature) for _ in node.children))
      root_likelihoods = getattr(tree, lh_feature) * frequencies
      root_likelihoods = root_likelihoods.sum() if is_marginal else root_likelihoods.max()
      return np.log(root_likelihoods) - getattr(tree, lh_sf_feature) * np.log(10)