docstr: Replace the value with its percentile in the range [0,1].
  
  This preprocesses several features in a single step by putting the
  quantile boundaries in the third dimension and broadcasting.
  
  The input is a JxF matrix where J is the batch size and F is the # of features.
code: def _preprocess_QUANTILE(
      self,
      begin_index: int,
      input: torch.Tensor,
      norm_params: List[NormalizationParameters],
  ) -> torch.Tensor:
      """
      Replace the value with its percentile in the range [0,1].
  
      This preprocesses several features in a single step by putting the
      quantile boundaries in the third dimension and broadcasting.
  
      The input is a JxF matrix where J is the batch size and F is the # of features.
      """
  
      # The number of quantiles is a 1xF matrix
      num_quantiles = self._fetch_parameter(
          begin_index, "num_quantiles"
      )
  
      quantile_boundaries = self._fetch_parameter(
          begin_index, "quantile_boundaries"
      )
      max_quantile_boundaries = self._fetch_parameter(
          begin_index, "max_quantile_boundaries"
      )
      min_quantile_boundaries = self._fetch_parameter(
          begin_index, "min_quantile_boundaries"
      )
  
      # Add a third dimension and repeat to create a JxFxB matrix, where the
      # inputs are repeated B times in the third dimension.  We need to
      # do this because we can't broadcast both operands in different
      # dimensions in the same operation.
  
      # repeat doesn't work yet, so * by a mask
      mask = self._fetch_parameter(
          begin_index, "quantile_boundary_mask"
      )
      expanded_inputs = input.unsqueeze(2) * mask
  
      input_greater_than_or_equal_to = (
          expanded_inputs >= quantile_boundaries
      ).float()
  
      input_less_than = (expanded_inputs < quantile_boundaries).float()
      set_to_max = (input >= max_quantile_boundaries).float()
      set_to_min = (input <= min_quantile_boundaries).float()
      min_or_max = (set_to_min + set_to_max).float()
      interpolate = (min_or_max < self.one_hundredth_tensor).float()
      interpolate_left, _ = torch.max(
          (input_greater_than_or_equal_to * quantile_boundaries)
          + (input_less_than * self.min_tensor),
          dim=2,
      )
      interpolate_right, _ = torch.min(
          (input_less_than * quantile_boundaries)
          + (input_greater_than_or_equal_to * self.max_tensor),
          dim=2,
      )
  
      # This assumes that we need to interpolate and computes the value.
      # If we don't need to interpolate, this will be some bogus value, but it
      # will be multiplied by 0 so no big deal.
      left_start = (
          torch.sum(input_greater_than_or_equal_to, dim=2)
          - self.one_tensor
      )
      interpolated_values = (
          (
              left_start
              + (
                  (input - interpolate_left)
                  / (
                      (interpolate_right + self.epsilon_tensor)
                      - interpolate_left
                  )  # Add a small amount to interpolate_right to avoid div-0
              )
          )
          / num_quantiles
      ).float()
      return set_to_max + (interpolate * interpolated_values).float()
  
imports: import logging
  from typing import cast, Dict, List, Optional, Tuple
  import torch
  from reagent.core.parameters import NormalizationParameters
  from reagent.preprocessing.identify_types import DO_NOT_PREPROCESS, ENUM, FEATURE_TYPES
  from reagent.preprocessing.normalization import EPS, MAX_FEATURE_VALUE, MIN_FEATURE_VALUE
  from torch.nn import Module, Parameter
label_desc: Use image node_id 23 for calling the Preprocessor obj's underlying member method code with example usage: obj._preprocess_QUANTILE(begin_index, input, norm_params) and returns: unknown