class_docstr: Abstract implementation of an intensifier supporting multi-fidelity, multi-objective, and multi-threading.
  The abstract intensifier keeps track of the incumbent, which is updated everytime the runhistory changes.
  
  Parameters
  ----------
  n_seeds : int | None, defaults to None
      How many seeds to use for each instance. It is used in the abstract intensifier to determine validation trials.
  max_config_calls : int, defaults to None
      Maximum number of configuration evaluations. Basically, how many instance-seed keys should be max evaluated
      for a configuration. It is used in the abstract intensifier to determine validation trials.
  max_incumbents : int, defaults to 10
      How many incumbents to keep track of in the case of multi-objective.
  seed : int, defaults to None
      Internal seed used for random events like shuffle seeds.
class_docstr_tok: ["Abstract","implementation","of","an","intensifier","supporting","multi-fidelity",",","multi-objective",",","and","multi-threading",".","The","abstract","intensifier","keeps","track","of","the","incumbent",",","which","is","updated","everytime","the","runhistory","changes",".","Parameters","--","--","--","--","--","n_seeds",":","int","|","None",",","defaults","to","None","How","many","seeds","to","use","for","each","instance",".","It","is","used","in","the","abstract","intensifier","to","determine","validation","trials",".","max_config_calls",":","int",",","defaults","to","None","Maximum","number","of","configuration","evaluations",".","Basically",",","how","many","instance-seed","keys","should","be","max","evaluated","for","a","configuration",".","It","is","used","in","the","abstract","intensifier","to","determine","validation","trials",".","max_incumbents",":","int",",","defaults","to","10","How","many","incumbents","to","keep","track","of","in","the","case","of","multi-objective",".","seed",":","int",",","defaults","to","None","Internal","seed","used","for","random","events","like","shuffle","seeds","."]
docstr: Updates the incumbents. This method is called everytime a trial is added to the runhistory. Since only
  the affected config and the current incumbents are used, this method is very efficient. Furthermore, a
  configuration is only considered incumbent if it has a better performance on all incumbent instances.
  
  Crucially, if there is no incumbent (at the start) then, the first configuration assumes
  incumbent status. For the next configuration, we need to check if the configuration
  is better on all instances that have been evaluated for the incumbent. If this is the
  case, then we can replace the incumbent. Otherwise, a) we need to requeue the config to
  obtain the missing instance-seed-budget combination or b) mark this configuration as
  inferior ("rejected") to not consider it again. The comparison behaviour is controlled by
  self.get_instance_seed_budget_keys() and self.get_incumbent_instance_seed_budget_keys().
  
  Notably, this method is written to support both multi-fidelity and multi-objective
  optimization. While the get_instance_seed_budget_keys() method and
  self.get_incumbent_instance_seed_budget_keys() are used for the multi-fidelity behaviour,
  calculate_pareto_front() is used as a hard coded way to support multi-objective
  optimization, including the single objective as special case. calculate_pareto_front()
  is called on the set of all (in case of MO) incumbents amended with the challenger
  configuration, provided it has a sufficient overlap in seed-instance-budget combinations.
  
  Lastly, if we have a self._max_incumbents and the pareto front provides more than this
  specified amount, we cut the incumbents using crowding distance.
