diff --git a/scripts/Manifest.toml b/scripts/Manifest.toml
index c1fdc71587300edab541dfdc115a35e364cca3df..0ad0147ff4f18b2616de879323d722c2ffe46422 100644
--- a/scripts/Manifest.toml
+++ b/scripts/Manifest.toml
@@ -1,6 +1,6 @@
 # This file is machine-generated - editing it directly is not advised
 
-julia_version = "1.7.0"
+julia_version = "1.7.1"
 manifest_format = "2.0"
 
 [[deps.AbstractFFTs]]
@@ -54,9 +54,9 @@ version = "0.9.11"
 
 [[deps.Cairo_jll]]
 deps = ["Artifacts", "Bzip2_jll", "Fontconfig_jll", "FreeType2_jll", "Glib_jll", "JLLWrappers", "LZO_jll", "Libdl", "Pixman_jll", "Pkg", "Xorg_libXext_jll", "Xorg_libXrender_jll", "Zlib_jll", "libpng_jll"]
-git-tree-sha1 = "f2202b55d816427cd385a9a4f3ffb226bee80f99"
+git-tree-sha1 = "4b859a208b2397a7a623a03449e4636bdb17bcf2"
 uuid = "83423d85-b0ee-5818-9007-b63ccbeb887a"
-version = "1.16.1+0"
+version = "1.16.1+1"
 
 [[deps.Calculus]]
 deps = ["LinearAlgebra"]
@@ -361,9 +361,9 @@ version = "0.21.0+0"
 
 [[deps.Glib_jll]]
 deps = ["Artifacts", "Gettext_jll", "JLLWrappers", "Libdl", "Libffi_jll", "Libiconv_jll", "Libmount_jll", "PCRE_jll", "Pkg", "Zlib_jll"]
-git-tree-sha1 = "74ef6288d071f58033d54fd6708d4bc23a8b8972"
+git-tree-sha1 = "a32d672ac2c967f3deb8a81d828afc739c838a06"
 uuid = "7746bdde-850d-59dc-9ae8-88ece973131d"
-version = "2.68.3+1"
+version = "2.68.3+2"
 
 [[deps.Graphics]]
 deps = ["Colors", "LinearAlgebra", "NaNMath"]
@@ -725,9 +725,9 @@ version = "1.10.8"
 
 [[deps.Ogg_jll]]
 deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"]
-git-tree-sha1 = "7937eda4681660b4d6aeeecc2f7e1c81c8ee4e2f"
+git-tree-sha1 = "887579a3eb005446d514ab7aeac5d1d027658b8f"
 uuid = "e7412a2a-1a6e-54c0-be00-318e2571c051"
-version = "1.3.5+0"
+version = "1.3.5+1"
 
 [[deps.OpenBLAS_jll]]
 deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"]
@@ -1289,9 +1289,9 @@ version = "1.6.38+0"
 
 [[deps.libvorbis_jll]]
 deps = ["Artifacts", "JLLWrappers", "Libdl", "Ogg_jll", "Pkg"]
-git-tree-sha1 = "c45f4e40e7aafe9d086379e5578947ec8b95a8fb"
+git-tree-sha1 = "b910cb81ef3fe6e78bf6acee440bda86fd6ae00c"
 uuid = "f27f6e37-5d2b-51aa-960f-b287f2bc3b7a"
-version = "1.3.7+0"
+version = "1.3.7+1"
 
 [[deps.nghttp2_jll]]
 deps = ["Artifacts", "Libdl"]
diff --git a/scripts/run_experiments.jl b/scripts/run_experiments.jl
index d288ef042da62b86f8a8cc54f9a0976f6fc3ce1b..477c081fb7eea1047bf4128740a7f992bc69384d 100644
--- a/scripts/run_experiments.jl
+++ b/scripts/run_experiments.jl
@@ -1,4 +1,6 @@
-using LinearAlgebra: norm, dot, I
+using LinearAlgebra: I, det, dot, norm, normalize
+using SparseArrays: sparse
+using Statistics: mean
 
 using Colors: Gray
 # avoid world-age-issues by preloading ColorTypes
@@ -6,13 +8,16 @@ import ColorTypes
 import CSV
 import DataFrames: DataFrame
 import FileIO
+using ForwardDiff: jacobian
 using ImageQualityIndexes: assess_psnr, assess_ssim
 using OpticalFlowUtils
 using WriteVTK: paraview_collection
 using Plots
