Skip to content
94 changes: 93 additions & 1 deletion python/tests/backends/test_IonQ.py
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@
# the terms of the Apache License 2.0 which accompanies this distribution. #
# ============================================================================ #

import cudaq, pytest, os
import cudaq, pytest, os, requests
from cudaq import spin
import numpy as np
from typing import List
Expand Down Expand Up @@ -362,6 +362,98 @@ def ctrl_z_kernel():
assert counts["0010011"] == 1000


def test_shot_wise_output_with_memory_and_qpu():

url = "http://localhost:{}".format(port)

# set the mock server target
requests.post(f"{url}/_mock_server_config_target?target=aria-1")

cudaq.set_target("ionq", url=url, noise='forte-enterprise-1', memory=True)

@cudaq.kernel
def bell_state():
qubits = cudaq.qvector(3)
x(qubits[0])
cx(qubits[0], qubits[1])

results = cudaq.sample(bell_state, shots_count=3)
assert (results.get_sequential_data() == ['011', '011', '011'])

# reset the mock server target
requests.post(f"{url}/_mock_server_config_target?target=")


def test_shot_wise_output_with_no_memory_and_qpu():

url = "http://localhost:{}".format(port)

# set the mock server target
requests.post(f"{url}/_mock_server_config_target?target=aria-1")

cudaq.set_target("ionq", url=url, noise='forte-enterprise-1', memory=False)

@cudaq.kernel
def bell_state():
qubits = cudaq.qvector(3)
x(qubits[0])
cx(qubits[0], qubits[1])

results = cudaq.sample(bell_state, shots_count=3)
assert (results.get_sequential_data() == [])

# reset the mock server target
requests.post(f"{url}/_mock_server_config_target?target=")


def test_shot_wise_output_with_memory_and_noise_model():

url = "http://localhost:{}".format(port)

# set the mock server target
requests.post(f"{url}/_mock_server_config_target?target=simulator")
requests.post(f"{url}/_mock_server_config_noise_model?noise=aria-1")

cudaq.set_target("ionq", url=url, noise='forte-enterprise-1', memory=True)

@cudaq.kernel
def bell_state():
qubits = cudaq.qvector(3)
x(qubits[0])
cx(qubits[0], qubits[1])

results = cudaq.sample(bell_state, shots_count=3)
assert (results.get_sequential_data() == ['011', '011', '011'])

# reset the mock server target
requests.post(f"{url}/_mock_server_config_target?target=")
requests.post(f"{url}/_mock_server_config_noise_model?noise=")


def test_shot_wise_output_with_no_memory_and_noise_model():

url = "http://localhost:{}".format(port)

# set the mock server target
requests.post(f"{url}/_mock_server_config_target?target=simulator")
requests.post(f"{url}/_mock_server_config_noise_model?noise=aria-1")

cudaq.set_target("ionq", url=url, noise='forte-enterprise-1', memory=False)

@cudaq.kernel
def bell_state():
qubits = cudaq.qvector(3)
x(qubits[0])
cx(qubits[0], qubits[1])

results = cudaq.sample(bell_state, shots_count=3)
assert (results.get_sequential_data() == [])

# reset the mock server target
requests.post(f"{url}/_mock_server_config_target?target=")
requests.post(f"{url}/_mock_server_config_noise_model?noise=")


