From 10abe5b40e5f4578cdaa60e417101e5b4b71613a Mon Sep 17 00:00:00 2001 From: Kevin Guo Date: Wed, 25 Oct 2023 16:02:18 -0400 Subject: [PATCH] write IonQ transpiler; initial iteration --- examples/ionq_test.qasm | 8 + include/output/ionq.hpp | 345 ++++++---------------- include/transformations/replace_ugate.hpp | 171 +++++++++++ tools/ionq.cpp | 33 ++- 4 files changed, 303 insertions(+), 254 deletions(-) create mode 100644 examples/ionq_test.qasm create mode 100644 include/transformations/replace_ugate.hpp diff --git a/examples/ionq_test.qasm b/examples/ionq_test.qasm new file mode 100644 index 00000000..be5d2f9b --- /dev/null +++ b/examples/ionq_test.qasm @@ -0,0 +1,8 @@ +OPENQASM 2.0; +include "qelib1.inc"; + +qreg q[2]; +qreg a[2]; + +tdg a[0]; +crz(2) a[1], q[0]; diff --git a/include/output/ionq.hpp b/include/output/ionq.hpp index 8816daaf..873f53b9 100644 --- a/include/output/ionq.hpp +++ b/include/output/ionq.hpp @@ -43,27 +43,9 @@ namespace output { namespace ast = qasmtools::ast; -/** \brief Equivalent Q# standard gates for qasm standard gates */ -std::unordered_map qasmstd_to_qsharp{ - {"id", "I"}, - {"x", "X"}, - {"y", "Y"}, - {"z", "Z"}, - {"h", "H"}, - {"s", "S"}, - {"sdg", "(Adjoint S)"}, - {"t", "T"}, - {"tdg", "(Adjoint T)"}, - {"cx", "CNOT"}, - {"cz", "CZ"}, - {"ch", "(Controlled H)"}, - {"ccx", "CCNOT"}, - {"rx", "Rx"}, - {"ry", "Ry"}, - {"rz", "Rz"}, - {"u1", "Rz"}, - {"crz", "(Controlled Rz)"}, - {"cu1", "(Controlled Rz)"}}; +/** \brief Equivalent IonQ standard gates for qasm standard gates */ +std::unordered_map qasmstd_to_ionq{ + {"sdg", "si"}, {"tdg", "ti"}, {"u1", "rz"}}; /** * \class staq::output::IonQOutputter @@ -71,306 +53,167 @@ std::unordered_map qasmstd_to_qsharp{ */ class IonQOutputter final : public ast::Visitor { public: - struct config { - bool driver = false; - std::string ns = "Quantum.staq"; - std::string opname = "Circuit"; - }; - IonQOutputter(std::ostream& os) : Visitor(), os_(os) {} - IonQOutputter(std::ostream& os, const config& params) - : Visitor(), os_(os), config_(params) {} ~IonQOutputter() = default; void run(ast::Program& prog) { prefix_ = ""; - ambiguous_ = false; - locals_.clear(); + first_gate = true; prog.accept(*this); } // Variables - void visit(ast::VarAccess& ap) { os_ << ap; } + void visit(ast::VarAccess& ap) {} // Expressions - void visit(ast::BExpr& expr) { - auto tmp = ambiguous_; - - if (expr.op() == ast::BinaryOp::Pow) { - ambiguous_ = false; - // Override since ^ is strictly integral in Q# - os_ << "PowD("; - expr.lexp().accept(*this); - os_ << ", "; - expr.rexp().accept(*this); - } else { - ambiguous_ = true; - if (tmp) { - os_ << "("; - expr.lexp().accept(*this); - os_ << expr.op(); - expr.rexp().accept(*this); - os_ << ")"; - } else { - expr.lexp().accept(*this); - os_ << expr.op(); - expr.rexp().accept(*this); - } - } - ambiguous_ = tmp; - } + void visit(ast::BExpr& expr) {} - void visit(ast::UExpr& expr) { - switch (expr.op()) { - case ast::UnaryOp::Neg: { - auto tmp = ambiguous_; - ambiguous_ = true; - os_ << "-"; - expr.subexp().accept(*this); - ambiguous_ = tmp; - break; - } - case ast::UnaryOp::Sin: - os_ << "Sin("; - expr.subexp().accept(*this); - os_ << ")"; - break; - case ast::UnaryOp::Cos: - os_ << "Cos("; - expr.subexp().accept(*this); - os_ << ")"; - break; - case ast::UnaryOp::Tan: - os_ << "Tan("; - expr.subexp().accept(*this); - os_ << ")"; - break; - case ast::UnaryOp::Ln: - os_ << "Log("; - expr.subexp().accept(*this); - os_ << ")"; - break; - case ast::UnaryOp::Sqrt: - os_ << "Sqrt("; - expr.subexp().accept(*this); - os_ << ")"; - break; - case ast::UnaryOp::Exp: - os_ << "ExpD("; - expr.subexp().accept(*this); - os_ << ")"; - break; - default: - break; - } - } - - void visit(ast::PiExpr&) { os_ << "PI()"; } + void visit(ast::UExpr& expr) {} - void visit(ast::IntExpr& expr) { os_ << expr.value() << ".0"; } + void visit(ast::PiExpr&) {} - void visit(ast::RealExpr& expr) { - auto tmp = expr.value(); + void visit(ast::IntExpr& expr) {} - os_ << tmp; - if (tmp - floor(tmp) == 0) - os_ << ".0"; - } + void visit(ast::RealExpr& expr) {} - void visit(ast::VarExpr& expr) { os_ << expr.var(); } + void visit(ast::VarExpr& expr) {} // Statements - void visit(ast::MeasureStmt& stmt) { - // Arrays are immutable in Q# - os_ << prefix_ << "set " << stmt.c_arg().var(); - os_ << " w/= " << *(stmt.c_arg().offset()); - os_ << " <- M(" << stmt.q_arg() << ");\n"; - } - - void visit(ast::ResetStmt& stmt) { - os_ << prefix_ << "Reset(" << stmt.arg() << ");\n"; - } + void visit(ast::MeasureStmt& stmt) {} - void visit(ast::IfStmt& stmt) { - os_ << prefix_ << "if (ResultArrayAsInt(" << stmt.var(); - os_ << ") == " << stmt.cond() << ") {\n"; + void visit(ast::ResetStmt& stmt) {} - prefix_ += " "; - stmt.then().accept(*this); - prefix_.resize(prefix_.size() - 4); - - os_ << prefix_ << "}\n"; - } + void visit(ast::IfStmt& stmt) {} // Gates - void visit(ast::UGate& gate) { - os_ << prefix_ << "U("; - gate.theta().accept(*this); - os_ << ", "; - gate.phi().accept(*this); - os_ << ", "; - gate.lambda().accept(*this); - os_ << ", "; - gate.arg().accept(*this); - os_ << ");\n"; - } + void visit(ast::UGate& gate) {} - void visit(ast::CNOTGate& gate) { - os_ << prefix_ << "CNOT("; - gate.ctrl().accept(*this); - os_ << ", "; - gate.tgt().accept(*this); - os_ << ");\n"; - } + void visit(ast::CNOTGate& gate) {} void visit(ast::BarrierGate&) {} void visit(ast::DeclaredGate& gate) { - os_ << prefix_; - - if (auto it = qasmstd_to_qsharp.find(gate.name()); - it != qasmstd_to_qsharp.end()) - os_ << it->second << "("; - else - os_ << gate.name() << "("; - - for (int i = 0; i < (gate.num_cargs() + gate.num_qargs()); i++) { - if (i != 0) - os_ << ", "; - - if (i < gate.num_cargs()) - gate.carg(i).accept(*this); - else - gate.qarg(i - gate.num_cargs()).accept(*this); + // JSON output: avoid outputting comma before first gate + if (first_gate) { + first_gate = false; + } else { + os_ << ",\n"; } - os_ << ");\n"; - } - // Declarations - void visit(ast::GateDecl& decl) { - if (decl.is_opaque()) - throw std::logic_error("Opaque declarations not supported"); - - if (qasmstd_to_qsharp.find(decl.id()) == qasmstd_to_qsharp.end()) { - - // Declaration header - os_ << prefix_ << "operation " << decl.id() << "("; - for (int i = 0; - i < (decl.c_params().size() + decl.q_params().size()); i++) { - if (i != 0) - os_ << ", "; - - if (i < decl.c_params().size()) - os_ << decl.c_params()[i] << " : Double"; - else - os_ << decl.q_params()[i - decl.c_params().size()] - << " : Qubit"; - } - os_ << ") : Unit {\n"; + os_ << prefix_ << "{\n"; + prefix_ += " "; - // Declaration body - prefix_ += " "; - decl.foreach_stmt([this](auto& stmt) { stmt.accept(*this); }); + std::string name = gate.name(); + if (auto it = qasmstd_to_ionq.find(gate.name()); + it != qasmstd_to_ionq.end()) + name = it->second; + + int cur_qarg = 0; + bool ctrl = false; + // Handle control gates + // Control is the first qarg, if applicable + if (name[0] == 'c') { + cur_qarg = 1; + ctrl = true; + name = name.substr(1); + } - // Reset all local ancillas - for (auto it = locals_.rbegin(); it != locals_.rend(); it++) { - os_ << prefix_ << "ResetAll(" << *it << ");\n"; - prefix_.resize(prefix_.size() - 4); - os_ << prefix_ << "}\n"; + // IonQ handles gates with multiple control args, + // but the QE standard gates have at most one control arg. + if (ctrl) { + os_ << prefix_ << "\"control\": " << gate.qarg(0).offset().value() + << ",\n"; + } + + if (gate.num_qargs() - cur_qarg == 1) { + // Single target gate + os_ << prefix_ + << "\"target\": " << gate.qarg(cur_qarg).offset().value() + << ",\n"; + } else { + // Multi target gate + os_ << prefix_ << "\"targets\": ["; + for (int i = cur_qarg; i < gate.num_qargs(); ++i) { + os_ << gate.qarg(i).offset().value(); + if (i + 1 != gate.num_qargs()) + os_ << ","; } - locals_.clear(); - prefix_.resize(prefix_.size() - 4); - os_ << prefix_ << "}\n\n"; + os_ << "],\n"; } - } - void visit(ast::OracleDecl& decl) { - throw std::logic_error( - "Q# has no support for oracle declarations via logic files"); + if (gate.num_cargs()) { + // TODO: assert that there is exactly one carg + // TODO: assert that this is a rotation gate + // TODO: Handle multiples of pi nicely + os_ << prefix_ << "\"angle\": " << std::fixed + << gate.carg(0).constant_eval().value() / M_PI << ",\n"; + } + + os_ << prefix_ << "\"gate\": \"" << name << "\"\n"; // no comma + + prefix_.resize(prefix_.size() - 4); + os_ << prefix_ + << "}"; // Do not output newline for proper JSON formatting } + // Declarations + void visit(ast::GateDecl& decl) {} + + void visit(ast::OracleDecl& decl) {} + void visit(ast::RegisterDecl& decl) { if (decl.is_quantum()) { - os_ << prefix_ << "using (" << decl.id() << " = Qubit[" - << decl.size() << "]) {"; - prefix_ += " "; - locals_.push_back(decl.id()); + os_ << prefix_ << "\"qubits\": " << decl.size() << ",\n"; } else { - os_ << prefix_ << "mutable " << decl.id() << " = new Result[" - << decl.size() << "];"; + // should never occur } - os_ << "\n"; } - void visit(ast::AncillaDecl& decl) { - os_ << prefix_ << "using (" << decl.id() << " = Qubit[" << decl.size() - << "]) {\n"; - prefix_ += " "; - locals_.push_back(decl.id()); - } + void visit(ast::AncillaDecl& decl) {} // Program void visit(ast::Program& prog) { - os_ << prefix_ << "namespace " << config_.ns << " {\n"; + os_ << prefix_ << "{\n"; prefix_ += " "; - os_ << prefix_ << "open Microsoft.Quantum.Intrinsic;\n"; - os_ << prefix_ << "open Microsoft.Quantum.Convert;\n"; - os_ << prefix_ << "open Microsoft.Quantum.Canon;\n"; - os_ << prefix_ << "open Microsoft.Quantum.Math;\n\n"; - - // QASM U gate - os_ << prefix_ - << "operation U(theta : Double, phi : Double, lambda : Double, q : " - "Qubit) : Unit {\n"; - prefix_ += " "; - os_ << prefix_ << "Rz(lambda, q);\n"; - os_ << prefix_ << "Ry(theta, q);\n"; - os_ << prefix_ << "Rz(phi, q);\n"; - prefix_.resize(prefix_.size() - 4); - os_ << prefix_ << "}\n\n"; + os_ << prefix_ << "\"format\": \"ionq.circuit.v0\",\n"; + os_ << prefix_ << "\"gateset\": \"qis\",\n"; - // Gate declarations + // Print the qubit count line (global register decl) prog.foreach_stmt([this](auto& stmt) { - if (typeid(stmt) == typeid(ast::GateDecl)) + if (typeid(stmt) == typeid(ast::RegisterDecl)) stmt.accept(*this); }); - // Program body - os_ << prefix_ << "operation " << config_.opname << "() : Unit {\n"; + os_ << prefix_ << "\"circuit\": [\n"; prefix_ += " "; + + // Program body prog.foreach_stmt([this](auto& stmt) { - if (typeid(stmt) != typeid(ast::GateDecl)) + // Skip the gate declarations from qelib1.inc + // and the global register decl + if ((typeid(stmt) != typeid(ast::GateDecl)) && + (typeid(stmt) != typeid(ast::RegisterDecl))) stmt.accept(*this); }); - // Reset all qubits + // Close circuit os_ << "\n"; - for (auto it = locals_.rbegin(); it != locals_.rend(); it++) { - os_ << prefix_ << "ResetAll(" << *it << ");\n"; - prefix_.resize(prefix_.size() - 4); - os_ << prefix_ << "}\n"; - } - locals_.clear(); - - // Close operation prefix_.resize(prefix_.size() - 4); - os_ << prefix_ << "}\n"; + os_ << prefix_ << "]\n"; - // Close namespace + // Close input prefix_.resize(prefix_.size() - 4); os_ << prefix_ << "}\n"; } private: std::ostream& os_; - config config_; std::string prefix_ = ""; - std::list locals_{}; - bool ambiguous_ = false; + bool first_gate = true; }; /** \brief Writes an AST in IonQ format to stdout */ diff --git a/include/transformations/replace_ugate.hpp b/include/transformations/replace_ugate.hpp new file mode 100644 index 00000000..43221fcd --- /dev/null +++ b/include/transformations/replace_ugate.hpp @@ -0,0 +1,171 @@ +/* + * This file is part of staq. + * + * Copyright (c) 2019 - 2023 softwareQ Inc. All rights reserved. + * + * MIT License + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + */ + +/** + * \file transformations/replace_ugate.hpp + * \brief Replacing common U gates with QE standard gates + */ + +#ifndef TRANSFORMATIONS_REPLACE_UGATE_HPP_ +#define TRANSFORMATIONS_REPLACE_UGATE_HPP_ + +#include +#include +#include + +#include "qasmtools/ast/replacer.hpp" + +namespace staq { +namespace transformations { + +namespace ast = qasmtools::ast; + +/** + * \brief Replacing UGates + * + * Visits an AST and replaces common U gates with QE standard + * gates if possible. Assumes qelib1.inc is included. + */ +void replace_ugates(ast::ASTNode& node); + +static const double pi = M_PI; +static const double EPS = 1e-9; + +struct UArgs { + double theta; + double phi; + double lambda; +}; + +// clang-format off +static const std::vector> standard_gates{ + {{pi, 0, pi}, "x"}, + {{pi, pi/2, pi/2}, "y"}, + {{0, 0, pi}, "z"}, + {{pi/2, 0, pi}, "h"}, + {{0, 0, pi/2}, "s"}, + {{0, 0, -pi/2}, "sdg"}, + {{0, 0, pi/4}, "t"}, + {{0, 0, -pi/4}, "tdg"} +}; +// clang-format on + +/* Implementation */ +class ReplaceUGateImpl final : public ast::Replacer { + public: + ReplaceUGateImpl() = default; + ~ReplaceUGateImpl() = default; + + void run(ast::ASTNode& node) { node.accept(*this); } + + // Replace ast::CNOT with ast::DeclaredGate cx + std::optional>> + replace(ast::CNOTGate& gate) override { + std::cerr << "CNOT\n"; + std::vector> c_args; + std::vector q_args{gate.ctrl(), gate.tgt()}; + + std::list> ret; + ret.emplace_back(std::make_unique(ast::DeclaredGate( + gate.pos(), "cx", std::move(c_args), std::move(q_args)))); + return std::move(ret); + } + + std::optional>> + replace(ast::UGate& gate) override { + double theta, phi, lambda; + try { + theta = gate.theta().constant_eval().value(); + phi = gate.phi().constant_eval().value(); + lambda = gate.lambda().constant_eval().value(); + } catch (...) { + std::cerr << "found VarExpr in UGate args" + << "\n"; + // this should never happen; if this is reached then the Replacer + // is recurring too far down the AST + throw; + } + + std::string name = ""; + std::vector> c_args; + std::vector q_args{gate.arg()}; + + // Simple cases: x y z h s sdg t tdg + for (auto& [g_args, g_name] : standard_gates) { + if (std::abs(theta - g_args.theta) < EPS && + std::abs(phi - g_args.phi) < EPS && + std::abs(lambda - g_args.lambda) < EPS) { + + name = g_name; + break; + } + } + + // Remaining cases: rz ry rx + if (name == "") { + if (std::abs(theta) < EPS && std::abs(phi) < EPS) { + name = "rz"; + // TODO: Directly copy the exprs from the U gate + // to avoid precision loss + c_args.emplace_back( + std::make_unique(gate.pos(), lambda)); + } else if (std::abs(phi) < EPS && std::abs(lambda) < EPS) { + name = "ry"; + c_args.emplace_back( + std::make_unique(gate.pos(), theta)); + } else if (std::abs(phi + pi / 2) < EPS && + std::abs(lambda - pi / 2) < EPS) { + name = "rx"; + c_args.emplace_back( + std::make_unique(gate.pos(), theta)); + } + } + + // Throw error if U gate is not a QE standard gate + if (name == "") { + throw std::logic_error{""}; + return std::nullopt; + } + + std::list> ret; + ret.emplace_back(std::make_unique(ast::DeclaredGate( + gate.pos(), name, std::move(c_args), std::move(q_args)))); + return std::move(ret); + } + + // Avoid visiting children of GateDecl + void visit(ast::GateDecl& decl) override {} +}; + +void replace_ugates(ast::ASTNode& node) { + ReplaceUGateImpl alg; + alg.run(node); +} + +} /* namespace transformations */ +} /* namespace staq */ + +#endif /* TRANSFORMATIONS_REPLACE_UGATE_HPP_ */ diff --git a/tools/ionq.cpp b/tools/ionq.cpp index 927a084e..6d7d364a 100644 --- a/tools/ionq.cpp +++ b/tools/ionq.cpp @@ -26,9 +26,18 @@ #include +#include "mapping/device.hpp" +#include "mapping/layout/basic.hpp" #include "output/ionq.hpp" #include "qasmtools/parser/parser.hpp" #include "transformations/desugar.hpp" +#include "transformations/expression_simplifier.hpp" +#include "transformations/inline.hpp" +#include "transformations/replace_ugate.hpp" + +static const std::set ionq_overrides{ + "x", "y", "z", "h", "s", "sdg", "t", "tdg", "rx", + "ry", "rz", "cz", "cy", "swap", "cx", "u1", "ch", "crz"}; int main(int argc, char** argv) { using namespace staq; @@ -42,11 +51,29 @@ int main(int argc, char** argv) { CLI11_PARSE(app, argc, argv); auto program = parse_stdin(); - // require: format of program - // decl - // gates + if (program) { transformations::desugar(*program); + + // Flatten qregs into one global qreg. + // IonQ Simulator has 29 qubits; + // the other IonQ devices have less than 29 qubits. + // For now let's set a cap of 11 qubits; change this later. + auto device = mapping::fully_connected(11); + auto layout = mapping::compute_basic_layout(device, *program); + mapping::apply_layout(layout, device, *program); + + // Inline declared gates + transformations::Inliner::config params{false, ionq_overrides}; + transformations::inline_ast(*program, params); + + // Evaluate expressions + // TODO: Handle multiples of pi nicely + transformations::expr_simplify(*program, true); + + // Replace U gates + transformations::replace_ugates(*program); + if (filename.empty()) output::output_ionq(*program); else