Repository: Qiskit/qiskit (python)

Issue:
SparsePauliOp.apply_layout should do nothing if given None
### What should we add?

Simulators give `None` for the layout when a circuit goes through the transpiler.  Currently this raises in `SparsePauliOp.apply_layout`.  Rather, it would be nice if `None` is a null op in `apply_layout` so that I can run the same code for a sim and a real backend.

Pull request description:
Fixed Issue 11026: SparsePauliOp.apply_layout should do nothing if given None
<!--
⚠️ If you do not respect this template, your pull request will be closed.
⚠️ Your pull request title should be short detailed and understandable for all.
⚠️ Also, please add a release note file using reno if the change needs to be
  documented in the release notes.
⚠️ If your pull request fixes an open issue, please link to the issue.

- [ ] I have added the tests to cover my changes.
- [ ] I have updated the documentation accordingly.
- [ ] I have read the CONTRIBUTING document.
-->

### Summary
fixes #11026 SparsePauliOp.apply_layout should do nothing if given None


### Details and comments
As pointed out in the issue #11026 : since using a simulator as a backend for the transpiler function gives a circuit with a None layout, when passing the layout to the apply_layout function an exception is raised. It would be better if the function does nothing in this case, so the same code can be run for both a simulator and a backend. Therefore, an additional condition is added to treat this case.



Patch:
diff --git a/.azure/lint-linux.yml b/.azure/lint-linux.yml
index 532c2072e..5855fe1e6 100644
--- a/.azure/lint-linux.yml
+++ b/.azure/lint-linux.yml
@@ -26,7 +26,10 @@ jobs:
             -c constraints.txt \
             -r requirements.txt \
             -r requirements-dev.txt \
+            ./qiskit_pkg \
             -e .
+          # Build and install both qiskit and qiskit-terra so that any optionals
+          # depending on `qiskit` will resolve correctly.
         displayName: 'Install dependencies'
         env:
           SETUPTOOLS_ENABLE_FEATURES: "legacy-editable"
diff --git a/.github/workflows/coverage.yml b/.github/workflows/coverage.yml
index de3485c5f..221840b4e 100644
--- a/.github/workflows/coverage.yml
+++ b/.github/workflows/coverage.yml
@@ -42,7 +42,7 @@ jobs:
         run: python -m pip install -c constraints.txt --upgrade pip setuptools wheel
 
       - name: Build and install qiskit-terra
-        run: python -m pip install -c constraints.txt -e .
+        run: python -m pip install -c constraints.txt -e . ./qiskit_pkg
         env:
           CARGO_INCREMENTAL: 0
           RUSTFLAGS: "-Cinstrument-coverage"
diff --git a/.github/workflows/slow.yml b/.github/workflows/slow.yml
index 62fa9ec19..528305740 100644
--- a/.github/workflows/slow.yml
+++ b/.github/workflows/slow.yml
@@ -19,7 +19,7 @@ jobs:
           python -m pip install -U pip setuptools wheel
           python -m pip install -U -r requirements.txt -c constraints.txt
           python -m pip install -U -r requirements-dev.txt -c constraints.txt
-          python -m pip install -c constraints.txt -e .
+          python -m pip install -c constraints.txt -e . ./qiskit_pkg
           python -m pip install "qiskit-aer" "z3-solver" "cplex" -c constraints.txt
         env:
           SETUPTOOLS_ENABLE_FEATURES: "legacy-editable"
diff --git a/qiskit/compiler/transpiler.py b/qiskit/compiler/transpiler.py
index 22dfd35be..a2b3971b5 100644
--- a/qiskit/compiler/transpiler.py
+++ b/qiskit/compiler/transpiler.py
@@ -28,7 +28,7 @@ from qiskit.providers.models import BackendProperties
 from qiskit.pulse import Schedule, InstructionScheduleMap
 from qiskit.transpiler import Layout, CouplingMap, PropertySet
 from qiskit.transpiler.basepasses import BasePass
-from qiskit.transpiler.exceptions import TranspilerError
+from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget
 from qiskit.transpiler.instruction_durations import InstructionDurations, InstructionDurationsType
 from qiskit.transpiler.passes.synthesis.high_level_synthesis import HLSConfig
 from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager
@@ -455,7 +455,7 @@ def _check_circuits_coupling_map(circuits, cmap, backend):
         # If coupling_map is not None or num_qubits == 1
         num_qubits = len(circuit.qubits)
         if max_qubits is not None and (num_qubits > max_qubits):
