class_docstr: Multi-objective sampler using the NSGA-II algorithm.
  
  NSGA-II stands for "Nondominated Sorting Genetic Algorithm II",
  which is a well known, fast and elitist multi-objective genetic algorithm.
  
  For further information about NSGA-II, please refer to the following paper:
  
  - `A fast and elitist multiobjective genetic algorithm: NSGA-II
    <https://ieeexplore.ieee.org/document/996017>`_
  
  Args:
      population_size:
          Number of individuals (trials) in a generation.
  
      mutation_prob:
          Probability of mutating each parameter when creating a new individual.
          If :obj:`None` is specified, the value ``1.0 / len(parent_trial.params)`` is used
          where ``parent_trial`` is the parent trial of the target individual.
  
      crossover_prob:
          Probability that a crossover (parameters swapping between parents) will occur
          when creating a new individual.
  
      swapping_prob:
          Probability of swapping each parameter of the parents during crossover.
  
      seed:
          Seed for random number generator.
class_docstr_tok: ["Multi-objective","sampler","using","the","NSGA-II","algorithm",".","NSGA-II","stands","for","``","Nondominated","Sorting","Genetic","Algorithm","II","''",",","which","is","a","well","known",",","fast","and","elitist","multi-objective","genetic","algorithm",".","For","further","information","about","NSGA-II",",","please","refer","to","the","following","paper",":","-","`","A","fast","and","elitist","multiobjective","genetic","algorithm",":","NSGA-II","<","https",":","\/\/ieeexplore.ieee.org\/document\/996017",">","`","_","Args",":","population_size",":","Number","of","individuals","(","trials",")","in","a","generation",".","mutation_prob",":","Probability","of","mutating","each","parameter","when","creating","a","new","individual",".","If",":","obj",":","`","None","`","is","specified",",","the","value","``","1.0","\/","len","(","parent_trial.params",")","``","is","used","where","``","parent_trial","``","is","the","parent","trial","of","the","target","individual",".","crossover_prob",":","Probability","that","a","crossover","(","parameters","swapping","between","parents",")","will","occur","when","creating","a","new","individual",".","swapping_prob",":","Probability","of","swapping","each","parameter","of","the","parents","during","crossover",".","seed",":","Seed","for","random","number","generator","."]
code: def _collect_parent_population(
      self, study: "multi_objective.study.MultiObjectiveStudy"
  ) -> Tuple[
      int, List["multi_objective.trial.FrozenMultiObjectiveTrial"]
  ]:
      trials = [
          multi_objective.trial.FrozenMultiObjectiveTrial(
              study.n_objectives, t
          )
          for t in study._storage.get_all_trials(
              study._study_id, deepcopy=False
          )
      ]
  
      generation_to_runnings = defaultdict(list)
      generation_to_population = defaultdict(list)
      for trial in trials:
          if _GENERATION_KEY not in trial.system_attrs:
              continue
  
          generation = trial.system_attrs[_GENERATION_KEY]
          if trial.state != optuna.trial.TrialState.COMPLETE:
              if trial.state == optuna.trial.TrialState.RUNNING:
                  generation_to_runnings[generation].append(trial)
              continue
  
          generation_to_population[generation].append(trial)
  
      hasher = hashlib.sha256()
      parent_population: List[
          multi_objective.trial.FrozenMultiObjectiveTrial
      ] = []
      parent_generation = -1
      while True:
          generation = parent_generation + 1
          population = generation_to_population[generation]
  
          # Under multi-worker settings, the population size might become larger than
          # `self._population_size`.
          if len(population) < self._population_size:
              break
  
          # [NOTE]
          # It's generally safe to assume that once the above condition is satisfied,
          # there are no additional individuals added to the generation (i.e., the members of
          # the generation have been fixed).
          # If the number of parallel workers is huge, this assumption can be broken, but
          # this is a very rare case and doesn't significantly impact optimization performance.
          # So we can ignore the case.
  
          # The cache key is calculated based on the key of the previous generation and
          # the remaining running trials in the current population.
          # If there are no running trials, the new cache key becomes exactly the same as
          # the previous one, and the cached content will be overwritten. This allows us to
          # skip redundant cache key calculations when this method is called for the subsequent
          # trials.
          for trial in generation_to_runnings[generation]:
              hasher.update(bytes(str(trial.number), "utf-8"))
  
          cache_key = "{}:{}".format(
              _POPULATION_CACHE_KEY_PREFIX, hasher.hexdigest()
          )
          study_system_attrs = study._storage.get_study_system_attrs(
              study._study_id
          )
          (
              cached_generation,
              cached_population_numbers,
          ) = study_system_attrs.get(cache_key, (-1, []))
          if cached_generation >= generation:
              generation = cached_generation
              population = [
                  trials[n] for n in cached_population_numbers
              ]
          else:
              population.extend(parent_population)
              population = self._select_elite_population(
                  study, population
              )
  
              # To reduce the number of system attribute entries,
              # we cache the population information only if there are no running trials
              # (i.e., the information of the population has been fixed).
              # Usually, if there are no too delayed running trials, the single entry
              # will be used.
              if len(generation_to_runnings[generation]) == 0:
                  population_numbers = [t.number for t in population]
                  study._storage.set_study_system_attr(
                      study._study_id,
                      cache_key,
                      (generation, population_numbers),
                  )
  
          parent_generation = generation
          parent_population = population
  
      return parent_generation, parent_population
  
imports: from collections import defaultdict
  import hashlib
  import itertools
  from typing import Any
  from typing import cast
  from typing import DefaultDict
  from typing import Dict
  from typing import List
  from typing import Optional
  from typing import Sequence
  from typing import Tuple
  import optuna
  from optuna import multi_objective
  from optuna._deprecated import deprecated_class
  from optuna.distributions import BaseDistribution
  from optuna.multi_objective.samplers import BaseMultiObjectiveSampler
  from optuna.samplers._lazy_random_state import LazyRandomState
label_desc: Use image node_id 6 for calling the NSGAIIMultiObjectiveSampler obj's underlying member method code with example usage: obj._collect_parent_population(study) and returns: parent_generation,  parent_population