\n",
+ " \n",
+ "**STEPS**\n",
+ " \n",
+ "* Input: a time series of stock price; Output: a time-evolution of topological properties.\n",
+ "\n",
+ "* Preparing point cloud\n",
+ " * Apply Taken's embedding.\n",
+ " * Apply a sliding window for obatining a time-varying point-cloud.\n",
+ "* Building Laplacian\n",
+ " * Construct the Vietoris-Rips (VR) complex from the point cloud using [`GUDHI`](https://gudhi.inria.fr/).\n",
+ " * Build the boudnary operator of this complex.\n",
+ " * Build the Laplacian matrix based on the boundary operators, then pad it and rescale it.\n",
+ "* Applying quantum phase estimation\n",
+ " * Use Quantum Phase Estimation (QPE) to find the number non-zero eigenvalues of the Laplacian matrix. Round up the results and get the Betti numbers.\n",
+ " * Vary the resolution threshold and obtain a series of Betti numbers, which are the Betti curves.\n",
+ "* Detecting financial market crashes\n",
+ " * Find the relation between Betti numbers and financial market crashes.\n",
+ " \n",
+ "
NOTE:\n",
+ "\n",
+ "In the coding process, it is recommended to start with the following initial values for the variables: \n",
+ "\n",
+ "* `N = 4` # dimension of vectors\n",
+ "* `d = 5` # time delay\n",
+ "* `w = 5` # window size\n",
+ "* `epsilon = 0.1` # resolution threshold\n",
+ "* `q = 3` # number of precision qubits\n",
+ "\n",
+ "However, you will be tasked with determining the optimal values later in this challenge.\n",
+ "\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1656ba25",
+ "metadata": {},
+ "source": [
+ "### Step 0: Loading data\n",
+ "\n",
+ "\n",
+ "To assess the practical applicability of TDA with quantum techniques, we will analyze a small dataset of the S&P 500 index from the period surrounding the 2008 financial crisis. You may find the associated file: *SP500.csv*"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 66,
+ "id": "4ec880cd",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
\n",
+ "
0
\n",
+ "
\n",
+ " \n",
+ " \n",
+ "
\n",
+ "
0
\n",
+ "
1253.7550
\n",
+ "
\n",
+ "
\n",
+ "
1
\n",
+ "
1269.8250
\n",
+ "
\n",
+ "
\n",
+ "
2
\n",
+ "
1284.1850
\n",
+ "
\n",
+ "
\n",
+ "
3
\n",
+ "
1274.1100
\n",
+ "
\n",
+ "
\n",
+ "
4
\n",
+ "
1279.9900
\n",
+ "
\n",
+ "
\n",
+ "
...
\n",
+ "
...
\n",
+ "
\n",
+ "
\n",
+ "
78
\n",
+ "
776.4250
\n",
+ "
\n",
+ "
\n",
+ "
79
\n",
+ "
834.2050
\n",
+ "
\n",
+ "
\n",
+ "
80
\n",
+ "
857.4500
\n",
+ "
\n",
+ "
\n",
+ "
81
\n",
+ "
864.3525
\n",
+ "
\n",
+ "
\n",
+ "
82
\n",
+ "
889.2825
\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
83 rows × 1 columns
\n",
+ "
"
+ ],
+ "text/plain": [
+ " 0\n",
+ "0 1253.7550\n",
+ "1 1269.8250\n",
+ "2 1284.1850\n",
+ "3 1274.1100\n",
+ "4 1279.9900\n",
+ ".. ...\n",
+ "78 776.4250\n",
+ "79 834.2050\n",
+ "80 857.4500\n",
+ "81 864.3525\n",
+ "82 889.2825\n",
+ "\n",
+ "[83 rows x 1 columns]"
+ ]
+ },
+ "execution_count": 66,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "import pandas as pd\n",
+ "import numpy as np\n",
+ "\n",
+ "df = pd.read_csv(\"sp500_full.csv\", header=None)\n",
+ "dfsmall=pd.read_csv(\"sp500.csv\", header=None)\n",
+ "df\n",
+ "dfsmall\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 68,
+ "id": "240232aa-4a72-496c-8ab3-30f6a2aacc5b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "time_series = np.log(df[1]).to_numpy().squeeze()\n",
+ "time_series_small = np.log(dfsmall[0]).to_numpy().squeeze()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "b4597392",
+ "metadata": {},
+ "source": [
+ "### Step 1: Preparing point cloud\n",
+ "\n",
+ "**Instruction:**\n",
+ "\n",
+ "Consider a time series $X = \\{x_0, x_1, \\ldots, x_{L-1}\\}$ of numerical values of length $L$ as input. We choose and fix an embedded dimension $N$ and a time-delay $d \\ge 1$. Taken's embedding theorem convert this time series into a series of $N$-dimensional time-delay coordinate vectors $Z=\\{z_0, z_1, \\dots, z_{L-1-(N-1)d}\\}$:\n",
+ "\n",
+ "$$\n",
+ "\\begin{align*}\n",
+ "z_0 =& (x_0, x_d, \\ldots, x_{(N-1)d}),\\\\\n",
+ "z_1 =& (x_1, x_{1+d}, \\ldots, x_{1+(N-1)d}),\\\\\n",
+ "\\vdots\\\\\n",
+ "z_t =& (x_t, x_{t+d}, \\ldots, x_{t+(N-1)d}),\\\\\n",
+ "\\vdots&\\\\\n",
+ "z_{L-1-(N-1)d} =& (x_{L-1-(N-1)d}, x_{L-1-(N-2)d}, \\ldots, x_{L-1}).\n",
+ "\\end{align*}\n",
+ "$$\n",
+ "\n",
+ "To detect qualitative changes along a time series, we apply a sliding window of size $w$ and assess how topological properties change along the sliding window. For a proper size, $w$ needs to fullfill $N \\ll w \\ll L$. The sliding window gives a time-varying point cloud embedded in $\\mathcal{R}^N$:\n",
+ "\n",
+ "$$\n",
+ "Z^t = \\{z_t, z_{t+1}, \\ldots, z_{t+w-1}\\}, \\quad \\text{for } t \\in \\{0, \\ldots, K-1\\}\n",
+ "$$\n",
+ "\n",
+ "**Action:**\n",
+ "\n",
+ "Following Takens' embedding theorem, transform the `time_series` into a series of $ N $-dimensional vectors. Afterward, apply a sliding window to these vectors and obtain a time-varying point cloud.\n",
+ "\n",
+ "**Answer:**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 70,
+ "id": "7b701be6",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "N = 3, w = 5, L = 5536\n"
+ ]
+ }
+ ],
+ "source": [
+ "N = 3 # embedding dimension\n",
+ "d = 1 # time delay\n",
+ "w = 5 # window size\n",
+ "\n",
+ "L = len(time_series)\n",
+ "vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ "\n",
+ "K = L - (N-1)*d - w + 1 # number of windows\n",
+ "Z = np.array([vectors[i:i+w] for i in range(K)])\n",
+ "print(f\"N = {N}, w = {w}, L = {L}\")\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "9376bd0b",
+ "metadata": {},
+ "source": [
+ "
\n",
+ " \n",
+ "BONUS EXERCISE: \n",
+ "\n",
+ "`gtda.time_series.TakensEmbedding` can conduct this transformation. Try avoid using this function and build your own embedding function.\n",
+ "\n",
+ "
NOTE:\n",
+ "\n",
+ "From step 2, we present an example originally provided in the appendix of [Khandelwal's and Chandra's paper](https://arxiv.org/abs/2302.09553). This example can be used to verify your code.\n",
+ "\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c945e850",
+ "metadata": {},
+ "source": [
+ "### Step 2: Building Laplacian\n",
+ "\n",
+ "**Instruction:**\n",
+ "\n",
+ "In this step, we are going to use `GUDHI`, a Python package specialized in TDA and and higher dimensional geometry understanding. For detailed instructions on the functions we will use, please refer to [this website](https://gudhi.inria.fr/python/latest/rips_complex_user.html#).\n",
+ "\n",
+ "A simplicial complex is constructed on the point cloud using `gudhi.RipsComplex` class, followed by its `create_simplex_tree` method. The resolution threshold `epsilon` is set via the `max_edge_length` parameter. This process identifies the connectivity of the complex within the resolution threshold and produces a simplex tree. The simplex tree serves as a general representation of simplicial complexes. Using its `get_filtration` method, simplicies are retrieved as a collection of lists, where elements are grouped based on their connections. Each dimension up to a specified maximum is represented by its respective collection of lists.\n",
+ "\n",
+ "**Example:**\n",
+ "\n",
+ "Here is an example of a simplex tree $\\mathcal{K}$ with a maximum dimension of 2. In its zeroth dimension, each point is a connected component; In the first dimension, 6 line segments connect 6 pairs of points $[1, 2], [1, 3], [2, 3], [3, 4], [3, 5], [4, 5]$; In the second dimension, a filled trangle is formed among points $[1 ,2 ,3]$.\n",
+ "\n",
+ "$$\n",
+ "\\mathcal{K} = [[[1], [2], [3], [4], [5]],[[1, 2], [1, 3], [2, 3], [3, 4], [3, 5], [4, 5]], [[1, 2, 3]]]\n",
+ "$$\n",
+ "\n",
+ "**Action:**\n",
+ "\n",
+ "Build a simplicial complex by applying functions from `GUDHI` on the point cloud obtained in step 1, and extract its simplex tree. It is recommended to store the simplex tree in a format similar to the example provided, i.e., in the format $[S_0, S_1, S_2, \\dots]$, where $S_i$ represents the set of $i$-simplices.\n",
+ "\n",
+ "**Answer:**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 72,
+ "id": "026d6b61",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "num 0-simplices: 5\n",
+ "num 1-simplices: 3\n",
+ "num 2-simplices: 0\n"
+ ]
+ }
+ ],
+ "source": [
+ "import gudhi\n",
+ "\n",
+ "epsilon = 0.03 # maximum edge length\n",
+ "max_dim = 2 # maximum simplex dimension\n",
+ "\n",
+ "all_simplices = []\n",
+ "\n",
+ "point_cloud = Z[0]\n",
+ "\n",
+ "\n",
+ " \n",
+ "def get_simplex_tree(point_cloud, epsilon):\n",
+ " # Simplicial Complex\n",
+ " rips = gudhi.RipsComplex(points=point_cloud, max_edge_length=epsilon)\n",
+ " filtration = rips.create_simplex_tree(max_dimension=max_dim).get_filtration()\n",
+ "\n",
+ " # Extract simplices by dimension\n",
+ " simplex_tree = [[] for _ in range(max_dim + 1)]\n",
+ " for simplex, filtration_value in filtration:\n",
+ " if filtration_value <= epsilon:\n",
+ " dim = len(simplex) - 1\n",
+ " simplex_tree[dim].append(simplex)\n",
+ " return simplex_tree\n",
+ "\n",
+ "simplex_tree = get_simplex_tree(point_cloud, epsilon)\n",
+ "\n",
+ "print(f\"num 0-simplices: {len(simplex_tree[0])}\")\n",
+ "print(f\"num 1-simplices: {len(simplex_tree[1])}\")\n",
+ "print(f\"num 2-simplices: {len(simplex_tree[2])}\")\n",
+ "# for dim, simplices in enumerate(simplex_tree):\n",
+ "# print(f\"{dim}-simplices: {simplices}\")"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "573efefa",
+ "metadata": {},
+ "source": [
+ "**Instruction:**\n",
+ "\n",
+ "Let $S_k$ be the set of $k$-simplicies in the complex $\\mathcal{K}$ with individual simplicies denoted by $s_k ∈ S_k$ written as $[j_0, j_1, \\dots, j_k]$ where $j_i$ is the $i$-th vertex of $s_k$. Note that the vertices are ordered in ascending fashion in the initial point cloud, and this order is kept throughout. The restricted boundary operator $\\partial_k$ is defined on the $k$-simplicies as:\n",
+ "\n",
+ "$$\n",
+ "\\begin{align*}\n",
+ "\\partial_k s_k &= \\sum_{t=0}^{k} (-1)^t [v_0, \\dots, v_{t-1}, v_{t+1}, \\dots, v_k]\\\\\n",
+ "&= \\sum_{t=0}^{k} (-1)^t s_{k-1} (t)\n",
+ "\\end{align*}\n",
+ "$$\n",
+ "\n",
+ "where $s_{k−1}(t)$ is defined as the lower simplex defined from $s_k$ by leaving out the vertex $v_t$. \n",
+ "\n",
+ "**Example:**\n",
+ "\n",
+ "In the simplex tree $\\mathcal{K}$ we have 1 2-simplex and 6 1-simplicies. By leaving out vertice $v_0=1$, $v_1=2$, $v_2=3$, we obtain the lower simplex $s_1=[2, 3]$, $s_2=[1, 3]$, $s_3=[1, 2]$, respectively. Therefore, the boundary operator on the 2-simplicies $\\partial_2$ should be a 6-by-1 matrix:\n",
+ "\n",
+ "$$\n",
+ "\\partial_2 =\n",
+ "\\begin{bmatrix}\n",
+ "1 \\\\\n",
+ "-1 \\\\\n",
+ "1 \\\\\n",
+ "0 \\\\\n",
+ "0 \\\\\n",
+ "0\n",
+ "\\end{bmatrix}\n",
+ "$$\n",
+ "\n",
+ "In the same way, the boundary operator on the 1-simplicies $\\partial_1$ is:\n",
+ "\n",
+ "$$\n",
+ "\\partial_1 =\n",
+ "\\begin{bmatrix}\n",
+ "1 & 1 & 0 & 0 & 0 & 0 \\\\\n",
+ "-1 & 0 & 1 & 0 & 0 & 0 \\\\\n",
+ "0 & -1 & -1 & 1 & 1 & 0 \\\\\n",
+ "0 & 0 & 0 & -1 & 0 & 1 \\\\\n",
+ "0 & 0 & 0 & 0 & -1 & -1\n",
+ "\\end{bmatrix}\n",
+ "$$\n",
+ "\n",
+ "**Action:**\n",
+ "\n",
+ "Define a function that generates the boundary operator for a specified dimension, using a given simplex tree as input.\n",
+ "\n",
+ "**Answer:**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 74,
+ "id": "c9a4b17b-188b-4f47-9739-9434451b8e0c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "simplex_tree = [[[1],[2],[3],[4],[5]],[[1,2],[1,3],[2,3],[3,4],[3,5],[4,5]],[[1,2,3]]]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6d44b576-fb0b-4a50-aa5e-15018e1fc019",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 76,
+ "id": "246e41ce",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Boundary 1: (5, 6)\n",
+ "[[-1 -1 0 0 0 0]\n",
+ " [ 1 0 -1 0 0 0]\n",
+ " [ 0 1 1 -1 -1 0]\n",
+ " [ 0 0 0 1 0 -1]\n",
+ " [ 0 0 0 0 1 1]]\n",
+ "Boundary 2: (6, 1)\n",
+ "[[ 1]\n",
+ " [-1]\n",
+ " [ 1]\n",
+ " [ 0]\n",
+ " [ 0]\n",
+ " [ 0]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "def boundary(k, simplex_tree): # geenrates boundary operator C_{k} -> C_{k-1}\n",
+ " if k == 0:\n",
+ " return None \n",
+ " \n",
+ " sk = simplex_tree[k] # k-simplices\n",
+ " sk_1 = simplex_tree[k-1] # (k-1)-simplices\n",
+ " \n",
+ " boundary = np.zeros((len(sk_1), len(sk)), dtype=int)\n",
+ " \n",
+ " for j, simplex in enumerate(sk):\n",
+ " for i in range(k+1):\n",
+ " face = simplex[:i] + simplex[i+1:]\n",
+ " index = sk_1.index(list(face))\n",
+ " boundary[index, j] = (-1)**i\n",
+ " \n",
+ " return boundary\n",
+ "\n",
+ "boundary1 = boundary(1, simplex_tree)\n",
+ "boundary2 = boundary(2, simplex_tree)\n",
+ "\n",
+ "print(\"Boundary 1:\", boundary1.shape)\n",
+ "print(boundary1)\n",
+ "\n",
+ "print(\"Boundary 2:\", boundary2.shape)\n",
+ "print(boundary2)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "81764f0d",
+ "metadata": {},
+ "source": [
+ "**Instruction:**\n",
+ "\n",
+ "The combinatorial laplacian $\\Delta_k$ is defined as:\n",
+ "\n",
+ "$$\n",
+ "\\Delta_k = \\left( \\partial_k \\right)^\\dagger \\partial_k \n",
+ "+ \\partial_{k+1} \\left( \\partial_{k+1} \\right)^\\dagger\n",
+ "$$\n",
+ "\n",
+ "The QPE algorithm will be used to estimate the number of zero eigenvalues of the Laplacian matrix. Since its exponential matrix serves as the unitary matrix in this algorithm, it must have dimensions $2^q \\times 2^q$, where $q$ represents the number of target qubits. It is recommanded to pad the combinatorial laplacian $\\Delta_k$ with an identity matrix with $\\tilde{\\lambda}_{max}/2$ in place of ones, where $\\tilde{\\lambda}_{max}$ is the estimate of the maximum eigenvalue of $\\Delta_k$ using the Gershgorin circle theorem ([details](https://mathworld.wolfram.com/GershgorinCircleTheorem.html)), such that:\n",
+ "\n",
+ "$$\n",
+ "\\tilde{\\Delta}_k =\n",
+ "\\begin{bmatrix}\n",
+ "\\Delta_k & 0 \\\\\n",
+ "0 & \\frac{\\widetilde{\\lambda}_{\\text{max}}}{2} \\cdot I_{2q - |S_k|}\n",
+ "\\end{bmatrix}_{2q \\times 2q}\n",
+ "$$\n",
+ "\n",
+ "where $\\Delta_k$ is the padded combinatorial laplacian and $q = \\lceil \\log_2 |S_k| \\rceil$ is the number of qubits this operator will act on. In QPE, as $2\\pi\\theta$ increases beyond $2\\pi$, the eigenvalues will start repeating due to their periodic form. Thus, $\\theta$ is restricted to $[0, 1)$. As $\\lambda \\to 2\\pi\\theta$ this means that $λ \\in [0, 2\\pi)$. Thus, we need to restrict the eigenvalues of the combinatorial laplacian to this range. This can be achieved by rescaling $\\tilde{\\Delta}_k$ by $\\delta/\\tilde{\\lambda}_{max}$ where $\\delta$ is slightly less than $2\\pi$. Thus, the rescaled matrix $H$ and the unitary marix $U$ for QPE are:\n",
+ "\n",
+ "$$\n",
+ "\\begin{align*}\n",
+ "H &= \\frac{\\delta}{\\tilde{\\lambda}_k} \\tilde{\\Delta}_k\\\\\n",
+ "U &= e^{iH}\n",
+ "\\end{align*}\n",
+ "$$\n",
+ "\n",
+ "**Example:**\n",
+ "\n",
+ "In our example, the combinational laplacian is in the form of a $6\\times6$ matrix:\n",
+ "\n",
+ "$$\n",
+ "\\begin{align*}\n",
+ "\\Delta_1 &= (\\partial_1)^\\dagger \\partial_1 + \\partial_2 (\\partial_2)^\\dagger\\\\\n",
+ "&=\n",
+ "\\begin{bmatrix}\n",
+ "3 & 0 & 0 & 0 & 0 & 0 \\\\\n",
+ "0 & 3 & 0 & -1 & -1 & 0 \\\\\n",
+ "0 & 0 & 3 & -1 & -1 & 0 \\\\\n",
+ "0 & -1 & -1 & 2 & 1 & -1 \\\\\n",
+ "0 & -1 & -1 & 1 & 2 & 1 \\\\\n",
+ "0 & 0 & 0 & -1 & 1 & 2\n",
+ "\\end{bmatrix}\n",
+ "\\end{align*}\n",
+ "$$\n",
+ "\n",
+ "It is padded with $\\tilde{\\lambda}_{max}=6$ and $\\delta=6$ to its nearest power of 2, which is 8 ($q=3$):\n",
+ "\n",
+ "$$\n",
+ "\\begin{align}\n",
+ "H_1 = \\begin{bmatrix}\n",
+ "3 & 0 & 0 & 0 & 0 & 0 & 0 & 0 \\\\\n",
+ "0 & 3 & 0 & -1 & -1 & 0 & 0 & 0 \\\\\n",
+ "0 & 0 & 3 & -1 & -1 & 0 & 0 & 0 \\\\\n",
+ "0 & -1 & -1 & 2 & 1 & -1 & 0 & 0 \\\\\n",
+ "0 & -1 & -1 & 1 & 2 & 1 & 0 & 0 \\\\\n",
+ "0 & 0 & 0 & -1 & 1 & 2 & 0 & 0 \\\\\n",
+ "0 & 0 & 0 & 0 & 0 & 0 & 3 & 0 \\\\\n",
+ "0 & 0 & 0 & 0 & 0 & 0 & 0 & 3\n",
+ "\\end{bmatrix}\n",
+ "\\end{align}\n",
+ "$$\n",
+ "\n",
+ "**Action:**\n",
+ "\n",
+ "Define a function to build the Laplacian, where Define a function that automatically determines whether the input Laplacian matrix requires padding. If padding is needed, the function will pad and rescale the matrix accordingly. Then, build the unitary based on the padded matrix, in the form of a circuit.\n",
+ "\n",
+ "**Answer:**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 78,
+ "id": "dbf2431a-aa85-41e1-a72c-563afec9c949",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Laplacian: (8, 8)\n",
+ "[[ 2. -1. -1. 0. 0. 0. 0. 0.]\n",
+ " [-1. 2. -1. 0. 0. 0. 0. 0.]\n",
+ " [-1. -1. 4. -1. -1. 0. 0. 0.]\n",
+ " [ 0. 0. -1. 2. -1. 0. 0. 0.]\n",
+ " [ 0. 0. -1. -1. 2. 0. 0. 0.]\n",
+ " [ 0. 0. 0. 0. 0. 4. 0. 0.]\n",
+ " [ 0. 0. 0. 0. 0. 0. 4. 0.]\n",
+ " [ 0. 0. 0. 0. 0. 0. 0. 4.]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "from scipy.linalg import expm\n",
+ "\n",
+ "def get_laplacian(k, simplex_tree): # Get combinatorial laplacian\n",
+ " if k == 0:\n",
+ " boundary_k1 = boundary(k+1, simplex_tree)\n",
+ " laplacian = boundary_k1 @ boundary_k1.T\n",
+ " else:\n",
+ " boundary_k = boundary(k, simplex_tree) # boundary k\n",
+ " boundary_k1 = boundary(k+1, simplex_tree) # boundary k+1\n",
+ " laplacian = boundary_k.T @ boundary_k + boundary_k1 @ boundary_k1.T\n",
+ " \n",
+ " # padding\n",
+ " n = laplacian.shape[0]\n",
+ " q = int(np.ceil(np.log2(n)))\n",
+ " n_pad = 2**q - n\n",
+ " lambda_max = max_eigenvalue(laplacian)\n",
+ " \n",
+ " padded_laplacian = np.zeros((2**q, 2**q))\n",
+ " padded_laplacian[:n, :n] = laplacian\n",
+ " padded_laplacian[n:, n:] = np.eye(n_pad) * (lambda_max/2)\n",
+ " \n",
+ " return padded_laplacian\n",
+ "\n",
+ "def max_eigenvalue(matrix): # estimate max eigenvalue using Gershgorin circle theorem\n",
+ " row_sums = np.sum(np.abs(matrix), axis=1)\n",
+ " return np.max(row_sums)\n",
+ "\n",
+ "\n",
+ "\n",
+ "def get_unitary(laplacian):\n",
+ " delta = 2 * np.pi * 7/8\n",
+ " lambda_max = max_eigenvalue(laplacian)\n",
+ "\n",
+ " if np.isnan(lambda_max) or lambda_max == 0:\n",
+ " lambda_max = 1\n",
+ " H = delta / lambda_max * laplacian\n",
+ " U = expm(1j * H)\n",
+ " return U\n",
+ "\n",
+ "k = 0\n",
+ "laplacian = get_laplacian(k, simplex_tree)\n",
+ "print(\"Laplacian:\", laplacian.shape)\n",
+ "print(laplacian)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 80,
+ "id": "099d53c4-e199-4456-8f73-4dd9808b6f0d",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Unitary: (8, 8)\n",
+ "[[ 0.10970723+0.58504472j 0.58110396-0.29687654j 0.39138807+0.05805694j\n",
+ " -0.04109963-0.17311255j -0.04109963-0.17311255j 0. +0.j\n",
+ " 0. +0.j 0. +0.j ]\n",
+ " [ 0.58110396-0.29687654j 0.10970723+0.58504472j 0.39138807+0.05805694j\n",
+ " -0.04109963-0.17311255j -0.04109963-0.17311255j 0. +0.j\n",
+ " 0. +0.j 0. +0.j ]\n",
+ " [ 0.39138807+0.05805694j 0.39138807+0.05805694j -0.56555227-0.23222774j\n",
+ " 0.39138807+0.05805694j 0.39138807+0.05805694j 0. +0.j\n",
+ " 0. +0.j 0. +0.j ]\n",
+ " [-0.04109963-0.17311255j -0.04109963-0.17311255j 0.39138807+0.05805694j\n",
+ " 0.10970723+0.58504472j 0.58110396-0.29687654j 0. +0.j\n",
+ " 0. +0.j 0. +0.j ]\n",
+ " [-0.04109963-0.17311255j -0.04109963-0.17311255j 0.39138807+0.05805694j\n",
+ " 0.58110396-0.29687654j 0.10970723+0.58504472j 0. +0.j\n",
+ " 0. +0.j 0. +0.j ]\n",
+ " [ 0. +0.j 0. +0.j 0. +0.j\n",
+ " 0. +0.j 0. +0.j -0.92387953+0.38268343j\n",
+ " 0. +0.j 0. +0.j ]\n",
+ " [ 0. +0.j 0. +0.j 0. +0.j\n",
+ " 0. +0.j 0. +0.j 0. +0.j\n",
+ " -0.92387953+0.38268343j 0. +0.j ]\n",
+ " [ 0. +0.j 0. +0.j 0. +0.j\n",
+ " 0. +0.j 0. +0.j 0. +0.j\n",
+ " 0. +0.j -0.92387953+0.38268343j]]\n"
+ ]
+ }
+ ],
+ "source": [
+ "\n",
+ "U = get_unitary(laplacian)\n",
+ "print(\"Unitary:\", U.shape)\n",
+ "print(U)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 82,
+ "id": "73f45de9-8671-45dd-84ff-0544a86b982d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/plain": [
+ "array([[1., 0., 0., 0., 0., 0., 0., 0.],\n",
+ " [0., 1., 0., 0., 0., 0., 0., 0.],\n",
+ " [0., 0., 1., 0., 0., 0., 0., 0.],\n",
+ " [0., 0., 0., 1., 0., 0., 0., 0.],\n",
+ " [0., 0., 0., 0., 1., 0., 0., 0.],\n",
+ " [0., 0., 0., 0., 0., 1., 0., 0.],\n",
+ " [0., 0., 0., 0., 0., 0., 1., 0.],\n",
+ " [0., 0., 0., 0., 0., 0., 0., 1.]])"
+ ]
+ },
+ "execution_count": 82,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "U = np.eye(8)\n",
+ "U"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "0acfea73",
+ "metadata": {},
+ "source": [
+ "### Step 3: Applying QPE\n",
+ "\n",
+ "**Instruction:**\n",
+ "\n",
+ "The Betti number is the number of zero eigenvalues in the Laplacian ([ref](https://link.springer.com/article/10.1007/PL00009218)). \n",
+ "\n",
+ "$$\n",
+ "\\beta_k = \\dim (\\ker(\\Delta_k))\n",
+ "$$\n",
+ "\n",
+ "The betti curve is then a series of Betti numbers on different resolution threshold `epsilon`.\n",
+ "\n",
+ "To estimate the number of zero eigenvalues (nullity) in the padded Laplacian matrix (padding didn't add more zero eigenvalues), QPE algorithm is employed. The fundamental concept is that, if the target qubits start out in the maximally mixed state (shown below), which can be thought of as a random choice of an eigenstate, the proportion of all-zero states among all measured states is equal to the proportion of zero eigenvalues among all eigenvalues. Assume the all-zero state show up for $\\{i|\\tilde{\\theta}_i=0\\}$ times in $\\alpha$ shots, the probability of getting all-zero state $p(0)$ is given by:\n",
+ "\n",
+ "$$\n",
+ "\\begin{align*}\n",
+ "p(0) &= \\frac{\\left| \\{i \\mid \\tilde{\\theta}_i = 0\\} \\right|}{\\alpha} = \\frac{\\tilde{\\beta}_k}{2^q} \\\\\n",
+ "\\implies \\tilde{\\beta}_k &= 2^q \\cdot p(0)\n",
+ "\\end{align*}\n",
+ "$$\n",
+ "\n",
+ "Where $\\tilde{\\beta}_k$ is the estimation of $k$-th Betti number. This estimation is then rounded to the nearest integer to obtain the final result."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "fc95fe90",
+ "metadata": {},
+ "source": [
+ "\n"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "72511671",
+ "metadata": {},
+ "source": [
+ "For your reference, the tutorial of QPE in several major quantum computing libraries are listed below:\n",
+ "\n",
+ "* [Qiskit](https://github.com/qiskit-community/qiskit-textbook/blob/main/content/ch-algorithms/quantum-phase-estimation.ipynb)\n",
+ "* [Pennylane](https://pennylane.ai/qml/demos/tutorial_qpe)\n",
+ "* [CUDA-Q](https://nvidia.github.io/cuda-quantum/latest/specification/cudaq/examples.html#quantum-phase-estimation:~:text=Quantum%20Phase%20Estimation-,%C2%B6,-C%2B%2B)\n",
+ "* [Cirq](https://quantumai.google/cirq/experiments/textbook_algorithms#phase_estimation)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "481e000c",
+ "metadata": {},
+ "source": [
+ "**Example:**\n",
+ "\n",
+ "In our example, the probability of measuring all-zero states is approximately $p(0)=0.137 = \\tilde{\\beta}_k / 2^3 \\implies \\tilde{\\beta}_k = 1.096$, which is then rounded to $1$.\n",
+ "\n",
+ "**Action:**\n",
+ "\n",
+ "Utilize your preferred quantum computing library to apply QPE for estimating the number of zero eigenvalues in the Laplacian matrix. Note that the exponential of the Laplacian matrix is used as the unitary operator in QPE.\n",
+ "\n",
+ "**Answer:**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 84,
+ "id": "848ef40c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qiskit import QuantumCircuit, QuantumRegister, ClassicalRegister, transpile\n",
+ "from qiskit_aer import AerSimulator\n",
+ "from qiskit.circuit.library import QFT\n",
+ "from qiskit.quantum_info import Operator\n",
+ "from qiskit.visualization import plot_histogram\n",
+ "import numpy as np\n",
+ "import math\n",
+ "\n",
+ "pi=np.pi\n",
+ "\n",
+ "\n",
+ "def phase_estimation(oracle, eigenstate, precision):\n",
+ " n = precision\n",
+ " \n",
+ " # Calculate number of target qubits needed from oracle size\n",
+ " oracle_size = oracle.num_qubits\n",
+ " \n",
+ " # Create registers: n counting qubits + oracle_size target qubits + oracle_size auxillary qubits\n",
+ " qr = QuantumRegister(n + 2*oracle_size, 'q')\n",
+ " c = ClassicalRegister(n+oracle_size, 'c')\n",
+ " qc = QuantumCircuit(qr, c)\n",
+ "\n",
+ " # Initialize target qubits in maximally entangled state\n",
+ " # First apply H to first target qubit\n",
+ " qc.h(n)\n",
+ " qc.cx(n,n+oracle_size)\n",
+ " # Then apply CX\n",
+ " for i in range(oracle_size-1):\n",
+ " \n",
+ " qc.h(n + i + 1) # H gate on target qubit\n",
+ " qc.cx(n + i+1, n + i + oracle_size+1) \n",
+ " qc.barrier()\n",
+ "\n",
+ " \n",
+ "\n",
+ " # Apply Hadamard to all counting qubits\n",
+ " for i in range(n):\n",
+ " qc.h(i)\n",
+ "\n",
+ " # Create controlled version of oracle\n",
+ " cGate = oracle.control(1)\n",
+ " qc.barrier()\n",
+ "\n",
+ " # Apply controlled operations with proper target qubits\n",
+ " for i in range(n):\n",
+ " for j in range(2**i):\n",
+ " all_qubits = [i] + list(range(n, n + oracle_size))\n",
+ " qc.append(cGate, all_qubits)\n",
+ "\n",
+ " qc.barrier()\n",
+ " iqft = QFT(n).inverse()\n",
+ " qc.append(iqft, range(n))\n",
+ " qc.barrier()\n",
+ "\n",
+ " # Measure counting qubits\n",
+ " for i in range(n):\n",
+ " qc.measure(i, i)\n",
+ "\n",
+ " #Measure auxillary qubits\n",
+ " for i in range(oracle_size):\n",
+ " \n",
+ " qc.measure(n+i+oracle_size,n+i)\n",
+ "\n",
+ " # Run with more shots for better statistics\n",
+ " aersim = AerSimulator(shots=10000)\n",
+ " circuit_transpile = transpile(qc, aersim, optimization_level=1)\n",
+ " result = aersim.run(circuit_transpile).result()\n",
+ " counts = result.get_counts()\n",
+ " counts=counts\n",
+ " return counts, qc\n",
+ "\n",
+ "def create_gate_from_matrix(matrix):\n",
+ " \"\"\"\n",
+ " Create a quantum circuit from a nxn unitary matrix\n",
+ " \"\"\"\n",
+ " n = matrix.shape[0]\n",
+ " n_qubits = int(math.log2(n)) # Calculate number of qubits needed\n",
+ " #print(f\"Matrix size: {n}x{n}\")\n",
+ " #print(f\"Number of qubits needed: {n_qubits}\")\n",
+ " \n",
+ " # Create circuit with the correct number of qubits\n",
+ " qc = QuantumCircuit(n_qubits)\n",
+ " \n",
+ " # Create list of qubits to apply unitary to\n",
+ " qubits = list(range(n_qubits))\n",
+ " #print(f\"Applying unitary to qubits: {qubits}\")\n",
+ " \n",
+ " # Create operator and verify its dimension\n",
+ " op = Operator(matrix)\n",
+ " #print(f\"Operator dimension: {op.dim}\")\n",
+ " \n",
+ " # Apply the unitary\n",
+ " qc.unitary(op, qubits, label='L')\n",
+ " \n",
+ " return n_qubits,qc\n",
+ "\n",
+ "def zero_phases(counts, precision, n_qubits):\n",
+ " \"\"\"\n",
+ " Analyze phases from measurement results and count zero phases\n",
+ " \"\"\"\n",
+ " total_shots = sum(counts.values())\n",
+ " phases = {}\n",
+ " zero_phase_count = 0\n",
+ " zero_bitstring='0'*precision\n",
+ "\n",
+ " #print(f\"zero bitsring {zero_bitstring}\")\n",
+ " for bitstring,count in counts.items():\n",
+ " length=len(bitstring)\n",
+ " #print(f\"Counts are {bitstring[-precision:],count}\")\n",
+ " if bitstring[-precision:] == zero_bitstring: # Only check last precision number of qubits\n",
+ " zero_phase_count =zero_phase_count+count\n",
+ "\n",
+ " #print(f\"Zero counts is {zero_phase_count}\")\n",
+ " \n",
+ " hist=plot_histogram(counts,title=\"Phase Estimation\")\n",
+ " n_shots=10000\n",
+ " betti=(zero_phase_count*(2**n_qubits)/n_shots) \n",
+ " #print(f\"Unrounded betti number: {betti}\")\n",
+ " betti=np.round(zero_phase_count*(2**n_qubits)/n_shots) \n",
+ " \n",
+ " return hist,betti\n",
+ "\n",
+ "\n",
+ "def analyze_matrix(matrix, precision=5):\n",
+ " \"\"\"\n",
+ " Analyze a matrix using QPE to detect zero eigenvalues\n",
+ " \"\"\"\n",
+ " \n",
+ " # Create gate from matrix\n",
+ " n_qubits,gate = create_gate_from_matrix(matrix)\n",
+ " \n",
+ " # Run phase estimation\n",
+ " counts,qc = phase_estimation(gate, 1, precision)\n",
+ " \n",
+ " # Analyze phases and count zeros\n",
+ " hist,betti = zero_phases(counts, precision, n_qubits)\n",
+ " \n",
+ " # print(f\"\\nResults:\")\n",
+ " # print(f\"Betti number: {betti}\")\n",
+ " \n",
+ " return betti,hist\n",
+ "\n",
+ "# print(\"Analyzing matrix:\")\n",
+ "# hist,betti,qc = analyze_matrix(U)\n",
+ "\n",
+ "# hist\n",
+ "# qc.draw(output='mpl')"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "5013deb5-9895-4d18-883a-a2e4936dd12e",
+ "metadata": {},
+ "source": [
+ "Bonus\n",
+ "\n",
+ "\n",
+ "1) The all zero state corresponds to the eigenstate corresponding to phase=0 corresponding to eigenvalue=1 of the unitary matrix of interest. In our case U=exp(iL_k) where L_k is the laplacian for k-simplexes. We are interested in calculate the number of zero eigenvalues of the laplacian, hence we are looking for the number of instances of phi=0 eigenstate in our phase estimator.\n",
+ "\n",
+ "2) The maximally mixed state corresponds to a equally random mixture of all eigenstates of some operator given that the eigenstates form a complete basis. In the case of the QPE, we use 2q qubits to form a maximally mixed state by tracing out q of the auxillary qubits. While the 2q qubits are in a pure state, the two subsystems of q qubits are in a maximally mixed state. There is no preferred basis for the maximally mixed state, and thus has no bias towards any eigenstates of the unitary matrix whose 0-eigenvalue eigenstate we are estimating. We start with a uniform random mixture of all eigenvectors of the unitary if our starting state is a maximally mixed state.\n",
+ "\n",
+ "The parallel state H|0>^*n on the other hand is a uniform superposition of all 2^n bit strings. But this uniform mixture is biased to the computational basis, and the probabilities in the basis of the eigenvectors of the unitary may not be uniform any longer. Thus we might start with a biased distribution of states which more often than not will not be biased to our state of interest i.e the 0-eigenvalue eigenstate, which is one state in a space of 2^n states. Thus we are better off starting with a fully random state like the maximally mixed state.\n",
+ "\n",
+ "3) delta schpiel done in slides for better estimation."
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "f8724ca3",
+ "metadata": {},
+ "source": [
+ "
NOTE:\n",
+ "\n",
+ "The unitary operator can be constructed by converting the exponential matrix of the Laplacian into a quantum circuit. For instance, in Qiskit, this can be implemented using `circuit.unitary(exp_matrix)`. Alternative methods for constructing the unitary operator will be optionally explored in the **BONUS** section at the end of this notebook.\n",
+ "
\n",
+ "
\n",
+ " \n",
+ "BONUS EXERCISE: \n",
+ "\n",
+ "1. Why we should measure all-zero states?\n",
+ "2. What is the difference between a maximally mixed state and the $(H\\ket{0})^{\\otimes n}$ state? Two possible aspects are:\n",
+ "\n",
+ "* Plotting of their density matrix.\n",
+ "* Results from QPE.\n",
+ "\n",
+ "3. What parameters affect the accuracy of the estimation before rounding? For Laplacian matrices of varying sizes, how does the accuracy depend on these parameters? Within what range of values do these parameters guarantee (or are highly likely to produce) a correct final result?\n",
+ "
"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "1cbe5cf2",
+ "metadata": {},
+ "source": [
+ "### Step 4: Detecting market crashes"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "7f27e81b",
+ "metadata": {},
+ "source": [
+ "**Instruction:**\n",
+ "\n",
+ "At this point, we have betti curves for each window across our dataset, and we wish to use this to detect market crashes. One such way is to take the difference between these curves—or the pairwise distance—for successive windows and look for spikes. This can be done with the $L^p$ norm of the betti curve for each window, defined as follows:\n",
+ "\n",
+ "$$||x||_p = (\\sum_{n}^{i=1} |x_i|^p)^{1/p}$$\n",
+ "\n",
+ "Combining these pairwise distances into a vector, we get a single output curve we can analyze. Experiment with different values of $p$, but a good starting point is the $L^2$ Norm. Using this, it is possible to detect regions where a market crash is occuring. Comparing detected crashes with the price data indicates how accurate the crash detection methodology is ([ref](https://github.com/giotto-ai/stock-market-crashes/blob/master/Stock%20Market%20Crash%20Detection.ipynb)). \n",
+ "\n",
+ "**Action:**\n",
+ "\n",
+ "Use the $L^p$ norm to create pairwise distance curves for successive windows, and then use the results to define when a crash is occuring. Compare this with your data to see how well it performs. You may find the following classical solver is useful.\n",
+ "\n",
+ "**Answer:**"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 89,
+ "id": "700a1375-a1b5-4424-bd1d-0b2b2b7e28d5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "#betti number estimation using quantum phase estimation\n",
+ "def get_betti_number(point_cloud, epsilon, dim):\n",
+ " # print(\"Constructing tree\")\n",
+ " simplex_tree = get_simplex_tree(point_cloud, epsilon)\n",
+ " # print(f\"num 0-simplices: {len(simplex_tree[0])}\")\n",
+ " # print(f\"num 1-simplices: {len(simplex_tree[1])}\")\n",
+ " # print(f\"num 2-simplices: {len(simplex_tree[2])}\")\n",
+ " if len(simplex_tree[dim]) == 0:\n",
+ " return 0\n",
+ " # print(\"Computing laplacian\")\n",
+ " laplacian = get_laplacian(dim, simplex_tree)\n",
+ " if laplacian.shape[0] ==1:\n",
+ " return 1 if abs(laplacian[0][0]) < 1e-9 else 0\n",
+ " # print(f\"Laplacian: {laplacian.shape}\")\n",
+ " #print(laplacian)\n",
+ " # print(\"Computing unitary\")\n",
+ " unitary = get_unitary(laplacian)\n",
+ " # print(f\"Unitary: {unitary.shape}\")\n",
+ " #print(unitary)\n",
+ " # print(\"Performing QPE\")\n",
+ " \n",
+ " betti_number = analyze_matrix(unitary)[0]\n",
+ " return betti_number"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 91,
+ "id": "3892a905-1e01-49b6-8729-bb7c7d450cd2",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "N = 5, w = 30, L = 5536\n",
+ "32.0\n"
+ ]
+ }
+ ],
+ "source": [
+ "N = 5 # embedding dimension\n",
+ "d = 2 # time delay\n",
+ "w = 30 # window size\n",
+ "L = len(time_series)\n",
+ "vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ "K = L - (N-1)*d - w + 1 # number of windows\n",
+ "point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ "print(f\"N = {N}, w = {w}, L = {L}\")\n",
+ "point_cloud = point_clouds[0]\n",
+ "print(get_betti_number(point_cloud,0.01,0))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "3600544a-6b29-4e97-ae7a-6f17aaa7a677",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 94,
+ "id": "a56afa9c-e36c-475a-ba2e-c1dcee42f643",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "N = 5, w = 30, L = 5536\n",
+ "0\n"
+ ]
+ }
+ ],
+ "source": [
+ "N = 5 # embedding dimension\n",
+ "d = 2 # time delay\n",
+ "w = 30 # window size\n",
+ "L = len(time_series)\n",
+ "vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ "K = L - (N-1)*d - w + 1 # number of windows\n",
+ "point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ "print(f\"N = {N}, w = {w}, L = {L}\")\n",
+ "point_cloud = point_clouds[0]\n",
+ "print(get_betti_number(point_cloud,0.01,1))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 96,
+ "id": "d29ee3eb-11a9-4c15-84c4-050992e27077",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "#betti number estimation using classical methods\n",
+ "import matplotlib.pyplot as plt\n",
+ "from ripser import ripser\n",
+ "\n",
+ "def classical_betti_solver(point_cloud, epsilon, dim):\n",
+ " '''Return the Betti number on a given point cloud.\n",
+ " Args:\n",
+ " point_cloud: the point cloud after applying the sliding window.\n",
+ " epsilon: resolution threshold.\n",
+ " dim: the dimension on which the Betti number is calculated\n",
+ " '''\n",
+ " result = ripser(point_cloud, maxdim=dim)\n",
+ " diagrams = result[\"dgms\"]\n",
+ " return len(\n",
+ " [interval for interval in diagrams[dim] if interval[0] < epsilon < interval[1]]\n",
+ " )\n"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 98,
+ "id": "8cc84280-84fd-483a-bb6c-4b4e97c46fbb",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "N = 5, w = 10, L = 5536\n",
+ "0\n"
+ ]
+ }
+ ],
+ "source": [
+ "N = 5 # embedding dimension\n",
+ "d = 1 # time delay\n",
+ "w = 10 # window size\n",
+ "L = len(time_series)\n",
+ "vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ "K = L - (N-1)*d - w + 1 # number of windows\n",
+ "point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ "print(f\"N = {N}, w = {w}, L = {L}\")\n",
+ "point_cloud = point_clouds[0]\n",
+ "print(classical_betti_solver(point_cloud,0.01,1))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 100,
+ "id": "1a909d85-1aac-44a8-b4bb-682165f58c9d",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "N = 5, w = 10, L = 5536\n",
+ "16.0\n"
+ ]
+ }
+ ],
+ "source": [
+ "N = 5 # embedding dimension\n",
+ "d = 1 # time delay\n",
+ "w = 10 # window size\n",
+ "L = len(time_series)\n",
+ "vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ "K = L - (N-1)*d - w + 1 # number of windows\n",
+ "point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ "print(f\"N = {N}, w = {w}, L = {L}\")\n",
+ "point_cloud = point_clouds[0]\n",
+ "print(get_betti_number(point_cloud,0.01,0))"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 102,
+ "id": "3a827b21-0242-4017-a649-383d446bf472",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "N = 3, w = 5, L = 5536\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Calculating Betti numbers: 100%|█████████| 3000/3000 [00:00<00:00, 12314.81it/s]\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAA0EAAAIhCAYAAACIfrE3AAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABMQ0lEQVR4nO3deXxU9fX/8fckmWyQAEpYjSARCZRVIhKoCiphUUQpYsEFqdr6FRXh51qxLFqgrriigoJWAYviVhGJIktlUTGICgK1rAoCsiQhkEyS+/uDZkpMgLk3d+7Nzbyej0ceZe7cmXsm/Zjw5nzmjM8wDEMAAAAAECGi3C4AAAAAAJxECAIAAAAQUQhBAAAAACIKIQgAAABARCEEAQAAAIgohCAAAAAAEYUQBAAAACCiEIIAAAAARBRCEAAAAICIQggCgAg2c+ZM+Xy+cl8pKSnq0aOH/vnPf1p+3ueee04zZ86scPynn37SuHHjtGbNmgr3jRs3Tj6fL+RrLFu2TIMHD1bTpk0VGxurOnXqqFu3bpo6daoOHTpkuXYAQM1HCAIAaMaMGVqxYoWWL1+uF198UdHR0erfv7/ef/99S893ohA0fvz4SkPQjTfeqBUrVoT0/GPHjtX555+vH3/8UQ8++KCys7M1Z84cXXTRRRo3bpzGjBljqW4AQGSIcbsAAID72rZtq4yMjODtPn36qF69epo9e7b69+/vSA2nnXaaTjvttJOeN3fuXE2YMEE33HCDpk2bVq571LdvX919990hh6mTKSgoUGJioi3PBQCoPugEAQAqiI+PV2xsrPx+f7njRUVFeuihh5Senq64uDilpKRo+PDh2rNnT/Cc5s2b67vvvtOSJUuCW+yaN2+uxYsX65xzzpEkDR8+PHjfuHHjJIW+HW7ChAmqV6+ennrqqUrPT0pKUlZWliRpy5Yt8vl8lXaljr32sdf/6quvNGjQINWrV09paWmaMmWKfD6f/v3vf1d4jnvuuUexsbHau3dv8NjHH3+siy66SMnJyUpMTFT37t31ySeflHvcnj179Mc//lGpqanB72P37t318ccfn/T1AwCqjhAEAFBJSYmKi4sVCAS0Y8cO3XHHHTp06JCGDh0aPKe0tFQDBgzQ5MmTNXToUH3wwQeaPHmysrOz1aNHDx0+fFiS9Pbbb6tFixbq1KmTVqxYoRUrVujtt9/W2WefrRkzZkiSxowZE7zvxhtvDLnOnTt36ttvv1VWVlbYOjQDBw7UmWeeqblz5+r555/XNddco9jY2ApBqqSkRK+99pr69++v+vXrS5Jee+01ZWVlKTk5Wa+88or+8Y9/6JRTTlHv3r3LBaFrr71W77zzjv7yl79o4cKFmj59ui6++GL98ssvYXlNAIDy2A4HAFDXrl3L3Y6Li9Mzzzyj3r17B4/94x//0IIFC/TWW29p4MCBweMdOnTQOeeco5kzZ+r//u//1KlTJyUkJCg5ObnC87Zt21aSlJaWVuG+UGzbtk2SdMYZZ5h+bKiGDRum8ePHlzt26aWX6pVXXtGECRMUFXX03w8XLlyon376ScOHD5d0dOvcyJEjdemll+rtt98OPrZfv346++yz9ec//1mrVq2SJH322We68cYbddNNNwXPGzBgQNheEwCgPDpBAAC9+uqr+uKLL/TFF1/oww8/1LBhwzRixAg988wzwXP++c9/qm7duurfv7+Ki4uDXx07dlSjRo20ePFi916AjX73u99VODZ8+HDt2LGj3Ha1GTNmqFGjRurbt68kafny5dq3b5+GDRtW7vtTWlqqPn366IsvvghOrevSpYtmzpyphx56SCtXrlQgEHDmxQEAJBGCAACSWrdurYyMDGVkZKhPnz564YUXlJWVpbvvvlsHDhyQJP388886cOBA8L1Cx37t2rWr3PtiwuX000+XJG3evDls12jcuHGFY3379lXjxo2D2/n279+v9957T9ddd52io6MlHf3+SNKgQYMqfH/+9re/yTAM7du3T5L0xhtvaNiwYZo+fboyMzN1yimn6LrrrtOuXbvC9roAAP/DdjgAQKXat2+vjz76SBs3blSXLl1Uv359nXrqqVqwYEGl5yclJYW9psaNG6tdu3ZauHBhSJPb4uPjJUmFhYXljp/ovTeVDVuIjo7Wtddeq6eeekoHDhzQrFmzVFhYGNwKJyn4vqCnn376uFv9GjZsGDx3ypQpmjJlirZt26b33ntP9957r3bv3n3c7y8AwD6EIABApco+yyclJUXS0ffFzJkzRyUlJTr33HNP+Ni4uLjgoIRfH5dU6X2heuCBBzR48GDdfvvtFUZkS1J+fr6WL1+urKwsNWzYUPHx8Vq7dm25c959913T1x0+fLgefvhhzZ49WzNnzlRmZqbS09OD93fv3l11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QTaiEwQA4UcnCACqj+LiYj3wwANKTU1VcnKyevToobVr17pd1kkRggAAnkIIAoDq4+WXX9b06dP15JNPKicnRy1bttRVV11V6bk7d+7U0KFD1apVK0VFRemOO+5wtthjEILCgE4QAIQPI7IBoPpYtGiR+vTpo4EDByotLU2jRo3S999/r3379lU4t7CwUCkpKbr//vvVoUMHF6r9nxhXr17jkH4AINzoBAGICIYhFRS4c+3ExJD/VX/Pnj1KT08P3t65c6ckKbrsDZzHaN68uZ588klJRztIbiIEAQA8hRAEICIUFEi1a7tz7fx8qVatkE41yt4UL2njxo267777lJmZqTp16oSrOlsQgmzEYAQACD9CEABUP/fee68eeeQR+Xw+zZ071+1yTooQBADwFEZkA4gIiYlHOzJuXduk0aNH67LLLtP8+fM1ZMgQvfbaaxo8eLCeeOIJtWnTRr179z7pc5SUlGjo0KGaOXOmEhISrFQeMkKQjQzjaAuIThAAhA+dIAARwecLeUtaddCgQQM1atRIv/3tb7V7924999xzuuSSS/TBBx9o1KhRIT1HdHS0Lr/8ck2bNk233357WOtlOhwAwFOYDgcA1UtxcXG5236/X/Hx8Vq2bJkyMjJMPVevXr30/vvv21lepegEhQGdIAAIHzpBAFC9zJ49W5mZmbrooov09ddfa9asWZowYYIOHDig5OTkcueuWbNGkpSfn689e/ZozZo1io2NVZs2bSRJSUlJ2r9/f9hrJgQBADyFEAQA1Uvr1q31+OOPa8SIEWrUqJFuv/123XrrrdqwYYPmz59f7txOnToF/7x69WrNmjVLzZo105YtWyRJGzZsUPv27cNeMyEIAOAphCAAqF4yMjL07LPPKiqq/Dtt2rRpo7y8PB04cEB169aVVH6kdmVeffVVjRgxIlylBvGeIJsc+/8n2+EAIHwIQQDgHY8++qg2btwY0rklJSU699xz1blz5zBXRScIAOAxjMgGAO9IS0tTWlpaSOdGR0fryiuvDHNFRxGCbEInCACcQScIAKqPRYsWKTc31+0yTGM7HADAUxiRDQCoKkKQTegEAYAz6AQBAKqKEAQA8BRCEACgqghBAABPIQQBAKqKEGQTtsMBgDOYDgcAqCpCEADAU+gEAQCqihBkEzpBAOAMpsMBQPVRXFysBx54QKmpqUpOTlaP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izUDs1gzMIs1A7Oqy5o5WQeoDIMRAAAAAEQUQhAAAACAiEIIsklcXJzGjh2ruLg4t0uBR7BmYBZrBmaxZmAWawZmeXXNeHowAgAAAACYRScIAAAAQEQhBAEAAACIKIQgAAAAABGFEAQAAAAgohCCTuC5557TGWecofj4eHXu3FnLli074flLlixR586dFR8frxYtWuj555+vcM5bb72lNm3aKC4uTm3atNHbb78drvLhArvXzHfffaff/e53at68uXw+n6ZMmRLG6uEGu9fMtGnTdN5556levXqqV6+eLr74Yn3++efhfAlwkN3rZd68ecrIyFDdunVVq1YtdezYUX//+9/D+RLgsHD8XabMnDlz5PP5dPnll9tcNdxk95qZOXOmfD5fha8jR46E82WcnIFKzZkzx/D7/ca0adOMdevWGSNHjjRq1aplbN26tdLz//Of/xiJiYnGyJEjjXXr1hnTpk0z/H6/8eabbwbPWb58uREdHW1MnDjRWL9+vTFx4kQjJibGWLlypVMvC2EUjjXz+eefG3feeacxe/Zso1GjRsYTTzzh0KuBE8KxZoYOHWo8++yzRk5OjrF+/Xpj+PDhRp06dYwdO3Y49bIQJuFYL59++qkxb948Y926dca///1vY8qUKUZ0dLSxYMECp14Wwigca6bMli1bjKZNmxrnnXeeMWDAgDC/EjglHGtmxowZRnJysrFz585yX24jBB1Hly5djJtvvrncsfT0dOPee++t9Py7777bSE9PL3fsT3/6k9G1a9fg7cGDBxt9+vQpd07v3r2N3//+9zZVDTeFY80cq1mzZoSgGibca8YwDKO4uNhISkoyXnnllaoXDFc5sV4MwzA6depkjBkzpmrFoloI15opLi42unfvbkyfPt0YNmwYIagGCceamTFjhlGnTh3ba60qtsNVoqioSKtXr1ZWVla541lZWVq+fHmlj1mxYkWF83v37q0vv/xSgUDghOcc7znhHeFaM6i5nFozBQUFCgQCOuWUU+wpHK5wYr0YhqFPPvlEGzZs0Pnnn29f8XBFONfMhAkTlJKSohtuuMH+wuGacK6Z/Px8NWvWTKeddpouvfRS5eTk2P8CTCIEVWLv3r0qKSlRw4YNyx1v2LChdu3aVeljdu3aVen5xcXF2rt37wnPOd5zwjvCtWZQczm1Zu699141bdpUF198sT2FwxXhXC8HDx5U7dq1FRsbq0suuURPP/20evXqZf+LgKPCtWY+++wzvfTSS5o2bVp4CodrwrVm0tPTNXPmTL333nuaPXu24uPj1b17d23atCk8LyREMa5evZrz+XzlbhuGUeHYyc7/9XGzzwlvCceaQc0WzjXz8MMPa/bs2Vq8eLHi4+NtqBZuC8d6SUpK0po1a5Sfn69PPvlEo0ePVosWLdSjRw/7Codr7FwzeXl5uuaaazRt2jTVr1/f/mJRLdj9c6Zr167q2rVr8P7u3bvr7LPP1tNPP62nnnrKrrJNIwRVon79+oqOjq6Qenfv3l0h7ZZp1KhRpefHxMTo1FNPPeE5x3tOeEe41gxqrnCvmUcffVQTJ07Uxx9/rPbt29tbPBwXzvUSFRWlM888U5LUsWNHrV+/XpMmTSIEeVw41sx3332nLVu2qH///sH7S0tLJUkxMTHasGGD0tLSbH4lcIpTf5eJiorSOeec43oniO1wlYiNjVXnzp2VnZ1d7nh2dra6detW6WMyMzMrnL9w4UJlZGTI7/ef8JzjPSe8I1xrBjVXONfMI488ogcffFALFixQRkaG/cXDcU7+jDEMQ4WFhVUvGq4Kx5pJT0/XN998ozVr1gS/LrvsMvXs2VNr1qxRampq2F4Pws+pnzOGYWjNmjVq3LixPYVb5fwsBm8oGxH40ksvGevWrTPuuOMOo1atWsaWLVsMwzCMe++917j22muD55eNCBw1apSxbt0646WXXqowIvCzzz4zoqOjjcmTJxvr1683Jk+ezIjsGiQca6awsNDIyckxcnJyjMaNGxt33nmnkZOTY2zatMnx1wf7hWPN/O1vfzNiY2ONN998s9wo0ry8PMdfH+wVjvUyceJEY+HChcYPP/xgrF+/3njssceMmJgYY9q0aY6/PtgvHGvm15gOV7OEY82MGzfOWLBggfHDDz8YOTk5xvDhw42YmBhj1apVjr++YxGCTuDZZ581mjVrZsTGxhpnn322sWTJkuB9w4YNMy644IJy5y9evNjo1KmTERsbazRv3tyYOnVqheecO3eu0apVK8Pv9xvp6enGW2+9Fe6XAQfZvWY2b95sSKrw9evngXfZvWaaNWtW6ZoZO3asA68G4Wb3ern//vuNM88804iPjzfq1atnZGZmGnPmzHHipcAh4fi7zLEIQTWP3WvmjjvuME4//XQjNjbWSElJMbKysozly5c78VJOyGcY/333EgAAAABEAN4TBAAAACCiEIIAAAAARBRCEAAAAICIQggCAAAAEFEIQQAAAAAiCiEIAAAAQEQhBAEAAACIKIQgAAAAABGFEAQAcMyGDRvUqFEj5eXlOXbN66+/Xpdffnnwdo8ePXTHHXfYeo0777xTt99+u63PCQAIH0IQAMAx999/v0aMGKGkpCRJ0uLFi+Xz+Sr92rVrly3XfPLJJzVz5kxbnut47r77bs2YMUObN28O63UAAPYgBAEAHLFjxw699957Gj58eIX7NmzYoJ07d5b7atCggS3XrVOnjurWrWvLcx1PgwYNlJWVpeeffz6s1wEA2IMQBACwZOXKlerevbuSkpJUq1YttWvXTl988cVxz//HP/6hDh066LTTTqtwX4MGDdSoUaNyX1FRR39FlW1nGz9+vBo0aKDk5GT96U9/UlFRUfDxb775ptq1a6eEhASdeuqpuvjii3Xo0KFyjz+e/fv367rrrlO9evWUmJiovn37atOmTcH7Z86cqbp16+qjjz5S69atVbt2bfXp00c7d+4s9zyXXXaZZs+eHdL3DgDgLkIQAMCSIUOG6IwzztDnn3+ub7/9VlOmTFHDhg2Pe/7SpUuVkZFh6VqffPKJ1q9fr08//VSzZ8/W22+/rfHjx0uSdu7cqSFDhugPf/iD1q9fr8WLF2vgwIEyDCOk577++uv15Zdf6r333tOKFStkGIb69eunQCAQPKegoECPPvqo/v73v2vp0qXatm2b7rzzznLP06VLF23fvl1bt2619BoBAM6JcbsAAIA3lZSU6PTTT9eZZ54pv9+vM84444Tnb9myRZ07d670vl93h5o2baoNGzYEb8fGxurll19WYmKifvOb32jChAm666679OCDD2rnzp0qLi7WwIED1axZM0lSu3btQnoNmzZt0nvvvafPPvtM3bp1kyS9/vrrSk1N1TvvvKMrr7xSkhQIBPT8888rLS1NknTrrbdqwoQJFWoue51ldQAAqidCEADAknnz5umKK67Qww8/rPj4eP3444+qU6fOcc8/fPiw4uPjK71v2bJlwWEJkhQTU/7XU4cOHZSYmBi8nZmZqfz8fG3fvl0dOnTQRRddpHbt2ql3797KysrSoEGDVK9evZO+hvXr1ysmJkbnnntu8Nipp56qVq1aaf369cFjiYmJwQAkSY0bN9bu3bvLPVdCQoKko10jAED1xnY4AIAl9913n84++2wtX75ca9asKRdiKlO/fn3t37+/0vvOOOMMnXnmmcGv5s2bh1SDz+dTdHS0srOz9eGHH6pNmzZ6+umn1apVq5AmtR1vy5xhGPL5fMHbfr+/wnV//dh9+/ZJklJSUkKqHQDgHkIQAMC0vXv36uOPP9aECRPUpUsXnXnmmcFBBsfTqVMnrVu3ztL1vv76ax0+fDh4e+XKlapdu3ZwG53P51P37t01fvx45eTkKDY2Vm+//fZJn7dNmzYqLi7WqlWrgsd++eUXbdy4Ua1btzZV47fffiu/36/f/OY3ph4HAHAeIQgAYFr9+vWVmpqqv/zlL1q9erW2bt2qxYsXa+HChcd9TO/evbVixQqVlJRUuG/37t3atWtXua9jBxMUFRXphhtu0Lp16/Thhx9q7NixuvXWWxUVFaVVq1Zp4sSJ+vLLL7Vt2zbNmzdPe/bsCSnEtGzZUgMGDNBNN92kf/3rX/r66691zTXXqGnTphowYICp78myZct03nnnBbfFAQCqL0IQAMCSDz/8UKWlperdu7fOOuss3XTTTfr555+Pe36/fv3k9/v18ccfV7ivVatWaty4cbmv1atXB++/6KKL1LJlS51//vkaPHiw+vfvr3HjxkmSkpOTtXTpUvXr109nnXWWxowZo8cee0x9+/YN6XXMmDFDnTt31qWXXqrMzEwZhqH58+dX2AJ3MrNnz9ZNN91k6jEAAHf4jFBniAIAUEXPPfec3n33XX300UchP+b666/XgQMH9M4774SvsCr64IMPdNddd2nt2rUVhjoAAKofflIDABzzxz/+Ufv371deXt5JByl4yaFDhzRjxgwCEAB4BJ0gAEC15oVOEADAWwhBAAAAACIKgxEAAAAARBRCEAAAAICIQggCAAAAEFEIQQAAAAAiCiEIAAAAQEQhBAEAAACIKIQgAAAAABGFEAQAAAAgovx/DlTOBDwxm+oAAAAASUVORK5CYII=",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Maximum β₍0₎: 5\n",
+ "Maximum β₍1₎: 1\n",
+ "Maximum β₍2₎: 0\n"
+ ]
+ }
+ ],
+ "source": [
+ "#classical Betti solver\n",
+ "import matplotlib.pyplot as plt\n",
+ "from ripser import ripser\n",
+ "\n",
+ "def classical_betti_solver(point_cloud, epsilon, dim):\n",
+ " '''Return the Betti number on a given point cloud.\n",
+ " Args:\n",
+ " point_cloud: the point cloud after applying the sliding window.\n",
+ " epsilon: resolution threshold.\n",
+ " dim: the dimension on which the Betti number is calculated\n",
+ " '''\n",
+ " result = ripser(point_cloud, maxdim=dim)\n",
+ " diagrams = result[\"dgms\"]\n",
+ " return len(\n",
+ " [interval for interval in diagrams[dim] if interval[0] < epsilon < interval[1]]\n",
+ " )\n",
+ "\n",
+ "# write your code here\n",
+ "from tqdm import tqdm\n",
+ "\n",
+ "N = 3 # embedding dimension\n",
+ "d = 1 # time delay\n",
+ "w =5 # window size\n",
+ "L = len(time_series)\n",
+ "vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ "K = L - (N-1)*d - w + 1 # number of windows\n",
+ "point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ "print(f\"N = {N}, w = {w}, L = {L}\")\n",
+ "point_cloud = point_clouds[0]\n",
+ "\n",
+ "# Define ranges\n",
+ "eps_range = np.linspace(0, 0.05, 1000)\n",
+ "k_range = [0,1,2] # Calculate for k = 0, 1, and 2\n",
+ "\n",
+ "# Dictionary to store Betti numbers for each k\n",
+ "betti_numbers = {k: [] for k in k_range}\n",
+ "\n",
+ "# Calculate Betti numbers for each epsilon and k with progress bar\n",
+ "total_iterations = len(eps_range) * len(k_range)\n",
+ "with tqdm(total=total_iterations, desc=\"Calculating Betti numbers\") as pbar:\n",
+ " for eps in eps_range:\n",
+ " for k in k_range:\n",
+ " betti_numbers[k].append(classical_betti_solver(point_cloud, eps, k))\n",
+ " pbar.update(1)\n",
+ "\n",
+ "# Create plot\n",
+ "plt.figure(figsize=(10, 6))\n",
+ "colors = ['blue', 'red', 'green'] # One color for each k\n",
+ "\n",
+ "# Plot each k dimension\n",
+ "for k, color in zip(k_range, colors):\n",
+ " plt.plot(eps_range, betti_numbers[k], \n",
+ " color=color, \n",
+ " label=f'β₍{k}₎')\n",
+ "\n",
+ "plt.xlabel('ε (Epsilon)')\n",
+ "plt.ylabel('Betti Number')\n",
+ "plt.title('Betti Curves')\n",
+ "plt.legend()\n",
+ "plt.grid(True)\n",
+ "plt.show()\n",
+ "\n",
+ "# Print maximum values\n",
+ "for k in k_range:\n",
+ " print(f\"Maximum β₍{k}₎: {max(betti_numbers[k])}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "4ce5ab90-9119-4df0-8fac-67b89f81b01a",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 104,
+ "id": "b4cab0b1-e620-4961-b605-836e0aec5f2f",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "N = 3, w = 5, L = 5536\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Calculating Betti numbers: 4%|▌ | 41/1000 [00:01<00:45, 20.95it/s]\n"
+ ]
+ },
+ {
+ "ename": "KeyboardInterrupt",
+ "evalue": "",
+ "output_type": "error",
+ "traceback": [
+ "\u001b[0;31m---------------------------------------------------------------------------\u001b[0m",
+ "\u001b[0;31mKeyboardInterrupt\u001b[0m Traceback (most recent call last)",
+ "Cell \u001b[0;32mIn[104], line 43\u001b[0m\n\u001b[1;32m 41\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m eps \u001b[38;5;129;01min\u001b[39;00m eps_range:\n\u001b[1;32m 42\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m k \u001b[38;5;129;01min\u001b[39;00m k_range:\n\u001b[0;32m---> 43\u001b[0m betti_numbers[k]\u001b[38;5;241m.\u001b[39mappend(get_betti_number(point_cloud, eps, k))\n\u001b[1;32m 44\u001b[0m pbar\u001b[38;5;241m.\u001b[39mupdate(\u001b[38;5;241m1\u001b[39m)\n\u001b[1;32m 46\u001b[0m \u001b[38;5;66;03m# Create plot\u001b[39;00m\n",
+ "Cell \u001b[0;32mIn[89], line 21\u001b[0m, in \u001b[0;36mget_betti_number\u001b[0;34m(point_cloud, epsilon, dim)\u001b[0m\n\u001b[1;32m 16\u001b[0m unitary \u001b[38;5;241m=\u001b[39m get_unitary(laplacian)\n\u001b[1;32m 17\u001b[0m \u001b[38;5;66;03m# print(f\"Unitary: {unitary.shape}\")\u001b[39;00m\n\u001b[1;32m 18\u001b[0m \u001b[38;5;66;03m#print(unitary)\u001b[39;00m\n\u001b[1;32m 19\u001b[0m \u001b[38;5;66;03m# print(\"Performing QPE\")\u001b[39;00m\n\u001b[0;32m---> 21\u001b[0m betti_number \u001b[38;5;241m=\u001b[39m analyze_matrix(unitary)[\u001b[38;5;241m0\u001b[39m]\n\u001b[1;32m 22\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m betti_number\n",
+ "Cell \u001b[0;32mIn[84], line 133\u001b[0m, in \u001b[0;36manalyze_matrix\u001b[0;34m(matrix, precision)\u001b[0m\n\u001b[1;32m 130\u001b[0m n_qubits,gate \u001b[38;5;241m=\u001b[39m create_gate_from_matrix(matrix)\n\u001b[1;32m 132\u001b[0m \u001b[38;5;66;03m# Run phase estimation\u001b[39;00m\n\u001b[0;32m--> 133\u001b[0m counts,qc \u001b[38;5;241m=\u001b[39m phase_estimation(gate, \u001b[38;5;241m1\u001b[39m, precision)\n\u001b[1;32m 135\u001b[0m \u001b[38;5;66;03m# Analyze phases and count zeros\u001b[39;00m\n\u001b[1;32m 136\u001b[0m hist,betti \u001b[38;5;241m=\u001b[39m zero_phases(counts, precision, n_qubits)\n",
+ "Cell \u001b[0;32mIn[84], line 66\u001b[0m, in \u001b[0;36mphase_estimation\u001b[0;34m(oracle, eigenstate, precision)\u001b[0m\n\u001b[1;32m 64\u001b[0m \u001b[38;5;66;03m# Run with more shots for better statistics\u001b[39;00m\n\u001b[1;32m 65\u001b[0m aersim \u001b[38;5;241m=\u001b[39m AerSimulator(shots\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m10000\u001b[39m)\n\u001b[0;32m---> 66\u001b[0m circuit_transpile \u001b[38;5;241m=\u001b[39m transpile(qc, aersim, optimization_level\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m)\n\u001b[1;32m 67\u001b[0m result \u001b[38;5;241m=\u001b[39m aersim\u001b[38;5;241m.\u001b[39mrun(circuit_transpile)\u001b[38;5;241m.\u001b[39mresult()\n\u001b[1;32m 68\u001b[0m counts \u001b[38;5;241m=\u001b[39m result\u001b[38;5;241m.\u001b[39mget_counts()\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/utils/deprecation.py:184\u001b[0m, in \u001b[0;36mdeprecate_arg..decorator..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 171\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mwraps(func)\n\u001b[1;32m 172\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mwrapper\u001b[39m(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 173\u001b[0m _maybe_warn_and_rename_kwarg(\n\u001b[1;32m 174\u001b[0m args,\n\u001b[1;32m 175\u001b[0m kwargs,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 182\u001b[0m predicate\u001b[38;5;241m=\u001b[39mpredicate,\n\u001b[1;32m 183\u001b[0m )\n\u001b[0;32m--> 184\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m func(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/utils/deprecation.py:184\u001b[0m, in \u001b[0;36mdeprecate_arg..decorator..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 171\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mwraps(func)\n\u001b[1;32m 172\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mwrapper\u001b[39m(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 173\u001b[0m _maybe_warn_and_rename_kwarg(\n\u001b[1;32m 174\u001b[0m args,\n\u001b[1;32m 175\u001b[0m kwargs,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 182\u001b[0m predicate\u001b[38;5;241m=\u001b[39mpredicate,\n\u001b[1;32m 183\u001b[0m )\n\u001b[0;32m--> 184\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m func(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n",
+ " \u001b[0;31m[... skipping similar frames: deprecate_arg..decorator..wrapper at line 184 (1 times)]\u001b[0m\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/utils/deprecation.py:184\u001b[0m, in \u001b[0;36mdeprecate_arg..decorator..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 171\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mwraps(func)\n\u001b[1;32m 172\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mwrapper\u001b[39m(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 173\u001b[0m _maybe_warn_and_rename_kwarg(\n\u001b[1;32m 174\u001b[0m args,\n\u001b[1;32m 175\u001b[0m kwargs,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 182\u001b[0m predicate\u001b[38;5;241m=\u001b[39mpredicate,\n\u001b[1;32m 183\u001b[0m )\n\u001b[0;32m--> 184\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m func(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/compiler/transpiler.py:423\u001b[0m, in \u001b[0;36mtranspile\u001b[0;34m(circuits, backend, basis_gates, inst_map, coupling_map, backend_properties, initial_layout, layout_method, routing_method, translation_method, scheduling_method, instruction_durations, dt, approximation_degree, timing_constraints, seed_transpiler, optimization_level, callback, output_name, unitary_synthesis_method, unitary_synthesis_plugin_config, target, hls_config, init_method, optimization_method, ignore_backend_supplied_default_methods, num_processes, qubits_initially_zero)\u001b[0m\n\u001b[1;32m 411\u001b[0m warnings\u001b[38;5;241m.\u001b[39mfilterwarnings(\n\u001b[1;32m 412\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mignore\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 413\u001b[0m category\u001b[38;5;241m=\u001b[39m\u001b[38;5;167;01mDeprecationWarning\u001b[39;00m,\n\u001b[1;32m 414\u001b[0m message\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m.*``instruction_durations`` is deprecated as of Qiskit 1.3.*\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 415\u001b[0m module\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mqiskit\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 416\u001b[0m )\n\u001b[1;32m 417\u001b[0m warnings\u001b[38;5;241m.\u001b[39mfilterwarnings(\n\u001b[1;32m 418\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mignore\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 419\u001b[0m category\u001b[38;5;241m=\u001b[39m\u001b[38;5;167;01mDeprecationWarning\u001b[39;00m,\n\u001b[1;32m 420\u001b[0m message\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124m.*``backend_properties`` is deprecated as of Qiskit 1.3.*\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 421\u001b[0m module\u001b[38;5;241m=\u001b[39m\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mqiskit\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 422\u001b[0m )\n\u001b[0;32m--> 423\u001b[0m pm \u001b[38;5;241m=\u001b[39m generate_preset_pass_manager(\n\u001b[1;32m 424\u001b[0m optimization_level,\n\u001b[1;32m 425\u001b[0m target\u001b[38;5;241m=\u001b[39mtarget,\n\u001b[1;32m 426\u001b[0m backend\u001b[38;5;241m=\u001b[39mbackend,\n\u001b[1;32m 427\u001b[0m basis_gates\u001b[38;5;241m=\u001b[39mbasis_gates,\n\u001b[1;32m 428\u001b[0m coupling_map\u001b[38;5;241m=\u001b[39mcoupling_map,\n\u001b[1;32m 429\u001b[0m instruction_durations\u001b[38;5;241m=\u001b[39minstruction_durations,\n\u001b[1;32m 430\u001b[0m backend_properties\u001b[38;5;241m=\u001b[39mbackend_properties,\n\u001b[1;32m 431\u001b[0m timing_constraints\u001b[38;5;241m=\u001b[39mtiming_constraints,\n\u001b[1;32m 432\u001b[0m inst_map\u001b[38;5;241m=\u001b[39minst_map,\n\u001b[1;32m 433\u001b[0m initial_layout\u001b[38;5;241m=\u001b[39minitial_layout,\n\u001b[1;32m 434\u001b[0m layout_method\u001b[38;5;241m=\u001b[39mlayout_method,\n\u001b[1;32m 435\u001b[0m routing_method\u001b[38;5;241m=\u001b[39mrouting_method,\n\u001b[1;32m 436\u001b[0m translation_method\u001b[38;5;241m=\u001b[39mtranslation_method,\n\u001b[1;32m 437\u001b[0m scheduling_method\u001b[38;5;241m=\u001b[39mscheduling_method,\n\u001b[1;32m 438\u001b[0m approximation_degree\u001b[38;5;241m=\u001b[39mapproximation_degree,\n\u001b[1;32m 439\u001b[0m seed_transpiler\u001b[38;5;241m=\u001b[39mseed_transpiler,\n\u001b[1;32m 440\u001b[0m unitary_synthesis_method\u001b[38;5;241m=\u001b[39munitary_synthesis_method,\n\u001b[1;32m 441\u001b[0m unitary_synthesis_plugin_config\u001b[38;5;241m=\u001b[39munitary_synthesis_plugin_config,\n\u001b[1;32m 442\u001b[0m hls_config\u001b[38;5;241m=\u001b[39mhls_config,\n\u001b[1;32m 443\u001b[0m init_method\u001b[38;5;241m=\u001b[39minit_method,\n\u001b[1;32m 444\u001b[0m optimization_method\u001b[38;5;241m=\u001b[39moptimization_method,\n\u001b[1;32m 445\u001b[0m dt\u001b[38;5;241m=\u001b[39mdt,\n\u001b[1;32m 446\u001b[0m qubits_initially_zero\u001b[38;5;241m=\u001b[39mqubits_initially_zero,\n\u001b[1;32m 447\u001b[0m )\n\u001b[1;32m 449\u001b[0m out_circuits \u001b[38;5;241m=\u001b[39m pm\u001b[38;5;241m.\u001b[39mrun(circuits, callback\u001b[38;5;241m=\u001b[39mcallback, num_processes\u001b[38;5;241m=\u001b[39mnum_processes)\n\u001b[1;32m 451\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m name, circ \u001b[38;5;129;01min\u001b[39;00m \u001b[38;5;28mzip\u001b[39m(output_name, out_circuits):\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/utils/deprecation.py:184\u001b[0m, in \u001b[0;36mdeprecate_arg..decorator..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 171\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mwraps(func)\n\u001b[1;32m 172\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mwrapper\u001b[39m(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 173\u001b[0m _maybe_warn_and_rename_kwarg(\n\u001b[1;32m 174\u001b[0m args,\n\u001b[1;32m 175\u001b[0m kwargs,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 182\u001b[0m predicate\u001b[38;5;241m=\u001b[39mpredicate,\n\u001b[1;32m 183\u001b[0m )\n\u001b[0;32m--> 184\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m func(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/utils/deprecation.py:184\u001b[0m, in \u001b[0;36mdeprecate_arg..decorator..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 171\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mwraps(func)\n\u001b[1;32m 172\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mwrapper\u001b[39m(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 173\u001b[0m _maybe_warn_and_rename_kwarg(\n\u001b[1;32m 174\u001b[0m args,\n\u001b[1;32m 175\u001b[0m kwargs,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 182\u001b[0m predicate\u001b[38;5;241m=\u001b[39mpredicate,\n\u001b[1;32m 183\u001b[0m )\n\u001b[0;32m--> 184\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m func(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n",
+ " \u001b[0;31m[... skipping similar frames: deprecate_arg..decorator..wrapper at line 184 (1 times)]\u001b[0m\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/utils/deprecation.py:184\u001b[0m, in \u001b[0;36mdeprecate_arg..decorator..wrapper\u001b[0;34m(*args, **kwargs)\u001b[0m\n\u001b[1;32m 171\u001b[0m \u001b[38;5;129m@functools\u001b[39m\u001b[38;5;241m.\u001b[39mwraps(func)\n\u001b[1;32m 172\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mwrapper\u001b[39m(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs):\n\u001b[1;32m 173\u001b[0m _maybe_warn_and_rename_kwarg(\n\u001b[1;32m 174\u001b[0m args,\n\u001b[1;32m 175\u001b[0m kwargs,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 182\u001b[0m predicate\u001b[38;5;241m=\u001b[39mpredicate,\n\u001b[1;32m 183\u001b[0m )\n\u001b[0;32m--> 184\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m func(\u001b[38;5;241m*\u001b[39margs, \u001b[38;5;241m*\u001b[39m\u001b[38;5;241m*\u001b[39mkwargs)\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py:333\u001b[0m, in \u001b[0;36mgenerate_preset_pass_manager\u001b[0;34m(optimization_level, backend, target, basis_gates, inst_map, coupling_map, instruction_durations, backend_properties, timing_constraints, initial_layout, layout_method, routing_method, translation_method, scheduling_method, approximation_degree, seed_transpiler, unitary_synthesis_method, unitary_synthesis_plugin_config, hls_config, init_method, optimization_method, dt, qubits_initially_zero, _skip_target)\u001b[0m\n\u001b[1;32m 330\u001b[0m inst_map \u001b[38;5;241m=\u001b[39m _parse_inst_map(inst_map, backend)\n\u001b[1;32m 331\u001b[0m \u001b[38;5;66;03m# The basis gates parser will set _skip_target to True if a custom basis gate is found\u001b[39;00m\n\u001b[1;32m 332\u001b[0m \u001b[38;5;66;03m# (known edge case).\u001b[39;00m\n\u001b[0;32m--> 333\u001b[0m basis_gates, name_mapping, _skip_target \u001b[38;5;241m=\u001b[39m _parse_basis_gates(\n\u001b[1;32m 334\u001b[0m basis_gates, backend, inst_map, _skip_target\n\u001b[1;32m 335\u001b[0m )\n\u001b[1;32m 336\u001b[0m coupling_map \u001b[38;5;241m=\u001b[39m _parse_coupling_map(coupling_map, backend)\n\u001b[1;32m 338\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m target \u001b[38;5;129;01mis\u001b[39;00m \u001b[38;5;28;01mNone\u001b[39;00m:\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/transpiler/preset_passmanagers/generate_preset_pass_manager.py:496\u001b[0m, in \u001b[0;36m_parse_basis_gates\u001b[0;34m(basis_gates, backend, inst_map, skip_target)\u001b[0m\n\u001b[1;32m 492\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mlist\u001b[39m(instructions), name_mapping, skip_target\n\u001b[1;32m 494\u001b[0m instructions \u001b[38;5;241m=\u001b[39m instructions \u001b[38;5;129;01mor\u001b[39;00m backend\u001b[38;5;241m.\u001b[39moperation_names\n\u001b[1;32m 495\u001b[0m name_mapping\u001b[38;5;241m.\u001b[39mupdate(\n\u001b[0;32m--> 496\u001b[0m {name: backend\u001b[38;5;241m.\u001b[39mtarget\u001b[38;5;241m.\u001b[39moperation_from_name(name) \u001b[38;5;28;01mfor\u001b[39;00m name \u001b[38;5;129;01min\u001b[39;00m backend\u001b[38;5;241m.\u001b[39moperation_names}\n\u001b[1;32m 497\u001b[0m )\n\u001b[1;32m 499\u001b[0m \u001b[38;5;66;03m# Check for custom instructions before removing calibrations\u001b[39;00m\n\u001b[1;32m 500\u001b[0m \u001b[38;5;28;01mfor\u001b[39;00m inst \u001b[38;5;129;01min\u001b[39;00m instructions:\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit_aer/backends/aerbackend.py:262\u001b[0m, in \u001b[0;36mAerBackend.target\u001b[0;34m(self)\u001b[0m\n\u001b[1;32m 259\u001b[0m properties \u001b[38;5;241m=\u001b[39m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mproperties()\n\u001b[1;32m 261\u001b[0m \u001b[38;5;66;03m# Load Qiskit object representation\u001b[39;00m\n\u001b[0;32m--> 262\u001b[0m qiskit_inst_mapping \u001b[38;5;241m=\u001b[39m get_standard_gate_name_mapping()\n\u001b[1;32m 263\u001b[0m qiskit_inst_mapping\u001b[38;5;241m.\u001b[39mupdate(NAME_MAPPING)\n\u001b[1;32m 265\u001b[0m qiskit_control_flow_mapping \u001b[38;5;241m=\u001b[39m {\n\u001b[1;32m 266\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mif_else\u001b[39m\u001b[38;5;124m\"\u001b[39m: IfElseOp,\n\u001b[1;32m 267\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mwhile_loop\u001b[39m\u001b[38;5;124m\"\u001b[39m: WhileLoopOp,\n\u001b[1;32m 268\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mfor_loop\u001b[39m\u001b[38;5;124m\"\u001b[39m: ForLoopOp,\n\u001b[1;32m 269\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mswitch_case\u001b[39m\u001b[38;5;124m\"\u001b[39m: SwitchCaseOp,\n\u001b[1;32m 270\u001b[0m }\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/circuit/library/standard_gates/__init__.py:96\u001b[0m, in \u001b[0;36mget_standard_gate_name_mapping\u001b[0;34m()\u001b[0m\n\u001b[1;32m 81\u001b[0m time \u001b[38;5;241m=\u001b[39m Parameter(\u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mt\u001b[39m\u001b[38;5;124m\"\u001b[39m)\n\u001b[1;32m 83\u001b[0m \u001b[38;5;66;03m# Standard gates library mapping, multicontrolled gates not included since they're\u001b[39;00m\n\u001b[1;32m 84\u001b[0m \u001b[38;5;66;03m# variable width\u001b[39;00m\n\u001b[1;32m 85\u001b[0m gates \u001b[38;5;241m=\u001b[39m [\n\u001b[1;32m 86\u001b[0m IGate(),\n\u001b[1;32m 87\u001b[0m SXGate(),\n\u001b[1;32m 88\u001b[0m XGate(),\n\u001b[1;32m 89\u001b[0m CXGate(),\n\u001b[1;32m 90\u001b[0m RZGate(lambda_),\n\u001b[1;32m 91\u001b[0m RGate(theta, phi),\n\u001b[1;32m 92\u001b[0m C3SXGate(),\n\u001b[1;32m 93\u001b[0m CCXGate(),\n\u001b[1;32m 94\u001b[0m DCXGate(),\n\u001b[1;32m 95\u001b[0m CHGate(),\n\u001b[0;32m---> 96\u001b[0m CPhaseGate(theta),\n\u001b[1;32m 97\u001b[0m CRXGate(theta),\n\u001b[1;32m 98\u001b[0m CRYGate(theta),\n\u001b[1;32m 99\u001b[0m CRZGate(theta),\n\u001b[1;32m 100\u001b[0m CSwapGate(),\n\u001b[1;32m 101\u001b[0m CSXGate(),\n\u001b[1;32m 102\u001b[0m CUGate(theta, phi, lambda_, gamma),\n\u001b[1;32m 103\u001b[0m CU1Gate(lambda_),\n\u001b[1;32m 104\u001b[0m CU3Gate(theta, phi, lambda_),\n\u001b[1;32m 105\u001b[0m CYGate(),\n\u001b[1;32m 106\u001b[0m CZGate(),\n\u001b[1;32m 107\u001b[0m CCZGate(),\n\u001b[1;32m 108\u001b[0m GlobalPhaseGate(theta),\n\u001b[1;32m 109\u001b[0m HGate(),\n\u001b[1;32m 110\u001b[0m PhaseGate(theta),\n\u001b[1;32m 111\u001b[0m RCCXGate(),\n\u001b[1;32m 112\u001b[0m RC3XGate(),\n\u001b[1;32m 113\u001b[0m RXGate(theta),\n\u001b[1;32m 114\u001b[0m RXXGate(theta),\n\u001b[1;32m 115\u001b[0m RYGate(theta),\n\u001b[1;32m 116\u001b[0m RYYGate(theta),\n\u001b[1;32m 117\u001b[0m RZZGate(theta),\n\u001b[1;32m 118\u001b[0m RZXGate(theta),\n\u001b[1;32m 119\u001b[0m XXMinusYYGate(theta, beta),\n\u001b[1;32m 120\u001b[0m XXPlusYYGate(theta, beta),\n\u001b[1;32m 121\u001b[0m ECRGate(),\n\u001b[1;32m 122\u001b[0m SGate(),\n\u001b[1;32m 123\u001b[0m SdgGate(),\n\u001b[1;32m 124\u001b[0m CSGate(),\n\u001b[1;32m 125\u001b[0m CSdgGate(),\n\u001b[1;32m 126\u001b[0m SwapGate(),\n\u001b[1;32m 127\u001b[0m iSwapGate(),\n\u001b[1;32m 128\u001b[0m SXdgGate(),\n\u001b[1;32m 129\u001b[0m TGate(),\n\u001b[1;32m 130\u001b[0m TdgGate(),\n\u001b[1;32m 131\u001b[0m UGate(theta, phi, lambda_),\n\u001b[1;32m 132\u001b[0m U1Gate(lambda_),\n\u001b[1;32m 133\u001b[0m U2Gate(phi, lambda_),\n\u001b[1;32m 134\u001b[0m U3Gate(theta, phi, lambda_),\n\u001b[1;32m 135\u001b[0m YGate(),\n\u001b[1;32m 136\u001b[0m ZGate(),\n\u001b[1;32m 137\u001b[0m Delay(time),\n\u001b[1;32m 138\u001b[0m Reset(),\n\u001b[1;32m 139\u001b[0m Measure(),\n\u001b[1;32m 140\u001b[0m ]\n\u001b[1;32m 141\u001b[0m name_mapping \u001b[38;5;241m=\u001b[39m {gate\u001b[38;5;241m.\u001b[39mname: gate \u001b[38;5;28;01mfor\u001b[39;00m gate \u001b[38;5;129;01min\u001b[39;00m gates}\n\u001b[1;32m 142\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m name_mapping\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/circuit/library/standard_gates/p.py:216\u001b[0m, in \u001b[0;36mCPhaseGate.__init__\u001b[0;34m(self, theta, label, ctrl_state, duration, unit, _base_label)\u001b[0m\n\u001b[1;32m 205\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21m__init__\u001b[39m(\n\u001b[1;32m 206\u001b[0m \u001b[38;5;28mself\u001b[39m,\n\u001b[1;32m 207\u001b[0m theta: ParameterValueType,\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 213\u001b[0m _base_label\u001b[38;5;241m=\u001b[39m\u001b[38;5;28;01mNone\u001b[39;00m,\n\u001b[1;32m 214\u001b[0m ):\n\u001b[1;32m 215\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Create new CPhase gate.\"\"\"\u001b[39;00m\n\u001b[0;32m--> 216\u001b[0m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39m\u001b[38;5;21m__init__\u001b[39m(\n\u001b[1;32m 217\u001b[0m \u001b[38;5;124m\"\u001b[39m\u001b[38;5;124mcp\u001b[39m\u001b[38;5;124m\"\u001b[39m,\n\u001b[1;32m 218\u001b[0m \u001b[38;5;241m2\u001b[39m,\n\u001b[1;32m 219\u001b[0m [theta],\n\u001b[1;32m 220\u001b[0m num_ctrl_qubits\u001b[38;5;241m=\u001b[39m\u001b[38;5;241m1\u001b[39m,\n\u001b[1;32m 221\u001b[0m label\u001b[38;5;241m=\u001b[39mlabel,\n\u001b[1;32m 222\u001b[0m ctrl_state\u001b[38;5;241m=\u001b[39mctrl_state,\n\u001b[1;32m 223\u001b[0m base_gate\u001b[38;5;241m=\u001b[39mPhaseGate(theta, label\u001b[38;5;241m=\u001b[39m_base_label),\n\u001b[1;32m 224\u001b[0m duration\u001b[38;5;241m=\u001b[39mduration,\n\u001b[1;32m 225\u001b[0m unit\u001b[38;5;241m=\u001b[39munit,\n\u001b[1;32m 226\u001b[0m )\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/circuit/controlledgate.py:106\u001b[0m, in \u001b[0;36mControlledGate.__init__\u001b[0;34m(self, name, num_qubits, params, label, num_ctrl_qubits, definition, ctrl_state, base_gate, duration, unit, _base_label)\u001b[0m\n\u001b[1;32m 104\u001b[0m \u001b[38;5;28msuper\u001b[39m()\u001b[38;5;241m.\u001b[39m\u001b[38;5;21m__init__\u001b[39m(name, num_qubits, params, label\u001b[38;5;241m=\u001b[39mlabel, duration\u001b[38;5;241m=\u001b[39mduration, unit\u001b[38;5;241m=\u001b[39munit)\n\u001b[1;32m 105\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_num_ctrl_qubits \u001b[38;5;241m=\u001b[39m \u001b[38;5;241m1\u001b[39m\n\u001b[0;32m--> 106\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mnum_ctrl_qubits \u001b[38;5;241m=\u001b[39m num_ctrl_qubits\n\u001b[1;32m 107\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39mdefinition \u001b[38;5;241m=\u001b[39m copy\u001b[38;5;241m.\u001b[39mdeepcopy(definition)\n\u001b[1;32m 108\u001b[0m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_ctrl_state \u001b[38;5;241m=\u001b[39m \u001b[38;5;28;01mNone\u001b[39;00m\n",
+ "File \u001b[0;32m/opt/anaconda3/lib/python3.12/site-packages/qiskit/circuit/controlledgate.py:178\u001b[0m, in \u001b[0;36mControlledGate.num_ctrl_qubits\u001b[0;34m(self, num_ctrl_qubits)\u001b[0m\n\u001b[1;32m 171\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Get number of control qubits.\u001b[39;00m\n\u001b[1;32m 172\u001b[0m \n\u001b[1;32m 173\u001b[0m \u001b[38;5;124;03m Returns:\u001b[39;00m\n\u001b[1;32m 174\u001b[0m \u001b[38;5;124;03m int: The number of control qubits for the gate.\u001b[39;00m\n\u001b[1;32m 175\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m 176\u001b[0m \u001b[38;5;28;01mreturn\u001b[39;00m \u001b[38;5;28mself\u001b[39m\u001b[38;5;241m.\u001b[39m_num_ctrl_qubits\n\u001b[0;32m--> 178\u001b[0m \u001b[38;5;129m@num_ctrl_qubits\u001b[39m\u001b[38;5;241m.\u001b[39msetter\n\u001b[1;32m 179\u001b[0m \u001b[38;5;28;01mdef\u001b[39;00m \u001b[38;5;21mnum_ctrl_qubits\u001b[39m(\u001b[38;5;28mself\u001b[39m, num_ctrl_qubits):\n\u001b[1;32m 180\u001b[0m \u001b[38;5;250m \u001b[39m\u001b[38;5;124;03m\"\"\"Set the number of control qubits.\u001b[39;00m\n\u001b[1;32m 181\u001b[0m \n\u001b[1;32m 182\u001b[0m \u001b[38;5;124;03m Args:\u001b[39;00m\n\u001b[0;32m (...)\u001b[0m\n\u001b[1;32m 186\u001b[0m \u001b[38;5;124;03m CircuitError: ``num_ctrl_qubits`` is not an integer in ``[1, num_qubits]``.\u001b[39;00m\n\u001b[1;32m 187\u001b[0m \u001b[38;5;124;03m \"\"\"\u001b[39;00m\n\u001b[1;32m 188\u001b[0m \u001b[38;5;28;01mif\u001b[39;00m num_ctrl_qubits \u001b[38;5;241m!=\u001b[39m \u001b[38;5;28mint\u001b[39m(num_ctrl_qubits):\n",
+ "\u001b[0;31mKeyboardInterrupt\u001b[0m: "
+ ]
+ }
+ ],
+ "source": [
+ "#classical Betti solver\n",
+ "import matplotlib.pyplot as plt\n",
+ "from ripser import ripser\n",
+ "\n",
+ "def classical_betti_solver(point_cloud, epsilon, dim):\n",
+ " '''Return the Betti number on a given point cloud.\n",
+ " Args:\n",
+ " point_cloud: the point cloud after applying the sliding window.\n",
+ " epsilon: resolution threshold.\n",
+ " dim: the dimension on which the Betti number is calculated\n",
+ " '''\n",
+ " result = ripser(point_cloud, maxdim=dim)\n",
+ " diagrams = result[\"dgms\"]\n",
+ " return len(\n",
+ " [interval for interval in diagrams[dim] if interval[0] < epsilon < interval[1]]\n",
+ " )\n",
+ "\n",
+ "# write your code here\n",
+ "from tqdm import tqdm\n",
+ "\n",
+ "N = 3 # embedding dimension\n",
+ "d = 1 # time delay\n",
+ "w =5 # window size\n",
+ "L = len(time_series)\n",
+ "vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ "K = L - (N-1)*d - w + 1 # number of windows\n",
+ "point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ "print(f\"N = {N}, w = {w}, L = {L}\")\n",
+ "point_cloud = point_clouds[0]\n",
+ "\n",
+ "# Define ranges\n",
+ "eps_range = np.linspace(0, 0.05, 1000)\n",
+ "k_range = [0] # Calculate for k = 0, 1, and 2\n",
+ "\n",
+ "# Dictionary to store Betti numbers for each k\n",
+ "betti_numbers = {k: [] for k in k_range}\n",
+ "\n",
+ "# Calculate Betti numbers for each epsilon and k with progress bar\n",
+ "total_iterations = len(eps_range) * len(k_range)\n",
+ "with tqdm(total=total_iterations, desc=\"Calculating Betti numbers\") as pbar:\n",
+ " for eps in eps_range:\n",
+ " for k in k_range:\n",
+ " betti_numbers[k].append(get_betti_number(point_cloud, eps, k))\n",
+ " pbar.update(1)\n",
+ "\n",
+ "# Create plot\n",
+ "plt.figure(figsize=(10, 6))\n",
+ "colors = ['blue', 'red', 'green'] # One color for each k\n",
+ "\n",
+ "# Plot each k dimension\n",
+ "for k, color in zip(k_range, colors):\n",
+ " plt.plot(eps_range, betti_numbers[k], \n",
+ " color=color, \n",
+ " label=f'β₍{k}₎')\n",
+ "\n",
+ "plt.xlabel('ε (Epsilon)')\n",
+ "plt.ylabel('Betti Number')\n",
+ "plt.title('Betti Curves')\n",
+ "plt.legend()\n",
+ "plt.grid(True)\n",
+ "plt.show()\n",
+ "\n",
+ "# Print maximum values\n",
+ "for k in k_range:\n",
+ " print(f\"Maximum β₍{k}₎: {max(betti_numbers[k])}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 106,
+ "id": "d9943731-d546-43a2-8f3d-d226d7d79e5e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "#quantum betti solver\n",
+ "\n",
+ "def get_betti_number(point_cloud, epsilon, dim):\n",
+ " # print(\"Constructing tree\")\n",
+ " simplex_tree = get_simplex_tree(point_cloud, epsilon)\n",
+ " # print(f\"num 0-simplices: {len(simplex_tree[0])}\")\n",
+ " # print(f\"num 1-simplices: {len(simplex_tree[1])}\")\n",
+ " # print(f\"num 2-simplices: {len(simplex_tree[2])}\")\n",
+ " if len(simplex_tree[dim]) == 0:\n",
+ " return 0\n",
+ " # print(\"Computing laplacian\")\n",
+ " laplacian = get_laplacian(dim, simplex_tree)\n",
+ " if laplacian.shape[0] ==1:\n",
+ " return 1 if abs(laplacian[0][0]) < 1e-9 else 0\n",
+ " # print(f\"Laplacian: {laplacian.shape}\")\n",
+ " print(laplacian)\n",
+ " # print(\"Computing unitary\")\n",
+ " unitary = get_unitary(laplacian)\n",
+ " # print(f\"Unitary: {unitary.shape}\")\n",
+ " # print(unitary)\n",
+ " # print(\"Performing QPE\")\n",
+ " betti_number = analyze_matrix(unitary)[0]\n",
+ " return betti_number"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "5a0df25c-2832-4b19-8898-0dd29355f3b0",
+ "metadata": {},
+ "outputs": [],
+ "source": []
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 109,
+ "id": "18de4e5a-8e1b-456c-a9ae-6c3e2422c868",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "N = 10, w = 50, L = 5536\n"
+ ]
+ }
+ ],
+ "source": [
+ "from tqdm import tqdm\n",
+ "\n",
+ "N = 10 # embedding dimension\n",
+ "d = 1 # time delay\n",
+ "w = 50 # window size\n",
+ "L = len(time_series)\n",
+ "vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ "K = L - (N-1)*d - w + 1 # number of windows\n",
+ "point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ "print(f\"N = {N}, w = {w}, L = {L}\")\n",
+ "point_cloud = point_clouds[0]"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 111,
+ "id": "246c0cb5-20c7-4455-a90e-9cf489928669",
+ "metadata": {},
+ "outputs": [
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "N = 10, w = 50, L = 5536\n"
+ ]
+ },
+ {
+ "name": "stderr",
+ "output_type": "stream",
+ "text": [
+ "Calculating Betti numbers: 100%|██████████| 3000/3000 [00:02<00:00, 1112.23it/s]\n"
+ ]
+ },
+ {
+ "data": {
+ "image/png": "iVBORw0KGgoAAAANSUhEUgAAA0oAAAIhCAYAAABwnkrAAAAAOXRFWHRTb2Z0d2FyZQBNYXRwbG90bGliIHZlcnNpb24zLjkuMiwgaHR0cHM6Ly9tYXRwbG90bGliLm9yZy8hTgPZAAAACXBIWXMAAA9hAAAPYQGoP6dpAABZjUlEQVR4nO3deXhU5d3G8XsymWyQEJAsgGGLEaQsIlEIVMAlYVFcqMsLLkjV2ooCUkuhaAnUN1iqiBXFCgpaBVqrqH1FTFxYKqCAIAiI1AKiEhNkyZ5MkvP+MZ2RJCwzycycmcn3c11zzcyZM3N+Ex+BO7/nPMdiGIYhAAAAAIBLmNkFAAAAAECgISgBAAAAQD0EJQAAAACoh6AEAAAAAPUQlAAAAACgHoISAAAAANRDUAIAAACAeghKAAAAAFAPQQkAAAAA6iEoAQDOaOnSpbJYLHVuCQkJGjp0qP7v//6v0Z/7zDPPaOnSpQ22f/fdd8rOztb27dsbvJadnS2LxeL2MdavX6+bbrpJHTp0UEREhFq1aqWBAwdq4cKFKi0tbXTtAIDQR1ACALhlyZIl2rhxozZs2KDnnntOVqtVo0aN0j//+c9Gfd6ZgtKsWbNOGZTuuusubdy40a3PnzlzpgYPHqxvv/1Wf/jDH5SXl6cVK1boiiuuUHZ2th566KFG1Q0AaB7CzS4AABAcevbsqfT0dNfz4cOHq3Xr1lq+fLlGjRrllxrOPfdcnXvuuWfd79VXX9Xs2bN15513atGiRXW6UCNGjNDUqVPdDlxnU1ZWppiYGK98FgAgcNBRAgA0SlRUlCIiImSz2epsr6qq0iOPPKLu3bsrMjJSCQkJGj9+vAoLC137dO7cWbt27dLatWtd0/k6d+6sNWvW6OKLL5YkjR8/3vVadna2JPen3s2ePVutW7fWn//851PuHxsbq6ysLEnSgQMHZLFYTtndOvnYJx//008/1Q033KDWrVsrNTVV8+fPl8Vi0b///e8Gn/Hb3/5WEREROnLkiGvbe++9pyuuuEJxcXGKiYnRoEGD9P7779d5X2FhoX7xi18oJSXF9XMcNGiQ3nvvvbN+fwBA0xGUAABuqampUXV1tex2u7755htNnjxZpaWlGjt2rGuf2tpaXXvttXr00Uc1duxYvf3223r00UeVl5enoUOHqry8XJK0cuVKde3aVX379tXGjRu1ceNGrVy5UhdddJGWLFkiSXrooYdcr911111u13n48GF9/vnnysrK8lmnZ/To0TrvvPP06quv6tlnn9Wtt96qiIiIBmGrpqZGL7/8skaNGqW2bdtKkl5++WVlZWUpLi5OL774ov7+97+rTZs2GjZsWJ2wdNttt+mNN97Q73//e+Xm5mrx4sW68sor9cMPP/jkOwEA6mLqHQDALQMGDKjzPDIyUgsWLNCwYcNc2/7+979r9erVeu211zR69GjX9j59+ujiiy/W0qVL9atf/Up9+/ZVdHS04uLiGnxuz549JUmpqakNXnPH119/LUnq0qWLx+9117hx4zRr1qw6266++mq9+OKLmj17tsLCHL+HzM3N1Xfffafx48dLckzTmzRpkq6++mqtXLnS9d6RI0fqoosu0u9+9zt9/PHHkqSPPvpId911l+6++27Xftdee63PvhMAoC46SgAAt7z00kvavHmzNm/erHfeeUfjxo3ThAkTtGDBAtc+//d//6f4+HiNGjVK1dXVrtuFF16o5ORkrVmzxrwv4EU/+9nPGmwbP368vvnmmzpT45YsWaLk5GSNGDFCkrRhwwYdPXpU48aNq/Pzqa2t1fDhw7V582bXanyXXHKJli5dqkceeUSbNm2S3W73z5cDAEgiKAEA3HTBBRcoPT1d6enpGj58uP7yl78oKytLU6dO1fHjxyVJ33//vY4fP+46d+nkW35+fp3zdHylY8eOkqT9+/f77Bjt2rVrsG3EiBFq166da+rgsWPH9NZbb+n222+X1WqV5Pj5SNINN9zQ4Ofzxz/+UYZh6OjRo5Kkv/3tbxo3bpwWL16sjIwMtWnTRrfffrvy8/N99r0AAD9i6h0AoNF69+6td999V19++aUuueQStW3bVuecc45Wr159yv1jY2N9XlO7du3Uq1cv5ebmurUiXVRUlCSpsrKyzvYznQt0qgUirFarbrvtNv35z3/W8ePHtWzZMlVWVrqm3Ulynaf01FNPnXZaYVJSkmvf+fPna/78+fr666/11ltvadq0aSooKDjtzxcA4D0EJQBAozmvdZSQkCDJcZ7OihUrVFNTo/79+5/xvZGRka7FHepvl3TK19z18MMP66abbtLEiRMbLA8uSSUlJdqwYYOysrKUlJSkqKgo7dixo84+b775psfHHT9+vObOnavly5dr6dKlysjIUPfu3V2v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+/fv9+lxAADeQVACAASMb775Rm+99ZbGjx/f4LW9e/fq8OHDdW6JiYleOW6rVq0UHx/vlc86ncTERGVlZenZZ5/16XEAAN5BUAIA+MymTZs0aNAgxcbGqkWLFurVq5c2b9582v3//ve/q0+fPjr33HMbvJaYmKjk5OQ6t7Awx19jzqlzs2bNUmJiouLi4nTPPfeoqqrK9f5//OMf6tWrl6Kjo3XOOefoyiuvVGlpaZ33n86xY8d0++23q3Xr1oqJidGIESO0b98+1+tLly5VfHy83n33XV1wwQVq2bKlhg8frsOHD9f5nGuuuUbLly9362cHADAXQQkA4DNjxoxRly5d9Mknn+jzzz/X/PnzlZSUdNr9161bp/T09EYd6/3339eePXv04Ycfavny5Vq5cqVmzZolSTp8+LDGjBmjn//859qzZ4/WrFmj0aNHyzAMtz77jjvu0JYtW/TWW29p48aNMgxDI0eOlN1ud+1TVlamxx57TH/961+1bt06ff3113rwwQfrfM4ll1yiQ4cO6eDBg436jgAA/wk3uwAAQOiqqalRx44ddd5558lms6lLly5n3P/AgQPq16/fKV+r32Xq0KGD9u7d63oeERGhF154QTExMfrJT36i2bNn6ze/+Y3+8Ic/6PDhw6qurtbo0aPVqVMnSVKvXr3c+g779u3TW2+9pY8++kgDBw6UJL3yyitKSUnRG2+8oRtvvFGSZLfb9eyzzyo1NVWSdN9992n27NkNanZ+T2cdAIDARFACAPjM66+/ruuvv15z585VVFSUvv32W7Vq1eq0+5eXlysqKuqUr61fv961wIMkhYfX/SusT58+iomJcT3PyMhQSUmJDh06pD59+uiKK65Qr169NGzYMGVlZemGG25Q69atz/od9uzZo/DwcPXv39+17ZxzzlG3bt20Z88e17aYmBhXSJKkdu3aqaCgoM5nRUdHS3J0nwAAgY2pdwAAn5k+fbouuugibdiwQdu3b68TdE6lbdu2Onbs2Clf69Kli8477zzXrXPnzm7VYLFYZLValZeXp3feeUc9evTQU089pW7durm1At3ppucZhiGLxeJ6brPZGhy3/nuPHj0qSUpISHCrdgCAeQhKAACfOHLkiN577z3Nnj1bl1xyic477zzX4gun07dvX+3evbtRx/vss89UXl7uer5p0ya1bNnSNWXPYrFo0KBBmjVrlrZt26aIiAitXLnyrJ/bo0cPVVdX6+OPP3Zt++GHH/Tll1/qggsu8KjGzz//XDabTT/5yU88eh8AwP8ISgAAn2jbtq1SUlL0+9//Xlu3btXBgwe1Zs0a5ebmnvY9w4YN08aNG1VTU9PgtYKCAuXn59e5nbyYQlVVle68807t3r1b77zzjmbOnKn77rtPYWFh+vjjj5WTk6MtW7bo66+/1uuvv67CwkK3gk5aWpquvfZa3X333frXv/6lzz77TLfeeqs6dOiga6+91qOfyfr163XppZe6puABAAIXQQkA4DPvvPOOamtrNWzYMJ1//vm6++679f333592/5EjR8pms+m9995r8Fq3bt3Url27OretW7e6Xr/iiiuUlpamwYMH66abbtKoUaOUnZ0tSYqLi9O6des0cuRInX/++XrooYf0+OOPa8SIEW59jyVLlqhfv366+uqrlZGRIcMwtGrVqgbT7c5m+fLluvvuuz16DwDAHBbD3bVRAQDwg2eeeUZvvvmm3n33Xbffc8cdd+j48eN64403fFdYE7399tv6zW9+ox07djRYiAIAEHj4kxoAEFB+8Ytf6NixYyouLj7r4g/BpLS0VEuWLCEkAUCQoKMEAAh6wdBRAgAEF4ISAAAAANTDYg4AAAAAUA9BCQAAAADqISgBAAAAQD0EJQAAAACoh6AEAAAAAPUQlAAAAACgHoISAAAAANRDUAIAAACAev4fS+bmGEmYY+MAAAAASUVORK5CYII=",
+ "text/plain": [
+ "
"
+ ]
+ },
+ "metadata": {},
+ "output_type": "display_data"
+ },
+ {
+ "name": "stdout",
+ "output_type": "stream",
+ "text": [
+ "Maximum β₍0₎: 50\n",
+ "Maximum β₍1₎: 1\n",
+ "Maximum β₍2₎: 0\n"
+ ]
+ }
+ ],
+ "source": [
+ "\n",
+ "N = 10 # embedding dimension\n",
+ "d = 1 # time delay\n",
+ "w = 50 # window size\n",
+ "L = len(time_series)\n",
+ "vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ "K = L - (N-1)*d - w + 1 # number of windows\n",
+ "point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ "print(f\"N = {N}, w = {w}, L = {L}\")\n",
+ "point_cloud = point_clouds[0]\n",
+ "\n",
+ "# Define ranges\n",
+ "eps_range = np.linspace(0, 0.05, 1000)\n",
+ "k_range = [0,1,2] # Calculate for k = 0, 1, and 2\n",
+ "\n",
+ "# Dictionary to store Betti numbers for each k\n",
+ "betti_numbers = {k: [] for k in k_range}\n",
+ "\n",
+ "# Calculate Betti numbers for each epsilon and k with progress bar\n",
+ "total_iterations = len(eps_range) * len(k_range)\n",
+ "with tqdm(total=total_iterations, desc=\"Calculating Betti numbers\") as pbar:\n",
+ " for eps in eps_range:\n",
+ " for k in k_range:\n",
+ " betti_numbers[k].append(classical_betti_solver(point_cloud, eps, k))\n",
+ " pbar.update(1)\n",
+ "\n",
+ "# Create plot\n",
+ "plt.figure(figsize=(10, 6))\n",
+ "colors = ['blue', 'red', 'green'] # One color for each k\n",
+ "\n",
+ "# Plot each k dimension\n",
+ "for k, color in zip(k_range, colors):\n",
+ " plt.plot(eps_range, betti_numbers[k], \n",
+ " color=color, \n",
+ " label=f'β₍{k}₎')\n",
+ "\n",
+ "plt.xlabel('ε (Epsilon)')\n",
+ "plt.ylabel('Betti Number')\n",
+ "plt.title('Betti Curves')\n",
+ "plt.legend()\n",
+ "plt.grid(True)\n",
+ "plt.show()\n",
+ "\n",
+ "# Print maximum values\n",
+ "for k in k_range:\n",
+ " print(f\"Maximum β₍{k}₎: {max(betti_numbers[k])}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 113,
+ "id": "81f2a530-f496-464c-98fa-868c334b443d",
+ "metadata": {},
+ "outputs": [
+ {
+ "data": {
+ "text/html": [
+ "
\n",
+ "\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
\n",
+ "
0
\n",
+ "
1
\n",
+ "
price
\n",
+ "
date
\n",
+ "
\n",
+ " \n",
+ " \n",
+ "
\n",
+ "
0
\n",
+ "
03/01/2000
\n",
+ "
1460.200
\n",
+ "
1460.200
\n",
+ "
2000-01-03
\n",
+ "
\n",
+ "
\n",
+ "
1
\n",
+ "
04/01/2000
\n",
+ "
1426.800
\n",
+ "
1426.800
\n",
+ "
2000-01-04
\n",
+ "
\n",
+ "
\n",
+ "
2
\n",
+ "
05/01/2000
\n",
+ "
1398.125
\n",
+ "
1398.125
\n",
+ "
2000-01-05
\n",
+ "
\n",
+ "
\n",
+ "
3
\n",
+ "
06/01/2000
\n",
+ "
1402.375
\n",
+ "
1402.375
\n",
+ "
2000-01-06
\n",
+ "
\n",
+ "
\n",
+ "
4
\n",
+ "
07/01/2000
\n",
+ "
1421.750
\n",
+ "
1421.750
\n",
+ "
2000-01-07
\n",
+ "
\n",
+ "
\n",
+ "
...
\n",
+ "
...
\n",
+ "
...
\n",
+ "
...
\n",
+ "
...
\n",
+ "
\n",
+ "
\n",
+ "
5531
\n",
+ "
27/12/2021
\n",
+ "
4762.675
\n",
+ "
4762.675
\n",
+ "
2021-12-27
\n",
+ "
\n",
+ "
\n",
+ "
5532
\n",
+ "
28/12/2021
\n",
+ "
4792.225
\n",
+ "
4792.225
\n",
+ "
2021-12-28
\n",
+ "
\n",
+ "
\n",
+ "
5533
\n",
+ "
29/12/2021
\n",
+ "
4790.975
\n",
+ "
4790.975
\n",
+ "
2021-12-29
\n",
+ "
\n",
+ "
\n",
+ "
5534
\n",
+ "
30/12/2021
\n",
+ "
4789.275
\n",
+ "
4789.275
\n",
+ "
2021-12-30
\n",
+ "
\n",
+ "
\n",
+ "
5535
\n",
+ "
31/12/2021
\n",
+ "
4773.500
\n",
+ "
4773.500
\n",
+ "
2021-12-31
\n",
+ "
\n",
+ " \n",
+ "
\n",
+ "
5536 rows × 4 columns
\n",
+ "
"
+ ],
+ "text/plain": [
+ " 0 1 price date\n",
+ "0 03/01/2000 1460.200 1460.200 2000-01-03\n",
+ "1 04/01/2000 1426.800 1426.800 2000-01-04\n",
+ "2 05/01/2000 1398.125 1398.125 2000-01-05\n",
+ "3 06/01/2000 1402.375 1402.375 2000-01-06\n",
+ "4 07/01/2000 1421.750 1421.750 2000-01-07\n",
+ "... ... ... ... ...\n",
+ "5531 27/12/2021 4762.675 4762.675 2021-12-27\n",
+ "5532 28/12/2021 4792.225 4792.225 2021-12-28\n",
+ "5533 29/12/2021 4790.975 4790.975 2021-12-29\n",
+ "5534 30/12/2021 4789.275 4789.275 2021-12-30\n",
+ "5535 31/12/2021 4773.500 4773.500 2021-12-31\n",
+ "\n",
+ "[5536 rows x 4 columns]"
+ ]
+ },
+ "execution_count": 113,
+ "metadata": {},
+ "output_type": "execute_result"
+ }
+ ],
+ "source": [
+ "df['price'] = df[1]\n",
+ "df['date'] = pd.to_datetime(df[0], format = \"%d/%m/%Y\")\n",
+ "df"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 115,
+ "id": "a05a4c13-54ed-4304-ad37-541d0347257c",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "dfsmall['price'] = dfsmall[0]\n",
+ "df['index']= np.arange(0,len(time_series),1)\n",
+ "dfsmall"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 120,
+ "id": "2d71eb4b-33e0-432e-99dc-2c132de38776",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import numpy as np\n",
+ "import pandas as pd\n",
+ "import matplotlib.pyplot as plt\n",
+ "from tqdm import tqdm\n",
+ "\n",
+ "def compute_betti_curve(point_cloud, epsilons, dims):\n",
+ " \"\"\"Compute Betti numbers for a range of epsilon values for multiple dimensions.\"\"\"\n",
+ " return {dim: np.array([classical_betti_solver(point_cloud, epsilon, dim) \n",
+ " for epsilon in epsilons]) \n",
+ " for dim in dims}\n",
+ "\n",
+ "def normalize_betti_curves(betti_curves, dims):\n",
+ " \"\"\"Normalize Betti curves across all dimensions.\"\"\"\n",
+ " normalized_curves = []\n",
+ " \n",
+ " for curve in betti_curves:\n",
+ " # Find max value across all dimensions for this window\n",
+ " max_vals = {dim: np.max(curve[dim]) for dim in dims}\n",
+ " overall_max = max(max_vals.values())\n",
+ " \n",
+ " # Normalize each dimension by the overall max if it's non-zero\n",
+ " normalized = {}\n",
+ " for dim in dims:\n",
+ " if overall_max > 0:\n",
+ " normalized[dim] = curve[dim] / overall_max\n",
+ " else:\n",
+ " normalized[dim] = curve[dim]\n",
+ " normalized_curves.append(normalized)\n",
+ " \n",
+ " return normalized_curves\n",
+ "\n",
+ "def compute_lp_distances(df, window_size=7, p=2):\n",
+ " \"\"\"Compute Lp norm distances between successive windows of price data.\"\"\"\n",
+ " time_series = np.log(df['price']).to_numpy().squeeze()\n",
+ " \n",
+ " # Takens embedding parameters\n",
+ " N = 2 # embedding dimension\n",
+ " d = 1 # time delay\n",
+ " w = window_size\n",
+ " L = len(time_series)\n",
+ " dims = [0] # Now including dimension 2\n",
+ " \n",
+ " # Create range of epsilon values\n",
+ " epsilons = np.linspace(0, 0.05, 10)\n",
+ " \n",
+ " # Create point clouds using Takens embedding\n",
+ " vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ " K = L - (N-1)*d - w + 1 # number of windows\n",
+ " point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ " print(f\"N = {N}, w = {w}, L = {L}, K = {K}\")\n",
+ " \n",
+ " # Compute Betti curves for each window\n",
+ " betti_curves = []\n",
+ " for point_cloud in tqdm(point_clouds, desc=\"Computing Betti curves\"):\n",
+ " betti_curve = compute_betti_curve(point_cloud, epsilons, dims)\n",
+ " betti_curves.append(betti_curve)\n",
+ " \n",
+ " # Normalize Betti curves\n",
+ " normalized_curves = normalize_betti_curves(betti_curves, dims)\n",
+ " \n",
+ " # Compute Lp distances between successive normalized Betti curves\n",
+ " distances = {}\n",
+ " for dim in dims:\n",
+ " dim_distances = [np.power(np.sum(np.power(np.abs(b1[dim]), p)), 1/p) \n",
+ " for b1 in normalized_curves[:-1]]\n",
+ " \n",
+ " # Normalize distances within each dimension\n",
+ " dim_distances = np.array(dim_distances)\n",
+ " if np.max(dim_distances) > 0:\n",
+ " dim_distances = dim_distances / np.max(dim_distances)\n",
+ " \n",
+ " distances[dim] = pd.Series(dim_distances, \n",
+ " index=df.index[w+(N-1)*d:w+(N-1)*d+len(dim_distances)])\n",
+ " \n",
+ " # Add moving average smoothing\n",
+ " distances[f\"{dim}_ma\"] = distances[dim].rolling(window=5).mean()\n",
+ " \n",
+ " return distances, normalized_curves, epsilons\n",
+ "\n",
+ "def plot_lp_vs_price_and_betti(df, distances, betti_curves, epsilons, interval=100):\n",
+ " \"\"\"Plot price, Lp distances, and Betti curves at specified intervals.\"\"\"\n",
+ " fig = plt.figure(figsize=(20, 12))\n",
+ " gs = fig.add_gridspec(3, 1, height_ratios=[2, 2, 2])\n",
+ " \n",
+ " # Price plot\n",
+ " ax1 = fig.add_subplot(gs[0])\n",
+ " ax1.plot(df['date'].to_numpy(), df['price'].to_numpy(), 'b-', label='Price')\n",
+ " ax1.set_ylabel('Price')\n",
+ " ax1.legend()\n",
+ " \n",
+ " # Lp distances plot with moving averages\n",
+ " ax2 = fig.add_subplot(gs[1])\n",
+ " colors = ['r', 'b', 'g'] # Colors for dimensions 0, 1, 2\n",
+ " \n",
+ " # Plot raw distances in lighter colors\n",
+ " for dim, color in zip([0, 1, 2], colors):\n",
+ " ax2.plot(distances[dim].index.to_numpy(), distances[dim].to_numpy(), \n",
+ " f'{color}:', alpha=0.3, label=f'β₁ (raw)')\n",
+ " ax2.plot(distances[f'{dim}_ma'].index.to_numpy(), \n",
+ " distances[f'{dim}_ma'].to_numpy(), f'{color}-', \n",
+ " label=f'β{dim} (MA)')\n",
+ " \n",
+ " ax2.set_ylabel('Normalized Lp Distance')\n",
+ " ax2.legend()\n",
+ " \n",
+ " # Betti curves plot\n",
+ " ax3 = fig.add_subplot(gs[2])\n",
+ " linestyles = ['-', '--', ':'] # Different line styles for each dimension\n",
+ " colors = plt.cm.rainbow(np.linspace(0, 1, len(range(0, len(betti_curves), interval))))\n",
+ " \n",
+ " selected_indices = range(0, len(betti_curves), interval)\n",
+ " for color_idx, idx in enumerate(selected_indices):\n",
+ " color = colors[color_idx]\n",
+ " for dim, ls in zip([0, 1, 2], linestyles):\n",
+ " label = f't={idx}, β{dim}'\n",
+ " ax3.plot(epsilons, betti_curves[idx][dim], ls, color=color, label=label)\n",
+ " \n",
+ " ax3.set_xlabel('Epsilon')\n",
+ " ax3.set_ylabel('Normalized Betti Number')\n",
+ " ax3.legend(loc='center left', bbox_to_anchor=(1, 0.5))\n",
+ " \n",
+ " plt.tight_layout()\n",
+ " return fig"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 122,
+ "id": "6bf9ef38-8ae5-4851-b78b-3ace77ffd725",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "\n",
+ "def compute_betti_curve(point_cloud, epsilons, dims):\n",
+ " \"\"\"Compute Betti numbers for a range of epsilon values for multiple dimensions.\"\"\"\n",
+ " return {dim: np.array([classical_betti_solver(point_cloud, epsilon, dim) \n",
+ " for epsilon in epsilons]) \n",
+ " for dim in dims}\n",
+ "\n",
+ "def normalize_betti_curves(betti_curves, dims):\n",
+ " \"\"\"Normalize Betti curves across all dimensions.\"\"\"\n",
+ " normalized_curves = []\n",
+ " \n",
+ " for curve in betti_curves:\n",
+ " # Find max value across all dimensions for this window\n",
+ " max_vals = {dim: np.max(curve[dim]) for dim in dims}\n",
+ " overall_max = max(max_vals.values())\n",
+ " \n",
+ " # Normalize each dimension by the overall max if it's non-zero\n",
+ " normalized = {}\n",
+ " for dim in dims:\n",
+ " if overall_max > 0:\n",
+ " normalized[dim] = curve[dim] / overall_max\n",
+ " else:\n",
+ " normalized[dim] = curve[dim]\n",
+ " normalized_curves.append(normalized)\n",
+ " \n",
+ " return normalized_curves\n",
+ "\n",
+ "def compute_lp_distances(df, window_size=7, p=2):\n",
+ " \"\"\"Compute Lp norm distances between successive windows of price data.\"\"\"\n",
+ " time_series = np.log(df['price']).to_numpy().squeeze()\n",
+ " \n",
+ " # Takens embedding parameters\n",
+ " N = 2 # embedding dimension\n",
+ " d = 1 # time delay\n",
+ " w = window_size\n",
+ " L = len(time_series)\n",
+ " dims = [0,1] # Now including dimension 2\n",
+ " \n",
+ " # Create range of epsilon values\n",
+ " epsilons = np.linspace(0, 0.05, 10)\n",
+ " \n",
+ " # Create point clouds using Takens embedding\n",
+ " vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ " K = L - (N-1)*d - w + 1 # number of windows\n",
+ " point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ " print(f\"N = {N}, w = {w}, L = {L}, K = {K}\")\n",
+ " \n",
+ " # Compute Betti curves for each window\n",
+ " betti_curves = []\n",
+ " for point_cloud in tqdm(point_clouds, desc=\"Computing Betti curves\"):\n",
+ " betti_curve = compute_betti_curve(point_cloud, epsilons, dims)\n",
+ " betti_curves.append(betti_curve)\n",
+ " \n",
+ " # Normalize Betti curves\n",
+ " normalized_curves = normalize_betti_curves(betti_curves, dims)\n",
+ " \n",
+ " # Compute Lp distances between successive normalized Betti curves\n",
+ " distances = {}\n",
+ " for dim in dims:\n",
+ " dim_distances = [np.power(np.sum(np.power(np.abs(b1[dim]), p)), 1/p) \n",
+ " for b1 in normalized_curves[:-1]]\n",
+ " \n",
+ " # Normalize distances within each dimension\n",
+ " dim_distances = np.array(dim_distances)\n",
+ " if np.max(dim_distances) > 0:\n",
+ " dim_distances = dim_distances / np.max(dim_distances)\n",
+ " \n",
+ " distances[dim] = pd.Series(dim_distances, \n",
+ " index=df.index[w+(N-1)*d:w+(N-1)*d+len(dim_distances)])\n",
+ " \n",
+ " # Add moving average smoothing\n",
+ " distances[f\"{dim}_ma\"] = distances[dim].rolling(window=5).mean()\n",
+ " \n",
+ " return distances, normalized_curves, epsilons\n",
+ "\n",
+ "def plot_lp_vs_price_and_betti(df, distances, betti_curves, epsilons, interval=100):\n",
+ " \"\"\"Plot price, Lp distances, and Betti curves at specified intervals.\"\"\"\n",
+ " fig = plt.figure(figsize=(20, 12))\n",
+ " gs = fig.add_gridspec(3, 1, height_ratios=[2, 2, 2])\n",
+ " \n",
+ " # Price plot\n",
+ " ax1 = fig.add_subplot(gs[0])\n",
+ " ax1.plot(df['date'].to_numpy(), df['price'].to_numpy(), 'b-', label='Price')\n",
+ " ax1.set_ylabel('Price')\n",
+ " ax1.legend()\n",
+ " \n",
+ " # Lp distances plot with moving averages\n",
+ " ax2 = fig.add_subplot(gs[1])\n",
+ " colors = ['r', 'b', 'g'] # Colors for dimensions 0, 1, 2\n",
+ " \n",
+ " # Plot raw distances in lighter colors\n",
+ " for dim, color in zip([0, 1, 2], colors):\n",
+ " ax2.plot(distances[dim].index.to_numpy(), distances[dim].to_numpy(), \n",
+ " f'{color}:', alpha=0.3, label=f'β₁ (raw)')\n",
+ " ax2.plot(distances[f'{dim}_ma'].index.to_numpy(), \n",
+ " distances[f'{dim}_ma'].to_numpy(), f'{color}-', \n",
+ " label=f'β{dim} (MA)')\n",
+ " \n",
+ " ax2.set_ylabel('Normalized Lp Distance')\n",
+ " ax2.legend()\n",
+ " \n",
+ " # Betti curves plot\n",
+ " ax3 = fig.add_subplot(gs[2])\n",
+ " linestyles = ['-', '--', ':'] # Different line styles for each dimension\n",
+ " colors = plt.cm.rainbow(np.linspace(0, 1, len(range(0, len(betti_curves), interval))))\n",
+ " \n",
+ " selected_indices = range(0, len(betti_curves), interval)\n",
+ " for color_idx, idx in enumerate(selected_indices):\n",
+ " color = colors[color_idx]\n",
+ " for dim, ls in zip([0, 1, 2], linestyles):\n",
+ " label = f't={idx}, β{dim}'\n",
+ " ax3.plot(epsilons, betti_curves[idx][dim], ls, color=color, label=label)\n",
+ " \n",
+ " ax3.set_xlabel('Epsilon')\n",
+ " ax3.set_ylabel('Normalized Betti Number')\n",
+ " ax3.legend(loc='center left', bbox_to_anchor=(1, 0.5))\n",
+ " \n",
+ " plt.tight_layout()\n",
+ " return fig"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "2c19f3c1-dade-4717-ba5e-f28a1b186b18",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df1 = df[1900:2400]\n",
+ "# Compute distances and get Betti curves\n",
+ "distances, betti_curves, epsilons = compute_lp_distances(df)\n",
+ "# Plot everything"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 124,
+ "id": "6ef7b180-6640-40c1-8c47-5041be3156b9",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def plot_lp_vs_price_and_betti_classic(df, distances, betti_curves, epsilons, interval=100):\n",
+ " \"\"\"Plot price, Lp distances, and Betti curves at specified intervals.\"\"\"\n",
+ " fig = plt.figure(figsize=(20, 12))\n",
+ " gs = fig.add_gridspec(3, 1, height_ratios=[2, 2, 2])\n",
+ " \n",
+ " # Price plot\n",
+ " ax1 = fig.add_subplot(gs[0])\n",
+ " ax1.plot(df['index'].to_numpy(), df['price'].to_numpy(), 'b-', label='Price')\n",
+ " ax1.set_ylabel('Price')\n",
+ " ax1.legend()\n",
+ " \n",
+ " # Lp distances plot with moving averages\n",
+ " ax2 = fig.add_subplot(gs[1])\n",
+ " colors = ['r', 'b', 'g'] # Colors for dimensions 0, 1, 2\n",
+ " \n",
+ " # Plot raw distances in lighter colors\n",
+ " for dim, color in zip([0, 1, 2], colors):\n",
+ " if dim in distances and f'{dim}_ma' in distances:\n",
+ " ax2.plot(range(len(distances[dim])), distances[dim], \n",
+ " f'{color}:', alpha=0.3, label=f'β{dim} (raw)')\n",
+ " ax2.plot(range(len(distances[f'{dim}_ma'])), \n",
+ " distances[f'{dim}_ma'], f'{color}-', \n",
+ " label=f'β{dim} (MA)')\n",
+ " else:\n",
+ " print(f\"Key {dim} or {dim}_ma not found in distances dictionary\")\n",
+ "\n",
+ " \n",
+ " ax2.set_ylabel('Normalized Lp Distance')\n",
+ " ax2.legend()\n",
+ " \n",
+ " # Betti curves plot\n",
+ " ax3 = fig.add_subplot(gs[2])\n",
+ " linestyles = ['-', '--', ':'] # Different line styles for each dimension\n",
+ " colors = plt.cm.rainbow(np.linspace(0, 1, len(range(0, len(betti_curves), interval))))\n",
+ " \n",
+ " selected_indices = range(0, len(betti_curves), interval)\n",
+ " for color_idx, idx in enumerate(selected_indices):\n",
+ " color = colors[color_idx]\n",
+ " for dim, ls in zip([0, 1, 2], linestyles):\n",
+ " label = f't={idx}, β{dim}'\n",
+ " ax3.plot(epsilons, betti_curves[idx][dim], ls, color=color, label=label)\n",
+ " \n",
+ " ax3.set_xlabel('Epsilon')\n",
+ " ax3.set_ylabel('Normalized Betti Number')\n",
+ " ax3.legend(loc='center left', bbox_to_anchor=(1, 0.5))\n",
+ " \n",
+ " plt.tight_layout()\n",
+ " return fig"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "de37efb2-26b5-4d99-b60d-b804aa7c3532",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "plot_lp_vs_price_and_betti_classic(df, distances, betti_curves, epsilons, interval=100)\n",
+ "plt.savefig('betti_analysis_full_norm.svg', format='svg', dpi=300)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": 126,
+ "id": "2ef7bcf9-5033-4e3b-b934-29bdf42ba359",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def compute_betti_curve(point_cloud, epsilons, dims):\n",
+ " \"\"\"Compute Betti numbers for a range of epsilon values for multiple dimensions.\"\"\"\n",
+ " return {dim: np.array([classical_betti_solver(point_cloud, epsilon, dim) \n",
+ " for epsilon in epsilons]) \n",
+ " for dim in dims}\n",
+ "\n",
+ "def normalize_betti_curves(betti_curves, dims):\n",
+ " \"\"\"Normalize Betti curves across all dimensions.\"\"\"\n",
+ " normalized_curves = []\n",
+ " \n",
+ " for curve in betti_curves:\n",
+ " # Find max value across all dimensions for this window\n",
+ " max_vals = {dim: np.max(curve[dim]) for dim in dims}\n",
+ " overall_max = max(max_vals.values())\n",
+ " \n",
+ " # Normalize each dimension by the overall max if it's non-zero\n",
+ " normalized = {}\n",
+ " for dim in dims:\n",
+ " if overall_max > 0:\n",
+ " normalized[dim] = curve[dim] / overall_max\n",
+ " else:\n",
+ " normalized[dim] = curve[dim]\n",
+ " normalized_curves.append(normalized)\n",
+ " \n",
+ " return normalized_curves\n",
+ "\n",
+ "def compute_lp_distances(df, window_size=7, p=2):\n",
+ " \"\"\"Compute Lp norm distances between successive windows of price data.\"\"\"\n",
+ " time_series = np.log(df['price']).to_numpy().squeeze()\n",
+ " \n",
+ " # Takens embedding parameters\n",
+ " N = 10 # embedding dimension\n",
+ " d = 2 # time delay\n",
+ " w = window_size\n",
+ " L = len(time_series)\n",
+ " dims = [0, 1, 2] # Now including dimension 2\n",
+ " \n",
+ " # Create range of epsilon values\n",
+ " epsilons = np.linspace(0, 0.1, 40)\n",
+ " \n",
+ " # Create point clouds using Takens embedding\n",
+ " vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ " K = L - (N-1)*d - w + 1 # number of windows\n",
+ " point_clouds = np.array([vectors[i:i+w] for i in range(K)])\n",
+ " print(f\"N = {N}, w = {w}, L = {L}, K = {K}\")\n",
+ " \n",
+ " # Compute Betti curves for each window\n",
+ " betti_curves = []\n",
+ " for point_cloud in tqdm(point_clouds, desc=\"Computing Betti curves\"):\n",
+ " betti_curve = compute_betti_curve(point_cloud, epsilons, dims)\n",
+ " betti_curves.append(betti_curve)\n",
+ " \n",
+ " # Normalize Betti curves\n",
+ " normalized_curves = normalize_betti_curves(betti_curves, dims)\n",
+ " \n",
+ " # Compute Lp distances between successive normalized Betti curves\n",
+ " distances = {}\n",
+ " for dim in dims:\n",
+ " dim_distances = [np.power(np.sum(np.power(np.abs(b1[dim]), p)), 1/p) \n",
+ " for b1 in normalized_curves[:-1]]\n",
+ " \n",
+ " # Normalize distances within each dimension\n",
+ " dim_distances = np.array(dim_distances)\n",
+ " if np.max(dim_distances) > 0:\n",
+ " dim_distances = dim_distances / np.max(dim_distances)\n",
+ " \n",
+ " distances[dim] = pd.Series(dim_distances, \n",
+ " index=df.index[w+(N-1)*d:w+(N-1)*d+len(dim_distances)])\n",
+ " \n",
+ " # Add moving average smoothing\n",
+ " distances[f\"{dim}_ma\"] = distances[dim].rolling(window=5).mean()\n",
+ " \n",
+ " return distances, normalized_curves, epsilons\n",
+ "\n",
+ "def plot_lp_vs_price_and_betti(df, distances, betti_curves, epsilons, interval=100):\n",
+ " \"\"\"Plot price, Lp distances, and Betti curves at specified intervals.\"\"\"\n",
+ " fig = plt.figure(figsize=(20, 12))\n",
+ " gs = fig.add_gridspec(3, 1, height_ratios=[2, 2, 2])\n",
+ " \n",
+ " # Price plot\n",
+ " ax1 = fig.add_subplot(gs[0])\n",
+ " ax1.plot(df['date'].to_numpy(), df['price'].to_numpy(), 'b-', label='Price')\n",
+ " ax1.set_ylabel('Price')\n",
+ " ax1.legend()\n",
+ " \n",
+ " # Lp distances plot with moving averages\n",
+ " ax2 = fig.add_subplot(gs[1])\n",
+ " colors = ['r', 'b', 'g'] # Colors for dimensions 0, 1, 2\n",
+ " \n",
+ " # Plot raw distances in lighter colors\n",
+ " for dim, color in zip([0, 1, 2], colors):\n",
+ " ax2.plot(distances[dim].index.to_numpy(), distances[dim].to_numpy(), \n",
+ " f'{color}:', alpha=0.3, label=f'β₁ (raw)')\n",
+ " ax2.plot(distances[f'{dim}_ma'].index.to_numpy(), \n",
+ " distances[f'{dim}_ma'].to_numpy(), f'{color}-', \n",
+ " label=f'β{dim} (MA)')\n",
+ " \n",
+ " ax2.set_ylabel('Normalized Lp Distance')\n",
+ " ax2.legend()\n",
+ " \n",
+ " # Betti curves plot\n",
+ " ax3 = fig.add_subplot(gs[2])\n",
+ " linestyles = ['-', '--', ':'] # Different line styles for each dimension\n",
+ " colors = plt.cm.rainbow(np.linspace(0, 1, len(range(0, len(betti_curves), interval))))\n",
+ " \n",
+ " selected_indices = range(0, len(betti_curves), interval)\n",
+ " for color_idx, idx in enumerate(selected_indices):\n",
+ " color = colors[color_idx]\n",
+ " for dim, ls in zip([0, 1, 2], linestyles):\n",
+ " label = f't={idx}, β{dim}'\n",
+ " ax3.plot(epsilons, betti_curves[idx][dim], ls, color=color, label=label)\n",
+ " \n",
+ " ax3.set_xlabel('Epsilon')\n",
+ " ax3.set_ylabel('Normalized Betti Number')\n",
+ " ax3.legend(loc='center left', bbox_to_anchor=(1, 0.5))\n",
+ " \n",
+ " plt.tight_layout()\n",
+ " return fig"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "b9d53592-f82d-433d-b403-1e16b4131df4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "df_filtered = df[(df['date'] >= '2007-01-01') & (df['date'] <= '2010-12-31')]\n",
+ "distances, betti_curves, epsilons = compute_lp_distances(df_filtered)\n",
+ "# Plot everything"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "77c87119-cb70-4001-b275-38e7eb19c1ad",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "distances_quantum=distances;\n",
+ "betti_curves_quantum=betti_curves;\n",
+ "epsilons_quantum=epsilons;"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "93cc4e93-06ac-4823-b1d7-d62507dd98b5",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "plot_lp_vs_price_and_betti(df_filtered, distances_quantum, betti_curves_quantum, epsilons_quantum, interval=100)\n",
+ "#print(distances_quantum[0].to_numpy())\n",
+ "plt.savefig(f'betti_analysis_norm_2008_{7}.svg', format='svg', dpi=300)\n",
+ "# plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "0ae1faa8-d47b-45e5-afba-e338fec445b7",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "def plot_betti_heatmaps(df, normalized_curves, epsilons, dims):\n",
+ " \"\"\"\n",
+ " Plot heat maps for each Betti curve βₖ as a function of date and epsilon.\n",
+ " \n",
+ " Parameters:\n",
+ " df : pandas.DataFrame\n",
+ " The dataframe containing the 'date' column.\n",
+ " normalized_curves : list of dicts\n",
+ " Each element is a dictionary with keys corresponding to dimensions (k) \n",
+ " and values being arrays (len(epsilons),) of normalized Betti numbers.\n",
+ " epsilons : array-like\n",
+ " The range of epsilon values.\n",
+ " dims : list\n",
+ " List of dimensions (e.g. [0, 1]) to plot.\n",
+ " \n",
+ " Returns:\n",
+ " fig : matplotlib.figure.Figure\n",
+ " The resulting figure with subplots.\n",
+ " \"\"\"\n",
+ " num_dims = len(dims)\n",
+ " fig, axs = plt.subplots(1, num_dims, figsize=(5 * num_dims, 4), squeeze=False)\n",
+ " \n",
+ " # Extract dates from the dataframe\n",
+ " dates = df['date'].iloc[len(df) - len(normalized_curves):]\n",
+ " \n",
+ " for i, dim in enumerate(dims):\n",
+ " # Build a matrix with shape (n_time, n_epsilon) for dimension k.\n",
+ " matrix = np.array([nc[dim] for nc in normalized_curves])\n",
+ " \n",
+ " # Plot using imshow\n",
+ " im = axs[0, i].imshow(matrix.T, aspect='auto', origin='lower',\n",
+ " extent=[0, len(dates) - 1, epsilons[0], epsilons[-1]])\n",
+ " axs[0, i].set_title(f\"Heatmap of β{dim}\")\n",
+ " axs[0, i].set_xlabel(\"Date\")\n",
+ " axs[0, i].set_ylabel(\"Epsilon\")\n",
+ " \n",
+ " # Set x-axis ticks and labels\n",
+ " num_ticks = 10 # Increased number of ticks\n",
+ " tick_locations = np.linspace(0, len(dates) - 1, num_ticks).astype(int)\n",
+ " axs[0, i].set_xticks(tick_locations)\n",
+ " \n",
+ " # Format dates to show only month (as number) and year\n",
+ " formatted_dates = [f\"{date.month:02d}/{date.year}\" for date in dates.iloc[tick_locations]]\n",
+ " axs[0, i].set_xticklabels(formatted_dates, rotation=45, ha='right')\n",
+ " \n",
+ " fig.colorbar(im, ax=axs[0, i], orientation='vertical')\n",
+ " \n",
+ " plt.tight_layout()\n",
+ " return fig\n",
+ "\n",
+ "normalized_curves = normalize_betti_curves(betti_curves_quantum,[0,1,2])\n",
+ "plot_betti_heatmaps(df_filtered,normalized_curves,epsilons_quantum,[1])\n",
+ "plt.savefig(f'betti1_analysis_heatmap_2000_{5}.svg', format='svg', dpi=300)"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "d1c2e4b5",
+ "metadata": {},
+ "source": [
+ "## *BONUS:* Explore future directions of quantum TDA\n",
+ "\n",
+ "The following is a non-exhaustive list of possible next steps for the quantum TDA pipeline. It is recommended to at least explore 1 option or sub-option.\n",
+ "\n",
+ "- **Find more of applications of TDA in finance**:\n",
+ "\n",
+ " There are several directions where to extend the analysis. Most work on time series analysis has used persistent homology, and more specifically, the $L^{P}$ norm of persistence landscapes, which can be used to detect early warning signals of imminent market crashes. This is precisely studied in the seminal work by Gidea and Katz, see [ref](https://arxiv.org/abs/1703.04385)\n",
+ " - Analyze financial correlation network and their degree of association with Betti curves or other topological features. From the time-series of multiple stock prices, we can build time-dependent correlation networks, which exhibit topological structures and might show some association to the evolution of betti curves or other topological data measures. Generally speaking, the cross correlations in a stock market will be in the form of a high-dimension topological space, with more complicated features. One can also think about other time varying financial graphs (e.g. cryptocurrencies). The following articles can help uncover more applications: \n",
+ " \n",
+ " - [Integral Betti signature confirms the hyperbolic geometry of brain, climate,and financial networks](https://www.arxiv.org/pdf/2406.15505)\n",
+ " - [Using Topological Data Analysis (TDA) and Persistent Homology to Analyze the Stock Markets in Singapore and Taiwan](https://www.frontiersin.org/journals/physics/articles/10.3389/fphy.2021.572216/full)\n",
+ " - Build a ML classifier or regressor on top of vectorized features such as Betti Curves (given their potential to identify trends, patterns or potential turning points in the market) to help with investment or risk management strategies. Show that Betti curves have some predictive skill, as key topological descriptors. See [ref1](https://arxiv.org/abs/2411.13881) and [ref2](https://www.sciencedirect.com/science/article/pii/S2405918823000235) for further information on the topic.\n",
+ "- **A hybrid and more NISQ-friendly quantum TDA pipeline**:\n",
+ " \n",
+ " QPE remains primarily theoretical. Its circuits are simply too deep to run on real hardware. Come up an with iterative or hybrid quantum phase estimation protocol or use tools that increase the algorithmic performance when running quantum circuits on real hardware. Benchmark them against textbook-QPE circuits. Here are some proposals to subtitute the QPE part:\n",
+ " - Variational Quantum Deflation (VQD) Algorithm: VQD is a quantum algorithm that uses a variational technique to find the k eigenvalues of the Hamiltonian H of a given system. [ref](https://quantum-journal.org/papers/q-2019-07-01-156/)\n",
+ " - Variational Quantum Eigensolver (VQE): Using VQE to determine the spectra of adjancency or laplacian matrix. Inspired by: [ref](https://arxiv.org/pdf/1912.12366)\n",
+ " \n",
+ " Finally, run some circuits on simulator + real hardware and compare the performance (runtime, noise effects, # resources) of the new proposal to the QPE solution of the above sections.\n",
+ "\n",
+ "- **A proper procedure of encoding the Laplacian matrix to the unitary**:\n",
+ "\n",
+ " In Step 3, encoding the exponential matrix is recommanded. An alternative approach is to conduct the Paulis decomposition on the Laplacian, then followed by Trotterization. Can you implement this approach in your pipeline? What parameters influence the accuracy? Can you optimize your code to minimize the circuit depth?\n",
+ "\n",
+ "- **Extend the quantum TDA to extract persistent Betti numbers**: \n",
+ " \n",
+ " Implement a quantum TDA algorithm for persistent Betti numbers. Esstimating the persistent Betti numbers is a more general task than estimating the Betti number and it is more practical for TDA. It is an open problem to construct a quantum algorithm for the persistent Betti numbers in a way that is preferable for NISQ devices, and the only current implementation of a quantum algorithm for persistent betti number is shown [here](https://quantum-journal.org/papers/q-2022-12-07-873/pdf/)."
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "884527bc-bce6-4560-88d5-f0eb2119455e",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "N = 3 # embedding dimension\n",
+ "d = 1 # time delay\n",
+ "w = 10 # window size\n",
+ "\n",
+ "L = len(time_series)\n",
+ "vectors = np.array([time_series[i:L-(N-1)*d+i] for i in range(0, N*d, d)]).T\n",
+ "\n",
+ "K = L - (N-1)*d - w + 1 # number of windows\n",
+ "Z = np.array([vectors[i:i+w] for i in range(K)])\n",
+ "print(f\"N = {N}, w = {w}, L = {L}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "acfaa739-9e12-4290-b098-c1020ed5f847",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "import gudhi\n",
+ "\n",
+ "epsilon = 0.2 # maximum edge length\n",
+ "max_dim = 2 # maximum simplex dimension\n",
+ "\n",
+ "all_simplices = []\n",
+ "\n",
+ "point_cloud = Z[0]\n",
+ "\n",
+ "# Simplicial Complex\n",
+ "rips = gudhi.RipsComplex(points=point_cloud, max_edge_length=epsilon)\n",
+ "filtration = rips.create_simplex_tree(max_dimension=max_dim).get_filtration()\n",
+ "\n",
+ "# Extract simplices by dimension\n",
+ "simplex_tree = [[] for _ in range(max_dim + 1)]\n",
+ "for simplex, filtration_value in filtration:\n",
+ " if filtration_value <= epsilon:\n",
+ " dim = len(simplex) - 1\n",
+ " simplex_tree[dim].append(simplex)\n",
+ "\n",
+ "print(f\"num 0-simplices: {len(simplex_tree[0])}\")\n",
+ "print(f\"num 1-simplices: {len(simplex_tree[1])}\")\n",
+ "print(f\"num 2-simplices: {len(simplex_tree[2])}\")\n",
+ "for dim, simplices in enumerate(simplex_tree):\n",
+ " print(f\"{dim}-simplices: {simplices}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "01b1899d-b92b-4e69-9fd4-8167f2b5fc2b",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "boundary1 = boundary(1, simplex_tree)\n",
+ "boundary2 = boundary(2, simplex_tree)\n",
+ "\n",
+ "print(\"Boundary 1:\", boundary1.shape)\n",
+ "print(boundary1)\n",
+ "\n",
+ "print(\"Boundary 2:\", boundary2.shape)\n",
+ "print(boundary2)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ebd6e21e-12f6-4172-abd8-c94b711ad640",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "k = 1\n",
+ "laplacian = get_laplacian(k, simplex_tree)\n",
+ "\n",
+ "U = get_unitary(laplacian)\n",
+ "\n",
+ "print(\"Laplacian:\", laplacian.shape)\n",
+ "print(laplacian)\n",
+ "print(\"Unitary:\", U.shape)\n",
+ "print(U)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "ac491552-88af-42ac-b067-fb45d34b1ab4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from qiskit.synthesis import SuzukiTrotter\n",
+ "from qiskit.circuit.library import PauliEvolutionGate\n",
+ "from qiskit.circuit import Parameter\n",
+ "from qiskit.quantum_info import Operator, SparsePauliOp\n",
+ "\n",
+ "pauli_op = SparsePauliOp.from_operator(laplacian)\n",
+ "evolution_time = 1.19\n",
+ "trotter_steps = 44\n",
+ "\n",
+ "# Create a Trotterized unitary evolution\n",
+ "trotter_op = PauliEvolutionGate(\n",
+ " pauli_op, \n",
+ " time=evolution_time, \n",
+ " synthesis=SuzukiTrotter(reps=trotter_steps)\n",
+ ")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "8c2ee193-72b5-43cd-a0ad-515b93396cd4",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Create a quantum circuit\n",
+ "from qiskit import QuantumCircuit\n",
+ "num_qubits = pauli_op.num_qubits\n",
+ "qc = QuantumCircuit(num_qubits)\n",
+ "\n",
+ "# Apply the Trotterized operator\n",
+ "qc.append(trotter_op, range(num_qubits))\n",
+ "qc.draw()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "1e3aecf4-45c7-424d-b91e-d3ba48b46823",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from scipy.linalg import expm, eigvals\n",
+ "exact_unitary = expm(-1j * laplacian * evolution_time)\n",
+ "\n",
+ "# Compute eigenvalues of the exact unitary\n",
+ "exact_eigenvalues = eigvals(exact_unitary)\n",
+ "\n",
+ "trotterized_unitary = Operator(qc).data\n",
+ "trotterized_eigenvalues = eigvals(trotterized_unitary)"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "d3a1113a-22b8-43f8-9872-024a8f6620cb",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# Sort eigenvalues to ensure they're compared correctly\n",
+ "exact_eigenvalues_sorted = np.sort(exact_eigenvalues)\n",
+ "trotterized_eigenvalues_sorted = np.sort(trotterized_eigenvalues)\n",
+ "\n",
+ "# Calculate relative error\n",
+ "relative_error = np.abs(exact_eigenvalues_sorted - trotterized_eigenvalues_sorted) / np.abs(exact_eigenvalues_sorted)\n",
+ "\n",
+ "# Print maximum and average relative error\n",
+ "print(f\"Maximum relative error: {np.max(relative_error)}\")\n",
+ "print(f\"Average relative error: {np.mean(relative_error)}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "fb5186dd-146b-4e4f-a3ab-13cbcbd4db5d",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "plt.figure(figsize=(10, 6))\n",
+ "plt.scatter(np.real(exact_eigenvalues), np.imag(exact_eigenvalues), label='Exact', alpha=0.7)\n",
+ "plt.scatter(np.real(trotterized_eigenvalues), np.imag(trotterized_eigenvalues), label='Trotterized', alpha=0.7, marker='x')\n",
+ "\n",
+ "plt.xlabel('Real part')\n",
+ "plt.ylabel('Imaginary part')\n",
+ "plt.title('Comparison of Eigenvalues')\n",
+ "plt.legend()\n",
+ "plt.grid(True)\n",
+ "plt.savefig('trotter.svg', format='svg', dpi=300)\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "6a5b831f-74bc-41fe-a523-2e6eb54bcecd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "from scipy.linalg import sqrtm\n",
+ "\n",
+ "def trace_distance(A, B):\n",
+ " return 0.5 * np.trace(sqrtm((A - B).conj().T @ (A - B)))\n",
+ "\n",
+ "error = trace_distance(exact_unitary, trotterized_unitary)\n",
+ "print(f\"Trace distance between matrices: {error}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "28beaf99-d3d9-4ca7-b58e-faa4738679bd",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "# If you still need the SparsePauliOp representation\n",
+ "trotterized_op = SparsePauliOp.from_operator(trotterized_unitary)\n",
+ "coefficients = trotterized_op.coeffs\n",
+ "pauli_matrices = trotterized_op.paulis\n",
+ "\n",
+ "# Step 3: Reconstruct the matrix using the formula\n",
+ "def reconstruct_matrix(coefficients, pauli_matrices):\n",
+ " result = np.eye(2**len(pauli_matrices[0])) # Identity matrix of appropriate size\n",
+ " for theta, sigma in zip(coefficients, pauli_matrices):\n",
+ " pauli_matrix = SparsePauliOp(sigma).to_matrix()\n",
+ " result = result @ (np.cos(theta) * np.eye(2**len(sigma)) + 1j * np.sin(theta) * pauli_matrix)\n",
+ " return result\n",
+ "\n",
+ "reconstructed_matrix = reconstruct_matrix(coefficients, pauli_matrices)\n",
+ "\n",
+ "# Step 4: Compare the reconstructed matrix to the original unitary\n",
+ "error_frobenius = np.linalg.norm(exact_unitary - reconstructed_matrix, 'fro')\n",
+ "error_trace = np.trace(np.abs(exact_unitary - reconstructed_matrix))\n",
+ "\n",
+ "print(f\"Frobenius norm of the difference: {error_frobenius}\")\n",
+ "print(f\"Trace distance: {error_trace}\")"
+ ]
+ },
+ {
+ "cell_type": "code",
+ "execution_count": null,
+ "id": "c47d6b40-023b-4344-8bbb-3227aa5b7d4a",
+ "metadata": {},
+ "outputs": [],
+ "source": [
+ "fig, axs = plt.subplots(2, 2, figsize=(12, 12))\n",
+ "\n",
+ "axs[0, 0].imshow(np.real(exact_unitary))\n",
+ "axs[0, 0].set_title(\"Real part of original unitary\")\n",
+ "\n",
+ "axs[0, 1].imshow(np.imag(exact_unitary))\n",
+ "axs[0, 1].set_title(\"Imaginary part of original unitary\")\n",
+ "\n",
+ "axs[1, 0].imshow(np.real(reconstructed_matrix))\n",
+ "axs[1, 0].set_title(\"Real part of reconstructed matrix\")\n",
+ "\n",
+ "axs[1, 1].imshow(np.imag(reconstructed_matrix))\n",
+ "axs[1, 1].set_title(\"Imaginary part of reconstructed matrix\")\n",
+ "\n",
+ "plt.tight_layout()\n",
+ "plt.show()\n",
+ "\n",
+ "# Plot the difference\n",
+ "plt.figure(figsize=(10, 4))\n",
+ "\n",
+ "plt.subplot(121)\n",
+ "plt.imshow(np.abs(np.real(exact_unitary - reconstructed_matrix)))\n",
+ "plt.colorbar()\n",
+ "plt.title(\"Absolute difference (Real part)\")\n",
+ "\n",
+ "plt.subplot(122)\n",
+ "plt.imshow(np.abs(np.imag(exact_unitary - reconstructed_matrix)))\n",
+ "plt.colorbar()\n",
+ "plt.title(\"Absolute difference (Imaginary part)\")\n",
+ "\n",
+ "plt.tight_layout()\n",
+ "plt.show()"
+ ]
+ },
+ {
+ "cell_type": "markdown",
+ "id": "c098599c-7ab0-48b1-9e33-51d22502310d",
+ "metadata": {},
+ "source": [
+ "# This is the end of the challenge. Good luck!"
+ ]
+ }
+ ],
+ "metadata": {
+ "kernelspec": {
+ "display_name": "Python 3 (ipykernel)",
+ "language": "python",
+ "name": "python3"
+ },
+ "language_info": {
+ "codemirror_mode": {
+ "name": "ipython",
+ "version": 3
+ },
+ "file_extension": ".py",
+ "mimetype": "text/x-python",
+ "name": "python",
+ "nbconvert_exporter": "python",
+ "pygments_lexer": "ipython3",
+ "version": "3.12.7"
+ }
+ },
+ "nbformat": 4,
+ "nbformat_minor": 5
+}