Metadata-Version: 2.4
Name: qlibx
Version: 1.0.4
Summary: A lightweight quantum computing library for foundational tools and mathematical operations.
Home-page: https://github.com/Suraj52721/qc_lib/tree/master
Author: Suraj Singh
Author-email: Suraj Singh <suraj.52721@gmail.com>
License: MIT License
        
        Copyright (c) 2025 Suraj Singh
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this Package and associated documentation files (the "Package"), to deal
        in the Package without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Package, and to permit persons to whom the Package is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Package.
        
        THE PACKAGE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE PACKAGE OR THE USE OR OTHER DEALINGS IN THE
        PACKAGE.
Classifier: Programming Language :: Python :: 3
Classifier: License :: OSI Approved :: MIT License
Classifier: Operating System :: OS Independent
Classifier: Intended Audience :: Science/Research
Classifier: Topic :: Scientific/Engineering :: Physics
Requires-Python: >=3.7
Description-Content-Type: text/markdown
License-File: License.txt
Requires-Dist: numpy>=1.21.0
Dynamic: author
Dynamic: home-page
Dynamic: license-file
Dynamic: requires-python

# Qlibx: A Quantum Computing Library

Qlibx is a comprehensive Python library designed to provide foundational tools for quantum computing. It offers classes and methods to represent and manipulate quantum states, operators, density matrices, and quantum channels. The library is lightweight, mathematically rigorous, and focuses on the foundations of quantum mechanics, making it an excellent choice for learning, experimentation, and research.

---

## Features

- **Quantum States**:
    - `Ket` and `Bra` classes to represent quantum states
    - Support for operations like addition, subtraction, scalar multiplication, tensor products, and inner/outer products
    - Automatic handling of complex and real coefficients

- **Quantum Operators**:
    - `Operator` class to represent quantum operators
    - Methods for Hermitian, anti-Hermitian, unitary, and normality checks
    - Commutator and anti-commutator calculations
    - Spectral decomposition for Hermitian operators
    - Partial trace for composite systems
    - Von Neumann entropy calculation
    - Support for multi-qubit gates (CNOT, Hadamard)

- **Density Matrices**:
    - `DensityMatrix` class for mixed quantum states
    - Automatic validation of density matrix properties
    - Fidelity calculation between states
    - Time evolution under unitary operators
    - Partial trace support

- **Quantum Channels**:
    - `QuantumChannel` class for representing quantum noise
    - Predefined channels: amplitude damping, depolarizing, phase damping
    - Quantum teleportation protocol implementation

- **Predefined Operators**:
    - Pauli matrices (`pauli_x`, `pauli_y`, `pauli_z`) and identity matrix

---

## Installation

### Via pip
```bash
pip install qlibx
```

### From source
Clone the repository and include the library in your Python project:

```bash
git clone https://github.com/Suraj52721/qc_lib/tree/master
cd qlibx
```

Ensure the `qlibx` directory is in your Python path.

---

## Usage Guide

### Importing the Library

```python
from qlibx import Ket, Bra, Operator, DensityMatrix, QuantumChannel
import numpy as np
```

---

## Quantum States

### Creating Ket and Bra Vectors

```python
# Create a Ket vector |0⟩
ket0 = Ket([1, 0])
print(ket0)  # Output: Ket([1. 0.])

# Create a Ket vector |1⟩
ket1 = Ket([0, 1])
print(ket1)  # Output: Ket([0. 1.])

# Create a Bra vector ⟨0|
bra0 = Bra([1, 0])
print(bra0)  # Output: Bra([1. 0.])

# Complex coefficients
ket_complex = Ket([1, 1j])
print(ket_complex)  # Output: Ket([1.+0.j 0.+1.j])
```

### Basic Operations

#### Addition and Subtraction

```python
ket1 = Ket([1, 0])
ket2 = Ket([0, 1])

# Addition: |+⟩ = (|0⟩ + |1⟩)/√2
ket_plus = (ket1 + ket2) * (1/np.sqrt(2))
print(ket_plus)  # Output: Ket([0.70710678 0.70710678])

# Subtraction: |-⟩ = (|0⟩ - |1⟩)/√2
ket_minus = (ket1 - ket2) * (1/np.sqrt(2))
print(ket_minus)  # Output: Ket([ 0.70710678 -0.70710678])
```

