Commit 83e7f524 authored by Nguyen, Thien Minh's avatar Nguyen, Thien Minh
Browse files

Merge branch 'master' into tnguyen/qsim-examples

parents 7a6bb450 bce07606
......@@ -217,6 +217,26 @@ void QCORSyntaxHandler::GetReplacement(
<< program_parameters[i] << "\")";
}
OS << ") {}\n";
// Forth constructor, give us a way to provide a HeterogeneousMap of
// arguments, and set a parent kernel - this is also used for Pythonic
// QJIT... KERNEL_NAME(std::shared_ptr<CompositeInstruction> parent,
// HeterogeneousMap args);
OS << kernel_name
<< "(std::shared_ptr<CompositeInstruction> parent, HeterogeneousMap& "
"args): QuantumKernel<"
<< kernel_name << ", " << program_arg_types[0];
for (int i = 1; i < program_arg_types.size(); i++) {
OS << ", " << program_arg_types[i];
}
OS << "> (parent, args.get<" << program_arg_types[0] << ">(\""
<< program_parameters[0] << "\")";
for (int i = 1; i < program_parameters.size(); i++) {
OS << ", "
<< "args.get<" << program_arg_types[i] << ">(\""
<< program_parameters[i] << "\")";
}
OS << ") {}\n";
}
// Destructor definition
......@@ -316,6 +336,12 @@ void QCORSyntaxHandler::GetReplacement(
<< "__with_hetmap_args(HeterogeneousMap& args) {\n";
OS << "class " << kernel_name << " __ker__temp__(args);\n";
OS << "}\n";
OS << "void " << kernel_name
<< "__with_parent_and_hetmap_args(std::shared_ptr<CompositeInstruction> parent, "
"HeterogeneousMap& args) {\n";
OS << "class " << kernel_name << " __ker__temp__(parent, args);\n";
OS << "}\n";
}
auto s = OS.str();
qcor::info("[qcor syntax-handler] Rewriting " + kernel_name + " to\n\n" + s);
......
......@@ -24,6 +24,7 @@ protected:
qreg q;
public:
KernelFunctor() = default;
KernelFunctor(qreg qReg) : q(qReg){};
// Direct construction via a Composite Instruction
KernelFunctor(std::shared_ptr<CompositeInstruction> composite) {
......
set(LIBRARY_NAME _pyqcor)
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-declarations")
set(CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -Wno-deprecated-declarations -Wno-attributes")
set (CMAKE_CXX_FLAGS "${CMAKE_CXX_FLAGS} -fno-strict-aliasing -O2 -g -pipe -Werror=format-security -Wp,-D_FORTIFY_SOURCE=2 -Wformat -fexceptions --param=ssp-buffer-size=4 -grecord-gcc-switches -m64 -mtune=native -D_GNU_SOURCE -fPIC -fwrapv")
if(APPLE)
......
from qcor import *
# Define the quantum kernel by providing a
# python function that is annotated with qjit for
# quantum just in time compilation
@qjit
def ansatz(q: qreg, theta: List[float]):
X(q[0])
Ry(q[1], theta[0])
CX(q[1], q[0])
# Define the hamiltonian
H = -2.1433 * X(0) * X(1) - 2.1433 * \
Y(0) * Y(1) + .21829 * Z(0) - 6.125 * Z(1) + 5.907
# Create the ObjectiveFunction, default is VQE
n_params = 1
obj = createObjectiveFunction(ansatz, H, n_params)
# evaluate at a concrete set of params
vqe_energy = obj([.59])
# Run full optimization
optimizer = createOptimizer('nlopt')
results = optimizer.optimize(obj)
print(results)
\ No newline at end of file
......@@ -12,9 +12,11 @@
#include "py_costFunctionEvaluator.hpp"
#include "py_qsimWorkflow.hpp"
#include "qcor_jit.hpp"
#include "qcor_observable.hpp"
#include "qrt.hpp"
#include "xacc.hpp"
#include "xacc_internal_compiler.hpp"
#include "xacc_service.hpp"
namespace py = pybind11;
using namespace xacc;
......@@ -48,10 +50,16 @@ struct visit_helper<mpark::variant> {
namespace {
// We only allow certain argument types for quantum kernel functors in python
// Here we enumerate them as a Variant
using AllowedKernelArgTypes =
xacc::Variant<bool, int, double, std::string, xacc::internal_compiler::qreg,
std::vector<double>>;
// We will take as input a mapping of arg variable names to the argument itself.
using KernelArgDict = std::map<std::string, AllowedKernelArgTypes>;
// Utility for mapping KernelArgDict to a HeterogeneousMap
class KernelArgDictToHeterogeneousMap {
protected:
xacc::HeterogeneousMap &m;
......@@ -68,7 +76,8 @@ class KernelArgDictToHeterogeneousMap {
};
// Add type name to this list to support receiving from Python.
using PyHeterogeneousMapTypes = xacc::Variant<bool, int, double, std::string>;
using PyHeterogeneousMapTypes = xacc::Variant<bool, int, double, std::string,
std::shared_ptr<qcor::Optimizer>>;
using PyHeterogeneousMap = std::map<std::string, PyHeterogeneousMapTypes>;
// Helper to convert a Python *dict* (as a map of variants) into a native
......@@ -85,6 +94,130 @@ xacc::HeterogeneousMap heterogeneousMapConvert(
}
} // namespace
namespace qcor {
// PyObjectiveFunction implements ObjectiveFunction to
// enable the utility of pythonic quantum kernels with the
// existing qcor ObjectiveFunction infrastructure. This class
// keeps track of the quantum kernel as a py::object, which it uses
// in tandem with the QCOR QJIT engine to create an executable
// functor representation of the quantum code at runtime. It exposes
// the ObjectiveFunction operator()() overloads to map vector<double>
// x to the correct pythonic argument structure. It delegates to the
// usual helper ObjectiveFunction (like vqe) for execution of the
// actual pre-, execution, and post-processing.
class PyObjectiveFunction : public qcor::ObjectiveFunction {
protected:
py::object py_kernel;
std::shared_ptr<ObjectiveFunction> helper;
xacc::internal_compiler::qreg qreg;
QJIT qjit;
public:
const std::string name() const override { return "py-objective-impl"; }
const std::string description() const override { return ""; }
PyObjectiveFunction(py::object q, qcor::PauliOperator &qq, const int n_dim,
const std::string &helper_name)
: py_kernel(q) {
// Set the OptFunction dimensions
_dim = n_dim;
// Set the helper objective
helper = xacc::getService<qcor::ObjectiveFunction>(helper_name);
// Store the observable pointer and give it to the helper
observable = xacc::as_shared_ptr(&qq);
helper->update_observable(observable);
// Extract the QJIT source code
auto src = py_kernel.attr("get_internal_src")().cast<std::string>();
// QJIT compile
// this will be fast if already done, and we just do it once
qjit.jit_compile(src, true);
qjit.write_cache();
}
// Evaluate this ObjectiveFunction at the dictionary of kernel args,
// return the scalar value
double operator()(const KernelArgDict args) {
// Map the kernel args to a hetmap
xacc::HeterogeneousMap m;
for (auto &item : args) {
KernelArgDictToHeterogeneousMap vis(m, item.first);
mpark::visit(vis, item.second);
}
// Get the kernel as a CompositeInstruction
auto kernel_name = py_kernel.attr("kernel_name")().cast<std::string>();
kernel = qjit.extract_composite_with_hetmap(kernel_name, m);
helper->update_kernel(kernel);
// FIXME, handle gradients
std::vector<double> dx;
return (*helper)(qreg, dx);
}
// Evaluate this ObjectiveFunction at the parameters x
double operator()(const std::vector<double> &x,
std::vector<double> &dx) override {
current_iterate_parameters = x;
helper->update_current_iterate_parameters(x);
// Translate x into kernel args
qreg = ::qalloc(observable->nBits());
auto args = py_kernel.attr("translate")(qreg, x).cast<KernelArgDict>();
// args will be a dictionary, arg_name to arg
return operator()(args);
}
virtual double operator()(xacc::internal_compiler::qreg &qreg,
std::vector<double> &dx) {
throw std::bad_function_call();
return 0.0;
}
};
// PyKernelFunctor is a subtype of KernelFunctor from the qsim library
// that returns a CompositeInstruction representation of a pythonic
// quantum kernel given a vector of parameters x. This will
// leverage the QJIT infrastructure to create executable functor
// representation of the python kernel.
class PyKernelFunctor : public qcor::KernelFunctor {
protected:
py::object py_kernel;
QJIT qjit;
std::size_t n_qubits;
public:
PyKernelFunctor(py::object q, const std::size_t nq, const std::size_t np)
: py_kernel(q), n_qubits(nq) {
nbParams = np;
auto src = py_kernel.attr("get_internal_src")().cast<std::string>();
// this will be fast if already done, and we just do it once
qjit.jit_compile(src, true);
qjit.write_cache();
}
// Delegate to QJIT to create a CompositeInstruction representation
// of the pythonic quantum kernel.
std::shared_ptr<xacc::CompositeInstruction> evaluate_kernel(
const std::vector<double> &x) override {
// Translate x into kernel args
auto qreg = ::qalloc(n_qubits);
auto args = py_kernel.attr("translate")(qreg, x).cast<KernelArgDict>();
xacc::HeterogeneousMap m;
for (auto &item : args) {
KernelArgDictToHeterogeneousMap vis(m, item.first);
mpark::visit(vis, item.second);
}
auto kernel_name = py_kernel.attr("kernel_name")().cast<std::string>();
return qjit.extract_composite_with_hetmap(kernel_name, m);
}
};
} // namespace qcor
PYBIND11_MODULE(_pyqcor, m) {
m.doc() = "Python bindings for QCOR.";
......@@ -148,8 +281,10 @@ PYBIND11_MODULE(_pyqcor, m) {
.def("counts", &xacc::internal_compiler::qreg::counts, "")
.def("exp_val_z", &xacc::internal_compiler::qreg::exp_val_z, "");
// m.def("createObjectiveFunction", [](const std::string name, ))
py::class_<qcor::QJIT, std::shared_ptr<qcor::QJIT>>(m, "QJIT", "")
.def(py::init<>(), "")
.def("write_cache", &qcor::QJIT::write_cache, "")
.def("jit_compile", &qcor::QJIT::jit_compile, "")
.def(
"internal_python_jit_compile",
......@@ -169,8 +304,38 @@ PYBIND11_MODULE(_pyqcor, m) {
}
qjit.invoke_with_hetmap(name, m);
},
"")
.def("extract_composite",
[](qcor::QJIT &qjit, const std::string name, KernelArgDict args) {
xacc::HeterogeneousMap m;
for (auto &item : args) {
KernelArgDictToHeterogeneousMap vis(m, item.first);
mpark::visit(vis, item.second);
}
return qjit.extract_composite_with_hetmap(name, m);
});
py::class_<qcor::ObjectiveFunction, std::shared_ptr<qcor::ObjectiveFunction>>(
m, "ObjectiveFunction", "")
.def("dimensions", &qcor::ObjectiveFunction::dimensions, "")
.def(
"__call__",
[](qcor::ObjectiveFunction &obj, std::vector<double> x) {
return obj(x);
},
"");
m.def(
"createObjectiveFunction",
[](py::object kernel, qcor::PauliOperator &obs, const int n_params) {
auto q = ::qalloc(obs.nBits());
std::shared_ptr<qcor::ObjectiveFunction> obj =
std::make_shared<qcor::PyObjectiveFunction>(kernel, obs, n_params,
"vqe");
return obj;
},
"");
// qsim sub-module bindings:
{
py::module qsim = m.def_submodule("qsim", "QCOR's python qsim submodule");
......@@ -193,7 +358,19 @@ PYBIND11_MODULE(_pyqcor, m) {
[](qcor::PauliOperator &obs, qcor::qsim::TdObservable ham_func) {
return qcor::qsim::ModelBuilder::createModel(obs, ham_func);
},
"Return the Model for a time-dependent problem.");
"Return the Model for a time-dependent problem.")
.def(
"createModel",
[](py::object py_kernel, qcor::PauliOperator &obs,
const int n_qubits, const int n_params) {
qcor::qsim::QuantumSimulationModel model;
auto kernel_functor = std::make_shared<qcor::PyKernelFunctor>(
py_kernel, n_qubits, n_params);
model.observable = &obs;
model.user_defined_ansatz = kernel_functor;
return std::move(model);
},
"");
// CostFunctionEvaluator bindings
py::class_<qcor::qsim::CostFunctionEvaluator,
......
#pragma once
#include "base/qcor_qsim.hpp"
#include <memory>
#include <pybind11/complex.h>
......
#pragma once
#include "base/qcor_qsim.hpp"
#include <memory>
#include <pybind11/complex.h>
......
from _pyqcor import *
# TODO: will need to selectively import XACC
import xacc
from _pyqcor import *
import sys
from typing import Union, List
import inspect
from typing import List
import typing
List = typing.List
def X(idx):
......@@ -18,30 +21,66 @@ def Z(idx):
class qjit(object):
"""
The qjit class serves a python function decorator that enables
the just-in-time compilation of quantum python functions (kernels) using
the QCOR QJIT infrastructure. Example usage:
@qjit
def kernel(qbits : qreg, theta : float):
X(q[0])
Ry(q[1], theta)
CX(q[1], q[0])
for i in range(q.size()):
Measure(q[i])
q = qalloc(2)
kernel(q)
print(q.counts())
Upon initialization, the python inspect module is used to extract the function body
as a string. This string is processed to create a corresponding C++ function with
pythonic function body as an embedded domain specific language. The QCOR QJIT engine
takes this function string, and delegates to the QCOR Clang SyntaxHandler infrastructure, which
maps this function to a QCOR QuantumKernel sub-type, compiles to LLVM bitcode, caches that bitcode
for future fast lookup, and extracts function pointers using the LLVM JIT engine that can be called
later, affecting execution of the quantum code.
Note that kernel function arguments must provide type hints, and allowed types are int, bool, float, List[float], and qreg.
qjit annotated functions can also be passed as general functors to other QCOR API calls like
createObjectiveFunction, and createModel from the QSim library.
"""
def __init__(self, function, *args, **kwargs):
"""Constructor for qjit, takes as input the annotated python function and any additional optional
arguments that are used to customize the workflow."""
self.args = args
self.kwargs = kwargs
self.function = function
self.allowed_type_cpp_map = {'<class \'_pyqcor.qreg\'>': 'qreg',
'<class \'float\'>': 'double', 'typing.List[float]': 'std::vector<double>'}
self.__dict__.update(kwargs)
return
def __call__(self, *args, **kwargs):
import inspect
# Get the function body source as a string
# Create the qcor just in time engine
self._qjit = QJIT()
# Get the kernel function body as a string
fbody_src = '\n'.join(inspect.getsource(self.function).split('\n')[2:])
# Get the arg names and type annotation
arg_names, _, _, _, _, _, type_annotations = inspect.getfullargspec(
# Get the arg variable names and their types
self.arg_names, _, _, _, _, _, self.type_annotations = inspect.getfullargspec(
self.function)
if not type_annotations:
# Users must provide arg types, if not we throw an error
if not self.type_annotations or len(self.arg_names) != len(self.type_annotations):
print('Error, you must provide type annotations for qcor quantum kernels.')
exit(1)
# Construct the C++ kernel arg string
cpp_arg_str = ''
for arg, _type in type_annotations.items():
for arg, _type in self.type_annotations.items():
if str(_type) not in self.allowed_type_cpp_map:
print('Error, this quantum kernel arg type is not allowed: ', str(_type))
exit(1)
......@@ -49,27 +88,102 @@ class qjit(object):
self.allowed_type_cpp_map[str(_type)] + ' ' + arg
cpp_arg_str = cpp_arg_str[1:]
# Update as a qcor quantum kernel function for QJIT
fbody_src = '__qpu__ void '+self.function.__name__ + \
# Create the qcor quantum kernel function src for QJIT and the Clang syntax handler
self.src = '__qpu__ void '+self.function.__name__ + \
'('+cpp_arg_str+') {\nusing qcor::pyxasm;\n'+fbody_src+"}\n"
# Run JIT, this will reused cached JIT LLVM Modules
_qjit = QJIT()
_qjit.internal_python_jit_compile(fbody_src)
# Run the QJIT compile step to store function pointers internally
self._qjit.internal_python_jit_compile(self.src)
self._qjit.write_cache()
return
def get_internal_src(self):
"""Return the C++ / embedded python DSL function code that will be passed to QJIT
and the clang syntax handler. This function is primarily to be used for developer purposes. """
return self.src
def kernel_name(self):
"""Return the quantum kernel function name."""
return self.function.__name__
def translate(self, q: qreg, x: List[float]):
"""
This method is primarily used internally to map Optimizer parameters x : List[float] to
the argument structure expected by the quantum kernel. For example, for a kernel
expecting (qreg, float) arguments, this method should return a dictionary where argument variable
names serve as keys, and values are corresponding argument instances. Specifically, the float
argument variable should point to x[0], for example.
"""
if [str(x) for x in self.type_annotations.values()] == ['<class \'_pyqcor.qreg\'>', '<class \'float\'>']:
ret_dict = {}
for arg_name, _type in self.type_annotations.items():
if str(_type) == '<class \'_pyqcor.qreg\'>':
ret_dict[arg_name] = q
elif str(_type) == '<class \'float\'>':
ret_dict[arg_name] = x[0]
if len(ret_dict) != len(self.type_annotations):
print(
'Error, could not translate vector parameters x into arguments for quantum kernel.')
exit(1)
return ret_dict
elif [str(x) for x in self.type_annotations.values()] == ['<class \'_pyqcor.qreg\'>', 'typing.List[float]']:
ret_dict = {}
for arg_name, _type in self.type_annotations.items():
if str(_type) == '<class \'_pyqcor.qreg\'>':
ret_dict[arg_name] = q
elif str(_type) == 'typing.List[float]':
ret_dict[arg_name] = x
if len(ret_dict) != len(self.type_annotations):
print(
'Error, could not translate vector parameters x into arguments for quantum kernel.')
exit(1)
return ret_dict
else:
print('currently cannot translate other arg structures')
exit(1)
def extract_composite(self, *args):
"""
Convert the quantum kernel into an XACC CompositeInstruction
"""
# Create a dictionary for the function arguments
args_dict = {}
for i, arg_name in enumerate(arg_names):
for i, arg_name in enumerate(self.arg_names):
args_dict[arg_name] = list(args)[i]
return self._qjit.extract_composite(self.function.__name__, args_dict)
def openqasm(self, *args):
"""
Return an OpenQasm string representation of this
quantum kernel.
"""
kernel = self.extract_composite(*args)
staq = xacc.getCompiler('staq')
return staq.translate(kernel)
def __call__(self, *args):
"""
Execute the decorated quantum kernel. This will directly
invoke the corresponding LLVM JITed function pointer.
"""
# Create a dictionary for the function arguments
args_dict = {}
for i, arg_name in enumerate(self.arg_names):
args_dict[arg_name] = list(args)[i]
# Invoke the JITed function
_qjit.invoke(self.function.__name__, args_dict)
self._qjit.invoke(self.function.__name__, args_dict)
return
# Must have qpu in the init kwargs
# defaults to qpp, but will search for -qpu flag
init_kwargs = {'qpu': sys.argv[sys.argv.index('-qpu')+1] if '-qpu' in sys.argv else 'qpp'}
init_kwargs = {'qpu': sys.argv[sys.argv.index(
'-qpu')+1] if '-qpu' in sys.argv else 'qpp'}
# get shots if provided
if '-shots' in sys.argv:
......
......@@ -10,4 +10,11 @@ add_test (NAME qcor_simple_kernel_jit_python
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
set_tests_properties(qcor_simple_kernel_jit_python
PROPERTIES ENVIRONMENT "PYTHONPATH=${CMAKE_INSTALL_PREFIX}:$ENV{PYTHONPATH}")
\ No newline at end of file
PROPERTIES ENVIRONMENT "PYTHONPATH=${CMAKE_INSTALL_PREFIX}:$ENV{PYTHONPATH}")
add_test (NAME qcor_test_qcor_spec_api
COMMAND ${Python_EXECUTABLE} ${CMAKE_CURRENT_SOURCE_DIR}/test_qcor_spec_api.py
WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}
)
set_tests_properties(qcor_test_qcor_spec_api
PROPERTIES ENVIRONMENT "PYTHONPATH=${CMAKE_INSTALL_PREFIX}:$ENV{PYTHONPATH}")
\ No newline at end of file
import unittest
from qcor import *
class TestVQEObjectiveFunction(unittest.TestCase):
def test_simple_deuteron(self):
@qjit
def ansatz(q: qreg, theta: List[float]):
X(q[0])
Ry(q[1], theta[0])
CX(q[1], q[0])
q = qalloc(2)
comp = ansatz.extract_composite(q, [2.2])
print(comp.toString())
H = -2.1433 * X(0) * X(1) - 2.1433 * \
Y(0) * Y(1) + .21829 * Z(0) - 6.125 * Z(1) + 5.907
n_params = 1
obj = createObjectiveFunction(ansatz, H, n_params)
vqe_energy = obj([.59])
self.assertAlmostEqual(vqe_energy, -1.74, places=1)
optimizer = createOptimizer('nlopt')
results = optimizer.optimize(obj)
self.assertAlmostEqual(results[0], -1.74, places=1)
print(results)
print(ansatz.openqasm(q, [2.2]))
if __name__ == '__main__':
unittest.main()
......@@ -23,5 +23,31 @@ class TestWorkflows(unittest.TestCase):
self.assertAlmostEqual(result["exp-vals"][0], 1.0, places=1)
self.assertAlmostEqual(result["exp-vals"][nbSteps], 0.5, places=1)
def test_vqe_ansatz(self):
H = -2.1433 * X(0) * X(1) - 2.1433 * \
Y(0) * Y(1) + .21829 * Z(0) - 6.125 * Z(1) + 5.907
@qjit
def ansatz(q : qreg, theta : float):
X(q[0])
Ry(q[1], theta)
CX(q[1], q[0])
num_qubits = 2
num_params = 1
problemModel = qsim.ModelBuilder.createModel(ansatz, H, num_qubits, num_params)
optimizer = createOptimizer('nlopt')
workflow = qsim.getWorkflow('vqe', {'optimizer': optimizer})