I have been meaning to checkout Julia for a while, an open source (and real) alternative to Matlab for doing scientific computing. I left Matlab for Python a few years ago, and have not looked back. However, Julia touts a mash-up of flexibility and high performance, through a dynamic type system and a JIT compiler, which makes it hard to ignore. Julia is also taking aim at parallel computing and cloud networks, I am extremely interested in these topics both from a research and an industry perspective.
Partial differential equations using finite volume, we will be working on a TensorMesh in 1D, 2D and 3D, and want gridding, plotting, operators, averaging and interpolation. This is a big step on the way towards usable geophysical simulations, and will hopefully stress test my learning of Julia.
First off, let us look to the SimPEG code base in Python to guide what we want to do. There is a Mesh class in SimPEG that holds all information about the mesh structure, linear operators, and has methods for easy plotting. This uses object oriented programming (OOP), inheritance, and lazy-loading of properties. What I love about this is that the methods are attached to the mesh, so in IPython you can create a mesh, and dot-tab to see the properties and methods which are available to that instance. In Julia, there is no OOP, they have opted to use Types and Multiple Dispatch instead. This has a lot of advantages for parallel computing, but could be a bit harder to get started with a new library. Here we will look at the high level differences of the two packages.
In Python:
In Julia:
In Python, faceDiv is actually a @property which points to _faceDiv or creates it if it does not exist. In Julia, because we are dealing with Types not classes, we cannot attach fancy methods to the types, so what I have done is create a DifferentialOperators type, which is by default empty:
And then when we call faceDiv, we can check if the operators are defined and return them, otherwise we do some logic and store it in the Mesh.ops field:
The ideas are pretty similar, but the code lives in different places, and, in Julia, is not attached directly to the Mesh object. Amazingly, I have gotten this far without even talking about the Mesh object, so what does that look like in a non-OOP language? We shall start with a simple 1D tensor product mesh, and define a Type for the mesh:
In addition to these basic parts of the 1D mesh, I talked previously about adding in the DifferentialOperators as a property of the Mesh which allows us to store operators rather than recompute them every time. I want to add that in without changing the structure of the caller:
This creates a new dispatch method for the TensorMesh1D that has the signature:
Wonderful, so now we have a Mesh type that stores the spacing and the origin. In reality I have also added some mixin-types for counting up cells in the mesh (e.g. M.cnt.nC). We could now feed this to faceDiv, and it should do something! However, what happens when we have multiple types of meshes (e.g. CylMesh, TensorMesh3D)? Each mesh should have its own faceDiv function. One thing that is awesome in Python is multiple inheritance, a 1D TensorMesh is a Mesh, a TensorMesh, and it has operators, inner-products and plotting functions. Each of these things are classes in SimPEG and we can just pull them all together to create a new thing; it makes code-reuse very easy. What is interesting is that Types in Julia cannot even inherit from other Types. Instead, Julia has AbstractTypes to include some structure in the Type Tree. This could allow you to create a function that accepts a Number type without caring too much if it is an Int or a Float etc. This is super important for Julia's multiple dispatch system, which loads many functions of the same name that are distinct based on their typed inputs. The canonical example of multiple dispatch is the + operator, which has many different dispatches depending on what is being added. This is done in object oriented languages like Python by overloading methods in a class (e.g. __add__), and then letting the classes figure out how to do things dynamically. In Julia the ideas of multiple inheritance are still being hotly debated and I am not sure how well it would play with their multiple dispatcher, as things could get somewhat ambiguous. So without multiple abstract inheritance of types, we have to make a choice:
Writing it out now, it seems pretty obvious which road to choose, but I wanted methods which acted on either the dimension or on the idea of what the mesh was. Plotting of 2D meshes might be pretty similar, but maybe the differential operators are grouped by TensorMesh, CylMesh, FiniteElementMesh. Unfortunately, we cannot have the best of both worlds at the moment. I chose AbstractTensorMesh and separated dimensions by putting that in the M.cnt.dim property:
This allows us to create a faceDiv method that is specific to the TensorMesh classes, and then have an if-statement over the dimension of the mesh. The entire faceDiv function is below:
It is almost identical to the implementation in Python (just add one), with the notable ease of use of horizontal and vertical concatenation, which is so much easier in Julia. For example, when concatenating the edgeCurl in Python compared to Julia.
In Python:
In Julia:
I found it pretty simple to port the core of the SimPEG meshing functionality over to Julia, and the obvious next step is to package it up and share it with the world.
It was easy. Check out the Julia docs on how to do it, but basically it takes one or two lines of code from inside the Julia environment. I was impressed with how slick this was.
You can use this now if you would like (go to Julia Box):
As a sort of side note, it did actually take a lot of messing around to get the structure of the modules and the file system hooked up, I ended up going with something like this (but I am not totally happy with it):
The SimPEG.jl file was the most difficult to play with, I ended up doing something like this:
I think what it basically does is inject the actual file into whatever name-space you are in. This is interesting, because I do not need references to Utils in the following files, because they are executed from the SimPEG module (which has the Utils in it). Weird.
Another thing that I found difficult to grasp was all the different ways to bring things into a name-space (using, import, importall, export). See the docs for more details; I stared at this page for a long time.
We have done a fair bit of work in SimPEG getting plotting really easy to do. For the TensorMesh these methods are attached directly to the mesh (e.g. M.plotImage(phi)). In Julia, plotting can be done by passing things to Python using PyPlot, which uses matplotlib. So Julia is calling Python to execute things and then returning an image to inject into the notebook (for example). The next question I asked was, why not use all of that SimPEG code that we have?! That is pretty easy actually!:
This code reproduces the SimPEG mesh in the python environment, and then uses it's plotImage method for the vector. So now we can have Julia call Python to plot the image of the dipole that we solved above:
It is certainly nice to bring the libraries functionality with you as we explore this new language! Interestingly, we can also go the other way (Python calling Julia):
I have started to translate some of the SimPEG functionality of the Mesh class over to Julia, and it was a pretty easy translation.
It is certainly nice to have some of the ease of notation back when dealing with vectors and matrices, some of this is cumbersome in Python/scipy.
The lack of OOP in Julia makes sense from a numerical computing side of things, but when you are building a bigger package/project (like SimPEG) these things are crucial to have.
Representing some of the higher level concepts (e.g. DataMisfit, Regularization) doesn't really make sense as a Type, because the primary things in those classes are methods rather than variables.
There seems to be some level of interoperability between the languages, but it is much better in the Julia calling Python direction; this will likely improve in the future.
I am excited about the combination of these languages, and exploring some of the parallel features of Julia in the coming months.