code: def extract_search_space_digest(
      search_space: SearchSpace, param_names: List[str]
  ) -> SearchSpaceDigest:
      """Extract basic parameter properties from a search space.
  
      This is typically called with the transformed search space and makes certain
      assumptions regarding the parameters being transformed.
  
      For ChoiceParameters:
      * The choices are assumed to be numerical. ChoiceEncode and OrderedChoiceEncode
      transforms handle this.
      * If is_task, its index is added to task_features.
      * If ordered, its index is added to ordinal_features.
      * Otherwise, its index is added to categorical_features.
      * In all cases, the choices are added to discrete_choices.
      * The minimum and maximum value are added to the bounds.
      * The target_value is added to target_values.
  
      For RangeParameters:
      * They're assumed not to be in the log_scale. The Log transform handles this.
      * If integer, its index is added to ordinal_features and the choices are added
      to discrete_choices.
      * The minimum and maximum value are added to the bounds.
  
      If a parameter is_fidelity:
      * Its target_value is assumed to be numerical.
      * The target_value is added to target_values.
      * Its index is added to fidelity_features.
      """
      bounds: List[Tuple[Union[int, float], Union[int, float]]] = []
      ordinal_features: List[int] = []
      categorical_features: List[int] = []
      discrete_choices: Dict[int, List[Union[int, float]]] = {}
      task_features: List[int] = []
      fidelity_features: List[int] = []
      target_values: Dict[int, Union[int, float]] = {}
  
      for i, p_name in enumerate(param_names):
          p = search_space.parameters[p_name]
          if isinstance(p, ChoiceParameter):
              if p.is_task:
                  task_features.append(i)
                  target_values[i] = checked_cast_to_tuple(
                      (int, float), p.target_value
                  )
              elif p.is_ordered:
                  ordinal_features.append(i)
              else:
                  categorical_features.append(i)
              # at this point we can assume that values are numeric due to transforms
              discrete_choices[i] = p.values  # pyre-ignore [6]
              bounds.append(
                  (min(p.values), max(p.values))
              )  # pyre-ignore [6]
          elif isinstance(p, RangeParameter):
              if p.log_scale:
                  raise ValueError(f"{p} is log scale")
              if p.parameter_type == ParameterType.INT:
                  ordinal_features.append(i)
                  d_choices = list(
                      range(int(p.lower), int(p.upper) + 1)
                  )
                  # pyre-ignore [6]
                  discrete_choices[i] = d_choices
              bounds.append((p.lower, p.upper))
          else:
              raise ValueError(f"Unknown parameter type {type(p)}")
          if p.is_fidelity:
              fidelity_features.append(i)
              target_values[i] = checked_cast_to_tuple(
                  (int, float), p.target_value
              )
  
      return SearchSpaceDigest(
          feature_names=param_names,
          bounds=bounds,
          ordinal_features=ordinal_features,
          categorical_features=categorical_features,
          discrete_choices=discrete_choices,
          task_features=task_features,
          fidelity_features=fidelity_features,
          target_values=target_values,
          robust_digest=extract_robust_digest(
              search_space=search_space, param_names=param_names
          ),
      )
  
imports: from __future__ import annotations
  import warnings
  from copy import deepcopy
  from functools import partial
  from logging import Logger
  from typing import Any, Callable, Dict, Iterable, List, Mapping, MutableMapping, Optional, Tuple, Type, TYPE_CHECKING, Union
  import numpy
  import torch
  from ax.core.data import Data
  from ax.core.experiment import Experiment
  from ax.core.objective import MultiObjective, Objective, ScalarizedObjective
  from ax.core.observation import Observation, ObservationData, ObservationFeatures
  from ax.core.optimization_config import MultiObjectiveOptimizationConfig, OptimizationConfig, TRefPoint
  from ax.core.outcome_constraint import ComparisonOp, OutcomeConstraint, ScalarizedOutcomeConstraint
  from ax.core.parameter import ChoiceParameter, Parameter, ParameterType, RangeParameter
  from ax.core.parameter_constraint import ParameterConstraint
  from ax.core.risk_measures import RiskMeasure
  from ax.core.search_space import RobustSearchSpace, RobustSearchSpaceDigest, SearchSpace, SearchSpaceDigest
  from ax.core.types import TBounds, TCandidateMetadata
  from ax.core.utils import get_pending_observation_features, get_pending_observation_features_based_on_trial_status
  from ax.exceptions.core import DataRequiredError, UnsupportedError, UserInputError
  from ax.modelbridge.transforms.base import Transform
  from ax.modelbridge.transforms.utils import derelativize_optimization_config_with_raw_status_quo
  from ax.models.torch.botorch_moo_defaults import pareto_frontier_evaluator
  from ax.models.torch.frontier_utils import get_weighted_mc_objective_and_objective_thresholds
  from ax.utils.common.logger import get_logger
  from ax.utils.common.typeutils import checked_cast, checked_cast_optional, checked_cast_to_tuple, not_none
  from botorch.acquisition.multi_objective.multi_output_risk_measures import IndependentCVaR, IndependentVaR, MARS, MultiOutputExpectation, MVaR
  from botorch.acquisition.risk_measures import CVaR, Expectation, RiskMeasureMCObjective, VaR, WorstCase
  from botorch.utils.datasets import ContextualDataset, SupervisedDataset
  from botorch.utils.multi_objective.box_decompositions.dominated import DominatedPartitioning
  from torch import Tensor
label_desc: Use image node_id 4 for calling a global function with example usage: extract_search_space_digest(search_space, param_names) and returns: SearchSpaceDigest