diff --git a/pfaffian.py b/pfaffian.py
new file mode 100644
index 0000000000000000000000000000000000000000..88d3f849a5d5d196285cbe7619eff1bb0e1e84cb
--- /dev/null
+++ b/pfaffian.py
@@ -0,0 +1,484 @@
+"""A package for computing Pfaffians"""
+
+
+import numpy as np
+import scipy.linalg as la
+import scipy.sparse as sp
+import math
+import cmath
+
+
+def householder_real(x):
+    """(v, tau, alpha) = householder_real(x)
+
+    Compute a Householder transformation such that
+    (1-tau v v^T) x = alpha e_1
+    where x and v a real vectors, tau is 0 or 2, and
+    alpha a real number (e_1 is the first unit vector)
+    """
+
+    assert x.shape[0] > 0
+
+    sigma = x[1:] @ x[1:]
+
+    if sigma == 0:
+        return (np.zeros(x.shape[0]), 0, x[0])
+    else:
+        norm_x = math.sqrt(x[0]**2+sigma)
+
+        v = x.copy()
+
+        # depending on whether x[0] is positive or negatvie
+        # choose the sign
+        if x[0] <= 0:
+            v[0] -= norm_x
+            alpha = +norm_x
+        else:
+            v[0] += norm_x
+            alpha = -norm_x
+
+        v /= np.linalg.norm(v)
+
+        return (v, 2, alpha)
+
+
+def householder_complex(x):
+    """(v, tau, alpha) = householder_real(x)
+
+    Compute a Householder transformation such that
+    (1-tau v v^T) x = alpha e_1
+    where x and v a complex vectors, tau is 0 or 2, and
+    alpha a complex number (e_1 is the first unit vector)
+    """
+    assert x.shape[0] > 0
+
+    sigma = np.conj(x[1:]) @ x[1:]
+
+    if sigma == 0:
+        return (np.zeros(x.shape[0]), 0, x[0])
+    else:
+        norm_x = cmath.sqrt(x[0].conjugate()*x[0]+sigma)
+
+        v = x.copy()
+
+        phase = cmath.exp(1j*math.atan2(x[0].imag, x[0].real))
+
+        v[0] += phase*norm_x
+
+        v /= np.linalg.norm(v)
+
+    return (v, 2, -phase*norm_x)
+
+
+def skew_tridiagonalize(A, overwrite_a=False, calc_q=True):
+    """ T, Q = skew_tridiagonalize(A, overwrite_a, calc_q=True)
+
+    or
+
+    T = skew_tridiagonalize(A, overwrite_a, calc_q=False)
+
+    Bring a real or complex skew-symmetric matrix (A=-A^T) into
+    tridiagonal form T (with zero diagonal) with a orthogonal
+    (real case) or unitary (complex case) matrix U such that
+    A = Q T Q^T
+    (Note that Q^T and *not* Q^dagger also in the complex case)
+
+    A is overwritten if overwrite_a=True (default: False), and
+    Q only calculated if calc_q=True (default: True)
+    """
+
+    # Check if matrix is square
+    assert A.shape[0] == A.shape[1] > 0
+    # Check if it's skew-symmetric
+    assert abs((A+A.T).max()) < 1e-14
+
+    n = A.shape[0]
+    A = np.asarray(A)  # the slice views work only properly for arrays
+
+    # Check if we have a complex data type
+    if np.issubdtype(A.dtype, np.complexfloating):
+        householder = householder_complex
+    elif not np.issubdtype(A.dtype, np.number):
+        raise TypeError("pfaffian() can only work on numeric input")
+    else:
+        householder = householder_real
+
+    if not overwrite_a:
+        A = A.copy()
+
+    if calc_q:
+        Q = np.eye(A.shape[0], dtype=A.dtype)
+
+    for i in range(A.shape[0]-2):
+        # Find a Householder vector to eliminate the i-th column
+        v, tau, alpha = householder(A[i+1:, i])
+        A[i+1, i] = alpha
+        A[i, i+1] = -alpha
+        A[i+2:, i] = 0
+        A[i, i+2:] = 0
+
+        # Update the matrix block A(i+1:N,i+1:N)
+        w = tau * A[i+1:, i+1:] @ v.conj()
+        A[i+1:, i+1:] += np.outer(v, w)-np.outer(w, v)
+
+        if calc_q:
+            # Accumulate the individual Householder reflections
+            # Accumulate them in the form P_1*P_2*..., which is
+            # (..*P_2*P_1)^dagger
+            y = tau * Q[:, i+1:] @ v
+            Q[:, i+1:] -= np.outer(y, v.conj())
+
+    if calc_q:
+        return (np.asmatrix(A), np.asmatrix(Q))
+    else:
+        return np.asmatrix(A)
+
+
+def skew_LTL(A, overwrite_a=False, calc_L=True, calc_P=True):
+    """ T, L, P = skew_LTL(A, overwrite_a, calc_q=True)
+
+    Bring a real or complex skew-symmetric matrix (A=-A^T) into
+    tridiagonal form T (with zero diagonal) with a lower unit
+    triangular matrix L such that
+    P A P^T= L T L^T
+
+    A is overwritten if overwrite_a=True (default: False),
+    L and P only calculated if calc_L=True or calc_P=True,
+    respectively (default: True).
+    """
+
+    # Check if matrix is square
+    assert A.shape[0] == A.shape[1] > 0
+    # Check if it's skew-symmetric
+    assert abs((A+A.T).max()) < 1e-14
+
+    n = A.shape[0]
+    A = np.asarray(A)  # the slice views work only properly for arrays
+
+    if not overwrite_a:
+        A = A.copy()
+
+    if calc_L:
+        L = np.eye(n, dtype=A.dtype)
+
+    if calc_P:
+        Pv = np.arange(n)
+
+    for k in range(n-2):
+        # First, find the largest entry in A[k+1:,k] and
+        # permute it to A[k+1,k]
+        kp = k+1+np.abs(A[k+1:, k]).argmax()
+
+        # Check if we need to pivot
+        if kp != k+1:
+            # interchange rows k+1 and kp
+            temp = A[k+1, k:].copy()
+            A[k+1, k:] = A[kp, k:]
+            A[kp, k:] = temp
+
+            # Then interchange columns k+1 and kp
+            temp = A[k:, k+1].copy()
+            A[k:, k+1] = A[k:, kp]
+            A[k:, kp] = temp
+
+            if calc_L:
+                # permute L accordingly
+                temp = L[k+1, 1:k+1].copy()
+                L[k+1, 1:k+1] = L[kp, 1:k+1]
+                L[kp, 1:k+1] = temp
+
+            if calc_P:
+                # accumulate the permutation matrix
+                temp = Pv[k+1]
+                Pv[k+1] = Pv[kp]
+                Pv[kp] = temp
+
+        # Now form the Gauss vector
+        if A[k+1, k] != 0.0:
+            tau = A[k+2:, k].copy()
+            tau /= A[k+1, k]
+
+            # clear eliminated row and column
+            A[k+2:, k] = 0.0
+            A[k, k+2:] = 0.0
+
+            # Update the matrix block A(k+2:,k+2)
+            A[k+2:, k+2:] += np.outer(tau, A[k+2:, k+1])
+            A[k+2:, k+2:] -= np.outer(A[k+2:, k+1], tau)
+
+            if calc_L:
+                L[k+2:, k+1] = tau
+
+    if calc_P:
+        # form the permutation matrix as a sparse matrix
+        P = sp.csr_matrix((np.ones(n), (np.arange(n), Pv)))
+
+    if calc_L:
+        if calc_P:
+            return (np.asmatrix(A), np.asmatrix(L), P)
+        else:
+            return (np.asmatrix(A), np.asmatrix(L))
+    else:
+        if calc_P:
+            return (np.asmatrix(A), P)
+        else:
+            return np.asmatrix(A)
+
+
+def pfaffian(A, overwrite_a=False, method='P', sign_only=False):
+    """ pfaffian(A, overwrite_a=False, method='P')
+
+    Compute the Pfaffian of a real or complex skew-symmetric
+    matrix A (A=-A^T). If overwrite_a=True, the matrix A
+    is overwritten in the process. This function uses
+    either the Parlett-Reid algorithm (method='P', default),
+    or the Householder tridiagonalization (method='H')
+    """
+    # Check if matrix is square
+    assert A.shape[0] == A.shape[1] > 0
+    # Check if it's skew-symmetric
+    assert abs((A+A.T).max()) < 1e-14, abs((A+A.T).max())
+    # Check that the method variable is appropriately set
+    assert method == 'P' or method == 'H'
+    if method == 'H' and sign_only:
+        raise Exception("Use `method='P'` when using `sign_only=True`")
+    if method == 'P':
+        return pfaffian_LTL(A, overwrite_a, sign_only)
+    else:
+        return pfaffian_householder(A, overwrite_a)
+
+
+def pfaffian_LTL(A, overwrite_a=False, sign_only=False):
+    """ pfaffian_LTL(A, overwrite_a=False)
+
+    Compute the Pfaffian of a real or complex skew-symmetric
+    matrix A (A=-A^T). If overwrite_a=True, the matrix A
+    is overwritten in the process. This function uses
+    the Parlett-Reid algorithm.
+    """
+    # Check if matrix is square
+    assert A.shape[0] == A.shape[1] > 0
+    # Check if it's skew-symmetric
+    assert abs((A+A.T).max()) < 1e-14
+
+    n = A.shape[0]
+    A = np.asarray(A)  # the slice views work only properly for arrays
+
+    # Quick return if possible
+    if n % 2 == 1:
+        return 0
+
+    if not overwrite_a:
+        A = A.copy()
+
+    pfaffian_val = 1.0
+
+    for k in range(0, n-1, 2):
+        # First, find the largest entry in A[k+1:,k] and
+        # permute it to A[k+1,k]
+        kp = k+1+np.abs(A[k+1:, k]).argmax()
+
+        # Check if we need to pivot
+        if kp != k+1:
+            # interchange rows k+1 and kp
+            temp = A[k+1, k:].copy()
+            A[k+1, k:] = A[kp, k:]
+            A[kp, k:] = temp
+
+            # Then interchange columns k+1 and kp
+            temp = A[k:, k+1].copy()
+            A[k:, k+1] = A[k:, kp]
+            A[k:, kp] = temp
+
+            # every interchange corresponds to a "-" in det(P)
+            pfaffian_val *= -1
+
+        # Now form the Gauss vector
+        if A[k+1, k] != 0.0:
+            tau = A[k, k+2:].copy()
+            tau /= A[k, k+1]
+
+            if sign_only:
+                pfaffian_val *= np.sign(A[k, k+1])
+            else:
+                pfaffian_val *= A[k, k+1]
+
+            if k+2 < n:
+                # Update the matrix block A(k+2:,k+2)
+                A[k+2:, k+2:] += np.outer(tau, A[k+2:, k+1])
+                A[k+2:, k+2:] -= np.outer(A[k+2:, k+1], tau)
+        else:
+            # if we encounter a zero on the super/subdiagonal, the
+            # Pfaffian is 0
+            return 0.0
+
+    return pfaffian_val
+
+
+def pfaffian_householder(A, overwrite_a=False):
+    """ pfaffian(A, overwrite_a=False)
+
+    Compute the Pfaffian of a real or complex skew-symmetric
+    matrix A (A=-A^T). If overwrite_a=True, the matrix A
+    is overwritten in the process. This function uses the
+    Householder tridiagonalization.
+
+    Note that the function pfaffian_schur() can also be used in the
+    real case. That function does not make use of the skew-symmetry
+    and is only slightly slower than pfaffian_householder().
+    """
+
+    # Check if matrix is square
+    assert A.shape[0] == A.shape[1] > 0
+    # Check if it's skew-symmetric
+    assert abs((A+A.T).max()) < 1e-14
+
+    n = A.shape[0]
+
+    # Quick return if possible
+    if n % 2 == 1:
+        return 0
+
+    # Check if we have a complex data type
+    if np.issubdtype(A.dtype, np.complexfloating):
+        householder = householder_complex
+    elif not np.issubdtype(A.dtype, np.number):
+        raise TypeError("pfaffian() can only work on numeric input")
+    else:
+        householder = householder_real
+
+    A = np.asarray(A)  # the slice views work only properly for arrays
+
+    if not overwrite_a:
+        A = A.copy()
+
+    pfaffian_val = 1.
+
+    for i in range(A.shape[0]-2):
+        # Find a Householder vector to eliminate the i-th column
+        v, tau, alpha = householder(A[i+1:, i])
+        A[i+1, i] = alpha
+        A[i, i+1] = -alpha
+        A[i+2:, i] = 0
+        A[i, i+2:] = 0
+
+        # Update the matrix block A(i+1:N,i+1:N)
+        w = tau * A[i+1:, i+1:] @ v.conj()
+        A[i+1:, i+1:] += np.outer(v, w) - np.outer(w, v)
+
+        if tau != 0:
+            pfaffian_val *= 1-tau
+        if i % 2 == 0:
+            pfaffian_val *= -alpha
+
+    pfaffian_val *= A[n-2, n-1]
+
+    return pfaffian_val
+
+
+def pfaffian_schur(A, overwrite_a=False):
+    """Calculate Pfaffian of a real antisymmetric matrix using
+    the Schur decomposition. (Hessenberg would in principle be faster,
+    but scipy-0.8 messed up the performance for scipy.linalg.hessenberg()).
+
+    This function does not make use of the skew-symmetry of the matrix A,
+    but uses a LAPACK routine that is coded in FORTRAN and hence faster
+    than python. As a consequence, pfaffian_schur is only slightly slower
+    than pfaffian().
+    """
+
+    assert np.issubdtype(A.dtype, np.number) and not np.issubdtype(
+        A.dtype, np.complexfloating)
+
+    assert A.shape[0] == A.shape[1] > 0
+
+    assert abs(A + A.T).max() < 1e-14
+
+    # Quick return if possible
+    if A.shape[0] % 2 == 1:
+        return 0
+
+    (t, z) = la.schur(A, output='real', overwrite_a=overwrite_a)
+    l = np.diag(t, 1)
+    return np.prod(l[::2]) * la.det(z)
+
+
+def pfaffian_sign(A, overwrite_a=False):
+    """ pfaffian(A, overwrite_a=False, method='P')
+
+    Compute the Pfaffian of a real or complex skew-symmetric
+    matrix A (A=-A^T). If overwrite_a=True, the matrix A
+    is overwritten in the process. This function uses
+    either the Parlett-Reid algorithm (method='P', default),
+    or the Householder tridiagonalization (method='H')
+    """
+    # Check if matrix is square
+    assert A.shape[0] == A.shape[1] > 0
+    # Check if it's skew-symmetric
+    assert abs((A+A.T).max()) < 1e-14, abs((A+A.T).max())
+
+    return pfaffian_LTL_sign(A, overwrite_a)
+
+
+def pfaffian_LTL_sign(A, overwrite_a=False):
+    """MODIFIED FROM pfaffian_LTL(A, overwrite_a=False)
+
+    Compute the Pfaffian of a real or complex skew-symmetric
+    matrix A (A=-A^T). If overwrite_a=True, the matrix A
+    is overwritten in the process. This function uses
+    the Parlett-Reid algorithm.
+    """
+    # Check if matrix is square
+    assert A.shape[0] == A.shape[1] > 0
+    # Check if it's skew-symmetric
+    assert abs((A+A.T).max()) < 1e-14
+
+    n = A.shape[0]
+    A = np.asarray(A)  # the slice views work only properly for arrays
+
+    # Quick return if possible
+    if n % 2 == 1:
+        return 0
+
+    if not overwrite_a:
+        A = A.copy()
+
+    pfaffian_val = 1.0
+
+    for k in range(0, n-1, 2):
+        # First, find the largest entry in A[k+1:,k] and
+        # permute it to A[k+1,k]
+        kp = k+1+np.abs(A[k+1:, k]).argmax()
+
+        # Check if we need to pivot
+        if kp != k+1:
+            # interchange rows k+1 and kp
+            temp = A[k+1, k:].copy()
+            A[k+1, k:] = A[kp, k:]
+            A[kp, k:] = temp
+
+            # Then interchange columns k+1 and kp
+            temp = A[k:, k+1].copy()
+            A[k:, k+1] = A[k:, kp]
+            A[k:, kp] = temp
+
+            # every interchange corresponds to a "-" in det(P)
+            pfaffian_val *= -1
+
+        # Now form the Gauss vector
+        if A[k+1, k] != 0.0:
+            tau = A[k, k+2:].copy()
+            tau /= A[k, k+1]
+
+            pfaffian_val *= A[k, k+1]
+
+            if k+2 < n:
+                # Update the matrix block A(k+2:,k+2)
+                A[k+2:, k+2:] += np.outer(tau, A[k+2:, k+1])
+                A[k+2:, k+2:] -= np.outer(A[k+2:, k+1], tau)
+        else:
+            # if we encounter a zero on the super/subdiagonal, the
+            # Pfaffian is 0
+            return 0.0
+
+    return pfaffian_val