Skip to content
Snippets Groups Projects
Fenics_FloryHugg_Binary.ipynb 11.8 KiB
Newer Older
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "# Binary mixture based on Flory Huggins free energy\n",
    "For $\\phi_{tot}$ in a binary mixture, assuming a Flory Huggins free energy density, we obtain (Overleaf):\n",
    "Reference for FEM: https://www.comsol.com/multiphysics/finite-element-method\n",
    "\n",
    "$\\partial_\\mathrm{t}\\phi_\\mathrm{tot}=k_\\mathrm{B}T\\Gamma_\\mathrm{0} \\,\\nabla\\cdot\\left[\\left[1-2\\chi\\phi_\\mathrm{tot}(1-\\phi_\\mathrm{tot})\\right]\\nabla \\phi_\\mathrm{tot}-\\phi_\\mathrm{tot}(1-\\phi_\\mathrm{tot})\\kappa\\nabla\\nabla^2\\phi_\\mathrm{tot}\\right]$\n",
    "\n",
    "To solve this equation we follow the [Fenics Cahn-Hilliard example](https://bitbucket.org/fenics-project/dolfin/src/master/python/demo/documented/cahn-hilliard/demo_cahn-hilliard.py.rst#rst-header-id1) (the maths is explained [here](https://fenicsproject.org/docs/dolfinx/dev/python/demos/cahn-hilliard/demo_cahn-hilliard.py.html), for our free energy see overleaf).\n",
    "$\\partial_t \\phi_{\\mathrm{tot}} = \\nabla \\cdot \\Gamma_{\\mathrm{tot}}\\nabla\\mu_\\mathrm{tot}$\n",
    "$\\mu_\\mathrm{tot}=\\nu\\frac{\\partial f}{\\partial \\phi_\\mathrm{tot}}=\\,k_\\mathrm{B}T\\left[\\ln\\phi_\\mathrm{tot} - \\ln\\left(1-\\phi_\\mathrm{tot}\\right)+\\chi(1-2\\phi_\\mathrm{tot})-\\kappa\\nabla^2 \\phi_\\mathrm{tot} +1-n \\right]$\n",
    "The **boundary conditions** for the flux of the chemical potential and concentration read:\n",
    "\n",
    "$\\nabla\\mu_\\mathrm{tot}\\cdot n=0 $ on $\\partial \\Omega\\;,$ \n",
    "\n",
    "$\\nabla\\phi_\\mathrm{tot}\\cdot n = 0$ on $\\partial \\Omega$\n",
    "\n",
    "Cast into the **weak form** and integrating by parts we obtain:\n",
    "\n",
    "$\\int_{\\Omega} \\partial_t c \\;q \\; dx = \\int_{\\partial \\Omega} \\Gamma_\\mathrm{tot}\\nabla\\mu_\\mathrm{tot}q \\; ds - \\int_\\Omega \\Gamma_\\mathrm{tot} \\nabla \\mu_\\mathrm{tot} \\nabla q\\;dx = - \\int_\\Omega \\Gamma_\\mathrm{tot} \\nabla \\mu_\\mathrm{tot} \\nabla q\\;dx\\;, $\n",
    "\n",
    "$\\int_\\Omega \\mu_\\mathrm{tot} q \\; dx = k_\\mathrm{B} T   \\int_\\Omega\\left(\\ln\\left(\\frac{\\phi_\\mathrm{tot}}{1-\\phi_\\mathrm{tot}}\\right)+\\chi(1-2\\phi_\\mathrm{tot})\\right)q + \\kappa\\nabla\\phi_\\mathrm{tot}\\nabla q \\; dx$\n",
    "\n",
    "where we dropped the boundary terms (not explicitly written in second equation) due to the boundary conditions/weak form. We also disregard 1-n, because constants don't change the flux in the absence of chemical reactions (an offset doesn't change the solution in this case, try out by adding  $-6.0*v*dx$ to L1, for this check out fenics tutorial Poisson equation).\n",
    "\n",
    "According to the review (Weber et al. 2019, eqn. 2.29) the boundary length scale is \n",
    "\n",
    "$w = \\sqrt{2\\kappa/b} = \\left( \\frac{\\kappa}{k_\\mathrm{B}T\\chi}\\right) ^{3/5} \\sqrt{ \\frac{\\chi}{\\chi-2} }$"
   "execution_count": null,
    "# Extended Standard Cahn-Hilliard Example to Binary Flory Huggins \n",
    "# as discussed on 28/11/2019\n",
    "# Example can be found at \n",
    "# https://bitbucket.org/fenics-project/dolfin/src/master/python/demo/\n",
    "# documented/cahn-hilliard/demo_cahn-hilliard.py.rst#rst-header-id1\n",
    "# Runs with fenics 2019.01\n",
    "# The resulting .pvd file can be opened using default settings in ParaView\n",
    "\n",
    "import matplotlib.pyplot as plt\n",
    "import numpy as np\n",
    "from scipy.optimize import curve_fit\n",
    "import random\n",
    "from dolfin import *\n",
    "def create_expr(p_list):\n",
    "    string = ''\n",
    "    for p in p_list:\n",
    "        string = (string+'(x[0]-'+str(p[0])+')*(x[0]-'+str(p[0])+')+(x[1]-'+str(p[1])+\n",
    "                  ')*(x[1]-'+str(p[1])+')+(x[2]-'+str(p[2])+')*(x[2]-'+str(p[2])+\n",
    "                  ')<=.05*.05 ? .76 :' )\n",
    "    string = string + '.268'\n",
    "    return string\n",
    "\n",
    "f = Expression((create_expr([[0.5, 0.5, 0.5], [0.2, 0.2, 0.2], [0.8, 0.8, 0.8],\n",
    "                            [0.2, 0.8, 0.8], [0.2, 0.8, 0.2], [0.2, 0.2, 0.8],\n",
    "                            [0.8, 0.2, 0.2], [0.8, 0.8, 0.2], [0.8, 0.2, 0.8],\n",
    "                            [0.65, 0.65, 0.65], [0.35, 0.35, 0.35], [0.65, 0.35, 0.65],\n",
    "                            [0.65, 0.65, 0.35], [0.35, 0.65, 0.65], [0.65, 0.35, 0.35],\n",
    "                            [0.35, 0.35, 0.65], [0.35, 0.65, 0.35]]), 1), degree=1)\n",
    "\n",
    "# Class representing the intial conditions\n",
    "# class InitialConditions(UserExpression):\n",
    "#     def __init__(self, **kwargs):\n",
    "#         random.seed(2 + MPI.rank(MPI.comm_world))\n",
    "#         super().__init__(**kwargs)\n",
    "#     def eval(self, values, x):\n",
    "# #         if (x[0] > -0.1) & (x[0] < 0.1) & (x[1] > -0.1) & (x[1] < 0.1) & (x[2] > -0.1) & (x[2] < 0.1):\n",
    "# # #             values[0] = 0.63 + 0.02*(0.5 - random.random())\n",
    "# #             values[0] = 0.82\n",
    "# #         if (x[0]**2+x[1]**2+x[2]**2 < 0.25):\n",
    "# #             values[0] = 0.73\n",
    "# #         if (self.in_rad(x, [0.5, 0.5, 0.5], 0.05)):\n",
    "# #             values[0] = 0.75\n",
    "#         if (self.in_rad(x, [0.5, 0.5, 0.5], 0.05) or\n",
    "#             self.in_rad(x, [0.2, 0.2, 0.2], 0.05) or\n",
    "#             self.in_rad(x, [0.8, 0.8, 0.8], 0.05) or\n",
    "#             self.in_rad(x, [0.2, 0.8, 0.8], 0.05) or\n",
    "#             self.in_rad(x, [0.2, 0.8, 0.2], 0.05) or\n",
    "#             self.in_rad(x, [0.2, 0.2, 0.8], 0.05) or\n",
    "#             self.in_rad(x, [0.8, 0.2, 0.2], 0.05) or\n",
    "#             self.in_rad(x, [0.8, 0.8, 0.2], 0.05) or\n",
    "#             self.in_rad(x, [0.8, 0.2, 0.8], 0.05) or\n",
    "#             self.in_rad(x, [0.65, 0.65, 0.65], 0.05) or\n",
    "#             self.in_rad(x, [0.35, 0.35, 0.35], 0.05) or\n",
    "#             self.in_rad(x, [0.65, 0.35, 0.65], 0.05) or\n",
    "#             self.in_rad(x, [0.65, 0.65, 0.35], 0.05) or\n",
    "#             self.in_rad(x, [0.35, 0.65, 0.65], 0.05) or\n",
    "#             self.in_rad(x, [0.65, 0.35, 0.35], 0.05) or\n",
    "#             self.in_rad(x, [0.35, 0.35, 0.65], 0.05) or\n",
    "#             self.in_rad(x, [0.35, 0.65, 0.35], 0.05)):\n",
    "#             values[0] = 0.76\n",
    "#         else:\n",
    "#             values[0] = 0.268                \n",
    "#         values[1] = 0.0\n",
    "# #         values[0] = 0.63 + 0.02*(0.5 - random.random())\n",
    "#         values[1] = 0.0\n",
    "#     def value_shape(self):\n",
    "#         return (2,)\n",
    "#     def in_rad(self, x, drop, rad):\n",
    "#         if np.sqrt((x[0]-drop[0])**2+(x[1]-drop[1])**2+(x[2]-drop[2])**2)<rad:\n",
    "#             return True\n",
    "#         else:\n",
    "#             return False\n",
    "\n",
    "# Class for interfacing with the Newton solver\n",
    "class CahnHilliardEquation(NonlinearProblem):\n",
    "    def __init__(self, a, L):\n",
    "        NonlinearProblem.__init__(self)\n",
    "        self.L = L\n",
    "        self.a = a\n",
    "    def F(self, b, x):\n",
    "        assemble(self.L, tensor=b)\n",
    "    def J(self, A, x):\n",
    "        assemble(self.a, tensor=A)\n",
    "X = 2.2 # Chi Flory Huggins parameter\n",
    "# time stepping family, e.g.: \n",
    "# theta=1 -> backward Euler, theta=0.5 -> Crank-Nicolson\n",
    "theta  = 0.5      \n",
    "# Form compiler optionsmai\n",
    "parameters[\"form_compiler\"][\"optimize\"]     = True\n",
    "parameters[\"form_compiler\"][\"cpp_optimize\"] = True\n",
    "mesh = Mesh('Meshes/box_spheres.xml')\n",
    "# domain = ms.Sphere(Point(0, 0, 0), 1.0)\n",
    "# mesh = ms.generate_mesh(domain, 100)\n",
    "P1 = FiniteElement(\"Lagrange\", mesh.ufl_cell(), 1)\n",
    "ME = FunctionSpace(mesh, P1*P1)\n",
    "# Define trial and test functions\n",
    "du    = TrialFunction(ME)\n",
    "q, v  = TestFunctions(ME)\n",
    "# Define functions\n",
    "u   = Function(ME)  # current solution\n",
    "u0  = Function(ME)  # solution from previous converged step\n",
    "# Split mixed functions\n",
    "dc, dmu = split(du)\n",
    "c,  mu  = split(u)\n",
    "c0, mu0 = split(u0)\n",
    "# Create intial conditions and interpolate\n",
    "# u_init = InitialConditions(degree=1)\n",
    "# u.interpolate(u_init)\n",
    "# u0.interpolate(u_init)\n",
    "u.interpolate(f)\n",
    "u0.interpolate(f)\n",
    "print(time.time()-tis)\n",
    "# Compute the chemical potential df/dc\n",
    "c = variable(c)\n",
    "# mu_(n+theta)\n",
    "mu_mid = (1.0-theta)*mu0 + theta*mu\n",
    "# Weak statement of the equations\n",
    "L0 = c*q*dx - c0*q*dx + dt*c*(1-c)*dot(grad(mu_mid), grad(q))*dx\n",
    "L1 = mu*v*dx - (ln(c/(1-c))+X*(1-2*c))*v*dx - kappa*dot(grad(c), grad(v))*dx \n",
    "L = L0 + L1\n",
    "# Compute directional derivative about u in the direction of du (Jacobian)\n",
    "a = derivative(L, u, du)\n",
    "# Create nonlinear problem and Newton solver\n",
    "problem = CahnHilliardEquation(a, L)\n",
    "solver = NewtonSolver()\n",
    "# solver.parameters[\"linear_solver\"] = \"lu\"\n",
    "solver.parameters[\"linear_solver\"] = 'gmres'\n",
    "solver.parameters[\"preconditioner\"] = 'ilu'\n",
    "solver.parameters[\"convergence_criterion\"] = \"incremental\"\n",
    "solver.parameters[\"relative_tolerance\"] = 1e-6"
   ]
  },
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "# file = File(\"output.pvd\", \"compressed\")\n",
    "file_c = XDMFFile('c_long.xdmf')\n",
    "#     file << (u.split()[0], t)\n",
    "    file_c.write(u.split()[0], t)\n",
    "    t += dt\n",
    "    u0.vector()[:] = u.vector()\n",
    "    solver.solve(problem, u.vector())\n",
    "    print(time.time() - ti)\n",
    "file_c.close()"
   "execution_count": null,
   "source": [
    "def tanh_fit(x, a, b, c, d):\n",
    "     return np.tanh((x-a)/b)*c+d\n",
    "# xdata = np.linspace(0, x_mesh, x_mesh+1)\n",
    "# ydata = u.compute_vertex_values()[0:x_mesh+1]\n",
    "xdata = np.linspace(0.1, 0.5, 100)\n",
    "ydata = np.array([u0([x, 0.5, 0.5])[0] for x in xdata])\n",
    "popt, pcov = curve_fit(tanh_fit, xdata, ydata)\n",
    "plt.plot(xdata[65:], tanh_fit(xdata[65:], a, b, c, d))# np.tanh((xdata-52)/12.4)*0.36+0.5"
  {
   "cell_type": "code",
   "execution_count": null,
   "metadata": {},
   "outputs": [],
   "source": [
    "mesh.num_cells()"
   ]
  },
  {
   "cell_type": "markdown",
   "metadata": {},
   "source": [
    "## Thoughts on stability of algorithm\n",
    "- relatively robust to grid size changes\n",
    "- initial conditions seem to have strong influence\n",
    "- time step also plays a role for Newton solver\n",
    "- best results with small time step and initial conditions close to final concentrations"
   ]
  }
 ],
 "metadata": {
  "kernelspec": {
   "display_name": "Python 3",
   "language": "python",
   "name": "python3"
  },
  "language_info": {
   "codemirror_mode": {
    "name": "ipython",
    "version": 3
   },
   "file_extension": ".py",
   "mimetype": "text/x-python",
   "name": "python",
   "nbconvert_exporter": "python",
   "pygments_lexer": "ipython3",