Add option to user config to control `idle_wires` in circuit drawer (#12462)

* Add option to user config to control idle_wires in circuit drawer

Co-Authored-By: diemilio <diemilio@live.com>

* docs

* 11339

* Update qiskit/visualization/circuit/circuit_visualization.py

Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>

---------

Co-authored-by: diemilio <diemilio@live.com>
Co-authored-by: Elena Peña Tapia <57907331+ElePT@users.noreply.github.com>
This commit is contained in:
Luciano Bello 2024-06-12 11:14:48 +02:00 committed by GitHub
parent bc685d3002
commit b933f6d377
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
7 changed files with 139 additions and 22 deletions

View File

@ -1757,14 +1757,14 @@ class QuantumCircuit:
this can be anything that :obj:`.append` will accept.
qubits (list[Qubit|int]): qubits of self to compose onto.
clbits (list[Clbit|int]): clbits of self to compose onto.
front (bool): If True, front composition will be performed. This is not possible within
front (bool): If ``True``, front composition will be performed. This is not possible within
control-flow builder context managers.
inplace (bool): If True, modify the object. Otherwise return composed circuit.
inplace (bool): If ``True``, modify the object. Otherwise, return composed circuit.
copy (bool): If ``True`` (the default), then the input is treated as shared, and any
contained instructions will be copied, if they might need to be mutated in the
future. You can set this to ``False`` if the input should be considered owned by
the base circuit, in order to avoid unnecessary copies; in this case, it is not
valid to use ``other`` afterwards, and some instructions may have been mutated in
valid to use ``other`` afterward, and some instructions may have been mutated in
place.
var_remap (Mapping): mapping to use to rewrite :class:`.expr.Var` nodes in ``other`` as
they are inlined into ``self``. This can be used to avoid naming conflicts.
@ -2068,7 +2068,7 @@ class QuantumCircuit:
Args:
other (QuantumCircuit): The other circuit to tensor this circuit with.
inplace (bool): If True, modify the object. Otherwise return composed circuit.
inplace (bool): If ``True``, modify the object. Otherwise return composed circuit.
Examples:
@ -2084,7 +2084,7 @@ class QuantumCircuit:
tensored.draw('mpl')
Returns:
QuantumCircuit: The tensored circuit (returns None if inplace==True).
QuantumCircuit: The tensored circuit (returns ``None`` if ``inplace=True``).
"""
num_qubits = self.num_qubits + other.num_qubits
num_clbits = self.num_clbits + other.num_clbits
@ -3126,7 +3126,7 @@ class QuantumCircuit:
reverse_bits: bool | None = None,
justify: str | None = None,
vertical_compression: str | None = "medium",
idle_wires: bool = True,
idle_wires: bool | None = None,
with_layout: bool = True,
fold: int | None = None,
# The type of ax is matplotlib.axes.Axes, but this is not a fixed dependency, so cannot be
@ -3157,7 +3157,7 @@ class QuantumCircuit:
Args:
output: Select the output method to use for drawing the circuit.
Valid choices are ``text``, ``mpl``, ``latex``, ``latex_source``.
By default the `text` drawer is used unless the user config file
By default, the ``text`` drawer is used unless the user config file
(usually ``~/.qiskit/settings.conf``) has an alternative backend set
as the default. For example, ``circuit_drawer = latex``. If the output
kwarg is set, that backend will always be used over the default in
@ -3203,7 +3203,9 @@ class QuantumCircuit:
will take less vertical room. Default is ``medium``. Only used by
the ``text`` output, will be silently ignored otherwise.
idle_wires: Include idle wires (wires with no circuit elements)
in output visualization. Default is ``True``.
in output visualization. Default is ``True`` unless the
user config file (usually ``~/.qiskit/settings.conf``) has an
alternative value set. For example, ``circuit_idle_wires = False``.
with_layout: Include layout information, with labels on the
physical layout. Default is ``True``.
fold: Sets pagination. It can be disabled using -1. In ``text``,
@ -3292,7 +3294,7 @@ class QuantumCircuit:
Args:
filter_function (callable): a function to filter out some instructions.
Should take as input a tuple of (Instruction, list(Qubit), list(Clbit)).
By default filters out "directives", such as barrier or snapshot.
By default, filters out "directives", such as barrier or snapshot.
Returns:
int: Total number of gate operations.
@ -3314,7 +3316,7 @@ class QuantumCircuit:
filter_function: A function to decide which instructions count to increase depth.
Should take as a single positional input a :class:`CircuitInstruction`.
Instructions for which the function returns ``False`` are ignored in the
computation of the circuit depth. By default filters out "directives", such as
computation of the circuit depth. By default, filters out "directives", such as
:class:`.Barrier`.
Returns:
@ -3445,7 +3447,7 @@ class QuantumCircuit:
bits = self.qubits if unitary_only else (self.qubits + self.clbits)
bit_indices: dict[Qubit | Clbit, int] = {bit: idx for idx, bit in enumerate(bits)}
# Start with each qubit or cbit being its own subgraph.
# Start with each qubit or clbit being its own subgraph.
sub_graphs = [[bit] for bit in range(len(bit_indices))]
num_sub_graphs = len(sub_graphs)
@ -3816,7 +3818,7 @@ class QuantumCircuit:
inplace (bool): All measurements inplace or return new circuit.
Returns:
QuantumCircuit: Returns circuit with measurements when `inplace = False`.
QuantumCircuit: Returns circuit with measurements when ``inplace = False``.
"""
from qiskit.converters.circuit_to_dag import circuit_to_dag
@ -5704,7 +5706,7 @@ class QuantumCircuit:
* Statevector or vector of complex amplitudes to initialize to.
* Labels of basis states of the Pauli eigenstates Z, X, Y. See
:meth:`.Statevector.from_label`. Notice the order of the labels is reversed with
respect to the qubit index to be applied to. Example label '01' initializes the
respect to the qubit index to be applied to. Example label ``'01'`` initializes the
qubit zero to :math:`|1\rangle` and the qubit one to :math:`|0\rangle`.
* An integer that is used as a bitmap indicating which qubits to initialize to
:math:`|1\rangle`. Example: setting params to 5 would initialize qubit 0 and qubit

View File

@ -31,6 +31,7 @@ class UserConfig:
circuit_mpl_style = default
circuit_mpl_style_path = ~/.qiskit:<default location>
circuit_reverse_bits = True
circuit_idle_wires = False
transpile_optimization_level = 1
parallel = False
num_processes = 4
@ -130,6 +131,18 @@ class UserConfig:
if circuit_reverse_bits is not None:
self.settings["circuit_reverse_bits"] = circuit_reverse_bits
# Parse circuit_idle_wires
try:
circuit_idle_wires = self.config_parser.getboolean(
"default", "circuit_idle_wires", fallback=None
)
except ValueError as err:
raise exceptions.QiskitUserConfigError(
f"Value assigned to circuit_idle_wires is not valid. {str(err)}"
)
if circuit_idle_wires is not None:
self.settings["circuit_idle_wires"] = circuit_idle_wires
# Parse transpile_optimization_level
transpile_optimization_level = self.config_parser.getint(
"default", "transpile_optimization_level", fallback=-1
@ -191,6 +204,7 @@ def set_config(key, value, section=None, file_path=None):
"circuit_mpl_style",
"circuit_mpl_style_path",
"circuit_reverse_bits",
"circuit_idle_wires",
"transpile_optimization_level",
"parallel",
"num_processes",

View File

@ -63,7 +63,7 @@ def circuit_drawer(
reverse_bits: bool | None = None,
justify: str | None = None,
vertical_compression: str | None = "medium",
idle_wires: bool = True,
idle_wires: bool | None = None,
with_layout: bool = True,
fold: int | None = None,
# The type of ax is matplotlib.axes.Axes, but this is not a fixed dependency, so cannot be
@ -115,7 +115,7 @@ def circuit_drawer(
output: Select the output method to use for drawing the circuit.
Valid choices are ``text``, ``mpl``, ``latex``, ``latex_source``.
By default the `text` drawer is used unless the user config file
By default, the ``text`` drawer is used unless the user config file
(usually ``~/.qiskit/settings.conf``) has an alternative backend set
as the default. For example, ``circuit_drawer = latex``. If the output
kwarg is set, that backend will always be used over the default in
@ -141,7 +141,9 @@ def circuit_drawer(
will take less vertical room. Default is ``medium``. Only used by
the ``text`` output, will be silently ignored otherwise.
idle_wires: Include idle wires (wires with no circuit elements)
in output visualization. Default is ``True``.
in output visualization. Default is ``True`` unless the
user config file (usually ``~/.qiskit/settings.conf``) has an
alternative value set. For example, ``circuit_idle_wires = False``.
with_layout: Include layout information, with labels on the
physical layout. Default is ``True``.
fold: Sets pagination. It can be disabled using -1. In ``text``,
@ -200,6 +202,7 @@ def circuit_drawer(
# Get default from config file else use text
default_output = "text"
default_reverse_bits = False
default_idle_wires = config.get("circuit_idle_wires", True)
if config:
default_output = config.get("circuit_drawer", "text")
if default_output == "auto":
@ -215,6 +218,9 @@ def circuit_drawer(
if reverse_bits is None:
reverse_bits = default_reverse_bits
if idle_wires is None:
idle_wires = default_idle_wires
if wire_order is not None and reverse_bits:
raise VisualizationError(
"The wire_order option cannot be set when the reverse_bits option is True."

View File

@ -0,0 +1,14 @@
---
features_visualization:
- |
The user configuration file has a new option ``circuit_idle_wires``, which takes a Boolean
value. This allows users to set their preferred default behavior of the ``idle_wires`` option
of the circuit drawers :meth:`.QuantumCircuit.draw` and :func:`.circuit_drawer`. For example,
adding a section to ``~/.qiskit/settings.conf`` with:
.. code-block:: text
[default]
circuit_idle_wires = false
will change the default to display the bits in reverse order.

View File

@ -94,6 +94,31 @@ class TestUserConfig(QiskitTestCase):
config.read_config_file()
self.assertEqual({"circuit_reverse_bits": False}, config.settings)
def test_invalid_circuit_idle_wires(self):
test_config = """
[default]
circuit_idle_wires = Neither
"""
self.addCleanup(os.remove, self.file_path)
with open(self.file_path, "w") as file:
file.write(test_config)
file.flush()
config = user_config.UserConfig(self.file_path)
self.assertRaises(exceptions.QiskitUserConfigError, config.read_config_file)
def test_circuit_idle_wires_valid(self):
test_config = """
[default]
circuit_idle_wires = true
"""
self.addCleanup(os.remove, self.file_path)
with open(self.file_path, "w") as file:
file.write(test_config)
file.flush()
config = user_config.UserConfig(self.file_path)
config.read_config_file()
self.assertEqual({"circuit_idle_wires": True}, config.settings)
def test_optimization_level_valid(self):
test_config = """
[default]
@ -152,6 +177,7 @@ class TestUserConfig(QiskitTestCase):
circuit_mpl_style = default
circuit_mpl_style_path = ~:~/.qiskit
circuit_reverse_bits = false
circuit_idle_wires = true
transpile_optimization_level = 3
suppress_packaging_warnings = true
parallel = false
@ -170,6 +196,7 @@ class TestUserConfig(QiskitTestCase):
"circuit_mpl_style": "default",
"circuit_mpl_style_path": ["~", "~/.qiskit"],
"circuit_reverse_bits": False,
"circuit_idle_wires": True,
"transpile_optimization_level": 3,
"num_processes": 15,
"parallel_enabled": False,
@ -184,6 +211,7 @@ class TestUserConfig(QiskitTestCase):
user_config.set_config("circuit_mpl_style", "default", file_path=self.file_path)
user_config.set_config("circuit_mpl_style_path", "~:~/.qiskit", file_path=self.file_path)
user_config.set_config("circuit_reverse_bits", "false", file_path=self.file_path)
user_config.set_config("circuit_idle_wires", "true", file_path=self.file_path)
user_config.set_config("transpile_optimization_level", "3", file_path=self.file_path)
user_config.set_config("parallel", "false", file_path=self.file_path)
user_config.set_config("num_processes", "15", file_path=self.file_path)
@ -198,6 +226,7 @@ class TestUserConfig(QiskitTestCase):
"circuit_mpl_style": "default",
"circuit_mpl_style_path": ["~", "~/.qiskit"],
"circuit_reverse_bits": False,
"circuit_idle_wires": True,
"transpile_optimization_level": 3,
"num_processes": 15,
"parallel_enabled": False,

View File

@ -1150,15 +1150,16 @@ class TestBasisTranslatorWithTarget(QiskitTestCase):
self.target.add_instruction(CXGate(), cx_props)
def test_2q_with_non_global_1q(self):
"""Test translation works with a 2q gate on an non-global 1q basis."""
"""Test translation works with a 2q gate on a non-global 1q basis."""
qc = QuantumCircuit(2)
qc.cz(0, 1)
bt_pass = BasisTranslator(std_eqlib, target_basis=None, target=self.target)
output = bt_pass(qc)
# We need a second run of BasisTranslator to correct gates outside of
# the target basis. This is a known isssue, see:
# https://docs.quantum.ibm.com/api/qiskit/release-notes/0.33#known-issues
# We need a second run of BasisTranslator to correct gates outside
# the target basis. This is a known issue, see:
# https://github.com/Qiskit/qiskit/issues/11339
# TODO: remove the second bt_pass call once fixed.
output = bt_pass(output)
expected = QuantumCircuit(2)
expected.rz(pi, 1)

View File

@ -495,6 +495,58 @@ class TestTextDrawerGatesInCircuit(QiskitTestCase):
test_reverse = str(circuit_drawer(circuit, output="text"))
self.assertEqual(test_reverse, expected_reverse)
def test_text_idle_wires_read_from_config(self):
"""Swap drawing with idle_wires set in the configuration file."""
expected_with = "\n".join(
[
" ┌───┐",
"q1_0: ┤ H ├",
" └───┘",
"q1_1: ─────",
" ┌───┐",
"q2_0: ┤ H ├",
" └───┘",
"q2_1: ─────",
" ",
]
)
expected_without = "\n".join(
[
" ┌───┐",
"q1_0: ┤ H ├",
" ├───┤",
"q2_0: ┤ H ├",
" └───┘",
]
)
qr1 = QuantumRegister(2, "q1")
qr2 = QuantumRegister(2, "q2")
circuit = QuantumCircuit(qr1, qr2)
circuit.h(qr1[0])
circuit.h(qr2[0])
self.assertEqual(
str(
circuit_drawer(
circuit,
output="text",
)
),
expected_with,
)
config_content = """
[default]
circuit_idle_wires = false
"""
with tempfile.TemporaryDirectory() as dir_path:
file_path = pathlib.Path(dir_path) / "qiskit.conf"
with open(file_path, "w") as fptr:
fptr.write(config_content)
with unittest.mock.patch.dict(os.environ, {"QISKIT_SETTINGS": str(file_path)}):
test_without = str(circuit_drawer(circuit, output="text"))
self.assertEqual(test_without, expected_without)
def test_text_cswap(self):
"""CSwap drawing."""
expected = "\n".join(
@ -514,6 +566,7 @@ class TestTextDrawerGatesInCircuit(QiskitTestCase):
circuit.cswap(qr[0], qr[1], qr[2])
circuit.cswap(qr[1], qr[0], qr[2])
circuit.cswap(qr[2], qr[1], qr[0])
self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=True)), expected)
def test_text_cswap_reverse_bits(self):
@ -4223,7 +4276,6 @@ class TestTextInstructionWithBothWires(QiskitTestCase):
cr6 = ClassicalRegister(6, "c")
circuit = QuantumCircuit(qr6, cr6)
circuit.append(inst, qr6[1:5], cr6[1:3])
self.assertEqual(str(circuit_drawer(circuit, output="text", initial_state=True)), expected)
def test_text_2q_1c(self):
@ -5668,7 +5720,6 @@ class TestTextPhase(QiskitTestCase):
qry = QuantumRegister(1, "qry")
crx = ClassicalRegister(2, "crx")
circuit = QuantumCircuit(qrx, [Qubit(), Qubit()], qry, [Clbit(), Clbit()], crx)
self.assertEqual(circuit.draw(output="text", cregbundle=True).single_string(), expected)