Repository: Qiskit/qiskit — pull request #14096 (python)
Title: Fix qpy for `MCMTGate`

<!--\r\n⚠️ If you do not respect this template, your pull request will be closed.\r\n⚠️ Your pull request title should be short detailed and understandable for all.\r\n⚠️ Also, please add a release note file using reno if the change needs to be\r\n  documented in the release notes.\r\n⚠️ If your pull request fixes an open issue, please link to the issue.\r\n\r\n- [x] I have added the tests to cover my changes.\r\n- [x] I have updated the documentation accordingly.\r\n- [x] I have read the CONTRIBUTING document.\r\n-->\r\n\r\n### Summary\r\nThis PR fixes #13965.\r\n\r\n\r\n### Details and comments\r\n`qpy` had to save `base_gate` for `MCMTGate`, which didn't. I only added `MCMTGate` to custom operations because `_read_custom_operations` and `_write_custom_operation` already handled `base_gate`.\r\n\r\n

Resolved issues:
title: QPY fails to serialize MCMTGate gate
body: ### Environment\n\n- **Qiskit version**: 1.4.0\n- **Python version**: 3.12.9\n- **Operating system**: Mac M1\n\n\n### What is happening?\n\nI saved a quantum circuit containing `qiskit.circuit.library.MCMTGate` using `qpy`. The following error happened when I had attempted to load the saved quantum circuit.\n\n```\n---------------------------------------------------------------------------\nTypeError                                 Traceback (most recent call last)\nCell In[9], line 2\n      1 with open(circuit_path, "rb") as circuit_file:\n----> 2     qc = qpy.load(circuit_file)\n      3 qc\n\nFile /opt/homebrew/Caskroom/miniforge/base/envs/qsvm/lib/python3.12/site-packages/qiskit/qpy/interface.py:370, in load(file_obj, metadata_deserializer)\n    367 programs = []\n    368 for _ in range(data.num_programs):\n    369     programs.append(\n--> 370         loader(\n    371             file_obj,\n    372             data.qpy_version,\n    373             metadata_deserializer=metadata_deserializer,\n    374             use_symengine=use_symengine,\n    375         )\n    376     )\n    377 return programs\n\nFile /opt/homebrew/Caskroom/miniforge/base/envs/qsvm/lib/python3.12/site-packages/qiskit/qpy/binary_io/circuits.py:1443, in read_circuit(file_obj, version, metadata_deserializer, use_symengine)\n   1441 custom_operations = _read_custom_operations(file_obj, version, vectors)\n   1442 for _instruction in range(num_instructions):\n-> 1443     _read_instruction(\n   1444         file_obj,\n   1445         circ,\n   1446         out_registers,\n   1447         custom_operations,\n   1448         version,\n   1449         vectors,\n   1450         use_symengine,\n   1451         standalone_var_indices,\n   1452     )\n   1454 # Read calibrations\n   1455 if version >= 5:\n\nFile /opt/homebrew/Caskroom/miniforge/base/envs/qsvm/lib/python3.12/site-packages/qiskit/qpy/binary_io/circuits.py:379, in _read_instruction(file_obj, circuit, registers, custom_operations, version, vectors, use_symengine, standalone_vars)\n    377     gate = gate_class(*params, instruction.num_ctrl_qubits, label=label)\n    378 else:\n--> 379     gate = gate_class(*params, label=label)\n    380     if (\n    381         gate.num_ctrl_qubits != instruction.num_ctrl_qubits\n    382         or gate.ctrl_state != instruction.ctrl_state\n    383     ):\n    384         gate = gate.to_mutable()\n\nTypeError: MCMTGate.__init__() missing 3 required positional arguments: 'gate', 'num_ctrl_qubits', and 'num_target_qubits'\n\n```\n\n### How can we reproduce the issue?\n\n```python\nqc = qiskit.QuantumCircuit(3)\nqc.compose(qiskit.circuit.library.MCMTGate(qiskit.circuit.library.ZGate(), 2, 1), inplace=True)\n\ncircuit_path = "circuit.qpy"\nwith open(circuit_path, "wb") as circuit_file:\n    qpy.dump(qc, circuit_file)\n\nwith open(circuit_path, "rb") as circuit_file:\n    qc = qpy.load(circuit_file)\n```\n\n### What should happen?\n\nThe saved quantum circuit should be loaded without any error.\n\n### Any suggestions?\n\nIt seems like this issue relates to #9390 for me?

