Skip to content

Write public guide for using BitArray directly #2612

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 16 commits into
base: main
Choose a base branch
from
194 changes: 146 additions & 48 deletions docs/guides/primitive-input-output.ipynb
Original file line number Diff line number Diff line change
Expand Up @@ -80,14 +80,22 @@
"metadata": {},
Copy link
Contributor

@ihincks ihincks Feb 19, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think this is a nice simplification/clarification in plainer english. I also think that one of the first things that should be said after BitArray is mentioned is that it is just a container for ordered shot data. Then, in the next paragraph, it can be emphasized, somehow, that get_counts is a convenience to change formats.


Reply via ReviewNB

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

"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",
Expand All @@ -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",
Expand Down Expand Up @@ -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,), num_shots=4096, num_bits=2>), shape=(100,)), metadata={'circuit_metadata': {}})], metadata={'execution': {'execution_spans': ExecutionSpans([DoubleSliceSpan(<start='2025-03-01 09:16:09', stop='2025-03-01 09:17:57', size=409600>)])}, 'version': 2})\n",
"\n",
"The associated PubResult of this Sampler job has the following DataBins:\n",
" DataBin(meas=BitArray(<shape=(100,), num_shots=4096, num_bits=2>), shape=(100,))\n",
"\n",
"It has a key-value pair dict: \n",
"dict_items([('meas', BitArray(<shape=(100,), num_shots=4096, num_bits=2>))])\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\")"
]
},
{
Expand Down
Loading