Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Move normalization and improve quality #43

Open
wants to merge 14 commits into
base: main
Choose a base branch
from
18 changes: 8 additions & 10 deletions .github/workflows/geqie.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@ permissions:
contents: read

jobs:
build:
test:

runs-on: ubuntu-latest

Expand All @@ -25,15 +25,13 @@ jobs:
python-version: "3.11"
- name: Install dependencies
run: |
pip install -U uv
uv pip install flake8 pytest
make install-dependencies-uv-dev
- name: Lint with flake8
run: |
# stop the build if there are Python syntax errors or undefined names
flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
make install-requirements-ci
# - name: Lint with flake8
# run: |
# # stop the build if there are Python syntax errors or undefined names
# flake8 . --count --select=E9,F63,F7,F82 --show-source --statistics
# # exit-zero treats all errors as warnings. The GitHub editor is 127 chars wide
# flake8 . --count --exit-zero --max-complexity=10 --max-line-length=127 --statistics
- name: Test with pytest
run: |
make test
21 changes: 21 additions & 0 deletions Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
install-requirements:
pip install -r requirements/requirements.in

install-requirements-dev:
pip install -r requirements/requirements_dev.in

install-requirements-uv:
uv pip install -r requirements/requirements.in

install-requirements-uv-dev:
uv pip install -r requirements/requirements_dev.in


install-requirements-ci:
pip install -U uv
uv pip install -r requirements/requirements_dev.in --system

test:
pytest tests -W ignore::DeprecationWarning


6 changes: 2 additions & 4 deletions geqie/cli.py
Original file line number Diff line number Diff line change
Expand Up @@ -53,11 +53,9 @@ def _get_encoding_functions(params: Dict):
def _parse_image(params):
image = Image.open(params.get("image"))
if params.get("grayscale"):
image = np.asarray(ImageOps.grayscale(image))
return np.asarray(ImageOps.grayscale(image))
else:
image = np.asarray(image)
image = image / 255.0
return image
return np.asarray(image)


@cloup.group()
Expand Down
2 changes: 1 addition & 1 deletion geqie/encodings/frqi/init.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,5 +4,5 @@


def init(n_qubits: int) -> Statevector:
state = np.tile([1, 0], 2**(n_qubits-1))
state = np.tile([1, 0], 2**(n_qubits - 1))
return Statevector(state)
2 changes: 1 addition & 1 deletion geqie/encodings/frqi/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@


def map(u: int, v: int, R: int, image: np.ndarray) -> Operator:
p = image[u, v] * np.pi / (2 * 255.0)
p = image[u, v] / 255.0 * (np.pi / 2)
map_operator = [
[np.cos(p), -np.sin(p)],
[np.sin(p), np.cos(p)],
Expand Down
38 changes: 16 additions & 22 deletions geqie/encodings/ifrqi/map.py
Original file line number Diff line number Diff line change
@@ -1,39 +1,33 @@
import numpy as np
from qiskit.quantum_info import Operator

angle_00 = 0
angle_01 = 2 * np.pi / 10
angle_10 = 3 * np.pi / 10
angle_11 = 5 * np.pi / 10

BIT_PAIR_ANGLES = {
(0, 0): 0,
(0, 1): 2 / 10 * np.pi,
(1, 0): 3 / 10 * np.pi,
(1, 1): 5 / 10 * np.pi,
}

def R_y_gate(p):
R_y = [[np.cos(p), -np.sin(p)],
[np.sin(p), np.cos(p)]]
return R_y


def get_angle(bit_pair):
"""Returns the angle corresponding to the given bit pair."""
angles = {
(0, 0): angle_00,
(0, 1): angle_01,
(1, 0): angle_10,
(1, 1): angle_11
}
return angles[bit_pair]
def ry_gate(theta: float) -> np.ndarray:
return [
[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)],
]


def map(u: int, v: int, R: int, image: np.ndarray) -> Operator:
p = image[u, v]
pixel_value_as_binary_array = [int(bit) for bit in bin(p)[2:].zfill(8)][::-1]
pixel_value_as_binary = [int(bit) for bit in bin(p)[2:].zfill(8)][::-1]

# Initialize the map operator with the first bit pair
map_operator = R_y_gate(get_angle(tuple(pixel_value_as_binary_array[:2])))
bit_pair = tuple(pixel_value_as_binary[:2])
map_operator = ry_gate(BIT_PAIR_ANGLES[bit_pair])

# Iterate over remaining bit pairs and build the Kronecker product
for k in range(2, 8, 2):
bit_pair = tuple(pixel_value_as_binary_array[k:k + 2])
map_operator = np.kron(R_y_gate(get_angle(bit_pair)), map_operator)
bit_pair = tuple(pixel_value_as_binary[k:k + 2])
map_operator = np.kron(ry_gate(BIT_PAIR_ANGLES[bit_pair]), map_operator)

