class_docstr: Truncated distribution
  
  The pdf of a Truncated distribution is
  
  .. math::
  
      \begin{cases}
          0 & \text{for } x < lower, \\
          \frac{\text{PDF}(x, dist)}{\text{CDF}(upper, dist) - \text{CDF}(lower, dist)}
          & \text{for } lower <= x <= upper, \\
          0 & \text{for } x > upper,
      \end{cases}
  
  
  Parameters
  ----------
  dist: unnamed distribution
      Univariate distribution created via the `.dist()` API, which will be truncated.
      This distribution must be a pure RandomVariable and have a logcdf method
      implemented for MCMC sampling.
  
      .. warning:: dist will be cloned, rendering it independent of the one passed as input.
  
  lower: tensor_like of float or None
      Lower (left) truncation point. If `None` the distribution will not be left truncated.
  upper: tensor_like of float or None
      Upper (right) truncation point. If `None`, the distribution will not be right truncated.
  max_n_steps: int, defaults 10_000
      Maximum number of resamples that are attempted when performing rejection sampling.
      A `TruncationError` is raised if convergence is not reached after that many steps.
  
  Returns
  -------
  truncated_distribution: TensorVariable
      Graph representing a truncated `RandomVariable`. A specialized `Op` may be used
      if the `Op` of the dist has a dispatched `_truncated` function. Otherwise, a
      `SymbolicRandomVariable` graph representing the truncation process, via inverse
      CDF sampling (if the underlying dist has a logcdf method), or rejection sampling
      is returned.
  
  Examples
  --------
  .. code-block:: python
  
      with pm.Model():
          normal_dist = pm.Normal.dist(mu=0.0, sigma=1.0)
          truncated_normal = pm.Truncated("truncated_normal", normal_dist, lower=-1, upper=1)