#### Scalar Multiplication

```python
ket = Ket([1, 0])

# Multiply by scalar
result = 2 * ket
print(result)  # Output: Ket([2. 0.])

# Normalization
ket_unnorm = Ket([3, 4])
norm = np.sqrt(ket_unnorm.inner_product(ket_unnorm.dagger()))
ket_norm = (1/norm) * ket_unnorm
print(ket_norm)  # Output: Ket([0.6 0.8])
```

### Dagger (Conjugate Transpose)

```python
# Ket to Bra
ket = Ket([1, 1j])
bra = ket.dagger()
print(bra)  # Output: Bra([1.-0.j 0.-1.j])

# Bra to Ket
bra = Bra([1, 1j])
ket = bra.dagger()
print(ket)  # Output: Ket([1.-0.j 0.-1.j])
```

### Inner and Outer Products

```python
# Inner product: ⟨ψ|φ⟩
ket_psi = Ket([1, 0])
bra_phi = Bra([1, 0])
inner = ket_psi.inner_product(bra_phi)
print(inner)  # Output: 1.0

# Outer product: |ψ⟩⟨φ| (creates a projection operator)
ket = Ket([1, 0])
bra = Bra([1, 0])
outer = ket.outer_product(bra)
print(outer)
# Output:
# [[1. 0.]
#  [0. 0.]]
```

### Tensor Products

```python
# Two-qubit state |00⟩
ket0 = Ket([1, 0])
ket00 = ket0.tensor(ket0)
print(ket00)  # Output: Ket([1. 0. 0. 0.])

# Bell state |Φ+⟩ = (|00⟩ + |11⟩)/√2
ket00 = Ket([1, 0]).tensor(Ket([1, 0]))
ket11 = Ket([0, 1]).tensor(Ket([0, 1]))
bell_state = (ket00 + ket11) * (1/np.sqrt(2))
print(bell_state)  # Output: Ket([0.70710678 0. 0. 0.70710678])

# Multiple tensor products
ket_3qubit = Ket([1, 0]).tensor(Ket([1, 0], Ket([1, 0]))
print(ket_3qubit)  # Output: Ket([1. 0. 0. 0. 0. 0. 0. 0.])
```

---

## Quantum Operators

### Creating and Applying Operators

```python
# Define Pauli-X operator (NOT gate)
pauli_x_op = Operator(Operator.pauli_x)
print(pauli_x_op)
# Output: Operator([[0 1]
#                   [1 0]])

# Apply operator to a Ket
ket0 = Ket([1, 0])
ket1 = pauli_x_op.op(ket0)
print(ket1)  # Output: Ket([0.+0.j 1.+0.j])

# Pauli-Y operator
pauli_y_op = Operator(Operator.pauli_y)
print(pauli_y_op)
# Output: Operator([[ 0.+0.j -0.-1.j]
#                   [ 0.+1.j  0.+0.j]])

# Pauli-Z operator
pauli_z_op = Operator(Operator.pauli_z)
print(pauli_z_op)
# Output: Operator([[ 1  0]
#                   [ 0 -1]])
```

### Operator Properties

```python
# Check if operator is Hermitian
X = Operator(Operator.pauli_x)
print(X.hermitian())  # Output: True

# Check if operator is anti-Hermitian
Y = Operator(Operator.pauli_y)
print(Y.antihermitian())  # Output: True

# Check if operator is unitary
print(X.unitary())  # Output: True

# Check if operator is normal (commutes with its adjoint)
print(X.normal())  # Output: True
```

### Operator Arithmetic

