Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/main.yml
Original file line number Diff line number Diff line change
Expand Up @@ -7,16 +7,16 @@ jobs:
runs-on: ubuntu-latest
steps:
- name: Check out code
uses: actions/checkout@v3
uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
run: |
pip install -U pip
pip install -r requirements.txt
pip install "black>=21.5b0" "coverage>=5.5" "pytest>=2.1.0"
pip install black coverage pytest
- name: Format code with Black
run: python3 -m black $(find . -name '*.py')
- name: Run tests with coverage
Expand Down
6 changes: 3 additions & 3 deletions .github/workflows/python-publish.yml
Original file line number Diff line number Diff line change
Expand Up @@ -21,9 +21,9 @@ jobs:
runs-on: ubuntu-latest

steps:
- uses: actions/checkout@v3
- uses: actions/checkout@v4
- name: Set up Python
uses: actions/setup-python@v4
uses: actions/setup-python@v5
with:
python-version: '3.x'
- name: Install dependencies
Expand All @@ -33,7 +33,7 @@ jobs:
- name: Build package
run: python -m build
- name: Publish package
uses: pypa/gh-action-pypi-publish@27b31702a0e7fc50959f5ad993c78deac1bdfc29
uses: pypa/gh-action-pypi-publish@release/v1
with:
user: __token__
password: ${{ secrets.PYPI_API_TOKEN }}
21 changes: 13 additions & 8 deletions .gitignore
Original file line number Diff line number Diff line change
@@ -1,7 +1,3 @@
# Development
Untitled.*
untitled.*

# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
Expand Down Expand Up @@ -80,9 +76,7 @@ docs/_build/
target/

# Jupyter Notebook
.ipynb_checkpoints/
.jupyter/
.virtual_documents/
.ipynb_checkpoints

# IPython
profile_default/
Expand All @@ -100,6 +94,12 @@ ipython_config.py
# install all needed dependencies.
#Pipfile.lock

# UV
# Similar to Pipfile.lock, it is generally recommended to include uv.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
#uv.lock

# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
Expand All @@ -112,8 +112,10 @@ ipython_config.py
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
# https://pdm.fming.dev/latest/usage/project/#working-with-version-control
.pdm.toml
.pdm-python
.pdm-build/

# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
Expand Down Expand Up @@ -164,3 +166,6 @@ cython_debug/
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/

# PyPI configuration file
.pypirc
30 changes: 2 additions & 28 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
# Qubit Simulator

Qubit Simulator is a simple and lightweight library that provides a quantum simulator for simulating qubits and quantum gates. It supports basic quantum operations and gates such as Hadamard, π/8, Controlled-Not, and generic unitary transformations.
Qubit Simulator is a simple and lightweight library that provides a quantum statevector simulator for simulating qubits and quantum gates. It supports basic quantum operations and gates using NumPy.

## Installation

Expand Down Expand Up @@ -37,7 +37,7 @@ simulator.cx(0, 2) # Controlled-Not gate
Define and apply custom gates using angles:

```python
simulator.u(2, 0.3, 0.4, 0.5) # Generic gate
simulator.u(0.3, 0.4, 0.5, 2) # Generic gate
```

