# Buggy code
def solve_collocation_system(
    fun, t, y, h, Z0, scale, tol, LU_real, LU_complex, solve_lu
):
    """Solve the collocation system.

    Parameters
    ----------
    fun : callable
        Right-hand side of the system.
    t : float
        Current time.
    y : ndarray, shape (n,)
        Current state.
    h : float
        Step to try.
    Z0 : ndarray, shape (3, n)
        Initial guess for the solution. It determines new values of `y` at
        ``t + h * C`` as ``y + Z0``, where ``C`` is the Radau method constants.
    scale : float
        Problem tolerance scale, i.e. ``rtol * abs(y) + atol``.
    tol : float
        Tolerance to which solve the system. This value is compared with
        the normalized by `scale` error.
    LU_real, LU_complex
        LU decompositions of the system Jacobians.
    solve_lu : callable
        Callable which solves a linear system given a LU decomposition. The
        signature is ``solve_lu(LU, b)``.

    Returns
    -------
    converged : bool
        Whether iterations converged.
    n_iter : int
        Number of completed iterations.
    Z : ndarray, shape (3, n)
        Found solution.
    rate : float
        The rate of convergence.
    """
    n = y.shape[0]
    M_real = MU_REAL / h
    M_complex = MU_COMPLEX / h

    W = TI.dot(Z0)
    Z = Z0

    F = np.empty((3, n))
    ch = h * C

    dW_norm_old = None
    dW = np.empty_like(W)
    converged = False
    for k in range(NEWTON_MAXITER):
        for i in range(3):
            F[i] = fun(t + ch[i], y + Z[i])

        if not np.all(np.isfinite(F)):
            break

        f_real = F.T.dot(TI_REAL) - M_real * W[0]
        f_complex = F.T.dot(TI_COMPLEX) - M_complex * (W[1] + 1j * W[2])

        dW_real = solve_lu(LU_real, f_real)
        dW_complex = solve_lu(LU_complex, f_complex)

        dW[0] = dW_real
        dW[1] = dW_complex.real
        dW[2] = dW_complex.imag

        dW_norm = norm(dW / scale)
        if dW_norm_old is not None:
            rate = dW_norm / dW_norm_old
        else:
            rate = None

        if rate is not None and (
            rate >= 1 or rate ** (NEWTON_MAXITER - k) / (1 - rate) * dW_norm > tol
        ):
            break

        W += dW
        Z = T.dot(W)

        if dW_norm == 0 or rate is not None and rate / (1 - rate) * dW_norm < tol:
            converged = True
            break

        dW_norm_old = dW_norm

    return converged, k + 1, Z, rate


# Traceback (UnboundLocalError)
Traceback (most recent call last):
File "/Users/gabriel/radau_issue.py", line 8, in <module>
radau.step()  # UnboundLocalError
File "/Users/gabriel/Library/Python/3.7/lib/python/site-packages/scipy/integrate/_ivp/base.py", line 182, in step
success, message = self._step_impl()
File "/Users/gabriel/Library/Python/3.7/lib/python/site-packages/scipy/integrate/_ivp/radau.py", line 451, in _step_impl
LU_real, LU_complex, self.solve_lu)
File "/Users/gabriel/Library/Python/3.7/lib/python/site-packages/scipy/integrate/_ivp/radau.py", line 138, in solve_collocation_system
return converged, k + 1, Z, rate
UnboundLocalError: local variable 'rate' referenced before assignment

# Fixed code
def solve_collocation_system(
    fun, t, y, h, Z0, scale, tol, LU_real, LU_complex, solve_lu
):
    """Solve the collocation system.

    Parameters
    ----------
    fun : callable
        Right-hand side of the system.
    t : float
        Current time.
    y : ndarray, shape (n,)
        Current state.
    h : float
        Step to try.
    Z0 : ndarray, shape (3, n)
        Initial guess for the solution. It determines new values of `y` at
        ``t + h * C`` as ``y + Z0``, where ``C`` is the Radau method constants.
    scale : float
        Problem tolerance scale, i.e. ``rtol * abs(y) + atol``.
    tol : float
        Tolerance to which solve the system. This value is compared with
        the normalized by `scale` error.
    LU_real, LU_complex
        LU decompositions of the system Jacobians.
    solve_lu : callable
        Callable which solves a linear system given a LU decomposition. The
        signature is ``solve_lu(LU, b)``.

    Returns
    -------
    converged : bool
        Whether iterations converged.
    n_iter : int
        Number of completed iterations.
    Z : ndarray, shape (3, n)
        Found solution.
    rate : float
        The rate of convergence.
    """
    n = y.shape[0]
    M_real = MU_REAL / h
    M_complex = MU_COMPLEX / h

    W = TI.dot(Z0)
    Z = Z0

    F = np.empty((3, n))
    ch = h * C

    dW_norm_old = None
    dW = np.empty_like(W)
    converged = False
    rate = None
    for k in range(NEWTON_MAXITER):
        for i in range(3):
            F[i] = fun(t + ch[i], y + Z[i])

        if not np.all(np.isfinite(F)):
            break

        f_real = F.T.dot(TI_REAL) - M_real * W[0]
        f_complex = F.T.dot(TI_COMPLEX) - M_complex * (W[1] + 1j * W[2])

        dW_real = solve_lu(LU_real, f_real)
        dW_complex = solve_lu(LU_complex, f_complex)

        dW[0] = dW_real
        dW[1] = dW_complex.real
        dW[2] = dW_complex.imag

        dW_norm = norm(dW / scale)
        if dW_norm_old is not None:
            rate = dW_norm / dW_norm_old

        if rate is not None and (
            rate >= 1 or rate ** (NEWTON_MAXITER - k) / (1 - rate) * dW_norm > tol
        ):
            break

        W += dW
        Z = T.dot(W)

        if dW_norm == 0 or rate is not None and rate / (1 - rate) * dW_norm < tol:
            converged = True
            break

        dW_norm_old = dW_norm

    return converged, k + 1, Z, rate