Repository: Qiskit/qiskit

Issue:
Support TensoredMeasFitter in QuantumInstance
<!-- ⚠️ If you do not respect this template, your issue will be closed -->
<!-- ⚠️ Make sure to browse the opened and closed issues to confirm this idea does not exist. -->

### What is the expected enhancement?
Currently QuantumInstance supports as error mitigation input classes: `CompleteMeasFitter` and `TensoredMeasFitter`.
However, its implementation is incomplete in `measurement_error_mitigation.py`:
```
    if fitter_cls == CompleteMeasFitter:
        meas_calibs_circuits, state_labels = complete_meas_cal(
            qubit_list=range(len(qubit_list)), circlabel=circlabel
        )
    elif fitter_cls == TensoredMeasFitter:
        # TODO support different calibration
        raise QiskitError("Does not support TensoredMeasFitter yet.")
    else:
        raise QiskitError("Unknown fitter {}".format(fitter_cls))
```

Support should be added for `TensoredMeasFitter`.





Discussion:

Resolving patch:
diff --git a/qiskit/utils/measurement_error_mitigation.py b/qiskit/utils/measurement_error_mitigation.py
--- a/qiskit/utils/measurement_error_mitigation.py
+++ b/qiskit/utils/measurement_error_mitigation.py
@@ -13,23 +13,29 @@
 """ Measurement error mitigation """
 
 import copy
-
+from typing import List, Optional, Tuple, Dict, Callable
 from qiskit import compiler
+from qiskit.providers import BaseBackend
+from qiskit.circuit import QuantumCircuit
+from qiskit.qobj import QasmQobj
+from qiskit.assembler.run_config import RunConfig
 from ..exceptions import QiskitError, MissingOptionalLibraryError
 
 # pylint: disable=invalid-name
 
 
-def get_measured_qubits(transpiled_circuits):
+def get_measured_qubits(
+    transpiled_circuits: List[QuantumCircuit],
+) -> Tuple[List[int], Dict[str, List[int]]]:
     """
     Retrieve the measured qubits from transpiled circuits.
 
     Args:
-        transpiled_circuits ([QuantumCircuit]): a list of transpiled circuits
+        transpiled_circuits: a list of transpiled circuits
 
     Returns:
-        list[int]: the used and sorted qubit index
-        dict: key is qubit index str connected by '_',
+        The used and sorted qubit index
+        Key is qubit index str connected by '_',
               value is the experiment index. {str: list[int]}
     Raises:
         QiskitError: invalid qubit mapping
@@ -62,16 +68,16 @@ def get_measured_qubits(transpiled_circuits):
     return sorted(qubit_index), qubit_mappings
 
 
-def get_measured_qubits_from_qobj(qobj):
+def get_measured_qubits_from_qobj(qobj: QasmQobj) -> Tuple[List[int], Dict[str, List[int]]]:
     """
     Retrieve the measured qubits from transpiled circuits.
 
     Args:
-        qobj (QasmObj): qobj
+        qobj: qobj
 
     Returns:
-        list[int]: the used and sorted qubit index
-        dict: key is qubit index str connected by '_',
+        the used and sorted qubit index
+        key is qubit index str connected by '_',
               value is the experiment index. {str: list[int]}
      Raises:
         QiskitError: invalid qubit mapping
