Skip to content

Commit 72993b5

Browse files
Density matrix simulator EVs (#3979)
Fixes #3964. CC @zaqqwerty This is a near-exact copy of the behavior and tests for `sparse_simulator`. If we see the need in the future, this code could probably be folded into a `SimulatesEVsAndFullState` interface as the default `simulate_expectation_values_sweep` behavior, but that doesn't seem particularly urgent at the moment. Relevant changes from the `sparse_simulator` code: - `obs.expectation_from_density_matrix(result.final_density_matrix, qmap)` instead of state-vector equivalent - added `test_simulate_noisy_expectation_values` test - that's it
1 parent 0d7534a commit 72993b5

File tree

2 files changed

+135
-0
lines changed

2 files changed

+135
-0
lines changed

cirq-core/cirq/sim/density_matrix_simulator.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ class DensityMatrixSimulator(
3131
'DensityMatrixSimulatorState',
3232
act_on_density_matrix_args.ActOnDensityMatrixArgs,
3333
],
34+
simulator.SimulatesExpectationValues,
3435
):
3536
"""A simulator for density matrices and noisy quantum circuits.
3637
@@ -220,6 +221,40 @@ def _create_simulator_trial_result(
220221
params=params, measurements=measurements, final_simulator_state=final_simulator_state
221222
)
222223

224+
# TODO(#4209): Deduplicate with identical code in sparse_simulator.
225+
def simulate_expectation_values_sweep(
226+
self,
227+
program: 'cirq.Circuit',
228+
observables: Union['cirq.PauliSumLike', List['cirq.PauliSumLike']],
229+
params: 'study.Sweepable',
230+
qubit_order: ops.QubitOrderOrList = ops.QubitOrder.DEFAULT,
231+
initial_state: Any = None,
232+
permit_terminal_measurements: bool = False,
233+
) -> List[List[float]]:
234+
if not permit_terminal_measurements and program.are_any_measurements_terminal():
235+
raise ValueError(
236+
'Provided circuit has terminal measurements, which may '
237+
'skew expectation values. If this is intentional, set '
238+
'permit_terminal_measurements=True.'
239+
)
240+
swept_evs = []
241+
qubit_order = ops.QubitOrder.as_qubit_order(qubit_order)
242+
qmap = {q: i for i, q in enumerate(qubit_order.order_for(program.all_qubits()))}
243+
if not isinstance(observables, List):
244+
observables = [observables]
245+
pslist = [ops.PauliSum.wrap(pslike) for pslike in observables]
246+
for param_resolver in study.to_resolvers(params):
247+
result = self.simulate(
248+
program, param_resolver, qubit_order=qubit_order, initial_state=initial_state
249+
)
250+
swept_evs.append(
251+
[
252+
obs.expectation_from_density_matrix(result.final_density_matrix, qmap)
253+
for obs in pslist
254+
]
255+
)
256+
return swept_evs
257+
223258

224259
class DensityMatrixStepResult(simulator.StepResult['DensityMatrixSimulatorState']):
225260
"""A single step in the simulation of the DensityMatrixSimulator.

cirq-core/cirq/sim/density_matrix_simulator_test.py

Lines changed: 100 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -808,6 +808,106 @@ def test_simulate_moment_steps_intermediate_measurement(dtype):
808808
np.testing.assert_almost_equal(step.density_matrix(), expected)
809809

810810