-            raise TranspilerError(
+            raise CircuitTooWideForTarget(
                 f"Number of qubits ({num_qubits}) in {circuit.name} "
                 f"is greater than maximum ({max_qubits}) in the coupling_map"
             )
diff --git a/qiskit/pulse/channels.py b/qiskit/pulse/channels.py
index 687b83770..e3da9b923 100644
--- a/qiskit/pulse/channels.py
+++ b/qiskit/pulse/channels.py
@@ -96,7 +96,6 @@ class Channel(metaclass=ABCMeta):
         """
         self._validate_index(index)
         self._index = index
-        self._hash = hash((self.__class__.__name__, self._index))
 
     @property
     def index(self) -> Union[int, ParameterExpression]:
@@ -156,7 +155,7 @@ class Channel(metaclass=ABCMeta):
         return type(self) is type(other) and self._index == other._index
 
     def __hash__(self):
-        return self._hash
+        return hash((type(self), self._index))
 
 
 class PulseChannel(Channel, metaclass=ABCMeta):
diff --git a/qiskit/pulse/instructions/instruction.py b/qiskit/pulse/instructions/instruction.py
index 7c6af5788..559b4fb38 100644
--- a/qiskit/pulse/instructions/instruction.py
+++ b/qiskit/pulse/instructions/instruction.py
@@ -53,7 +53,6 @@ class Instruction(ABC):
         """
         self._operands = operands
         self._name = name
-        self._hash = None
         self._validate()
 
     def _validate(self):
@@ -301,9 +300,7 @@ class Instruction(ABC):
         return isinstance(other, type(self)) and self.operands == other.operands
 
     def __hash__(self) -> int:
-        if self._hash is None:
-            self._hash = hash((type(self), self.operands, self.name))
-        return self._hash
+        return hash((type(self), self.operands, self.name))
 
     def __add__(self, other):
         """Return a new schedule with `other` inserted within `self` at `start_time`.
diff --git a/qiskit/pulse/model/frames.py b/qiskit/pulse/model/frames.py
index ae63fe3c7..803573301 100644
--- a/qiskit/pulse/model/frames.py
+++ b/qiskit/pulse/model/frames.py
@@ -33,28 +33,6 @@ class Frame(ABC):
     The default initial phase for every frame is 0.
     """
 
-    def __init__(self, identifier):
-        """Create ``Frame``.
-
-        Args:
-            identifier: A unique identifier used to hash the Frame.
-        """
-        self._hash = hash((type(self), identifier))
-
-    def __eq__(self, other: "Frame") -> bool:
-        """Return True iff self and other are equal, specifically, iff they have the same type and hash.
-
-        Args:
-            other: The frame to compare to this one.
-
-        Returns:
-            True iff equal.
-        """
-        return type(self) is type(other) and self._hash == other._hash
-
-    def __hash__(self) -> int:
-        return self._hash
-
 
 class GenericFrame(Frame):
     """Pulse module GenericFrame.
@@ -74,7 +52,6 @@ class GenericFrame(Frame):
             name: A unique identifier used to identify the frame.
         """
         self._name = name
-        super().__init__(name)
 
     @property
     def name(self) -> str:
@@ -84,6 +61,12 @@ class GenericFrame(Frame):
     def __repr__(self) -> str:
         return f"GenericFrame({self._name})"
 
+    def __eq__(self, other):
+        return type(self) is type(other) and self._name == other._name
+
+    def __hash__(self):
+        return hash((type(self), self._name))
+
 
 class QubitFrame(Frame):
     """A frame associated with the driving of a qubit.
@@ -102,7 +85,6 @@ class QubitFrame(Frame):
         """
         self._validate_index(index)
         self._index = index
-        super().__init__("QubitFrame" + str(index))
 
     @property
     def index(self) -> int:
@@ -122,6 +104,12 @@ class QubitFrame(Frame):
     def __repr__(self) -> str:
         return f"QubitFrame({self._index})"
 
+    def __eq__(self, other):
+        return type(self) is type(other) and self._index == other._index
+
+    def __hash__(self):
+        return hash((type(self), self._index))
+
 
 class MeasurementFrame(Frame):
     """A frame associated with the measurement of a qubit.
@@ -141,7 +129,6 @@ class MeasurementFrame(Frame):
         """
         self._validate_index(index)
         self._index = index
-        super().__init__("MeasurementFrame" + str(index))
 
     @property
     def index(self) -> int:
@@ -160,3 +147,9 @@ class MeasurementFrame(Frame):
 
     def __repr__(self) -> str:
         return f"MeasurementFrame({self._index})"
+
+    def __eq__(self, other):
+        return type(self) is type(other) and self._index == other._index
+
+    def __hash__(self):
+        return hash((type(self), self._index))
diff --git a/qiskit/pulse/model/mixed_frames.py b/qiskit/pulse/model/mixed_frames.py
index 50ff13c47..454cdbf0d 100644
--- a/qiskit/pulse/model/mixed_frames.py
+++ b/qiskit/pulse/model/mixed_frames.py
@@ -47,7 +47,6 @@ class MixedFrame:
         """
         self._pulse_target = pulse_target
         self._frame = frame
-        self._hash = hash((self._pulse_target, self._frame, type(self)))
 
     @property
     def pulse_target(self) -> PulseTarget:
@@ -75,4 +74,4 @@ class MixedFrame:
         return self._pulse_target == other._pulse_target and self._frame == other._frame
 
     def __hash__(self) -> int:
-        return self._hash
+        return hash((self._pulse_target, self._frame, type(self)))
diff --git a/qiskit/pulse/model/pulse_target.py b/qiskit/pulse/model/pulse_target.py
index 69894780d..bb9702ccf 100644
--- a/qiskit/pulse/model/pulse_target.py
+++ b/qiskit/pulse/model/pulse_target.py
@@ -59,7 +59,6 @@ class Port(PulseTarget):
             name: A string identifying the port.
         """
         self._name = name
-        self._hash = hash((name, type(self)))
 
     @property
     def name(self) -> str:
@@ -78,12 +77,12 @@ class Port(PulseTarget):
         """
         return type(self) is type(other) and self._name == other._name
 
+    def __hash__(self) -> int:
+        return hash((self._name, type(self)))
+
     def __repr__(self) -> str:
         return f"Port({self._name})"
 
-    def __hash__(self) -> int:
-        return self._hash
-
 
 class LogicalElement(PulseTarget, ABC):
     """Base class of logical elements.
@@ -104,7 +103,6 @@ class LogicalElement(PulseTarget, ABC):
         """
         self._validate_index(index)
         self._index = index
-        self._hash = hash((index, type(self)))
 
     @property
     def index(self) -> Tuple[int, ...]:
@@ -132,13 +130,13 @@ class LogicalElement(PulseTarget, ABC):
         """
         return type(self) is type(other) and self._index == other._index
 
+    def __hash__(self) -> int:
+        return hash((self._index, type(self)))
+
     def __repr__(self) -> str:
         ind_str = str(self._index) if len(self._index) > 1 else f"({self._index[0]})"
         return type(self).__name__ + ind_str
 
-    def __hash__(self) -> int:
-        return self._hash
-
 
 class Qubit(LogicalElement):
     """Qubit logical element.
diff --git a/qiskit/quantum_info/operators/symplectic/pauli.py b/qiskit/quantum_info/operators/symplectic/pauli.py
index 295d8a97d..c5b93ceaf 100644
--- a/qiskit/quantum_info/operators/symplectic/pauli.py
+++ b/qiskit/quantum_info/operators/symplectic/pauli.py
@@ -110,7 +110,7 @@ class Pauli(BasePauli):
 
         P = (-i)^{q + z\cdot x} Z^z \cdot X^x.
 
-    The :math:`k`th qubit corresponds to the :math:`k`th entry in the
+    The :math:`k`-th qubit corresponds to the :math:`k`-th entry in the
     :math:`z` and :math:`x` arrays
 
     .. math::
diff --git a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py
index 9eecbed2f..65938410c 100644
--- a/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py
+++ b/qiskit/quantum_info/operators/symplectic/sparse_pauli_op.py
@@ -1109,17 +1109,20 @@ class SparsePauliOp(LinearOp):
         return None if inplace else bound
 
     def apply_layout(
-        self, layout: TranspileLayout | List[int], num_qubits: int | None = None
+        self, layout: TranspileLayout | List[int] | None, num_qubits: int | None = None
     ) -> SparsePauliOp:
         """Apply a transpiler layout to this :class:`~.SparsePauliOp`
 
         Args:
-            layout: Either a :class:`~.TranspileLayout` or a list of integers.
+            layout: Either a :class:`~.TranspileLayout`, a list of integers or None.
+                    If both layout and num_qubits are none, a copy of the operator is
+                    returned.
             num_qubits: The number of qubits to expand the operator to. If not
                 provided then if ``layout`` is a :class:`~.TranspileLayout` the
                 number of the transpiler output circuit qubits will be used by
                 default. If ``layout`` is a list of integers the permutation
-                specified will be applied without any expansion.
+                specified will be applied without any expansion. If layout is
+                None, the operator will be expanded to the given number of qubits.
 
 
         Returns:
@@ -1127,6 +1130,9 @@ class SparsePauliOp(LinearOp):
         """
         from qiskit.transpiler.layout import TranspileLayout
 
+        if layout is None and num_qubits is None:
+            return self.copy()
+
         n_qubits = self.num_qubits
         if isinstance(layout, TranspileLayout):
             n_qubits = len(layout._output_qubit_list)
@@ -1138,8 +1144,10 @@ class SparsePauliOp(LinearOp):
                     f"applied to a {n_qubits} qubit operator"
                 )
             n_qubits = num_qubits
-        if any(x >= n_qubits for x in layout):
+        if layout is not None and any(x >= n_qubits for x in layout):
             raise QiskitError("Provided layout contains indicies outside the number of qubits.")
+        if layout is None:
+            layout = list(range(self.num_qubits))
         new_op = type(self)("I" * n_qubits)
         return new_op.compose(self, qargs=layout)
 
diff --git a/qiskit/transpiler/__init__.py b/qiskit/transpiler/__init__.py
index 9abab3aa5..13ae12e09 100644
--- a/qiskit/transpiler/__init__.py
+++ b/qiskit/transpiler/__init__.py
@@ -1251,6 +1251,8 @@ Exceptions
 .. autoexception:: TranspilerAccessError
 .. autoexception:: CouplingError
 .. autoexception:: LayoutError
+.. autoexception:: CircuitTooWideForTarget
+
 """
 
 # For backward compatibility
@@ -1263,7 +1265,13 @@ from qiskit.passmanager import (
 from .passmanager import PassManager, StagedPassManager
 from .passmanager_config import PassManagerConfig
 from .propertyset import PropertySet  # pylint: disable=no-name-in-module
-from .exceptions import TranspilerError, TranspilerAccessError, CouplingError, LayoutError
+from .exceptions import (
+    TranspilerError,
+    TranspilerAccessError,
+    CouplingError,
+    LayoutError,
+    CircuitTooWideForTarget,
+)
 from .fencedobjs import FencedDAGCircuit, FencedPropertySet
 from .basepasses import AnalysisPass, TransformationPass
 from .coupling import CouplingMap
diff --git a/qiskit/transpiler/exceptions.py b/qiskit/transpiler/exceptions.py
index ef79603bf..5c23cb2b3 100644
--- a/qiskit/transpiler/exceptions.py
+++ b/qiskit/transpiler/exceptions.py
@@ -49,3 +49,7 @@ class LayoutError(QiskitError):
     def __str__(self):
         """Return the message."""
         return repr(self.msg)
+
+
+class CircuitTooWideForTarget(TranspilerError):
+    """Error raised if the circuit is too wide for the target."""
diff --git a/qiskit/transpiler/passes/optimization/inverse_cancellation.py b/qiskit/transpiler/passes/optimization/inverse_cancellation.py
index 35dae7d85..560cdcd70 100644
--- a/qiskit/transpiler/passes/optimization/inverse_cancellation.py
+++ b/qiskit/transpiler/passes/optimization/inverse_cancellation.py
@@ -58,12 +58,16 @@ class InverseCancellation(TransformationPass):
 
         self.self_inverse_gates = []
         self.inverse_gate_pairs = []
+        self.self_inverse_gate_names = set()
+        self.inverse_gate_pairs_names = set()
 
         for gates in gates_to_cancel:
             if isinstance(gates, Gate):
                 self.self_inverse_gates.append(gates)
+                self.self_inverse_gate_names.add(gates.name)
             else:
                 self.inverse_gate_pairs.append(gates)
+                self.inverse_gate_pairs_names.update(x.name for x in gates)
 
         super().__init__()
 
@@ -76,11 +80,13 @@ class InverseCancellation(TransformationPass):
         Returns:
             DAGCircuit: Transformed DAG.
         """
+        if self.self_inverse_gates:
+            dag = self._run_on_self_inverse(dag)
+        if self.inverse_gate_pairs:
+            dag = self._run_on_inverse_pairs(dag)
+        return dag
 
-        dag = self._run_on_self_inverse(dag, self.self_inverse_gates)
-        return self._run_on_inverse_pairs(dag, self.inverse_gate_pairs)
-
-    def _run_on_self_inverse(self, dag: DAGCircuit, self_inverse_gates: List[Gate]):
+    def _run_on_self_inverse(self, dag: DAGCircuit):
         """
         Run self-inverse gates on `dag`.
 
@@ -91,14 +97,27 @@ class InverseCancellation(TransformationPass):
         Returns:
             DAGCircuit: Transformed DAG.
         """
+        op_counts = dag.count_ops()
+        if not self.self_inverse_gate_names.intersection(op_counts):
+            return dag
         # Sets of gate runs by name, for instance: [{(H 0, H 0), (H 1, H 1)}, {(X 0, X 0}]
-        gate_runs_sets = [dag.collect_runs([gate.name]) for gate in self_inverse_gates]
-        for gate_runs in gate_runs_sets:
+        for gate in self.self_inverse_gates:
+            gate_name = gate.name
+            gate_count = op_counts.get(gate_name, 0)
+            if gate_count <= 1:
+                continue
+            gate_runs = dag.collect_runs([gate_name])
             for gate_cancel_run in gate_runs:
                 partitions = []
                 chunk = []
                 for i in range(len(gate_cancel_run) - 1):
-                    chunk.append(gate_cancel_run[i])
+                    if gate_cancel_run[i].op == gate:
+                        chunk.append(gate_cancel_run[i])
+                    else:
+                        if chunk:
+                            partitions.append(chunk)
+                            chunk = []
+                        continue
                     if gate_cancel_run[i].qargs != gate_cancel_run[i + 1].qargs:
                         partitions.append(chunk)
                         chunk = []
@@ -112,7 +131,7 @@ class InverseCancellation(TransformationPass):
                         dag.remove_op_node(node)
         return dag
 
-    def _run_on_inverse_pairs(self, dag: DAGCircuit, inverse_gate_pairs: List[Tuple[Gate, Gate]]):
+    def _run_on_inverse_pairs(self, dag: DAGCircuit):
         """
         Run inverse gate pairs on `dag`.
 
@@ -123,8 +142,16 @@ class InverseCancellation(TransformationPass):
         Returns:
             DAGCircuit: Transformed DAG.
         """
-        for pair in inverse_gate_pairs:
-            gate_cancel_runs = dag.collect_runs([pair[0].name, pair[1].name])
+        op_counts = dag.count_ops()
+        if not self.inverse_gate_pairs_names.intersection(op_counts):
+            return dag
+
+        for pair in self.inverse_gate_pairs:
+            gate_0_name = pair[0].name
+            gate_1_name = pair[1].name
+            if gate_0_name not in op_counts or gate_1_name not in op_counts:
+                continue
+            gate_cancel_runs = dag.collect_runs([gate_0_name, gate_1_name])
             for dag_nodes in gate_cancel_runs:
                 i = 0
                 while i < len(dag_nodes) - 1:
diff --git a/releasenotes/notes/allow-none-layout-for-sparse_pauli_op-212470d5b5b307a0.yaml b/releasenotes/notes/allow-none-layout-for-sparse_pauli_op-212470d5b5b307a0.yaml
new file mode 100644
index 000000000..d40486990
--- /dev/null
+++ b/releasenotes/notes/allow-none-layout-for-sparse_pauli_op-212470d5b5b307a0.yaml
@@ -0,0 +1,6 @@
+---
+features:
+  - |
+    The function :meth:`~.SparsePauliOp.apply_layout` from :class:`.SparsePauliOp` now allows for the
+    layout argument to also be None. That is, the method can now also be used for circuits where no transpilation/routing
+    took place (for example when transpiling for a simulator).
diff --git a/releasenotes/notes/fix-parameterized-self-inverse-7cb2d68b273640f8.yaml b/releasenotes/notes/fix-parameterized-self-inverse-7cb2d68b273640f8.yaml
new file mode 100644
index 000000000..7ca796cdd
--- /dev/null
+++ b/releasenotes/notes/fix-parameterized-self-inverse-7cb2d68b273640f8.yaml
@@ -0,0 +1,22 @@
+---
+fixes:
+  - |
+    Fixed an issue with the :class:`~.InverseCancellation` pass where it would
+    incorrectly cancel gates passed in as self inverses with a parameter
+    value, if a run of gates had a different parameter value. For example::
+
+        from math import pi
+
+        from qiskit.circuit.library import RZGate
+        from qiskit.circuit import QuantumCircuit
+        from qiskit.transpiler.passes import InverseCancellation
+
+        inverse_pass = InverseCancellation([RZGate(0)])
+
+        qc = QuantumCircuit(1)
+        qc.rz(0, 0)
+        qc.rz(pi, 0)
+
+        inverse_pass(qc)
+
+    would previously have incorrectly cancelled the two rz gates.
diff --git a/releasenotes/notes/fix-pulse-channel-hash-549a8fb5d8738c4d.yaml b/releasenotes/notes/fix-pulse-channel-hash-549a8fb5d8738c4d.yaml
new file mode 100644
index 000000000..19d022572
--- /dev/null
+++ b/releasenotes/notes/fix-pulse-channel-hash-549a8fb5d8738c4d.yaml
@@ -0,0 +1,6 @@
+---
+fixes:
+  - |
+    Fixed the :func:`hash` of Qiskit Pulse ``Channel`` objects (such as :class:`.DriveChannel`) in
+    cases where the channel was transferred from one Python process to another that used a different
+    hash seed.
diff --git a/releasenotes/notes/new-exception-too-wide-3231c1df15952445.yaml b/releasenotes/notes/new-exception-too-wide-3231c1df15952445.yaml
new file mode 100644
index 000000000..d3bfa72c0
--- /dev/null
+++ b/releasenotes/notes/new-exception-too-wide-3231c1df15952445.yaml
@@ -0,0 +1,9 @@
+---
+features:
+  - |
+    Added a new exception class :exc:`.CircuitToWideForTarget` which
+    subclasses :exc:`.TranspilerError`. It's used in places where a
+    :exc:`.TranspilerError` was previously raised when the error was that
+    the number of circuit qubits was larger than the target backend's qubits.
+    The new class enables more differentiating between this error condition and
+    other :exc:`.TranspilerError`\s.
diff --git a/tox.ini b/tox.ini
index fa6d1017d..0c799d28b 100644
--- a/tox.ini
+++ b/tox.ini
@@ -4,7 +4,10 @@ envlist = py38, py39, py310, py311, lint-incr
 isolated_build = true
 
 [testenv]
-usedevelop = True
+# We pretend that we're not actually installing the package, because we need tox to let us have two
+# packages ('qiskit' and 'qiskit-terra') under test at the same time.  For that, we have to stuff
+# them into 'deps'.
+skip_install = true
 install_command = pip install -c{toxinidir}/constraints.txt -U {opts} {packages}
 setenv =
   VIRTUAL_ENV={envdir}
@@ -15,9 +18,12 @@ setenv =
   QISKIT_TEST_CAPTURE_STREAMS=1
   QISKIT_PARALLEL=FALSE
 passenv = RAYON_NUM_THREADS, OMP_NUM_THREADS, QISKIT_PARALLEL, RUST_BACKTRACE, SETUPTOOLS_ENABLE_FEATURES, QISKIT_TESTS, QISKIT_IN_PARALLEL
-deps = setuptools_rust  # This is work around for the bug of tox 3 (see #8606 for more details.)
-       -r{toxinidir}/requirements.txt
-       -r{toxinidir}/requirements-dev.txt
+deps =
+    setuptools_rust  # This is work around for the bug of tox 3 (see #8606 for more details.)
+    -r{toxinidir}/requirements.txt
+    -r{toxinidir}/requirements-dev.txt
+    -e .
+    -e ./qiskit_pkg
 commands =
   stestr run {posargs}
 
@@ -50,6 +56,9 @@ commands =
   reno lint
 
 [testenv:black]
+skip_install = true
+deps =
+    -r requirements-dev.txt
 commands = black {posargs} qiskit test tools examples setup.py qiskit_pkg
 
 [testenv:coverage]
@@ -57,17 +66,15 @@ basepython = python3
 setenv =
   {[testenv]setenv}
   PYTHON=coverage3 run --source qiskit --parallel-mode
-deps = -r{toxinidir}/requirements.txt
-       -r{toxinidir}/requirements-dev.txt
-       -r{toxinidir}/requirements-optional.txt
+deps =
+  {[testenv]deps}
+  -r{toxinidir}/requirements-optional.txt
 commands =
   stestr run {posargs}
   coverage3 combine
   coverage3 report
 
 [testenv:docs]
-# Editable mode breaks macOS: https://github.com/sphinx-doc/sphinx/issues/10943
-usedevelop = False
 basepython = python3
 setenv =
   {[testenv]setenv}
@@ -75,15 +82,9 @@ setenv =
   RUST_DEBUG=1  # Faster to compile.
 passenv = {[testenv]passenv}, QISKIT_DOCS_BUILD_TUTORIALS, QISKIT_CELL_TIMEOUT, DOCS_PROD_BUILD
 deps =
-  setuptools_rust  # This is work around for the bug of tox 3 (see #8606 for more details.)
-  -r{toxinidir}/requirements-dev.txt
+  {[testenv]deps}
   -r{toxinidir}/requirements-optional.txt
   -r{toxinidir}/requirements-tutorials.txt
-  # Some optionals depend on Terra. We want to make sure pip satisfies that requirement from a local
-  # installation, not from PyPI. But Tox normally doesn't install the local installation until
-  # after `deps` is installed. So, instead, we tell pip to do the local installation at the same
-  # time as the optionals. See https://github.com/Qiskit/qiskit-terra/pull/9477.
-  .
 commands =
   sphinx-build -W -j auto -T --keep-going -b html docs/ docs/_build/html {posargs}
 


Test patch:
diff --git a/.azure/test-linux.yml b/.azure/test-linux.yml
index 3268f29bd..eb456f849 100644
--- a/.azure/test-linux.yml
+++ b/.azure/test-linux.yml
@@ -71,15 +71,17 @@ jobs:
             # Use stable Rust, rather than MSRV, to spot-check that stable builds properly.
             rustup override set stable
             source test-job/bin/activate
-            python -m pip install -U pip setuptools wheel
-            # Install setuptools-rust for building sdist
-            python -m pip install -U -c constraints.txt setuptools-rust
-            python setup.py sdist
+            python -m pip install -U pip
+            python -m pip install -U build
+            python -m build --sdist .
+            python -m build --sdist qiskit_pkg
             python -m pip install -U \
               -c constraints.txt \
               -r requirements.txt \
               -r requirements-dev.txt \
-              dist/qiskit-terra*.tar.gz
+              dist/qiskit*.tar.gz
+            # Build and install both qiskit and qiskit-terra so that any optionals
+            # depending on `qiskit` will resolve correctly.
           displayName: "Install Terra from sdist"
 
       - ${{ if eq(parameters.installFromSdist, false) }}:
@@ -90,7 +92,10 @@ jobs:
               -c constraints.txt \
               -r requirements.txt \
               -r requirements-dev.txt \
+              ./qiskit_pkg \
               -e .
+            # Build and install both qiskit and qiskit-terra so that any optionals
+            # depending on `qiskit` will resolve correctly.
           displayName: "Install Terra directly"
           env:
             SETUPTOOLS_ENABLE_FEATURES: "legacy-editable"
diff --git a/.azure/test-macos.yml b/.azure/test-macos.yml
index 2b195afbd..cbf2fc8b0 100644
--- a/.azure/test-macos.yml
+++ b/.azure/test-macos.yml
@@ -43,7 +43,10 @@ jobs:
             -c constraints.txt \
             -r requirements.txt \
             -r requirements-dev.txt \
+            ./qiskit_pkg \
             -e .
+            # Build and install both qiskit and qiskit-terra so that any optionals
+            # depending on `qiskit` will resolve correctly.
           pip check
         displayName: 'Install dependencies'
         env:
diff --git a/.azure/test-windows.yml b/.azure/test-windows.yml
index 30591a5da..6ba2e4429 100644
--- a/.azure/test-windows.yml
+++ b/.azure/test-windows.yml
@@ -42,7 +42,10 @@ jobs:
             -c constraints.txt \
             -r requirements.txt \
             -r requirements-dev.txt \
+            ./qiskit_pkg \
             -e .
+          # Build and install both qiskit and qiskit-terra so that any optionals
+          # depending on `qiskit` will resolve correctly.
           pip check
         displayName: 'Install dependencies'
         env:
diff --git a/test/python/circuit/library/test_functional_pauli_rotations.py b/test/python/circuit/library/test_functional_pauli_rotations.py
index 38f6568f1..294a2b328 100644
--- a/test/python/circuit/library/test_functional_pauli_rotations.py
+++ b/test/python/circuit/library/test_functional_pauli_rotations.py
@@ -84,7 +84,7 @@ class TestFunctionalPauliRotations(QiskitTestCase):
 
         with self.subTest(msg="missing number of state qubits"):
             with self.assertRaises(AttributeError):  # no state qubits set
-                print(polynomial_rotations.draw())
+                _ = str(polynomial_rotations.draw())
 
         with self.subTest(msg="default setup, just setting number of state qubits"):
             polynomial_rotations.num_state_qubits = 2
@@ -121,7 +121,7 @@ class TestFunctionalPauliRotations(QiskitTestCase):
 
         with self.subTest(msg="missing number of state qubits"):
             with self.assertRaises(AttributeError):  # no state qubits set
-                print(linear_rotation.draw())
+                _ = str(linear_rotation.draw())
 
         with self.subTest(msg="default setup, just setting number of state qubits"):
             linear_rotation.num_state_qubits = 2
@@ -171,7 +171,7 @@ class TestFunctionalPauliRotations(QiskitTestCase):
 
         with self.subTest(msg="missing number of state qubits"):
             with self.assertRaises(AttributeError):  # no state qubits set
-                print(pw_linear_rotations.draw())
+                _ = str(pw_linear_rotations.draw())
 
         with self.subTest(msg="default setup, just setting number of state qubits"):
             pw_linear_rotations.num_state_qubits = 2
diff --git a/test/python/circuit/library/test_integer_comparator.py b/test/python/circuit/library/test_integer_comparator.py
index 2c6459cf3..55bd2b907 100644
--- a/test/python/circuit/library/test_integer_comparator.py
+++ b/test/python/circuit/library/test_integer_comparator.py
@@ -71,13 +71,13 @@ class TestIntegerComparator(QiskitTestCase):
 
         with self.subTest(msg="missing num state qubits and value"):
             with self.assertRaises(AttributeError):
-                print(comp.draw())
+                _ = str(comp.draw())
 
         comp.num_state_qubits = 2
 
         with self.subTest(msg="missing value"):
             with self.assertRaises(AttributeError):
-                print(comp.draw())
+                _ = str(comp.draw())
 
         comp.value = 0
         comp.geq = True
diff --git a/test/python/circuit/library/test_nlocal.py b/test/python/circuit/library/test_nlocal.py
index f20377368..62e791260 100644
--- a/test/python/circuit/library/test_nlocal.py
+++ b/test/python/circuit/library/test_nlocal.py
@@ -378,7 +378,7 @@ class TestNLocal(QiskitTestCase):
 
         # pairwise entanglement is only defined if the entangling gate has 2 qubits
         with self.assertRaises(ValueError):
-            print(nlocal.draw())
+            _ = str(nlocal.draw())
 
     def test_entanglement_by_list(self):
         """Test setting the entanglement by list.
diff --git a/test/python/circuit/library/test_permutation.py b/test/python/circuit/library/test_permutation.py
index bf4da582b..25bfa0bfa 100644
--- a/test/python/circuit/library/test_permutation.py
+++ b/test/python/circuit/library/test_permutation.py
@@ -170,7 +170,6 @@ class TestPermutationGatesOnCircuit(QiskitTestCase):
         circuit.cx(0, 1)
         circuit.append(PermutationGate([1, 2, 0]), [2, 4, 5])
         circuit.h(4)
-        print(circuit)
 
         qpy_file = io.BytesIO()
         dump(circuit, qpy_file)
diff --git a/test/python/circuit/library/test_piecewise_chebyshev.py b/test/python/circuit/library/test_piecewise_chebyshev.py
index 4b8cb6054..c721fe47e 100644
--- a/test/python/circuit/library/test_piecewise_chebyshev.py
+++ b/test/python/circuit/library/test_piecewise_chebyshev.py
@@ -103,7 +103,7 @@ class TestPiecewiseChebyshev(QiskitTestCase):
 
         with self.subTest(msg="missing number of state qubits"):
             with self.assertRaises(AttributeError):  # no state qubits set
-                print(pw_approximation.draw())
+                _ = str(pw_approximation.draw())
 
         with self.subTest(msg="default setup, just setting number of state qubits"):
             pw_approximation.num_state_qubits = 2
diff --git a/test/python/circuit/library/test_weighted_adder.py b/test/python/circuit/library/test_weighted_adder.py
index 7bfe614e0..db33156aa 100644
--- a/test/python/circuit/library/test_weighted_adder.py
+++ b/test/python/circuit/library/test_weighted_adder.py
@@ -67,7 +67,7 @@ class TestWeightedAdder(QiskitTestCase):
 
         with self.subTest(msg="missing number of state qubits"):
             with self.assertRaises(AttributeError):
-                print(adder.draw())
+                _ = str(adder.draw())
 
         with self.subTest(msg="default weights"):
             adder.num_state_qubits = 3
@@ -81,7 +81,7 @@ class TestWeightedAdder(QiskitTestCase):
         with self.subTest(msg="mismatching number of state qubits and weights"):
             with self.assertRaises(ValueError):
                 adder.weights = [0, 1, 2, 3]
-                print(adder.draw())
+                _ = str(adder.draw())
 
         with self.subTest(msg="change all attributes"):
             adder.num_state_qubits = 4
diff --git a/test/python/circuit/test_piecewise_polynomial.py b/test/python/circuit/test_piecewise_polynomial.py
index 9bc34179b..e84cc35de 100644
--- a/test/python/circuit/test_piecewise_polynomial.py
+++ b/test/python/circuit/test_piecewise_polynomial.py
@@ -101,7 +101,7 @@ class TestPiecewisePolynomialRotations(QiskitTestCase):
 
         with self.subTest(msg="missing number of state qubits"):
             with self.assertRaises(AttributeError):  # no state qubits set
-                print(pw_polynomial_rotations.draw())
+                _ = str(pw_polynomial_rotations.draw())
 
         with self.subTest(msg="default setup, just setting number of state qubits"):
             pw_polynomial_rotations.num_state_qubits = 2
diff --git a/test/python/compiler/test_transpiler.py b/test/python/compiler/test_transpiler.py
index 9a54bdc7d..98db50a28 100644
--- a/test/python/compiler/test_transpiler.py
+++ b/test/python/compiler/test_transpiler.py
@@ -87,7 +87,7 @@ from qiskit.quantum_info import Operator, random_unitary
 from qiskit.test import QiskitTestCase, slow_test
 from qiskit.tools import parallel
 from qiskit.transpiler import CouplingMap, Layout, PassManager, TransformationPass
-from qiskit.transpiler.exceptions import TranspilerError
+from qiskit.transpiler.exceptions import TranspilerError, CircuitTooWideForTarget
 from qiskit.transpiler.passes import BarrierBeforeFinalMeasurements, GateDirection, VF2PostLayout
 from qiskit.transpiler.passmanager_config import PassManagerConfig
 from qiskit.transpiler.preset_passmanagers import generate_preset_pass_manager, level_0_pass_manager
@@ -987,7 +987,7 @@ class TestTranspile(QiskitTestCase):
 
         qc = QuantumCircuit(15, 15)
 
-        with self.assertRaises(TranspilerError):
+        with self.assertRaises(CircuitTooWideForTarget):
             transpile(qc, coupling_map=cmap)
 
     @data(0, 1, 2, 3)
diff --git a/test/python/primitives/test_backend_sampler.py b/test/python/primitives/test_backend_sampler.py
index 86dc709f8..971104af9 100644
--- a/test/python/primitives/test_backend_sampler.py
+++ b/test/python/primitives/test_backend_sampler.py
@@ -117,7 +117,6 @@ class TestBackendSampler(QiskitTestCase):
         bell = self._circuit[1]
         sampler = BackendSampler(backend=backend)
         result = sampler.run([bell, bell, bell]).result()
-        # print([q.binary_probabilities() for q in result.quasi_dists])
         self._compare_probs(result.quasi_dists[0], self._target[1])
         self._compare_probs(result.quasi_dists[1], self._target[1])
         self._compare_probs(result.quasi_dists[2], self._target[1])
diff --git a/test/python/primitives/test_sampler.py b/test/python/primitives/test_sampler.py
index 2dc2aac11..fac9a250a 100644
--- a/test/python/primitives/test_sampler.py
+++ b/test/python/primitives/test_sampler.py
@@ -93,7 +93,6 @@ class TestSampler(QiskitTestCase):
         self.assertIsInstance(job, JobV1)
         result = job.result()
         self.assertIsInstance(result, SamplerResult)
-        # print([q.binary_probabilities() for q in result.quasi_dists])
         self._compare_probs(result.quasi_dists, self._target[1])
 
     def test_sample_run_multiple_circuits(self):
@@ -103,7 +102,6 @@ class TestSampler(QiskitTestCase):
         bell = self._circuit[1]
         sampler = Sampler()
         result = sampler.run([bell, bell, bell]).result()
-        # print([q.binary_probabilities() for q in result.quasi_dists])
         self._compare_probs(result.quasi_dists[0], self._target[1])
         self._compare_probs(result.quasi_dists[1], self._target[1])
         self._compare_probs(result.quasi_dists[2], self._target[1])
diff --git a/test/python/quantum_info/operators/symplectic/test_clifford.py b/test/python/quantum_info/operators/symplectic/test_clifford.py
index 76cd9463f..615f09e6a 100644
--- a/test/python/quantum_info/operators/symplectic/test_clifford.py
+++ b/test/python/quantum_info/operators/symplectic/test_clifford.py
@@ -1044,7 +1044,8 @@ class TestCliffordOperators(QiskitTestCase):
         # An error may be thrown if visualization code calls op.condition instead
         # of getattr(op, "condition", None)
         clifford = random_clifford(3, seed=0)
-        print(clifford)
+        _ = str(clifford)
+        _ = repr(clifford)
 
     @combine(num_qubits=[1, 2, 3, 4])
     def test_from_matrix_round_trip(self, num_qubits):
diff --git a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py
index 929534d26..c519855fa 100644
--- a/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py
+++ b/test/python/quantum_info/operators/symplectic/test_sparse_pauli_op.py
@@ -1088,6 +1088,25 @@ class TestSparsePauliOpMethods(QiskitTestCase):
         res = op.apply_layout([4, 0], 5)
         self.assertEqual(SparsePauliOp.from_list([("IIIIY", 2), ("IIIIX", 1)]), res)
 
+    def test_apply_layout_null_layout_no_num_qubits(self):
+        """Test apply_layout with a null layout"""
+        op = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)])
+        res = op.apply_layout(layout=None)
+        self.assertEqual(op, res)
+
+    def test_apply_layout_null_layout_and_num_qubits(self):
+        """Test apply_layout with a null layout a num_qubits provided"""
+        op = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)])
+        res = op.apply_layout(layout=None, num_qubits=5)
+        # this should expand the operator
+        self.assertEqual(SparsePauliOp.from_list([("IIIII", 1), ("IIIIZ", 2), ("IIIXI", 3)]), res)
+
+    def test_apply_layout_null_layout_invalid_num_qubits(self):
+        """Test apply_layout with a null layout and num_qubits smaller than capable"""
+        op = SparsePauliOp.from_list([("II", 1), ("IZ", 2), ("XI", 3)])
+        with self.assertRaises(QiskitError):
+            op.apply_layout(layout=None, num_qubits=1)
+
 
 if __name__ == "__main__":
     unittest.main()
diff --git a/test/python/transpiler/test_inverse_cancellation.py b/test/python/transpiler/test_inverse_cancellation.py
index 686b01dd5..c39b531c1 100644
--- a/test/python/transpiler/test_inverse_cancellation.py
+++ b/test/python/transpiler/test_inverse_cancellation.py
@@ -22,7 +22,17 @@ from qiskit.transpiler.exceptions import TranspilerError
 from qiskit.transpiler.passes import InverseCancellation
 from qiskit.transpiler import PassManager
 from qiskit.test import QiskitTestCase
-from qiskit.circuit.library import RXGate, HGate, CXGate, PhaseGate, XGate, TGate, TdgGate
+from qiskit.circuit.library import (
+    RXGate,
+    HGate,
+    CXGate,
+    PhaseGate,
+    XGate,
+    TGate,
+    TdgGate,
+    CZGate,
+    RZGate,
+)
 
 
 class TestInverseCancellation(QiskitTestCase):
@@ -271,6 +281,111 @@ class TestInverseCancellation(QiskitTestCase):
         self.assertIn("cx", gates_after)
         self.assertEqual(gates_after["cx"], 2)
 
+    def test_no_gates_to_cancel(self):
+        """Test when there are no gates to cancel."""
+        qc = QuantumCircuit(2)
+        qc.cx(0, 1)
+        qc.cx(1, 0)
+        inverse_pass = InverseCancellation([HGate()])
+        new_circ = inverse_pass(qc)
+        self.assertEqual(qc, new_circ)
+
+    def test_some_cancel_rules_to_cancel(self):
+        """Test when there are some gates to cancel."""
+        qc = QuantumCircuit(2)
+        qc.cx(0, 1)
+        qc.cx(1, 0)
+        qc.h(0)
+        qc.h(0)
+        inverse_pass = InverseCancellation([HGate(), CXGate(), CZGate()])
+        new_circ = inverse_pass(qc)
+        self.assertNotIn("h", new_circ.count_ops())
+
+    def test_no_inverse_pairs(self):
+        """Test when there are no inverse pairs to cancel."""
+        qc = QuantumCircuit(1)
+        qc.s(0)
+        qc.sdg(0)
+        inverse_pass = InverseCancellation([(TGate(), TdgGate())])
+        new_circ = inverse_pass(qc)
+        self.assertEqual(qc, new_circ)
+
+    def test_some_inverse_pairs(self):
+        """Test when there are some but not all inverse pairs to cancel."""
+        qc = QuantumCircuit(1)
+        qc.s(0)
+        qc.sdg(0)
+        qc.t(0)
+        qc.tdg(0)
+        inverse_pass = InverseCancellation([(TGate(), TdgGate())])
+        new_circ = inverse_pass(qc)
+        self.assertNotIn("t", new_circ.count_ops())
+        self.assertNotIn("tdg", new_circ.count_ops())
+
+    def test_some_inverse_and_cancelled(self):
+        """Test when there are some but not all pairs to cancel."""
+        qc = QuantumCircuit(2)
+        qc.s(0)
+        qc.sdg(0)
+        qc.t(0)
+        qc.tdg(0)
+        qc.cx(0, 1)
+        qc.cx(1, 0)
+        qc.h(0)
+        qc.h(0)
+        inverse_pass = InverseCancellation([HGate(), CXGate(), CZGate(), (TGate(), TdgGate())])
+        new_circ = inverse_pass(qc)
+        self.assertNotIn("h", new_circ.count_ops())
+        self.assertNotIn("t", new_circ.count_ops())
+        self.assertNotIn("tdg", new_circ.count_ops())
+
+    def test_half_of_an_inverse_pair(self):
+        """Test that half of an inverse pair doesn't do anything."""
+        qc = QuantumCircuit(1)
+        qc.t(0)
+        qc.t(0)
+        qc.t(0)
+        inverse_pass = InverseCancellation([(TGate(), TdgGate())])
+        new_circ = inverse_pass(qc)
+        self.assertEqual(new_circ, qc)
+
+    def test_parameterized_self_inverse(self):
+        """Test that a parameterized self inverse gate cancels correctly."""
+        qc = QuantumCircuit(1)
+        qc.rz(0, 0)
+        qc.rz(0, 0)
+        inverse_pass = InverseCancellation([RZGate(0)])
+        new_circ = inverse_pass(qc)
+        self.assertEqual(new_circ, QuantumCircuit(1))
+
+    def test_parameterized_self_inverse_not_equal_parameter(self):
+        """Test that a parameterized self inverse gate doesn't cancel incorrectly."""
+        qc = QuantumCircuit(1)
+        qc.rz(0, 0)
+        qc.rz(3.14159, 0)
+        qc.rz(0, 0)
+        inverse_pass = InverseCancellation([RZGate(0)])
+        new_circ = inverse_pass(qc)
+        self.assertEqual(new_circ, qc)
+
+    def test_controlled_gate_open_control_does_not_cancel(self):
+        """Test that a controlled gate with an open control doesn't cancel."""
+        qc = QuantumCircuit(2)
+        qc.cx(0, 1)
+        qc.cx(0, 1, ctrl_state=0)
+        inverse_pass = InverseCancellation([CXGate()])
+        new_circ = inverse_pass(qc)
+        self.assertEqual(new_circ, qc)
+
+    def test_backwards_pair(self):
+        """Test a backwards inverse pair works."""
+        qc = QuantumCircuit(1)
+        qc.tdg(0)
+        qc.t(0)
+        inverse_pass = InverseCancellation([(TGate(), TdgGate())])
+        new_circ = inverse_pass(qc)
+        self.assertEqual(new_circ, QuantumCircuit(1))
+
 
 if __name__ == "__main__":
     unittest.main()