The notebook interface

The IPython -- being rebranded as Jupyter -- notebook interface is becoming a standard for a number of languages other than Python: Julia, Scala, R, Haskell, bash are all getting their kernels in IPython. Since Python allows you to call MATLAB anyway, you can also use the notebook interface for MATLAB if you wish so.

Many features of IPython are independent of the underlying language. The so-called magic functions make it extremely powerful. This are prefixed by a percentage sign. For a quick reference, try


In [1]:
%quickref

For instance, you can benchmark the speed of a function:


In [2]:
%timeit range(1000000)


The slowest run took 4.40 times longer than the fastest. This could mean that an intermediate result is being cached 
1000000 loops, best of 3: 260 ns per loop

You also have direct access to the operating system calls:


In [13]:
!uname -a


Linux localhost 3.17.3-1-ARCH #1 SMP PREEMPT Fri Nov 14 23:13:48 CET 2014 x86_64 GNU/Linux

You can, of course, always access the result of the previous command:


In [14]:
print(_)


['Linux localhost 3.17.3-1-ARCH #1 SMP PREEMPT Fri Nov 14 23:13:48 CET 2014 x86_64 GNU/Linux']

Some other cool features to explore: LaTeX export of notebooks with support for Bibtex -- also works for HTML -- and launching parallel computations in Python interpreters distributed across a cluster.

Symbolic operations

First, let us bypass the debate over Python 2 and 3 by forcing us to write code that functions identically in either version:


In [15]:
from __future__ import print_function, division

We also want nicely formatted output:


In [2]:
from sympy.interactive import printing
printing.init_printing(use_latex='mathjax')

Then we bulk-import everything we might need:


In [3]:
from sympy import *

We define some symbols:


In [4]:
x, y = symbols('x y')

In [5]:
sin(pi*x)/cos(pi*y)


Out[5]:
$$\frac{\sin{\left (\pi x \right )}}{\cos{\left (\pi y \right )}}$$

It renders nicely as $\frac{\sin\pi x}{\cos\pi x}$. Now a symbolic integral:


In [6]:
integrate(pi*sin(x*y), x)


Out[6]:
$$\pi \begin{cases} 0 & \text{for}\: y = 0 \\- \frac{1}{y} \cos{\left (x y \right )} & \text{otherwise} \end{cases}$$

Evaluate this for a particular value of x and y:


In [9]:
integrate(pi*sin(x*y), x).subs([(x, pi), (y, 1)])


Out[9]:
$$\pi$$

SymPy, just like Mathematica, keeps results symbolic as long as possible. If you want a numerical result, you specifically have to request it:


In [25]:
N(integrate(pi*sin(x*y), x).subs([(x, pi), (y, 1)]))


Out[25]:
$$3.14159265358979$$

The quantum physics module is especially helpful. The noncommutative algebra is better than the respective package in Mathematica, especially when it comes to non-Hermitian variables:


In [7]:
from sympy.physics.quantum import *
X = HermitianOperator('X')
Y = Operator('Y')
Dagger(X*Y)


Out[7]:
$$Y^{\dagger} X$$

We can easily define Hamiltonians. For instance, the Hubbard model on a chain is as follows:


In [31]:
t = 1.0
U = 4.0
n_sites = 2
cu = [Operator("%s_%s_u" % ("c", i + 1)) for i in range(n_sites)]
cd = [Operator("%s_%s_d" % ("c", i + 1)) for i in range(n_sites)]
hamiltonian = sum(U*Dagger(cu[r])*cu[r]*Dagger(cd[r])*cd[r] for r in range(n_sites))
hamiltonian += sum(-t*(Dagger(cu[r])*cu[r+1]+Dagger(cu[r+1])*cu[r]
                       +Dagger(cd[r])*cd[r+1]+Dagger(cd[r+1])*cd[r]) for r in range(n_sites-1))
expand(hamiltonian)


Out[31]:
$$- 1.0 c_{1 d}^{\dagger} c_{2 d} + 4.0 c_{1 u}^{\dagger} c_{1 u} c_{1 d}^{\dagger} c_{1 d} - 1.0 c_{1 u}^{\dagger} c_{2 u} - 1.0 c_{2 d}^{\dagger} c_{1 d} - 1.0 c_{2 u}^{\dagger} c_{1 u} + 4.0 c_{2 u}^{\dagger} c_{2 u} c_{2 d}^{\dagger} c_{2 d}$$

List comprehensions

Python was retrofitted with some elements of functional programming, mainly building on ideas coming from Haskell. It nevertheless remains an object-oriented language, but it is highly opportunistic. This approach is a lot like Mathematica, which is quintessentially functional, but you can follow any programming paradigm when using it.

List comprehensions are probably the most used construct in Python from functional programming. In fact, it is considered a Pythonesque way of doing things. It is a quick way of generating transformed lists from other lists: list comprehension is a simple map function in disguise.


In [22]:
[i**2 for i in range(5)]


Out[22]:
$$\left [ 0, \quad 1, \quad 4, \quad 9, \quad 16\right ]$$

The expression on the left inside the list comprehension is similar to a pure function and we can have more complex forms:


In [26]:
[i*j for i in range(1, 4) for j in range(1, 4)]


Out[26]:
$$\left [ 1, \quad 2, \quad 3, \quad 2, \quad 4, \quad 6, \quad 3, \quad 6, \quad 9\right ]$$

We can also include conditionals in the list comprehension:


In [27]:
[i for i in range(30) if i%3 == 0]


Out[27]:
$$\left [ 0, \quad 3, \quad 6, \quad 9, \quad 12, \quad 15, \quad 18, \quad 21, \quad 24, \quad 27\right ]$$

It is easy to emulate the MapIndex function of Mathematica:


In [29]:
[[a, i] for i, a in enumerate([sqrt(2), pi, x])]


Out[29]:
$$\left [ \left [ \sqrt{2}, \quad 0\right ], \quad \left [ \pi, \quad 1\right ], \quad \left [ x, \quad 2\right ]\right ]$$

List comprehension does not actually return a list, it returns an iterator. Iterators are essentially generating functions for list and they are very important to functional programming in Python. Iterators have a next() function to retrieve subsequent elements, making them very easy to loop over. For instance:


In [16]:
l = iter([1, 2, 3])
next(l)


Out[16]:
$$1$$

This is, of course, not very useful, as we could have just used the list itself. List comprehensions are a more sensible way of getting iterators. Another way of creating an iterator is by defining a function that returns values through yield rather than through return. This allows an internal state for the function and lets it continue where it left it off. For example:


In [17]:
def squares(N):
    for i in range(N):
        yield(i**2)

In [19]:
for j in squares(5):
    print(j)


0
1
4
9
16

There is no shortage of useful examples for using iterators. Take combinatoric functions, for instance:


In [24]:
import itertools
for combination in itertools.combinations([1, 2, 3, 4, 5], 2):
    print(combination)


(1, 2)
(1, 3)
(1, 4)
(1, 5)
(2, 3)
(2, 4)
(2, 5)
(3, 4)
(3, 5)
(4, 5)