Most often, the Julia compiler does a very good job of infering the types of variables that are unspecified.\n", "There are a few specific instances where declarations are helpful, these are covered in detail in Julia's [Performance Tips](https://docs.julialang.org/en/v1/manual/performance-tips/index.html#Type-declarations).\n", "The main advantage of type declaration is most often readability and extended functionality.\n", "More information on types and the possibilities this offers, is found at Julia's documentation on [types](https://docs.julialang.org/en/v1/manual/types)." ] }, { "cell_type": "code", "execution_count": 6, "metadata": {}, "outputs": [ { "ename": "InexactError", "evalue": "InexactError: Int64(1.5)", "output_type": "error", "traceback": [ "InexactError: Int64(1.5)", "", "Stacktrace:", " [1] Int64 at ./float.jl:709 [inlined]", " [2] convert(::Type{Int64}, ::Float64) at ./number.jl:7", " [3] demonstrate_types() at ./In[6]:3", " [4] top-level scope at In[6]:5" ] } ], "source": [ "function demonstrate_types()\n", " x::Int = 1 # Define x as an integer\n", " x = x + 0.5 # Causes error, as 1.5 is not an integer.\n", "end\n", "\n", "demonstrate_types()" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "There is too much to be said about Julia's powerful type-system to cover it all here.\n", "It suffices to say that Julia's type-system is extremely versatile, and well worth looking into for the serious Julia developer.\n", "\n", "Below is a short snippet courtesy of Julia's [documentation](https://docs.julialang.org/en/v1/manual/types/) which demonstrates one of the possibilities offered." ] }, { "cell_type": "code", "execution_count": 7, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "norm (generic function with 1 method)" ] }, "execution_count": 7, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Courtesy of Julia's documentation.\n", "\n", "# Define an object Point, with x- and y-coordinates of some unspecified type T.\n", "struct Point{T}\n", " x::T\n", " y::T\n", "end\n", "\n", "# Define the norm of point, for all points where the type T is a subtype of Real (Int, Float, etc.).\n", "function norm(p::Point{<:Real})\n", " sqrt(p.x^2 + p.y^2)\n", "end" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "**4.** Two print functions.\n", "Using `print` in Julia does not append a newline.\n", "The solution is simply to use `println`, short for \"print newline\".\n", "\n", "**5.** Strict difference between String and Char.\n", "In Python, there is no difference between Strings and Chars.\n", "Julia, like many programming languages, differentiates between these two.\n", "A String is delimited by double quotation marks, as in `\"my string\"`.\n", "A Char is, put simply, a singe character, and is delimited by single quotation marks, as in `'C'`." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "### Why the exclamation mark?\n", "When reading Julia code, you will sometimes see function names ending with an exclamation mark, for example `push!(my_vector, my_element)`.\n", "In our notebook on the Bak Sneppen model, for example, we have such a function, called `simulate!`.\n", "This is a naming convention inherited from older languages, where one appends an exclamation mark to the function name on functions that alter one or more of its arguments.\n", "For example, one could imagine having two variants of a function for sorting a list, one with an exclamation mark and one without.\n", "One would then expect the latter to return a new sorted list, leaving the original list as it were, while the former would be expected to sort the list in-place, ie. altering the original list.\n", "This specific example corresponds to `sort` and `sorted` in Python, where the latter returns a new list which is sorted and the former sorts the list in-place.\n", "\n", "This is especially relevant for many numerical solvers, where one in an effort to save memory, will use the memory allocated to the arguments in the process of solving the system.\n", "As a result, many solvers, especially in linear algebra, come in two variants, one with an exclamation mark and one without." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Linear algebra is part of the language itself\n", "In Python, we are so used to thinking about NumPy as an intrinsic part of Python itself, that we might forget that it is simply a package.\\\n", "In Julia, linear algebra is built right into the language.\n", "Now, some might argue that linear algebra should not be included in an introductory article, but we beg to differ.\n", "\n", "For representing vectors and matrices, we will use the `Array`-object.\n", "The syntax for creating these arrays is quite intuitive.\n", "Elements of the same row is separated by a space, columns are separated by semicolon.\n", "Everything is wrapped in square brackets.\n", "Let's first show a simple example of solving a matrix equation $Ax = b$.\n", "We use Julia's \"matrix division\" operator `\\`, defined such that `x=A\\b => Ax==b`.\n", "For those experienced with Matlab, this syntax should be familiar." ] }, { "cell_type": "code", "execution_count": 8, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ "x: [27.000000000000096, -22.00000000000008, 6.00000000000002]\n", "Ax = [0.9999999999999929, 2.0, 3.0]\n" ] }, { "name": "stderr", "output_type": "stream", "text": [ "WARNING: using LinearAlgebra.norm in module Main conflicts with an existing identifier.\n" ] } ], "source": [ "# Firstly, we must include the linear algebra module.\n", "using LinearAlgebra\n", "\n", "A = [1 2 3; 0 1 4; 5 6 0]\n", "b = [1; 2; 3]\n", "#=\n", "Note: We here use Julia's syntax for \n", "multiline comments, '#= ... =#'.\n", "\n", " 1 2 3\n", "A = 0 1 4\n", " 5 6 0\n", ",\n", " 1\n", "b = 2\n", " 3\n", "\n", "Solve Ax=b.\n", "=#\n", "x = A \\ b\n", "println(\"x: \", x)\n", "# Verify\n", "println(\"Ax = \", A * x)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "We see that our solution is correct within some round-off error.\n", "\n", "Note: Now, some might say that it is strange that we put emphasis on the fact that Julia has linear algebra built in, and then seeing that we had to write `using LinearAlgebra`.\n", "This is simply for importing the module, which must not be confused with it being a package in itself." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Julia's linear algebra module functions as a wrapper to the powerful LAPACK library (same as used by NumPy).\n", "For specialized matrices, such as symmetric, hermitian, upper triangular, Julia can use more specialized, and thus efficient, methods.\n", "For example, we might use the specialized method for finding eigenvalues and eigenvectors of a symmetric tridiagonal matrix." ] }, { "cell_type": "code", "execution_count": 9, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "Eigen{Float64,Float64,Array{Float64,2},Array{Float64,1}}\n", "eigenvalues:\n", "4-element Array{Float64,1}:\n", " 0.381966011250106\n", " 2.381966011250106\n", " 2.618033988749895\n", " 4.618033988749895\n", "eigenvectors:\n", "4×4 Array{Float64,2}:\n", " 0.850651 0.0 0.525731 0.0 \n", " -0.525731 0.0 0.850651 0.0 \n", " 0.0 0.850651 0.0 0.525731\n", " 0.0 -0.525731 0.0 0.850651" ] }, "execution_count": 9, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Construct a symmetric tridiagonal matrix\n", "diagonal = [1, 2, 3, 4]\n", "off_diagonal = [1, 0, 1]\n", "A = SymTridiagonal(diagonal, off_diagonal)\n", "#=\n", " 1 1 0 0\n", "A = 1 2 0 0\n", " 0 0 3 1\n", " 0 0 1 4\n", "=#\n", "\n", "# eigen calls the correct method by looking at the type of the input, here LinearAlgebra.SymTridiagonal.\n", "eigen(A)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Why bother?\n", "\n", "One important point is of course, why bother?\n", "What is the benefit of using Julia over Python?\n", "The answer is _speed_.\n", "\n", "In contrast to Python, Julia is a compiled language$^1$, and as a consequence one can expect huge differences in performance between Julia and Python.\n", "We will now show a simple example:\n", "Consider some lattice, or grid if you want, of size $N \\times N$.\n", "At each lattice point $(i,j)$, we attribute a value $\\sigma_{ij}$.\n", "For this system, we say that the energy of one lattice point, is its value times the sum of its neighbors: $\\sigma_{ij} (\\sigma_{\\text{right}} + \\sigma_{\\text{left}} + \\sigma_\\text{up} + \\sigma_\\text{down})$.\n", "To find the total energy of the system, we then iterate over each lattice point, and sum the energy of each point.\n", "This is a type of model one encounters often in Physics, for example in a nearest-neigbor spin-spin interaction model.\n", "Calculations on such systems quickly become very computationally demanding as our system grows, thus efficient code is of the essence.\n", "\n", "_Note: If you are not interested in the physics of this example, and simply want to learn Julia, you may disregard the explanation above and keep on reading without any loss._" ] }, { "cell_type": "code", "execution_count": 10, "metadata": {}, "outputs": [ { "data": { "text/plain": [ "energy (generic function with 1 method)" ] }, "execution_count": 10, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Create some grid\n", "N = 1000\n", "grid = rand(N, N) # Populate our grid with random numbers between 0 and 1.\n", "\n", "function energy(grid)\n", " energy = 0\n", " # This notation gives us two for-loops,\n", " # an outer loop over i and\n", " # an inner loop over j.\n", " for i in 2:N-1, j in 2:N-1\n", " right = grid[i+1, j]\n", " left = grid[i-1, j]\n", " up = grid[i, j+1]\n", " down = grid[i, j-1]\n", " \n", " # Nearest neighbor interaction\n", " nn_interaction = grid[i,j] * (right + left + up + down)\n", " energy += nn_interaction\n", " end\n", " return energy\n", "end" ] }, { "cell_type": "code", "execution_count": 12, "metadata": {}, "outputs": [ { "name": "stdout", "output_type": "stream", "text": [ " 0.221708 seconds (11.89 M allocations: 196.658 MiB, 5.63% gc time)\n" ] }, { "data": { "text/plain": [ "995700.5089307781" ] }, "execution_count": 12, "metadata": {}, "output_type": "execute_result" } ], "source": [ "# Measure the time it takes to execute the function\n", "@time energy(grid)" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "In this example, the function takes 0.22s on our machine.\n", "In a very similar implementation in Python, shown below, the execution time on the same computer was measured to 1.21s, so 6 times slower! That is the difference between waiting one day and almost a week for some simulation!\n", "\n", "```Python\n", "# A similar implementation in Python.\n", "import numpy as np\n", "\n", "N = 1000\n", "grid = np.random.rand(N,N)\n", "\n", "def energy(grid):\n", " energy = 0\n", " for i in range(1, N-1):\n", " for j in range(1, N-1):\n", " right = grid[i+1, j]\n", " left = grid[i-1, j]\n", " up = grid[i, j+1]\n", " down = grid[i, j-1]\n", " \n", " # Nearest neighbor interaction\n", " nn_interaction = 4*grid[i,j] - (right + left + up + down)\n", " energy += nn_interaction\n", " return energy\n", "```" ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "Admittedly, the implementation could be made more clever.\n", "Also, one could use more NumPy-functionality to speed up the Python implementation.\n", "The example does however demonstrate that Julia may offer an advantage in performance, especially in cases where one is unable to replace for-loops with NumPy functionality.\n", "In our notebook on [Monte Carlo simulations on the Ising model](https://nbviewer.jupyter.org/urls/www.numfys.net/media/notebooks/Monte_Carlo_Ising.ipynb), we showed how replacing some inner loops with Fortran could improve performance dramatically.\n", "That is also an excelent example of a situation where we could have used Julia." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "# Closing remarks\n", "This notebook is in no way a complete guide to the Julia programming language.\n", "We do however hope that it may serve as a starting point to Julia." ] }, { "cell_type": "markdown", "metadata": {}, "source": [ "## Footnote\n", "$^1$ Julia is a just-in-time compiled language. We will not discuss the meaning of that here, but as a mental image think of it as a middle ground between Python, which is not compiled at all, and C-like languages where one creates a new file that is the machine code to be executed." ] } ], "metadata": { "jupytext": { "formats": "ipynb,md" }, "kernelspec": { "display_name": "Julia 1.3.1", "language": "julia", "name": "julia-1.3" }, "language_info": { "file_extension": ".jl", "mimetype": "application/julia", "name": "julia", "version": "1.3.1" } }, "nbformat": 4, "nbformat_minor": 4 }