```python
# Addition
I = Operator(Operator.identity)
X = Operator(Operator.pauli_x)
result = I + X
print(result)
# Output: Operator([[1 1]
#                   [1 1]])

# Subtraction
result = I - X
print(result)
# Output: Operator([[ 1 -1]
#                   [-1  1]])

# Scalar multiplication
result = 2 * X
print(result)
# Output: Operator([[0 2]
#                   [2 0]])

# Matrix multiplication
X = Operator(Operator.pauli_x)
Y = Operator(Operator.pauli_y)
result = X @ Y
print(result)
# Output: Operator([[0.+1.j 0.+0.j]
#                   [0.+0.j 0.-1.j]])
```

### Commutators and Anti-Commutators

```python
X = Operator(Operator.pauli_x)
Y = Operator(Operator.pauli_y)
Z = Operator(Operator.pauli_z)

# Commutator [X, Y] = XY - YX = 2iZ
commutator = X.commutator(Y)
print(commutator)
# Output: Operator([[0.+2.j 0.+0.j]
#                   [0.+0.j 0.-2.j]])

# Anti-commutator {X, Y} = XY + YX
anti_comm = X.anti_commutator(Y)
print(anti_comm)
# Output: Operator([[0.+0.j 0.+0.j]
#                   [0.+0.j 0.+0.j]])

# Pauli matrices anti-commute
print(X.anti_commutator(Z))
# Output: Operator([[0 0]
#                   [0 0]])
```

### Spectral Decomposition

```python
# Spectral decomposition of Pauli-Z
Z = Operator(Operator.pauli_z)
spectral = Z.spectral_decom()
for eigenvalue, eigenvector in spectral:
    print(f"Eigenvalue: {eigenvalue}, Eigenvector: {eigenvector}")
# Output:
# Eigenvalue: -1.0, Eigenvector: [0. 1.]
# Eigenvalue: 1.0, Eigenvector: [1. 0.]

# General Hermitian operator
H = Operator([[2, 1], [1, 2]])
spectral = H.spectral_decom()
for eigenvalue, eigenvector in spectral:
    print(f"Eigenvalue: {eigenvalue}, Eigenvector: {eigenvector}")
# Output:
# Eigenvalue: 1.0, Eigenvector: [-0.70710678  0.70710678]
# Eigenvalue: 3.0, Eigenvector: [0.70710678 0.70710678]
```

### Tensor Products of Operators

```python
# Two-qubit operator: X ⊗ I
X = Operator(Operator.pauli_x)
I = Operator(Operator.identity)
X_I = X.tensor(I)
print(X_I)
# Output: Operator([[0 0 1 0]
#                   [0 0 0 1]
#                   [1 0 0 0]
#                   [0 1 0 0]])

# I ⊗ X (different qubit)
I_X = I.tensor(X)
print(I_X)
# Output: Operator([[0 1 0 0]
#                   [1 0 0 0]
#                   [0 0 0 1]
#                   [0 0 1 0]])
```

### Multi-Qubit Gates

#### CNOT Gate

```python
# CNOT gate on 2 qubits (control=0, target=1)
cnot = Operator.cnot(control=0, target=1, no_of_qubits=2)
print(cnot)
# Output: Operator([[1.+0.j 0.+0.j 0.+0.j 0.+0.j]
#                   [0.+0.j 1.+0.j 0.+0.j 0.+0.j]
#                   [0.+0.j 0.+0.j 0.+0.j 1.+0.j]
#                   [0.+0.j 0.+0.j 1.+0.j 0.+0.j]])

# Apply CNOT to |10⟩ → |11⟩
ket10 = Ket([0, 1, 0, 0])
result = cnot.op(ket10)
print(result)  # Output: Ket([0.+0.j 0.+0.j 0.+0.j 1.+0.j])  # |11⟩
```

#### Hadamard Gate

```python
# Hadamard on qubit 0 in a 2-qubit system
H0 = Operator.hadamard(qubit=0, no_of_qubits=2)
print(H0)
# Output: Operator([[ 0.70710678+0.j  0.70710678+0.j  0.        +0.j  0.        +0.j]
#                   [ 0.70710678+0.j -0.70710678+0.j  0.        +0.j  0.        +0.j]
#                   [ 0.        +0.j  0.        +0.j  0.70710678+0.j  0.70710678+0.j]
#                   [ 0.        +0.j  0.        +0.j  0.70710678+0.j -0.70710678+0.j]])

# Apply Hadamard to |0⟩ → |+⟩
ket0 = Ket([1, 0])
H = Operator.hadamard(0, 1)
ket_plus = H.op(ket0)
print(ket_plus)  # Output: Ket([0.70710678+0.j 0.70710678+0.j])
```

### Partial Trace

```python
# Create a Bell state density matrix
bell = (Ket([1, 0, 0, 0]) + Ket([0, 0, 0, 1])) * (1/np.sqrt(2))
rho_bell = Operator(bell.outer_product(bell.dagger()))

# Trace out second qubit (keep first)
rho_reduced = rho_bell.partial_trace(keep=[0], dims=[2, 2])
print(rho_reduced)
# Output: Operator([[0.5 0. ]
#                   [0.  0.5]])  # Maximally mixed state

# Trace out first qubit (keep second)
rho_reduced = rho_bell.partial_trace(keep=[1], dims=[2, 2])
print(rho_reduced)
# Output: Operator([[0.5 0. ]
#                   [0.  0.5]])  # Also maximally mixed
```

### Von Neumann Entropy

```python
# Entropy of a pure state (should be 0)
ket = Ket([1, 0])
rho_pure = Operator(ket.outer_product(ket.dagger()))
entropy = rho_pure.von_neumann_entropy()
print(f"Entropy of pure state: {entropy:.6f}")  # Output: 0.000000

# Entropy of maximally mixed state (should be ln(2))
rho_mixed = Operator([[0.5, 0], [0, 0.5]])
entropy = rho_mixed.von_neumann_entropy()
print(f"Entropy of mixed state: {entropy:.6f}")  # Output: 0.693147 (ln(2))
```

---

## Density Matrices

### Creating Density Matrices

```python
# Pure state density matrix: |0⟩⟨0|
ket0 = Ket([1, 0])
rho_pure = DensityMatrix([[1, 0], [0, 0]])
print(rho_pure)

# Maximally mixed state: I/2
rho_mixed = DensityMatrix([[0.5, 0], [0, 0.5]])
print(rho_mixed)

# From a Ket
ket = Ket([1, 0])
rho = DensityMatrix(ket.outer_product(ket.dagger()))
```

### Fidelity Calculation

```python
# Fidelity between pure states
rho1 = DensityMatrix([[1, 0], [0, 0]])  # |0⟩
rho2 = DensityMatrix([[0, 0], [0, 1]])  # |1⟩
fidelity = rho1.fidelity(rho2)
print(f"Fidelity: {fidelity:.4f}")  # Output: 0.0000 (orthogonal states)

# Fidelity between identical states
rho1 = DensityMatrix([[1, 0], [0, 0]])
rho2 = DensityMatrix([[1, 0], [0, 0]])
fidelity = rho1.fidelity(rho2)
print(f"Fidelity: {fidelity:.4f}")  # Output: 1.0000

# Fidelity with mixed state
rho_pure = DensityMatrix([[1, 0], [0, 0]])
rho_mixed = DensityMatrix([[0.5, 0], [0, 0.5]])
fidelity = rho_pure.fidelity(rho_mixed)
print(f"Fidelity: {fidelity:.4f}")  # Output: 0.2500
```

### Time Evolution

```python
# Initialize state
rho = DensityMatrix([[1, 0], [0, 0]])  # |0⟩

# Apply Pauli-X (bit flip)
X = Operator(Operator.pauli_x)
rho_evolved = rho.evolve(X)
print(rho_evolved)
# Output: DensityMatrix([[0 0]
#                        [0 1]])  # |1⟩

# Apply Hadamard
H = Operator.hadamard(0, 1)
rho_evolved = rho.evolve(H)
print(rho_evolved)
# Output: DensityMatrix([[0.5 0.5]
#                        [0.5 0.5]])  # |+⟩ state
```

### Partial Trace for Density Matrices