### Measurements
Expand All @@ -52,32 +52,6 @@ print(simulator.run(shots=100))
{'000': 46, '001': 4, '100': 4, '101': 46}
```

### Circuit Representation

Get a string representation of the circuit:

```python
print(simulator)
```

```plaintext
-----------------------------------
| H | | @ | |
| | T | | |
| | | X | U(0.30, 0.40, 0.50) |
-----------------------------------
```

### Wavefunction Plot

Show the amplitude and phase of all quantum states:

```python
simulator.plot_wavefunction()
```

![Wavefunction Scatter Plot](https://github.com/splch/qubit-simulator/assets/25377399/de3242ef-9c14-44be-b49b-656e9727c618)

## Testing

Tests are included in the package to verify its functionality and provide more advanced examples:
Expand Down
2 changes: 2 additions & 0 deletions qubit_simulator/__init__.py
Original file line number Diff line number Diff line change
@@ -1,2 +1,4 @@
from .simulator import QubitSimulator
from .gates import Gates

__all__ = ["QubitSimulator", "Gates"]
125 changes: 48 additions & 77 deletions qubit_simulator/gates.py
Original file line number Diff line number Diff line change
@@ -1,98 +1,69 @@
import numpy as np
from typing import Union


class Gates:
"""
A class that represents the quantum gates.
"""
"""Minimal collection of common gates and helper methods."""

# Hadamard (H) gate
H: np.ndarray = np.array([[1, 1], [1, -1]]) / np.sqrt(2)
# π/8 (T) gate
T: np.ndarray = np.array([[1, 0], [0, np.exp(1j * np.pi / 4)]])
# Pauli-X (NOT) gate
X: np.ndarray = np.array([[0, 1], [1, 0]])
# Single-qubit gates (2x2)
X = np.array([[0, 1], [1, 0]], dtype=complex)
Y = np.array([[0, -1j], [1j, 0]], dtype=complex)
Z = np.array([[1, 0], [0, -1]], dtype=complex)
H = np.array([[1, 1], [1, -1]], dtype=complex) / np.sqrt(2)
S = np.diag([1, 1j]).astype(complex)
T = np.diag([1, np.exp(1j * np.pi / 4)]).astype(complex)

@staticmethod
def U(theta: float, phi: float, lambda_: float) -> np.ndarray:
"""
Generic (U) gate.

:param theta: Angle theta.
:param phi: Angle phi.
:param lambda_: Angle lambda.
:return: Unitary matrix representing the U gate.
"""
def U(theta: float, phi: float, lam: float) -> np.ndarray:
return np.array(
[
[np.cos(theta), -np.exp(1j * lambda_) * np.sin(theta)],
[np.cos(theta / 2), -np.exp(1j * lam) * np.sin(theta / 2)],
[
np.exp(1j * phi) * np.sin(theta),
np.exp(1j * (phi + lambda_)) * np.cos(theta),
np.exp(1j * phi) * np.sin(theta / 2),
np.exp(1j * (phi + lam)) * np.cos(theta / 2),
],
]
],
dtype=complex,
)

# Two-qubit gates (4x4)
@staticmethod
def create_controlled_gate(
gate: np.ndarray, control_qubit: int, target_qubit: int, num_qubits: int
) -> np.ndarray:
"""
Creates a controlled gate.
def SWAP_matrix() -> np.ndarray:
return np.array(
[[1, 0, 0, 0], [0, 0, 1, 0], [0, 1, 0, 0], [0, 0, 0, 1]], dtype=complex
)

:param gate: Matrix representing the gate.
:param control_qubit: Index of the control qubit.
:param target_qubit: Index of the target qubit.
:param num_qubits: Total number of qubits.
:return: Matrix representing the controlled gate.
"""
controlled_gate = np.eye(2**num_qubits, dtype=complex)
for basis in range(2**num_qubits):
basis_binary = format(basis, f"0{num_qubits}b")
if basis_binary[control_qubit] == "1":
target_state = int(
basis_binary[:target_qubit]
+ str(1 - int(basis_binary[target_qubit]))
+ basis_binary[target_qubit + 1 :],
2,
)
controlled_gate[basis, basis] = gate[
int(basis_binary[target_qubit]), int(basis_binary[target_qubit])
]
controlled_gate[basis, target_state] = gate[
int(basis_binary[target_qubit]), 1 - int(basis_binary[target_qubit])
]
controlled_gate[target_state, basis] = gate[
1 - int(basis_binary[target_qubit]), int(basis_binary[target_qubit])
]
controlled_gate[target_state, target_state] = gate[
1 - int(basis_binary[target_qubit]),
1 - int(basis_binary[target_qubit]),
]
return controlled_gate
@staticmethod
def iSWAP_matrix() -> np.ndarray:
return np.array(
[[1, 0, 0, 0], [0, 0, 1j, 0], [0, 1j, 0, 0], [0, 0, 0, 1]], dtype=complex
)

# Three-qubit gates (8x8)
@staticmethod
def create_inverse_gate(gate: np.ndarray) -> np.ndarray:
"""
Creates an inverse gate.
def Toffoli_matrix() -> np.ndarray:
# Flip the 3rd qubit if first two are |1>
m = np.eye(8, dtype=complex)
m[[6, 7], [6, 7]] = 0
m[6, 7] = 1
m[7, 6] = 1
return m

:param gate: Matrix representing the gate.
:return: Matrix representing the inverse gate.
"""
return np.conjugate(gate.T)
@staticmethod
def Fredkin_matrix() -> np.ndarray:
# Swap the last two qubits if the first is |1>
m = np.eye(8, dtype=complex)
m[[5, 6], [5, 6]] = 0
m[5, 6] = 1
m[6, 5] = 1
return m

@staticmethod
def _validate_gate(gate: np.ndarray):
"""
Validates the gate.
def inverse_gate(U: np.ndarray) -> np.ndarray:
return U.conjugate().T

:param gate: Matrix representing the gate.
:raises ValueError: If the gate is invalid.
"""
if not np.allclose(
gate @ Gates.create_inverse_gate(gate), np.eye(gate.shape[0])
):
raise ValueError(
"The gate must be unitary. Its conjugate transpose must be equal to its inverse."
)
@staticmethod
def controlled_gate(U: np.ndarray) -> np.ndarray:
c = np.zeros((4, 4), dtype=complex)
c[:2, :2] = np.eye(2)
c[2:, 2:] = U
return c
Loading
Loading