# leave for gdb debugging
if __name__ == "__main__":
loc = os.path.abspath(__file__)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -70,6 +70,14 @@ class IonQServerHelper : public ServerHelper {
cudaq::sample_result processResults(ServerMessage &postJobResponse,
std::string &jobId) override;

/// @brief extract the job shots url from jobs returned by Ion API
std::string getShotsUrl(nlohmann::json_v3_11_1::json &jobs,
const char *DEFAULT_URL);

/// @brief Verify if shot-wise output was requested by user and can be
/// extracted
bool shotWiseOutputIsNeeded(nlohmann::json_v3_11_1::json &jobs);

private:
/// @brief RestClient used for HTTP requests.
RestClient client;
Expand Down Expand Up @@ -103,6 +111,8 @@ void IonQServerHelper::initialize(BackendConfig config) {
// Retrieve the noise model setting (if provided)
if (config.find("noise") != config.end())
backendConfig["noise_model"] = config["noise"];
else if (config.find("noise_model") != config.end())
backendConfig["noise_model"] = config["noise_model"];
// Retrieve the API key from the environment variables
bool isTokenRequired = [&]() {
auto it = config.find("emulate");
Expand All @@ -126,6 +136,12 @@ void IonQServerHelper::initialize(BackendConfig config) {
backendConfig["sharpen"] = config["sharpen"];
if (config.find("format") != config.end())
backendConfig["format"] = config["format"];

// Enable memory, true by default
if (config.find("memory") != config.end())
backendConfig["memory"] = config["memory"];
else
backendConfig["memory"] = "true";
}

// Implementation of the getValueOrDefault function
Expand Down Expand Up @@ -351,6 +367,45 @@ bool IonQServerHelper::jobIsDone(ServerMessage &getJobResponse) {
return jobs[0].at("status").get<std::string>() == "completed";
}

std::string IonQServerHelper::getShotsUrl(nlohmann::json_v3_11_1::json &jobs,
const char *DEFAULT_URL) {
if (!keyExists("url"))
throw std::runtime_error("Key 'url' doesn't exist in backendConfig.");

std::string base_url = backendConfig.at("url");
std::string shotsUrl = "";

if (!jobs.empty() && jobs[0].contains("results") &&
jobs[0].at("results").contains("shots") &&
jobs[0].at("results").at("shots").contains("url")) {
shotsUrl = base_url +
jobs[0].at("results").at("shots").at("url").get<std::string>();
}

return shotsUrl;
}

bool IonQServerHelper::shotWiseOutputIsNeeded(
nlohmann::json_v3_11_1::json &jobs) {
std::string noiseModel = "ideal";
if (!jobs.empty() && jobs[0].contains("noise") &&
jobs[0]["noise"].contains("model")) {
noiseModel = jobs[0]["noise"]["model"].get<std::string>();
}

std::string target = "simulator";
if (!jobs.empty() && jobs[0].contains("target")) {
target = jobs[0]["target"].get<std::string>();
}

bool targetHasMemoryOption =
keyExists("memory") && backendConfig["memory"] == "true";
bool noiseModelIsNotIdeal = noiseModel != "ideal";
bool targetIsQpu = target != "simulator";

return targetHasMemoryOption && (targetIsQpu || noiseModelIsNotIdeal);
}

// Process the results from a job
cudaq::sample_result
IonQServerHelper::processResults(ServerMessage &postJobResponse,
Expand Down Expand Up @@ -448,9 +503,36 @@ IonQServerHelper::processResults(ServerMessage &postJobResponse,
execResults.emplace_back(regCounts, info.registerName);
}

// Add shot-wise output if requested by user
bool extractShots = shotWiseOutputIsNeeded(jobs);
auto shotsUrl = getShotsUrl(jobs, DEFAULT_URL);
if (extractShots && shotsUrl != "") {

std::vector<std::string> bitStrings;
auto shotsResults = getResults(shotsUrl);

for (const auto &element : shotsResults.items()) {
assert(nQubits <= 64);
int64_t s = std::stoull(element.value().get<std::string>());
std::string bitString = std::bitset<64>(s).to_string();
auto firstone = bitString.find_first_not_of('0');
bitString =
(firstone == std::string::npos) ? "0" : bitString.substr(firstone);
if (bitString.size() < static_cast<size_t>(nQubits)) {
bitString.insert(bitString.begin(),
static_cast<size_t>(nQubits) - bitString.size(), '0');
}
bitStrings.push_back(bitString);
}

if (!execResults.empty())
execResults[0].sequentialData = std::move(bitStrings);
}

// Return a sample result including the global register and all individual
// registers.
auto ret = cudaq::sample_result(execResults);

return ret;
}

Expand Down
7 changes: 6 additions & 1 deletion runtime/cudaq/platform/default/rest/helpers/ionq/ionq.yml
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,15 @@ target-arguments:
- key: debias
required: false
type: string
platform-arg: debias
platform-arg: debias
help-string: "Specify debiasing."
- key: sharpen
required: false
type: string
platform-arg: sharpen
help-string: "Specify sharpening."
- key: memory
required: false
type: string
platform-arg: memory
help-string: "When `true`, returns sequential data for QPU or noisy simulation runs. Default is `true`."
51 changes: 49 additions & 2 deletions utils/mock_qpu/ionq/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,12 @@ class Job(BaseModel):
# Save how many qubits were needed for each test (emulates real backend)
numQubitsRequired = 0

# Sets the target for the job
jobTarget = ""

# Sets the noise model for the job
noiseModel = ""

llvm.initialize()
llvm.initialize_native_target()
llvm.initialize_native_asmprinter()
Expand Down Expand Up @@ -141,7 +147,7 @@ async def postJob(job: Job,
@app.get("/v0.3/jobs")
async def getJob(id: str):
global countJobGetRequests, createdJobs, numQubitsRequired

global jobTarget, noiseModel
# Simulate asynchronous execution
if countJobGetRequests < 3:
countJobGetRequests += 1
Expand All @@ -152,9 +158,19 @@ async def getJob(id: str):
"jobs": [{
"status": "completed",
"qubits": numQubitsRequired,
"results_url": "/v0.3/jobs/{}/results".format(id)
"results_url": "/v0.3/jobs/{}/results".format(id),
"results": {
"shots": {
"url": "/v0.4/jobs/{}/results/shots".format(id)
}
}
}]
}
if jobTarget:
res["jobs"][0]["target"] = jobTarget
if noiseModel:
res["jobs"][0]["noise"] = {"model": noiseModel}

return res


Expand All @@ -178,6 +194,37 @@ async def getResults(jobId: str):
return res


@app.get("/v0.4/jobs/{jobId}/results/shots")
async def getResults(jobId: str):
global countJobGetRequests, createdJobs

counts = createdJobs[jobId]
counts.dump()
retData = []
# Note, the real IonQ backend reverses the bitstring relative to what the
# simulator does, so flip the bitstring with [::-1].
for bits, count in counts.items():
for _ in range(count):
retData.append(str(int(bits[::-1], 2)))

res = retData
return res


@app.post("/_mock_server_config_target")
async def set_mock_server_target(target: str):
global jobTarget
jobTarget = target
return {"status": "ok"}


@app.post("/_mock_server_config_noise_model")
async def set_mock_server_noise_model(noise: str):
global noiseModel
noiseModel = noise
return {"status": "ok"}


def startServer(port):
uvicorn.run(app, port=port, host='0.0.0.0', log_level="info")

Expand Down
Loading