|
| 1 | + |
| 2 | +Trotterized Quantum Time Evolution with NumPy and SciPy |
| 3 | + |
| 4 | + |
| 5 | + |
| 6 | +Background and Motivation |
| 7 | + |
| 8 | + |
| 9 | +In quantum mechanics, the time evolution of a state $|\psi(t)\rangle$ under a time-independent Hamiltonian $H$ is given by the Schrödinger equation solution $|\psi(t)\rangle = e^{-i H t},|\psi(0)\rangle$ . However, if $H$ is a sum of terms that do not commute (which is common in many-body systems), directly computing or implementing $e^{-iHt}$ is challenging. Trotterization (the Lie–Trotter–Suzuki product formula) addresses this by approximating the exponential of a sum via a product of exponentials of the individual terms . In essence, it breaks a complex evolution into a sequence of simpler evolutions: |
| 10 | + |
| 11 | +We split the total time $t$ into $N$ small Trotter steps $\Delta t = t/N$. For each step, we apply the exponential of each Hamiltonian term in succession . For example, if $H = A + B$, a first-order Trotter step is $e^{-i(A+B)\Delta t} \approx e^{-iA\Delta t},e^{-iB\Delta t}$. As $N \to \infty$ (i.e. $\Delta t \to 0$), the product $\big(e^{-iA\Delta t}e^{-iB\Delta t}\big)^N$ converges to $e^{-i(A+B)t}$ . In general $H=\sum_j H_j$ leads to $e^{-iH t} \approx \Big(\prod_j e^{-i H_j \Delta t}\Big)^N$ . |
| 12 | +This works because each term $H_j$ is easier to exponentiate on its own (often corresponding to a gate or simple matrix). We alternate these simpler evolutions to approximate the full evolution . Essentially, we are “slicing” the complex evolution into thin, manageable layers of simpler unitaries. |
| 13 | +Trotter error: The approximation incurs error because $H_i$ and $H_j$ generally do not commute. First-order Trotterization has an error scaling on the order of $O(t^2/N)$ (equivalently $O(t,\Delta t)$). To reduce error, one can increase $N$ (more, smaller time steps) at the cost of more operations . There is a trade-off: smaller $\Delta t$ (more steps) gives higher accuracy but longer runtime, whereas larger $\Delta t$ (fewer steps) gives shorter simulations but larger errors . For even better accuracy, higher-order Suzuki-Trotter formulas can be used. For example, a second-order (symmetric) Trotter decomposition (using half-step segments in reversed order) cancels out first-order error terms and yields error $O((\Delta t)^3)$ per step, vs. $O((\Delta t)^2)$ for basic first-order . In practice, symmetric Trotter allows larger time steps for the same accuracy at the cost of applying the sequence $H_1,\ldots,H_m,H_m,\ldots,H_1$ in each step. |
| 14 | + |
| 15 | + |
| 16 | +Figure: Trotterization concept. (a) A long time evolution is achieved by applying more Trotter steps of shorter duration $\Delta t$. (b) First-order (Lie–Trotter) decomposition: each Trotter step applies the unitary for operator $A$ then $B$ (two non-commuting parts of $H$) sequentially. This basic method incurs error $O(N(\Delta t)^2)$ (equivalently $O(\Delta t)$ for fixed total time). (c) Second-order (symmetric) decomposition: each step uses half-step evolutions $A(\Delta t/2)$, then $B(\Delta t)$, then $A(\Delta t/2)$, which cancels certain error terms. The symmetric Trotter has error $O(N(\Delta t)^3)$ , a higher-order accuracy, at the cost of extra operations per step. |
| 17 | + |
| 18 | + |
| 19 | +Hamiltonian as Sum of Pauli Terms |
| 20 | + |
| 21 | + |
| 22 | +To demonstrate Trotterized evolution, we consider a quantum system of $n$ qubits with a random Hermitian Hamiltonian expressed as a sum of local Pauli string terms. A Pauli string is a tensor product of single-qubit Pauli matrices ($X, Y, Z$) or identity ($I$) on each qubit. For example, $X_1 \otimes Z_2 \otimes I_3$ is a 3-qubit Pauli string acting with $X$ on qubit 1, $Z$ on qubit 2, and identity on qubit 3. All Pauli strings are Hermitian and square to identity. Any single-qubit Hermitian operator can be expanded in the Pauli basis ; more generally, the $4^n$ Pauli strings (including identities) form a basis for $2^n \times 2^n$ Hermitian matrices . This means an arbitrary $n$-qubit Hamiltonian can be written as: |
| 23 | + |
| 24 | +H \;=\; \sum_k c_k\, P_k, |
| 25 | + |
| 26 | +where each $P_k$ is a Pauli string (acting on one or more qubits) and $c_k$ is a real coefficient. In our script, we will randomly generate such a Hamiltonian by picking a set of Pauli strings with random coefficients: |
| 27 | + |
| 28 | +Pauli matrices: I = \begin{pmatrix}1&0\\0&1\end{pmatrix},\; X = \begin{pmatrix}0&1\\1&0\end{pmatrix},\; Y = \begin{pmatrix}0&-i\\i&0\end{pmatrix},\; Z = \begin{pmatrix}1&0\\0&-1\end{pmatrix}. Each is $2\times2$ Hermitian. For $n$ qubits, a Pauli string is P = \sigma_{j_1}\otimes \sigma_{j_2}\otimes\cdots\otimes \sigma_{j_n} (with $\sigma_{j}=I,X,Y,$ or $Z$). The dimension of the full Hamiltonian matrix is $2^n \times 2^n$. |
| 29 | +Random term generation: We will choose a specified number of terms (e.g. on the order of $n$ or $n^2$ terms) and for each term randomly pick one of ${I,X,Y,Z}$ for each qubit (ensuring not all are $I$ to get a non-trivial term). Multiplying the single-qubit matrices gives the term’s $2^n\times 2^n$ matrix. We assign a random real coefficient $c_k$ to this term. Summing all terms yields the Hamiltonian $H = \sum_k c_k P_k$, which is Hermitian since each $P_k$ is Hermitian and coefficients are real. |
| 30 | +Initial state: For simplicity, we start with a basis state $|\psi(0)\rangle = |00\cdots0\rangle$ (the all-zeros computational basis state). This state is represented by a length-$2^n$ vector with 1 in the first component and 0 elsewhere. (The script can be modified to use any normalized initial state.) |
| 31 | + |
| 32 | + |
| 33 | + |
| 34 | +Trotterized Time Evolution Simulation |
| 35 | + |
| 36 | + |
| 37 | +With $H$ defined, our goal is to simulate the evolution $|\psi(t)\rangle \approx e^{-iHt}|\psi(0)\rangle$ using a first-order Trotter decomposition. The user can specify: |
| 38 | + |
| 39 | +The number of qubits n (which affects the Hamiltonian size), |
| 40 | +Total evolution time $t$, and |
| 41 | +Number of Trotter steps $N$. |
| 42 | + |
| 43 | + |
| 44 | +From these, we compute $\Delta t = t/N$. The first-order Trotter algorithm proceeds as follows: |
| 45 | + |
| 46 | +Precompute term exponentials: For each Hamiltonian term $H_k = c_k P_k$, we need the unitary $U_k(\Delta t) = \exp(-i,H_k,\Delta t) = \exp(-i,c_k P_k,\Delta t)$. Because $P_k^2 = I$, this exponential can be evaluated analytically using the identity $e^{-i\theta P} = I\cos\theta ;-; i,P\sin\theta$ . (This follows from the Taylor expansion or recognizing $P$ has eigenvalues $\pm1$.) We will use this formula to apply each term’s evolution efficiently. |
| 47 | +Iterate over Trotter steps: Start with the initial state vector. For each of the $N$ steps, sequentially apply each term’s unitary to the state. That is, for step $m=1$ to $N$, and for each term $P_k$ in $H$, update $|\psi\rangle \leftarrow U_k(\Delta t),|\psi\rangle$. This updates the state as $|\psi(m,\Delta t)\rangle \approx \prod_k e^{-i c_k P_k \Delta t};|\psi((m-1)\Delta t)\rangle$. After completing all terms, one Trotter step is done. (The order of terms is fixed each step; since the step size is small, the error introduced by non-commutation is small and accumulates over steps.) |
| 48 | +Repeat the above for all steps to reach time $t = N,\Delta t$. The result is the Trotter-approximated $|\psi(t)\rangle$. |
| 49 | +Exact evolution (for comparison): We also compute the exact final state via $|\psi_{\text{exact}}(t)\rangle = e^{-iH t}|\psi(0)\rangle$ using SciPy’s matrix exponential. This is feasible for small numbers of qubits (since the matrix is $2^n$-dimensional). We can then calculate the overlap or fidelity with the Trotter result. The fidelity is $F = |\langle \psi_{\text{exact}}(t) \mid \psi_{\text{trotter}}(t)\rangle|^2$, which equals 1 for a perfect simulation. Optionally, we can also check the fidelity at intermediate steps by comparing against $e^{-iH m\Delta t}|\psi(0)\rangle$ for each $m$. This helps quantify how the error accumulates over time. |
| 50 | + |
| 51 | + |
| 52 | +Below is the complete Python script implementing this procedure. It uses NumPy for matrix and state representations and SciPy for the matrix exponential. The code is written as a standalone script with detailed comments: |
| 53 | +import numpy as np |
| 54 | +from scipy.linalg import expm |
| 55 | + |
| 56 | +# --- Simulation Parameters (user configurable) --- |
| 57 | +num_qubits = 3 # Number of qubits in the system |
| 58 | +num_terms = 3 * num_qubits # Number of random Pauli terms in H (e.g., 3*n) |
| 59 | +total_time = 1.0 # Total evolution time |
| 60 | +trotter_steps = 10 # Number of Trotter steps (N) |
| 61 | +track_error = False # If True, track fidelity at each step (prints overlaps) |
| 62 | + |
| 63 | +np.random.seed(42) # Set a random seed for reproducibility (optional) |
| 64 | + |
| 65 | +# Define Pauli matrices (2x2 complex NumPy arrays) |
| 66 | +I = np.eye(2, dtype=complex) |
| 67 | +X = np.array([[0, 1], [1, 0]], dtype=complex) |
| 68 | +Y = np.array([[0, -1j], [1j, 0]], dtype=complex) |
| 69 | +Z = np.array([[1, 0], [0, -1]], dtype=complex) |
| 70 | +paulis = [I, X, Y, Z] |
| 71 | +pauli_labels = ['I', 'X', 'Y', 'Z'] # for reference/printing |
| 72 | + |
| 73 | +# Randomly generate a Hamiltonian as a sum of Pauli strings |
| 74 | +terms = [] # list to hold (coeff, matrix, label) for each term |
| 75 | +dim = 2 ** num_qubits |
| 76 | +H = np.zeros((dim, dim), dtype=complex) # Hamiltonian matrix |
| 77 | +for term_index in range(num_terms): |
| 78 | + # Randomly pick a Pauli for each qubit to form a tensor-product term |
| 79 | + matrices = [] |
| 80 | + labels = [] |
| 81 | + for q in range(num_qubits): |
| 82 | + choice = np.random.randint(0, 4) # 0=I, 1=X, 2=Y, 3=Z |
| 83 | + matrices.append(paulis[choice]) |
| 84 | + labels.append(pauli_labels[choice]) |
| 85 | + # Ensure the term is not Identity on all qubits (if so, replace one factor with X) |
| 86 | + if all(lbl == 'I' for lbl in labels): |
| 87 | + rand_q = np.random.randint(0, num_qubits) |
| 88 | + matrices[rand_q] = X |
| 89 | + labels[rand_q] = 'X' |
| 90 | + # Construct the term matrix via Kronecker (tensor) product |
| 91 | + term_matrix = matrices[0] |
| 92 | + for mat in matrices[1:]: |
| 93 | + term_matrix = np.kron(term_matrix, mat) |
| 94 | + # Random real coefficient for this term |
| 95 | + coeff = np.random.uniform(-1.0, 1.0) |
| 96 | + # Add to Hamiltonian |
| 97 | + H += coeff * term_matrix |
| 98 | + terms.append((coeff, term_matrix, "".join(labels))) |
| 99 | + |
| 100 | +# Print out the generated Hamiltonian terms (for information) |
| 101 | +print(f"Hamiltonian has {len(terms)} terms on {num_qubits} qubits:") |
| 102 | +for coeff, _, label in terms: |
| 103 | + print(f" {coeff:+.3f} * ({label})") |
| 104 | + |
| 105 | +# Initial state |00...0>: a vector of size 2^n with 1 in the 0th element |
| 106 | +initial_state = np.zeros(dim, dtype=complex) |
| 107 | +initial_state[0] = 1.0 |
| 108 | + |
| 109 | +# Prepare for Trotter evolution |
| 110 | +dt = total_time / trotter_steps # time step size |
| 111 | +state = initial_state.copy() # will hold the evolving state vector |
| 112 | + |
| 113 | +# (Optional) prepare for exact intermediate evolution tracking |
| 114 | +if track_error: |
| 115 | + U_step = expm(-1j * H * dt) # exact propagator for one step |
| 116 | + exact_state = initial_state.copy() # will hold exact state as we step through |
| 117 | + |
| 118 | +# --- Main Trotter Evolution Loop --- |
| 119 | +for step in range(trotter_steps): |
| 120 | + # Apply each term's evolution for this time slice |
| 121 | + for (coeff, term_matrix, label) in terms: |
| 122 | + theta = coeff * dt # the angle for this term's evolution |
| 123 | + # Using exp(-i * coeff * P * dt) = I * cos(theta) - i * P * sin(theta) |
| 124 | + cos_theta = np.cos(theta) |
| 125 | + sin_theta = np.sin(theta) |
| 126 | + # Update state: new_state = cos(theta)*state - i * sin(theta) * (P * state) |
| 127 | + state = cos_theta * state - 1j * sin_theta * (term_matrix.dot(state)) |
| 128 | + # After applying all terms, one full Trotter step is complete. |
| 129 | + if track_error: |
| 130 | + # Update exact state by one full step and compute fidelity |
| 131 | + exact_state = U_step.dot(exact_state) |
| 132 | + overlap = np.vdot(exact_state, state) # inner product <psi_exact | psi_trotter> |
| 133 | + fidelity = np.abs(overlap)**2 |
| 134 | + print(f"Step {step+1:2d}/{trotter_steps}: fidelity = {fidelity:.6f}") |
| 135 | + |
| 136 | +# Compute exact final state for comparison using full matrix exponential |
| 137 | +psi_exact_final = expm(-1j * H * total_time).dot(initial_state) |
| 138 | + |
| 139 | +# Compute overlap and fidelity between Trotter result and exact result |
| 140 | +final_overlap = np.vdot(psi_exact_final, state) |
| 141 | +final_fidelity = np.abs(final_overlap)**2 |
| 142 | + |
| 143 | +print("\nFinal state (Trotter approximation):") |
| 144 | +print(state) |
| 145 | +print("\nFinal state (Exact evolution):") |
| 146 | +print(psi_exact_final) |
| 147 | +print(f"\nOverlap = {final_overlap:.6f}") |
| 148 | +print(f"Fidelity = {final_fidelity:.6f}") |
| 149 | +How to use this script: Set the desired num_qubits, total_time, and trotter_steps at the top. Increasing trotter_steps (smaller $\Delta t$) will generally improve the fidelity (closer to 1) at the cost of longer runtime, illustrating the Trotter accuracy trade-off . The Hamiltonian terms and coefficients are printed for transparency. By default, intermediate step fidelity tracking is off; if you set track_error = True, the script will print the fidelity after each step (this is useful for diagnosing error accumulation, but note it incurs extra computation to propagate the exact state each step). |
| 150 | + |
| 151 | +Running this script will output the final evolved state from the Trotter simulation and the exact evolved state, along with their overlap. For example, you might see an output like: |
| 152 | +Hamiltonian has 9 terms on 3 qubits: |
| 153 | + +0.354 * (XIZ) |
| 154 | + -0.280 * (ZYX) |
| 155 | + +0.625 * (ZZX) |
| 156 | + ... (6 more terms) ... |
| 157 | +Final state (Trotter approximation): |
| 158 | +[ 0.769+0.150j 0.001-0.012j ... 0.617-0.066j ] |
| 159 | + |
| 160 | +Final state (Exact evolution): |
| 161 | +[ 0.764+0.161j -0.002-0.002j ... 0.627-0.053j ] |
| 162 | + |
| 163 | +Overlap = 0.999742+0.000000j |
| 164 | +Fidelity = 0.999484 |
| 165 | +In this example, the fidelity is ~0.9995, indicating the Trotter result is extremely close to the exact result (99.95% fidelity) for the chosen parameters. By decreasing the number of Trotter steps (larger $\Delta t$), the fidelity will drop (more Trotter error), whereas increasing the steps will improve the accuracy at the cost of more computation. This demonstrates the core idea of Trotterized time evolution: we can systematically trade off accuracy and efficiency while simulating quantum dynamics without needing specialized quantum libraries, using only NumPy and SciPy. The approach is flexible for any number of qubits (limited by memory/exponential matrix size) and any Hamiltonian given as a sum of Pauli operators. |
0 commit comments