diff --git a/tensorflow_quantum/core/ops/batch_util.py b/tensorflow_quantum/core/ops/batch_util.py index 75f148424..fad31c476 100644 --- a/tensorflow_quantum/core/ops/batch_util.py +++ b/tensorflow_quantum/core/ops/batch_util.py @@ -23,7 +23,7 @@ # be used end to end with engine. This current issue is that # cirq.PauliSumCollector does not produce serializable gates for basis # conversion. -class TFQPauliSumCollector(cirq.work.collector.Collector): +class TFQPauliProjectorSumCollector(cirq.work.collector.Collector): """Copy of cirq.PauliSumCollector with some fixes to work with engine.""" def __init__(self, @@ -261,7 +261,7 @@ def batch_calculate_expectation(circuits, param_resolvers, ops, simulator): if not isinstance(sub_list, (list, tuple, np.ndarray)): raise TypeError('elements of ops must be type list.') for x in sub_list: - if not isinstance(x, cirq.PauliSum): + if not isinstance(x, (cirq.PauliSum, cirq.ProjectorSum)): raise TypeError('ops must contain only cirq.PauliSum objects.' ' Given: {}'.format(type(x))) @@ -279,8 +279,13 @@ def batch_calculate_expectation(circuits, param_resolvers, ops, simulator): sim_result = simulator.simulate(c, p) for j, op in enumerate(op_row): dm = sim_result.final_density_matrix - all_exp_vals[i][j] = op.expectation_from_density_matrix( - dm, qubit_order, check_preconditions=False) + if isinstance(op, cirq.PauliSum): + all_exp_vals[i][j] = op.expectation_from_density_matrix( + dm, qubit_order, check_preconditions=False) + else: + assert isinstance(op, cirq.ProjectorSum) + all_exp_vals[i][j] = op.expectation_from_density_matrix( + dm, qubit_order) else: # Valid observables always have real expectation values. all_exp_vals[i] = np.real( @@ -359,7 +364,7 @@ def batch_calculate_sampled_expectation(circuits, param_resolvers, ops, continue circuit = cirq.resolve_parameters(c, params) for op_index, op in enumerate(ops[c_index]): - collector = TFQPauliSumCollector( + collector = TFQPauliProjectorSumCollector( circuit, op, samples_per_term=n_samples[c_index][op_index]) collector.collect(sampler) result = collector.estimated_energy().real diff --git a/tensorflow_quantum/core/ops/math_ops/tfq_inner_product.cc b/tensorflow_quantum/core/ops/math_ops/tfq_inner_product.cc index 2a66d2919..44183efd2 100644 --- a/tensorflow_quantum/core/ops/math_ops/tfq_inner_product.cc +++ b/tensorflow_quantum/core/ops/math_ops/tfq_inner_product.cc @@ -35,7 +35,6 @@ limitations under the License. namespace tfq { using ::tensorflow::Status; -using ::tfq::proto::PauliSum; using ::tfq::proto::Program; typedef qsim::Cirq::GateCirq QsimGate; diff --git a/tensorflow_quantum/core/ops/math_ops/tfq_inner_product_grad.cc b/tensorflow_quantum/core/ops/math_ops/tfq_inner_product_grad.cc index 5b29571d2..25fafce2b 100644 --- a/tensorflow_quantum/core/ops/math_ops/tfq_inner_product_grad.cc +++ b/tensorflow_quantum/core/ops/math_ops/tfq_inner_product_grad.cc @@ -36,7 +36,6 @@ limitations under the License. namespace tfq { using ::tensorflow::Status; -using ::tfq::proto::PauliSum; using ::tfq::proto::Program; typedef qsim::Cirq::GateCirq QsimGate; diff --git a/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_expectation.cc b/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_expectation.cc index adb1d9bb6..894e20975 100644 --- a/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_expectation.cc +++ b/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_expectation.cc @@ -85,7 +85,7 @@ class TfqSimulateMPS1DExpectationOp : public tensorflow::OpKernel { // is resolved. OP_REQUIRES_OK(context, GetProgramsAndNumQubits(context, &programs, &num_qubits, - &pauli_sums, true)); + &pauli_sums, nullptr, true)); std::vector maps; OP_REQUIRES_OK(context, GetSymbolMaps(context, &maps)); diff --git a/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_sampled_expectation.cc b/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_sampled_expectation.cc index 750531f16..2604615b1 100644 --- a/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_sampled_expectation.cc +++ b/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_sampled_expectation.cc @@ -81,7 +81,7 @@ class TfqSimulateMPS1DSampledExpectationOp : public tensorflow::OpKernel { std::vector> pauli_sums; OP_REQUIRES_OK(context, GetProgramsAndNumQubits(context, &programs, &num_qubits, - &pauli_sums, true)); + &pauli_sums, nullptr, true)); std::vector maps; OP_REQUIRES_OK(context, GetSymbolMaps(context, &maps)); diff --git a/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_samples.cc b/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_samples.cc index 495e5f8f2..962615a00 100644 --- a/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_samples.cc +++ b/tensorflow_quantum/core/ops/math_ops/tfq_simulate_1d_samples.cc @@ -66,7 +66,7 @@ class TfqSimulateMPS1DSamplesOp : public tensorflow::OpKernel { std::vector num_qubits; OP_REQUIRES_OK(context, GetProgramsAndNumQubits(context, &programs, &num_qubits, - nullptr, true)); + nullptr, nullptr, true)); // Parse symbol maps for parameter resolution in the circuits. std::vector maps; diff --git a/tensorflow_quantum/core/ops/noise/BUILD b/tensorflow_quantum/core/ops/noise/BUILD index a72665780..597004ee1 100644 --- a/tensorflow_quantum/core/ops/noise/BUILD +++ b/tensorflow_quantum/core/ops/noise/BUILD @@ -83,6 +83,7 @@ py_library( py_test( name = "noisy_expectation_op_test", + timeout = "long", srcs = ["noisy_expectation_op_test.py"], python_version = "PY3", deps = [ @@ -103,6 +104,7 @@ py_library( py_test( name = "noisy_sampled_expectation_op_test", + timeout = "long", srcs = ["noisy_sampled_expectation_op_test.py"], python_version = "PY3", deps = [ diff --git a/tensorflow_quantum/core/ops/noise/noisy_expectation_op.py b/tensorflow_quantum/core/ops/noise/noisy_expectation_op.py index e621988a0..b7e85eb8d 100644 --- a/tensorflow_quantum/core/ops/noise/noisy_expectation_op.py +++ b/tensorflow_quantum/core/ops/noise/noisy_expectation_op.py @@ -20,7 +20,12 @@ NOISY_OP_MODULE = load_module(os.path.join("noise", "_tfq_noise_ops.so")) -def expectation(programs, symbol_names, symbol_values, pauli_sums, num_samples): +def expectation(programs, + symbol_names, + symbol_values, + pauli_sums, + num_samples, + projector_sums=None): """Calculate the analytic expectation values using monte-carlo trajectories. Simulate the final state of `programs` given `symbol_values` are placed @@ -88,11 +93,21 @@ def expectation(programs, symbol_names, symbol_values, pauli_sums, num_samples): threads to TensorFlow. For best performance ensure that the quantities in `num_samples` are a multiple of the number of available threads. + projector_sums: `tf.Tensor` of strings with shape [batch_size, n_ops] + containing the string representation of the operators that will + be used on all of the circuits in the expectation calculations. Returns: `tf.Tensor` with shape [batch_size, n_ops] that holds the expectation value for each circuit with each op applied to it (after resolving the corresponding parameters in). """ + # TODO(tonybruguier): Always supply a projector sum to this function and + # remove the special-casing below. + if projector_sums is None: + if len(pauli_sums.shape) == 1: + projector_sums = pauli_sums[0:0] + else: + projector_sums = pauli_sums[:, 0:0] return NOISY_OP_MODULE.tfq_noisy_expectation( programs, symbol_names, tf.cast(symbol_values, tf.float32), pauli_sums, - tf.cast(num_samples, dtype=tf.int32)) + projector_sums, tf.cast(num_samples, dtype=tf.int32)) diff --git a/tensorflow_quantum/core/ops/noise/noisy_expectation_op_test.py b/tensorflow_quantum/core/ops/noise/noisy_expectation_op_test.py index 1e73500b8..7c767bfd6 100644 --- a/tensorflow_quantum/core/ops/noise/noisy_expectation_op_test.py +++ b/tensorflow_quantum/core/ops/noise/noisy_expectation_op_test.py @@ -49,7 +49,8 @@ def test_noisy_expectation_inputs(self): for resolver in resolver_batch]) pauli_sums = util.random_pauli_sums(qubits, 3, batch_size) - num_samples = [[10]] * batch_size + projector_sums = util.random_projector_sums(qubits, 3, batch_size) + num_samples = [[10, 10]] * batch_size with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'programs must be rank 1'): @@ -57,7 +58,8 @@ def test_noisy_expectation_inputs(self): noisy_expectation_op.expectation( util.convert_to_tensor([circuit_batch]), symbol_names, symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'symbol_names must be rank 1.'): @@ -65,7 +67,8 @@ def test_noisy_expectation_inputs(self): noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), np.array([symbol_names]), symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'symbol_values must be rank 2.'): @@ -73,7 +76,8 @@ def test_noisy_expectation_inputs(self): noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), symbol_names, np.array([symbol_values_array]), - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'symbol_values must be rank 2.'): @@ -81,7 +85,8 @@ def test_noisy_expectation_inputs(self): noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array[0], - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'pauli_sums must be rank 2.'): @@ -89,7 +94,17 @@ def test_noisy_expectation_inputs(self): noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, - util.convert_to_tensor(list(pauli_sums)), num_samples) + util.convert_to_tensor(list(pauli_sums)), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'projector_sums must be rank 2.'): + # pauli_sums tensor has too few dimensions. + noisy_expectation_op.expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor(list(projector_sums))) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'pauli_sums must be rank 2.'): @@ -98,7 +113,17 @@ def test_noisy_expectation_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, [util.convert_to_tensor([[x] for x in pauli_sums])], - num_samples) + num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'projector_sums must be rank 2.'): + # pauli_sums tensor has too many dimensions. + noisy_expectation_op.expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + [util.convert_to_tensor([[x] for x in projector_sums])]) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'num_samples must be rank 2'): @@ -107,7 +132,8 @@ def test_noisy_expectation_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, util.convert_to_tensor([[x] for x in pauli_sums]), - [num_samples]) + [num_samples], + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'num_samples must be rank 2'): @@ -116,14 +142,16 @@ def test_noisy_expectation_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, util.convert_to_tensor([[x] for x in pauli_sums]), - num_samples[0]) + num_samples[0], + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'Unparseable proto'): # circuit tensor has the right type but invalid values. noisy_expectation_op.expectation( ['junk'] * batch_size, symbol_names, symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'Could not find symbol in parameter map'): @@ -131,7 +159,8 @@ def test_noisy_expectation_inputs(self): noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), ['junk'], symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'qubits not found in circuit'): @@ -142,40 +171,66 @@ def test_noisy_expectation_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, util.convert_to_tensor([[x] for x in new_pauli_sums]), - num_samples) + num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'qubits not found in circuit'): + # pauli_sums tensor has the right type but invalid values. + new_qubits = [cirq.GridQubit(5, 5), cirq.GridQubit(9, 9)] + new_projector_sums = util.random_pauli_sums(new_qubits, 2, + batch_size) + noisy_expectation_op.expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in new_projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'Unparseable proto'): # pauli_sums tensor has the right type but invalid values 2. noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), symbol_names, - symbol_values_array, [['junk']] * batch_size, num_samples) + symbol_values_array, [['junk']] * batch_size, num_samples, + [['junk']] * batch_size) with self.assertRaisesRegex(TypeError, 'Cannot convert'): # circuits tensor has the wrong type. noisy_expectation_op.expectation( [1.0] * batch_size, symbol_names, symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(TypeError, 'Cannot convert'): # symbol_names tensor has the wrong type. noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), [0.1234], symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.UnimplementedError, ''): # symbol_values tensor has the wrong type. noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), symbol_names, [['junk']] * batch_size, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) + + with self.assertRaisesRegex(TypeError, 'Cannot convert'): + # pauli_sums tensor has the wrong type. + noisy_expectation_op.expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, [[1.0]] * batch_size, num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(TypeError, 'Cannot convert'): # pauli_sums tensor has the wrong type. noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), symbol_names, - symbol_values_array, [[1.0]] * batch_size, num_samples) + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + [[1.0]] * batch_size) with self.assertRaisesRegex(TypeError, 'missing'): # we are missing an argument. @@ -190,8 +245,9 @@ def test_noisy_expectation_inputs(self): noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), [], - num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), + util.convert_to_tensor([[x] for x in projector_sums]), + num_samples, []) # pylint: enable=too-many-function-args with self.assertRaisesRegex(tf.errors.InvalidArgumentError, @@ -200,7 +256,8 @@ def test_noisy_expectation_inputs(self): noisy_expectation_op.expectation( util.convert_to_tensor([cirq.Circuit()]), symbol_names, symbol_values_array.astype(np.float64), - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'greater than 0'): @@ -209,7 +266,8 @@ def test_noisy_expectation_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, util.convert_to_tensor([[x] for x in pauli_sums]), - [[-1]] * batch_size) + [[-1]] * batch_size, + util.convert_to_tensor([[x] for x in projector_sums])) # pylint: enable=too-many-function-args with self.assertRaisesRegex(tf.errors.InvalidArgumentError, @@ -218,7 +276,8 @@ def test_noisy_expectation_inputs(self): noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array[:int(batch_size * 0.5)], - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) @parameterized.parameters([ { @@ -259,16 +318,24 @@ def test_simulate_consistency(self, batch_size, n_qubits, noisy): pauli_sums1 = util.random_pauli_sums(qubits, 3, batch_size) pauli_sums2 = util.random_pauli_sums(qubits, 3, batch_size) batch_pauli_sums = [[x, y] for x, y in zip(pauli_sums1, pauli_sums2)] - num_samples = [[10000 if noisy else 3] * 2] * batch_size + projector_sums1 = util.random_projector_sums(qubits, 3, batch_size) + projector_sums2 = util.random_projector_sums(qubits, 3, batch_size) + batch_projector_sums = [ + [x, y] for x, y in zip(projector_sums1, projector_sums2) + ] + batch_both = [[x, y, z, t] for x, y, z, t in zip( + pauli_sums1, pauli_sums2, projector_sums1, projector_sums2)] + num_samples = [[10000 if noisy else 3] * 4] * batch_size op_exps = noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, - util.convert_to_tensor(batch_pauli_sums), num_samples) + util.convert_to_tensor(batch_pauli_sums), num_samples, + util.convert_to_tensor(batch_projector_sums)) cirq_exps = batch_util.batch_calculate_expectation( - circuit_batch, resolver_batch, batch_pauli_sums, - cirq.DensityMatrixSimulator() if noisy else cirq.Simulator()) + circuit_batch, resolver_batch, batch_both, + cirq.DensityMatrixSimulator()) tol = 5e-2 if noisy else 5e-4 self.assertAllClose(cirq_exps, op_exps, atol=tol, rtol=tol) @@ -297,15 +364,23 @@ def test_single_channel(self, channel): pauli_sums1 = util.random_pauli_sums(qubits, 3, batch_size) pauli_sums2 = util.random_pauli_sums(qubits, 3, batch_size) batch_pauli_sums = [[x, y] for x, y in zip(pauli_sums1, pauli_sums2)] - num_samples = [[10000] * 2] * batch_size + projector_sums1 = util.random_projector_sums(qubits, 3, batch_size) + projector_sums2 = util.random_projector_sums(qubits, 3, batch_size) + batch_projector_sums = [ + [x, y] for x, y in zip(projector_sums1, projector_sums2) + ] + batch_both = [[x, y, z, t] for x, y, z, t in zip( + pauli_sums1, pauli_sums2, projector_sums1, projector_sums2)] + num_samples = [[10000] * 4] * batch_size op_exps = noisy_expectation_op.expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, - util.convert_to_tensor(batch_pauli_sums), num_samples) + util.convert_to_tensor(batch_pauli_sums), num_samples, + util.convert_to_tensor(batch_projector_sums)) cirq_exps = batch_util.batch_calculate_expectation( - circuit_batch, resolver_batch, batch_pauli_sums, + circuit_batch, resolver_batch, batch_both, cirq.DensityMatrixSimulator()) self.assertAllClose(cirq_exps, op_exps, atol=5e-2, rtol=5e-2) @@ -316,11 +391,14 @@ def test_correctness_empty(self): empty_symbols = tf.convert_to_tensor([], dtype=tf.dtypes.string) empty_values = tf.convert_to_tensor([[]]) empty_paulis = tf.convert_to_tensor([[]], dtype=tf.dtypes.string) + empty_projector_sums = tf.convert_to_tensor([[]], + dtype=tf.dtypes.string) empty_n_samples = tf.convert_to_tensor([[]], dtype=tf.int32) out = noisy_expectation_op.expectation(empty_circuit, empty_symbols, empty_values, empty_paulis, - empty_n_samples) + empty_n_samples, + empty_projector_sums) expected = np.array([[]], dtype=np.complex64) self.assertAllClose(out, expected) @@ -331,11 +409,13 @@ def test_correctness_no_circuit(self): empty_symbols = tf.raw_ops.Empty(shape=(0,), dtype=tf.string) empty_values = tf.raw_ops.Empty(shape=(0, 0), dtype=tf.float32) empty_paulis = tf.raw_ops.Empty(shape=(0, 0), dtype=tf.string) + empty_projector_sums = tf.raw_ops.Empty(shape=(0, 0), dtype=tf.string) empty_n_samples = tf.raw_ops.Empty(shape=(0, 0), dtype=tf.int32) out = noisy_expectation_op.expectation(empty_circuit, empty_symbols, empty_values, empty_paulis, - empty_n_samples) + empty_n_samples, + empty_projector_sums) self.assertShapeEqual(np.zeros((0, 0)), out) diff --git a/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op.py b/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op.py index 1874c8a5e..94bd16660 100644 --- a/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op.py +++ b/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op.py @@ -20,8 +20,12 @@ NOISY_OP_MODULE = load_module(os.path.join("noise", "_tfq_noise_ops.so")) -def sampled_expectation(programs, symbol_names, symbol_values, pauli_sums, - num_samples): +def sampled_expectation(programs, + symbol_names, + symbol_values, + pauli_sums, + num_samples, + projector_sums=None): """Estimates (via sampling) expectation values using monte-carlo simulation. Simulate the final state of `programs` given `symbol_values` are placed @@ -87,11 +91,22 @@ def sampled_expectation(programs, symbol_names, symbol_values, pauli_sums, threads to TensorFlow. For best performance ensure that the quantities in `num_samples` are a multiple of the number of available threads. + projector_sums: `tf.Tensor` of strings with shape [batch_size, n_ops] + containing the string representation of the operators that will + be used on all of the circuits in the expectation calculations. Returns: `tf.Tensor` with shape [batch_size, n_ops] that holds the expectation value for each circuit with each op applied to it (after resolving the corresponding parameters in). """ + # TODO(tonybruguier): Always supply a projector sum to this function and + # remove the special-casing below. + if projector_sums is None: + if len(pauli_sums.shape) == 1: + projector_sums = pauli_sums[0:0] + else: + projector_sums = pauli_sums[:, 0:0] + print(f'TONYBOOM projector_sums.shape={projector_sums.shape}') return NOISY_OP_MODULE.tfq_noisy_sampled_expectation( programs, symbol_names, tf.cast(symbol_values, tf.float32), pauli_sums, - tf.cast(num_samples, dtype=tf.int32)) + projector_sums, tf.cast(num_samples, dtype=tf.int32)) diff --git a/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op_test.py b/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op_test.py index 35d1cc113..f119a681c 100644 --- a/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op_test.py +++ b/tensorflow_quantum/core/ops/noise/noisy_sampled_expectation_op_test.py @@ -49,7 +49,8 @@ def test_noisy_expectation_inputs(self): for resolver in resolver_batch]) pauli_sums = util.random_pauli_sums(qubits, 3, batch_size) - num_samples = [[10]] * batch_size + projector_sums = util.random_projector_sums(qubits, 3, batch_size) + num_samples = [[10, 10]] * batch_size with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'programs must be rank 1'): @@ -57,7 +58,8 @@ def test_noisy_expectation_inputs(self): noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor([circuit_batch]), symbol_names, symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'symbol_names must be rank 1.'): @@ -65,7 +67,8 @@ def test_noisy_expectation_inputs(self): noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), np.array([symbol_names]), symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'symbol_values must be rank 2.'): @@ -73,7 +76,8 @@ def test_noisy_expectation_inputs(self): noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), symbol_names, np.array([symbol_values_array]), - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'symbol_values must be rank 2.'): @@ -81,7 +85,8 @@ def test_noisy_expectation_inputs(self): noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array[0], - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'pauli_sums must be rank 2.'): @@ -89,7 +94,17 @@ def test_noisy_expectation_inputs(self): noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, - util.convert_to_tensor(list(pauli_sums)), num_samples) + util.convert_to_tensor(list(pauli_sums)), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'projector_sums must be rank 2.'): + # pauli_sums tensor has too few dimensions. + noisy_sampled_expectation_op.sampled_expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor(list(projector_sums))) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'pauli_sums must be rank 2.'): @@ -98,7 +113,17 @@ def test_noisy_expectation_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, [util.convert_to_tensor([[x] for x in pauli_sums])], - num_samples) + num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'projector_sums must be rank 2.'): + # pauli_sums tensor has too many dimensions. + noisy_sampled_expectation_op.sampled_expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + [util.convert_to_tensor([[x] for x in projector_sums])]) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'num_samples must be rank 2'): @@ -107,7 +132,8 @@ def test_noisy_expectation_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, util.convert_to_tensor([[x] for x in pauli_sums]), - [num_samples]) + [num_samples], + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'num_samples must be rank 2'): @@ -116,14 +142,16 @@ def test_noisy_expectation_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, util.convert_to_tensor([[x] for x in pauli_sums]), - num_samples[0]) + num_samples[0], + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'Unparseable proto'): # circuit tensor has the right type but invalid values. noisy_sampled_expectation_op.sampled_expectation( ['junk'] * batch_size, symbol_names, symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'Could not find symbol in parameter map'): @@ -131,7 +159,8 @@ def test_noisy_expectation_inputs(self): noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), ['junk'], symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'qubits not found in circuit'): @@ -142,40 +171,74 @@ def test_noisy_expectation_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, util.convert_to_tensor([[x] for x in new_pauli_sums]), - num_samples) + num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'qubits not found in circuit'): + # pauli_sums tensor has the right type but invalid values. + new_qubits = [cirq.GridQubit(5, 5), cirq.GridQubit(9, 9)] + new_projector_sums = util.random_projector_sums( + new_qubits, 2, batch_size) + noisy_sampled_expectation_op.sampled_expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in new_projector_sums])) + + with self.assertRaisesRegex(tf.errors.InvalidArgumentError, + 'Unparseable proto'): + # pauli_sums tensor has the right type but invalid values 2. + noisy_sampled_expectation_op.sampled_expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, [['junk']] * batch_size, num_samples, + util.convert_to_tensor([[x] for x in new_projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'Unparseable proto'): # pauli_sums tensor has the right type but invalid values 2. noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), symbol_names, - symbol_values_array, [['junk']] * batch_size, num_samples) + symbol_values_array, + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + [['junk']] * batch_size) with self.assertRaisesRegex(TypeError, 'Cannot convert'): # circuits tensor has the wrong type. noisy_sampled_expectation_op.sampled_expectation( [1.0] * batch_size, symbol_names, symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(TypeError, 'Cannot convert'): # symbol_names tensor has the wrong type. noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), [0.1234], symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.UnimplementedError, ''): # symbol_values tensor has the wrong type. noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), symbol_names, [['junk']] * batch_size, - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(TypeError, 'Cannot convert'): # pauli_sums tensor has the wrong type. noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), symbol_names, - symbol_values_array, [[1.0]] * batch_size, num_samples) + symbol_values_array, [[1.0]] * batch_size, num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) + + with self.assertRaisesRegex(TypeError, 'Cannot convert'): + # projector_sums tensor has the wrong type. + noisy_sampled_expectation_op.sampled_expectation( + util.convert_to_tensor(circuit_batch), symbol_names, + symbol_values_array, [[1.0]] * batch_size, num_samples, + util.convert_to_tensor([[x] for x in pauli_sums])) with self.assertRaisesRegex(TypeError, 'missing'): # we are missing an argument. @@ -190,8 +253,8 @@ def test_noisy_expectation_inputs(self): noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, - util.convert_to_tensor([[x] for x in pauli_sums]), [], - num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums]), []) # pylint: enable=too-many-function-args with self.assertRaisesRegex(tf.errors.InvalidArgumentError, @@ -200,7 +263,8 @@ def test_noisy_expectation_inputs(self): noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor([cirq.Circuit()]), symbol_names, symbol_values_array.astype(np.float64), - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) with self.assertRaisesRegex(tf.errors.InvalidArgumentError, 'greater than 0'): @@ -209,7 +273,8 @@ def test_noisy_expectation_inputs(self): util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, util.convert_to_tensor([[x] for x in pauli_sums]), - [[-1]] * batch_size) + [[-1]] * batch_size, + util.convert_to_tensor([[x] for x in projector_sums])) # pylint: enable=too-many-function-args with self.assertRaisesRegex(tf.errors.InvalidArgumentError, @@ -218,7 +283,8 @@ def test_noisy_expectation_inputs(self): noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array[:int(batch_size * 0.5)], - util.convert_to_tensor([[x] for x in pauli_sums]), num_samples) + util.convert_to_tensor([[x] for x in pauli_sums]), num_samples, + util.convert_to_tensor([[x] for x in projector_sums])) @parameterized.parameters([ { @@ -259,16 +325,24 @@ def test_simulate_consistency(self, batch_size, n_qubits, noisy): pauli_sums1 = util.random_pauli_sums(qubits, 3, batch_size) pauli_sums2 = util.random_pauli_sums(qubits, 3, batch_size) batch_pauli_sums = [[x, y] for x, y in zip(pauli_sums1, pauli_sums2)] - num_samples = [[10000] * 2] * batch_size + projector_sums1 = util.random_projector_sums(qubits, 3, batch_size) + projector_sums2 = util.random_projector_sums(qubits, 3, batch_size) + batch_projector_sums = [ + [x, y] for x, y in zip(projector_sums1, projector_sums2) + ] + batch_both = [[x, y, z, t] for x, y, z, t in zip( + pauli_sums1, pauli_sums2, projector_sums1, projector_sums2)] + num_samples = [[10000] * 4] * batch_size op_exps = noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, - util.convert_to_tensor(batch_pauli_sums), num_samples) + util.convert_to_tensor(batch_pauli_sums), num_samples, + util.convert_to_tensor(batch_projector_sums)) cirq_exps = batch_util.batch_calculate_expectation( - circuit_batch, resolver_batch, batch_pauli_sums, - cirq.DensityMatrixSimulator() if noisy else cirq.Simulator()) + circuit_batch, resolver_batch, batch_both, + cirq.DensityMatrixSimulator()) tol = 0.5 self.assertAllClose(cirq_exps, op_exps, atol=tol, rtol=tol) @@ -297,15 +371,23 @@ def test_single_channel(self, channel): pauli_sums1 = util.random_pauli_sums(qubits, 3, batch_size) pauli_sums2 = util.random_pauli_sums(qubits, 3, batch_size) batch_pauli_sums = [[x, y] for x, y in zip(pauli_sums1, pauli_sums2)] - num_samples = [[20000] * 2] * batch_size + projector_sums1 = util.random_projector_sums(qubits, 3, batch_size) + projector_sums2 = util.random_projector_sums(qubits, 3, batch_size) + batch_projector_sums = [ + [x, y] for x, y in zip(projector_sums1, projector_sums2) + ] + batch_both = [[x, y, z, t] for x, y, z, t in zip( + pauli_sums1, pauli_sums2, projector_sums1, projector_sums2)] + num_samples = [[20000] * 4] * batch_size op_exps = noisy_sampled_expectation_op.sampled_expectation( util.convert_to_tensor(circuit_batch), symbol_names, symbol_values_array, - util.convert_to_tensor(batch_pauli_sums), num_samples) + util.convert_to_tensor(batch_pauli_sums), num_samples, + util.convert_to_tensor(batch_projector_sums)) cirq_exps = batch_util.batch_calculate_expectation( - circuit_batch, resolver_batch, batch_pauli_sums, + circuit_batch, resolver_batch, batch_both, cirq.DensityMatrixSimulator()) self.assertAllClose(cirq_exps, op_exps, atol=0.35, rtol=0.35) @@ -316,11 +398,12 @@ def test_correctness_empty(self): empty_symbols = tf.convert_to_tensor([], dtype=tf.dtypes.string) empty_values = tf.convert_to_tensor([[]]) empty_paulis = tf.convert_to_tensor([[]], dtype=tf.dtypes.string) + empty_projectors = tf.convert_to_tensor([[]], dtype=tf.dtypes.string) empty_n_samples = tf.convert_to_tensor([[]], dtype=tf.int32) out = noisy_sampled_expectation_op.sampled_expectation( empty_circuit, empty_symbols, empty_values, empty_paulis, - empty_n_samples) + empty_n_samples, empty_projectors) expected = np.array([[]], dtype=np.complex64) self.assertAllClose(out, expected) @@ -331,11 +414,12 @@ def test_correctness_no_circuit(self): empty_symbols = tf.raw_ops.Empty(shape=(0,), dtype=tf.string) empty_values = tf.raw_ops.Empty(shape=(0, 0), dtype=tf.float32) empty_paulis = tf.raw_ops.Empty(shape=(0, 0), dtype=tf.string) + empty_projectors = tf.raw_ops.Empty(shape=(0, 0), dtype=tf.string) empty_n_samples = tf.raw_ops.Empty(shape=(0, 0), dtype=tf.int32) out = noisy_sampled_expectation_op.sampled_expectation( empty_circuit, empty_symbols, empty_values, empty_paulis, - empty_n_samples) + empty_n_samples, empty_projectors) self.assertShapeEqual(np.zeros((0, 0)), out) diff --git a/tensorflow_quantum/core/ops/noise/tfq_noisy_expectation.cc b/tensorflow_quantum/core/ops/noise/tfq_noisy_expectation.cc index 88b78166e..33308d1f3 100644 --- a/tensorflow_quantum/core/ops/noise/tfq_noisy_expectation.cc +++ b/tensorflow_quantum/core/ops/noise/tfq_noisy_expectation.cc @@ -48,6 +48,7 @@ namespace tfq { using ::tensorflow::Status; using ::tfq::proto::PauliSum; using ::tfq::proto::Program; +using ::tfq::proto::ProjectorSum; typedef qsim::Cirq::GateCirq QsimGate; typedef qsim::Circuit QsimCircuit; @@ -61,13 +62,14 @@ class TfqNoisyExpectationOp : public tensorflow::OpKernel { void Compute(tensorflow::OpKernelContext* context) override { // TODO (mbbrough): add more dimension checks for other inputs here. const int num_inputs = context->num_inputs(); - OP_REQUIRES(context, num_inputs == 5, + OP_REQUIRES(context, num_inputs == 6, tensorflow::errors::InvalidArgument(absl::StrCat( - "Expected 5 inputs, got ", num_inputs, " inputs."))); + "Expected 6 inputs, got ", num_inputs, " inputs."))); // Create the output Tensor. const int output_dim_batch_size = context->input(0).dim_size(0); - const int output_dim_op_size = context->input(3).dim_size(1); + const int output_dim_op_size = + context->input(3).dim_size(1) + context->input(4).dim_size(1); tensorflow::TensorShape output_shape; output_shape.AddDim(output_dim_batch_size); output_shape.AddDim(output_dim_op_size); @@ -79,8 +81,10 @@ class TfqNoisyExpectationOp : public tensorflow::OpKernel { std::vector programs; std::vector num_qubits; std::vector> pauli_sums; - OP_REQUIRES_OK(context, GetProgramsAndNumQubits(context, &programs, - &num_qubits, &pauli_sums)); + std::vector> projector_sums; + OP_REQUIRES_OK(context, + GetProgramsAndNumQubits(context, &programs, &num_qubits, + &pauli_sums, &projector_sums)); std::vector maps; OP_REQUIRES_OK(context, GetSymbolMaps(context, &maps)); @@ -101,11 +105,22 @@ class TfqNoisyExpectationOp : public tensorflow::OpKernel { pauli_sums.size(), " lists of pauli sums."))); OP_REQUIRES( - context, context->input(4).dim_size(1) == context->input(3).dim_size(1), + context, num_samples.size() == projector_sums.size(), tensorflow::errors::InvalidArgument(absl::StrCat( - "Dimension 1 of num_samples and pauli_sums do not match.", "Got ", - context->input(4).dim_size(1), " lists of sample sizes and ", - context->input(3).dim_size(1), " lists of pauli sums."))); + "Dimension 0 of num_samples and projector_sums do not match.", + "Got ", num_samples.size(), " lists of sample sizes and ", + projector_sums.size(), " lists of projector sums."))); + + OP_REQUIRES( + context, + context->input(5).dim_size(1) == + context->input(3).dim_size(1) + context->input(4).dim_size(1), + tensorflow::errors::InvalidArgument(absl::StrCat( + "Dimension 1 of num_samples and pauli_sums + projector_sums do ", + "not match. Got ", context->input(5).dim_size(1), + " lists of sample sizes and ", context->input(3).dim_size(1), + " lists of pauli sums and ", context->input(4).dim_size(1), + " lists of projector sums."))); // Construct qsim circuits. std::vector qsim_circuits(programs.size(), @@ -141,23 +156,25 @@ class TfqNoisyExpectationOp : public tensorflow::OpKernel { // alternate parallelization scheme with runtime: // O(n_circuits * max_j(num_samples[i])) with parallelization being // multiple threads per wavefunction. - ComputeLarge(num_qubits, qsim_circuits, pauli_sums, num_samples, context, - &output_tensor); + ComputeLarge(num_qubits, qsim_circuits, pauli_sums, projector_sums, + num_samples, context, &output_tensor); } else { // Runtime: O(n_circuits * max_j(num_samples[i])) with parallelization // being done over number of trials. ComputeSmall(num_qubits, max_num_qubits, qsim_circuits, pauli_sums, - num_samples, context, &output_tensor); + projector_sums, num_samples, context, &output_tensor); } } private: - void ComputeLarge(const std::vector& num_qubits, - const std::vector& ncircuits, - const std::vector>& pauli_sums, - const std::vector>& num_samples, - tensorflow::OpKernelContext* context, - tensorflow::TTypes::Matrix* output_tensor) { + void ComputeLarge( + const std::vector& num_qubits, + const std::vector& ncircuits, + const std::vector>& pauli_sums, + const std::vector>& projector_sums, + const std::vector>& num_samples, + tensorflow::OpKernelContext* context, + tensorflow::TTypes::Matrix* output_tensor) { // Instantiate qsim objects. const auto tfq_for = tfq::QsimFor(context); using Simulator = qsim::Simulator; @@ -210,6 +227,8 @@ class TfqNoisyExpectationOp : public tensorflow::OpKernel { param.normalize_before_mea_gates = true; std::vector unused_stats; // Track op-wise stats. + CHECK_EQ(num_samples[i].size(), + pauli_sums[i].size() + projector_sums[i].size()); std::vector run_samples(num_samples[i].size(), 0); std::vector rolling_sums(num_samples[i].size(), 0.0); @@ -231,6 +250,18 @@ class TfqNoisyExpectationOp : public tensorflow::OpKernel { rolling_sums[j] += static_cast(exp_v); run_samples[j]++; } + for (int j = 0; j < projector_sums[i].size(); j++) { + if (run_samples[j + pauli_sums[i].size()] >= + num_samples[i][j + pauli_sums[i].size()]) { + continue; + } + float exp_v = 0.0; + OP_REQUIRES_OK( + context, ComputeExpectationQsim(projector_sums[i][j], sim, ss, sv, + scratch, &exp_v)); + rolling_sums[j + pauli_sums[i].size()] += static_cast(exp_v); + run_samples[j + pauli_sums[i].size()]++; + } bool break_loop = true; for (int j = 0; j < num_samples[i].size(); j++) { if (run_samples[j] < num_samples[i][j]) { @@ -249,13 +280,14 @@ class TfqNoisyExpectationOp : public tensorflow::OpKernel { } } - void ComputeSmall(const std::vector& num_qubits, - const int max_num_qubits, - const std::vector& ncircuits, - const std::vector>& pauli_sums, - const std::vector>& num_samples, - tensorflow::OpKernelContext* context, - tensorflow::TTypes::Matrix* output_tensor) { + void ComputeSmall( + const std::vector& num_qubits, const int max_num_qubits, + const std::vector& ncircuits, + const std::vector>& pauli_sums, + const std::vector>& projector_sums, + const std::vector>& num_samples, + tensorflow::OpKernelContext* context, + tensorflow::TTypes::Matrix* output_tensor) { using Simulator = qsim::Simulator; using StateSpace = Simulator::StateSpace; using QTSimulator = @@ -327,6 +359,8 @@ class TfqNoisyExpectationOp : public tensorflow::OpKernel { param.normalize_before_mea_gates = true; std::vector unused_stats; // Track op-wise stats. + CHECK_EQ(num_samples[i].size(), + pauli_sums[i].size() + projector_sums[i].size()); std::vector run_samples(num_samples[i].size(), 0); std::vector rolling_sums(num_samples[i].size(), 0.0); @@ -352,6 +386,24 @@ class TfqNoisyExpectationOp : public tensorflow::OpKernel { run_samples[j]++; } + for (int j = 0; j < projector_sums[i].size(); j++) { + int p_reps = + (num_samples[i][j + pauli_sums[i].size()] + num_threads - 1) / + num_threads; + if (run_samples[j + pauli_sums[i].size()] >= p_reps + rep_offset) { + continue; + } + float exp_v = 0.0; + NESTED_FN_STATUS_SYNC( + compute_status, + ComputeExpectationQsim(projector_sums[i][j], sim, ss, sv, + scratch, &exp_v), + c_lock); + rolling_sums[j + pauli_sums[i].size()] += + static_cast(exp_v); + run_samples[j + pauli_sums[i].size()]++; + } + // Check if we have run enough trajectories for all ops. bool break_loop = true; for (int j = 0; j < num_samples[i].size(); j++) { @@ -394,6 +446,7 @@ REGISTER_OP("TfqNoisyExpectation") .Input("symbol_names: string") .Input("symbol_values: float") .Input("pauli_sums: string") + .Input("projector_sums: string") .Input("num_samples: int32") .Output("expectations: float") .SetShapeFn([](tensorflow::shape_inference::InferenceContext* c) { @@ -409,8 +462,11 @@ REGISTER_OP("TfqNoisyExpectation") tensorflow::shape_inference::ShapeHandle pauli_sums_shape; TF_RETURN_IF_ERROR(c->WithRank(c->input(3), 2, &pauli_sums_shape)); + tensorflow::shape_inference::ShapeHandle projector_sums_shape; + TF_RETURN_IF_ERROR(c->WithRank(c->input(4), 2, &projector_sums_shape)); + tensorflow::shape_inference::ShapeHandle num_samples_shape; - TF_RETURN_IF_ERROR(c->WithRank(c->input(4), 2, &num_samples_shape)); + TF_RETURN_IF_ERROR(c->WithRank(c->input(5), 2, &num_samples_shape)); tensorflow::shape_inference::DimensionHandle output_rows = c->Dim(programs_shape, 0); diff --git a/tensorflow_quantum/core/ops/noise/tfq_noisy_sampled_expectation.cc b/tensorflow_quantum/core/ops/noise/tfq_noisy_sampled_expectation.cc index 77d6197ae..b99246838 100644 --- a/tensorflow_quantum/core/ops/noise/tfq_noisy_sampled_expectation.cc +++ b/tensorflow_quantum/core/ops/noise/tfq_noisy_sampled_expectation.cc @@ -41,6 +41,7 @@ limitations under the License. #include "tensorflow_quantum/core/ops/parse_context.h" #include "tensorflow_quantum/core/proto/pauli_sum.pb.h" #include "tensorflow_quantum/core/proto/program.pb.h" +#include "tensorflow_quantum/core/proto/projector_sum.pb.h" #include "tensorflow_quantum/core/src/util_qsim.h" namespace tfq { @@ -48,6 +49,7 @@ namespace tfq { using ::tensorflow::Status; using ::tfq::proto::PauliSum; using ::tfq::proto::Program; +using ::tfq::proto::ProjectorSum; typedef qsim::Cirq::GateCirq QsimGate; typedef qsim::Circuit QsimCircuit; @@ -62,13 +64,14 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { void Compute(tensorflow::OpKernelContext* context) override { // TODO (mbbrough): add more dimension checks for other inputs here. const int num_inputs = context->num_inputs(); - OP_REQUIRES(context, num_inputs == 5, + OP_REQUIRES(context, num_inputs == 6, tensorflow::errors::InvalidArgument(absl::StrCat( - "Expected 5 inputs, got ", num_inputs, " inputs."))); + "Expected 6 inputs, got ", num_inputs, " inputs."))); // Create the output Tensor. const int output_dim_batch_size = context->input(0).dim_size(0); - const int output_dim_op_size = context->input(3).dim_size(1); + const int output_dim_op_size = + context->input(3).dim_size(1) + context->input(4).dim_size(1); tensorflow::TensorShape output_shape; output_shape.AddDim(output_dim_batch_size); output_shape.AddDim(output_dim_op_size); @@ -80,8 +83,10 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { std::vector programs; std::vector num_qubits; std::vector> pauli_sums; - OP_REQUIRES_OK(context, GetProgramsAndNumQubits(context, &programs, - &num_qubits, &pauli_sums)); + std::vector> projector_sums; + OP_REQUIRES_OK(context, + GetProgramsAndNumQubits(context, &programs, &num_qubits, + &pauli_sums, &projector_sums)); std::vector maps; OP_REQUIRES_OK(context, GetSymbolMaps(context, &maps)); @@ -102,11 +107,22 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { pauli_sums.size(), " lists of pauli sums."))); OP_REQUIRES( - context, context->input(4).dim_size(1) == context->input(3).dim_size(1), + context, num_samples.size() == projector_sums.size(), tensorflow::errors::InvalidArgument(absl::StrCat( - "Dimension 1 of num_samples and pauli_sums do not match.", "Got ", - context->input(4).dim_size(1), " lists of sample sizes and ", - context->input(3).dim_size(1), " lists of pauli sums."))); + "Dimension 0 of num_samples and projector_sums do not match.", + "Got ", num_samples.size(), " lists of sample sizes and ", + projector_sums.size(), " lists of projector sums."))); + + OP_REQUIRES( + context, + context->input(5).dim_size(1) == + context->input(3).dim_size(1) + context->input(4).dim_size(1), + tensorflow::errors::InvalidArgument(absl::StrCat( + "Dimension 1 of num_samples and pauli_sums + projector_sums do ", + "not match. Got ", context->input(5).dim_size(1), + " lists of sample sizes and ", context->input(3).dim_size(1), + " lists of pauli sums and ", context->input(4).dim_size(1), + " lists of projector sums."))); // Construct qsim circuits. std::vector qsim_circuits(programs.size(), @@ -142,23 +158,25 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { // alternate parallelization scheme with runtime: // O(n_circuits * max_j(num_samples[i])) with parallelization being // multiple threads per wavefunction. - ComputeLarge(num_qubits, qsim_circuits, pauli_sums, num_samples, context, - &output_tensor); + ComputeLarge(num_qubits, qsim_circuits, pauli_sums, projector_sums, + num_samples, context, &output_tensor); } else { // Runtime: O(n_circuits * max_j(num_samples[i])) with parallelization // being done over number of trials. ComputeSmall(num_qubits, max_num_qubits, qsim_circuits, pauli_sums, - num_samples, context, &output_tensor); + projector_sums, num_samples, context, &output_tensor); } } private: - void ComputeLarge(const std::vector& num_qubits, - const std::vector& ncircuits, - const std::vector>& pauli_sums, - const std::vector>& num_samples, - tensorflow::OpKernelContext* context, - tensorflow::TTypes::Matrix* output_tensor) { + void ComputeLarge( + const std::vector& num_qubits, + const std::vector& ncircuits, + const std::vector>& pauli_sums, + const std::vector>& projector_sums, + const std::vector>& num_samples, + tensorflow::OpKernelContext* context, + tensorflow::TTypes::Matrix* output_tensor) { // Instantiate qsim objects. const auto tfq_for = tfq::QsimFor(context); using Simulator = qsim::Simulator; @@ -184,6 +202,14 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { max_n_shots = std::max(max_n_shots, num_samples[i][j]); } } + for (int i = 0; i < projector_sums.size(); i++) { + for (int j = 0; j < projector_sums[i].size(); j++) { + max_psum_length = + std::max(max_psum_length, projector_sums[i][j].terms().size()); + max_n_shots = + std::max(max_n_shots, num_samples[i][j + pauli_sums[i].size()]); + } + } random_gen.Init(tensorflow::random::New64(), tensorflow::random::New64()); auto local_gen = random_gen.ReserveSamples128( ncircuits.size() * (1 + max_psum_length) * max_n_shots); @@ -214,6 +240,8 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { param.normalize_before_mea_gates = true; std::vector unused_stats; // Track op-wise stats. + CHECK_EQ(num_samples[i].size(), + pauli_sums[i].size() + projector_sums[i].size()); std::vector run_samples(num_samples[i].size(), 0); std::vector rolling_sums(num_samples[i].size(), 0.0); @@ -235,6 +263,18 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { rolling_sums[j] += static_cast(exp_v); run_samples[j]++; } + for (int j = 0; j < projector_sums[i].size(); j++) { + if (run_samples[j + pauli_sums[i].size()] >= + num_samples[i][j + pauli_sums[i].size()]) { + continue; + } + float exp_v = 0.0; + OP_REQUIRES_OK(context, ComputeSampledExpectationQsim( + projector_sums[i][j], sim, ss, sv, + scratch, 1, rand_source, &exp_v)); + rolling_sums[j + pauli_sums[i].size()] += static_cast(exp_v); + run_samples[j + pauli_sums[i].size()]++; + } bool break_loop = true; for (int j = 0; j < num_samples[i].size(); j++) { if (run_samples[j] < num_samples[i][j]) { @@ -253,13 +293,14 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { } } - void ComputeSmall(const std::vector& num_qubits, - const int max_num_qubits, - const std::vector& ncircuits, - const std::vector>& pauli_sums, - const std::vector>& num_samples, - tensorflow::OpKernelContext* context, - tensorflow::TTypes::Matrix* output_tensor) { + void ComputeSmall( + const std::vector& num_qubits, const int max_num_qubits, + const std::vector& ncircuits, + const std::vector>& pauli_sums, + const std::vector>& projector_sums, + const std::vector>& num_samples, + tensorflow::OpKernelContext* context, + tensorflow::TTypes::Matrix* output_tensor) { using Simulator = qsim::Simulator; using StateSpace = Simulator::StateSpace; using QTSimulator = @@ -292,6 +333,14 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { max_n_shots = std::max(max_n_shots, num_samples[i][j]); } } + for (int i = 0; i < projector_sums.size(); i++) { + for (int j = 0; j < projector_sums[i].size(); j++) { + max_psum_length = + std::max(max_psum_length, projector_sums[i][j].terms().size()); + max_n_shots = + std::max(max_n_shots, num_samples[i][j + pauli_sums[i].size()]); + } + } random_gen.Init(tensorflow::random::New64(), tensorflow::random::New64()); Status compute_status = Status::OK(); @@ -333,6 +382,8 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { param.normalize_before_mea_gates = true; std::vector unused_stats; // Track op-wise stats. + CHECK_EQ(num_samples[i].size(), + pauli_sums[i].size() + projector_sums[i].size()); std::vector run_samples(num_samples[i].size(), 0); std::vector rolling_sums(num_samples[i].size(), 0.0); @@ -357,6 +408,23 @@ class TfqNoisySampledExpectationOp : public tensorflow::OpKernel { rolling_sums[j] += static_cast(exp_v); run_samples[j]++; } + for (int j = 0; j < projector_sums[i].size(); j++) { + int p_reps = + (num_samples[i][j + pauli_sums[i].size()] + num_threads - 1) / + num_threads; + if (run_samples[j + pauli_sums[i].size()] >= p_reps + rep_offset) { + continue; + } + float exp_v = 0.0; + NESTED_FN_STATUS_SYNC( + compute_status, + ComputeSampledExpectationQsim(projector_sums[i][j], sim, ss, sv, + scratch, 1, rand_source, &exp_v), + c_lock); + rolling_sums[j + pauli_sums[i].size()] += + static_cast(exp_v); + run_samples[j + pauli_sums[i].size()]++; + } // Check if we have run enough trajectories for all ops. bool break_loop = true; @@ -400,6 +468,7 @@ REGISTER_OP("TfqNoisySampledExpectation") .Input("symbol_names: string") .Input("symbol_values: float") .Input("pauli_sums: string") + .Input("projector_sums: string") .Input("num_samples: int32") .Output("expectations: float") .SetShapeFn([](tensorflow::shape_inference::InferenceContext* c) { @@ -415,8 +484,11 @@ REGISTER_OP("TfqNoisySampledExpectation") tensorflow::shape_inference::ShapeHandle pauli_sums_shape; TF_RETURN_IF_ERROR(c->WithRank(c->input(3), 2, &pauli_sums_shape)); + tensorflow::shape_inference::ShapeHandle projector_sums_shape; + TF_RETURN_IF_ERROR(c->WithRank(c->input(4), 2, &projector_sums_shape)); + tensorflow::shape_inference::ShapeHandle num_samples_shape; - TF_RETURN_IF_ERROR(c->WithRank(c->input(4), 2, &num_samples_shape)); + TF_RETURN_IF_ERROR(c->WithRank(c->input(5), 2, &num_samples_shape)); tensorflow::shape_inference::DimensionHandle output_rows = c->Dim(programs_shape, 0); diff --git a/tensorflow_quantum/core/ops/parse_context.cc b/tensorflow_quantum/core/ops/parse_context.cc index dd8f941fc..4f3c30be4 100644 --- a/tensorflow_quantum/core/ops/parse_context.cc +++ b/tensorflow_quantum/core/ops/parse_context.cc @@ -37,6 +37,7 @@ using ::tensorflow::Status; using ::tensorflow::Tensor; using ::tfq::proto::PauliSum; using ::tfq::proto::Program; +using ::tfq::proto::ProjectorSum; template Status ParseProto(const std::string& text, T* proto) { @@ -151,10 +152,12 @@ Status GetProgramsAndNumQubits( OpKernelContext* context, std::vector* programs, std::vector* num_qubits, std::vector>* p_sums /*=nullptr*/, + std::vector>* proj_sums /*=nullptr*/, bool swap_endianness /*=false*/) { // 1. Parse input programs // 2. (Optional) Parse input PauliSums - // 3. Convert GridQubit locations to integers. + // 3. (Optional) Parse input ProjectorSums + // 4. Convert GridQubit locations to integers. Status status = ParsePrograms(context, "programs", programs); if (!status.ok()) { return status; @@ -174,19 +177,37 @@ Status GetProgramsAndNumQubits( } } + if (proj_sums) { + status = GetProjectorSums(context, proj_sums); + if (!status.ok()) { + return status; + } + if (programs->size() != proj_sums->size()) { + return Status( + tensorflow::error::INVALID_ARGUMENT, + absl::StrCat( + "Number of circuits and ProjectorSums do not match. Got ", + programs->size(), " circuits and ", proj_sums->size(), + " projectorsums.")); + } + } + // Resolve qubit ID's in parallel. num_qubits->assign(programs->size(), -1); auto DoWork = [&](int start, int end) { for (int i = start; i < end; i++) { Program& program = (*programs)[i]; unsigned int this_num_qubits; - if (p_sums) { + if (p_sums || proj_sums) { + auto iter_p_sums = p_sums ? &(p_sums->at(i)) : nullptr; + auto iter_proj_sums = proj_sums ? &(proj_sums->at(i)) : nullptr; OP_REQUIRES_OK(context, - ResolveQubitIds(&program, &this_num_qubits, - &(p_sums->at(i)), swap_endianness)); + ResolveQubitIds(&program, &this_num_qubits, iter_p_sums, + iter_proj_sums, swap_endianness)); } else { - OP_REQUIRES_OK(context, ResolveQubitIds(&program, &this_num_qubits, - nullptr, swap_endianness)); + OP_REQUIRES_OK(context, + ResolveQubitIds(&program, &this_num_qubits, nullptr, + nullptr, swap_endianness)); } (*num_qubits)[i] = this_num_qubits; } @@ -281,6 +302,44 @@ Status GetPauliSums(OpKernelContext* context, return Status::OK(); } +Status GetProjectorSums(OpKernelContext* context, + std::vector>* proj_sums) { + // 1. Parses ProjectorSum proto. + const Tensor* input; + Status status = context->input("projector_sums", &input); + if (!status.ok()) { + return status; + } + + if (input->dims() != 2) { + return Status(tensorflow::error::INVALID_ARGUMENT, + absl::StrCat("projector_sums must be rank 2. Got rank ", + input->dims(), ".")); + } + + const auto sum_specs = input->matrix(); + proj_sums->assign( + sum_specs.dimension(0), + std::vector(sum_specs.dimension(1), ProjectorSum())); + const int op_dim = sum_specs.dimension(1); + auto DoWork = [&](int start, int end) { + for (int ii = start; ii < end; ii++) { + const int i = ii / op_dim; + const int j = ii % op_dim; + ProjectorSum p; + OP_REQUIRES_OK(context, ParseProto(sum_specs(i, j), &p)); + (*proj_sums)[i][j] = p; + } + }; + + // TODO(mbbrough): Determine if this is a good cycle estimate. + const int cycle_estimate = 1000; + context->device()->tensorflow_cpu_worker_threads()->workers->ParallelFor( + sum_specs.dimension(0) * sum_specs.dimension(1), cycle_estimate, DoWork); + + return Status::OK(); +} + Status GetSymbolMaps(OpKernelContext* context, std::vector* maps) { // 1. Convert to dictionary representation for param resolution. const Tensor* input_names; @@ -364,7 +423,7 @@ tensorflow::Status GetNumSamples( } sub_parsed_num_samples.push_back(num_samples); } - parsed_num_samples->push_back(sub_parsed_num_samples); + parsed_num_samples->emplace_back(sub_parsed_num_samples); } return Status::OK(); diff --git a/tensorflow_quantum/core/ops/parse_context.h b/tensorflow_quantum/core/ops/parse_context.h index c811b68c5..bf169e77c 100644 --- a/tensorflow_quantum/core/ops/parse_context.h +++ b/tensorflow_quantum/core/ops/parse_context.h @@ -33,6 +33,7 @@ limitations under the License. #include "tensorflow/core/lib/core/status.h" #include "tensorflow_quantum/core/proto/pauli_sum.pb.h" #include "tensorflow_quantum/core/proto/program.pb.h" +#include "tensorflow_quantum/core/proto/projector_sum.pb.h" namespace tfq { @@ -65,6 +66,7 @@ tensorflow::Status GetProgramsAndNumQubits( tensorflow::OpKernelContext* context, std::vector* programs, std::vector* num_qubits, std::vector>* p_sums = nullptr, + std::vector>* proj_sums = nullptr, bool swap_endianness = false); // Parses Cirq Program protos out of the 'circuit_specs' input Tensor. Also @@ -83,6 +85,13 @@ tensorflow::Status GetPauliSums( tensorflow::OpKernelContext* context, std::vector>* p_sums); +// Parses PauliSum protos out of the 'projector_sums' input tensor. Note this +// function does NOT resolve QubitID's as any projectorsum needs a reference +// program to "discover" all of the active qubits and define the ordering. +tensorflow::Status GetProjectorSums( + tensorflow::OpKernelContext* context, + std::vector>* proj_sums); + // Parses the input context to construct the SymbolMaps for the entire batch. // The two input Tensors are expected to be of size: // diff --git a/tensorflow_quantum/core/src/circuit_parser_qsim.cc b/tensorflow_quantum/core/src/circuit_parser_qsim.cc index 8a7b2d490..06402b8c6 100644 --- a/tensorflow_quantum/core/src/circuit_parser_qsim.cc +++ b/tensorflow_quantum/core/src/circuit_parser_qsim.cc @@ -41,6 +41,7 @@ using ::tfq::proto::Moment; using ::tfq::proto::Operation; using ::tfq::proto::PauliTerm; using ::tfq::proto::Program; +using ::tfq::proto::ProjectorTerm; namespace { @@ -450,6 +451,39 @@ inline Status PhasedXGate(const Operation& op, const SymbolMap& param_map, return Status::OK(); } +inline Status MatrixGate1(const Operation& op, const SymbolMap& param_map, + const unsigned int num_qubits, + const unsigned int time, QsimCircuit* circuit, + std::vector* metadata) { + int q0; + bool unused; + // float pexp, pexp_s, exp, exp_s, gs; + Status u; + unused = absl::SimpleAtoi(op.qubits(0).id(), &q0); + + std::vector matrix; + matrix.reserve(8); + + for (const char* param_name : + {"x00", "y00", "x01", "y01", "x10", "y10", "x11", "y11"}) { + float param_value; + u = ParseProtoArg(op, param_name, param_map, ¶m_value); + if (!u.ok()) { + return u; + } + matrix.push_back(param_value); + } + auto gate = + qsim::Cirq::MatrixGate1::Create(time, num_qubits - q0 - 1, matrix); + Status s = OptionalInsertControls(op, num_qubits, &gate); + if (!s.ok()) { + return s; + } + circuit->gates.push_back(gate); + + return Status::OK(); +} + // two qubit fsim -> Create(time, q0, q1, theta, phi) inline Status FsimGate(const Operation& op, const SymbolMap& param_map, const unsigned int num_qubits, const unsigned int time, @@ -578,14 +612,15 @@ tensorflow::Status ParseAppendGate(const Operation& op, std::function*)>> - func_map = {{"I", &IGate}, {"HP", &HGate}, - {"XP", &XGate}, {"XXP", &XXGate}, - {"YP", &YGate}, {"YYP", &YYGate}, - {"ZP", &ZGate}, {"ZZP", &ZZGate}, - {"CZP", &CZGate}, {"I2", &I2Gate}, - {"CNP", &CXGate}, {"SP", &SwapGate}, - {"ISP", &ISwapGate}, {"PXP", &PhasedXGate}, - {"FSIM", &FsimGate}, {"PISP", &PhasedISwapGate}}; + func_map = {{"I", &IGate}, {"HP", &HGate}, + {"XP", &XGate}, {"XXP", &XXGate}, + {"YP", &YGate}, {"YYP", &YYGate}, + {"ZP", &ZGate}, {"ZZP", &ZZGate}, + {"CZP", &CZGate}, {"I2", &I2Gate}, + {"CNP", &CXGate}, {"SP", &SwapGate}, + {"ISP", &ISwapGate}, {"PXP", &PhasedXGate}, + {"FSIM", &FsimGate}, {"PISP", &PhasedISwapGate}, + {"MG1", &MatrixGate1}}; auto build_f = func_map.find(op.gate().id()); if (build_f == func_map.end()) { @@ -885,7 +920,7 @@ Status QsimCircuitFromPauliTerm( // create corresponding eigen gate op. new_op->add_qubits()->set_id(pair.qubit_id()); - new_op->mutable_gate()->set_id(pair.pauli_type() + "P"); + new_op->mutable_gate()->set_id(absl::StrCat(pair.pauli_type(), "P")); (*new_op->mutable_args())["exponent"].mutable_arg_value()->set_float_value( 1.0); (*new_op->mutable_args())["global_shift"] @@ -906,6 +941,40 @@ Status QsimCircuitFromPauliTerm( circuit, fused_circuit); } +Status QsimCircuitFromProjectorTerm( + const ProjectorTerm& term, const int num_qubits, QsimCircuit* circuit, + std::vector>* fused_circuit) { + Program measurement_program; + SymbolMap empty_map; + measurement_program.mutable_circuit()->set_scheduling_strategy( + tfq::proto::Circuit::MOMENT_BY_MOMENT); + Moment* term_moment = measurement_program.mutable_circuit()->add_moments(); + for (const tfq::proto::ProjectorDictEntry& entry : term.projector_dict()) { + Operation* new_op = term_moment->add_operations(); + + // create corresponding eigen gate op. + new_op->add_qubits()->set_id(entry.qubit_id()); + new_op->mutable_gate()->set_id("MG1"); + auto& mutable_args = *new_op->mutable_args(); + mutable_args["x00"].mutable_arg_value()->set_float_value( + entry.basis_state() ? 0.0 : 1.0); + mutable_args["y00"].mutable_arg_value()->set_float_value(0.0); + mutable_args["x01"].mutable_arg_value()->set_float_value(0.0); + mutable_args["y01"].mutable_arg_value()->set_float_value(0.0); + mutable_args["x10"].mutable_arg_value()->set_float_value(0.0); + mutable_args["y10"].mutable_arg_value()->set_float_value(0.0); + mutable_args["x11"].mutable_arg_value()->set_float_value( + entry.basis_state() ? 1.0 : 0.0); + mutable_args["y11"].mutable_arg_value()->set_float_value(0.0); + + mutable_args["control_values"].mutable_arg_value()->set_string_value(""); + mutable_args["control_qubits"].mutable_arg_value()->set_string_value(""); + } + + return QsimCircuitFromProgram(measurement_program, empty_map, num_qubits, + circuit, fused_circuit); +} + Status QsimZBasisCircuitFromPauliTerm( const PauliTerm& term, const int num_qubits, QsimCircuit* circuit, std::vector>* fused_circuit) { @@ -956,4 +1025,49 @@ Status QsimZBasisCircuitFromPauliTerm( circuit, fused_circuit); } +Status QsimZBasisCircuitFromProjectorTerm( + const ProjectorTerm& term, const int num_qubits, QsimCircuit* circuit, + std::vector>* fused_circuit) { + Program measurement_program; + SymbolMap empty_map; + measurement_program.mutable_circuit()->set_scheduling_strategy( + tfq::proto::Circuit::MOMENT_BY_MOMENT); + + std::map> qid_to_counts; + for (const tfq::proto::ProjectorDictEntry& entry : term.projector_dict()) { + auto& counts = qid_to_counts[entry.qubit_id()]; + if (entry.basis_state()) { + counts.first += 1; + } else { + counts.second += 1; + } + } + + Moment* term_moment = measurement_program.mutable_circuit()->add_moments(); + for (const auto& count_pair : qid_to_counts) { + Operation* new_op = term_moment->add_operations(); + // create corresponding eigen gate op. + new_op->add_qubits()->set_id(count_pair.first); + new_op->mutable_gate()->set_id("MG1"); + auto& mutable_args = *new_op->mutable_args(); + mutable_args["x00"].mutable_arg_value()->set_float_value( + count_pair.second.first); + mutable_args["y00"].mutable_arg_value()->set_float_value(0.0); + mutable_args["x01"].mutable_arg_value()->set_float_value( + count_pair.second.second); + mutable_args["y01"].mutable_arg_value()->set_float_value(0.0); + mutable_args["x10"].mutable_arg_value()->set_float_value( + count_pair.second.second); + mutable_args["y10"].mutable_arg_value()->set_float_value(0.0); + mutable_args["x11"].mutable_arg_value()->set_float_value( + count_pair.second.first); + mutable_args["y11"].mutable_arg_value()->set_float_value(0.0); + mutable_args["control_values"].mutable_arg_value()->set_string_value(""); + mutable_args["control_qubits"].mutable_arg_value()->set_string_value(""); + } + + return QsimCircuitFromProgram(measurement_program, empty_map, num_qubits, + circuit, fused_circuit); +} + } // namespace tfq diff --git a/tensorflow_quantum/core/src/circuit_parser_qsim.h b/tensorflow_quantum/core/src/circuit_parser_qsim.h index e2966407d..6f920baa9 100644 --- a/tensorflow_quantum/core/src/circuit_parser_qsim.h +++ b/tensorflow_quantum/core/src/circuit_parser_qsim.h @@ -27,6 +27,7 @@ limitations under the License. #include "tensorflow/core/lib/core/status.h" #include "tensorflow_quantum/core/proto/pauli_sum.pb.h" #include "tensorflow_quantum/core/proto/program.pb.h" +#include "tensorflow_quantum/core/proto/projector_sum.pb.h" namespace tfq { @@ -93,6 +94,13 @@ tensorflow::Status QsimCircuitFromPauliTerm( qsim::Circuit>* circuit, std::vector>>* fused_circuit); +// parse a serialized projectorTerm from a larger cirq.ProjectorSum proto +// into a qsim Circuit and fused circuit. +tensorflow::Status QsimCircuitFromProjectorTerm( + const tfq::proto::ProjectorTerm& term, const int num_qubits, + qsim::Circuit>* circuit, + std::vector>>* fused_circuit); + // parse a serialized pauliTerm from a larger cirq.Paulisum proto // into a qsim Circuit and fused circuit that represents the transformation // to the z basis. @@ -101,6 +109,14 @@ tensorflow::Status QsimZBasisCircuitFromPauliTerm( qsim::Circuit>* circuit, std::vector>>* fused_circuit); +// parse a serialized projectorTerm from a larger cirq.Projectorsum proto +// into a qsim Circuit and fused circuit that represents the transformation +// to the z basis. +tensorflow::Status QsimZBasisCircuitFromProjectorTerm( + const tfq::proto::ProjectorTerm& term, const int num_qubits, + qsim::Circuit>* circuit, + std::vector>>* fused_circuit); + } // namespace tfq #endif // TFQ_CORE_SRC_CIRCUIT_PARSER_QSIM_H_ diff --git a/tensorflow_quantum/core/src/program_resolution.cc b/tensorflow_quantum/core/src/program_resolution.cc index 82af94d35..04606d49c 100644 --- a/tensorflow_quantum/core/src/program_resolution.cc +++ b/tensorflow_quantum/core/src/program_resolution.cc @@ -38,6 +38,9 @@ using tfq::proto::PauliQubitPair; using tfq::proto::PauliSum; using tfq::proto::PauliTerm; using tfq::proto::Program; +using tfq::proto::ProjectorDictEntry; +using tfq::proto::ProjectorSum; +using tfq::proto::ProjectorTerm; using tfq::proto::Qubit; inline absl::string_view IntMaxStr() { @@ -84,6 +87,7 @@ Status RegisterQubits( Status ResolveQubitIds(Program* program, unsigned int* num_qubits, std::vector* p_sums /*=nullptr*/, + std::vector* projector_sums /*=nullptr*/, bool swap_endianness /*=false*/) { if (program->circuit().moments().empty()) { // (#679) Just ignore empty program. @@ -176,6 +180,23 @@ Status ResolveQubitIds(Program* program, unsigned int* num_qubits, } } + if (projector_sums) { + for (size_t i = 0; i < projector_sums->size(); i++) { + // Replace the ProjectorSum Qubit ids with the indices. + for (ProjectorTerm& term : *(projector_sums->at(i)).mutable_terms()) { + for (ProjectorDictEntry& pair : *term.mutable_projector_dict()) { + const auto result = id_to_index.find(pair.qubit_id()); + if (result == id_to_index.end()) { + return Status(tensorflow::error::INVALID_ARGUMENT, + "Found a Projector sum operating on qubits not found " + "in circuit."); + } + pair.set_qubit_id(result->second); + } + } + } + } + return Status::OK(); } diff --git a/tensorflow_quantum/core/src/program_resolution.h b/tensorflow_quantum/core/src/program_resolution.h index 40d5760dd..18c47c272 100644 --- a/tensorflow_quantum/core/src/program_resolution.h +++ b/tensorflow_quantum/core/src/program_resolution.h @@ -25,6 +25,7 @@ limitations under the License. #include "tensorflow/core/lib/core/status.h" #include "tensorflow_quantum/core/proto/pauli_sum.pb.h" #include "tensorflow_quantum/core/proto/program.pb.h" +#include "tensorflow_quantum/core/proto/projector_sum.pb.h" namespace tfq { @@ -38,6 +39,7 @@ namespace tfq { tensorflow::Status ResolveQubitIds( tfq::proto::Program* program, unsigned int* num_qubits, std::vector* p_sums = nullptr, + std::vector* projector_sums = nullptr, bool swap_endianness = false); // Overload which allows for strict resolution of multiple programs. diff --git a/tensorflow_quantum/core/src/util_qsim.h b/tensorflow_quantum/core/src/util_qsim.h index 94531a884..8b9e24fbf 100644 --- a/tensorflow_quantum/core/src/util_qsim.h +++ b/tensorflow_quantum/core/src/util_qsim.h @@ -187,6 +187,47 @@ tensorflow::Status ComputeExpectationQsim(const tfq::proto::PauliSum& p_sum, return status; } +template +tensorflow::Status ComputeExpectationQsim( + const tfq::proto::ProjectorSum& projector_sum, const SimT& sim, + const StateSpaceT& ss, StateT& state, StateT& scratch, + float* expectation_value) { + // apply the gates of the projector terms to a copy of the state vector + // and add up expectation value term by term. + tensorflow::Status status = tensorflow::Status::OK(); + for (const tfq::proto::ProjectorTerm& term : projector_sum.terms()) { + // catch identity terms + if (term.projector_dict_size() == 0) { + *expectation_value += term.coefficient_real(); + // TODO(tonybruguier): error somewhere if identities have any imaginary + // part + continue; + } + + QsimCircuit main_circuit; + std::vector> fused_circuit; + + status = QsimCircuitFromProjectorTerm(term, state.num_qubits(), + &main_circuit, &fused_circuit); + + if (!status.ok()) { + return status; + } + // copy from src to scratch. + ss.Copy(state, scratch); + for (const qsim::GateFused& fused_gate : fused_circuit) { + qsim::ApplyFusedGate(sim, fused_gate, scratch); + } + + if (!status.ok()) { + return status; + } + *expectation_value += + term.coefficient_real() * ss.RealInnerProduct(state, scratch); + } + return status; +} + // bad style standards here that we are forced to follow from qsim. // computes the expectation value using // scratch to save on memory. Implementation does this: @@ -269,6 +310,91 @@ tensorflow::Status ComputeSampledExpectationQsim( return status; } +// bad style standards here that we are forced to follow from qsim. +// computes the expectation value using +// scratch to save on memory. Implementation does this: +// 1. Copy state onto scratch +// 2. Convert scratch to Z basis +// 3. Compute < state | scratch > via sampling. +// 4. Sum and repeat. +// scratch is required to have memory initialized, but does not require +// values in memory to be set. +template +tensorflow::Status ComputeSampledExpectationQsim( + const tfq::proto::ProjectorSum& projector_sum, const SimT& sim, + const StateSpaceT& ss, StateT& state, StateT& scratch, + const int num_samples, tensorflow::random::SimplePhilox& random_source, + float* expectation_value) { + std::uniform_int_distribution<> distrib(1, 1 << 30); + + if (num_samples == 0) { + return tensorflow::Status::OK(); + } + // apply the gates of the pauliterms to a copy of the state vector + // and add up expectation value term by term. + tensorflow::Status status = tensorflow::Status::OK(); + for (const tfq::proto::ProjectorTerm& term : projector_sum.terms()) { + // catch identity terms + if (term.projector_dict_size() == 0) { + *expectation_value += term.coefficient_real(); + // TODO(tonybruguier): error somewhere if identities have any imaginary + // part + continue; + } + + // Transform state into the measurement basis and sample it + QsimCircuit main_circuit; + std::vector> fused_circuit; + + status = QsimZBasisCircuitFromProjectorTerm(term, state.num_qubits(), + &main_circuit, &fused_circuit); + if (!status.ok()) { + return status; + } + // copy from src to scratch. + ss.Copy(state, scratch); + for (const qsim::GateFused& fused_gate : fused_circuit) { + qsim::ApplyFusedGate(sim, fused_gate, scratch); + } + + if (!status.ok()) { + return status; + } + std::vector state_samples = + ss.Sample(scratch, num_samples, random_source.Rand32()); + + // Find qubits on which to measure parity + std::vector parity_bits; + for (const tfq::proto::ProjectorDictEntry& entry : term.projector_dict()) { + unsigned int location; + // GridQubit id should be parsed down to integer at this upstream + // so it is safe to just use atoi. + (void)absl::SimpleAtoi(entry.qubit_id(), &location); + // Parity functions use little-endian indexing + parity_bits.push_back(state.num_qubits() - location - 1); + } + + // Compute the BitMask. + uint64_t mask = 0; + for (const unsigned int parity_bit : parity_bits) { + mask |= uint64_t(1) << uint64_t(parity_bit); + } + + // Compute the running parity. + int parity_total(0); + int count = 0; + for (const uint64_t state_sample : state_samples) { + count = + std::bitset<64>(state_sample & mask).count() == parity_bits.size(); + parity_total += count ? 1 : 0; + } + *expectation_value += static_cast(parity_total) * + term.coefficient_real() / + static_cast(num_samples); + } + return status; +} + // Overloading for MPS : it requires more scratch states. // bad style standards here that we are forced to follow from qsim. // computes the expectation value using diff --git a/tensorflow_quantum/python/util.py b/tensorflow_quantum/python/util.py index 92ebeabee..2d58254b5 100644 --- a/tensorflow_quantum/python/util.py +++ b/tensorflow_quantum/python/util.py @@ -25,6 +25,7 @@ from tensorflow_quantum.core.proto import pauli_sum_pb2 from tensorflow_quantum.core.proto import program_pb2 +from tensorflow_quantum.core.proto import projector_sum_pb2 from tensorflow_quantum.core.serialize import serializer # Can't use set() since channels don't give proper support. @@ -259,6 +260,23 @@ def random_pauli_sums(qubits, max_sum_length, n_sums): return sums +def random_projector_sums(qubits, max_sum_length, n_sums): + """Generate a list of random cirq projector sums of length |n_sums|.""" + sums = [] + for _ in range(n_sums): + this_sum_length = np.random.randint(1, max_sum_length + 1) + terms = [] + for _ in range(this_sum_length): + term_length = np.random.randint(1, len(qubits) + 1) + this_term_qubits = random.sample(qubits, term_length) + this_term_ids = np.random.randint(0, 2, term_length) + terms.append( + cirq.ProjectorString(dict(zip(this_term_qubits, + this_term_ids)))) + sums.append(cirq.ProjectorSum.from_projector_strings(terms)) + return sums + + # There are no native convertible ops inside of this function. @tf.autograph.experimental.do_not_convert def convert_to_tensor(items_to_convert, deterministic_proto_serialize=False): @@ -317,6 +335,12 @@ def recur(items_to_convert, curr_type=None): tensored_items.append( serializer.serialize_paulisum(item).SerializeToString( deterministic=deterministic_proto_serialize)) + elif isinstance(item, (cirq.ProjectorSum, cirq.ProjectorString)) \ + and not curr_type == cirq.Circuit: + curr_type = cirq.ProjectorSum + tensored_items.append( + serializer.serialize_projectorsum(item).SerializeToString( + deterministic=deterministic_proto_serialize)) elif isinstance(item, cirq.Circuit) and\ not curr_type == cirq.PauliSum: curr_type = cirq.Circuit @@ -347,6 +371,14 @@ def _parse_single(item): obj.ParseFromString(item) out = serializer.deserialize_paulisum(obj) return out + except Exception: + pass + try: + # Return a ProjectorSum parsing. + obj = projector_sum_pb2.ProjectorSum() + obj.ParseFromString(item) + out = serializer.deserialize_projectorsum(obj) + return out except Exception: raise TypeError('Error decoding item: ' + str(item)) diff --git a/tensorflow_quantum/python/util_test.py b/tensorflow_quantum/python/util_test.py index 3d4e2dd76..e10134096 100644 --- a/tensorflow_quantum/python/util_test.py +++ b/tensorflow_quantum/python/util_test.py @@ -31,12 +31,16 @@ def _single_to_tensor(item): - if not isinstance(item, (cirq.PauliSum, cirq.PauliString, cirq.Circuit)): + if not isinstance(item, (cirq.PauliSum, cirq.PauliString, cirq.ProjectorSum, + cirq.ProjectorString, cirq.Circuit)): raise TypeError("Item must be a Circuit or PauliSum. Got {}.".format( type(item))) if isinstance(item, (cirq.PauliSum, cirq.PauliString)): return serializer.serialize_paulisum(item).SerializeToString( deterministic=True) + if isinstance(item, (cirq.ProjectorSum, cirq.ProjectorString)): + return serializer.serialize_projectorsum(item).SerializeToString( + deterministic=True) return serializer.serialize_circuit(item).SerializeToString( deterministic=True) @@ -53,15 +57,18 @@ def _items_to_tensorize(): """Objects on which convert_to_tensor convert_from_tensor will be tested.""" return [{ 'item': x - } for x in (util.random_pauli_sums(BITS, 5, 5) + [ - cirq.PauliSum.from_pauli_strings([ - cirq.PauliString(), - cirq.PauliString(cirq.Z(cirq.GridQubit(0, 0))) - ]) - ] + [cirq.PauliString(), cirq.PauliString()] + [cirq.Circuit()] + [ - cirq.testing.random_circuit(BITS, 25, 0.9, util.get_supported_gates()) - for _ in range(5) - ])] + } for x in (util.random_pauli_sums(BITS, 5, 5) + + util.random_projector_sums(BITS, 5, 5) + [ + cirq.PauliSum.from_pauli_strings([ + cirq.PauliString(), + cirq.PauliString(cirq.Z(cirq.GridQubit(0, 0))) + ]) + ] + [cirq.PauliString(), cirq.PauliString()] + + [cirq.Circuit()] + [ + cirq.testing.random_circuit(BITS, 25, 0.9, + util.get_supported_gates()) + for _ in range(5) + ])] class UtilFunctionsTest(tf.test.TestCase, parameterized.TestCase):