diff --git a/tensorflow_quantum/core/src/circuit_parser_qsim.cc b/tensorflow_quantum/core/src/circuit_parser_qsim.cc index d9f8d80b3..6c6ed8d3f 100644 --- a/tensorflow_quantum/core/src/circuit_parser_qsim.cc +++ b/tensorflow_quantum/core/src/circuit_parser_qsim.cc @@ -15,6 +15,8 @@ limitations under the License. #include "tensorflow_quantum/core/src/circuit_parser_qsim.h" +#include +#include #include #include @@ -28,6 +30,7 @@ limitations under the License. #include "../qsim/lib/io.h" #include "absl/container/flat_hash_map.h" #include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" #include "absl/strings/str_split.h" #include "absl/strings/string_view.h" #include "absl/types/optional.h" @@ -81,6 +84,51 @@ inline Status ParseProtoArg( return ::tensorflow::Status(); } +// Parses the qubit ids of `op` into indices in [0, num_qubits). `op` must act +// on exactly `qubits.size()` distinct qubits. +// +// Ids reaching the parser are normally decimal indices written by +// ResolveQubitIds, but they end up as raw offsets into simulator memory, so +// nothing is taken on trust here: a missing, extra, unparseable, negative, +// out-of-range, or repeated id is reported as an error instead of becoming an +// out-of-bounds access downstream. +inline Status ParseQubitIds(const Operation& op, const unsigned int num_qubits, + std::initializer_list qubits) { + if (op.qubits_size() != static_cast(qubits.size())) { + return Status( + absl::StatusCode::kInvalidArgument, + absl::StrCat("Operation ", op.gate().id(), " expects ", qubits.size(), + " qubit(s) but got ", op.qubits_size(), ".")); + } + // Slots [0, i) of `parsed` hold validated indices by the time slot i is + // filled, so the distinctness check only ever compares validated values. + unsigned int* const* const parsed = qubits.begin(); + for (int i = 0; i < op.qubits_size(); i++) { + unsigned int* qubit = parsed[i]; + const std::string& id = op.qubits(i).id(); + // SimpleAtoi into an unsigned target also rejects negative values. It + // tolerates surrounding whitespace and a leading '+', which is harmless + // because the parsed value is range-checked. + if (!absl::SimpleAtoi(id, qubit) || *qubit >= num_qubits) { + return Status( + absl::StatusCode::kInvalidArgument, + absl::StrCat("Invalid qubit id: '", id, "' in operation ", + op.gate().id(), ". Expected an integer in [0, ", + num_qubits, ").")); + } + // qsim's index arithmetic assumes that the qubits of a gate are distinct. + for (int j = 0; j < i; j++) { + if (*parsed[j] == *qubit) { + return Status( + absl::StatusCode::kInvalidArgument, + absl::StrCat("Duplicate qubit id: '", id, "'. Operation ", + op.gate().id(), " must act on distinct qubits.")); + } + } + } + return ::tensorflow::Status(); +} + inline Status ParseProtoControls(const Operation& op, const unsigned int num_qubits, std::vector* control_qubits, @@ -111,9 +159,14 @@ inline Status ParseProtoControls(const Operation& op, unsigned int tmp; control_qubits->reserve(control_toks.size()); for (auto tok : control_toks) { - // don't bother error checking since this is done earlier - // in program_resolution. valid = absl::SimpleAtoi(tok, &tmp); + if (!valid || tmp >= num_qubits) { + return Status( + absl::StatusCode::kInvalidArgument, + absl::StrCat("Invalid control qubit id: '", tok, "' in operation ", + op.gate().id(), ". Expected an integer in [0, ", + num_qubits, ").")); + } control_qubits->push_back(num_qubits - tmp - 1); } control_values->reserve(control_v_toks.size()); @@ -141,6 +194,26 @@ inline Status OptionalInsertControls(const Operation& op, if (control_qubits.empty()) { return ::tensorflow::Status(); } + // qsim's index arithmetic assumes that the control qubits of a gate are + // distinct from each other and from its target qubits. + for (size_t i = 0; i < control_qubits.size(); i++) { + const unsigned int q = control_qubits[i]; + const bool is_target = std::find(gate->qubits.begin(), gate->qubits.end(), + q) != gate->qubits.end(); + const bool is_repeated = + std::find(control_qubits.begin(), control_qubits.begin() + i, q) != + control_qubits.begin() + i; + if (is_target || is_repeated) { + // control_qubits holds qsim indices; report the id as written in the + // proto so that the message matches the user's circuit. + const unsigned int proto_id = num_qubits - q - 1; + return Status( + absl::StatusCode::kInvalidArgument, + absl::StrCat("Duplicate control qubit id: '", proto_id, + "'. Control qubits must be distinct from each other " + "and from the target qubits.")); + } + } qsim::MakeControlledGate(control_qubits, control_values, *gate); return ::tensorflow::Status(); } @@ -157,7 +230,10 @@ inline Status SingleConstantGate( const unsigned int num_qubits, const unsigned int time, QsimCircuit* circuit, std::vector* metadata) { unsigned int q0; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q0); + Status u = ParseQubitIds(op, num_qubits, {&q0}); + if (!u.ok()) { + return u; + } auto gate = create_f(time, num_qubits - q0 - 1); Status s = OptionalInsertControls(op, num_qubits, &gate); if (!s.ok()) { @@ -182,8 +258,10 @@ inline Status TwoConstantGate( const unsigned int num_qubits, const unsigned int time, QsimCircuit* circuit, std::vector* metadata) { unsigned int q0, q1; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q0); - (void)absl::SimpleAtoi(op.qubits(1).id(), &q1); + Status u = ParseQubitIds(op, num_qubits, {&q0, &q1}); + if (!u.ok()) { + return u; + } auto gate = create_f(time, num_qubits - q0 - 1, num_qubits - q1 - 1); Status s = OptionalInsertControls(op, num_qubits, &gate); if (!s.ok()) { @@ -209,8 +287,10 @@ inline Status SingleEigenGate( QsimCircuit* circuit, std::vector* metadata) { unsigned int q0; float exp, exp_s, gs; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q0); + Status u = ParseQubitIds(op, num_qubits, {&q0}); + if (!u.ok()) { + return u; + } absl::optional exponent_symbol; u = ParseProtoArg(op, "exponent", param_map, &exp, &exponent_symbol); @@ -257,9 +337,10 @@ inline Status TwoEigenGate( QsimCircuit* circuit, std::vector* metadata) { unsigned int q0, q1; float exp, exp_s, gs; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q0); - (void)absl::SimpleAtoi(op.qubits(1).id(), &q1); + Status u = ParseQubitIds(op, num_qubits, {&q0, &q1}); + if (!u.ok()) { + return u; + } absl::optional exponent_symbol; u = ParseProtoArg(op, "exponent", param_map, &exp, &exponent_symbol); @@ -394,10 +475,12 @@ inline Status PhasedXGate(const Operation& op, const SymbolMap& param_map, const unsigned int num_qubits, const unsigned int time, QsimCircuit* circuit, std::vector* metadata) { - int q0; + unsigned int q0; float pexp, pexp_s, exp, exp_s, gs; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q0); + Status u = ParseQubitIds(op, num_qubits, {&q0}); + if (!u.ok()) { + return u; + } absl::optional exponent_symbol; u = ParseProtoArg(op, "exponent", param_map, &exp, &exponent_symbol); @@ -453,11 +536,12 @@ inline Status FsimGate(const Operation& op, const SymbolMap& param_map, const unsigned int num_qubits, const unsigned int time, QsimCircuit* circuit, std::vector* metadata) { - int q0, q1; + unsigned int q0, q1; float theta, theta_s, phi, phi_s; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q0); - (void)absl::SimpleAtoi(op.qubits(1).id(), &q1); + Status u = ParseQubitIds(op, num_qubits, {&q0, &q1}); + if (!u.ok()) { + return u; + } absl::optional theta_symbol; u = ParseProtoArg(op, "theta", param_map, &theta, &theta_symbol); @@ -509,11 +593,12 @@ inline Status PhasedISwapGate(const Operation& op, const SymbolMap& param_map, const unsigned int num_qubits, const unsigned int time, QsimCircuit* circuit, std::vector* metadata) { - int q0, q1; + unsigned int q0, q1; float pexp, pexp_s, exp, exp_s; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q0); - (void)absl::SimpleAtoi(op.qubits(1).id(), &q1); + Status u = ParseQubitIds(op, num_qubits, {&q0, &q1}); + if (!u.ok()) { + return u; + } absl::optional exponent_symbol; u = ParseProtoArg(op, "exponent", param_map, &exp, &exponent_symbol); @@ -599,10 +684,12 @@ inline Status AsymmetricDepolarizingChannel(const Operation& op, const unsigned int num_qubits, const unsigned int time, NoisyQsimCircuit* ncircuit) { - int q; + unsigned int q; float p_x, p_y, p_z; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q); + Status u = ParseQubitIds(op, num_qubits, {&q}); + if (!u.ok()) { + return u; + } u = ParseProtoArg(op, "p_x", {}, &p_x); u = ParseProtoArg(op, "p_y", {}, &p_y); @@ -620,10 +707,12 @@ inline Status DepolarizingChannel(const Operation& op, const unsigned int num_qubits, const unsigned int time, NoisyQsimCircuit* ncircuit) { - int q; + unsigned int q; float p; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q); + Status u = ParseQubitIds(op, num_qubits, {&q}); + if (!u.ok()) { + return u; + } u = ParseProtoArg(op, "p", {}, &p); if (!u.ok()) { @@ -637,10 +726,12 @@ inline Status DepolarizingChannel(const Operation& op, inline Status GADChannel(const Operation& op, const unsigned int num_qubits, const unsigned int time, NoisyQsimCircuit* ncircuit) { - int q; + unsigned int q; float p, gamma; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q); + Status u = ParseQubitIds(op, num_qubits, {&q}); + if (!u.ok()) { + return u; + } u = ParseProtoArg(op, "p", {}, &p); if (!u.ok()) { @@ -660,8 +751,11 @@ inline Status GADChannel(const Operation& op, const unsigned int num_qubits, inline Status ResetChannel(const Operation& op, const unsigned int num_qubits, const unsigned int time, NoisyQsimCircuit* ncircuit) { - int q; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q); + unsigned int q; + Status u = ParseQubitIds(op, num_qubits, {&q}); + if (!u.ok()) { + return u; + } auto chan = qsim::Cirq::ResetChannel::Create(time, num_qubits - q - 1); ncircuit->channels.push_back(chan); @@ -672,10 +766,12 @@ inline Status AmplitudeDampingChannel(const Operation& op, const unsigned int num_qubits, const unsigned int time, NoisyQsimCircuit* ncircuit) { - int q; + unsigned int q; float gamma; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q); + Status u = ParseQubitIds(op, num_qubits, {&q}); + if (!u.ok()) { + return u; + } u = ParseProtoArg(op, "gamma", {}, &gamma); if (!u.ok()) { @@ -691,10 +787,12 @@ inline Status PhaseDampingChannel(const Operation& op, const unsigned int num_qubits, const unsigned int time, NoisyQsimCircuit* ncircuit) { - int q; + unsigned int q; float gamma; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q); + Status u = ParseQubitIds(op, num_qubits, {&q}); + if (!u.ok()) { + return u; + } u = ParseProtoArg(op, "gamma", {}, &gamma); if (!u.ok()) { @@ -711,10 +809,12 @@ inline Status PhaseFlipChannel(const Operation& op, const unsigned int num_qubits, const unsigned int time, NoisyQsimCircuit* ncircuit) { - int q; + unsigned int q; float p; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q); + Status u = ParseQubitIds(op, num_qubits, {&q}); + if (!u.ok()) { + return u; + } u = ParseProtoArg(op, "p", {}, &p); if (!u.ok()) { @@ -730,10 +830,12 @@ inline Status PhaseFlipChannel(const Operation& op, inline Status BitFlipChannel(const Operation& op, const unsigned int num_qubits, const unsigned int time, NoisyQsimCircuit* ncircuit) { - int q; + unsigned int q; float p; - Status u; - (void)absl::SimpleAtoi(op.qubits(0).id(), &q); + Status u = ParseQubitIds(op, num_qubits, {&q}); + if (!u.ok()) { + return u; + } u = ParseProtoArg(op, "p", {}, &p); if (!u.ok()) { diff --git a/tensorflow_quantum/core/src/circuit_parser_qsim_test.cc b/tensorflow_quantum/core/src/circuit_parser_qsim_test.cc index a158d2ae3..55be867b0 100644 --- a/tensorflow_quantum/core/src/circuit_parser_qsim_test.cc +++ b/tensorflow_quantum/core/src/circuit_parser_qsim_test.cc @@ -16,6 +16,8 @@ limitations under the License. #include "tensorflow_quantum/core/src/circuit_parser_qsim.h" #include +#include +#include #include "../qsim/lib/channel.h" #include "../qsim/lib/channels_cirq.h" @@ -24,6 +26,7 @@ limitations under the License. #include "../qsim/lib/gates_cirq.h" #include "absl/container/flat_hash_map.h" #include "absl/strings/numbers.h" +#include "absl/strings/str_cat.h" #include "gtest/gtest.h" #include "tensorflow/core/lib/core/status.h" #include "tensorflow_quantum/core/proto/program.pb.h" @@ -1706,5 +1709,306 @@ TEST(QsimCircuitParserTest, ZBasisCircuitFromPauliTermEmpty) { ASSERT_EQ(test_circuit.gates.size(), 0); } +// The parser turns qubit ids into offsets into simulator memory, so an op with +// missing, extra, unparseable, negative, out-of-range, or repeated qubit ids +// must be rejected with an error instead of being handed to qsim. Every gate +// and channel builder reads its qubits from the proto, so each one is +// exercised. + +// Every gate id accepted by ParseAppendGate, with the number of qubits it acts +// on. +const std::pair kGateArities[] = { + {"I", 1}, {"HP", 1}, {"XP", 1}, {"YP", 1}, {"ZP", 1}, {"PXP", 1}, + {"I2", 2}, {"XXP", 2}, {"YYP", 2}, {"ZZP", 2}, {"CZP", 2}, {"CNP", 2}, + {"SP", 2}, {"ISP", 2}, {"FSIM", 2}, {"PISP", 2}}; + +// Every channel id accepted by ParseAppendChannel; all act on one qubit. +const char* const kChannelIds[] = {"DP", "ADP", "GAD", "AD", + "RST", "PD", "PF", "BF"}; + +constexpr int kValidationNumQubits = 3; + +// Qubit id strings that must never be accepted when num_qubits is +// kValidationNumQubits: malformed, negative, fractional, equal to the bound, +// far past it, and one past UINT32_MAX (overflows unsigned int). +const char* const kInvalidQubitIds[] = {"", "junk", "-1", "1.5", + "3", "100", "4294967296"}; + +// Adds an op for `gate_id` acting on `qubit_ids`, with every float argument any +// gate or channel builder may read, so that the qubit list is the only thing +// that varies between test cases. +void AddOperationWithAllArgs(Moment* moment, const std::string& gate_id, + const std::vector& qubit_ids) { + Operation* op = moment->add_operations(); + op->mutable_gate()->set_id(gate_id); + google::protobuf::Map* args = op->mutable_args(); + for (const char* name : + {"exponent", "exponent_scalar", "global_shift", "phase_exponent", + "phase_exponent_scalar", "theta", "theta_scalar", "phi", "phi_scalar", + "p", "p_x", "p_y", "p_z", "gamma"}) { + (*args)[name] = MakeArg(0.25); + } + (*args)["control_qubits"] = MakeControlArg(""); + (*args)["control_values"] = MakeControlArg(""); + for (const std::string& id : qubit_ids) { + op->add_qubits()->set_id(id); + } +} + +std::vector ValidQubitIds(int count) { + std::vector ids; + for (int i = 0; i < count; i++) { + ids.push_back(absl::StrCat(i)); + } + return ids; +} + +tensorflow::Status ParseAsGateCircuit(const Program& program) { + QsimCircuit circuit; + std::vector> fused_circuit; + return QsimCircuitFromProgram(program, {}, kValidationNumQubits, &circuit, + &fused_circuit); +} + +tensorflow::Status ParseAsNoisyCircuit(const Program& program) { + NoisyQsimCircuit circuit; + return NoisyQsimCircuitFromProgram(program, {}, kValidationNumQubits, false, + &circuit); +} + +tensorflow::Status WrongArityStatus(const std::string& gate_id, int expected, + int got) { + return tensorflow::Status( + absl::StatusCode::kInvalidArgument, + absl::StrCat("Operation ", gate_id, " expects ", expected, + " qubit(s) but got ", got, ".")); +} + +tensorflow::Status InvalidQubitIdStatus(const std::string& gate_id, + const std::string& id) { + return tensorflow::Status( + absl::StatusCode::kInvalidArgument, + absl::StrCat("Invalid qubit id: '", id, "' in operation ", gate_id, + ". Expected an integer in [0, ", kValidationNumQubits, + ").")); +} + +tensorflow::Status InvalidControlQubitIdStatus(const std::string& gate_id, + const std::string& id) { + return tensorflow::Status( + absl::StatusCode::kInvalidArgument, + absl::StrCat("Invalid control qubit id: '", id, "' in operation ", + gate_id, ". Expected an integer in [0, ", + kValidationNumQubits, ").")); +} + +tensorflow::Status DuplicateQubitIdStatus(const std::string& gate_id, + const std::string& id) { + return tensorflow::Status( + absl::StatusCode::kInvalidArgument, + absl::StrCat("Duplicate qubit id: '", id, "'. Operation ", gate_id, + " must act on distinct qubits.")); +} + +tensorflow::Status DuplicateControlQubitIdStatus(const std::string& id) { + return tensorflow::Status( + absl::StatusCode::kInvalidArgument, + absl::StrCat("Duplicate control qubit id: '", id, + "'. Control qubits must be distinct from each other and " + "from the target qubits.")); +} + +class GateQubitValidationTest + : public ::testing::TestWithParam> { + protected: + std::string gate_id() const { return GetParam().first; } + int arity() const { return GetParam().second; } +}; + +TEST_P(GateQubitValidationTest, AcceptsWellFormedOp) { + // Guards the tests below: a failure there must be caused by the qubits + // alone, not by the fixture's choice of arguments. + Program program; + AddOperationWithAllArgs(program.mutable_circuit()->add_moments(), gate_id(), + ValidQubitIds(arity())); + EXPECT_EQ(ParseAsGateCircuit(program), tensorflow::Status()); + EXPECT_EQ(ParseAsNoisyCircuit(program), tensorflow::Status()); +} + +TEST_P(GateQubitValidationTest, RejectsWrongNumberOfQubits) { + for (int count = 0; count <= arity() + 1; count++) { + if (count == arity()) { + continue; + } + Program program; + AddOperationWithAllArgs(program.mutable_circuit()->add_moments(), gate_id(), + ValidQubitIds(count)); + EXPECT_EQ(ParseAsGateCircuit(program), + WrongArityStatus(gate_id(), arity(), count)) + << "qubit count " << count; + EXPECT_EQ(ParseAsNoisyCircuit(program), + WrongArityStatus(gate_id(), arity(), count)) + << "qubit count " << count; + } +} + +TEST_P(GateQubitValidationTest, RejectsInvalidQubitId) { + // Place the bad id at every position so that each qubit is checked. + for (int position = 0; position < arity(); position++) { + for (const char* bad_id : kInvalidQubitIds) { + std::vector ids = ValidQubitIds(arity()); + ids[position] = bad_id; + Program program; + AddOperationWithAllArgs(program.mutable_circuit()->add_moments(), + gate_id(), ids); + EXPECT_EQ(ParseAsGateCircuit(program), + InvalidQubitIdStatus(gate_id(), bad_id)) + << "position " << position; + EXPECT_EQ(ParseAsNoisyCircuit(program), + InvalidQubitIdStatus(gate_id(), bad_id)) + << "position " << position; + } + } +} + +TEST_P(GateQubitValidationTest, RejectsRepeatedQubitId) { + if (arity() < 2) { + GTEST_SKIP() << "A single-qubit gate cannot repeat a qubit."; + } + Program program; + AddOperationWithAllArgs(program.mutable_circuit()->add_moments(), gate_id(), + {"1", "1"}); + EXPECT_EQ(ParseAsGateCircuit(program), + DuplicateQubitIdStatus(gate_id(), "1")); + EXPECT_EQ(ParseAsNoisyCircuit(program), + DuplicateQubitIdStatus(gate_id(), "1")); +} + +INSTANTIATE_TEST_SUITE_P(QubitValidationTests, GateQubitValidationTest, + ::testing::ValuesIn(kGateArities)); + +class ChannelQubitValidationTest + : public ::testing::TestWithParam {}; + +TEST_P(ChannelQubitValidationTest, AcceptsWellFormedOp) { + Program program; + AddOperationWithAllArgs(program.mutable_circuit()->add_moments(), GetParam(), + ValidQubitIds(1)); + EXPECT_EQ(ParseAsNoisyCircuit(program), tensorflow::Status()); +} + +TEST_P(ChannelQubitValidationTest, RejectsWrongNumberOfQubits) { + for (int count : {0, 2}) { + Program program; + AddOperationWithAllArgs(program.mutable_circuit()->add_moments(), + GetParam(), ValidQubitIds(count)); + EXPECT_EQ(ParseAsNoisyCircuit(program), + WrongArityStatus(GetParam(), 1, count)) + << "qubit count " << count; + } +} + +TEST_P(ChannelQubitValidationTest, RejectsInvalidQubitId) { + for (const char* bad_id : kInvalidQubitIds) { + Program program; + AddOperationWithAllArgs(program.mutable_circuit()->add_moments(), + GetParam(), {bad_id}); + EXPECT_EQ(ParseAsNoisyCircuit(program), + InvalidQubitIdStatus(GetParam(), bad_id)); + } +} + +INSTANTIATE_TEST_SUITE_P(QubitValidationTests, ChannelQubitValidationTest, + ::testing::ValuesIn(kChannelIds)); + +TEST(QsimCircuitParserTest, RejectsInvalidControlQubitId) { + for (const char* bad_id : kInvalidQubitIds) { + Program program; + Moment* moment = program.mutable_circuit()->add_moments(); + AddOperationWithAllArgs(moment, "I", {"0"}); + google::protobuf::Map* args = + moment->mutable_operations(0)->mutable_args(); + (*args)["control_qubits"] = MakeControlArg(absl::StrCat("1,", bad_id)); + (*args)["control_values"] = MakeControlArg("1,1"); + EXPECT_EQ(ParseAsGateCircuit(program), + InvalidControlQubitIdStatus("I", bad_id)); + EXPECT_EQ(ParseAsNoisyCircuit(program), + InvalidControlQubitIdStatus("I", bad_id)); + } +} + +TEST(QsimCircuitParserTest, RejectsControlQubitEqualToTargetQubit) { + // Place the offending control at the second position so that the first, + // valid control is seen to be accepted. + Program program; + Moment* moment = program.mutable_circuit()->add_moments(); + AddOperationWithAllArgs(moment, "CZP", {"0", "1"}); + google::protobuf::Map* args = + moment->mutable_operations(0)->mutable_args(); + (*args)["control_qubits"] = MakeControlArg("2,1"); + (*args)["control_values"] = MakeControlArg("1,1"); + EXPECT_EQ(ParseAsGateCircuit(program), DuplicateControlQubitIdStatus("1")); + EXPECT_EQ(ParseAsNoisyCircuit(program), DuplicateControlQubitIdStatus("1")); +} + +TEST(QsimCircuitParserTest, RejectsRepeatedControlQubit) { + Program program; + Moment* moment = program.mutable_circuit()->add_moments(); + AddOperationWithAllArgs(moment, "I", {"0"}); + google::protobuf::Map* args = + moment->mutable_operations(0)->mutable_args(); + (*args)["control_qubits"] = MakeControlArg("1,2,1"); + (*args)["control_values"] = MakeControlArg("1,0,1"); + EXPECT_EQ(ParseAsGateCircuit(program), DuplicateControlQubitIdStatus("1")); + EXPECT_EQ(ParseAsNoisyCircuit(program), DuplicateControlQubitIdStatus("1")); +} + +TEST(QsimCircuitParserTest, AcceptsDistinctControlAndTargetQubits) { + // Guards the two tests above: the same shape with distinct qubits parses. + Program program; + Moment* moment = program.mutable_circuit()->add_moments(); + AddOperationWithAllArgs(moment, "I", {"0"}); + google::protobuf::Map* args = + moment->mutable_operations(0)->mutable_args(); + (*args)["control_qubits"] = MakeControlArg("2,1"); + (*args)["control_values"] = MakeControlArg("1,0"); + EXPECT_EQ(ParseAsGateCircuit(program), tensorflow::Status()); + EXPECT_EQ(ParseAsNoisyCircuit(program), tensorflow::Status()); +} + +TEST(QsimCircuitParserTest, RejectsEmptyControlQubitsWithControlValues) { + // An empty string splits into one empty token, so the token counts match and + // the empty token itself must be rejected. + Program program; + Moment* moment = program.mutable_circuit()->add_moments(); + AddOperationWithAllArgs(moment, "I", {"0"}); + google::protobuf::Map* args = + moment->mutable_operations(0)->mutable_args(); + (*args)["control_qubits"] = MakeControlArg(""); + (*args)["control_values"] = MakeControlArg("1"); + EXPECT_EQ(ParseAsGateCircuit(program), InvalidControlQubitIdStatus("I", "")); + EXPECT_EQ(ParseAsNoisyCircuit(program), InvalidControlQubitIdStatus("I", "")); +} + +TEST(QsimCircuitParserTest, RejectsOpWithoutQubitsNextToValidOp) { + // Shape of the reported proof of concept: a gate with no qubits survives + // qubit id resolution because it registers nothing, and reaches the parser + // as soon as another op in the program makes num_qubits positive. + Program program; + Moment* moment = program.mutable_circuit()->add_moments(); + Operation* op = moment->add_operations(); + op->mutable_gate()->set_id("HP"); + google::protobuf::Map* args = op->mutable_args(); + (*args)["exponent"] = MakeArg(1.0); + (*args)["exponent_scalar"] = MakeArg(1.0); + (*args)["global_shift"] = MakeArg(0.0); + (*args)["control_qubits"] = MakeControlArg(""); + (*args)["control_values"] = MakeControlArg(""); + AddOperationWithAllArgs(moment, "XP", {"0"}); + + EXPECT_EQ(ParseAsGateCircuit(program), WrongArityStatus("HP", 1, 0)); + EXPECT_EQ(ParseAsNoisyCircuit(program), WrongArityStatus("HP", 1, 0)); +} + } // namespace } // namespace tfq