diff --git a/phasefield/analytical.py b/phasefield/analytical.py index ca84f6b53b14ede4f96a3779c52ba906150c44c5..d98ac5423e859498749c42507ba086acfedf1ec4 100644 --- a/phasefield/analytical.py +++ b/phasefield/analytical.py @@ -29,8 +29,8 @@ def freeEnergyFunction3Phases(orderParameters=None, interfaceWidth=interfaceWidt kappaPrime = tuple(interfaceWidth**2 * k for k in kappa) C = sp.symbols("C_:3") - bulkFreeEnergy = sum(k / 2 * C_i ** 2 * (1 - C_i) ** 2 for k, C_i in zip(kappa, C)) - surfaceFreeEnergy = sum(k / 2 * Diff(C_i) ** 2 for k, C_i in zip(kappaPrime, C)) + bulkFreeEnergy = sum(k * C_i ** 2 * (1 - C_i) ** 2 / 2 for k, C_i in zip(kappa, C)) + surfaceFreeEnergy = sum(k * Diff(C_i) ** 2 / 2 for k, C_i in zip(kappaPrime, C)) F = 0 if includeBulk: @@ -59,6 +59,25 @@ def freeEnergyFunction3Phases(orderParameters=None, interfaceWidth=interfaceWidt return F +def freeEnergyFunctionalNPhasesPenaltyTerm(orderParameters, interfaceWidth=interfaceWidthSymbol, kappa=None, + penaltyTermFactor=0.01): + numPhases = len(orderParameters) + if kappa is None: + kappa = sp.symbols("kappa_:%d" % (numPhases,)) + if not hasattr(kappa, "__length__"): + kappa = [kappa] * numPhases + + def f(x): + return x ** 2 * (1 - x) ** 2 + + bulk = sum(f(c) * k / 2 for c, k in zip(orderParameters, kappa)) + interface = sum(Diff(c) ** 2 / 2 * interfaceWidth ** 2 * k + for c, k in zip(orderParameters, kappa)) + + bulkPenaltyTerm = (1 - sum(c for c in orderParameters)) ** 2 + return bulk + interface + penaltyTermFactor * bulkPenaltyTerm + + def freeEnergyFunctionalNPhases(numPhases=None, surfaceTensions=symmetricSymbolicSurfaceTension, interfaceWidth=interfaceWidthSymbol, orderParameters=None, includeBulk=True, includeInterface=True, symbolicLambda=False, diff --git a/phasefield/experiments1D.py b/phasefield/experiments1D.py index ae304d6c0f9b9c494aec276f3eea71ec99e1058f..867e09024034113efec2097e6171131a40a5ea02 100644 --- a/phasefield/experiments1D.py +++ b/phasefield/experiments1D.py @@ -1,4 +1,7 @@ import numpy as np +import sympy as sp + +from lbmpy.chapman_enskog import Diff from pystencils import makeSlice @@ -33,24 +36,132 @@ def plotStatus(phaseFieldStep, fromX=None, toX=None): plt.legend() -def initSharpInterface(pfStep, phaseIdx1=1, phaseIdx2=2, x1=None, x2=None): +def plotFreeEnergyBulkContours(freeEnergy, orderParameters, phase0=0, phase1=1, **kwargs): + import lbmpy.plot2d as plt + + x = np.linspace(-.2, 1.2, 100) + y = np.linspace(-.2, 1.2, 100) + xg, yg = np.meshgrid(x, y) + substitutions = {op: 0 for i, op in enumerate(orderParameters) if i not in (phase0, phase1)} + substitutions.update({d: 0 for d in freeEnergy.atoms(Diff)}) # remove interface components of free energy + freeEnergyLambda = sp.lambdify([orderParameters[phase0], orderParameters[phase1]], + freeEnergy.subs(substitutions)) + if 'levels' not in kwargs: + kwargs['levels'] = np.linspace(0, 1, 60) + plt.contour(x, y, freeEnergyLambda(xg, yg), **kwargs) + + +def initSharpInterface(pfStep, phaseIdx, x1=None, x2=None, inverse=False): domainSize = pfStep.dataHandling.shape if x1 is None: x1 = domainSize[0] // 4 if x2 is None: x2 = 3 * x1 + if phaseIdx >= pfStep.numOrderParameters: + return + for b in pfStep.dataHandling.iterate(): x = b.cellIndexArrays[0] mid = np.logical_and(x1 < x, x < x2) phi = b[pfStep.phiFieldName] + val1, val2 = (1, 0) if inverse else (0, 1) + + phi[..., phaseIdx].fill(val1) + phi[mid, phaseIdx] = val2 + + pfStep.setPdfFieldsFromMacroscopicValues() - if phaseIdx1 is not None: - phi[..., phaseIdx1].fill(0) - phi[mid, phaseIdx1] = 1 - if phaseIdx2 is not None: - phi[..., phaseIdx2].fill(1) - phi[mid, phaseIdx2] = 0 +def tanhTest(pfStep, phase0, phase1, expectedInterfaceWidth=1, timeSteps=10000): + """ + Initializes a sharp interface and checks if tanh-shaped profile is developing + :param pfStep: phase field scenario / step + :param phase0: index of first phase to initialize + :param phase1: index of second phase to initialize inversely + :param expectedInterfaceWidth: interface width parameter alpha that is used in analytical form + :param timeSteps: number of time steps run before evaluation + :return: deviation of simulated profile from analytical solution as average(abs(simulation-analytic)) + """ + import lbmpy.plot2d as plt + from lbmpy.phasefield.analytical import analyticInterfaceProfile + + domainSize = pfStep.dataHandling.shape + pfStep.reset() + pfStep.dataHandling.fill(pfStep.phiFieldName, 0) + initSharpInterface(pfStep, phaseIdx=phase0, inverse=False) + initSharpInterface(pfStep, phaseIdx=phase1, inverse=True) pfStep.setPdfFieldsFromMacroscopicValues() + pfStep.run(timeSteps) + + visWidth = 20 + x = np.arange(visWidth) - (visWidth // 2) + analytic = np.array([analyticInterfaceProfile(x_i - 0.5, expectedInterfaceWidth) for x_i in x], dtype=np.float64) + + stepLocation = domainSize[0] // 4 + simulated = pfStep.phi[stepLocation - visWidth//2:stepLocation + visWidth//2, 0, phase0] + plt.plot(analytic, label='analytic', marker='o') + plt.plot(simulated, label='simulated', marker='x') + plt.legend() + + return np.average(np.abs(simulated - analytic)) + + +def galileanInvarianceTest(pfStep, velocity=0.05, rounds=3, phase0=0, phase1=1, + expectedInterfaceWidth=1, initTimeSteps=5000): + """ + Moves interface at constant speed through periodic domain - check if momentum is conserved + :param pfStep: phase field scenario / step + :param velocity: constant velocity to move interface + :param rounds: how many times the interface should travel through the domain + :param phase0: index of first phase to initialize + :param phase1: index of second phase to initialize inversely + :param expectedInterfaceWidth: interface width parameter alpha that is used in analytical form + :param initTimeSteps: before velocity is set, this many time steps are run to let interface settle to tanh shape + :return: change in velocity + """ + import lbmpy.plot2d as plt + from lbmpy.phasefield.analytical import analyticInterfaceProfile + + domainSize = pfStep.dataHandling.shape + roundTimeSteps = int((domainSize[0]+0.25) / velocity) + + print("Velocity:", velocity, " Timesteps for round:", roundTimeSteps) + + pfStep.reset() + pfStep.dataHandling.fill(pfStep.phiFieldName, 0) + initSharpInterface(pfStep, phaseIdx=phase0, inverse=False) + initSharpInterface(pfStep, phaseIdx=phase1, inverse=True) + pfStep.setPdfFieldsFromMacroscopicValues() + + print("Running", initTimeSteps, "initial time steps") + pfStep.run(initTimeSteps) + pfStep.dataHandling.fill(pfStep.velFieldName, velocity, fValue=0) + pfStep.setPdfFieldsFromMacroscopicValues() + + stepLocation = domainSize[0] // 4 + visWidth = 20 + + simulatedProfiles = [] + + def captureProfile(): + simulated = pfStep.phi[stepLocation - visWidth // 2:stepLocation + visWidth // 2, 0, phase0].copy() + simulatedProfiles.append(simulated) + + captureProfile() + for rt in range(rounds ): + print("Running round %d/%d" % (rt+1, rounds)) + pfStep.run(roundTimeSteps) + captureProfile() + + x = np.arange(visWidth) - (visWidth // 2) + analytic = np.array([analyticInterfaceProfile(x_i - 0.5, expectedInterfaceWidth) for x_i in x], dtype=np.float64) + + plt.plot(x, analytic, label='analytic', marker='o') + for i, profile in enumerate(simulatedProfiles): + plt.plot(x, profile, label="After %d rounds" % (i,)) + + plt.legend() + + return np.average(pfStep.velocity[:, 0, 0]) - velocity diff --git a/phasefield/phasefieldstep.py b/phasefield/phasefieldstep.py index e6dc213dcbddd75d038cfae7e152ad66027359cb..98245be4b68c91aea808d23204ea18f05d9530e3 100644 --- a/phasefield/phasefieldstep.py +++ b/phasefield/phasefieldstep.py @@ -17,7 +17,7 @@ class PhaseFieldStep: target='cpu', openMP=False, kernelParams={}, dx=1, dt=1, solveCahnHilliardWithFiniteDifferences=False): if dataHandling is None: - dataHandling = SerialDataHandling(domainSize) + dataHandling = SerialDataHandling(domainSize, periodicity=True) self.freeEnergy = freeEnergy self.chemicalPotentials = chemicalPotentialsFromFreeEnergy(freeEnergy, orderParameters) @@ -95,17 +95,22 @@ class PhaseFieldStep: name=name + "_chLbm_%d" % (i,), ) self.cahnHilliardSteps.append(chStep) + self.runHydroLbm = True + self.densityOrderParameter = densityOrderParameter + self.timeStepsRun = 0 + self.reset() + + def reset(self): # Init φ and μ self.dataHandling.fill(self.phiFieldName, 0.0) - self.dataHandling.fill(self.phiFieldName, 1.0 if densityOrderParameter is not None else 0.0, fValue=0) + self.dataHandling.fill(self.phiFieldName, 1.0 if self.densityOrderParameter is not None else 0.0, fValue=0) self.dataHandling.fill(self.muFieldName, 0.0) self.dataHandling.fill(self.forceFieldName, 0.0) + self.dataHandling.fill(self.velFieldName, 0.0) self.setPdfFieldsFromMacroscopicValues() self.timeStepsRun = 0 - self.runHydroLbm = True - def setPdfFieldsFromMacroscopicValues(self): self.hydroLbmStep.setPdfFieldsFromMacroscopicValues() for chStep in self.cahnHilliardSteps: