# QGBx
<img width="1600" height="300" alt="QGBXhEADER" src="https://github.com/user-attachments/assets/90c3af9a-2f7b-4386-a4de-aeb187c13ebf" />

---
*"Nature isn't classical, dammit, and if you want to make a simulation of nature, you'd better make it quantum mechanical"*


QGBx is an open-source, extensible package designed as a submission for the <a href="https://www.thewiser.org/quantum-walks-monte-carlo" target="_blank">Project 1:</a> "Quantum Walks and Monte Carlo" hosted by the Wiser-Womanium Quantum Summer Program. This package serves as a sandbox for generating several statistical distributions using different Variational Multi-Dimensional Quantum Galton Board architectures, mainly derived from the <a href="https://arxiv.org/abs/2202.01735"> Universal Statistical Distribution</a> paper by Mark Carney & Ben Varcoe., along with the authors’ efforts to optimize its structure and results for various uses under different noiseless and noisy quantum simulations and even an end-point to run the generated circuits on a real QPU.

## Installation

You can install QGBx using

```bash
pip install --upgrade qgbx
```

## Usage

QGBx was designed to be extensible in the future, where other Distributions, Devices, Visualization, and Analysis methods can be supported through the same architecture for educational purposes of studying the behavior of the Galton board under both classical and quantum physics, where it was proven that it may help to achieve universal statistical simulators and probability distributions' encoders.

The diagram below shows the relationship between the different objects and the steps of creating, simulating, and retrieving results using QGBx:

<p align="center">
  <img src="https://github.com/user-attachments/assets/188ead0c-ddd5-4356-9b2f-eaad6d1d27da" 
       alt="Qgbx Diagram Usage drawio" 
       width="700" />
</p>

### 1. Creating a Device: 

QGBx supports two kinds of `Device`: **Simulators** and **Real Devices**.

- **Simulators**  
  - Noiseless all-to-all simulators: `PennylanDefaultQubit` and `QiskitAerSimulator`.  
  - Noisy simulator: `QiskitFakeTorino` — simulates the noise effects of the Heron IBM Torino device.  

- **Real Devices**  
  - `IBM_Torino`: Runs your generated circuit on the real QPU.  
    > Requires a personal IBM token and a CRN code for a created instance.

You can create a device object using QGBx as follows:

```python
# Import the desired device class
from QGBx.devices import QiskitAerSimulator, PennylanDefaultQubit, QiskitFakeTorino, IBM_Torino

# Create a PennylanDefaultQubit device (noiseless simulator)
dev = PennylanDefaultQubit(shots=1000)

# Create a QiskitAerSimulator device (noiseless simulator)
dev = QiskitAerSimulator(shots=1000)

# Create a QiskitFakeTorino device (noisy simulator replicating IBM Torino)
dev = QiskitFakeTorino(ai_optimized=False, optimization_level=0, ai_optimization_level=0, shots=1000)

# Create an IBM_Torino device (real QPU) - requires IBM token and CRN code
dev = IBM_Torino(token="YOUR_IBM_TOKEN", instance_CRN="YOUR_CRN_CODE", optimization_level=1, shots=1000)
```
> **Note:**  
> - `shots`: The number of circuit executions (measurements).  
> - `optimization_level`: Qiskit's built-in preset pass manager circuit optimization level (0–3). Higher values can reduce circuit depth but may change gate structure.
> - `ai_optimized`: Boolean (`True`/`False`). Enables or disables the generate_ai_pass_manager.  
> - `ai_optimization_level`: QGBx's AI-based optimization level (0–3). Controls how much the ai_pass manager try to optimize circuit depth and transpilation.  

### 2. Creating a Distribution

QGBx supports three built-in distributions:  

- `Gaussian` — Which is essentially a binomial distribution that converges toward a Gaussian distribution under the Laplace–de Moivre theorem when the number of layers of the Galton board becomes large.
  
> **Note:**  
> -`Distribution` object should take a device as the first parameter :)

```python
from QGBx.distributions import Gaussian

# p is a parameter of the binomial distribution that controls its bias (0.5 is centered)
dist = Gaussian(dev, p=0.4) 
```

- `Exponential` — A distribution where the probabilities follow an exponential decay pattern, defined by a rate parameter (λ).

```python
from QGBx.distributions import Exponential

# rate is λ (lambda) of the exponential function
dist = Exponential(dev, rate=0.5)
```
 

- `Hadamard_QW` — A quantum walk distribution generated by Hadamard operations, with different types that define the result of the walk (related to the input state — see the literature for more).