class_docstr_tok: ["Truncated","distribution","The","pdf","of","a","Truncated","distribution","is","..","math",":",":","\\begin","{","cases","}","0","&","\\text","{","for","}","x","<","lower",",","\\\\","\\frac","{","\\text","{","PDF","}","(","x",",","dist",")","}","{","\\text","{","CDF","}","(","upper",",","dist",")","-","\\text","{","CDF","}","(","lower",",","dist",")","}","&","\\text","{","for","}","lower","<","=","x","<","=","upper",",","\\\\","0","&","\\text","{","for","}","x",">","upper",",","\\end","{","cases","}","Parameters","--","--","--","--","--","dist",":","unnamed","distribution","Univariate","distribution","created","via","the","`",".dist","(",")","`","API",",","which","will","be","truncated",".","This","distribution","must","be","a","pure","RandomVariable","and","have","a","logcdf","method","implemented","for","MCMC","sampling",".","..","warning",":",":","dist","will","be","cloned",",","rendering","it","independent","of","the","one","passed","as","input",".","lower",":","tensor_like","of","float","or","None","Lower","(","left",")","truncation","point",".","If","`","None","`","the","distribution","will","not","be","left","truncated",".","upper",":","tensor_like","of","float","or","None","Upper","(","right",")","truncation","point",".","If","`","None","`",",","the","distribution","will","not","be","right","truncated",".","max_n_steps",":","int",",","defaults","10_000","Maximum","number","of","resamples","that","are","attempted","when","performing","rejection","sampling",".","A","`","TruncationError","`","is","raised","if","convergence","is","not","reached","after","that","many","steps",".","Returns","--","--","--","-","truncated_distribution",":","TensorVariable","Graph","representing","a","truncated","`","RandomVariable","`",".","A","specialized","`","Op","`","may","be","used","if","the","`","Op","`","of","the","dist","has","a","dispatched","`","_truncated","`","function",".","Otherwise",",","a","`","SymbolicRandomVariable","`","graph","representing","the","truncation","process",",","via","inverse","CDF","sampling","(","if","the","underlying","dist","has","a","logcdf","method",")",",","or","rejection","sampling","is","returned",".","Examples","--","--","--","--","..","code-block",":",":","python","with","pm.Model","(",")",":","normal_dist","=","pm.Normal.dist","(","mu=0.0",",","sigma=1.0",")","truncated_normal","=","pm.Truncated","(","``","truncated_normal","''",",","normal_dist",",","lower=-1",",","upper=1",")"]
code: def rv_op(cls, dist, lower, upper, max_n_steps, size=None):
      # Try to use specialized Op
      try:
          return _truncated(
              dist.owner.op, lower, upper, size, *dist.owner.inputs
          )
      except NotImplementedError:
          pass
  
      lower = (
          pt.as_tensor_variable(lower)
          if lower is not None
          else pt.constant(-np.inf)
      )
      upper = (
          pt.as_tensor_variable(upper)
          if upper is not None
          else pt.constant(np.inf)
      )
  
      if size is None:
          size = pt.broadcast_shape(dist, lower, upper)
      dist = change_dist_size(dist, new_size=size)
  
      # Variables with `_` suffix identify dummy inputs for the OpFromGraph
      graph_inputs = [*dist.owner.inputs[1:], lower, upper]
      graph_inputs_ = [inp.type() for inp in graph_inputs]
      *rv_inputs_, lower_, upper_ = graph_inputs_
  
      # We will use a Shared RNG variable because Scan demands it, even though it
      # would not be necessary for the OpFromGraph inverse cdf.
      rng = pytensor.shared(np.random.default_rng())
      rv_ = dist.owner.op.make_node(rng, *rv_inputs_).default_output()
  
      # Try to use inverted cdf sampling
      try:
          # For left truncated discrete RVs, we need to include the whole lower bound.
          # This may result in draws below the truncation range, if any uniform == 0
          lower_value = (
              lower_ - 1
              if dist.owner.op.dtype.startswith("int")
              else lower_
          )
          cdf_lower_ = pt.exp(logcdf(rv_, lower_value))
          cdf_upper_ = pt.exp(logcdf(rv_, upper_))
          # It's okay to reuse the same rng here, because the rng in rv_ will not be
          # used by either the logcdf of icdf functions
          uniform_ = pt.random.uniform(
              cdf_lower_,
              cdf_upper_,
              rng=rng,
              size=rv_inputs_[0],
          )
          truncated_rv_ = icdf(rv_, uniform_)
          return TruncatedRV(
              base_rv_op=dist.owner.op,
              inputs=graph_inputs_,
              outputs=[uniform_.owner.outputs[0], truncated_rv_],
              ndim_supp=0,
              max_n_steps=max_n_steps,
          )(*graph_inputs)
      except NotImplementedError:
          pass
  
      # Fallback to rejection sampling
      def loop_fn(
          truncated_rv, reject_draws, lower, upper, rng, *rv_inputs
      ):
          next_rng, new_truncated_rv = dist.owner.op.make_node(
              rng, *rv_inputs
          ).outputs
          # Avoid scalar boolean indexing
          if truncated_rv.type.ndim == 0:
              truncated_rv = new_truncated_rv
          else:
              truncated_rv = pt.set_subtensor(
                  truncated_rv[reject_draws],
                  new_truncated_rv[reject_draws],
              )
          reject_draws = pt.or_(
              (truncated_rv < lower), (truncated_rv > upper)
          )
  
          return (
              (truncated_rv, reject_draws),
              [(rng, next_rng)],
              until(~pt.any(reject_draws)),
          )
  
      (truncated_rv_, reject_draws_), updates = scan(
          loop_fn,
          outputs_info=[
              pt.zeros_like(rv_),
              pt.ones_like(rv_, dtype=bool),
          ],
          non_sequences=[lower_, upper_, rng, *rv_inputs_],
          n_steps=max_n_steps,
          strict=True,
      )
  
      truncated_rv_ = truncated_rv_[-1]
      convergence_ = ~pt.any(reject_draws_[-1])
      truncated_rv_ = TruncationCheck(
          f"Truncation did not converge in {max_n_steps} steps"
      )(truncated_rv_, convergence_)
  
      return TruncatedRV(
          base_rv_op=dist.owner.op,
          inputs=graph_inputs_,
          outputs=[tuple(updates.values())[0], truncated_rv_],
          ndim_supp=0,
          max_n_steps=max_n_steps,
      )(*graph_inputs)
  
imports: from functools import singledispatch
  import numpy
  import pytensor
  import pytensor.tensor
  from pytensor import scan
  from pytensor.graph import Op
  from pytensor.graph.basic import Node
  from pytensor.raise_op import CheckAndRaise
  from pytensor.scan import until
  from pytensor.tensor import TensorConstant, TensorVariable
  from pytensor.tensor.random.basic import NormalRV
  from pytensor.tensor.random.op import RandomVariable
  from pymc.distributions.continuous import TruncatedNormal, bounded_cont_transform
  from pymc.distributions.dist_math import check_parameters
  from pymc.distributions.distribution import Distribution, SymbolicRandomVariable, _moment, moment
  from pymc.distributions.shape_utils import _change_dist_size, change_dist_size, to_tuple
  from pymc.distributions.transforms import _default_transform
  from pymc.exceptions import TruncationError
  from pymc.logprob.abstract import _logcdf, _logprob
  from pymc.logprob.basic import icdf, logcdf
  from pymc.math import logdiffexp
  from pymc.util import check_dist_not_registered
label_desc: Use image node_id 2 for calling the Truncated obj's underlying member method code with example usage: obj.rv_op(cls, dist, lower, upper, max_n_steps, size) and returns: TruncatedRV, _truncated, TruncatedRV, truncated_rv,  reject_draws,  list,  until