Hints:

Fix patch:
diff --git a/qiskit/qpy/binary_io/circuits.py b/qiskit/qpy/binary_io/circuits.py\nindex 91a06ccfe92f..46837b9f2deb 100644\n--- a/qiskit/qpy/binary_io/circuits.py\n+++ b/qiskit/qpy/binary_io/circuits.py\n@@ -783,6 +783,11 @@ def _write_instruction(\n         custom_operations[gate_class_name] = instruction.operation\n         custom_operations_list.append(gate_class_name)\n \n+    elif isinstance(instruction.operation, library.MCMTGate):\n+        gate_class_name = instruction.operation.name + "_" + str(uuid.uuid4())\n+        custom_operations[gate_class_name] = instruction.operation\n+        custom_operations_list.append(gate_class_name)\n+\n     condition_type = type_keys.Condition.NONE\n     condition_register = b""\n     condition_value = 0\ndiff --git a/releasenotes/notes/fix-qpy-mcmtgate-ab735812998f4107.yaml b/releasenotes/notes/fix-qpy-mcmtgate-ab735812998f4107.yaml\nnew file mode 100644\nindex 000000000000..72de29c3c31e\n--- /dev/null\n+++ b/releasenotes/notes/fix-qpy-mcmtgate-ab735812998f4107.yaml\n@@ -0,0 +1,6 @@\n+---\n+fixes:\n+  - |\n+    Fixed a bug in QPY (:mod:`qiskit.qpy`) where circuits containing gates of class\n+    :class:`.MCMTGate` would fail to serialize.\n+    See `#13965 <https://github.com/Qiskit/qiskit/issues/13965>`__.\n\\ No newline at end of file\n

Test patch:
diff --git a/test/python/circuit/test_circuit_load_from_qpy.py b/test/python/circuit/test_circuit_load_from_qpy.py\nindex 32cea72a783c..f8d17a5fd100 100644\n--- a/test/python/circuit/test_circuit_load_from_qpy.py\n+++ b/test/python/circuit/test_circuit_load_from_qpy.py\n@@ -30,6 +30,7 @@\n from qiskit.circuit.gate import Gate\n from qiskit.circuit.library import (\n     XGate,\n+    ZGate,\n     CXGate,\n     RYGate,\n     QFT,\n@@ -41,6 +42,7 @@\n     MCXGrayCode,\n     MCXRecursive,\n     MCXVChain,\n+    MCMTGate,\n     UCRXGate,\n     UCRYGate,\n     UCRZGate,\n@@ -1261,11 +1263,13 @@ def test_standard_control_gates(self):\n         mcx_gray_gate = MCXGrayCode(5)\n         mcx_recursive_gate = MCXRecursive(4)\n         mcx_vchain_gate = MCXVChain(3)\n+        mcmt_gate = MCMTGate(ZGate(), 2, 1)\n         qc.append(mcu1_gate, [0, 2, 1])\n         qc.append(mcx_gate, list(range(0, 6)))\n         qc.append(mcx_gray_gate, list(range(0, 6)))\n         qc.append(mcx_recursive_gate, list(range(0, 5)))\n         qc.append(mcx_vchain_gate, list(range(0, 5)))\n+        qc.append(mcmt_gate, list(range(0, 3)))\n         qc.mcp(np.pi, [0, 2], 1)\n         qc.mcx([0, 2], 1)\n         qc.measure_all()\n