diff --git a/examples/overview_endtoend.ipynb b/examples/overview_endtoend.ipynb index 8fc85b24b..e4a329b1a 100644 --- a/examples/overview_endtoend.ipynb +++ b/examples/overview_endtoend.ipynb @@ -701,19 +701,21 @@ "os.environ['QEMIST_AUTH_TOKEN'] = \"your_qemist_authentication_token\"\n", "\n", "# Estimate, submit and get the results of your job / quantum task through our wrappers\n", - "from tangelo.linq.qpu_connection import job_submit, job_status, job_cancel, job_result, job_estimate\n", + "from tangelo.linq.qpu_connection import QEMISTCloudConnection\n", + "\n", + "qcloud_connection = QEMISTCloudConnection()\n", "\n", "circuit_YY = quantum_circuit[((0, \"Y\"), (1, \"Y\"))]\n", "\n", - "price_estimates = job_estimate(circuit_YY, n_shots=n_shots)\n", + "price_estimates = qcloud_connection.job_estimate(circuit_YY, n_shots=n_shots)\n", "print(price_estimates)\n", "\n", "backend = 'arn:aws:braket:::device/qpu/ionq/ionQdevice'\n", "\n", "# This two commands would respecfully submit the job to the quantum device and make a blocking call\n", "# to retrieve the results, through a job ID returned by QEMIST Cloud\n", - "# job_id = job_submit(circuit_YY, n_shots=n_shots, backend=backend)\n", - "# freqs, raw_data = job_result(job_id)\n", + "# job_id = qcloud_connection.job_submit(circuit_YY, n_shots=n_shots, backend=backend)\n", + "# freqs, raw_data = qcloud_connection.job_results(job_id)\n", "```\n", "\n", "Output:\n", diff --git a/examples/qemist_cloud_hardware_experiments_braket.ipynb b/examples/qemist_cloud_hardware_experiments_braket.ipynb index 8728923ad..107c2aa3c 100755 --- a/examples/qemist_cloud_hardware_experiments_braket.ipynb +++ b/examples/qemist_cloud_hardware_experiments_braket.ipynb @@ -111,7 +111,8 @@ "metadata": {}, "outputs": [], "source": [ - "from tangelo.linq.qpu_connection import job_submit, job_status, job_cancel, job_result, job_estimate" + "from tangelo.linq.qpu_connection import QEMISTCloudConnection\n", + "qcloud_connection = QEMISTCloudConnection()" ] }, { @@ -141,7 +142,7 @@ } ], "source": [ - "price_estimates = job_estimate(circuit, n_shots=1000)\n", + "price_estimates = qcloud_connection.job_estimate(circuit, n_shots=1000)\n", "print(price_estimates)" ] }, @@ -190,7 +191,7 @@ } ], "source": [ - "job_id = job_submit(circuit, n_shots=100, backend=backend)\n", + "job_id = qcloud_connection.job_submit(circuit, n_shots=100, backend=backend)\n", "print(f\"Job submitted with job id :: {job_id}\")" ] }, @@ -219,7 +220,7 @@ } ], "source": [ - "print(job_status(job_id))" + "print(qcloud_connection.job_status(job_id))" ] }, { @@ -237,7 +238,7 @@ "metadata": {}, "outputs": [], "source": [ - "# print(job_cancel(job_id))" + "# print(qcloud_connection.job_cancel(job_id))" ] }, { @@ -272,7 +273,7 @@ } ], "source": [ - "freqs, raw_data = job_result(job_id)\n", + "freqs, raw_data = qcloud_connection.job_results(job_id)\n", "print(f\"Frequencies :: {freqs}\")" ] }, @@ -305,9 +306,9 @@ ], "metadata": { "kernelspec": { - "display_name": "tangelo_docs_aesthetics", + "display_name": "qemist", "language": "python", - "name": "tangelo_docs_aesthetics" + "name": "qemist" }, "language_info": { "codemirror_mode": { @@ -319,7 +320,7 @@ "name": "python", "nbconvert_exporter": "python", "pygments_lexer": "ipython3", - "version": "3.8.10" + "version": "3.9.9" } }, "nbformat": 4, diff --git a/tangelo/linq/qpu_connection/__init__.py b/tangelo/linq/qpu_connection/__init__.py index 1c95aeeeb..4e59fa043 100644 --- a/tangelo/linq/qpu_connection/__init__.py +++ b/tangelo/linq/qpu_connection/__init__.py @@ -12,5 +12,5 @@ # See the License for the specific language governing permissions and # limitations under the License. -from .qemist_cloud_connection import job_submit, job_status, job_cancel, job_result, job_estimate +from .qemist_cloud_connection import QEMISTCloudConnection from .ionq_connection import IonQConnection diff --git a/tangelo/linq/qpu_connection/qemist_cloud_connection.py b/tangelo/linq/qpu_connection/qemist_cloud_connection.py index f6595df23..848c284d2 100644 --- a/tangelo/linq/qpu_connection/qemist_cloud_connection.py +++ b/tangelo/linq/qpu_connection/qemist_cloud_connection.py @@ -19,142 +19,145 @@ QEMIST_PROJECT_ID with values retrieved from their QEMIST Cloud dashboard. """ -try: - import qemist_client.util as qclient_util -except ModuleNotFoundError: - print("qemist_client python package not found (optional dependency for hardware experiment submission)") - - -def job_submit(circuit, n_shots, backend): - """Job submission to run a circuit on quantum hardware. - - Args: - circuit: a quantum circuit in the abstract format. - n_shots (int): the number of shots. - backend (str): the identifier string for the desired backend. - - Returns: - int: A problem handle / job ID that can be used to retrieve the result - or cancel the problem. - """ - - # Serialize circuit data - circuit_data = circuit.serialize() - - # Build option dictionary - job_options = {'shots': n_shots, 'backend': backend} - - # Submit the problem - qemist_cloud_job_id = qclient_util.solve_quantum_circuits_async(serialized_fragment=circuit_data, - serialized_solver=job_options)[0] - return qemist_cloud_job_id - - -def job_status(qemist_cloud_job_id): - """Returns the current status of the problem, as a string. Possible values: - ready, in_progress, complete, cancelled. - - Args: - qemist_cloud_job_id (int): problem handle / job identifier. - - Returns: - str: current status of the problem, as a string. - """ - res = qclient_util.get_problem_statuses(qemist_cloud_job_id) - - return res - - -def job_cancel(qemist_cloud_job_id): - """Cancels the job matching the input job id, if done in time before it - starts. - - Args: - qemist_cloud_job_id (int): problem handle / job identifier. - - Returns: - dict: cancelled problems / subproblems. - """ - res = qclient_util.cancel_problems(qemist_cloud_job_id) - # TODO: If res is coming out as an error code, we should raise an error - - return res - - -def job_result(qemist_cloud_job_id): - """Blocks until the job results are available. Returns a tuple containing - the histogram of frequencies, and also the more in-depth raw data from the - cloud services provider as a nested dictionary - - Args: - qemist_cloud_job_id (int): problem handle / job identifier. - - Returns: - dict: Histogram of measurement frequencies. - dict: The cloud provider raw data. - """ - - try: - qclient_util.monitor_problem_status(problem_handles=[qemist_cloud_job_id], verbose=False) - - except KeyboardInterrupt: - print(f"\nYour problem is still running with id {qemist_cloud_job_id}.\n") - command = input("Type 'cancel' and return to cancel your problem." - "Type anything else to disconnect but keep the problem running.\n") - if command.lower() == "cancel": - ret = job_cancel(qemist_cloud_job_id) - print("Problem cancelled.", ret) - else: - print(f"Reconnect and block until the problem is complete with " - f"qemist_client.util.monitor_problem_status({qemist_cloud_job_id}).\n\n") - raise - - except Exception: - print(f"\n\nYour problem is still running with handle {qemist_cloud_job_id}.\n" - f"Cancel the problem with qemist_client.util.cancel_problems({qemist_cloud_job_id}).\n" - f"Reconnect and block until the problem is complete with qemist_client.util.monitor_problem_status({qemist_cloud_job_id}).\n\n") - raise - - # Once a result is available, retrieve it. - # If the util module is not found earlier, an error has been raised. - output = qclient_util.get_results(qemist_cloud_job_id) - - # Amazon Braket: parsing of output - freqs = output['results']['measurement_probabilities'] - raw_data = output - - return freqs, raw_data - - -def job_estimate(circuit, n_shots, backend=None): - """Returns an estimate of the cost of running an experiment, for a specified backend - or all backends available. Some service providers care about the - complexity / structure of the input quantum circuit, some do not. - - The backend identifier strings that a user can provide as argument can be obtained - by calling this function without specifying a backend. They appear as keys in - the returned dictionary. These strings may change with time, as we adjust to the - growing cloud quantum offer (services and devices). - - Args: - circuit (Circuit): the abstract circuit to be run on the target device. - n_shots (int): number of shots in the expriment. - backend (str): the identifier string for the desired backend. - - Returns: - dict: Returns dict of prices in USD. If backend is not None, dictionary - contains the cost for running the desired job. If backend is None, - returns dictionary of prices for all supported backends. - """ - - # Serialize circuit data - circuit_data = circuit.serialize() - - # Build option dictionary - job_options = {'shots': n_shots} - if backend: - job_options['backend'] = backend - - price_estimate = qclient_util.check_qpu_cost(circuit_data, job_options) - - return price_estimate +from tangelo.linq.qpu_connection.qpu_connection import QpuConnection + + +class QEMISTCloudConnection(QpuConnection): + """ Wrapper about the QEMIST Cloud connection to QPUs. """ + + def __init__(self): + try: + import qemist_client.util as qclient_util + except ModuleNotFoundError: + raise ModuleNotFoundError("qemist_client python package not found (optional dependency for hardware experiment submission)") + self.qclient_util = qclient_util + + def job_submit(self, circuit, n_shots, backend): + """Job submission to run a circuit on quantum hardware. + + Args: + circuit: a quantum circuit in the abstract format. + n_shots (int): the number of shots. + backend (str): the identifier string for the desired backend. + + Returns: + int: A problem handle / job ID that can be used to retrieve the result + or cancel the problem. + """ + + # Serialize circuit data + circuit_data = circuit.serialize() + + # Build option dictionary + job_options = {'shots': n_shots, 'backend': backend} + + # Submit the problem + qemist_cloud_job_id = self.qclient_util.solve_quantum_circuits_async(serialized_fragment=circuit_data, + serialized_solver=job_options)[0] + return qemist_cloud_job_id + + def job_status(self, qemist_cloud_job_id): + """Returns the current status of the problem, as a string. Possible values: + ready, in_progress, complete, cancelled. + + Args: + qemist_cloud_job_id (int): problem handle / job identifier. + + Returns: + str: current status of the problem, as a string. + """ + res = self.qclient_util.get_problem_statuses(qemist_cloud_job_id) + + return res + + def job_cancel(self, qemist_cloud_job_id): + """Cancels the job matching the input job id, if done in time before it + starts. + + Args: + qemist_cloud_job_id (int): problem handle / job identifier. + + Returns: + dict: cancelled problems / subproblems. + """ + res = self.qclient_util.cancel_problems(qemist_cloud_job_id) + # TODO: If res is coming out as an error code, we should raise an error + + return res + + def job_results(self, qemist_cloud_job_id): + """Blocks until the job results are available. Returns a tuple containing + the histogram of frequencies, and also the more in-depth raw data from the + cloud services provider as a nested dictionary + + Args: + qemist_cloud_job_id (int): problem handle / job identifier. + + Returns: + dict: Histogram of measurement frequencies. + dict: The cloud provider raw data. + """ + + try: + self.qclient_util.monitor_problem_status(problem_handles=[qemist_cloud_job_id], verbose=False) + + except KeyboardInterrupt: + print(f"\nYour problem is still running with id {qemist_cloud_job_id}.\n") + command = input("Type 'cancel' and return to cancel your problem." + "Type anything else to disconnect but keep the problem running.\n") + if command.lower() == "cancel": + ret = self.job_cancel(qemist_cloud_job_id) + print("Problem cancelled.", ret) + else: + print(f"Reconnect and block until the problem is complete with " + f"qemist_client.util.monitor_problem_status({qemist_cloud_job_id}).\n\n") + raise + + except Exception: + print(f"\n\nYour problem is still running with handle {qemist_cloud_job_id}.\n" + f"Cancel the problem with qemist_client.util.cancel_problems({qemist_cloud_job_id}).\n" + f"Reconnect and block until the problem is complete with qemist_client.util.monitor_problem_status({qemist_cloud_job_id}).\n\n") + raise + + # Once a result is available, retrieve it. + # If the util module is not found earlier, an error has been raised. + output = self.qclient_util.get_results(qemist_cloud_job_id) + + # Amazon Braket: parsing of output + freqs = output['results']['measurement_probabilities'] + raw_data = output + + return freqs, raw_data + + def job_estimate(self, circuit, n_shots, backend=None): + """Returns an estimate of the cost of running an experiment, for a specified backend + or all backends available. Some service providers care about the + complexity / structure of the input quantum circuit, some do not. + + The backend identifier strings that a user can provide as argument can be obtained + by calling this function without specifying a backend. They appear as keys in + the returned dictionary. These strings may change with time, as we adjust to the + growing cloud quantum offer (services and devices). + + Args: + circuit (Circuit): the abstract circuit to be run on the target device. + n_shots (int): number of shots in the expriment. + backend (str): the identifier string for the desired backend. + + Returns: + dict: Returns dict of prices in USD. If backend is not None, dictionary + contains the cost for running the desired job. If backend is None, + returns dictionary of prices for all supported backends. + """ + + # Serialize circuit data + circuit_data = circuit.serialize() + + # Build option dictionary + job_options = {'shots': n_shots} + if backend: + job_options['backend'] = backend + + price_estimate = self.qclient_util.check_qpu_cost(circuit_data, job_options) + + return price_estimate