This is an automated email from the ASF dual-hosted git repository.
guanmingchiu pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/mahout.git
The following commit(s) were added to refs/heads/main by this push:
new 858af4a3b MAHOUT-613: execute_circuit() error in Amazon Braket backend
(#614)
858af4a3b is described below
commit 858af4a3b524893794be76e823442fd8bd693372
Author: GUAN-HAO HUANG <[email protected]>
AuthorDate: Fri Nov 14 17:50:09 2025 +0800
MAHOUT-613: execute_circuit() error in Amazon Braket backend (#614)
* execute_circuit() error in Amazon Braket backend
* test for parameter binding
* add Error message in execute_circuit()
* add test for all backend
* fix lint error
---
qumat/amazon_braket_backend.py | 12 ++-
qumat/qumat.py | 20 +++-
testing/test_parameter_binding.py | 206 ++++++++++++++++++++++++++++++++++++++
3 files changed, 236 insertions(+), 2 deletions(-)
diff --git a/qumat/amazon_braket_backend.py b/qumat/amazon_braket_backend.py
index dae8478d9..377c756ea 100644
--- a/qumat/amazon_braket_backend.py
+++ b/qumat/amazon_braket_backend.py
@@ -83,7 +83,17 @@ def apply_pauli_z_gate(circuit, qubit_index):
def execute_circuit(circuit, backend, backend_config):
shots = backend_config["backend_options"].get("shots", 1)
- task = backend.run(circuit, shots=shots)
+ parameter_values = backend_config.get("parameter_values", {})
+ if parameter_values and circuit.parameters:
+ # Braket accepts parameter names as strings in inputs dict
+ inputs = {
+ param_name: value
+ for param_name, value in parameter_values.items()
+ if param_name in {p.name for p in circuit.parameters}
+ }
+ task = backend.run(circuit, shots=shots, inputs=inputs)
+ else:
+ task = backend.run(circuit, shots=shots)
result = task.result()
return result.measurement_counts
diff --git a/qumat/qumat.py b/qumat/qumat.py
index 31c5adb49..a7b8390fc 100644
--- a/qumat/qumat.py
+++ b/qumat/qumat.py
@@ -276,7 +276,25 @@ class QuMat:
if parameter_values:
self.bind_parameters(parameter_values)
- self.backend_config["parameter_values"] = self.parameters # Pass
parameters
+
+ # Only pass bound parameters (non-None values) to backend
+ bound_parameters = {
+ param: value
+ for param, value in self.parameters.items()
+ if value is not None
+ }
+
+ # Check if there are unbound parameters in the circuit
+ if self.parameters and not bound_parameters:
+ unbound_params = [
+ p for p in self.parameters.keys() if self.parameters[p] is None
+ ]
+ raise ValueError(
+ f"Circuit contains unbound parameters: {unbound_params}. "
+ f"Please provide parameter_values when executing the circuit."
+ )
+
+ self.backend_config["parameter_values"] = bound_parameters
return self.backend_module.execute_circuit(
self.circuit, self.backend, self.backend_config
)
diff --git a/testing/test_parameter_binding.py
b/testing/test_parameter_binding.py
new file mode 100644
index 000000000..1187d9d20
--- /dev/null
+++ b/testing/test_parameter_binding.py
@@ -0,0 +1,206 @@
+#
+# Licensed to the Apache Software Foundation (ASF) under one or more
+# contributor license agreements. See the NOTICE file distributed with
+# this work for additional information regarding copyright ownership.
+# The ASF licenses this file to You under the Apache License, Version 2.0
+# (the "License"); you may not use this file except in compliance with
+# the License. You may obtain a copy of the License at
+#
+# http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+#
+
+import math
+
+import pytest
+
+from qumat import QuMat
+
+from .utils import TESTING_BACKENDS, get_backend_config
+
+
+def get_state_probability(results, target_state, num_qubits=1):
+ """Calculate the probability of measuring a target state."""
+ if isinstance(results, list):
+ results = results[0]
+
+ total_shots = sum(results.values())
+ if total_shots == 0:
+ return 0.0
+
+ if isinstance(target_state, str):
+ target_str = target_state
+ target_int = int(target_state, 2) if target_state else 0
+ else:
+ target_int = target_state
+ target_str = format(target_state, f"0{num_qubits}b")
+
+ target_count = 0
+ for state, count in results.items():
+ if isinstance(state, str):
+ if state == target_str:
+ target_count = count
+ break
+ else:
+ if state == target_int:
+ target_count = count
+ break
+
+ return target_count / total_shots
+
+
+class TestParameterBinding:
+ """Regression tests for parameter binding functionality across all
backends.
+
+ These tests ensure that parameter binding support in all backends
+ (Qiskit, Cirq, Amazon Braket) is not accidentally removed or broken.
+ """
+
+ @pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
+ def test_rx_gate_parameter_binding(self, backend_name):
+ """Test RX gate parameter binding across all backends."""
+ backend_config = get_backend_config(backend_name)
+ qumat = QuMat(backend_config)
+ qumat.create_empty_circuit(num_qubits=1)
+
+ # Apply parameterized RX gate
+ qumat.apply_rx_gate(0, "theta")
+
+ # Execute with parameter binding
+ results = qumat.execute_circuit(parameter_values={"theta": math.pi})
+
+ # RX(π) should flip |0⟩ to |1⟩
+ prob = get_state_probability(results, "1", num_qubits=1)
+ assert prob > 0.95, (
+ f"Expected |1⟩ after RX(π) with parameter binding in
{backend_name}, "
+ f"got probability {prob:.4f}"
+ )
+
+ @pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
+ def test_ry_gate_parameter_binding(self, backend_name):
+ """Test RY gate parameter binding across all backends."""
+ backend_config = get_backend_config(backend_name)
+ qumat = QuMat(backend_config)
+ qumat.create_empty_circuit(num_qubits=1)
+
+ # Apply parameterized RY gate
+ qumat.apply_ry_gate(0, "phi")
+
+ # Execute with parameter binding
+ results = qumat.execute_circuit(parameter_values={"phi": math.pi / 2})
+
+ # RY(π/2) creates superposition
+ # Handle both string and integer state formats (Cirq uses integers)
+ if isinstance(results, list):
+ results = results[0]
+
+ total_shots = sum(results.values())
+ zero_count = 0
+ for state, count in results.items():
+ if isinstance(state, str):
+ if state == "0":
+ zero_count = count
+ break
+ else:
+ if state == 0:
+ zero_count = count
+ break
+
+ prob_zero = zero_count / total_shots if total_shots > 0 else 0.0
+
+ assert 0.45 < prob_zero < 0.55, (
+ f"Expected ~0.5 probability for |0⟩ after RY(π/2) in
{backend_name}, "
+ f"got {prob_zero:.4f}"
+ )
+
+ @pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
+ def test_rz_gate_parameter_binding(self, backend_name):
+ """Test RZ gate parameter binding across all backends."""
+ backend_config = get_backend_config(backend_name)
+ qumat = QuMat(backend_config)
+ qumat.create_empty_circuit(num_qubits=1)
+
+ # Apply parameterized RZ gate
+ qumat.apply_rz_gate(0, "lambda")
+
+ # Execute with parameter binding
+ results = qumat.execute_circuit(parameter_values={"lambda": math.pi})
+
+ # RZ(π) doesn't change |0⟩ measurement probability
+ prob = get_state_probability(results, "0", num_qubits=1)
+ assert prob > 0.95, (
+ f"Expected |0⟩ after RZ(π) with parameter binding in
{backend_name}, "
+ f"got probability {prob:.4f}"
+ )
+
+ @pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
+ def test_multiple_parameter_binding(self, backend_name):
+ """Test binding multiple parameters across all backends."""
+ backend_config = get_backend_config(backend_name)
+ qumat = QuMat(backend_config)
+ qumat.create_empty_circuit(num_qubits=2)
+
+ # Apply different parameterized gates
+ qumat.apply_rx_gate(0, "theta0")
+ qumat.apply_ry_gate(1, "phi1")
+
+ # Execute with multiple parameter bindings
+ results = qumat.execute_circuit(
+ parameter_values={"theta0": math.pi, "phi1": math.pi / 2}
+ )
+
+ # Qubit 0 should be |1⟩ (RX(π) = X)
+ # Check that we get states with qubit 0 = |1⟩
+ # Handle backend-specific result formats
+ if isinstance(results, list):
+ results = results[0]
+
+ total_shots = sum(results.values())
+ target_count = 0
+
+ for state, count in results.items():
+ if isinstance(state, str):
+ # Qiskit: little-endian (rightmost bit is qubit 0)
+ # Amazon Braket: big-endian (leftmost bit is qubit 0)
+ if backend_name == "qiskit":
+ # For Qiskit, qubit 0 is rightmost, so check last character
+ if len(state) > 0 and state[-1] == "1":
+ target_count += count
+ else:
+ # For Amazon Braket, qubit 0 is leftmost, so check first
character
+ if len(state) > 0 and state[0] == "1":
+ target_count += count
+ else:
+ # Cirq: integer format, big-endian
+ # Qubit i is at bit position (num_qubits - 1 - i)
+ # For qubit 0 with 2 qubits: bit_position = 2 - 1 - 0 = 1
+ num_qubits = 2
+ bit_position = num_qubits - 1 - 0
+ if ((state >> bit_position) & 1) == 1:
+ target_count += count
+
+ prob = target_count / total_shots if total_shots > 0 else 0.0
+
+ assert prob > 0.4, (
+ f"Expected high probability for states with qubit 0=|1⟩ in
{backend_name}, "
+ f"got {prob:.4f}"
+ )
+
+ @pytest.mark.parametrize("backend_name", TESTING_BACKENDS)
+ def test_unbound_parameter_error(self, backend_name):
+ """Test that unbound parameters raise clear error message across all
backends."""
+ backend_config = get_backend_config(backend_name)
+ qumat = QuMat(backend_config)
+ qumat.create_empty_circuit(num_qubits=1)
+
+ # Apply parameterized gate but don't bind
+ qumat.apply_rx_gate(0, "theta")
+
+ # Should raise ValueError with clear message
+ with pytest.raises(ValueError, match="unbound parameters"):
+ qumat.execute_circuit()