@@ -104,19 +110,29 @@ def get_measured_qubits_from_qobj(qobj):
 
 
 def build_measurement_error_mitigation_circuits(
-    qubit_list, fitter_cls, backend, backend_config=None, compile_config=None
-):
+    qubit_list: List[int],
+    fitter_cls: Callable,
+    backend: BaseBackend,
+    backend_config: Optional[Dict] = None,
+    compile_config: Optional[Dict] = None,
+    mit_pattern: Optional[List[List[int]]] = None,
+) -> Tuple[QuantumCircuit, List[str], List[str]]:
     """Build measurement error mitigation circuits
     Args:
-        qubit_list (list[int]): list of ordered qubits used in the algorithm
-        fitter_cls (callable): CompleteMeasFitter or TensoredMeasFitter
-        backend (BaseBackend): backend instance
-        backend_config (dict, optional): configuration for backend
-        compile_config (dict, optional): configuration for compilation
+        qubit_list: list of ordered qubits used in the algorithm
+        fitter_cls: CompleteMeasFitter or TensoredMeasFitter
+        backend: backend instance
+        backend_config: configuration for backend
+        compile_config: configuration for compilation
+        mit_pattern: Qubits on which to perform the
+            measurement correction, divided to groups according to tensors.
+            If `None` and `qr` is given then assumed to be performed over the entire
+            `qr` as one group (default `None`).
+
     Returns:
-        QasmQobj: the Qobj with calibration circuits at the beginning
-        list[str]: the state labels for build MeasFitter
-        list[str]: the labels of the calibration circuits
+        the circuit
+        the state labels for build MeasFitter
+        the labels of the calibration circuits
     Raises:
         QiskitError: when the fitter_cls is not recognizable.
         MissingOptionalLibraryError: Qiskit-Ignis not installed
@@ -124,6 +140,7 @@ def build_measurement_error_mitigation_circuits(
     try:
         from qiskit.ignis.mitigation.measurement import (
             complete_meas_cal,
+            tensored_meas_cal,
             CompleteMeasFitter,
             TensoredMeasFitter,
         )
@@ -144,8 +161,9 @@ def build_measurement_error_mitigation_circuits(
             qubit_list=range(len(qubit_list)), circlabel=circlabel
         )
     elif fitter_cls == TensoredMeasFitter:
-        # TODO support different calibration
-        raise QiskitError("Does not support TensoredMeasFitter yet.")
+        meas_calibs_circuits, state_labels = tensored_meas_cal(
+            mit_pattern=mit_pattern, circlabel=circlabel
+        )
     else:
         raise QiskitError("Unknown fitter {}".format(fitter_cls))
 
@@ -161,21 +179,31 @@ def build_measurement_error_mitigation_circuits(
 
 
 def build_measurement_error_mitigation_qobj(
-    qubit_list, fitter_cls, backend, backend_config=None, compile_config=None, run_config=None
-):
+    qubit_list: List[int],
+    fitter_cls: Callable,
+    backend: BaseBackend,
+    backend_config: Optional[Dict] = None,
+    compile_config: Optional[Dict] = None,
+    run_config: Optional[RunConfig] = None,
+    mit_pattern: Optional[List[List[int]]] = None,
+) -> Tuple[QasmQobj, List[str], List[str]]:
     """
     Args:
-        qubit_list (list[int]): list of ordered qubits used in the algorithm
-        fitter_cls (callable): CompleteMeasFitter or TensoredMeasFitter
-        backend (BaseBackend): backend instance
-        backend_config (dict, optional): configuration for backend
-        compile_config (dict, optional): configuration for compilation
-        run_config (RunConfig, optional): configuration for running a circuit
+        qubit_list: list of ordered qubits used in the algorithm
+        fitter_cls: CompleteMeasFitter or TensoredMeasFitter
+        backend: backend instance
+        backend_config: configuration for backend
+        compile_config: configuration for compilation
+        run_config: configuration for running a circuit
+        mit_pattern: Qubits on which to perform the
+            measurement correction, divided to groups according to tensors.
+            If `None` and `qr` is given then assumed to be performed over the entire
+            `qr` as one group (default `None`).
 
     Returns:
-        QasmQobj: the Qobj with calibration circuits at the beginning
-        list[str]: the state labels for build MeasFitter
-        list[str]: the labels of the calibration circuits
+        the Qobj with calibration circuits at the beginning
+        the state labels for build MeasFitter
+        the labels of the calibration circuits
 
     Raises:
         QiskitError: when the fitter_cls is not recognizable.
