-
Notifications
You must be signed in to change notification settings - Fork 40
fix: classical shadow with program set #316
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
base: main
Are you sure you want to change the base?
Changes from all commits
74081e1
73f236a
7e1b247
001d4da
7cbf5f2
ff93291
f824855
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -48,6 +48,7 @@ | |
| from pennylane.exceptions import QuantumFunctionError | ||
| from pennylane.gradients import param_shift | ||
| from pennylane.measurements import ( | ||
| ClassicalShadowMP, | ||
| CountsMP, | ||
| ExpectationMP, | ||
| MeasurementProcess, | ||
|
|
@@ -172,6 +173,11 @@ def __init__( | |
| DeviceActionType.OPENQASM_PROGRAM_SET in self._device.properties.action | ||
| and self._shots is not None | ||
| ) | ||
| self._max_program_set_executables = ( | ||
| self._device.properties.action["braket.ir.openqasm.program_set"].maximumExecutables | ||
| if self._supports_program_sets | ||
| else None | ||
| ) | ||
|
|
||
| if noise_model: | ||
| self._validate_noise_model_support() | ||
|
|
@@ -206,20 +212,25 @@ def parallel(self) -> bool: | |
| return self._parallel | ||
|
|
||
| def batch_execute(self, circuits, **run_kwargs): | ||
| if not self._parallel and not self._supports_program_sets: | ||
| return super().batch_execute(circuits) | ||
|
|
||
| if self._supports_program_sets and ( | ||
| len(circuits) | ||
| > self._device.properties.action["braket.ir.openqasm.program_set"].maximumExecutables | ||
| ): | ||
| if not self._parallel and not self._can_run_as_program_set(len(circuits)): | ||
| return super().batch_execute(circuits) | ||
|
|
||
| for circuit in circuits: | ||
| self.check_validity(circuit.operations, circuit.observables) | ||
| all_trainable = [] | ||
| braket_circuits = [] | ||
| for circuit in circuits: | ||
| if isinstance(circuit.observables[0], ClassicalShadowMP): | ||
| if len(circuit.observables) > 1: | ||
| raise ValueError( | ||
| "A circuit with a ClassicalShadowMP observable must " | ||
| "have that as its only return type." | ||
| ) | ||
| if len(circuits) > 1: | ||
| raise ValueError("Classical shadow must be called with a single circuit.") | ||
| bits, recipes = self.classical_shadow(circuit.observables[0], circuit) | ||
| return [(bits, recipes)] | ||
|
|
||
| trainable = ( | ||
| BraketQubitDevice._get_trainable_parameters(circuit) | ||
| if self._parametrize_differentiable | ||
|
|
@@ -230,7 +241,7 @@ def batch_execute(self, circuits, **run_kwargs): | |
| self._pl_to_braket_circuit( | ||
| circuit, | ||
| trainable_indices=frozenset(trainable.keys()), | ||
| add_observables=not self._supports_program_sets, | ||
| add_observables=not self._can_run_as_program_set(len(circuits)), | ||
| **run_kwargs, | ||
| ) | ||
| ) | ||
|
|
@@ -245,6 +256,10 @@ def batch_execute(self, circuits, **run_kwargs): | |
|
|
||
| return self._run_task_batch(braket_circuits, circuits, batch_shots, batch_inputs) | ||
|
|
||
| def _can_run_as_program_set(self, n_executables: int) -> bool: | ||
| """Whether a list of executables can be run as a program set""" | ||
| return self._supports_program_sets and n_executables < self._max_program_set_executables | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. why is this additional condition necessary now?
Member
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This just allows us to reuse the logic that used to be on line 212 |
||
|
|
||
| def _pl_to_braket_circuit( | ||
| self, | ||
| circuit: QuantumTape, | ||
|
|
@@ -510,7 +525,7 @@ def execute(self, circuit: QuantumTape, compute_gradient=False, **run_kwargs) -> | |
| if len(circuit.observables) > 1: | ||
| raise ValueError( | ||
| "A circuit with a ShadowExpvalMP observable must " | ||
| "have that as its only result type." | ||
| "have that as its only return type." | ||
| ) | ||
| return [self.shadow_expval(circuit.observables[0], circuit)] | ||
| raise RuntimeError("The circuit has an unsupported MeasurementTransform.") | ||
|
|
@@ -707,7 +722,7 @@ def use_grouping(self) -> bool: | |
| return not (caps.get("provides_jacobian")) | ||
|
|
||
| def _run_task_batch(self, braket_circuits, pl_circuits, batch_shots: int, inputs): | ||
| if self._supports_program_sets: | ||
| if self._can_run_as_program_set(len(braket_circuits)): | ||
| program_set = ( | ||
| ProgramSet.zip(braket_circuits, input_sets=inputs) | ||
| if inputs | ||
|
|
@@ -764,7 +779,7 @@ def _run_task(self, circuit, inputs=None): | |
| def _run_snapshots(self, snapshot_circuits, n_qubits, mapped_wires): | ||
| n_snapshots = len(snapshot_circuits) | ||
| outcomes = np.zeros((n_snapshots, n_qubits)) | ||
| if self._supports_program_sets: | ||
| if self._can_run_as_program_set(n_snapshots): | ||
| program_set = ProgramSet(snapshot_circuits) | ||
| task = self._device.run( | ||
| program_set, | ||
|
|
||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -1125,8 +1125,8 @@ def test_batch_execute_program_set_noncommuting(): | |
| dev.batch_execute(circuits) | ||
|
|
||
|
|
||
| @patch.object(AwsDevice, "run") | ||
| def test_batch_execute_program_set_exceeds_max_executables(mock_run): | ||
| @patch.object(AwsDevice, "run_batch") | ||
|
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. was this previously a bug? |
||
| def test_batch_execute_program_set_exceeds_max_executables(mock_run_batch): | ||
| """Test batch_execute falls back to individual programs when exceeding maximumExecutables""" | ||
| custom_result = GateModelQuantumTaskResult.from_string( | ||
| json.dumps( | ||
|
|
@@ -1174,7 +1174,12 @@ def test_batch_execute_program_set_exceeds_max_executables(mock_run): | |
| task.result.return_value = custom_result | ||
| type(task).id = PropertyMock(return_value="task_arn") | ||
| task.state.return_value = "COMPLETED" | ||
| mock_run.return_value = task | ||
|
|
||
| # Mock the batch to return 101 results (one for each circuit) | ||
| task_batch = Mock() | ||
| task_batch.results.return_value = [custom_result] * 101 | ||
| type(task_batch).tasks = PropertyMock(return_value=[task] * 101) | ||
| mock_run_batch.return_value = task_batch | ||
|
|
||
| dev = _aws_device(wires=4, foo="bar", parallel=True, supports_program_sets=True) | ||
|
|
||
|
|
@@ -1199,7 +1204,9 @@ def test_batch_execute_program_set_exceeds_max_executables(mock_run): | |
| ) | ||
|
|
||
| result = dev.batch_execute(circuits) | ||
| assert mock_run.call_count == 101 | ||
| assert mock_run_batch.call_count == 1 | ||
| call_args = mock_run_batch.call_args[0] | ||
| assert len(call_args[0]) == 101 # First argument is the list of circuits | ||
| assert len(result) == 101 | ||
|
|
||
|
|
||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This should be first condition in the loop to ensure early return