code: def update_incumbents(self, config: Configuration) -> None:
      """Updates the incumbents. This method is called everytime a trial is added to the runhistory. Since only
      the affected config and the current incumbents are used, this method is very efficient. Furthermore, a
      configuration is only considered incumbent if it has a better performance on all incumbent instances.
  
      Crucially, if there is no incumbent (at the start) then, the first configuration assumes
      incumbent status. For the next configuration, we need to check if the configuration
      is better on all instances that have been evaluated for the incumbent. If this is the
      case, then we can replace the incumbent. Otherwise, a) we need to requeue the config to
      obtain the missing instance-seed-budget combination or b) mark this configuration as
      inferior ("rejected") to not consider it again. The comparison behaviour is controlled by
      self.get_instance_seed_budget_keys() and self.get_incumbent_instance_seed_budget_keys().
  
      Notably, this method is written to support both multi-fidelity and multi-objective
      optimization. While the get_instance_seed_budget_keys() method and
      self.get_incumbent_instance_seed_budget_keys() are used for the multi-fidelity behaviour,
      calculate_pareto_front() is used as a hard coded way to support multi-objective
      optimization, including the single objective as special case. calculate_pareto_front()
      is called on the set of all (in case of MO) incumbents amended with the challenger
      configuration, provided it has a sufficient overlap in seed-instance-budget combinations.
  
      Lastly, if we have a self._max_incumbents and the pareto front provides more than this
      specified amount, we cut the incumbents using crowding distance.
      """
      rh = self.runhistory
  
      # What happens if a config was rejected, but it appears again? Give it another try even if it
      # has already been evaluated? Yes!
  
      # Associated trials and id
      config_isb_keys = self.get_instance_seed_budget_keys(config)
      config_id = rh.get_config_id(config)
      config_hash = get_config_hash(config)
  
      # We skip updating incumbents if no instances are available
      # Note: This is especially the case if trials of a config are still running
      # because if trials are running, the runhistory does not update the trials in the fast data structure
      if len(config_isb_keys) == 0:
          logger.debug(
              f"No relevant instances evaluated for config {config_hash}. Updating incumbents is skipped."
          )
          return
  
      # Now we get the incumbents and see which trials have been used
      incumbents = self.get_incumbents()
      incumbent_ids = [rh.get_config_id(c) for c in incumbents]
      # Find the lowest intersection of instance-seed-budget keys for all incumbents.
      incumbent_isb_keys = (
          self.get_incumbent_instance_seed_budget_keys()
      )
  
      # Save for later
      previous_incumbents = incumbents.copy()
      previous_incumbent_ids = incumbent_ids.copy()
  
      # Little sanity check here for consistency
      if len(incumbents) > 0:
          assert incumbent_isb_keys is not None
          assert len(incumbent_isb_keys) > 0
  
      # If there are no incumbents at all, we just use the new config as new incumbent
      # Problem: We can add running incumbents
      if (
          len(incumbents) == 0
      ):  # incumbent_isb_keys is None and len(incumbents) == 0:
          logger.info(
              f"Added config {config_hash} as new incumbent because there are no incumbents yet."
          )
          self._update_trajectory([config])
  
          # Nothing else to do
          return
  
      # Comparison keys
      # This one is a bit tricky: We would have problems if we compare with budgets because we might have different
      # scenarios (depending on the incumbent selection specified in Successive Halving).
      # 1) Any budget/highest observed budget: We want to get rid of the budgets because if we know it is calculated
      # on the same instance-seed already then we are ready to go. Imagine we would check for the same budgets,
      # then the configs can not be compared although the user does not care on which budgets configurations have
      # been evaluated.
      # 2) Highest budget: We only want to compare the configs if they are evaluated on the highest budget.
      # Here we do actually care about the budgets. Please see the ``get_instance_seed_budget_keys`` method from
      # Successive Halving to get more information.
      # Noitce: compare=True only takes effect when subclass implemented it. -- e.g. in SH it
      # will remove the budgets from the keys.
      config_isb_comparison_keys = self.get_instance_seed_budget_keys(
          config, compare=True
      )
      # Find the lowest intersection of instance-seed-budget keys for all incumbents.
      config_incumbent_isb_comparison_keys = (
          self.get_incumbent_instance_seed_budget_keys(compare=True)
      )
  
      # Now we have to check if the new config has been evaluated on the same keys as the incumbents
      if not all(
          [
              key in config_isb_comparison_keys
              for key in config_incumbent_isb_comparison_keys
          ]
      ):
          # We can not tell if the new config is better/worse than the incumbents because it has not been
          # evaluated on the necessary trials
          logger.debug(
              f"Could not compare config {config_hash} with incumbents because it's evaluated on "
              f"different trials."
          )
  
          # The config has to go to a queue now as it is a challenger and a potential incumbent
          return
      else:
          # If all instances are available and the config is incumbent and even evaluated on more trials
          # then there's nothing we can do
          if config in incumbents and len(config_isb_keys) > len(
              incumbent_isb_keys
          ):
              logger.debug(
                  "Config is already an incumbent but can not be compared to other incumbents because "
                  "the others are missing trials."
              )
              return
  
      # Add config to incumbents so that we compare only the new config and existing incumbents
      if config not in incumbents:
          incumbents.append(config)
          incumbent_ids.append(config_id)
  
      # Now we get all instance-seed-budget keys for each incumbent (they might be different when using budgets)
      all_incumbent_isb_keys = []
      for incumbent in incumbents:
          all_incumbent_isb_keys.append(
              self.get_instance_seed_budget_keys(incumbent)
          )
  
      # We compare the incumbents now and only return the ones on the pareto front
      new_incumbents = calculate_pareto_front(
          rh, incumbents, all_incumbent_isb_keys
      )
      new_incumbent_ids = [rh.get_config_id(c) for c in new_incumbents]
  
      if len(previous_incumbents) == len(new_incumbents):
          if previous_incumbents == new_incumbents:
              # No changes in the incumbents
              self._remove_rejected_config(config_id)
              return
          else:
              # In this case, we have to determine which config replaced which incumbent and reject it
              removed_incumbent_id = list(
                  set(previous_incumbent_ids) - set(new_incumbent_ids)
              )[0]
              removed_incumbent_hash = get_config_hash(
                  rh.get_config(removed_incumbent_id)
              )
              self._add_rejected_config(removed_incumbent_id)
  
              if removed_incumbent_id == config_id:
                  logger.debug(
                      f"Rejected config {config_hash} because it is not better than the incumbents on "
                      f"{len(config_isb_keys)} instances."
                  )
              else:
                  self._remove_rejected_config(config_id)
                  logger.info(
                      f"Added config {config_hash} and rejected config {removed_incumbent_hash} as incumbent because "
                      f"it is not better than the incumbents on {len(config_isb_keys)} instances:"
                  )
                  print_config_changes(
                      rh.get_config(removed_incumbent_id),
                      config,
                      logger=logger,
                  )
      elif len(previous_incumbents) < len(new_incumbents):
          # Config becomes a new incumbent; nothing is rejected in this case
          self._remove_rejected_config(config_id)
          logger.info(
              f"Config {config_hash} is a new incumbent. "
              f"Total number of incumbents: {len(new_incumbents)}."
          )
      else:
          # There might be situations that the incumbents might be removed because of updated cost information of
          # config
          for incumbent in previous_incumbents:
              if incumbent not in new_incumbents:
                  self._add_rejected_config(incumbent)
                  logger.debug(
                      f"Removed incumbent {get_config_hash(incumbent)} because of the updated costs from config "
                      f"{config_hash}."
                  )
  
      # Cut incumbents: We only want to keep a specific number of incumbents
      # We use the crowding distance for that
      if len(new_incumbents) > self._max_incumbents:
          new_incumbents = sort_by_crowding_distance(
              rh, new_incumbents, all_incumbent_isb_keys
          )
          new_incumbents = new_incumbents[: self._max_incumbents]
  
          # or random?
          # idx = self._rng.randint(0, len(new_incumbents))
          # del new_incumbents[idx]
          # del new_incumbent_ids[idx]
  
          logger.info(
              f"Removed one incumbent using crowding distance because more than {self._max_incumbents} are "
              "available."
          )
  
      self._update_trajectory(new_incumbents)
  
imports: from __future__ import annotations
  from abc import abstractmethod
  from typing import Any, Callable, Iterator
  import dataclasses
  import json
  from collections import defaultdict
  from pathlib import Path
  import numpy
  from ConfigSpace import Configuration
  import smac
  from smac.callback.callback import Callback
  from smac.constants import MAXINT
  from smac.main.config_selector import ConfigSelector
  from smac.runhistory import TrialInfo
  from smac.runhistory.dataclasses import InstanceSeedBudgetKey, InstanceSeedKey, TrajectoryItem, TrialValue
  from smac.runhistory.runhistory import RunHistory
  from smac.scenario import Scenario
  from smac.utils.configspace import get_config_hash, print_config_changes
  from smac.utils.logging import get_logger
  from smac.utils.pareto_front import calculate_pareto_front, sort_by_crowding_distance
label_desc: Use image node_id 26 for calling the AbstractIntensifier obj's underlying member method code with example usage: obj.update_incumbents(config) without return types