class_docstr: Implements REMBO (Bayesian optimization in a linear subspace).
  
  The (D x d) projection matrix A must be provided, and must be that used for
  the initialization. In the original REMBO paper A ~ N(0, 1). Box bounds
  in the low-d space must also be provided, which in the REMBO paper should
  be [(-sqrt(d), sqrt(d)]^d.
  
  Function evaluations happen in the high-D space, and so the arms on the
  experiment will also be tracked in the high-D space. This class maintains
  a list of points in the low-d spac that have been launched, so we can match
  arms in high-D space back to their low-d point on update.
  
  Args:
      A: (D x d) projection matrix.
      initial_X_d: Points in low-d space for initial data.
      bounds_d: Box bounds in the low-d space.
      kwargs: kwargs for BotorchModel init
docstr: Map points in the high-D space to the low-d space by looking them
  up in self.X_d.
  
  We assume that X_D = self.project_up(self.X_d), except possibly with
  rows shuffled. If a value in X_d cannot be found for each row in X_D,
  an error will be raised.
  
  This is quite fast relative to model fitting, so we do it in O(n^2)
  time and don't worry about it.
  
  Args:
      X_D: Tensor in high-D space.
  
  Returns:
      X_d: Tensor in low-d space.
docstr_tok: ["Map","points","in","the","high-D","space","to","the","low-d","space","by","looking","them","up","in","self.X_d",".","We","assume","that","X_D","=","self.project_up","(","self.X_d",")",",","except","possibly","with","rows","shuffled",".","If","a","value","in","X_d","can","not","be","found","for","each","row","in","X_D",",","an","error","will","be","raised",".","This","is","quite","fast","relative","to","model","fitting",",","so","we","do","it","in","O","(","n^2",")","time","and","do","n't","worry","about","it",".","Args",":","X_D",":","Tensor","in","high-D","space",".","Returns",":","X_d",":","Tensor","in","low-d","space","."]
code: def project_down(self, X_D: Tensor) -> Tensor:
      """Map points in the high-D space to the low-d space by looking them
      up in self.X_d.
  
      We assume that X_D = self.project_up(self.X_d), except possibly with
      rows shuffled. If a value in X_d cannot be found for each row in X_D,
      an error will be raised.
  
      This is quite fast relative to model fitting, so we do it in O(n^2)
      time and don't worry about it.
  
      Args:
          X_D: Tensor in high-D space.
  
      Returns:
          X_d: Tensor in low-d space.
      """
      X_d = []
      unmatched = list(range(len(self.X_d)))
      for x_D in X_D:
          idx_match = None
          for d_idx in unmatched:
              if torch.allclose(x_D, self.project_up(self.X_d[d_idx])):
                  idx_match = d_idx
                  break
          if idx_match is not None:
              X_d.append(self.X_d[idx_match])
              unmatched.remove(idx_match)
          else:
              raise ValueError("Failed to project X down.")
      return torch.stack(X_d)
  
imports: import dataclasses
  from typing import Any, List, Optional, Tuple
  from warnings import warn
  import torch
  from ax.core.search_space import SearchSpaceDigest
  from ax.core.types import TCandidateMetadata
  from ax.models.torch.botorch import BotorchModel
  from ax.models.torch_base import TorchGenResults, TorchModel, TorchOptConfig
  from ax.utils.common.docutils import copy_doc
  from botorch.utils.datasets import SupervisedDataset
  from torch import Tensor
label_desc: Use image node_id 5 for calling the REMBO obj's underlying member method code with example usage: obj.project_down(X_D) and returns: torch