diff --git a/cirq-core/cirq/__init__.py b/cirq-core/cirq/__init__.py index cee317963d3..350ab8c5837 100644 --- a/cirq-core/cirq/__init__.py +++ b/cirq-core/cirq/__init__.py @@ -429,6 +429,7 @@ chosen_angle_to_half_turns, Duration, DURATION_LIKE, + GenericMetaImplementAnyOneOf, LinearDict, MEASUREMENT_KEY_SEPARATOR, MeasurementKey, diff --git a/cirq-core/cirq/protocols/json_test_data/spec.py b/cirq-core/cirq/protocols/json_test_data/spec.py index f2e557e141e..ef407752558 100644 --- a/cirq-core/cirq/protocols/json_test_data/spec.py +++ b/cirq-core/cirq/protocols/json_test_data/spec.py @@ -119,6 +119,10 @@ 'ThreeQubitGate', 'TwoQubitGate', 'ABCMetaImplementAnyOneOf', + 'GenericMetaImplementAnyOneOf', + 'SimulatesAmplitudes', + 'SimulatesExpectationValues', + 'SimulatesFinalState', # protocols: 'SupportsActOn', 'SupportsApplyChannel', diff --git a/cirq-core/cirq/sim/simulator.py b/cirq-core/cirq/sim/simulator.py index dcf65716775..f3152a8c939 100644 --- a/cirq-core/cirq/sim/simulator.py +++ b/cirq-core/cirq/sim/simulator.py @@ -73,6 +73,14 @@ def run_sweep( params: study.Sweepable, repetitions: int = 1, ) -> List[study.Result]: + return list(self.run_sweep_iter(program, params, repetitions)) + + def run_sweep_iter( + self, + program: 'cirq.Circuit', + params: study.Sweepable, + repetitions: int = 1, + ) -> Iterator[study.Result]: """Runs the supplied Circuit, mimicking quantum hardware. In contrast to run, this allows for sweeping over different parameter @@ -92,7 +100,6 @@ def run_sweep( _verify_unique_measurement_keys(program) - trial_results = [] # type: List[study.Result] for param_resolver in study.to_resolvers(params): measurements = {} if repetitions == 0: @@ -102,12 +109,9 @@ def run_sweep( measurements = self._run( circuit=program, param_resolver=param_resolver, repetitions=repetitions ) - trial_results.append( - study.Result.from_single_parameter_set( - params=param_resolver, measurements=measurements - ) + yield study.Result.from_single_parameter_set( + params=param_resolver, measurements=measurements ) - return trial_results @abc.abstractmethod def _run( @@ -131,13 +135,13 @@ def _run( raise NotImplementedError() -class SimulatesAmplitudes(metaclass=abc.ABCMeta): +class SimulatesAmplitudes(metaclass=value.ABCMetaImplementAnyOneOf): """Simulator that computes final amplitudes of given bitstrings. Given a circuit and a list of bitstrings, computes the amplitudes of the given bitstrings in the state obtained by applying the circuit to the all zeros state. Implementors of this interface should implement - the compute_amplitudes_sweep method. + the compute_amplitudes_sweep_iter method. """ def compute_amplitudes( @@ -169,7 +173,6 @@ def compute_amplitudes( program, bitstrings, study.ParamResolver(param_resolver), qubit_order )[0] - @abc.abstractmethod def compute_amplitudes_sweep( self, program: 'cirq.Circuit', @@ -177,6 +180,35 @@ def compute_amplitudes_sweep( params: study.Sweepable, qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, ) -> Sequence[Sequence[complex]]: + """Wraps computed amplitudes in a list. + + Prefer overriding `compute_amplitudes_sweep_iter`. + """ + return list(self.compute_amplitudes_sweep_iter(program, bitstrings, params, qubit_order)) + + def _compute_amplitudes_sweep_to_iter( + self, + program: 'cirq.Circuit', + bitstrings: Sequence[int], + params: study.Sweepable, + qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, + ) -> Iterator[Sequence[complex]]: + if type(self).compute_amplitudes_sweep == SimulatesAmplitudes.compute_amplitudes_sweep: + raise RecursionError( + "Must define either compute_amplitudes_sweep or compute_amplitudes_sweep_iter." + ) + yield from self.compute_amplitudes_sweep(program, bitstrings, params, qubit_order) + + @value.alternative( + requires='compute_amplitudes_sweep', implementation=_compute_amplitudes_sweep_to_iter + ) + def compute_amplitudes_sweep_iter( + self, + program: 'cirq.Circuit', + bitstrings: Sequence[int], + params: study.Sweepable, + qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, + ) -> Iterator[Sequence[complex]]: """Computes the desired amplitudes. The initial state is assumed to be the all zeros state. @@ -193,20 +225,20 @@ def compute_amplitudes_sweep( ordering of the computational basis states. Returns: - List of lists of amplitudes. The outer dimension indexes the - circuit parameters and the inner dimension indexes the bitstrings. + An Iterator over lists of amplitudes. The outer dimension indexes + the circuit parameters and the inner dimension indexes bitstrings. """ raise NotImplementedError() -class SimulatesExpectationValues(metaclass=abc.ABCMeta): +class SimulatesExpectationValues(metaclass=value.ABCMetaImplementAnyOneOf): """Simulator that computes exact expectation values of observables. Given a circuit and an observable map, computes exact (to float precision) expectation values for each observable at the end of the circuit. Implementors of this interface should implement the - simulate_expectation_values_sweep method. + simulate_expectation_values_sweep_iter method. """ def simulate_expectation_values( @@ -257,7 +289,6 @@ def simulate_expectation_values( permit_terminal_measurements, )[0] - @abc.abstractmethod def simulate_expectation_values_sweep( self, program: 'cirq.Circuit', @@ -267,6 +298,60 @@ def simulate_expectation_values_sweep( initial_state: Any = None, permit_terminal_measurements: bool = False, ) -> List[List[float]]: + """Wraps computed expectation values in a list. + + Prefer overriding `simulate_expectation_values_sweep_iter`. + """ + return list( + self.simulate_expectation_values_sweep_iter( + program, + observables, + params, + qubit_order, + initial_state, + permit_terminal_measurements, + ) + ) + + def _simulate_expectation_values_sweep_to_iter( + self, + program: 'cirq.Circuit', + observables: Union['cirq.PauliSumLike', List['cirq.PauliSumLike']], + params: 'study.Sweepable', + qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, + initial_state: Any = None, + permit_terminal_measurements: bool = False, + ) -> Iterator[List[float]]: + if ( + type(self).simulate_expectation_values_sweep + == SimulatesExpectationValues.simulate_expectation_values_sweep + ): + raise RecursionError( + "Must define either simulate_expectation_values_sweep or " + "simulate_expectation_values_sweep_iter." + ) + yield from self.simulate_expectation_values_sweep( + program, + observables, + params, + qubit_order, + initial_state, + permit_terminal_measurements, + ) + + @value.alternative( + requires='simulate_expectation_values_sweep', + implementation=_simulate_expectation_values_sweep_to_iter, + ) + def simulate_expectation_values_sweep_iter( + self, + program: 'cirq.Circuit', + observables: Union['cirq.PauliSumLike', List['cirq.PauliSumLike']], + params: 'study.Sweepable', + qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, + initial_state: Any = None, + permit_terminal_measurements: bool = False, + ) -> Iterator[List[float]]: """Simulates the supplied circuit and calculates exact expectation values for the given observables on its final state, sweeping over the given params. @@ -291,10 +376,10 @@ def simulate_expectation_values_sweep( ruining expectation value calculations. Returns: - A list of expectation-value lists. The outer index determines the - sweep, and the inner index determines the observable. For instance, - results[1][3] would select the fourth observable measured in the - second sweep. + An Iterator over expectation-value lists. The outer index determines + the sweep, and the inner index determines the observable. For + instance, results[1][3] would select the fourth observable measured + in the second sweep. Raises: ValueError if 'program' has terminal measurement(s) and @@ -302,10 +387,12 @@ def simulate_expectation_values_sweep( """ -class SimulatesFinalState(Generic[TSimulationTrialResult], metaclass=abc.ABCMeta): +class SimulatesFinalState( + Generic[TSimulationTrialResult], metaclass=value.GenericMetaImplementAnyOneOf +): """Simulator that allows access to the simulator's final state. - Implementors of this interface should implement the simulate_sweep + Implementors of this interface should implement the simulate_sweep_iter method. This simulator only returns the state of the quantum system for the final step of a simulation. This simulator state may be a state vector, the density matrix, or another representation, depending on the @@ -342,7 +429,6 @@ def simulate( program, study.ParamResolver(param_resolver), qubit_order, initial_state )[0] - @abc.abstractmethod def simulate_sweep( self, program: 'cirq.Circuit', @@ -350,6 +436,31 @@ def simulate_sweep( qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, initial_state: Any = None, ) -> List[TSimulationTrialResult]: + """Wraps computed states in a list. + + Prefer overriding `simulate_sweep_iter`. + """ + return list(self.simulate_sweep_iter(program, params, qubit_order, initial_state)) + + def _simulate_sweep_to_iter( + self, + program: 'cirq.Circuit', + params: study.Sweepable, + qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, + initial_state: Any = None, + ) -> Iterator[TSimulationTrialResult]: + if type(self).simulate_sweep == SimulatesFinalState.simulate_sweep: + raise RecursionError("Must define either simulate_sweep or simulate_sweep_iter.") + yield from self.simulate_sweep(program, params, qubit_order, initial_state) + + @value.alternative(requires='simulate_sweep', implementation=_simulate_sweep_to_iter) + def simulate_sweep_iter( + self, + program: 'cirq.Circuit', + params: study.Sweepable, + qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, + initial_state: Any = None, + ) -> Iterator[TSimulationTrialResult]: """Simulates the supplied Circuit. This method returns a result which allows access to the entire final @@ -367,7 +478,7 @@ def simulate_sweep( documentation of the implementing class for details. Returns: - List of SimulationTrialResults for this run, one for each + Iterator over SimulationTrialResults for this run, one for each possible parameter resolver. """ raise NotImplementedError() @@ -391,13 +502,13 @@ class SimulatesIntermediateState( a state vector. """ - def simulate_sweep( + def simulate_sweep_iter( self, program: 'cirq.Circuit', params: study.Sweepable, qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, initial_state: Any = None, - ) -> List[TSimulationTrialResult]: + ) -> Iterator[TSimulationTrialResult]: """Simulates the supplied Circuit. This method returns a result which allows access to the entire @@ -419,7 +530,6 @@ def simulate_sweep( List of SimulationTrialResults for this run, one for each possible parameter resolver. """ - trial_results = [] qubit_order = ops.QubitOrder.as_qubit_order(qubit_order) for param_resolver in study.to_resolvers(params): all_step_results = self.simulate_moment_steps( @@ -429,14 +539,11 @@ def simulate_sweep( for step_result in all_step_results: for k, v in step_result.measurements.items(): measurements[k] = np.array(v, dtype=np.uint8) - trial_results.append( - self._create_simulator_trial_result( - params=param_resolver, - measurements=measurements, - final_simulator_state=step_result._simulator_state(), - ) + yield self._create_simulator_trial_result( + params=param_resolver, + measurements=measurements, + final_simulator_state=step_result._simulator_state(), ) - return trial_results def simulate_moment_steps( self, diff --git a/cirq-core/cirq/sim/simulator_test.py b/cirq-core/cirq/sim/simulator_test.py index d4f57ada2f1..d363d1db85d 100644 --- a/cirq-core/cirq/sim/simulator_test.py +++ b/cirq-core/cirq/sim/simulator_test.py @@ -13,7 +13,7 @@ # limitations under the License. """Tests for simulator.py""" import abc -from typing import Generic, Dict, Any +from typing import Generic, Dict, Any, List, Sequence, Union from unittest import mock import numpy as np import pytest @@ -23,6 +23,9 @@ from cirq.sim.simulator import ( TStepResult, TSimulatorState, + SimulatesAmplitudes, + SimulatesExpectationValues, + SimulatesFinalState, SimulatesIntermediateState, SimulationTrialResult, TActOnArgs, @@ -440,3 +443,97 @@ def _channel_(self): np.testing.assert_allclose( out.state_vector(), cirq.one_hot(index=k % 3, shape=4, dtype=np.complex64), atol=1e-8 ) + + +def test_iter_definitions(): + dummy_trial_result = SimulationTrialResult(params={}, measurements={}, final_simulator_state=[]) + + class FakeNonIterSimulatorImpl( + SimulatesAmplitudes, + SimulatesExpectationValues, + SimulatesFinalState, + ): + """A class which defines the non-Iterator simulator API methods. + + After v0.12, simulators are expected to implement the *_iter methods. + """ + + def compute_amplitudes_sweep( + self, + program: 'cirq.Circuit', + bitstrings: Sequence[int], + params: study.Sweepable, + qubit_order: cirq.QubitOrderOrList = cirq.QubitOrder.DEFAULT, + ) -> Sequence[Sequence[complex]]: + return [[1.0]] + + def simulate_expectation_values_sweep( + self, + program: 'cirq.Circuit', + observables: Union['cirq.PauliSumLike', List['cirq.PauliSumLike']], + params: 'study.Sweepable', + qubit_order: cirq.QubitOrderOrList = cirq.QubitOrder.DEFAULT, + initial_state: Any = None, + permit_terminal_measurements: bool = False, + ) -> List[List[float]]: + return [[1.0]] + + def simulate_sweep( + self, + program: 'cirq.Circuit', + params: study.Sweepable, + qubit_order: cirq.QubitOrderOrList = cirq.QubitOrder.DEFAULT, + initial_state: Any = None, + ) -> List[SimulationTrialResult]: + return [dummy_trial_result] + + non_iter_sim = FakeNonIterSimulatorImpl() + q0 = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(q0)) + bitstrings = [0b0] + params = {} + assert non_iter_sim.compute_amplitudes_sweep(circuit, bitstrings, params) == [[1.0]] + amp_iter = non_iter_sim.compute_amplitudes_sweep_iter(circuit, bitstrings, params) + assert next(amp_iter) == [1.0] + + obs = cirq.X(q0) + assert non_iter_sim.simulate_expectation_values_sweep(circuit, obs, params) == [[1.0]] + ev_iter = non_iter_sim.simulate_expectation_values_sweep_iter(circuit, obs, params) + assert next(ev_iter) == [1.0] + + assert non_iter_sim.simulate_sweep(circuit, params) == [dummy_trial_result] + state_iter = non_iter_sim.simulate_sweep_iter(circuit, params) + assert next(state_iter) == dummy_trial_result + + +def test_missing_iter_definitions(): + class FakeMissingIterSimulatorImpl( + SimulatesAmplitudes, + SimulatesExpectationValues, + SimulatesFinalState, + ): + """A class which fails to define simulator methods.""" + + missing_iter_sim = FakeMissingIterSimulatorImpl() + q0 = cirq.LineQubit(0) + circuit = cirq.Circuit(cirq.X(q0)) + bitstrings = [0b0] + params = {} + with pytest.raises(RecursionError): + missing_iter_sim.compute_amplitudes_sweep(circuit, bitstrings, params) + with pytest.raises(RecursionError): + amp_iter = missing_iter_sim.compute_amplitudes_sweep_iter(circuit, bitstrings, params) + next(amp_iter) + + obs = cirq.X(q0) + with pytest.raises(RecursionError): + missing_iter_sim.simulate_expectation_values_sweep(circuit, obs, params) + with pytest.raises(RecursionError): + ev_iter = missing_iter_sim.simulate_expectation_values_sweep_iter(circuit, obs, params) + next(ev_iter) + + with pytest.raises(RecursionError): + missing_iter_sim.simulate_sweep(circuit, params) + with pytest.raises(RecursionError): + state_iter = missing_iter_sim.simulate_sweep_iter(circuit, params) + next(state_iter) diff --git a/cirq-core/cirq/sim/sparse_simulator.py b/cirq-core/cirq/sim/sparse_simulator.py index ff6020a44db..4c3837e0806 100644 --- a/cirq-core/cirq/sim/sparse_simulator.py +++ b/cirq-core/cirq/sim/sparse_simulator.py @@ -17,6 +17,7 @@ from typing import ( Any, Dict, + Iterator, List, Type, TYPE_CHECKING, @@ -26,7 +27,7 @@ import numpy as np -from cirq import ops, protocols, qis, study, devices +from cirq import ops, protocols, qis, devices from cirq.sim import ( simulator, state_vector, @@ -209,38 +210,32 @@ def _create_step_result( dtype=self._dtype, ) - def simulate_expectation_values_sweep( + def simulate_expectation_values_sweep_iter( self, program: 'cirq.Circuit', observables: Union['cirq.PauliSumLike', List['cirq.PauliSumLike']], - params: 'study.Sweepable', + params: 'cirq.Sweepable', qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, initial_state: Any = None, permit_terminal_measurements: bool = False, - ) -> List[List[float]]: + ) -> Iterator[List[float]]: if not permit_terminal_measurements and program.are_any_measurements_terminal(): raise ValueError( 'Provided circuit has terminal measurements, which may ' 'skew expectation values. If this is intentional, set ' 'permit_terminal_measurements=True.' ) - swept_evs = [] qubit_order = ops.QubitOrder.as_qubit_order(qubit_order) qmap = {q: i for i, q in enumerate(qubit_order.order_for(program.all_qubits()))} if not isinstance(observables, List): observables = [observables] pslist = [ops.PauliSum.wrap(pslike) for pslike in observables] - for param_resolver in study.to_resolvers(params): - result = self.simulate( - program, param_resolver, qubit_order=qubit_order, initial_state=initial_state + yield from ( + [obs.expectation_from_state_vector(result.final_state_vector, qmap) for obs in pslist] + for result in self.simulate_sweep_iter( + program, params, qubit_order=qubit_order, initial_state=initial_state ) - swept_evs.append( - [ - obs.expectation_from_state_vector(result.final_state_vector, qmap) - for obs in pslist - ] - ) - return swept_evs + ) class SparseSimulatorStep( diff --git a/cirq-core/cirq/sim/state_vector_simulator.py b/cirq-core/cirq/sim/state_vector_simulator.py index 629fddafbe4..b6eb1166fbe 100644 --- a/cirq-core/cirq/sim/state_vector_simulator.py +++ b/cirq-core/cirq/sim/state_vector_simulator.py @@ -15,7 +15,7 @@ import abc -from typing import Any, Dict, Sequence, TYPE_CHECKING, Tuple, Generic, TypeVar, Type +from typing import Any, Dict, Iterator, Sequence, TYPE_CHECKING, Tuple, Generic, TypeVar, Type import numpy as np @@ -69,13 +69,13 @@ def _create_simulator_trial_result( params=params, measurements=measurements, final_simulator_state=final_simulator_state ) - def compute_amplitudes_sweep( + def compute_amplitudes_sweep_iter( self, program: 'cirq.Circuit', bitstrings: Sequence[int], params: study.Sweepable, qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT, - ) -> Sequence[Sequence[complex]]: + ) -> Iterator[Sequence[complex]]: if isinstance(bitstrings, np.ndarray) and len(bitstrings.shape) > 1: raise ValueError( 'The list of bitstrings must be input as a ' @@ -83,19 +83,16 @@ def compute_amplitudes_sweep( f'shape {bitstrings.shape}.' ) - trial_results = self.simulate_sweep(program, params, qubit_order) - # 1-dimensional tuples don't trigger advanced Numpy array indexing # https://docs.scipy.org/doc/numpy/reference/arrays.indexing.html if isinstance(bitstrings, tuple): bitstrings = list(bitstrings) - all_amplitudes = [] - for trial_result in trial_results: - amplitudes = trial_result.final_state_vector[bitstrings] - all_amplitudes.append(amplitudes) + trial_result_iter = self.simulate_sweep_iter(program, params, qubit_order) - return all_amplitudes + yield from ( + trial_result.final_state_vector[bitstrings] for trial_result in trial_result_iter + ) class StateVectorStepResult( diff --git a/cirq-core/cirq/value/__init__.py b/cirq-core/cirq/value/__init__.py index 6149ccb1b56..ff37be6c695 100644 --- a/cirq-core/cirq/value/__init__.py +++ b/cirq-core/cirq/value/__init__.py @@ -16,6 +16,7 @@ from cirq.value.abc_alt import ( ABCMetaImplementAnyOneOf, alternative, + GenericMetaImplementAnyOneOf, ) from cirq.value.angle import ( diff --git a/cirq-core/cirq/value/abc_alt.py b/cirq-core/cirq/value/abc_alt.py index da7a21e00ad..a4bc0f99d55 100644 --- a/cirq-core/cirq/value/abc_alt.py +++ b/cirq-core/cirq/value/abc_alt.py @@ -17,6 +17,16 @@ import functools from typing import cast, Callable, Set, TypeVar +# Required due to PEP 560 +try: + # python 3.6 class for generic metaclasses + from typing import GenericMeta # type: ignore +except ImportError: + # In python 3.7, GenericMeta doesn't exist but we don't need it + class GenericMeta(type): # type: ignore + pass + + T = TypeVar('T') @@ -144,3 +154,14 @@ def impl_of_abstract(*args, **kwargs): cls.__abstractmethods__ |= abstracts # Add to the set made by ABCMeta cls._implemented_by_ = implemented_by return cls + + +class GenericMetaImplementAnyOneOf(GenericMeta, ABCMetaImplementAnyOneOf): + """Generic version of ABCMetaImplementAnyOneOf. + + Classes which inherit from Generic[T] must use this type instead of + ABCMetaImplementAnyOneOf due to https://github.com/python/typing/issues/449. + + This issue is specific to python3.6; this class can be removed when Cirq + python3.6 support is turned down. + """ diff --git a/cirq-google/cirq_google/calibration/engine_simulator.py b/cirq-google/cirq_google/calibration/engine_simulator.py index f3e2f2cc786..0028f6c53fe 100644 --- a/cirq-google/cirq_google/calibration/engine_simulator.py +++ b/cirq-google/cirq_google/calibration/engine_simulator.py @@ -3,6 +3,7 @@ Callable, Dict, Iterable, + Iterator, List, Optional, Sequence, @@ -389,14 +390,14 @@ def create_gate_with_drift( return gate_calibration.as_characterized_phased_fsim_gate(parameters) - def run_sweep( + def run_sweep_iter( self, program: Circuit, params: Sweepable, repetitions: int = 1, - ) -> List[Result]: + ) -> Iterator[Result]: converted = _convert_to_circuit_with_drift(self, program) - return self._simulator.run_sweep(converted, params, repetitions) + yield from self._simulator.run_sweep_iter(converted, params, repetitions) def simulate( self,