527 lines
20 KiB
Plaintext
527 lines
20 KiB
Plaintext
{
|
|
"cells": [
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "5b6cbc5d-14d4-40c6-a702-0ce635c483d8",
|
|
"metadata": {},
|
|
"source": [
|
|
"# Representing quantum computers for the transpiler"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "aed00912-17af-4e4c-8639-77957b314b98",
|
|
"metadata": {
|
|
"tags": [
|
|
"version-info"
|
|
]
|
|
},
|
|
"source": [
|
|
"<details>\n",
|
|
"<summary><b>Package versions</b></summary>\n",
|
|
"\n",
|
|
"The code on this page was developed using the following requirements.\n",
|
|
"We recommend using these versions or newer.\n",
|
|
"\n",
|
|
"```\n",
|
|
"qiskit[all]~=2.0.0\n",
|
|
"qiskit-ibm-runtime~=0.37.0\n",
|
|
"```\n",
|
|
"</details>"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "9dbd70a7-3353-4286-aa6b-3547d7894f58",
|
|
"metadata": {},
|
|
"source": [
|
|
"To convert an abstract circuit to an ISA circuit that can run on a specific QPU (quantum processing unit), the transpiler needs certain information about the QPU. This information is found in two places: the `BackendV2` (or legacy `BackendV1`) object you plan to submit jobs to, and the backend's `Target` attribute.\n",
|
|
"\n",
|
|
"- The [`Target`](../api/qiskit/qiskit.transpiler.Target) contains all the relevant constraints of a device, such as its native basis gates, qubit connectivity, and pulse or timing information.\n",
|
|
"- The [`Backend`](../api/qiskit/qiskit.providers.BackendV2) possesses a `Target` by default, contains additional information -- such as the [`InstructionScheduleMap`](/docs/api/qiskit/1.4/qiskit.pulse.InstructionScheduleMap), and provides the interface for submitting quantum circuit jobs.\n",
|
|
"\n",
|
|
"You can also explicitly provide information for the transpiler to use, for example, if you have a specific use case, or if you believe this information will help the transpiler generate a more optimized circuit.\n",
|
|
"\n",
|
|
"\n",
|
|
"The precision with which the transpiler produces the most appropriate circuit for specific hardware depends on how much information the `Target` or `Backend` has about its constraints.\n",
|
|
"\n",
|
|
"<Admonition type=\"note\">\n",
|
|
"Because many of the underlying transpilation algorithms are stochastic, there is no guarantee that a better circuit will be found.\n",
|
|
"</Admonition>\n",
|
|
"\n",
|
|
"This page shows several examples of passing QPU information to the transpiler. These examples use the target from the [`FakeSherbrooke`](/docs/api/qiskit-ibm-runtime/fake-provider-fake-sherbrooke#fakesherbrooke) mock backend.\n",
|
|
"\n",
|
|
"<span id=\"default-config\"></span>\n",
|
|
"## Default configuration\n",
|
|
"\n",
|
|
"The simplest use of the transpiler is to provide all the QPU information by providing the `Backend` or `Target`. To better understand how the transpiler works, construct a circuit and transpile it with different information, as follows.\n",
|
|
"\n",
|
|
"Import the necessary libraries and instantiate the QPU:\n",
|
|
"In order to convert an abstract circuit to an ISA circuit that can run on a specific processor, the transpiler needs certain information about the processor. Typically, this information is stored in the [`Backend`](/docs/api/qiskit/qiskit.providers.Backend#backend) or [`Target`](/docs/api/qiskit/qiskit.transpiler.Target#target) provided to the transpiler, and no further information is needed. However, you can also explicitly provide information for the transpiler to use, for example, if you have a specific use case, or if you believe this information will help the transpiler generate a more optimized circuit.\n",
|
|
"\n",
|
|
"This topic shows several examples of passing information to the transpiler. These examples use the target from the [`FakeSherbrooke`](/docs/api/qiskit-ibm-runtime/fake-provider-fake-sherbrooke#fakesherbrooke) mock backend."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 1,
|
|
"id": "a52d0681-5436-4d7a-8d45-9832472e59ca",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from qiskit_ibm_runtime.fake_provider import FakeSherbrooke\n",
|
|
"\n",
|
|
"backend = FakeSherbrooke()\n",
|
|
"target = backend.target"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "4064b5ad-8a3a-462b-ae30-29576230c084",
|
|
"metadata": {},
|
|
"source": [
|
|
"The example circuit uses an instance of [`efficient_su2`](/docs/api/qiskit/qiskit.circuit.library.efficient_su2) from Qiskit's circuit library."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 2,
|
|
"id": "97f9acc1-ac53-4025-b413-485777932a9b",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"<Image src=\"/docs/images/guides/represent-quantum-computers/extracted-outputs/97f9acc1-ac53-4025-b413-485777932a9b-0.svg\" alt=\"Output of the previous code cell\" />"
|
|
]
|
|
},
|
|
"execution_count": 2,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"from qiskit.circuit.library import efficient_su2\n",
|
|
"\n",
|
|
"qc = efficient_su2(12, entanglement=\"circular\", reps=1)\n",
|
|
"\n",
|
|
"qc.draw(\"mpl\")"
|
|
]
|
|
},
|
|
{
|
|
"attachments": {},
|
|
"cell_type": "markdown",
|
|
"id": "b133229c-0146-4feb-a366-548d175b858e",
|
|
"metadata": {},
|
|
"source": [
|
|
"This example uses default settings to transpile to the `backend`'s `target`, which provides all the information needed to convert the circuit to one that will run on the backend."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 3,
|
|
"id": "4b81fb9d-d199-45c5-b119-c1f0b973afe9",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"<Image src=\"/docs/images/guides/represent-quantum-computers/extracted-outputs/4b81fb9d-d199-45c5-b119-c1f0b973afe9-0.svg\" alt=\"Output of the previous code cell\" />"
|
|
]
|
|
},
|
|
"execution_count": 3,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"from qiskit.transpiler import generate_preset_pass_manager\n",
|
|
"\n",
|
|
"pass_manager = generate_preset_pass_manager(\n",
|
|
" optimization_level=1, target=target, seed_transpiler=12345\n",
|
|
")\n",
|
|
"qc_t_target = pass_manager.run(qc)\n",
|
|
"qc_t_target.draw(\"mpl\", idle_wires=False, fold=-1)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "2ad3848d",
|
|
"metadata": {},
|
|
"source": [
|
|
"This example is used in later sections of this topic to illustrate that the coupling map and basis gates are the essential pieces of information to pass to the transpiler for optimal circuit construction. The QPU can usually select default settings for other information that is not passed in, such as timing and scheduling."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "956f1b7a-80b4-4cda-9ecf-6057f560deb9",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Coupling map\n",
|
|
"\n",
|
|
"The coupling map is a graph that shows which qubits are connected and hence have two-qubit gates between them. Sometimes this graph is directional, meaning that the two-qubit gates can only go in one direction. However, the transpiler can always flip a gate's direction by adding additional single-qubit gates. An abstract quantum circuit can always be represented on this graph, even if its connectivity is limited, by introducing SWAP gates to move the quantum information around.\n",
|
|
"\n",
|
|
"The qubits from our abstract circuits are called _virtual qubits_ and those on the coupling map are _physical qubits_. The transpiler provides a mapping between virtual and physical qubits. One of the first steps in transpilation, the _layout_ stage, performs this mapping.\n",
|
|
"\n",
|
|
"<Admonition type=\"note\">\n",
|
|
"Although the routing stage is intertwined with the _layout_ stage — which selects the actual qubits — by default, this topic treats them as separate stages for simplicity. The combination of routing and layout is called _qubit mapping_. Learn more about these stages in the [Transpiler stages](transpiler-stages) topic.\n",
|
|
"</Admonition>\n",
|
|
"\n",
|
|
"Pass the `coupling_map` keyword argument to see its effect on the transpiler:"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 4,
|
|
"id": "ec354bee-e06b-42ea-a117-6c1a9308ca73",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"<Image src=\"/docs/images/guides/represent-quantum-computers/extracted-outputs/ec354bee-e06b-42ea-a117-6c1a9308ca73-0.svg\" alt=\"Output of the previous code cell\" />"
|
|
]
|
|
},
|
|
"execution_count": 4,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"coupling_map = target.build_coupling_map()\n",
|
|
"\n",
|
|
"pass_manager = generate_preset_pass_manager(\n",
|
|
" optimization_level=0, coupling_map=coupling_map, seed_transpiler=12345\n",
|
|
")\n",
|
|
"qc_t_cm_lv0 = pass_manager.run(qc)\n",
|
|
"qc_t_cm_lv0.draw(\"mpl\", idle_wires=False, fold=-1)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "0d3fe7d4-3a40-423d-975f-b4de6613efe9",
|
|
"metadata": {},
|
|
"source": [
|
|
"As shown above, several SWAP gates were inserted (each consisting of three CX gates), which will cause a lot of errors on current devices. To see which qubits are selected on the actual qubit topology, use `plot_circuit_layout` from Qiskit Visualizations:"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 5,
|
|
"id": "9be74535-ed36-4d51-afeb-ee53c3f8a046",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"<Image src=\"/docs/images/guides/represent-quantum-computers/extracted-outputs/9be74535-ed36-4d51-afeb-ee53c3f8a046-0.svg\" alt=\"Output of the previous code cell\" />"
|
|
]
|
|
},
|
|
"execution_count": 5,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"from qiskit.visualization import plot_circuit_layout\n",
|
|
"\n",
|
|
"plot_circuit_layout(qc_t_cm_lv0, backend, view=\"physical\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "633744e3-b9e7-4333-ac7b-0089f28e9c27",
|
|
"metadata": {},
|
|
"source": [
|
|
"This shows that our virtual qubits 0-11 were trivially mapped to the line of physical qubits 0-11. Let's return to the default (`optimization_level=1`), which uses `VF2Layout` if any routing is required."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 6,
|
|
"id": "8035fd05-f7cd-4151-b19a-4968202246e6",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"<Image src=\"/docs/images/guides/represent-quantum-computers/extracted-outputs/8035fd05-f7cd-4151-b19a-4968202246e6-0.svg\" alt=\"Output of the previous code cell\" />"
|
|
]
|
|
},
|
|
"execution_count": 6,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"pass_manager = generate_preset_pass_manager(\n",
|
|
" optimization_level=1, coupling_map=coupling_map, seed_transpiler=12345\n",
|
|
")\n",
|
|
"qc_t_cm_lv1 = pass_manager.run(qc)\n",
|
|
"qc_t_cm_lv1.draw(\"mpl\", idle_wires=False, fold=-1)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "217c75f4-4c86-46bb-9a34-b451ff4743ef",
|
|
"metadata": {},
|
|
"source": [
|
|
"Now there are no SWAP gates inserted and the physical qubits selected are the same when using the `target` class."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 7,
|
|
"id": "25d9fac3-abda-4b2d-81b4-351dc0772722",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"<Image src=\"/docs/images/guides/represent-quantum-computers/extracted-outputs/25d9fac3-abda-4b2d-81b4-351dc0772722-0.svg\" alt=\"Output of the previous code cell\" />"
|
|
]
|
|
},
|
|
"execution_count": 7,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"from qiskit.visualization import plot_circuit_layout\n",
|
|
"\n",
|
|
"plot_circuit_layout(qc_t_cm_lv1, backend, view=\"physical\")"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "db62df0f",
|
|
"metadata": {},
|
|
"source": [
|
|
"Now the layout is in a ring. Because this layout respects the circuit's connectivity, there are no SWAP gates, providing a much better circuit for execution."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "f62e0541",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Basis gates\n",
|
|
"\n",
|
|
"Every quantum computer supports a limited instruction set, called its _basis gates_. Every gate in the circuit must be translated to the elements of this set. This set should consist of single- and two-qubit gates that provide a universal gates set, meaning that any quantum operation can be decomposed into those gates. This is done by the [BasisTranslator](../api/qiskit/qiskit.transpiler.passes.BasisTranslator), and the basis gates can be specified as a keyword argument to the transpiler to provide this information."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 8,
|
|
"id": "4445ff03",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"name": "stdout",
|
|
"output_type": "stream",
|
|
"text": [
|
|
"['sx', 'x', 'measure', 'reset', 'switch_case', 'rz', 'for_loop', 'id', 'ecr', 'delay', 'if_else']\n"
|
|
]
|
|
}
|
|
],
|
|
"source": [
|
|
"basis_gates = list(target.operation_names)\n",
|
|
"print(basis_gates)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "6e9aed06",
|
|
"metadata": {},
|
|
"source": [
|
|
"The default single-qubit gates on _ibm_sherbrooke_ are `rz`, `x`, and `sx`, and the default two-qubit gate is `ecr` (echoed cross-resonance). CX gates are constructed from `ecr` gates, so on some QPUs `ecr` is specified as the two-qubit basis gate, while on others `cx` is the default. The `ecr` gate is the _entangling_ part of the `cx` gate. To use a gate that is not in the basis gate set, follow instructions in the Qiskit SDK API documentation for custom gates using [pulse gates](/docs/api/qiskit/1.4/qiskit.transpiler.passes.PulseGates#pulsegates). In addition to the control gates, there are also `delay` and `measurement` instructions.\n",
|
|
"\n",
|
|
"<Admonition type=\"note\">\n",
|
|
" QPUs have default basis gates, but you can choose whatever gates you want, as long as you provide the instruction or add pulse gates (see [Create transpiler passes.](custom-transpiler-pass)) The default basis gates are those that calibrations have been done for on the QPU, so no further instruction/pulse gates need to be provided. For example, on some QPUs `cx` is the default two-qubit gate and `ecr` on others. See the [Native gates and operations](./native-gates) topic for more details.\n",
|
|
"</Admonition>"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 9,
|
|
"id": "313e4743",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"<Image src=\"/docs/images/guides/represent-quantum-computers/extracted-outputs/313e4743-0.svg\" alt=\"Output of the previous code cell\" />"
|
|
]
|
|
},
|
|
"execution_count": 9,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"pass_manager = generate_preset_pass_manager(\n",
|
|
" optimization_level=1,\n",
|
|
" coupling_map=coupling_map,\n",
|
|
" basis_gates=basis_gates,\n",
|
|
" seed_transpiler=12345,\n",
|
|
")\n",
|
|
"qc_t_cm_bg = pass_manager.run(qc)\n",
|
|
"qc_t_cm_bg.draw(\"mpl\", idle_wires=False, fold=-1)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "b0195ba6",
|
|
"metadata": {},
|
|
"source": [
|
|
"Note that the `CXGate`s have been decomposed to `ecr` gates and single-qubit basis gates."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "eb3f5aff-3ca2-4688-a59e-da05c724ef09",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Device error rates\n",
|
|
"\n",
|
|
"The `Target` class can contain information about the error rates for operations on the device.\n",
|
|
"For example, the following code retrieves the properties for the echoed cross-resonance (ECR) gate between qubit 1 and 0 (note that the ECR gate is directional):"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 10,
|
|
"id": "38aa1eca-a6aa-4db9-af36-66289e2059d2",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"InstructionProperties(duration=5.333333333333332e-07, error=0.005822316907363928)"
|
|
]
|
|
},
|
|
"execution_count": 10,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"target[\"ecr\"][(1, 0)]"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "90cca3a8-ca25-4795-a22f-57ba6d8cfcaa",
|
|
"metadata": {},
|
|
"source": [
|
|
"The output displays the duration of the gate (in seconds) and its error rate. To reveal error information to the transpiler, build a target model with the `basis_gates` and `coupling_map` from above and populate it with error values from the backend `FakeSherbrooke`."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 11,
|
|
"id": "e92c39d7-4aab-4657-aa9a-1b8b5788cb58",
|
|
"metadata": {},
|
|
"outputs": [],
|
|
"source": [
|
|
"from qiskit.transpiler import Target\n",
|
|
"from qiskit.circuit.controlflow import IfElseOp, SwitchCaseOp, ForLoopOp\n",
|
|
"\n",
|
|
"err_targ = Target.from_configuration(\n",
|
|
" basis_gates=basis_gates,\n",
|
|
" coupling_map=coupling_map,\n",
|
|
" num_qubits=target.num_qubits,\n",
|
|
" custom_name_mapping={\n",
|
|
" \"if_else\": IfElseOp,\n",
|
|
" \"switch_case\": SwitchCaseOp,\n",
|
|
" \"for_loop\": ForLoopOp,\n",
|
|
" },\n",
|
|
")\n",
|
|
"\n",
|
|
"for i, (op, qargs) in enumerate(target.instructions):\n",
|
|
" if op.name in basis_gates:\n",
|
|
" err_targ[op.name][qargs] = target.instruction_properties(i)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "433f2772-b397-42ab-b3a5-a44a85192f3c",
|
|
"metadata": {},
|
|
"source": [
|
|
"Transpile with our new target `err_targ` as the target:"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "code",
|
|
"execution_count": 12,
|
|
"id": "f1e270c4-e2cc-487e-a050-4180bc321b0b",
|
|
"metadata": {},
|
|
"outputs": [
|
|
{
|
|
"data": {
|
|
"text/plain": [
|
|
"<Image src=\"/docs/images/guides/represent-quantum-computers/extracted-outputs/f1e270c4-e2cc-487e-a050-4180bc321b0b-0.svg\" alt=\"Output of the previous code cell\" />"
|
|
]
|
|
},
|
|
"execution_count": 12,
|
|
"metadata": {},
|
|
"output_type": "execute_result"
|
|
}
|
|
],
|
|
"source": [
|
|
"pass_manager = generate_preset_pass_manager(\n",
|
|
" optimization_level=1, target=err_targ, seed_transpiler=12345\n",
|
|
")\n",
|
|
"qc_t_cm_bg_et = pass_manager.run(qc)\n",
|
|
"qc_t_cm_bg_et.draw(\"mpl\", idle_wires=False, fold=-1)"
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "f8b516be-805d-4d0b-92f9-44d3fbe3b51f",
|
|
"metadata": {},
|
|
"source": [
|
|
"Because the target includes error information, the `VF2PostLayout` pass tries to find the optimal qubits to use, resulting in the same circuit that was originally found with the same physical qubits."
|
|
]
|
|
},
|
|
{
|
|
"cell_type": "markdown",
|
|
"id": "e815a158-c77a-4e82-bebe-1710d0ae4f1f",
|
|
"metadata": {},
|
|
"source": [
|
|
"## Next steps\n",
|
|
"\n",
|
|
"<Admonition type=\"tip\" title=\"Recommendations\">\n",
|
|
" - Understand [Transpilation default settings and configuration options.](defaults-and-configuration-options)\n",
|
|
" - Review the [Commonly used parameters for transpilation](common-parameters) topic.\n",
|
|
" - Try the [Submit transpiled circuits](https://learning.quantum.ibm.com/tutorial/submit-transpiled-circuits) tutorial.\n",
|
|
" - See the [Transpile API documentation.](/docs/api/qiskit/transpiler)\n",
|
|
"</Admonition>"
|
|
]
|
|
}
|
|
],
|
|
"metadata": {
|
|
"description": "Learn about coupling maps, basis gates, and processor errors for transpiling",
|
|
"kernelspec": {
|
|
"display_name": "Python 3",
|
|
"language": "python",
|
|
"name": "python3"
|
|
},
|
|
"language_info": {
|
|
"codemirror_mode": {
|
|
"name": "ipython",
|
|
"version": 3
|
|
},
|
|
"file_extension": ".py",
|
|
"mimetype": "text/x-python",
|
|
"name": "python",
|
|
"nbconvert_exporter": "python",
|
|
"pygments_lexer": "ipython3",
|
|
"version": "3"
|
|
},
|
|
"title": "Represent quantum computers"
|
|
},
|
|
"nbformat": 4,
|
|
"nbformat_minor": 5
|
|
}
|