diff --git a/phasefield/analytical.py b/phasefield/analytical.py index 1318b341f476260f6421616f664032c49a85d76e..20b7171e14b6e2bc9749aea4176396cb2f293f50 100644 --- a/phasefield/analytical.py +++ b/phasefield/analytical.py @@ -303,7 +303,7 @@ def extract_gamma(free_energy, order_parameters): return result -def pressure_tensor_bulk_component(free_energy, order_parameters, bulk_chemical_potential): +def pressure_tensor_bulk_component(free_energy, order_parameters, bulk_chemical_potential=None): """Diagonal component of pressure tensor in bulk""" bulk_free_energy, _ = separate_into_bulk_and_interface(free_energy) if bulk_chemical_potential is None: diff --git a/phasefield/n_phase_boyer.py b/phasefield/n_phase_boyer.py new file mode 100644 index 0000000000000000000000000000000000000000..e18bff95a9a35969276a4f9b9c28603d6fadb082 --- /dev/null +++ b/phasefield/n_phase_boyer.py @@ -0,0 +1,383 @@ +"""n-phase model according to +Hierarchy of consistent n-component Cahn-Hilliard systems by Franck Boyer, Sebastian Minjeaud +""" + +import sympy as sp +from itertools import combinations + +from lbmpy.phasefield.analytical import chemical_potentials_from_free_energy +from pystencils.cache import memorycache +from pystencils.fd import Diff +import pystencils.fd as fd +from pystencils.sympyextensions import prod + + +@memorycache(maxsize=8) +def diffusion_coefficients(surface_tensions): + r"""Computes diffusion coefficients labeled ̅α in the paper""" + assert surface_tensions.rows == surface_tensions.cols + num_phases = surface_tensions.rows + assert all(surface_tensions[k, k] == 0 for k in range(num_phases)), "Diagonal of surface tension matrix has to be 0" + alpha_symbolic = sp.Matrix(num_phases, num_phases, + lambda i, j: sp.symbols(f"α_{i}{j}" if i < j else f"α_{j}{i}")) + for i in range(num_phases): + alpha_symbolic[i, i] = -sum(alpha_symbolic[i, j] + for j in range(num_phases) if i != j) + + gamma_vector = sp.Matrix(sp.symbols(f"γ_:{num_phases}")) + unit_vector = sp.Matrix(num_phases, 1, lambda a, b: 1) + unit_matrix = sp.Matrix(num_phases, num_phases, lambda a, b: 1 if a == b else 0) + + lemma_2_1 = alpha_symbolic * surface_tensions - unit_matrix - gamma_vector * unit_vector.T + + eq_sys = [lemma_2_1[i, j] for i in range(num_phases) for j in range(num_phases)] + unknowns = list(alpha_symbolic.atoms(sp.Symbol)) + list(gamma_vector.atoms(sp.Symbol)) + solution = sp.solve(eq_sys, unknowns) + assert solution, "Equation system for diffusion coefficients could not be solved" + return sp.ImmutableDenseMatrix(alpha_symbolic.subs(solution)), gamma_vector.subs(solution) + + +def capital_i(k, i): + """third equation in section 3.1.1""" + if not hasattr(i, '__len__'): + i = list(range(i)) + for result in combinations(i, k): + yield result + + +def capital_s(k, c, f): + n = len(c) + result = 0 + for i in capital_i(k, n): + result += f(sum(c[index] for index in i)) + return result + + +def psi(k, c, f): + """first equation in section 3.1.1""" + n = len(c) + assert k <= n + result = 0 + for i in capital_i(k, n): + for s in range(1, k + 1): + for j in capital_i(s, i): + result += (-1) ** (k - s) * f(sum(c[index] for index in j)) + return result + + +def psi2(k, c, f): + n = len(c) + assert k <= n + result = 0 + for s in range(1, k + 1): + result += (-1) ** (k - s) * sp.binomial(n - s, k - s) * capital_s(s, c, f) + return result + + +def h(u, v): + return sp.Abs(v) * v / (sp.Abs(v) + u ** 2) + + +def compute_ab(f): + c = sp.Dummy(positive=True) + sqrt_f = sp.sqrt(f(c)) + max_sqrt_f = max(sqrt_f.subs(c, 0), + sqrt_f.subs(c, sp.Rational(1, 2)), + sqrt_f.subs(c, 1)) + sqrt_int = sp.integrate(sqrt_f, (c, 0, 1)) + return max_sqrt_f / sqrt_int, 1 / (2 * max_sqrt_f * sqrt_int) + + +def l_bar(free_energy, alpha_bar, c): + result = [] + n = len(c) + for i in range(n): + result.append(-sum(alpha_bar[i, j] * sp.diff(free_energy, c[j]) for j in range(n))) + return sp.Matrix(result) + + +def free_energy_interfacial(c, surface_tensions, a, epsilon): + n = len(c) + return -epsilon * a / 2 * sum(surface_tensions[i, j] * Diff(c[i]) * Diff(c[j]) + for i in range(n) for j in range(n)) + + +def free_energy_bulk(capital_f, b, epsilon): + return b / epsilon * capital_f + + +def lambda_equal_surface_tension(k, scalar_sigma, lambda1): + k += 1 + one = sp.sympify(1) + return lambda1 - scalar_sigma / 2 * sum(one / s for s in range(1, k)) + + +def capital_f_bulk_equal_surface_tension(c, f, scalar_sigma, lambda1): + n = len(c) + return sum(lambda_equal_surface_tension(k, scalar_sigma, lambda1) * psi(k + 1, c, f) + for k in range(n)) + + +def capital_f0(c, surface_tension, f=lambda c: c ** 2 * (1 - c) ** 2): + n = len(c) + result = 0 + for j in range(n): + for i in range(j): + result += surface_tension[i, j] / 2 * (f(c[i]) + f(c[j]) - f(c[i] + c[j])) + return result + + +def symbolic_surface_tensions(num_phases): + def creation_func(i, j): + if i == j: + return 0 + if j < i: + i, j = j, i + return sp.Symbol("sigma_{}{}".format(i, j)) + return sp.ImmutableDenseMatrix(num_phases, num_phases, creation_func) + + +def cahn_hilliard_fd(c, mu, alpha, dx=1, dt=1): + discretize = fd.Discretization2ndOrder(dx=dx, dt=dt) + result = [] + for c_i in c: + pde = fd.transient(c_i) + fd.diffusion(alpha * mu, diffusion_coeff=1) + result.append(discretize(pde)) + return result + + +# noinspection PyPep8Naming +class capital_h(sp.Function): + nargs = (2,) + + def fdiff(self, argindex=1): + u, v = self.args + av = sp.Abs(v) + zero_cond = sp.And(sp.Eq(u, 0), sp.Eq(v, 0)) + if argindex == 1: + val = -2 * u * av * v / (av + u ** 2) ** 2 + return sp.Piecewise((0, zero_cond), (val, True)) + elif argindex == 2: + val = (2 * av * u ** 2 + v ** 2) / (av + u ** 2) ** 2 + return sp.Piecewise((1, zero_cond), (val, True)) + else: + raise sp.function.ArgumentIndexError(self, argindex) + + @staticmethod + def insert(expr): + if isinstance(expr, capital_h): + u, v = expr.args + av = sp.Abs(v) + zero_cond = sp.And(sp.Eq(u, 0), sp.Eq(v, 0)) + return sp.Piecewise((0, zero_cond), + (av * v / (av + u**2), True)) + else: + new_args = [capital_h.insert(arg) for arg in expr.args] + return expr.func(*new_args) if new_args else expr + + +def correction_g(c, surface_tensions, symbolic_coefficients=False): + assert len(c) == surface_tensions.rows + n = len(c) + result = 0 + for i in capital_i(4, n): + for s in i: + reduced_i = tuple(e for e in i if e != s) + if symbolic_coefficients: + coeff = sp.Symbol("Lambda_{}{}{}{}{}".format(s, *i)) + else: + coeff = capital_lambda(surface_tensions, reduced_i)[s] + result += coeff * capital_h(c[s], prod(c[j] for j in i)) + return result + + +def capital_gamma(sigma, i, index_tuple): + assert tuple(sorted(index_tuple)) == index_tuple + j, k, m = index_tuple + alpha, gamma = diffusion_coefficients(sigma) + return -6 * (alpha[i, j] * (sigma[j, k] + sigma[j, m]) + + alpha[i, k] * (sigma[j, k] + sigma[k, m]) + + alpha[i, m] * (sigma[j, m] + sigma[k, m]) - gamma[i]) + + +def capital_lambda(surface_tensions, index_tuple): + n = surface_tensions.rows + alpha, _ = diffusion_coefficients(surface_tensions) + + unknowns = [sp.Dummy() for _ in range(n)] + for i in index_tuple: + unknowns[i] = sp.oo + + eqs = [] + for i in range(n): + if i in index_tuple: + continue + eq = - capital_gamma(surface_tensions, i, index_tuple) + for j in range(n): + if j in index_tuple: + continue + eq += alpha[i, j] * unknowns[j] + eqs.append(eq) + + unknown_symbols = [unknowns[i] for i in range(n) if i not in index_tuple] + solve_result = sp.solve(eqs, unknown_symbols) + return sp.Matrix(unknowns).subs(solve_result) + + +def stabilization_term(c, alpha): + n = alpha.rows + result = sum(c[i]**2 * c[j]**2 * c[k]**2 + for i in range(n) for j in range(i) for k in range(j)) + + for j in range(n): + for i in range(j): + sub = 0 + for l in range(n): + if l in (i, j): + continue + for k in range(l): + if k in (i, j): + continue + sub += capital_theta(alpha, (i, j, k, l)) * capital_h(c[k], c[k] * c[l]) + sub += capital_theta(alpha, (i, j, l, k)) * capital_h(c[l], c[k] * c[l]) + result -= c[i]**2 * c[j]**2 * sub + return result + + +def capital_theta(alpha, index): + assert len(index) == 4 + reduced_index = (index[0], index[1], index[3]) + return capital_theta_helper(alpha, reduced_index)[index[2]] + + +@memorycache(maxsize=512) +def capital_theta_helper(alpha, index): + n = alpha.rows + j0, k0, l0 = index + unknowns = [sp.Dummy() for _ in range(n)] + for i in index: + unknowns[i] = sp.oo + + eqs = [] + for i in range(n): + if i in index: + continue + eq = - 2 * alpha[i, l0] + for j in range(n): + if j in index: + continue + eq += alpha[i, j] * unknowns[j] + eqs.append(eq) + + unknown_symbols = [unknowns[i] for i in range(n) if i not in index] + solve_res = sp.solve(eqs, unknown_symbols) + return sp.Matrix(unknowns).subs(solve_res) + + +# ------------------------------------------ Custom Piecewise simplification ------------------------------------------- + +def condition_variables(expression): + """Returns set of variables that are used in Piecewise conditions""" + result = set() + + def visit(expr): + if isinstance(expr, sp.Piecewise): + for e in expr.args: + result.update(e[1].atoms(sp.Symbol)) + else: + for a in expr.args: + visit(a) + visit(expression) + return result + + +def simplify_zero_conditions_old(expression, cond=True): + cond_vars = condition_variables(expression) + if not cond_vars: + return [(expression, cond)] + + result = [] + substitutions = {} + for cond_var in cond_vars: + zero_expr = expression.subs(cond_var, 0) + condition_type = sp.LessThan if cond_var.is_nonnegative else sp.Eq + result += simplify_zero_conditions_old(zero_expr, sp.And(cond, condition_type(cond_var, 0))) + substitutions[cond_var] = sp.Dummy(nonzero=True) + + non_zero_expr = expression.subs(substitutions).subs({v: k for k, v in substitutions.items()}) + result += [(non_zero_expr, cond)] + return result + + +def simplify_zero_conditions(expression): + cond_vars = condition_variables(expression) + if not cond_vars: + return expression + + result = [] + substitutions = {} + for cond_var in cond_vars: + zero_expr = expression.subs(cond_var, 0) + condition_type = sp.LessThan if cond_var.is_nonnegative else sp.Eq + result += [(simplify_zero_conditions(zero_expr), condition_type(cond_var, 0))] + substitutions[cond_var] = sp.Dummy(nonzero=True) + + non_zero_expr = expression.subs(substitutions).subs({v: k for k, v in substitutions.items()}) + result += [(non_zero_expr, True)] + return sp.Piecewise(*result) + + +def chemical_potential_n_phase_boyer(order_parameters, interface_width, surface_tensions, correction_factor, + zero_threshold=0, assume_nonnegative=False): + n = len(order_parameters) + c = order_parameters + if hasattr(surface_tensions, '__call__'): + sigma = sp.ImmutableDenseMatrix(n, n, lambda i, j: surface_tensions(i, j) if i != j else 0) + else: + sigma = sp.ImmutableDenseMatrix(n, n, lambda i, j: surface_tensions[i, j] if i != j else 0) + + alpha, _ = diffusion_coefficients(sigma) + capital_f = capital_f0(c, sigma) + correction_g(c, sigma) + correction_factor * stabilization_term(c, alpha) + + def f(c): + return c ** 2 * (1 - c) ** 2 + + a, b = compute_ab(f) + + fe_bulk = free_energy_bulk(capital_f, b, interface_width) + fe_if = free_energy_interfacial(c, sigma, a, interface_width) + + mu_bulk = chemical_potentials_from_free_energy(fe_bulk, order_parameters) + mu_bulk = sp.Matrix([simplify_zero_conditions(capital_h.insert(e)) for e in mu_bulk]) + if zero_threshold != 0: + substitutions = {sp.Eq(c_i, 0): sp.StrictLessThan(sp.Abs(c_i), zero_threshold) for c_i in c} + mu_bulk = mu_bulk.subs(substitutions) + + if assume_nonnegative: + substitutions = {c_i: sp.Dummy(nonnegative=True) for c_i in c} + mu_bulk = mu_bulk.subs(substitutions).subs({v: k for k, v in substitutions.items()}) + + mu_if = chemical_potentials_from_free_energy(fe_if, order_parameters) + return fe_bulk, fe_if, mu_bulk, mu_if + + +# noinspection PyUnresolvedReferences +def plot_h(): + from mpl_toolkits.mplot3d import Axes3D # noqa + import matplotlib.pyplot as plt + import numpy as np + + fig = plt.figure() + ax = fig.add_subplot(111, projection='3d') + + # Make data + u = np.linspace(-1, 1, 100) + v = np.linspace(0, 1, 100) + x_grid, y_grid = np.meshgrid(u, v) + us, vs = sp.symbols("u, v", real=True) + h_vals = capital_h(us, vs) + hl = sp.lambdify((us, vs), h_vals, modules=['numpy']) + z_vals = hl(x_grid, y_grid) + # Plot the surface + ax.plot_surface(x_grid, y_grid, z_vals, color='b') + plt.show()