How to use pyphasefield: Creating your own Engine — pyphasefield documentation

# How to use pyphasefield: Creating your own Engine

While the engines included with pyphasefield can be useful, it is very likely that the average user may want to run their own custom phase field model. To enable this, a Template engine file is shown here, which may be modified quite freely. This template file may be downloaded `here`.

## The Code

    try:
            #import from within Engines folder
            from ..field import Field
            from ..simulation import Simulation
            from ..ppf_utils import COLORMAP_OTHER, COLORMAP_PHASE
    except:
            try:
                    #import classes from pyphasefield library
                    from pyphasefield.field import Field
                    from pyphasefield.simulation import Simulation
                    from pyphasefield.ppf_utils import COLORMAP_OTHER, COLORMAP_PHASE
            except:
                    raise ImportError("Cannot import from pyphasefield library!")

    class Template(Simulation):
            def __init__(self, **kwargs):
                    super().__init__(**kwargs)
                    #additional initialization code goes below
                    #runs *before* tdb, thermal, fields, and boundary conditions are loaded/initialized

            def init_tdb_params(self):
                    super().init_tdb_params()
                    #additional tdb-related code goes below
                    #runs *after* tdb file is loaded, tdb_phases and tdb_components are initialized
                    #runs *before* thermal, fields, and boundary conditions are loaded/initialized

            def init_fields(self):
                    #initialization of fields code goes here
                    #runs *after* tdb and thermal data is loaded/initialized
                    #runs *before* boundary conditions are initialized
                    pass

            def initialize_engine(self):
                    super().initialize_engine()
                    #final initialization of the engine goes below
                    #runs *after* tdb, thermal, fields, and boundary conditions are loaded/initialized

            def just_before_simulating(self):
                    super().just_before_simulating()
                    #additional code to run just before beginning the simulation goes below
                    #runs immediately before simulating, no manual changes permitted to changes implemented here

            def simulation_loop(self):
                    #code to run each simulation step goes here
                    pass

Creating MyDiffusionClass from the Template

In order to create an engine, the first piece is to add code to the simulation loop to define what the engine actually does. Let us use a 2D diffusion equation to drive this engine. This will essentially recreate part of the included Diffusion engine that comes with pyphasefield. The diffusion equation in 2D is given as the following:

\(\frac{dc}{dt} = D\nabla ^2 c = D(\frac{d^2c}{dx^2} + \frac{d^2c}{dy^2})\).

In discretized form, using finite difference expressions for each of these terms, we get:

\(\frac{t-t_0}{\Delta t} = D\frac{c_{x+1, y}+c_{x-1, y}+c_{x, y+1}+c_{x, y-1}-4*c_{x, y}}{\Delta x^2}\).

\(t = t_0 + D\Delta t\frac{c_{x+1, y}+c_{x-1, y}+c_{x, y+1}+c_{x, y-1}-4*c_{x, y}}{\Delta x^2}\).

In our code, we can write these offset terms using the built-in numpy function numpy.roll, which offsets an array along a particular axis by a given amount. In this simulation, c will be our only field (sim.fields[0]). We will also include D as a user_data parameter in pyphasefield. Ultimately, this code looks like this:

    import numpy as np

    ...

    class Template(Simulation):
            ...

            def simulation_loop(self):
                    #code to run each simulation step goes here
                    c = self.fields[0].data
                    D = self.user_data["D"]
                    dx = self.dx
                    dt = self.dt

                    #define offset arrays, remember the sign of roll is opposite the direction of the cell of interest
                    #also, x is dimension 1, y is dimension 0 (C style arrays...)
                    c_p0 = np.roll(c, -1, 1) #x+1, y.
                    c_m0 = np.roll(c, 1, 1) #x-1, y.
                    c_0p = np.roll(c, -1, 0) #x, y+1.
                    c_0m = np.roll(c, 1, 0) #x, y-1.

                    #apply change from a single step
                    c += D*dt*(c_p0 + c_m0 + c_0p + c_0m - 4*c)/(dx**2)

