diff --git a/phasefield/eos.py b/phasefield/eos.py new file mode 100644 index 0000000000000000000000000000000000000000..be600eb189a0b9e850955f7259c8afd7503d2f31 --- /dev/null +++ b/phasefield/eos.py @@ -0,0 +1,90 @@ +import sympy as sp + + +def eos_from_free_energy(free_energy, density): + """Compute equation of state from free energy""" + chemical_potential = sp.diff(free_energy, density) + return density * chemical_potential - free_energy + + +def free_energy_from_eos(eos, density, integration_constant): + """Compute free energy from equation of state by integration + Args: + eos: equation of state + density: symbolic! density parameter + integration_constant: + """ + return (sp.integrate(eos / (density**2), density) + integration_constant) * density + + +# ---------------------------------- Equations of state ---------------------------------------------------------------- + + +def maxwell_construction(eos, tolerance=1e-4): + """Numerical Maxwell construction to find Ï_gas and Ï_liquid for a given equation of state. + + Args: + eos: equation of state, that has only one symbol (the density) in it + tolerance: internally a bisection algorithm is used to find pressure such that areas below and + above are equal. The tolerance parameter refers to the pressure. If the integral is smaller than + the tolerance the bisection algorithm is stopped. + + Returns: + (gas density, liquid density) + """ + # The following solve and integrate calls can make use of the fact that the density is a positive, real number + dofs = eos.atoms(sp.Symbol) + assert len(dofs) == 1 + density = dofs.pop() + rho = sp.Dummy(real=True, positive=True) + eos = eos.subs(density, rho) + + # pre-compute integral once - then it is evaluated in every bisection iteration + symbolic_offset = sp.Dummy(real=True) + integral = sp.integrate(sp.nsimplify((eos - symbolic_offset) / (rho ** 2)), rho) + upper_bound, lower_bound = sp.Dummy(real=True), sp.Dummy(real=True) + symbolic_deviation = integral.subs(rho, upper_bound) - integral.subs(rho, lower_bound) + get_deviation = sp.lambdify((lower_bound, upper_bound, symbolic_offset), symbolic_deviation) + + critical_points = sorted(sp.solve(sp.diff(eos, rho))) + max_rho, min_rho, _ = critical_points + max_p, min_p = eos.subs(rho, max_rho), eos.subs(rho, min_rho) + shift_max = max_p * 0.999 + shift_min = max_p * 0.0001 + + c = (shift_max + shift_min) / 2 + deviation = tolerance * 2 + while abs(deviation) > tolerance: + zeros = sp.solve(eos - c) + integral_bounds = (min(zeros), max(zeros)) + deviation = get_deviation(float(integral_bounds[0]), float(integral_bounds[1]), float(c)) + if deviation > 0: + shift_min = c + else: + shift_max = c + c = (shift_max + shift_min) / 2 + + return integral_bounds + + +# To get final free energy: +# - from maxwell construciton $\rho_{min}$ and $\rho_{max}$ +# - remove slope from free energy function: C determined by $C = - \frac{d}{dÏ} F(C=0) $ +# - energy shift = $F(Ï_{liquid})$ or $F(Ï_{gas})$ (should be equal) +# - final free energy := $F - F(Ï_{liquid})$ + + +def carnahan_starling_eos(density, gas_constant, temperature, a, b): + """Carnahan Starling equation of state. + + a, b are parameters specific to this equation of state + for details see: Equations of state in a lattice Boltzmann model, by Yuan and Schaefer, 2006 + """ + e = b * density / 4 + fraction = (1 + e + e**2 - e**3) / (1 - e)**3 + return density * gas_constant * temperature * fraction - a * density ** 2 + + +def carnahan_starling_critical_temperature(a, b, gas_constant): + return 0.3773 * a / b / gas_constant +