{
 "cells": [
  {
   "cell_type": "markdown",
   "source": [
    "# Practical error bounds for the forces\n",
    "\n",
    "DFTK includes an implementation of the strategy from [^CDKL2022] to compute\n",
    "practical error bounds for forces and other quantities of interest.\n",
    "\n",
    "This is an example showing how to compute error estimates for the forces\n",
    "on a ${\\rm TiO}_2$ molecule, from which we can either compute asymptotically\n",
    "valid error bounds or increase the precision on the computation of the forces.\n",
    "\n",
    "[^CDKL2022]:\n",
    "    E. Cancès, G. Dusson, G. Kemlin, and A. Levitt\n",
    "    *Practical error bounds for properties in plane-wave electronic structure calculations*\n",
    "    [SIAM Journal on Scientific Computing 44 (5), B1312-B1340](https://doi.org/10.1137/21M1456224)"
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "using DFTK\n",
    "using PseudoPotentialData\n",
    "using Printf\n",
    "using LinearAlgebra\n",
    "using ForwardDiff"
   ],
   "metadata": {},
   "execution_count": 1
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Setup\n",
    "We setup manually the ${\\rm TiO}_2$ configuration from\n",
    "[Materials Project](https://materialsproject.org/materials/mp-2657/)."
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "pseudopotentials = PseudoFamily(\"cp2k.nc.sr.lda.v0_1.largecore.gth\")\n",
    "Ti = ElementPsp(:Ti, pseudopotentials)\n",
    "O  = ElementPsp(:O,  pseudopotentials)\n",
    "atoms     = [Ti, Ti, O, O, O, O]\n",
    "positions = [[0.5,     0.5,     0.5],  # Ti\n",
    "             [0.0,     0.0,     0.0],  # Ti\n",
    "             [0.19542, 0.80458, 0.5],  # O\n",
    "             [0.80458, 0.19542, 0.5],  # O\n",
    "             [0.30458, 0.30458, 0.0],  # O\n",
    "             [0.69542, 0.69542, 0.0]]  # O\n",
    "lattice   = [[8.79341  0.0      0.0];\n",
    "             [0.0      8.79341  0.0];\n",
    "             [0.0      0.0      5.61098]];"
   ],
   "metadata": {},
   "execution_count": 2
  },
  {
   "cell_type": "markdown",
   "source": [
    "We apply a small displacement to one of the $\\rm Ti$ atoms to get nonzero\n",
    "forces."
   ],
   "metadata": {}
  },
  {
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": "3-element Vector{Float64}:\n 0.478\n 0.528\n 0.535"
     },
     "metadata": {},
     "execution_count": 3
    }
   ],
   "cell_type": "code",
   "source": [
    "positions[1] .+= [-0.022, 0.028, 0.035]"
   ],
   "metadata": {},
   "execution_count": 3
  },
  {
   "cell_type": "markdown",
   "source": [
    "We build a model with one $k$-point only, not too high `Ecut_ref` and small\n",
    "tolerance to limit computational time. These parameters can be increased for\n",
    "more precise results."
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "model = model_DFT(lattice, atoms, positions; functionals=LDA())\n",
    "kgrid = [1, 1, 1]\n",
    "Ecut_ref = 35\n",
    "basis_ref = PlaneWaveBasis(model; Ecut=Ecut_ref, kgrid)\n",
    "tol = 1e-5;"
   ],
   "metadata": {},
   "execution_count": 4
  },
  {
   "cell_type": "markdown",
   "source": [
    "We also build a basis with smaller `Ecut`, to compute a variational approximation of the reference solution.\n",
    "\n",
    "> **Choice of convergence parameters**\n",
    ">\n",
    "> Be careful to choose `Ecut` not too close to `Ecut_ref`.\n",
    "> Note also that the current choice `Ecut_ref = 35` is such that the\n",
    "> reference solution is not converged and `Ecut = 15` is such that the\n",
    "> asymptotic regime (crucial to validate the approach) is barely established."
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "Ecut = 15\n",
    "basis = PlaneWaveBasis(model; Ecut, kgrid);"
   ],
   "metadata": {},
   "execution_count": 5
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Computations\n",
    "Compute the solution on the smaller basis:"
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "scfres = self_consistent_field(basis; tol, callback=identity);"
   ],
   "metadata": {},
   "execution_count": 6
  },
  {
   "cell_type": "markdown",
   "source": [
    "Compute first order corrections `refinement.δψ` and `refinement.δρ`.\n",
    "Note that `refinement.ψ` and `refinement.ρ` are the quantities computed with `Ecut`\n",
    "and then extended to the reference grid.\n",
    "This step is roughly as expensive as the `self_consistent_field` call above."
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "refinement = refine_scfres(scfres, basis_ref; tol, callback=identity);"
   ],
   "metadata": {},
   "execution_count": 7
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Error estimates\n",
    "- Computation of the force from the variational solution without any post-processing:"
   ],
   "metadata": {}
  },
  {
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": "6-element Vector{StaticArraysCore.SVector{3, Float64}}:\n [0.03418855950413535, -0.016276394305248854, 0.025767637977328173]\n [-0.5139216515045787, 0.6479201505659945, 0.2170681817193305]\n [-0.5238675354873454, 0.4801368623775526, -0.15971058202151486]\n [0.777613457067849, -0.8064496580824967, -0.12223539112573223]\n [0.6154633962454343, 0.3547562213960296, -0.0020629086600183477]\n [-0.3894877436373152, -0.6601071031231927, 0.04160786043271103]"
     },
     "metadata": {},
     "execution_count": 8
    }
   ],
   "cell_type": "code",
   "source": [
    "f = compute_forces(scfres)"
   ],
   "metadata": {},
   "execution_count": 8
  },
  {
   "cell_type": "markdown",
   "source": [
    "- Computation of the forces by a linearization argument when replacing the\n",
    "  error $P-P_*$ by the modified residual $R_{\\rm Schur}(P)$. The latter\n",
    "  quantity is computable in practice."
   ],
   "metadata": {}
  },
  {
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": "6-element Vector{StaticArraysCore.SVector{3, Float64}}:\n [-0.21301722681824328, 0.2730113145648059, 0.29520434209420665]\n [-0.5681968786100761, 0.72046263411358, 0.2549407435641571]\n [-0.3622378313098461, 0.32855929296399394, -0.1598791007347069]\n [0.9488413396929462, -0.9818851370497104, -0.13380603475680183]\n [0.6787075799839407, 0.41328591303068946, -0.14153407483203972]\n [-0.48592754346491185, -0.7512568996296056, -0.11259874995984637]"
     },
     "metadata": {},
     "execution_count": 9
    }
   ],
   "cell_type": "code",
   "source": [
    "force_refinement = refine_forces(refinement)\n",
    "forces_refined = f + force_refinement.dF"
   ],
   "metadata": {},
   "execution_count": 9
  },
  {
   "cell_type": "markdown",
   "source": [
    "A practical estimate of the error on the forces is then the following:"
   ],
   "metadata": {}
  },
  {
   "outputs": [
    {
     "output_type": "execute_result",
     "data": {
      "text/plain": "6-element Vector{StaticArraysCore.SVector{3, Float64}}:\n [-0.24720578632237863, 0.2892877088700547, 0.2694367041168785]\n [-0.05427522710549737, 0.07254248354758552, 0.03787256184482657]\n [0.16162970417749933, -0.15157756941355865, -0.0001685187131920396]\n [0.17122788262509714, -0.17543547896721368, -0.011570643631069605]\n [0.06324418373850638, 0.058529691634659875, -0.13947116617202138]\n [-0.09643979982759665, -0.09114979650641297, -0.1542066103925574]"
     },
     "metadata": {},
     "execution_count": 10
    }
   ],
   "cell_type": "code",
   "source": [
    "dF_estimate = forces_refined - f"
   ],
   "metadata": {},
   "execution_count": 10
  },
  {
   "cell_type": "markdown",
   "source": [
    "# Comparisons against non-practical estimates.\n",
    "For practical computations one can stop at `forces_refined` and `dF_estimate`.\n",
    "We continue here with a comparison of different ways to obtain the refined forces,\n",
    "noting that the computational cost is much higher."
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Computations\n",
    "We compute the reference solution $P_*$ from which we will compute the\n",
    "references forces."
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "scfres_ref = self_consistent_field(basis_ref; tol, callback=identity)\n",
    "ψ_ref = DFTK.select_occupied_orbitals(basis_ref, scfres_ref.ψ, scfres_ref.occupation).ψ;"
   ],
   "metadata": {},
   "execution_count": 11
  },
  {
   "cell_type": "markdown",
   "source": [
    "- Compute the error $P-P_*$ on the associated orbitals $ϕ-ψ$ after aligning\n",
    "  them: this is done by solving $\\min |ϕ - ψU|$ for $U$ unitary matrix of\n",
    "  size $N×N$ ($N$ being the number of electrons) whose solution is\n",
    "  $U = S(S^*S)^{-1/2}$ where $S$ is the overlap matrix $ψ^*ϕ$."
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "function compute_error(ϕ, ψ)\n",
    "    map(zip(ϕ, ψ)) do (ϕk, ψk)\n",
    "        S = ψk'ϕk\n",
    "        U = S*(S'S)^(-1/2)\n",
    "        ϕk - ψk*U\n",
    "    end\n",
    "end\n",
    "error = compute_error(refinement.ψ, ψ_ref);"
   ],
   "metadata": {},
   "execution_count": 12
  },
  {
   "cell_type": "markdown",
   "source": [
    "## Error estimates"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "We start with different estimations of the forces:\n",
    "- Force from the reference solution"
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "f_ref = compute_forces(scfres_ref)\n",
    "forces   = Dict(\"F(P_*)\" => f_ref)\n",
    "relerror = Dict(\"F(P_*)\" => 0.0)\n",
    "compute_relerror(f) = norm(f - f_ref) / norm(f_ref);"
   ],
   "metadata": {},
   "execution_count": 13
  },
  {
   "cell_type": "markdown",
   "source": [
    "- Force from the variational solution and relative error without\n",
    "  any post-processing:"
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "forces[\"F(P)\"]   = f\n",
    "\n",
    "relerror[\"F(P)\"] = compute_relerror(f);"
   ],
   "metadata": {},
   "execution_count": 14
  },
  {
   "cell_type": "markdown",
   "source": [
    "We then try to improve $F(P)$ using the first order linearization:\n",
    "\n",
    "$$\n",
    "F(P) = F(P_*) + {\\rm d}F(P)·(P-P_*).\n",
    "$$"
   ],
   "metadata": {}
  },
  {
   "cell_type": "markdown",
   "source": [
    "To this end, we use the `ForwardDiff.jl` package to compute ${\\rm d}F(P)$\n",
    "using automatic differentiation."
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "function df(basis, occupation, ψ, δψ, ρ)\n",
    "    δρ = DFTK.compute_δρ(basis, ψ, δψ, occupation)\n",
    "    ForwardDiff.derivative(ε -> compute_forces(basis, ψ.+ε.*δψ, occupation; ρ=ρ+ε.*δρ), 0)\n",
    "end;"
   ],
   "metadata": {},
   "execution_count": 15
  },
  {
   "cell_type": "markdown",
   "source": [
    "- Computation of the forces by a linearization argument if we have access to\n",
    "  the actual error $P-P_*$. Usually this is of course not the case, but this\n",
    "  is the \"best\" improvement we can hope for with a linearisation, so we are\n",
    "  aiming for this precision."
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "df_err = df(basis_ref, refinement.occupation, refinement.ψ,\n",
    "            DFTK.proj_tangent(error, refinement.ψ), refinement.ρ)\n",
    "forces[\"F(P) - df(P)⋅(P-P_*)\"]   = f - df_err\n",
    "relerror[\"F(P) - df(P)⋅(P-P_*)\"] = compute_relerror(f - df_err);"
   ],
   "metadata": {},
   "execution_count": 16
  },
  {
   "cell_type": "markdown",
   "source": [
    "- Computation of the forces by a linearization argument when replacing the\n",
    "  error $P-P_*$ by the modified residual $R_{\\rm Schur}(P)$. The latter\n",
    "  quantity is computable in practice."
   ],
   "metadata": {}
  },
  {
   "outputs": [],
   "cell_type": "code",
   "source": [
    "forces[\"F(P) - df(P)⋅Rschur(P)\"]   = forces_refined\n",
    "relerror[\"F(P) - df(P)⋅Rschur(P)\"] = compute_relerror(forces_refined);"
   ],
   "metadata": {},
   "execution_count": 17
  },
  {
   "cell_type": "markdown",
   "source": [
    "Summary of all forces on the first atom (Ti)"
   ],
   "metadata": {}
  },
  {
   "outputs": [
    {
     "name": "stdout",
     "output_type": "stream",
     "text": [
      "                        F(P_*) = [-0.29996, 0.37533, 0.44083]   (rel. error: 0.00000)\n",
      "                          F(P) = [0.03419, -0.01628, 0.02577]   (rel. error: 0.41550)\n",
      "        F(P) - df(P)⋅Rschur(P) = [-0.21302, 0.27301, 0.29520]   (rel. error: 0.14681)\n",
      "          F(P) - df(P)⋅(P-P_*) = [-0.32017, 0.38917, 0.43183]   (rel. error: 0.08583)\n"
     ]
    }
   ],
   "cell_type": "code",
   "source": [
    "for (key, value) in pairs(forces)\n",
    "    @printf(\"%30s = [%7.5f, %7.5f, %7.5f]   (rel. error: %7.5f)\\n\",\n",
    "            key, (value[1])..., relerror[key])\n",
    "end"
   ],
   "metadata": {},
   "execution_count": 18
  },
  {
   "cell_type": "markdown",
   "source": [
    "Notice how close the computable expression $F(P) - {\\rm d}F(P)⋅R_{\\rm Schur}(P)$\n",
    "is to the best linearization ansatz $F(P) - {\\rm d}F(P)⋅(P-P_*)$."
   ],
   "metadata": {}
  }
 ],
 "nbformat_minor": 3,
 "metadata": {
  "language_info": {
   "file_extension": ".jl",
   "mimetype": "application/julia",
   "name": "julia",
   "version": "1.11.4"
  },
  "kernelspec": {
   "name": "julia-1.11",
   "display_name": "Julia 1.11.4",
   "language": "julia"
  }
 },
 "nbformat": 4
}