code: def sample_truncated_normal_perturbations(
      X: Tensor,
      n_discrete_points: int,
      sigma: float,
      bounds: Tensor,
      qmc: bool = True,
  ) -> Tensor:
      r"""Sample points around `X`.
  
      Sample perturbed points around `X` such that the added perturbations
      are sampled from N(0, sigma^2 I) and truncated to be within [0,1]^d.
  
      Args:
          X: A `n x d`-dim tensor starting points.
          n_discrete_points: The number of points to sample.
          sigma: The standard deviation of the additive gaussian noise for
              perturbing the points.
          bounds: A `2 x d`-dim tensor containing the bounds.
          qmc: A boolean indicating whether to use qmc.
  
      Returns:
          A `n_discrete_points x d`-dim tensor containing the sampled points.
      """
      X = normalize(X, bounds=bounds)
      d = X.shape[1]
      # sample points from N(X_center, sigma^2 I), truncated to be within
      # [0, 1]^d.
      if X.shape[0] > 1:
          rand_indices = torch.randint(
              X.shape[0], (n_discrete_points,), device=X.device
          )
          X = X[rand_indices]
      if qmc:
          std_bounds = torch.zeros(2, d, dtype=X.dtype, device=X.device)
          std_bounds[1] = 1
          u = draw_sobol_samples(
              bounds=std_bounds, n=n_discrete_points, q=1
          ).squeeze(1)
      else:
          u = torch.rand(
              (n_discrete_points, d), dtype=X.dtype, device=X.device
          )
      # compute bounds to sample from
      a = -X
      b = 1 - X
      # compute z-score of bounds
      alpha = a / sigma
      beta = b / sigma
      normal = Normal(0, 1)
      cdf_alpha = normal.cdf(alpha)
      # use inverse transform
      perturbation = (
          normal.icdf(cdf_alpha + u * (normal.cdf(beta) - cdf_alpha))
          * sigma
      )
      # add perturbation and clip points that are still outside
      perturbed_X = (X + perturbation).clamp(0.0, 1.0)
      return unnormalize(perturbed_X, bounds=bounds)
  
imports: from __future__ import annotations
  import warnings
  from math import ceil
  from typing import Callable, Dict, List, Optional, Tuple, Union
  import torch
  from botorch import settings
  from botorch.acquisition import analytic, monte_carlo, multi_objective
  from botorch.acquisition.acquisition import AcquisitionFunction
  from botorch.acquisition.fixed_feature import FixedFeatureAcquisitionFunction
  from botorch.acquisition.knowledge_gradient import _get_value_function, qKnowledgeGradient
  from botorch.acquisition.multi_objective.hypervolume_knowledge_gradient import _get_hv_value_function, qHypervolumeKnowledgeGradient, qMultiFidelityHypervolumeKnowledgeGradient
  from botorch.exceptions.errors import BotorchTensorDimensionError, UnsupportedError
  from botorch.exceptions.warnings import BadInitialCandidatesWarning, BotorchWarning, SamplingWarning
  from botorch.models.model import Model
  from botorch.optim.utils import fix_features, get_X_baseline
  from botorch.utils.multi_objective.pareto import is_non_dominated
  from botorch.utils.sampling import batched_multinomial, draw_sobol_samples, get_polytope_samples, manual_seed
  from botorch.utils.transforms import normalize, standardize, unnormalize
  from torch import Tensor
  from torch.distributions import Normal
  from torch.quasirandom import SobolEngine
label_desc: Use image node_id 12 for calling a global function with example usage: sample_truncated_normal_perturbations(X, n_discrete_points, sigma, bounds, qmc) and returns: unnormalize