//  [python]
def ld_derivative(data, xs, alpha=10, maxit=1000, linalg_solver_maxit=100, tol=1e-4, atol=1e-4, rtol=1e-6,
                  verbose=False, show_progress=True, solver='lgmres', precondition=True):
    assert isinstance(data, np.ndarray)

    label = None
    if show_progress:
        from ipywidgets import Label, Box
        from IPython.display import display
        label = Label("Progress: 0/{} it, atol={}/{}, rtol={}/{}".format(0, '?', atol, '?', rtol))
        box = Box([label])
        display(box)

    if show_progress:
        label.value = 'copying data and setting f(0) = 0'

    # require f(0) = 0
    data = np.copy(data) - data[0]

    data = data.squeeze()
    assert len(data.shape) == 1

    epsilon = 1e-4

    n = len(data)

    # differentiation operator
    if show_progress:
        label.value = 'obtaining differentiation operator'
    D = get_fd_matrix_midpoints(xs, k=1, window_width=5)
    # D = #get_differentiation_operator_midpoint(xs)
    D_T = D.transpose(copy=True).tocsc()

    def A(v):
        # integrate v from 0 to x
        # return cumsimps(xs, v)
        return integrate.cumulative_trapezoid(y=v, x=xs, initial=0)

    def A_adjoint(w):
        # integrate w from x to L <=> int_0^Lw - int_0^xw
        full_integral = integrate.trapz(w, x=xs)
        # assert np.isclose(full_integral, A(w)[-1]), "got {}, but expected {}".format(full_integral, A(w)[-1])
        #

        return np.ones_like(w) * full_integral - integrate.cumulative_trapezoid(w, xs, initial=0)  # cumsimps(xs, w)#

    Aadj_A = lambda v: A_adjoint(A(v))

    if show_progress:
        label.value = 'calculating native gradient of data'
    u = np.gradient(data, xs)

    if show_progress:
        label.value = 'precalculating A*(data)'
    KT_data = A_adjoint(data)

    xs_diff = np.diff(xs)
    # DX = sparse.spdiags(np.diff(xs), 0, n - 1, n - 1)

    if show_progress:
        label.value = 'preparing E_n'
    E_n = sparse.dia_matrix((n - 1, n - 1), dtype=xs.dtype)
    prev_grad_norm = None

    spsolve_term = None
    if precondition or solver == 'spsolve' or solver == 'np' or True:
        if show_progress:
            if precondition:
                label.value = 'computing preconditioner: getting integration operator'
            else:
                label.value = 'assembling matrix'
        K = get_integration_operator(xs)
        if show_progress and precondition:
            label.value = 'computing preconditioner: getting adjoint integration operator'
        KT = get_integration_adjoint_operator(xs)
        if show_progress and precondition:
            label.value = 'calculating K^T * K'
        spsolve_term = KT * K

    if show_progress:
        label.value = 'begin solver loop for alpha={}'.format(alpha)
    # Main loop.
    relative_change = None
    first_strike = False
    for ii in range(1, maxit + 1):
        # Diagonal matrix of weights, for linearizing E-L equation.
        E_n.setdiag(xs_diff * (1. / np.sqrt(np.diff(u) ** 2.0 + epsilon)))
        # Q = E_n #DX *
        L = D_T * E_n * D
        # Gradient of functional.
        g = Aadj_A(u) - KT_data
        g = g + alpha * L * u

        # solve linear equation.
        info_i = 0
        if solver == 'lgmres' or solver == 'lgmres_scipy':
            linop = splin.LinearOperator((n, n), lambda v: (alpha * L * v + Aadj_A(v)))
            if precondition:
                if show_progress:
                    label.value = 'Progress: {}/{} it, atol={}/{}, rtol={}/{}, compute spilu' \
                        .format(ii, maxit, prev_grad_norm, atol, relative_change, rtol)
                lu = splin.spilu(alpha * L + spsolve_term, drop_tol=5e-2)
                precond = splin.LinearOperator((n, n), lambda v: lu.solve(v))
                if show_progress:
                    label.value = 'Progress: {}/{} it, atol={}/{}, rtol={}/{}, lgmres' \
                        .format(ii, maxit, prev_grad_norm, atol, relative_change, rtol)
                if solver == 'lgmres_scipy':
                    s, info_i = splin.lgmres(A=linop, b=-g, x0=u, tol=tol, maxiter=linalg_solver_maxit, M=precond)
                else:
                    s = lgmres(A=linop, b=-g, x0=u, tol=tol, maxiter=linalg_solver_maxit, M=precond)
            else:
                if solver == 'lgmres_scipy':
                    s, info_i = splin.lgmres(A=linop, b=-g, x0=u, tol=tol, maxiter=linalg_solver_maxit, outer_k=7)
                else:
                    s = lgmres(A=linop, b=-g, x0=u, tol=tol, maxiter=linalg_solver_maxit)
        elif solver == 'bicgstab':
            linop = splin.LinearOperator((n, n), lambda v: (alpha * L * v + Aadj_A(v)))
            if precondition:
                if show_progress:
                    label.value = 'Progress: {}/{} it, atol={}/{}, rtol={}/{}, compute spilu' \
                        .format(ii, maxit, prev_grad_norm, atol, relative_change, rtol)
                lu = splin.spilu(alpha * L + spsolve_term, drop_tol=5e-2)
                precond = splin.LinearOperator((n, n), lambda v: lu.solve(v))
                if show_progress:
                    label.value = 'Progress: {}/{} it, atol={}/{}, rtol={}/{}, bicgstab' \
                        .format(ii, maxit, prev_grad_norm, atol, relative_change, rtol)
                [s, info_i] = splin.bicgstab(A=linop, b=-g, x0=u, tol=tol, maxiter=linalg_solver_maxit, M=precond)
            else:
                [s, info_i] = splin.bicgstab(A=linop, b=-g, x0=u, tol=tol, maxiter=linalg_solver_maxit)
        elif solver == 'spsolve':
            # todo this behaves oddly
            s = splin.spsolve((alpha * L + spsolve_term), -g, use_umfpack=False, permc_spec='COLAMD')
        elif solver == 'np':
            # todo this behaves oddly
            s = np.linalg.solve((alpha * L + spsolve_term).todense().astype(np.float64), (-g).astype(np.float64))

        relative_change = np.linalg.norm(s[0]) / np.linalg.norm(u)
        if verbose:
            print('iteration {0:4d}: relative change = {1:.3e}, gradient norm = {2:.3e}'
                  .format(ii, relative_change, np.linalg.norm(g)))
            if info_i > 0:
                print("WARNING - convergence to tolerance not achieved!")
            elif info_i < 0:
                print("WARNING - illegal input or breakdown")

        # if prev_grad_norm is not None and np.linalg.norm(g) > prev_grad_norm:
        #    linalg_solver_maxit = int(2 * linalg_solver_maxit)
        # else:
        #    linalg_solver_maxit = int(.95 * linalg_solver_maxit)

        if prev_grad_norm is not None and np.linalg.norm(g) > prev_grad_norm and np.linalg.norm(g) > 1:
            print("WARNING - increasing large gradient norm: {} -> {}".format(prev_grad_norm, np.linalg.norm(g)))
            if not first_strike:
                break
            first_strike = True
        else:
            first_strike = False

        prev_grad_norm = np.linalg.norm(g)

        # Update current solution
        u = u + s

        if show_progress:
            label.value = "Progress: {}/{} it, atol={}/{}, rtol={}/{}" \
                .format(ii, maxit, np.linalg.norm(g), atol, relative_change, rtol)

        if atol is not None and np.linalg.norm(g) < atol:
            if show_progress:
                label.value = "ld derivative reached atol = {} < {}, finish".format(atol, np.linalg.norm(g))
            break
        if rtol is not None and relative_change < rtol:
            if show_progress:
                label.value = "ld derivative reached rtol = {} < {}, finish".format(rtol, relative_change)
            break

    if show_progress:
        box.close()

    return u