+using StaticArrays: MVector, SArray, SMatrix, SVector, SA
 
 using SemiSmoothNewton
-using SemiSmoothNewton: HMesh, ncells, refine, area, mesh_size, ndofs
+using SemiSmoothNewton: HMesh, ncells, refine, area, mesh_size, ndofs, diam,
+    elmap
 using SemiSmoothNewton: project!, project_l2_lagrange!, project_qi_lagrange!,
     project_l2_pixel!
 using SemiSmoothNewton: vtk_mesh, vtk_append!, vtk_save
@@ -125,6 +130,7 @@ function OptFlowState(mesh;
 
     # tdata will be something like nabla(fw)
     T(tdata, u) = tdata * u
+    T(::typeof(adjoint), tdata, v) = tdata' * v
     S(u, nablau) = nablau
 
     est = FeFunction(Vest, name = "est")
@@ -446,9 +452,29 @@ end
 
 huber(x, gamma) = abs(x) < gamma ? x^2 / (2 * gamma) : abs(x) - gamma / 2
 
+# TODO: finish!
+function refine_and_estimate_pd(st::L1L2TVState)
+    # globally refine
+    marked_cells = Set(axes(st.mesh.cells, 2))
+    mesh_new, fs_new = refine(st.mesh, marked_cells;
+        st.est, st.g, st.u, st.p1, st.p2, st.du, st.dp1, st.dp2)
+    st_new = L1L2TVState(mesh_new, st.d, st.m, T, nothing, S,
+        st.alpha1, st.alpha2, st.beta, st.lambda,
+        st.gamma1, st.gamma2,
+        fs_new.est, fs_new.g, fs_new.u, fs_new.p1, fs_new.p2,
+        fs_new.du, fs_new.dp1, fs_new.dp2)
+
+    # compute primal dual error indicators
+    estimate_pd!(st_new)
+
+    # transfer data to old state
+    #
+end
+
 # this computes the primal-dual error indicator which is not really useful
-# if not computed on a finer mesh than `u` was solved on
-function estimate!(st::L1L2TVState)
+# if not computed on a finer mesh than `u` was solved on and has problems with
+# alpha1 > 0
+function estimate_pd!(st::L1L2TVState)
     function estf(x_; g, u, p1, p2, nablau, w, nablaw, tdata)
 	alpha1part =
             st.alpha1 * huber(norm(st.T(tdata, u) - g), st.gamma1) -
@@ -482,6 +508,95 @@ function estimate!(st::L1L2TVState)
     st.est.data .= sqrt.(st.est.data)
 end
 
+# this computes the residual error indicator which has missing theoretical
+# support for alpha1 > 0
+# TODO: finish!
+function estimate_res!(st::L1L2TVState)
+    # FIXME: dirty hack
+    isSnabla = st.S(0, 1) == 1
+    norm2(v) = dot(v, v)
+
+    cellf(hcell; g, u, nablau, p1, p2, tdata) =
+         st.alpha2 *
+            st.T(adjoint, tdata, st.alpha2 * st.T(tdata, u) - g) +
+            st.T(adjoint, tdata, p1)
+    facetf(hfacet, n; g, u, nablau, p1, p2, tdata) =
+        # norm2 is calculated later after both facet contributions are
+        # consolidated
+        n' * (st.beta * st.S(zero(u), nablau) + p2)
+
+    # manual method
+
+    mesh = st.mesh
+    fs = (;st.g, st.u, nablau = nabla(st.u), st.p1, st.p2, st.tdata)
+    space = st.est.space
+
+    facetV = Dict{Tuple{Int, Int}, MVector{st.m, Float64}}()
+
+    for cell in cells(mesh)
+        A = SArray{Tuple{ndims_space(mesh), nvertices_cell(mesh)}}(
+            view(mesh.vertices, :, view(mesh.cells, :, cell)))
+
+	for f in fs
+	    bind!(f, cell)
+	end
+
+        # cell contribution
+
+        hcell = diam(mesh, cell)
+	delmap = jacobian(elmap(mesh, cell), SA[0., 0.])
+	intel = abs(det(delmap))
+
+	centroid = SArray{Tuple{ndims_domain(mesh)}}(mean(A, dims = 2))
+	lcentroid = SA[1/3, 1/3]
+
+	opvalues = map(f -> evaluate(f, lcentroid), fs)
+
+        cellres = (isSnabla ? hcell^2 : hcell) *
+            norm2(cellf(hcell; opvalues...)) .* intel
+
+	gdofs = space.dofmap[:, 1, cell]
+	st.est.data[gdofs] .= cellres
+
+        # facet contributions
+
+        cross(v) = SA[-v[2], v[1]]
+        for (i, j) in ((i, mod1(i + 1, 3)) for i in 1:3)
+            fi = mesh.cells[i, cell]
+            fj = mesh.cells[j, cell]
+
+            v = A[:, j] - A[:, i]
+            hfacet = norm(v)
+            normal = -normalize(cross(v))
+            intel = hfacet
+
+            # we don't need to recompute values since only piecewise
+            # constant data is used in `facetf`
+            # we use sqrt here since this value will be squared later
+            facetres = (isSnabla ? sqrt(hfacet) : inv(sqrt(hfacet))) *
+                facetf(hfacet, normal; opvalues...) .* sqrt(intel)
+
+            # average facet contributions
+            v = SVector(facetres)
+            facetV[(fi, fj)] = get(facetV, (fi, fj), zero(v)) + SVector(v)
+            facetV[(fj, fi)] = get(facetV, (fj, fi), zero(v)) + SVector(v)
+        end
+    end
+
+    # add facet contributions to cells
+    for cell in cells(mesh)
+        for (i, j) in ((i, mod1(i + 1, 3)) for i in 1:3)
+            fi = mesh.cells[i, cell]
+            fj = mesh.cells[j, cell]
+
+            gdofs = space.dofmap[:, 1, cell]
+            st.est.data[gdofs] .+= norm2(facetV[(fi, fj)])
+        end
+    end
+
+    st.est.data .= sqrt.(st.est.data)
+end
+
 estimate_error(st::L1L2TVState) =
     sqrt(sum(x -> x^2, st.est.data) / area(st.mesh))
 
@@ -596,7 +711,7 @@ function denoise(img; params...)
 	while true
 	    k += 1
 	    step!(ctx)
-	    estimate!(ctx)
+	    estimate_pd!(ctx)
 	    pvd[k] = save_denoise(ctx, k)
 	    println()
 
@@ -687,7 +802,7 @@ function denoise_pd(st, img; df=nothing, algorithm, params_...)
 
 	domain_factor = 1 / sqrt(area(mesh))
 	norm_step_ = norm_step(st) * domain_factor
-        #estimate!(st)
+        #estimate_pd!(st)
 
         log!(df; k,
             norm_step = norm_step_,
@@ -820,7 +935,7 @@ function denoise_approximation(ctx)
 
             k += 1
             norm_residual_ = norm_residual(st) * domain_factor
-            estimate!(st)
+            estimate_pd!(st)
 
             pvd[k] = save_vtk(st, k)
             log!(ctx.params.df; k,
@@ -834,7 +949,7 @@ function denoise_approximation(ctx)
             if haskey(ctx.params, :tol) && norm_step_ < ctx.params.tol
                 k += 1
                 norm_residual_ = norm_residual(st) * domain_factor
-                estimate!(st)
+                estimate_pd!(st)
 
                 pvd[k] = save_vtk(st, k)
                 log!(ctx.params.df; k,
@@ -858,7 +973,7 @@ function denoise_approximation(ctx)
                 w = fs.w
 
                 norm_residual_ = norm_residual(st) * domain_factor
-                estimate!(st)
+                estimate_pd!(st)
 
                 pvd[k] = save_vtk(st, k)
                 log!(ctx.params.df; k,
@@ -925,7 +1040,7 @@ function inpaint(img, imgmask; params...)
     pvd[0] = save_inpaint(0)
     for i in 1:3
 	step!(ctx)
-	estimate!(ctx)
+	estimate_pd!(ctx)
 	pvd[i] = save_inpaint(i)
 	println()
     end
@@ -1010,7 +1125,7 @@ function optflow(ctx)
                 k += 1
                 println()
                 step!(st)
-                #estimate!(st)
+                #estimate_pd!(st)
 
                 norm_step_ = norm_step(st) / sqrt(mesh_area)
                 println("norm_step = $norm_step_")
@@ -1025,7 +1140,7 @@ function optflow(ctx)
 
             display(plot(colorflow(to_img(sample(st.u)); ctx.params.maxflow)))
 
-            estimate!(st)
+            estimate_res!(st)
             pvd[i] = save_step(i)
 
             println("refine ...")