@@ -184,6 +212,7 @@ def build_measurement_error_mitigation_qobj(
     try:
         from qiskit.ignis.mitigation.measurement import (
             complete_meas_cal,
+            tensored_meas_cal,
             CompleteMeasFitter,
             TensoredMeasFitter,
         )
@@ -204,8 +233,9 @@ def build_measurement_error_mitigation_qobj(
             qubit_list=range(len(qubit_list)), circlabel=circlabel
         )
     elif fitter_cls == TensoredMeasFitter:
-        # TODO support different calibration
-        raise QiskitError("Does not support TensoredMeasFitter yet.")
+        meas_calibs_circuits, state_labels = tensored_meas_cal(
+            mit_pattern=mit_pattern, circlabel=circlabel
+        )
     else:
         raise QiskitError("Unknown fitter {}".format(fitter_cls))
 
diff --git a/qiskit/utils/quantum_instance.py b/qiskit/utils/quantum_instance.py
--- a/qiskit/utils/quantum_instance.py
+++ b/qiskit/utils/quantum_instance.py
@@ -20,7 +20,7 @@
 
 from qiskit.qobj import Qobj
 from qiskit.utils import circuit_utils
-from qiskit.exceptions import QiskitError
+from qiskit.exceptions import QiskitError, MissingOptionalLibraryError
 from .backend_utils import (
     is_ibmq_provider,
     is_statevector_backend,
@@ -81,6 +81,7 @@ def __init__(
         cals_matrix_refresh_period: int = 30,
         measurement_error_mitigation_shots: Optional[int] = None,
         job_callback: Optional[Callable] = None,
+        mit_pattern: Optional[List[List[int]]] = None,
     ) -> None:
         """
         Quantum Instance holds a Qiskit Terra backend as well as configuration for circuit
@@ -114,8 +115,9 @@ def __init__(
                 processing time during submission to backend.
             measurement_error_mitigation_cls: The approach to mitigate
                 measurement errors. Qiskit Ignis provides fitter classes for this functionality
-                and CompleteMeasFitter from qiskit.ignis.mitigation.measurement module can be used
-                here. (TensoredMeasFitter is not supported).
+                and CompleteMeasFitter or TensoredMeasFitter
+                from qiskit.ignis.mitigation.measurement module can be used here.
+                TensoredMeasFitter doesn't support subset fitter.
             cals_matrix_refresh_period: How often to refresh the calibration
                 matrix in measurement mitigation. in minutes
             measurement_error_mitigation_shots: The number of shots number for
@@ -124,6 +126,10 @@ def __init__(
                 to monitor job progress as jobs are submitted for processing by an Aqua algorithm.
                 The callback is provided the following arguments: `job_id, job_status,
                 queue_position, job`
+            mit_pattern: Qubits on which to perform the TensoredMeasFitter
+                measurement correction, divided to groups according to tensors.
+                If `None` and `qr` is given then assumed to be performed over the entire
+                `qr` as one group (default `None`).
 
         Raises:
             QiskitError: the shots exceeds the maximum number of shots
@@ -215,6 +221,7 @@ def __init__(
         self._meas_error_mitigation_method = "least_squares"
         self._cals_matrix_refresh_period = cals_matrix_refresh_period
         self._meas_error_mitigation_shots = measurement_error_mitigation_shots
+        self._mit_pattern = mit_pattern
 
         if self._meas_error_mitigation_cls is not None:
             logger.info(
@@ -312,6 +319,11 @@ def execute(self, circuits, had_transpiled: bool = False):
                         circuits to execute
             had_transpiled: whether or not circuits had been transpiled
 
+        Raises:
+            QiskitError: Invalid error mitigation fitter class
+            QiskitError: TensoredMeasFitter class doesn't support subset fitter
+            MissingOptionalLibraryError: Ignis not installed
+
         Returns:
             Result: result object
 
@@ -364,6 +376,9 @@ def _find_save_state(data):
                 if circuit_job
                 else get_measured_qubits_from_qobj(qobj)
             )
+            mit_pattern = self._mit_pattern
+            if mit_pattern is None:
+                mit_pattern = [[i] for i in range(len(qubit_index))]
             qubit_index_str = "_".join([str(x) for x in qubit_index]) + "_{}".format(
                 self._meas_error_mitigation_shots or self._run_config.shots
             )
@@ -422,6 +437,7 @@ def _find_save_state(data):
                         self._backend,
                         self._backend_config,
                         self._compile_config,
+                        mit_pattern=mit_pattern,
                     )
                     if use_different_shots:
                         cals_result = run_circuits(
@@ -479,6 +495,7 @@ def _find_save_state(data):
                         self._backend_config,
                         self._compile_config,
                         temp_run_config,
+                        mit_pattern=mit_pattern,
                     )
                     if use_different_shots or is_aer_qasm(self._backend):
                         cals_result = run_qobj(
@@ -517,9 +534,28 @@ def _find_save_state(data):
                         cals_result = result
 
                 logger.info("Building calibration matrix for measurement error mitigation.")
-                meas_error_mitigation_fitter = self._meas_error_mitigation_cls(
-                    cals_result, state_labels, qubit_list=qubit_index, circlabel=circuit_labels
-                )
+                try:
+                    from qiskit.ignis.mitigation.measurement import (
+                        CompleteMeasFitter,
+                        TensoredMeasFitter,
+                    )
+                except ImportError as ex:
+                    raise MissingOptionalLibraryError(
+                        libname="qiskit-ignis",
+                        name="execute",
+                        pip_install="pip install qiskit-ignis",
+                    ) from ex
+                if self._meas_error_mitigation_cls == CompleteMeasFitter:
+                    meas_error_mitigation_fitter = self._meas_error_mitigation_cls(
+                        cals_result, state_labels, qubit_list=qubit_index, circlabel=circuit_labels
+                    )
+                elif self._meas_error_mitigation_cls == TensoredMeasFitter:
+                    meas_error_mitigation_fitter = self._meas_error_mitigation_cls(
+                        cals_result, mit_pattern=state_labels, circlabel=circuit_labels
+                    )
+                else:
+                    raise QiskitError("Unknown fitter {}".format(self._meas_error_mitigation_cls))
+
                 self._meas_error_mitigation_fitters[qubit_index_str] = (
                     meas_error_mitigation_fitter,
                     time.time(),
@@ -571,8 +607,14 @@ def _find_save_state(data):
                     tmp_result.results = [result.results[i] for i in c_idx]
                     if curr_qubit_index == qubit_index:
                         tmp_fitter = meas_error_mitigation_fitter
-                    else:
+                    elif isinstance(meas_error_mitigation_fitter, CompleteMeasFitter):
                         tmp_fitter = meas_error_mitigation_fitter.subset_fitter(curr_qubit_index)
+                    else:
+                        raise QiskitError(
+                            "{} doesn't support subset_fitter.".format(
+                                meas_error_mitigation_fitter.__class__.__name__
+                            )
+                        )
                     tmp_result = tmp_fitter.filter.apply(
                         tmp_result, self._meas_error_mitigation_method
                     )


Test patch:
diff --git a/test/python/algorithms/test_measure_error_mitigation.py b/test/python/algorithms/test_measure_error_mitigation.py
--- a/test/python/algorithms/test_measure_error_mitigation.py
+++ b/test/python/algorithms/test_measure_error_mitigation.py
@@ -15,6 +15,7 @@
 import unittest
 
 from test.python.algorithms import QiskitAlgorithmsTestCase
+from ddt import ddt, data
 from qiskit import QuantumCircuit
 from qiskit.exceptions import QiskitError
 from qiskit.utils import QuantumInstance, algorithm_globals
@@ -23,18 +24,27 @@
 from qiskit.algorithms.optimizers import SPSA
 from qiskit.circuit.library import EfficientSU2
 
+try:
+    from qiskit.ignis.mitigation.measurement import CompleteMeasFitter, TensoredMeasFitter
+    from qiskit import Aer
+    from qiskit.providers.aer import noise
 
+    _ERROR_MITIGATION_IMPORT_ERROR = None
+except ImportError as ex:
+    _ERROR_MITIGATION_IMPORT_ERROR = str(ex)
+
+
+@ddt
 class TestMeasurementErrorMitigation(QiskitAlgorithmsTestCase):
     """Test measurement error mitigation."""
 
-    def test_measurement_error_mitigation_with_diff_qubit_order(self):
+    @data("CompleteMeasFitter", "TensoredMeasFitter")
+    def test_measurement_error_mitigation_with_diff_qubit_order(self, fitter_str):
         """measurement error mitigation with different qubit order"""
-        try:
-            from qiskit.ignis.mitigation.measurement import CompleteMeasFitter
-            from qiskit import Aer
-            from qiskit.providers.aer import noise
-        except ImportError as ex:
-            self.skipTest("Package doesn't appear to be installed. Error: '{}'".format(str(ex)))
+        if _ERROR_MITIGATION_IMPORT_ERROR is not None:
+            self.skipTest(
+                f"Package doesn't appear to be installed. Error: '{_ERROR_MITIGATION_IMPORT_ERROR}'"
+            )
             return
 
         algorithm_globals.random_seed = 0
@@ -44,6 +54,9 @@ def test_measurement_error_mitigation_with_diff_qubit_order(self):
         read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]])
         noise_model.add_all_qubit_readout_error(read_err)
 
+        fitter_cls = (
+            CompleteMeasFitter if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter
+        )
         backend = Aer.get_backend("aer_simulator")
         quantum_instance = QuantumInstance(
             backend=backend,
@@ -51,7 +64,7 @@ def test_measurement_error_mitigation_with_diff_qubit_order(self):
             seed_transpiler=167,
             shots=1000,
             noise_model=noise_model,
-            measurement_error_mitigation_cls=CompleteMeasFitter,
+            measurement_error_mitigation_cls=fitter_cls,
             cals_matrix_refresh_period=0,
         )
         # circuit
@@ -66,8 +79,17 @@ def test_measurement_error_mitigation_with_diff_qubit_order(self):
         qc2.measure(1, 0)
         qc2.measure(0, 1)
 
-        # this should run smoothly
-        quantum_instance.execute([qc1, qc2])
+        if fitter_cls == TensoredMeasFitter:
+            self.assertRaisesRegex(
+                QiskitError,
+                "TensoredMeasFitter doesn't support subset_fitter.",
+                quantum_instance.execute,
+                [qc1, qc2],
+            )
+        else:
+            # this should run smoothly
+            quantum_instance.execute([qc1, qc2])
+
         self.assertGreater(quantum_instance.time_taken, 0.0)
         quantum_instance.reset_execution_results()
 
@@ -80,16 +102,16 @@ def test_measurement_error_mitigation_with_diff_qubit_order(self):
 
         self.assertRaises(QiskitError, quantum_instance.execute, [qc1, qc3])
 
-    def test_measurement_error_mitigation_with_vqe(self):
+    @data(("CompleteMeasFitter", None), ("TensoredMeasFitter", [[0], [1]]))
+    def test_measurement_error_mitigation_with_vqe(self, config):
         """measurement error mitigation test with vqe"""
-        try:
-            from qiskit.ignis.mitigation.measurement import CompleteMeasFitter
-            from qiskit import Aer
-            from qiskit.providers.aer import noise
-        except ImportError as ex:
-            self.skipTest("Package doesn't appear to be installed. Error: '{}'".format(str(ex)))
+        if _ERROR_MITIGATION_IMPORT_ERROR is not None:
+            self.skipTest(
+                f"Package doesn't appear to be installed. Error: '{_ERROR_MITIGATION_IMPORT_ERROR}'"
+            )
             return
 
+        fitter_str, mit_pattern = config
         algorithm_globals.random_seed = 0
 
         # build noise model
@@ -97,14 +119,17 @@ def test_measurement_error_mitigation_with_vqe(self):
         read_err = noise.errors.readout_error.ReadoutError([[0.9, 0.1], [0.25, 0.75]])
         noise_model.add_all_qubit_readout_error(read_err)
 
+        fitter_cls = (
+            CompleteMeasFitter if fitter_str == "CompleteMeasFitter" else TensoredMeasFitter
+        )
         backend = Aer.get_backend("aer_simulator")
-
         quantum_instance = QuantumInstance(
             backend=backend,
             seed_simulator=167,
             seed_transpiler=167,
             noise_model=noise_model,
-            measurement_error_mitigation_cls=CompleteMeasFitter,
+            measurement_error_mitigation_cls=fitter_cls,
+            mit_pattern=mit_pattern,
         )
 
         h2_hamiltonian = (