811+
@pytest.mark.parametrize('dtype', [np.complex64, np.complex128])
812+
def test_simulate_expectation_values(dtype):
813+
# Compare with test_expectation_from_state_vector_two_qubit_states
814+
# in file: cirq/ops/linear_combinations_test.py
815+
q0, q1 = cirq.LineQubit.range(2)
816+
psum1 = cirq.Z(q0) + 3.2 * cirq.Z(q1)
817+
psum2 = -1 * cirq.X(q0) + 2 * cirq.X(q1)
818+
c1 = cirq.Circuit(cirq.I(q0), cirq.X(q1))
819+
simulator = cirq.DensityMatrixSimulator(dtype=dtype)
820+
result = simulator.simulate_expectation_values(c1, [psum1, psum2])
821+
assert cirq.approx_eq(result[0], -2.2, atol=1e-6)
822+
assert cirq.approx_eq(result[1], 0, atol=1e-6)
823+
824+
c2 = cirq.Circuit(cirq.H(q0), cirq.H(q1))
825+
result = simulator.simulate_expectation_values(c2, [psum1, psum2])
826+
assert cirq.approx_eq(result[0], 0, atol=1e-6)
827+
assert cirq.approx_eq(result[1], 1, atol=1e-6)
828+
829+
psum3 = cirq.Z(q0) + cirq.X(q1)
830+
c3 = cirq.Circuit(cirq.I(q0), cirq.H(q1))
831+
result = simulator.simulate_expectation_values(c3, psum3)
832+
assert cirq.approx_eq(result[0], 2, atol=1e-6)
833+
834+
835+
@pytest.mark.parametrize('dtype', [np.complex64, np.complex128])
836+
def test_simulate_noisy_expectation_values(dtype):
837+
q0 = cirq.LineQubit(0)
838+
psums = [cirq.Z(q0), cirq.X(q0)]
839+
c1 = cirq.Circuit(
840+
cirq.X(q0),
841+
cirq.amplitude_damp(gamma=0.1).on(q0),
842+
)
843+
simulator = cirq.DensityMatrixSimulator(dtype=dtype)
844+
result = simulator.simulate_expectation_values(c1, psums)
845+
# <Z> = (gamma - 1) + gamma = -0.8
846+
assert cirq.approx_eq(result[0], -0.8, atol=1e-6)
847+
assert cirq.approx_eq(result[1], 0, atol=1e-6)
848+
849+
c2 = cirq.Circuit(
850+
cirq.H(q0),
851+
cirq.depolarize(p=0.3).on(q0),
852+
)
853+
result = simulator.simulate_expectation_values(c2, psums)
854+
assert cirq.approx_eq(result[0], 0, atol=1e-6)
855+
# <X> = (1 - p) + (-p / 3) = 0.6
856+
assert cirq.approx_eq(result[1], 0.6, atol=1e-6)
857+
858+
859+
@pytest.mark.parametrize('dtype', [np.complex64, np.complex128])
860+
def test_simulate_expectation_values_terminal_measure(dtype):
861+
q0 = cirq.LineQubit(0)
862+
circuit = cirq.Circuit(cirq.H(q0), cirq.measure(q0))
863+
obs = cirq.Z(q0)
864+
simulator = cirq.DensityMatrixSimulator(dtype=dtype)
865+
with pytest.raises(ValueError):
866+
_ = simulator.simulate_expectation_values(circuit, obs)
867+
868+
results = {-1: 0, 1: 0}
869+
for _ in range(100):
870+
result = simulator.simulate_expectation_values(
871+
circuit, obs, permit_terminal_measurements=True
872+
)
873+
if cirq.approx_eq(result[0], -1, atol=1e-6):
874+
results[-1] += 1
875+
if cirq.approx_eq(result[0], 1, atol=1e-6):
876+
results[1] += 1
877+
878+
# With a measurement after H, the Z-observable expects a specific state.
879+
assert results[-1] > 0
880+
assert results[1] > 0
881+
assert results[-1] + results[1] == 100
882+
883+
circuit = cirq.Circuit(cirq.H(q0))
884+
results = {0: 0}
885+
for _ in range(100):
886+
result = simulator.simulate_expectation_values(
887+
circuit, obs, permit_terminal_measurements=True
888+
)
889+
if cirq.approx_eq(result[0], 0, atol=1e-6):
890+
results[0] += 1
891+
892+
# Without measurement after H, the Z-observable is indeterminate.
893+
assert results[0] == 100
894+
895+
896+
@pytest.mark.parametrize('dtype', [np.complex64, np.complex128])
897+
def test_simulate_expectation_values_qubit_order(dtype):
898+
q0, q1, q2 = cirq.LineQubit.range(3)
899+
circuit = cirq.Circuit(cirq.H(q0), cirq.H(q1), cirq.X(q2))
900+
obs = cirq.X(q0) + cirq.X(q1) - cirq.Z(q2)
901+
simulator = cirq.DensityMatrixSimulator(dtype=dtype)
902+
903+
result = simulator.simulate_expectation_values(circuit, obs)
904+
assert cirq.approx_eq(result[0], 3, atol=1e-6)
905+
906+
# Adjusting the qubit order has no effect on the observables.
907+
result_flipped = simulator.simulate_expectation_values(circuit, obs, qubit_order=[q1, q2, q0])
908+
assert cirq.approx_eq(result_flipped[0], 3, atol=1e-6)
909+
910+
811911
def test_density_matrix_simulator_state_eq():
812912
q0, q1 = cirq.LineQubit.range(2)
813913
eq = cirq.testing.EqualsTester()

0 commit comments

Comments
 (0)