return Operator(map_operator)
51 changes: 26 additions & 25 deletions geqie/encodings/mcqi/map.py
Original file line number Diff line number Diff line change
@@ -1,32 +1,33 @@
import numpy as np
from qiskit.quantum_info import Operator

identity_gate = np.array([[1, 0], [0, 1]])
x_gate = np.array([[0, 1], [1, 0]])

I_GATE = np.eye(2)
X_GATE = np.array([
[0, 1],
[1, 0]
])

CHANNEL_POSITIONING = {
0: np.kron(I_GATE, I_GATE), # red
1: np.kron(I_GATE, X_GATE), # green
2: np.kron(X_GATE, I_GATE), # blue
}


def ry_gate(theta: float) -> np.ndarray:
return [
[np.cos(theta), -np.sin(theta)],
[np.sin(theta), np.cos(theta)]
]


def map(u: int, v: int, R: int, image: np.ndarray) -> Operator:
# Red channel:
p_r = image[u, v, 0] * np.pi / (2 * 255.0)
red_channel_Ry_gate = [
[np.cos(p_r), -np.sin(p_r)],
[np.sin(p_r), np.cos(p_r)]]
red_channel_map_operator = np.kron(np.kron(identity_gate, identity_gate), red_channel_Ry_gate)

# Green channel:
p_g = image[u, v, 1] * np.pi / (2 * 255.0)
green_channel_Ry_gate = [
[np.cos(p_g), -np.sin(p_g)],
[np.sin(p_g), np.cos(p_g)]]
green_channel_map_operator = np.kron(np.kron(identity_gate, x_gate), green_channel_Ry_gate)

# Blue channel:
p_b = image[u, v, 2] * np.pi / (2 * 255.0)
blue_channel_Ry_gate = [
[np.cos(p_b), -np.sin(p_b)],
[np.sin(p_b), np.cos(p_b)]]
blue_channel_map_operator = np.kron(np.kron(x_gate, identity_gate), blue_channel_Ry_gate)

# total:
map_operator = red_channel_map_operator + green_channel_map_operator + blue_channel_map_operator
operators = []
for channel in range(3):
p = image[u, v, channel] / 255.0 * (np.pi / 2)
operators.append(np.kron(CHANNEL_POSITIONING[channel], ry_gate(p)))

map_operator = np.array(operators).sum(axis=0)

return Operator(map_operator)
19 changes: 12 additions & 7 deletions geqie/encodings/ncqi/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,13 @@

from qiskit.quantum_info import Operator

identity_gate = np.array([[1, 0], [0, 1]])
x_gate = np.array([[0, 1], [1, 0]])

I_GATE = np.eye(2)
X_GATE = np.array([
[0, 1],
[1, 0]
])


def map(u: int, v: int, R: int, image: np.ndarray) -> Operator:
map_operator = [None, None, None]
Expand All @@ -17,17 +22,17 @@ def map(u: int, v: int, R: int, image: np.ndarray) -> Operator:
pixel_value_as_binary_array = [int(bit) for bit in pixel_value_as_binary_string][::-1]

if channel == 0:
map_operator[channel] = np.kron(identity_gate, identity_gate)
map_operator[channel] = np.kron(I_GATE, I_GATE)
elif channel == 1:
map_operator[channel] = np.kron(identity_gate, x_gate)
map_operator[channel] = np.kron(I_GATE, X_GATE)
elif channel == 2:
map_operator[channel] = np.kron(x_gate, identity_gate)
map_operator[channel] = np.kron(X_GATE, I_GATE)

for bit in pixel_value_as_binary_array[0:8]:
if bit == 1:
map_operator[channel] = np.kron(x_gate, map_operator[channel])
map_operator[channel] = np.kron(X_GATE, map_operator[channel])
else:
map_operator[channel] = np.kron(identity_gate, map_operator[channel])
map_operator[channel] = np.kron(I_GATE, map_operator[channel])

map_operator = map_operator[0] + map_operator[1] + map_operator[2]

Expand Down
17 changes: 10 additions & 7 deletions geqie/encodings/neqr/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,29 @@

from qiskit.quantum_info import Operator

identity_gate = np.array([[1, 0], [0, 1]])
x_gate = np.array([[0, 1], [1, 0]])
I_GATE = np.eye(2)
X_GATE = np.array([
[0, 1],
[1, 0]
])


def map(u: int, v: int, R: int, image: np.ndarray) -> Operator:
p = image[u, v]
# Convert value to string to the binary form, cut '0b', and padd with 0 example: '0001 1101':
# Convert value to binary string, without '0b' and padded with 0s, e.g.: '0001 1101':
pixel_value_as_binary_string = bin(p)[2:].zfill(8)
# Convert to logic array:
pixel_value_as_binary_array = [int(bit) for bit in pixel_value_as_binary_string][::-1]

