diff --git a/docs/guides/primitive-input-output.ipynb b/docs/guides/primitive-input-output.ipynb index cd40eb5cb5f..daf15bcdcd6 100644 --- a/docs/guides/primitive-input-output.ipynb +++ b/docs/guides/primitive-input-output.ipynb @@ -80,14 +80,22 @@ "metadata": {}, "outputs": [], "source": [ - "from qiskit_ibm_runtime import QiskitRuntimeService\n", - "from qiskit.circuit import Parameter, QuantumCircuit\n", + "from qiskit.circuit import (\n", + " Parameter,\n", + " QuantumCircuit,\n", + " ClassicalRegister,\n", + " QuantumRegister,\n", + ")\n", + "from qiskit.transpiler import generate_preset_pass_manager\n", + "from qiskit.quantum_info import SparsePauliOp\n", + "from qiskit.primitives.containers import BitArray\n", + "\n", "from qiskit_ibm_runtime import (\n", + " QiskitRuntimeService,\n", " EstimatorV2 as Estimator,\n", " SamplerV2 as Sampler,\n", ")\n", - "from qiskit.transpiler import generate_preset_pass_manager\n", - "from qiskit.quantum_info import SparsePauliOp\n", + "\n", "import numpy as np\n", "\n", "# Instantiate runtime service and get\n", @@ -109,7 +117,6 @@ "transpiled_circuit = pm.run(circuit)\n", "layout = transpiled_circuit.layout\n", "\n", - "\n", "# Now define a sweep over parameter values, the last axis of dimension 2 is\n", "# for the two parameters \"a\" and \"b\"\n", "params = np.vstack(\n", @@ -408,76 +415,167 @@ "source": [ "### Sampler output\n", "\n", + "When a Sampler job is completed successfully, the returned [`PrimitiveResult`](../api/qiskit/qiskit.primitives.PrimitiveResult) object contains a list of [`SamplerPubResult`](../api/qiskit/qiskit.primitives.SamplerPubResult)s, one per PUB. The databins of these `SamplerPubResult`s are dict-like objects that contain one `BitArray`s per `ClassicalRegister` in the circuit.\n", "\n", - "The Sampler primitive outputs job results in a similar format, with the exception that each `DataBin` will contain one or more `BitArray` objects which store the samples of the circuit attached to a particular `ClassicalRegister`, typically one bitstring per shot. The attribute label for each bit array object depends on the `ClassicalRegisters` defined in the circuit being executed. The measurement data from these `BitArrays` can then be processed into a dictionary with key-value pairs corresponding to each bitstring measured (for example, '1011001') and the number of times (or counts) it was measured.\n", + "The `BitArray` class is a container for ordered shot data. In more detail, it stores the information sampled bitstrings as bytes inside a two-dimensional array. The left-most axis of this array runs over ordered shots, while the right-most axis runs over bytes.\n", "\n", - "For example, a circuit that has measurement instructions added by the [`QuantumCircuit.measure_all()`](../api/qiskit/qiskit.circuit.QuantumCircuit#measure_all) function possesses a classical register with the label *'meas'*. After execution, a count data dictionary can be created by executing:" + "As a first example, let us look at the following ten-qubit circuit:" ] }, { "cell_type": "code", - "execution_count": 4, - "id": "a2316ece-c1ef-49b9-ae07-860decb0747d", + "execution_count": null, + "id": "40f4fa9d-9464-4063-b8e9-a1b57c42a579", "metadata": {}, "outputs": [], "source": [ - "# Add measurement instructions to the example circuit\n", + "# generate a ten-qubit GHZ circuit\n", + "circuit = QuantumCircuit(10)\n", + "circuit.h(0)\n", + "circuit.cx(range(0, 9), range(1, 10))\n", + "\n", + "# append measurements with the `measure_all` method\n", "circuit.measure_all()\n", "\n", - "# Transpile the circuit\n", - "pm = generate_preset_pass_manager(optimization_level=1, backend=backend)\n", + "# transpile the circuit\n", "transpiled_circuit = pm.run(circuit)\n", "\n", - "# Create a PUB for the Sampler primitive using the same parameters defined earlier\n", - "sampler_pub = (transpiled_circuit, params)\n", + "# run the Sampler job and retrieve the results\n", + "sampler = Sampler(mode=backend)\n", + "job = sampler.run([transpiled_circuit])\n", + "result = job.result()\n", + "\n", + "# the databin contains one BitArray\n", + "data = result[0].data\n", + "print(f\"Databin: {data}\\n\")\n", + "\n", + "# to access the BitArray, use the key \"meas\", which is the default name of\n", + "# the classical register when this is added by the `measure_all` method\n", + "array = data.meas\n", + "print(f\"BitArray: {array}\\n\")\n", + "print(f\"The shape of register `meas` is {data.meas.array.shape}.\\n\")\n", + "print(f\"The bytes in register `alpha`, shot by shot:\\n{data.meas.array}\\n\")" + ] + }, + { + "cell_type": "markdown", + "id": "9739b9d5-26e0-45a0-b301-f206c9f95fd3", + "metadata": {}, + "source": [ + "It can sometimes be convenient to convert away from the bytes format in the `BitArray` to bitstrings. The `get_count` method returns a dictionary mapping bitstrings to the number of times that they occurred." + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "d3654210-34f9-4db4-a08b-28cb01f5d433", + "metadata": {}, + "outputs": [], + "source": [ + "# optionally, convert away from the native BitArray format to a dictionary format\n", + "counts = data.meas.get_counts()\n", + "print(f\"Counts: {counts}\")" + ] + }, + { + "cell_type": "markdown", + "id": "61c70ff7-7814-4535-9268-ac163c10121c", + "metadata": {}, + "source": [ + "When a circuit contains more than one classical register, the results are stored in different `BitArray`s. The following example modifies the previous snippet by splitting the classical register into two distinct registers:" + ] + }, + { + "cell_type": "code", + "execution_count": null, + "id": "7c21a74d-26b0-4494-8d5f-92bd5c0cbdc2", + "metadata": {}, + "outputs": [], + "source": [ + "# generate a ten-qubit GHZ circuit with two classical registers\n", + "circuit = QuantumCircuit(\n", + " qreg := QuantumRegister(10),\n", + " alpha := ClassicalRegister(1, \"alpha\"),\n", + " beta := ClassicalRegister(9, \"beta\"),\n", + ")\n", + "circuit.h(0)\n", + "circuit.cx(range(0, 9), range(1, 10))\n", + "\n", + "# append measurements with the `measure_all` method\n", + "circuit.measure([0], alpha)\n", + "circuit.measure(range(1, 10), beta)\n", "\n", + "# transpile the circuit\n", + "transpiled_circuit = pm.run(circuit)\n", "\n", + "# run the Sampler job and retrieve the results\n", "sampler = Sampler(mode=backend)\n", - "job = sampler.run([sampler_pub])\n", - "result = job.result()" + "job = sampler.run([transpiled_circuit])\n", + "result = job.result()\n", + "\n", + "# the databin contains two BitArrays, one per register, and can be accessed\n", + "# as attributes using the registers' names\n", + "data = result[0].data\n", + "print(f\"BitArray for register 'alpha': {data.alpha}\")\n", + "print(f\"BitArray for register 'beta': {data.beta}\")" + ] + }, + { + "cell_type": "markdown", + "id": "ed24d1ba-e2ab-4d02-b218-c61d9336080e", + "metadata": {}, + "source": [ + "#### Leveraging `BitArray`s for performant post-processing\n", + "\n", + "Since arrays generally offer better performance compared to dictionaries, it is advisable to perform any post-processing directly on the `BitArray`s rather than on dictionaries of counts. The `BitArray` class offers a range of methods to perform some common post-processing operations:" ] }, { "cell_type": "code", - "execution_count": 5, - "id": "f459cd09-3243-487c-9fc4-a8e7a5589e4e", + "execution_count": null, + "id": "7682a2d3-be68-4822-a59f-7a5e8e55c11a", "metadata": {}, - "outputs": [ - { - "name": "stdout", - "output_type": "stream", - "text": [ - "The result of the submitted job had 1 PUB and has a value:\n", - " PrimitiveResult([SamplerPubResult(data=DataBin(meas=BitArray(), shape=(100,)), metadata={'circuit_metadata': {}})], metadata={'execution': {'execution_spans': ExecutionSpans([DoubleSliceSpan()])}, 'version': 2})\n", - "\n", - "The associated PubResult of this Sampler job has the following DataBins:\n", - " DataBin(meas=BitArray(), shape=(100,))\n", - "\n", - "It has a key-value pair dict: \n", - "dict_items([('meas', BitArray())])\n", - "\n" - ] - }, - { - "name": "stdout", - "output_type": "stream", - "text": [ - "And the raw data can be converted to a bitstring-count format: \n", - "{'11': 107172, '10': 101645, '01': 100212, '00': 100571}\n" - ] - } - ], + "outputs": [], "source": [ + "print(f\"The shape of register `alpha` is {data.alpha.array.shape}.\")\n", + "print(f\"The bytes in register `alpha`, shot by shot:\\n{data.alpha.array}\\n\")\n", + "\n", + "print(f\"The shape of register `beta` is {data.beta.array.shape}.\")\n", + "print(f\"The bytes in register `beta`, shot by shot:\\n{data.beta.array}\\n\")\n", + "\n", + "# post-select the bitstrings of `beta` based on having sampled \"1\" in `alpha`\n", + "mask = data.alpha.array == \"0b1\"\n", + "ps_beta = data.beta[mask[:, 0]]\n", + "print(f\"The shape of `beta` after post-selection is {ps_beta.array.shape}.\")\n", + "print(f\"The bytes in `beta` after post-selection:\\n{ps_beta.array}\")\n", + "\n", + "# get a slice of `beta` to retrieve the first three bits\n", + "beta_sl_bits = data.beta.slice_bits([0, 1, 2])\n", "print(\n", - " f\"The result of the submitted job had {len(result)} PUB and has a value:\\n {result}\\n\"\n", + " f\"The shape of `beta` after bit-wise slicing is {beta_sl_bits.array.shape}.\"\n", ")\n", + "print(f\"The bytes in `beta` after bit-wise slicing:\\n{beta_sl_bits.array}\\n\")\n", + "\n", + "# get a slice of `beta` to retrieve the bytes of the first five shots\n", + "beta_sl_shots = data.beta.slice_shots([0, 1, 2, 3, 4])\n", "print(\n", - " f\"The associated PubResult of this Sampler job has the following DataBins:\\n {result[0].data}\\n\"\n", + " f\"The shape of `beta` after shot-wise slicing is {beta_sl_shots.array.shape}.\"\n", ")\n", - "print(f\"It has a key-value pair dict: \\n{result[0].data.items()}\\n\")\n", "print(\n", - " f\"And the raw data can be converted to a bitstring-count format: \\n{result[0].data.meas.get_counts()}\"\n", - ")" + " f\"The bytes in `beta` after shot-wise slicing:\\n{beta_sl_shots.array}\\n\"\n", + ")\n", + "\n", + "# calculate the expectation value of diagonal operators on `beta`\n", + "ops = [SparsePauliOp(\"ZZZZZZZZZ\"), SparsePauliOp(\"IIIIIIIIZ\")]\n", + "exp_vals = data.beta.expectation_values(ops)\n", + "for o, e in zip(ops, exp_vals):\n", + " print(f\"Exp. val. for observable `{o}` is: {e}\")\n", + "\n", + "# concatenate the bitstrings in `alpha` and `beta` to \"merge\" the results of the two\n", + "# registers\n", + "merged_results = BitArray.concatenate_bits([data.alpha, data.beta])\n", + "print(f\"\\nThe shape of the merged results is {merged_results.array.shape}.\")\n", + "print(f\"The bytes of the merged results:\\n{merged_results.array}\\n\")" ] }, {