We also can define what the default field is for c, as well as specify a default value for D if the user did not specify one. This isn’t required, but doesn’t hurt:

    import numpy as np

    ...

    class Template(Simulation):
            ...

            def init_fields(self):
                    #initialization of fields code goes here
                    #runs *after* tdb and thermal data is loaded/initialized
                    #runs *before* boundary conditions are initialized
                    if not ("D" in self.user_data):
                            self.user_data["D"] = 0.1
                    dim = self.dimensions
                    c = np.zeros(dim)
                    length = dim[0]
                    width = dim[1]
                    c[length // 4:3 * length // 4, width // 4:3 * width // 4] = 1
                    self.add_field(c, "c")

Altogether, the code looks like this for MyDiffusionClass.py

    import numpy as np
    try:
            #import from within Engines folder
            from ..field import Field
            from ..simulation import Simulation
            from ..ppf_utils import COLORMAP_OTHER, COLORMAP_PHASE
    except:
            try:
                    #import classes from pyphasefield library
                    from pyphasefield.field import Field
                    from pyphasefield.simulation import Simulation
                    from pyphasefield.ppf_utils import COLORMAP_OTHER, COLORMAP_PHASE
            except:
                    raise ImportError("Cannot import from pyphasefield library!")

    class Template(Simulation):
            def __init__(self, **kwargs):
                    super().__init__(**kwargs)
                    #additional initialization code goes below
                    #runs *before* tdb, thermal, fields, and boundary conditions are loaded/initialized

            def init_tdb_params(self):
                    super().init_tdb_params()
                    #additional tdb-related code goes below
                    #runs *after* tdb file is loaded, tdb_phases and tdb_components are initialized
                    #runs *before* thermal, fields, and boundary conditions are loaded/initialized

            def init_fields(self):
                    #initialization of fields code goes here
                    #runs *after* tdb and thermal data is loaded/initialized
                    #runs *before* boundary conditions are initialized
                    if not ("D" in self.user_data):
                            self.user_data["D"] = 0.1
                    dim = self.dimensions
                    c = np.zeros(dim)
                    length = dim[0]
                    width = dim[1]
                    c[length // 4:3 * length // 4, width // 4:3 * width // 4] = 1
                    self.add_field(c, "c")

            def initialize_engine(self):
                    super().initialize_engine()
                    #final initialization of the engine goes below
                    #runs *after* tdb, thermal, fields, and boundary conditions are loaded/initialized

            def just_before_simulating(self):
                    super().just_before_simulating()
                    #additional code to run just before beginning the simulation goes below
                    #runs immediately before simulating, no manual changes permitted to changes implemented here

            def simulation_loop(self):
                    #code to run each simulation step goes here
                    c = self.fields[0].data
                    D = self.user_data["D"]
                    dx = self.dx
                    dt = self.dt

                    #define offset arrays, remember the sign of roll is opposite the direction of the cell of interest
                    #also, x is dimension 1, y is dimension 0 (C style arrays...)
                    c_p0 = np.roll(c, -1, 1) #x+1, y.
                    c_m0 = np.roll(c, 1, 1) #x-1, y.
                    c_0p = np.roll(c, -1, 0) #x, y+1.
                    c_0m = np.roll(c, 1, 0) #x, y-1.

                    #apply change from a single step
                    c += D*dt*(c_p0 + c_m0 + c_0p + c_0m - 4*c)/(dx**2)

Now, if we use a slightly edited script from the diffusion example:

    from MyDiffusion import MyDiffusionClass

    sim = MyDiffusionClass(dimensions=[500, 500])

    #initialize non-array parameters
    sim.set_framework("CPU_SERIAL") #"CPU_SERIAL" (GPU_SERIAL not coded for MyDiffusionClass yet!)
    sim.set_dx(1.)
    sim.set_dt(0.1)
    sim.set_save_path("data/diffusion_test")
    sim.set_autosave_flag(True)
    sim.set_autosave_save_images_flag(True)
    sim.set_autosave_rate(2000)
    sim.set_boundary_conditions("PERIODIC")

    data = {
            "D":1.
    }
    sim.set_user_data(data)

    #initialize simulation arrays, all parameter changes should be BEFORE this point!
    sim.initialize_engine()

    #change array data here, for custom simulations
    """
    sim.fields[0].data[:] = 1.
    length = sim.dimensions[0]
    width = sim.dimensions[1]
    sim.fields[0].data[length // 4:3 * length // 4, width // 4:3 * width // 4] = 0.
    """

    #initial conditions
    sim.plot_simulation()

    #run simulation
    sim.simulate(2000)

    #final conditions
    sim.plot_simulation()

We obtain the same results as the built-in diffusion class!

## Downloads

`MyDiffusionClass`.