diff --git a/.github/workflows/notebook-testing.yml b/.github/workflows/notebook-testing.yml index 56c105d32..cc7559618 100644 --- a/.github/workflows/notebook-testing.yml +++ b/.github/workflows/notebook-testing.yml @@ -25,6 +25,7 @@ jobs: run: | python -m pip install --upgrade pip pip install nbconvert nbclient ipykernel + pip install -e . - name: Run Jupyter Notebooks run: | diff --git a/docs/api.md b/docs/api.md index 82ae5a918..e97855fcd 100644 --- a/docs/api.md +++ b/docs/api.md @@ -72,4 +72,44 @@ - **Purpose**: Visualizes the quantum circuit. - **Usage**: Provides a graphical representation of the quantum circuit for better understanding. - **Note**: Just a pass through function, will use underlying libraries - method for drawing circuit. \ No newline at end of file + method for drawing circuit. + +## `apply_rx_gate(self, qubit_index, angle)` +- **Purpose**: Applies a rotation around the X-axis to a specified qubit with an optional parameter for optimization. +- **Parameters**: + - `qubit_index` (int): Index of the qubit. + - `angle` (str or float): Angle in radians for the rotation. Can be a static value or a parameter name for optimization. +- **Usage**: Used to rotate a qubit around the X-axis, often in parameterized quantum circuits for variational algorithms. + +## `apply_ry_gate(self, qubit_index, angle)` +- **Purpose**: Applies a rotation around the Y-axis to a specified qubit with an optional parameter for optimization. +- **Parameters**: + - `qubit_index` (int): Index of the qubit. + - `angle` (str or float): Angle in radians for the rotation. Can be a static value or a parameter name for optimization. +- **Usage**: Used to rotate a qubit around the Y-axis in parameterized circuits, aiding in the creation of complex quantum states. + +## `apply_rz_gate(self, qubit_index, angle)` +- **Purpose**: Applies a rotation around the Z-axis to a specified qubit with an optional parameter for optimization. +- **Parameters**: + - `qubit_index` (int): Index of the qubit. + - `angle` (str or float): Angle in radians for the rotation. Can be a static value or a parameter name for optimization. +- **Usage**: Utilized in parameterized quantum circuits to modify the phase of a qubit state during optimization. + +## `execute_circuit(self, parameter_values=None)` +- **Purpose**: Executes the quantum circuit with the ability to bind specific parameter values if provided. +- **Parameters**: + - `parameter_values` (dict, optional): A dictionary where keys are parameter names and values are the numerical values to bind. +- **Usage**: Enables the execution of parameterized circuits by binding parameter values, facilitating optimization processes. + +## `bind_parameters(self, parameter_values)` +- **Purpose**: Binds numerical values to the parameters of the quantum circuit, allowing for dynamic updates during optimization. +- **Parameters**: + - `parameter_values` (dict): A dictionary with parameter names as keys and numerical values to bind. +- **Usage**: Essential for optimization loops where parameters are adjusted based on cost function evaluations. + +## `_handle_parameter(self, param_name)` +- **Purpose**: Internal function to manage parameter registration. +- **Parameters**: + - `param_name` (str): The name of the parameter to handle. +- **Usage**: Automatically invoked when applying parameterized gates to keep track of parameters efficiently. + diff --git a/examples/Optimization_Example.ipynb b/examples/Optimization_Example.ipynb new file mode 100644 index 000000000..32cf3fa44 --- /dev/null +++ b/examples/Optimization_Example.ipynb @@ -0,0 +1,144 @@ +{ + "nbformat": 4, + "nbformat_minor": 2, + "cells": [ + { + "cell_type": "code", + "source": [ + "# pip install git+https://github.com/apache/mahout.git@main" + ], + "metadata": { + "colab": { + "base_uri": "https://localhost:8080/" + }, + "id": "lNDTZhztd2dp", + "outputId": "ea3b9e41-43a8-44e7-9daf-e62e71d93143" + }, + "execution_count": null, + "outputs": [] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "# Quantum Circuit Parameterization and Optimization\n", + "\n", + "In this notebook, we'll explore how to create parameterized quantum circuits using the QuMat framework. This feature allows us to bind values to parameters at execution time, paving the way for optimization tasks in quantum computing.\n", + "\n", + "## Overview\n", + "\n", + "1. **Parameter Handling**: We will use symbols to represent parameters in quantum gates, allowing these parameters to be updated during optimization.\n", + "2. **Circuit Execution with Binding**: We will bind parameter values to a circuit prior to its execution, a critical step in parameter optimization routines.\n", + "3. **Simple Optimization Loop**: We'll implement a basic optimization loop that updates parameters based on a cost function.\n" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 1: Setting Up\n", + "\n", + "We start by setting up the backend configuration and initializing the QuMat framework. This framework interfaces with quantum computing libraries like Qiskit or Cirq to manage the underlying quantum computations.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 1, + "metadata": {}, + "outputs": [], + "source": [ + "from qumat.qumat import QuMat\n", + "\n", + "# Configure the backend to use Qiskit with a simulator\n", + "backend_config = {\n", + " 'backend_name': 'qiskit',\n", + " 'backend_options': {'simulator_type': 'qasm_simulator', 'shots': 1024}\n", + "}\n", + "\n", + "# Create an instance of QuMat\n", + "qumat_instance = QuMat(backend_config)" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 2: Creating a Parameterized Quantum Circuit\n", + "\n", + "We create a simple quantum circuit with one qubit and apply parameterized RX, RY, and RZ gates. The parameters will be defined symbolically, allowing them to be replaced with actual values during execution.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 2, + "metadata": {}, + "outputs": [], + "source": [ + "# Create a quantum circuit with 1 qubit\n", + "qumat_instance.create_empty_circuit(1)\n", + "\n", + "# Apply parameterized RX, RY, and RZ gates\n", + "qumat_instance.apply_rx_gate(0, 'theta')\n", + "qumat_instance.apply_ry_gate(0, 'phi')\n", + "qumat_instance.apply_rz_gate(0, 'lambda')" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Step 3: Running the Optimization Loop\n", + "\n", + "We'll iterate over a simple optimization loop, updating the parameter values for each iteration. This example does not feature a sophisticated cost function, but in practical scenarios, you'd compute and leverage a cost function to guide these updates.\n" + ] + }, + { + "cell_type": "code", + "execution_count": 3, + "metadata": {}, + "outputs": [], + "source": [ + "# Example optimization loop\n", + "\n", + "# Initial parameter values\n", + "current_parameters = {'theta': 0, 'phi': 0, 'lambda': 0}\n", + "\n", + "# Define a simple placeholder cost function\n", + "def your_cost_function():\n", + " # Placeholder: replace with a real function in actual applications\n", + " return 0\n", + "\n", + "# Run the optimization loop\n", + "for iteration in range(10): # Reduced iteration count for brevity\n", + " cost = your_cost_function() # Evaluate the cost function\n", + "\n", + " # Update parameter(s) based on some optimization logic\n", + " # This is a placeholder update mechanism\n", + " current_parameters['theta'] += 0.1\n", + " current_parameters['phi'] += 0.1\n", + " current_parameters['lambda'] += 0.1\n", + "\n", + " # Execute the circuit with the updated parameters\n", + " result = qumat_instance.execute_circuit(parameter_values=current_parameters)\n", + "\n", + " print(f\"Iteration {iteration}, Result: {result}, Parameters: {current_parameters}\")" + ] + }, + { + "cell_type": "markdown", + "metadata": {}, + "source": [ + "## Conclusion\n", + "\n", + "This notebook demonstrates how to effectively handle parameters within quantum circuits using the QuMat framework. Although the optimization loop here uses a placeholder mechanism, it highlights how parameterized circuits can be used in iterative optimization processes, often encountered in variational quantum algorithms.\n" + ] + } + ], + "metadata": { + "kernelspec": { + "display_name": "Python 3", + "language": "python", + "name": "python3" + } + } +} diff --git a/examples/Simple_Example.ipynb b/examples/Simple_Example.ipynb index 7905ca11d..7e04c82f0 100644 --- a/examples/Simple_Example.ipynb +++ b/examples/Simple_Example.ipynb @@ -29,7 +29,7 @@ { "cell_type": "code", "source": [ - "!pip install git+https://github.com/apache/mahout.git@main" + "# pip install git+https://github.com/apache/mahout.git@main" ], "metadata": { "colab": { diff --git a/pyproject.toml b/pyproject.toml index 20f24b2eb..fba0eca67 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -12,6 +12,7 @@ qiskit = "^0.45.1" qiskit-aer = "^0.13.2" cirq = "^1.3.0" amazon-braket-sdk = "^1.10.0" +sympy = "^1.9" [tool.poetry.dev-dependencies] pytest = "^8.1.1" diff --git a/qumat/amazon_braket_backend.py b/qumat/amazon_braket_backend.py index 9422dda8b..efa533b69 100644 --- a/qumat/amazon_braket_backend.py +++ b/qumat/amazon_braket_backend.py @@ -15,7 +15,7 @@ # limitations under the License. # from braket.aws import AwsQuantumSimulator, AwsDevice -from braket.circuits import Circuit +from braket.circuits import Circuit, FreeParameter def initialize_backend(backend_config): backend_options = backend_config['backend_options'] @@ -70,13 +70,27 @@ def draw_circuit(circuit): print(circuit) def apply_rx_gate(circuit, qubit_index, angle): - circuit.rx(qubit_index, angle) + if isinstance(angle, (int, float)): + circuit.rx(qubit_index, angle) + else: + param = FreeParameter(angle) + circuit.rx(qubit_index, param) + def apply_ry_gate(circuit, qubit_index, angle): - circuit.ry(qubit_index, angle) + if isinstance(angle, (int, float)): + circuit.ry(qubit_index, angle) + else: + param = FreeParameter(angle) + circuit.ry(qubit_index, param) + def apply_rz_gate(circuit, qubit_index, angle): - circuit.rz(qubit_index, angle) + if isinstance(angle, (int, float)): + circuit.rz(qubit_index, angle) + else: + param = FreeParameter(angle) + circuit.rz(qubit_index, param) def apply_u_gate(circuit, qubit_index, theta, phi, lambd): circuit.rx(qubit_index, theta) diff --git a/qumat/cirq_backend.py b/qumat/cirq_backend.py index e27ab0701..245edf445 100644 --- a/qumat/cirq_backend.py +++ b/qumat/cirq_backend.py @@ -15,6 +15,7 @@ # limitations under the License. # import cirq +import sympy def initialize_backend(backend_config): # Assuming 'simulator_type' specifies the type of simulator in Cirq @@ -64,27 +65,41 @@ def apply_pauli_z_gate(circuit, qubit_index): qubit = cirq.LineQubit(qubit_index) circuit.append(cirq.Z(qubit)) + def execute_circuit(circuit, backend, backend_config): - # This is a simplified example. You'll need to adjust this based on how you're handling backend configuration. - circuit.append(cirq.measure(*circuit.all_qubits(), key='result')) + # Ensure measurement is added to capture the results + if not circuit.has_measurements(): + circuit.append(cirq.measure(*circuit.all_qubits(), key='result')) simulator = cirq.Simulator() - result = simulator.run(circuit, repetitions=backend_config['backend_options'].get('shots', 1)) - return result.histogram(key='result') + parameter_values = backend_config.get('parameter_values', None) + if parameter_values: + # Convert parameter_values to applicable resolvers + res = [cirq.ParamResolver(parameter_values)] + results = simulator.run_sweep(circuit, repetitions=backend_config['backend_options'].get('shots', 1), params=res) + return [result.histogram(key='result') for result in results] + else: + result = simulator.run(circuit, repetitions=backend_config['backend_options'].get('shots', 1)) + return [result.histogram(key='result')] def draw_circuit(circuit): print(circuit) def apply_rx_gate(circuit, qubit_index, angle): + param = sympy.Symbol(angle) if isinstance(angle, str) else angle qubit = cirq.LineQubit(qubit_index) - circuit.append(cirq.rx(angle).on(qubit)) + circuit.append(cirq.rx(param).on(qubit)) + def apply_ry_gate(circuit, qubit_index, angle): + param = sympy.Symbol(angle) if isinstance(angle, str) else angle qubit = cirq.LineQubit(qubit_index) - circuit.append(cirq.ry(angle).on(qubit)) + circuit.append(cirq.ry(param).on(qubit)) + def apply_rz_gate(circuit, qubit_index, angle): + param = sympy.Symbol(angle) if isinstance(angle, str) else angle qubit = cirq.LineQubit(qubit_index) - circuit.append(cirq.rz(angle).on(qubit)) + circuit.append(cirq.rz(param).on(qubit)) def apply_u_gate(circuit, qubit_index, theta, phi, lambd): qubit = cirq.LineQubit(qubit_index) diff --git a/qumat/qiskit_backend.py b/qumat/qiskit_backend.py index ca3b0ae62..b9eeaa14a 100644 --- a/qumat/qiskit_backend.py +++ b/qumat/qiskit_backend.py @@ -67,15 +67,24 @@ def apply_pauli_z_gate(circuit, qubit_index): circuit.z(qubit_index) def execute_circuit(circuit, backend, backend_config): - # Transpile the circuit - circuit.measure_all() - transpiled_circuit = qiskit.compiler.transpile(circuit, backend) - - # Execute the transpiled circuit on the backend - job = qiskit.execute(transpiled_circuit, backend, - shots=backend_config['backend_options']['shots']) - result = job.result() - return result.get_counts(transpiled_circuit) + # Add measurements if they are not already present + if not circuit.cregs: + circuit.measure_all() + + # Ensure the circuit is parameterized properly + if circuit.parameters: + # Parse the global parameter configuration + parameter_bindings = {param: backend_config['parameter_values'][str(param)] for param in circuit.parameters} + transpiled_circuit = qiskit.transpile(circuit, backend) + qobj = qiskit.assemble(transpiled_circuit, parameter_binds=[parameter_bindings], shots=backend_config['backend_options']['shots']) + job = backend.run(qobj) + result = job.result() + return result.get_counts() + else: + transpiled_circuit = qiskit.transpile(circuit, backend) + job = qiskit.execute(transpiled_circuit, backend, shots=backend_config['backend_options']['shots']) + result = job.result() + return result.get_counts() # placeholder method for use in the testing suite def get_final_state_vector(circuit, backend, backend_config): @@ -92,13 +101,16 @@ def draw_circuit(circuit): print(circuit.draw()) def apply_rx_gate(circuit, qubit_index, angle): - circuit.rx(angle, qubit_index) + param = qiskit.circuit.Parameter(angle) if isinstance(angle, str) else angle + circuit.rx(param, qubit_index) def apply_ry_gate(circuit, qubit_index, angle): - circuit.ry(angle, qubit_index) + param = qiskit.circuit.Parameter(angle) if isinstance(angle, str) else angle + circuit.ry(param, qubit_index) def apply_rz_gate(circuit, qubit_index, angle): - circuit.rz(angle, qubit_index) + param = qiskit.circuit.Parameter(angle) if isinstance(angle, str) else angle + circuit.rz(param, qubit_index) def apply_u_gate(circuit, qubit_index, theta, phi, lambd): # Apply the U gate directly with specified parameters diff --git a/qumat/qumat.py b/qumat/qumat.py index 97101a4a4..db0dc1952 100644 --- a/qumat/qumat.py +++ b/qumat/qumat.py @@ -20,10 +20,10 @@ class QuMat: def __init__(self, backend_config): self.backend_config = backend_config self.backend_name = backend_config['backend_name'] - # Dynamically load the backend module based on the user's choice self.backend_module = import_module(f".{self.backend_name}_backend", package="qumat") self.backend = self.backend_module.initialize_backend(backend_config) self.circuit = None + self.parameters = {} def create_empty_circuit(self, num_qubits): self.circuit = self.backend_module.create_empty_circuit(num_qubits) @@ -52,9 +52,17 @@ def apply_pauli_y_gate(self, qubit_index): def apply_pauli_z_gate(self, qubit_index): self.backend_module.apply_pauli_z_gate(self.circuit, qubit_index) - def execute_circuit(self): + def execute_circuit(self, parameter_values=None): + if parameter_values: + self.bind_parameters(parameter_values) + self.backend_config['parameter_values'] = self.parameters # Pass parameters return self.backend_module.execute_circuit(self.circuit, self.backend, self.backend_config) + def bind_parameters(self, parameter_values): + for param, value in parameter_values.items(): + if param in self.parameters: + self.parameters[param] = value + # placeholder method for use in the testing suite def get_final_state_vector(self): return self.backend_module.get_final_state_vector(self.circuit, self.backend, self.backend_config) @@ -63,13 +71,20 @@ def draw(self): return self.backend_module.draw_circuit(self.circuit) def apply_rx_gate(self, qubit_index, angle): + self._handle_parameter(angle) self.backend_module.apply_rx_gate(self.circuit, qubit_index, angle) def apply_ry_gate(self, qubit_index, angle): + self._handle_parameter(angle) self.backend_module.apply_ry_gate(self.circuit, qubit_index, angle) def apply_rz_gate(self, qubit_index, angle): + self._handle_parameter(angle) self.backend_module.apply_rz_gate(self.circuit, qubit_index, angle) + def _handle_parameter(self, param_name): + if isinstance(param_name, str) and param_name not in self.parameters: + self.parameters[param_name] = None + def apply_u_gate(self, qubit_index, theta, phi, lambd): self.backend_module.apply_u_gate(self.circuit, qubit_index, theta, phi, lambd)