```python
# Two-qubit maximally entangled state
bell = (Ket([1, 0, 0, 0]) + Ket([0, 0, 0, 1])) * (1/np.sqrt(2))
rho_bell = DensityMatrix(bell.outer_product(bell.dagger()))

# Trace out second qubit
rho_A = rho_bell.partial_trace(keep=[0], dims=[2, 2])
print(rho_A)
# Output: DensityMatrix([[0.5 0. ]
#                        [0.  0.5]])  # Maximally mixed

# Entropy of reduced state
entropy = rho_A.von_neumann_entropy()
print(f"Entanglement entropy: {entropy:.6f}")  # Output: 0.693147
```

---

## Quantum Channels

### Amplitude Damping Channel

```python
# Create amplitude damping channel with γ = 0.2
gamma = 0.2
channel = QuantumChannel.amplitude_damping(gamma)

# Apply to excited state |1⟩
rho_initial = DensityMatrix([[0, 0], [0, 1]])
rho_final = channel.apply(rho_initial)
print(rho_final)
# The state decays towards |0⟩

# Fidelity with initial state
fidelity = rho_final.fidelity(rho_initial)
print(f"Fidelity after damping: {fidelity:.4f}")
```

### Depolarizing Channel

```python
# Create depolarizing channel with p = 0.1
p = 0.1
channel = QuantumChannel.depolarizing(p)

# Apply to pure state
rho_initial = DensityMatrix([[1, 0], [0, 0]])
rho_final = channel.apply(rho_initial)
print(rho_final)
# The state becomes more mixed

# Calculate entropy increase
entropy_initial = rho_initial.von_neumann_entropy()
entropy_final = rho_final.von_neumann_entropy()
print(f"Entropy increase: {entropy_final - entropy_initial:.4f}")
```

### Phase Damping Channel

```python
# Create phase damping channel with γ = 0.3
gamma = 0.3
channel = QuantumChannel.phase_damping(gamma)

# Apply to superposition state |+⟩
ket_plus = (Ket([1, 0]) + Ket([0, 1])) * (1/np.sqrt(2))
rho_initial = DensityMatrix(ket_plus.outer_product(ket_plus.dagger()))
rho_final = channel.apply(rho_initial)
print(rho_final)
# Off-diagonal elements decay
```

### Custom Quantum Channel

```python
# Define custom Kraus operators
K0 = Operator([[1, 0], [0, np.sqrt(0.9)]])
K1 = Operator([[0, np.sqrt(0.1)], [0, 0]])

# Create custom channel
custom_channel = QuantumChannel([K0, K1])

# Apply to state
rho = DensityMatrix([[1, 0], [0, 0]])
rho_final = custom_channel.apply(rho)
print(rho_final)
```

---

## Quantum Teleportation

### Basic Teleportation (No Noise)

```python
# State to teleport: |+⟩
psi = (Ket([1, 0]) + Ket([0, 1])) * (1/np.sqrt(2))

# Run teleportation protocol
results = QuantumChannel.quantum_teleportation(psi, gamma=0.0)

# Check results
print(f"Average fidelity: {results['average_fidelity']:.6f}")
print(f"Measurement probabilities: {results['measurement_probabilities']}")
```

### Teleportation with Noise

```python
# State to teleport: |0⟩
psi = Ket([1, 0])

# Run with amplitude damping noise
results = QuantumChannel.quantum_teleportation(psi, gamma=0.1)

# Analyze results
print(f"Average fidelity: {results['average_fidelity']:.6f}")
for outcome, fidelity in results['fidelities'].items():
    prob = results['measurement_probabilities'][outcome]
    print(f"Outcome {outcome}: prob={prob:.4f}, fidelity={fidelity:.6f}")
```

---

## Example: Bell State Creation and Analysis

```python
from qlibx import Ket, Operator, DensityMatrix

# Create |00⟩ state
ket00 = Ket([1, 0]).tensor(Ket([1, 0]))

# Apply Hadamard to first qubit
H = Operator.hadamard(0, 2)
state_after_H = H.op(ket00)

# Apply CNOT
CNOT = Operator.cnot(0, 1, 2)
bell_state = CNOT.op(state_after_H)

print("Bell state |Φ+⟩:")
print(bell_state)

# Create density matrix
rho = DensityMatrix(bell_state.outer_product(bell_state.dagger()))

# Check entanglement via partial trace
rho_A = rho.partial_trace(keep=[0], dims=[2, 2])
entropy = rho_A.von_neumann_entropy()
print(f"\nEntanglement entropy: {entropy:.6f}")
print(f"This is {'maximal' if abs(entropy - np.log(2)) < 0.001 else 'not maximal'} entanglement")
```

