#!/usr/bin/env python # # file: qmllab/backends.py # # revision history: # 20260810 (am): initial version for workshop lab 02 # # Where the circuits actually run: a simulator, a simulator carrying a # real device's noise model, or a real quantum processor. # # Rehearse on a simulator, then perform on hardware -- session 3, # slide 11. #------------------------------------------------------------------------------ # import system modules # from pathlib import Path #------------------------------------------------------------------------------ # # global variables are listed here # #------------------------------------------------------------------------------ # the open plan allowance, and what one circuit costs against it # QPU_SECONDS_PER_MONTH = 600 QPU_SECONDS_PER_CIRCUIT = 0.31 #------------------------------------------------------------------------------ # # functions are listed here # #------------------------------------------------------------------------------ def qpu_cost(n_train, n_test=0): """ function: qpu_cost arguments: n_train: the number of training points n_test: the number of test points return: a dict of circuit count, qpu seconds and share of the allowance description: Estimates what a kernel job would cost before it is submitted. A symmetric kernel needs n(n-1)/2 circuits and a test block needs n_test * n_train more. Always check this first -- qpu time is the scarcest resource in the lab. """ # count the circuits both blocks require # circuits = n_train * (n_train - 1) // 2 + n_test * n_train seconds = circuits * QPU_SECONDS_PER_CIRCUIT # exit gracefully # return {"circuits": circuits, "qpu_seconds": seconds, "percent_of_monthly": 100.0 * seconds / QPU_SECONDS_PER_MONTH} # # end of function def discover_crn(token, verbose=True): """ function: discover_crn arguments: token: an ibm cloud api key verbose: print what was found return: a list of quantum-computing instance crns for this key description: Finds the crn of every Qiskit Runtime instance your api key can reach. This exists because an ibm cloud account usually owns several services -- object storage, a data catalog, and so on -- and the runtime client will happily attach to the wrong one if you do not name the instance. When that happens the failure is confusing: a dns error on a host like us-south.quantum-computing.cloud.ibm.com, which looks like a network problem but is really a wrong crn. Exchanging the api key for an iam token and asking the resource controller is the reliable way to get the right answer. """ # import here so a plain import of this module stays light # import requests # exchange the api key for a short-lived iam access token # reply = requests.post( "https://iam.cloud.ibm.com/identity/token", data={"grant_type": "urn:ibm:params:oauth:grant-type:apikey", "apikey": token}, headers={"Content-Type": "application/x-www-form-urlencoded"}, timeout=30) reply.raise_for_status() access_token = reply.json()["access_token"] # ask the resource controller for everything this key can see # listing = requests.get( "https://resource-controller.cloud.ibm.com/v2/resource_instances", headers={"Authorization": f"Bearer {access_token}"}, params={"limit": 100}, timeout=30) listing.raise_for_status() # keep only the quantum-computing services # found = [] for item in listing.json().get("resources", []): crn = item.get("crn", "") if ":quantum-computing:" in crn: found.append({"name": item.get("name"), "crn": crn, "state": item.get("state"), "region": item.get("region_id")}) # report what turned up # if verbose: if not found: print("no quantum-computing instance found for this api key.") print("create one at https://quantum.cloud.ibm.com (the Open " "plan is free).") for entry in found: print(f"name : {entry['name']} ({entry['state']}, " f"{entry['region']})") print(f"crn : {entry['crn']}") # exit gracefully # return found # # end of function def connect(api_key, crn=None, verbose=True): """ function: connect arguments: api_key: your IBM Quantum api key, pasted as a string crn: an instance crn, or None to find it automatically verbose: print which instance and processors were found return: a QiskitRuntimeService ready to use description: The easy way to reach IBM Quantum. Paste your api key, call this, and you are connected: service = backends.connect("YOUR_API_KEY") The crn is looked up for you. That matters because an ibm cloud account usually owns several services, and attaching to the wrong one fails with a confusing dns error rather than a clear message. See discover_crn below for the details. """ # import here so the module imports without a runtime installed # from qiskit_ibm_runtime import QiskitRuntimeService # reject an empty key with a message that says what to do # api_key = (api_key or "").strip() if not api_key: raise ValueError( "no api key given -- paste yours from " "https://quantum.cloud.ibm.com into the notebook cell above") # find the quantum instance unless the caller already knows it # if crn is None: found = discover_crn(api_key, verbose=False) if not found: raise RuntimeError( "this api key has no quantum-computing instance. create a " "free one at https://quantum.cloud.ibm.com and try again") crn = found[0]["crn"] if verbose: print(f"instance: {found[0]['name']} ({found[0]['region']})") # open the service # service = QiskitRuntimeService(channel="ibm_cloud", token=api_key, instance=crn) # report what is available, which costs no qpu time # if verbose: for row in list_hardware(service): print(f" {row['name']:16s} qubits={row['qubits']:4d} " f"queue={row['queue']:4d}") # exit gracefully # return service # # end of function def get_service(api_key=None, token_path=None, crn=None): """ function: get_service arguments: api_key: your api key as a string token_path: a file holding the api key, if you prefer to store it crn: an instance crn, or None to find it automatically return: a QiskitRuntimeService description: Same as connect, but it will also read the key out of a file when you would rather not paste it into a notebook. Supply either api_key or token_path. """ # read the key from a file when that is how it was supplied # if api_key is None and token_path is not None: api_key = Path(token_path).expanduser().read_text().strip() # exit gracefully # return connect(api_key, crn=crn, verbose=False) # # end of function def get_backend(kind="ideal", noise_model_from="FakeManilaV2", name=None, service=None): """ function: get_backend arguments: kind: ideal, noisy or hardware noise_model_from: the fake backend supplying a noise model name: a specific qpu, or None for the least busy one service: a live service, or None to open one return: a qiskit backend description: Resolves the three places circuits can run in this lab. The noisy option is an aer simulator carrying a real device's error rates, which is the cheapest way to rehearse hardware conditions. """ # a noiseless simulator # if kind == "ideal": from qiskit_aer import AerSimulator return AerSimulator() # a simulator carrying a real device's noise model # if kind == "noisy": from qiskit_aer import AerSimulator from qiskit_ibm_runtime import fake_provider fake = getattr(fake_provider, noise_model_from)() return AerSimulator.from_backend(fake) # a real quantum processor # if kind == "hardware": service = service or get_service() if name: return service.backend(name) return service.least_busy(operational=True, simulator=False) # exit ungracefully -- unknown backend kind # raise ValueError(f"unknown backend kind {kind!r}") # # end of function def list_hardware(service=None): """ function: list_hardware arguments: service: a live service, or None to open one return: a list of dicts describing each available qpu description: Reports the available processors and their queue depths. Listing backends costs no qpu time. """ # open a service if the caller did not supply one # service = service or get_service() # collect the status of every backend # rows = [] for backend in service.backends(): status = backend.status() rows.append({"name": backend.name, "qubits": backend.num_qubits, "queue": status.pending_jobs, "operational": status.operational}) # exit gracefully # return rows # # end of function # # end of file