```python
from QGBx.distributions import HadamardQW

# type defines the result of the Hadamard walk (related to the input state)
# type options: ["Symmetric", "Asymmetric_Right", "Asymmetric_Left"]
dist = HadamardQW(dev, type="Symmetric")
```  

The distribution object will then be filled with the results of the simulation and its ideal (theoretical) counterpart in the next step.  

The package also gives the user the ability to create a custom Galton board by manipulating the probabilities of pegs in two ways:  
- Per peg — each individual peg of the Galton board gets its own probability (of going left or right) using `PegControlled`.  
- Per layer — all pegs of the same layer get the same probability using `LayerControlled` *(not yet tested)*.  

The user can give specific probabilities for each peg (or layer) or can set a target probability they want, and the package will try to numerically (variationally) tune the pegs (or layers) for that specific target (if possible) using the *least squares* method.  

> The user can also directly define the angles of the RX gates. See the structure of the circuits to better understand.

For this distribution the target probabilities "target=" (and effectively the angles "angles=") should be a list encoded as shown in the picture below:

<p align="center">
  <img src="https://github.com/user-attachments/assets/1efa4e38-c473-4fc9-976e-bb057f23ac22" 
       alt="PegsNaming drawio"
       width="500" />
</p>

The code for it is like the following:

```python
from QGBx.distributions import PegControlled

# probs (and angles) should be a list encoded as shown in the picture above
dist = PegControlled(device, angles=None, probs=None, optimizer="least_squares", target=None)
```

There are also the `GaussianOp` and `ExponentialOp`, which follow the same logic of the Galton board but use another circuit designed by the authors for the purpose of the Project. This alternative design overcomes many shortcomings in circuit depth and efficiency compared to the main one proposed in the *Universal Statistical Simulator* method.  

They can be instantiated as follows:  

```python
from QGBx.distributions import ExponentialOp, GaussianOp

dist = ExponentialOp(dev, rate=0.5) # rate is λ (lambda) of the exponential function

dist = GaussianOp(dev, p=0.4) # p is a parameter of the binomial distribution that controls its bias (0.5 is centered)
```

### 3. Creating a Generator

In this package, the `Generator` is the orchestrator that creates the circuit for a specific Quantum Galton Board architecture to achieve a given `Distribution` using a chosen `Device`.  

It can be instantiated as follows:  

```python
import QGBx

gen = QGBx.Generator(dev, dist)
```

After that, it provides the user with the following methods:

---
#### `galton_board(n_layers)`

Build the Galton Board circuit.

**Parameters**:  
- `n_layers` *(int)* – Number of layers in the Galton Board.

**Returns**:  
- *Quantum circuit object* – The constructed Galton Board circuit and it will automatically assign the circuit to the device, and will be ready for the running phase



#### `run()`
Execute the Galton Board circuit on the configured device.  

**Returns:**  
- `dict` – One-hot probability distribution from execution and will assign the measured probabilities to the Distribution object, and calculate the ideal probabilities and assign them to the same.



#### `job_results(jobID)`
Retrieve results for a given IBM job ID after using the job on the real device is completed (it may take time if the QPU is busy).  

**Parameters:**  
- `jobID` (`str`): IBM Quantum job ID.  

**Returns:**  
- `dict` – One-hot probability distribution.



#### `export_circuit_as_png(fold=-1, filename="circuit.png", style="black_white")`
Export the circuit diagram as a PNG image.  

**Parameters:**  
- `fold` (`int`): Folding parameter for circuit visualization. set to 25 if you want the circuit to be splitted (only with qiskit circuits)  
- `filename` (`str`): Output filename (and directory) 
- `style` (`str`): Diagram style. Only the supported



#### `draw_circuit(fold=-1)`
Draw the circuit using the device's visualization method.  

**Parameters:**  
- `fold` (`int`): Folding parameter for circuit visualization. set to 25 if you want the circuit to be splitted (only with qiskit circuits)



#### `export_qasm(version="2", filename="exported_circuit.txt")`
Export the circuit as a QASM file (only with qiskit circuits)  

**Parameters:**  
- `version` (`str`): QASM version. "2" or "3" 
- `filename` (`str`): Output filename (and directory)

---

### 4. Generating the n_layers galton board:

This step can be achieved through the genertaor `.galton_board(n_layers)` method as follow:

```python
n_layers = 6
gen.galton_board(n_layers) #bigger n_layers (more than 6 in some machines) will become computaionally hard if not used the Optimized circuit
```

> - it will automatically assign the circuit to the device, and will be ready for the running

### 6. Running the circuit:
This step can be achieved through the genertaor `.run()` method as follow:

```python
gen.run()
```

> - This will assign the measured probabilities to the Distribution object, and calculate the ideal probabilities and assign them to the same.

### 5. Analyze and Visualize the Results

After retrieving the measured and the ideal theoretical distributions, we can visualize the results through plotting, and analyze the circuit's performance by comparing the measured and the ideal distributions using different metrics.

#### Visualizing:
QGBx includes a `Visualizer` object that takes a `Distribution` object when instantiated, and gives the user the ability to:


---
#### `plot(interval_length=None, ideal=False)`

Plot the probability distribution (measured or ideal if "ideal" = True)

**Parameters**:  
- interval_length (int, optional): Number of bins to display.
- ideal (bool): Whether to plot the ideal distribution instead of measured.

This can be achieved through:
```python
QGBx.Visualiser(dist).plot()
```

this will result in the following:
<p align="center">
  <img alt="ExponentialGlaton" src="https://github.com/user-attachments/assets/11c8b365-9a34-4e0d-a42c-dab31fb5bb91"
       width="500" />
</p>


---

#### `plot_with_ideal(interval_length=None)`

Plot the measured probability distribution with the ideal distribution overlaid as points connected with lignes

**Parameters**:  
- interval_length (int, optional): Number of bins to display.
- ideal (bool): Whether to plot the ideal distribution instead of measured.

This can be achieved through:
```python
QGBx.Visualiser(dist).plot_with_ideal()
```

this will result in the following:
<p align="center">
  <img alt="Expo" src="https://github.com/user-attachments/assets/2bb8d6e4-65d2-40e0-a2e6-c4b901c78dad"
       width="500" />
</p>

---

#### Analyzing:
QGBx includes the `Analyzer` object that takes a `Distribution` object when instantiated that give the ability to benchmark the measured probability distribution against the ideal theoretical one to assess the accuracy of the circuit and the method through various metrics, and can print a summary of the results.  
This object also gives the user the ability to define a tolerance for each test in order to generate a report indicating whether the circuit achieved satisfactory results.

The `Analyzer` have the following methods:

| Method | Description |
|--------|-------------|
| `total_variation_distance()` | Calculate the Total Variation Distance (TVD) between the ideal (theoritical) and measured distributions. TVD measures the largest possible probability difference between two distributions. Values closer to 0 indicate high similarity. |
| `jensen_shannon_divergence()` | Calculate the Jensen–Shannon Divergence (JSD) between the ideal (theoritical) and measured distributions. JSD is symmetric and bounded between 0 and 1. A value close to 0 means the distributions are identical. |
| `chi_squared_test()` | Calculate the Chi-Squared statistic. Smaller values indicate that the measured results are close to the ideal distribution. |
| `hellinger_distance()` | Calculate the Hellinger Distance between the ideal and quantum distributions. Values range between 0 (identical) and 1 (maximally different). Values < 0.1 are generally acceptable. |
| `entropy_difference()` | Calculate the absolute difference in **Shannon entropy** between the ideal and measured distributions. it measures randomness and uncertaintywhere smaller differences mean the distributions have similar uncertainty levels. |
| **`analyze(thresholds=None, show_passed=True)`** | Run all analysis metrics and display results in a matplotlib table. Updates pass/fail status using thresholds and displays a formatted table of results. |
| **`get_analyze_results()`** | Return the results of the last analysis as a dictionary. If `analyze()` hasn't been run, it is executed automatically without displaying the table. |


This object can be used in the defined pipeline as follow:

```python
Analyzer = QGBx.Analyzer(dist)
Analyzer.analyze(show_passed=False)
```

This will results in the following output summary:
<p align="center">
  <img alt="Intrprt" src="https://github.com/user-attachments/assets/762b4585-a804-4212-af9a-aece8a1b52ec"
       width="800" />
</p>

The user can also define thresholds and compare the results to the:

```python
Analyzer = QGBx.Analyzer(dist)
Analyzer.analyze(thresholds = [0.02,0.05,0.1,0.1,0.05], show_passed=True)
```

This will results in the following output summary:
<p align="center">
  <img alt="Intrprt" src="https://github.com/user-attachments/assets/32c48c5c-2214-4db1-9e5d-102998db9b27"
       width="800" />
</p>