---

## Example: Quantum Circuit Simulation

```python
from qlibx import Ket, Operator

# Initialize in |0⟩
psi = Ket([1, 0])
print(f"Initial state: {psi.coef}")

# Apply Hadamard gate
H = Operator.hadamard(0, 1)
psi = H.op(psi)
print(f"After Hadamard: {psi.coef}")

# Apply Pauli-Z gate
Z = Operator(Operator.pauli_z)
psi = Z.op(psi)
print(f"After Pauli-Z: {psi.coef}")

# Apply Hadamard again
psi = H.op(psi)
print(f"Final state: {psi.coef}")
```

---

## Example: Decoherence Simulation

```python
from qlibx import Ket, DensityMatrix, QuantumChannel
import matplotlib.pyplot as plt

# Initial superposition state
ket_plus = (Ket([1, 0]) + Ket([0, 1])) * (1/np.sqrt(2))
rho_initial = DensityMatrix(ket_plus.outer_product(ket_plus.dagger()))

# Simulate decoherence over time
gammas = np.linspace(0, 1, 20)
fidelities = []

for gamma in gammas:
    channel = QuantumChannel.amplitude_damping(gamma)
    rho_final = channel.apply(rho_initial)
    fidelity = rho_final.fidelity(rho_initial)
    fidelities.append(fidelity)

# Plot results
plt.plot(gammas, fidelities)
plt.xlabel('Damping parameter γ')
plt.ylabel('Fidelity')
plt.title('Decoherence of Superposition State')
plt.grid(True)
plt.show()
```

---

## Advanced Usage Tips

### Working with Complex States

```python
# Create complex superposition
alpha = 1/np.sqrt(2)
beta = 1j/np.sqrt(2)
psi = alpha * Ket([1, 0]) + beta * Ket([0, 1])
print(psi)

# Check normalization
norm_squared = psi.inner_product(psi.dagger())
print(f"Norm squared: {norm_squared}")  # Should be 1
```

### Chaining Operations

```python
# Multiple gate applications
ket = Ket([1, 0])
X = Operator(Operator.pauli_x)
Y = Operator(Operator.pauli_y)
Z = Operator(Operator.pauli_z)

# Apply X, then Y, then Z
result = Z.op(Y.op(X.op(ket)))
print(result)

# Or using operator composition
XYZ = Z @ Y @ X
result = XYZ.op(ket)
print(result)
```

### Error Handling

```python
try:
    # This will raise an error (invalid density matrix)
    rho = DensityMatrix([[1, 0], [0, 2]])  # Trace > 1
except ValueError as e:
    print(f"Error: {e}")

try:
    # This will raise an error (not Hermitian)
    op = Operator([[1, 1], [0, 1]])
    entropy = op.von_neumann_entropy()
except ValueError as e:
    print(f"Error: {e}")
```

---

## Contributing

Contributions are welcome! If you find a bug or have a feature request, please open an issue or submit a pull request on [GitHub](https://github.com/Suraj52721/qc_lib).

---

## License

This project is licensed under the [MIT License](LICENSE).

---

## Acknowledgments

Qlibx is inspired by the mathematical foundations of quantum mechanics and quantum information theory. It aims to provide an intuitive and powerful interface for quantum computing enthusiasts, researchers, and educators.

---

## Requirements

- Python 3.7+
- NumPy
- SciPy

Install dependencies:
```bash
pip install numpy scipy
```

---

## Citation

If you use Qlibx in your research or projects, please cite:

```
@software{qlibx2024,
  author = {Suraj},
  title = {Qlibx: A Quantum Computing Library},
  year = {2024},
  url = {https://github.com/Suraj52721/qc_lib}
}
```
