class_docstr: An `Op` that provides conditional graph evaluation.
  
  According to a scalar condition, this `Op` evaluates and then
  returns all the tensors provided on the "then"-branch, otherwise it
  evaluates and returns the tensors provided on the "else"-branch. The `Op`
  supports multiple tensors on each branch, with the condition that the same
  number of tensors are on the "then"-branch as on the "else"-branch and
  there is a one to one correspondence between their dtypes and numbers of
  dimensions.
  
  The "then"-branch is defined as the first ``N`` tensors (after the
  condition), while the "else"-branch is defined as the last ``N`` tensors.
  
  Example usage:
  
  .. code-block::
  
      rval = ifelse(condition,
                    rval_if_true_1, ..., rval_if_true_N,
                    rval_if_false_1, ..., rval_if_false_N)
  
  .. note:
  
      `Linker`\s other than `CVM`, and some other `VM` subclasses, are
      incompatible with this `Op`, and will ignore its lazy characteristic,
      computing both the true and false branches before returning one.
code: def make_node(
      self, condition: "TensorLike", *true_false_branches: Any
  ):
      if len(true_false_branches) != 2 * self.n_outs:
          raise ValueError(
              f"Wrong number of arguments: expected "
              f"{int(2 * self.n_outs)}, got {len(true_false_branches)}"
          )
  
      condition = pt.basic.as_tensor_variable(condition)
  
      if condition.type.ndim > 0:
          raise TypeError(
              "The condition argument must be a truthy scalar value"
          )
  
      inputs_true_branch = true_false_branches[: self.n_outs]
      inputs_false_branch = true_false_branches[self.n_outs :]
  
      output_vars = []
      new_inputs_true_branch = []
      new_inputs_false_branch = []
      for input_t, input_f in zip(
          inputs_true_branch, inputs_false_branch
      ):
          if not isinstance(input_t, Variable):
              input_t = as_symbolic(input_t)
          if not isinstance(input_f, Variable):
              input_f = as_symbolic(input_f)
  
          if type(input_f.type) != type(input_t.type):  # noqa: E721
              raise TypeError(
                  f"Input types {type(input_t.type)} and {type(input_f.type)} do not match."
              )
  
          if isinstance(input_t.type, HasDataType) and isinstance(
              input_f.type, HasDataType
          ):
              # TODO: Be smarter about dtype casting.
              # up_dtype = ps.upcast(input_t.type.dtype, input_f.type.dtype)
  
              if input_t.type.dtype != input_f.type.dtype:
                  raise TypeError(
                      "IfElse requires compatible dtypes for both branches: got "
                      f"true_branch={input_t.type.dtype}, false_branch={input_f.type.dtype}"
                  )
  
          if isinstance(input_t.type, HasShape) and isinstance(
              input_f.type, HasShape
          ):
              if input_t.type.ndim != input_f.type.ndim:
                  raise TypeError(
                      "IfElse requires compatible ndim values for both branches: got "
                      f"true_branch={input_t.type.ndim}, false_branch={input_f.type.ndim}"
                  )
  
              # We can only use static shape information that corresponds
              # in both branches, because the outputs of this `Op` are
              # allowed to have distinct shapes from either branch
              new_shape = tuple(
                  s_t if s_t == s_f else None
                  for s_t, s_f in zip(
                      input_t.type.shape, input_f.type.shape
                  )
              )
              # TODO FIXME: The presence of this keyword is a strong
              # assumption.  Find something that's guaranteed by the/a
              # confirmed interface.
              output_var_t = input_t.type.clone(shape=new_shape)()
              output_var_f = input_f.type.clone(shape=new_shape)()
          else:
              output_var_t = input_t.type()
              output_var_f = input_f.type()
  
          input_t_ = output_var_f.type.filter_variable(input_t)
          input_f_ = output_var_t.type.filter_variable(input_f)
  
          new_inputs_true_branch.append(input_t_)
          new_inputs_false_branch.append(input_f_)
          output_vars.append(output_var_t)
  
      return Apply(
          self,
          [condition]
          + new_inputs_true_branch
          + new_inputs_false_branch,
          output_vars,
      )
  
imports: from collections.abc import Sequence
  from copy import deepcopy
  from typing import TYPE_CHECKING, Any, Optional, Union
  import numpy
  import pytensor.tensor
  from pytensor import as_symbolic
  from pytensor.compile import optdb
  from pytensor.configdefaults import config
  from pytensor.graph.basic import Apply, Variable, apply_depends_on
  from pytensor.graph.op import _NoPythonOp
  from pytensor.graph.replace import clone_replace
  from pytensor.graph.rewriting.basic import GraphRewriter, in2out, node_rewriter
  from pytensor.graph.type import HasDataType, HasShape
  from pytensor.tensor.shape import Reshape, Shape, SpecifyShape, Unbroadcast
label_desc: Use image node_id 6 for calling the IfElse obj's underlying member method code with example usage: obj.make_node(condition) and returns: Apply