class_docstr: This class provides functionality to initialize the inducing point locations
  of an inducing point-based model, e.g. a `SingleTaskVariationalGP`.
docstr: Initialize the `num_inducing` inducing point locations according to a
  specific initialization strategy. todo say something about quality
  
  Args:
      inputs: A (\*batch_shape, n, d)-dim input data tensor.
      covar_module: GPyTorch Module returning a LinearOperator kernel matrix.
      num_inducing: The maximun number (m) of inducing points (m <= n).
      input_batch_shape: The non-task-related batch shape.
  
  Returns:
      A (\*batch_shape, m, d)-dim tensor of inducing point locations.
docstr_tok: ["Initialize","the","`","num_inducing","`","inducing","point","locations","according","to","a","specific","initialization","strategy",".","todo","say","something","about","quality","Args",":","inputs",":","A","(","\\","*","batch_shape",",","n",",","d",")","-dim","input","data","tensor",".","covar_module",":","GPyTorch","Module","returning","a","LinearOperator","kernel","matrix",".","num_inducing",":","The","maximun","number","(","m",")","of","inducing","points","(","m","<","=","n",")",".","input_batch_shape",":","The","non-task-related","batch","shape",".","Returns",":","A","(","\\","*","batch_shape",",","m",",","d",")","-dim","tensor","of","inducing","point","locations","."]
code: def allocate_inducing_points(
      self,
      inputs: Tensor,
      covar_module: Module,
      num_inducing: int,
      input_batch_shape: torch.Size,
  ) -> Tensor:
      r"""
      Initialize the `num_inducing` inducing point locations according to a
      specific initialization strategy. todo say something about quality
  
      Args:
          inputs: A (\*batch_shape, n, d)-dim input data tensor.
          covar_module: GPyTorch Module returning a LinearOperator kernel matrix.
          num_inducing: The maximun number (m) of inducing points (m <= n).
          input_batch_shape: The non-task-related batch shape.
  
      Returns:
          A (\*batch_shape, m, d)-dim tensor of inducing point locations.
      """
      quality_function = self._get_quality_function()
      covar_module = covar_module.to(inputs.device)
  
      # We use 'no_grad' here because `inducing_points` are not
      # auto-differentiable with respect to the kernel hyper-parameters,
      # because `_pivoted_cholesky_init` does in-place operations.
      with torch.no_grad():
          # Evaluate lazily because this may only be needed to figure out what
          # case we are in
          possibly_lazy_kernel = covar_module(inputs)
  
      base_case = possibly_lazy_kernel.ndimension() == 2
      multi_task_case = (
          possibly_lazy_kernel.ndimension() == 3
          and len(input_batch_shape) == 0
      )
  
      if base_case or multi_task_case:
          train_train_kernel = possibly_lazy_kernel.evaluate_kernel()
  
      if base_case:
          quality_scores = quality_function(inputs)
          inducing_points = _pivoted_cholesky_init(
              train_inputs=inputs,
              kernel_matrix=train_train_kernel,
              max_length=num_inducing,
              quality_scores=quality_scores,
          )
          return inducing_points
  
      if multi_task_case:
          input_element = (
              inputs[0] if inputs.ndimension() == 3 else inputs
          )
          kernel_element = train_train_kernel[0]
          quality_scores = quality_function(input_element)
          inducing_points = _pivoted_cholesky_init(
              train_inputs=input_element,
              kernel_matrix=kernel_element,
              max_length=num_inducing,
              quality_scores=quality_scores,
          )
          return inducing_points
  
      # batched input cases
      batched_inputs = (
          inputs.expand(*input_batch_shape, -1, -1)
          if inputs.ndimension() == 2
          else inputs
      )
      reshaped_inputs = batched_inputs.flatten(end_dim=-3)
      inducing_points = []
      for input_element in reshaped_inputs:
          # the extra kernel evals are a little wasteful but make it
          # easier to infer the task batch size
          # We use 'no_grad' here because `inducing_points` are not
          # auto-differentiable with respect to the kernel hyper-parameters,
          # because `_pivoted_cholesky_init` does in-place operations.
          with torch.no_grad():
              kernel_element = covar_module(
                  input_element
              ).evaluate_kernel()
          # handle extra task batch dimension
          kernel_element = (
              kernel_element[0]
              if kernel_element.ndimension() == 3
              else kernel_element
          )
          quality_scores = quality_function(input_element)
          inducing_points.append(
              _pivoted_cholesky_init(
                  train_inputs=input_element,
                  kernel_matrix=kernel_element,
                  max_length=num_inducing,
                  quality_scores=quality_scores,
              )
          )
      inducing_points = torch.stack(inducing_points).view(
          *input_batch_shape, num_inducing, -1
      )
  
      return inducing_points
  
imports: from __future__ import annotations
  from abc import ABC, abstractmethod
  from typing import Union
  import torch
  from botorch.exceptions.errors import UnsupportedError
  from botorch.models.model import Model
  from botorch.utils.probability.utils import ndtr, phi
  from gpytorch.module import Module
  from linear_operator.operators import LinearOperator
  from torch import Tensor
label_desc: Use image node_id 2 for calling the InducingPointAllocator obj's underlying member method code with example usage: obj.allocate_inducing_points(inputs, covar_module, num_inducing, input_batch_shape) and returns: inducing_points, inducing_points, inducing_points