Set up your first equations#

This is a jupyter notebook serving as a quick/first interactive tutorial for the usage of phyem.

To let Python know where to find the phyem package, we need to add the dir that contains the package to the searching path of Python unless it is already in a system path.

[1]:
import sys
ph_dir = '../../../../../'   # the path to dir that containing the phyem package.
sys.path.append(ph_dir)
import phyem as ph  # import the phyem package
ph.config._set_matplot_block(False)

We now set the dimensions of the embedding space to be 3.

[2]:
ph.config.set_embedding_space_dim(3)

Initialize an abstract manifold by

[3]:
manifold = ph.manifold(3)

Then an abstract mesh is built upon manifold manifold.

[4]:
mesh = ph.mesh(manifold)

Using function list_meshes of phyem can list all existing meshes in the current console.

[5]:
ph.list_meshes()
Existing meshes:
---------------- symbolic - <manifold> -------------------------
             \mathfrak{M} | <Manifold \mathcal{M} at 0x0000020735AEA570>

where we can see the symbolic representation of the mesh and the manifold on which it is built. If we render the symbolic representation of the mesh with an equation environment, we see \(\mathfrak{M}\).

We then can set spaces upon this mesh. For example, we set up spaces \(\Lambda^{(0)}(\mathcal{M})\), \(\Lambda^{(1)}(\mathcal{M})\), \(\Lambda^{(2)}(\mathcal{M})\), \(\Lambda^{(3)}(\mathcal{M})\), i.e., the Hilbert spaces of scalar valued 0- to 3-forms, by

[6]:
ph.space.set_mesh(mesh)
O0 = ph.space.new('Lambda', 0)  # Lambda is the indicator for (standard) scalar valued form spaces.
O1 = ph.space.new('Lambda', 1)
O2 = ph.space.new('Lambda', 2)
O3 = ph.space.new('Lambda', 3)
ph.list_spaces()  # we can also list all existing spaces
Implemented spaces:
   abbreviation - description
         Lambda | scalar valued k-form space
         bundle | bundle valued k-form space
bundle-diagonal | diagonal bundle valued k-form space

 Existing spaces:
        On mesh \mathfrak{M}
              0: \widetilde\Lambda^{(0)}(\mathcal{M})
              1: \widetilde\Lambda^{(1)}(\mathcal{M})
              2: \widetilde\Lambda^{(2)}(\mathcal{M})
              3: \widetilde\Lambda^{(3)}(\mathcal{M})

where we see first a list of all implemented spaces and then the existing spaces till this moment.

A form is just a instance of such space. So we make forms from spaces by calling method make_form which takes two arguments representing the symbolic representation and the linguistic representation of the form. These forms are the root forms.

[7]:
w = O1.make_form(r'\omega^1', "vorticity1")
u = O2.make_form(r'u^2', r"velocity2")
f = O2.make_form(r'f^2', r"body-force")
P = O3.make_form(r'P^3', r"total-pressure3")
ph.list_forms()  # this will generate a table in a separete figure showing.
[7]:
<Figure size 1000x500 with 1 Axes>

where we have used function list_forms to visualize/list the exsiting forms.

When it is the first time to invoke matplotlib and latex, it may be very slow since there are large amount of interplays among the packages. Be patient. Things become much better later on.

We can also visualize a particular form by calling its pr method. For example,

[8]:
u.pr()
[8]:
<Figure size 1200x600 with 1 Axes>

Furthermore, we can use these root forms to build other forms through operators like \(\wedge\), \(\star\), \(\mathrm{d}^\ast\), \(\mathrm{d}\), \(\partial_t\) and so on.

[9]:
dsP = ph.codifferential(P)
dsu = ph.codifferential(u)
du = ph.d(u)
du_dt = ph.time_derivative(u)
# ph.list_forms(locals())

Now, if you try ph.list_forms() which does not restrict the range of list_forms function to the local environment, the outputs are different.

[10]:
ph.list_forms()
[10]:
<Figure size 1000x500 with 1 Axes>

Basically, we see the id and then symbolic representation = linguistic representation of all forms.

With forms we can construct equations (usually partial differential equations, PDEs) through function ph.pde.

[11]:
exp1 = [
    'dudt - dsP = f',
    'w = dsu',
    'du = 0',
]
itp = {
    'dudt': du_dt,
    'dsP': dsP,
    'f': f,
    'w': w,
    'dsu': dsu,
    'du': du,
}
pde1 = ph.pde(exp1, itp)
pde1.unknowns = [u, w, P]

where we send an expression (exp1) and an interpreter (itp) to ph.pde to initialize an equation object named pde1. You can see that in exp1 we use string to represent the variables, terms and operators. The interpreter, itp, inteprets the string representations and thus ph.pde knows to use correct ingredients.

You can avoid defining the interpreter manually by use the built-in function locals. For example,

[12]:
exp2 = [
    'du_dt - dsP = f',
    'w = dsu',
    'du = 0',
]
pde2 = ph.pde(exp2, locals())
pde2.unknowns = [u, w, P]

In this way, you lose the freedom of naming the terms in the expression because locals() gives a dictionary whose keys are exactly the vraible names in the local environment. See 'dudt' in exp1 and du_dt in exp2.

After constructing our equations, we may want to have a close look at it to check if any mistakes we made. We can call method pr, standing for print representation, to do that. For example,

[13]:
pde2.pr(indexing=False)
[13]:
<Figure size 800x600 with 1 Axes>