if pixel_value_as_binary_array[0] == 1:
map_operator = x_gate
map_operator = X_GATE
else:
map_operator = identity_gate
map_operator = I_GATE

for bit in pixel_value_as_binary_array[1:8]:
if bit == 1:
map_operator = np.kron(x_gate, map_operator)
map_operator = np.kron(X_GATE, map_operator)
else:
map_operator = np.kron(identity_gate, map_operator)
map_operator = np.kron(I_GATE, map_operator)

return Operator(map_operator)
17 changes: 10 additions & 7 deletions geqie/encodings/qualpi/map.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,26 +2,29 @@

from qiskit.quantum_info import Operator

identity_gate = np.array([[1, 0], [0, 1]])
x_gate = np.array([[0, 1], [1, 0]])
I_GATE = np.eye(2)
X_GATE = np.array([
[0, 1],
[1, 0]
])


def map(rho: int, theta: int, R: int, image: np.ndarray) -> Operator:
p = image[rho, theta]
# Convert value to string to the binary form, cut '0b', and padd with 0 example: '0001 1101':
# Convert value to binary string, without '0b' and padded with 0s, e.g.: '0001 1101':
pixel_value_as_binary_string = bin(p)[2:].zfill(8)
# Convert to logic array:
pixel_value_as_binary_array = [int(bit) for bit in pixel_value_as_binary_string][::-1]

if pixel_value_as_binary_array[0] == 1:
map_operator = x_gate
map_operator = X_GATE
else:
map_operator = identity_gate
map_operator = I_GATE

for bit in pixel_value_as_binary_array[1:8]:
if bit == 1:
map_operator = np.kron(x_gate, map_operator)
map_operator = np.kron(X_GATE, map_operator)
else:
map_operator = np.kron(identity_gate, map_operator)
map_operator = np.kron(I_GATE, map_operator)

return Operator(map_operator)
5 changes: 2 additions & 3 deletions geqie/main.py
Original file line number Diff line number Diff line change
Expand Up @@ -55,7 +55,7 @@ def encode(

circuit = QuantumCircuit(n_qubits)
if not np.all(init_state.data == 1):
circuit.initialize(init_state, range(n_qubits-data_vectors[0].num_qubits-1, n_qubits), normalize=True)
circuit.initialize(init_state, range(n_qubits-data_vectors[0].num_qubits - 1, n_qubits), normalize=True)
circuit.append(U_op, range(n_qubits))
circuit.measure_all()

Expand All @@ -81,7 +81,6 @@ def simulate(

if return_padded_counts:
counts_padded = {f"{n:0{circuit.num_qubits}b}": 0 for n in range(2**circuit.num_qubits)}
counts_padded = {**counts_padded, **counts}
return counts_padded
return {**counts_padded, **counts}
else:
return counts
6 changes: 6 additions & 0 deletions requirements/requirements_dev.in
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
# install from optiq-geqie directory, e.g., $ pip install -r requirements\requirements_dev.in

-r requirements.in
-e .

pytest
13 changes: 13 additions & 0 deletions tests/encodings/test_frqi.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
import numpy as np
import subprocess
from PIL import Image, ImageOps

import geqie
from geqie.encodings import frqi

def test_frqi():
image = Image.open("assets/test_image_4x4.png")
image = ImageOps.grayscale(image)
image = np.asarray(image)
circuit = geqie.encode(frqi.init_function, frqi.data_function, frqi.map_function, image)
geqie.simulate(circuit, 1024)
30 changes: 30 additions & 0 deletions tests/test_cli.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import subprocess
import pytest

from dataclasses import dataclass

@dataclass
class CLIParameters:
image_path: str
grayscale: bool


METHOD_CLI_MAPPING = {
"frqi": CLIParameters(image_path="assets/test_image.png", grayscale=True),
"ifrqi": CLIParameters(image_path="assets/rgb.png", grayscale=True),
"neqr": CLIParameters(image_path="assets/test_image.png", grayscale=True),

"mcqi": CLIParameters(image_path="assets/rgb.png", grayscale=False),
"ncqi": CLIParameters(image_path="assets/rgb.png", grayscale=False),
"qualpi": CLIParameters(image_path="assets/test_flag_4x4.png", grayscale=False),
}


@pytest.mark.parametrize("method, params", METHOD_CLI_MAPPING.items())
def test_cli(method: str, params: CLIParameters):
subprocess.run([
"geqie", "simulate",
"--encoding", method,
"--image", params.image_path,
"--grayscale", str(params.grayscale)
], check=True)
Loading