Repository: qutip/qutip

Issue:
Setting `qutip-qip` as an optional package for qutip-5
### Problem Description

From version 5.0, the subpackage `qutip.qip` will be removed and replaced by a separate package `qutip-qip`. To make it easier for others to migrate from 4.7 to 5.0, we can set up `qutip-qip` as an optional plug-in for qutip, so that if `qutip-qip` is installed, the `qutip.qip` name space can be used as before.

### Expected behaviour
- If only `qutip` is installed, importing `qutip.qip` will raise an `ImportError` with instruction for installing `qutip_qip`.
- If `qutip-qip` is installed, using `qutip.qip` will automatically use the `qutip_qip` namespace, with no difference in the user interface.
- Update `setup.cfg` so that `pip install qutip[qip]` will automatically install `qutip_qip`.

E.g. the following should work if `qutip`-5.0 and `qutip-qip` are both installed
```python
# The following should be the same
from qutip.qip.circuit import QubitCircuit
from qutip_qip.circuit import QubitCircuit
```


### Proposed Solution

See https://github.com/qutip/qutip/discussions/1500 for ideals on the implementation.

**Note: The implementation should be done against the `dev.major` branch of qutip.**


Discussion:
Hey! I'd like to help you out with this, is that okay @nathanshammah @hodgestar ? If there's a problem with me being a maintainer at Unitaryhack just let me know, no hard feelings of course
@HGSilveri @nathanshammah will have to be final arbitrator on the rules, but from my side there are no objections. Note that this would be a PR againt the `dev.major` branch.
@HGSilveri that's totally fine, no restrictions whatsoever! Actually that's very cool. I hope I can make some contributions too...
Awesome, I'll do my best!

> Note that this would be a PR againt the `dev.major` branch.

Does this entail any extra steps not encompassed in the contributing guidelines, or is it just a matter of branching from and PRing to `dev.major` instead of `master`?
> Does this entail any extra steps not encompassed in the contributing guidelines, or is it just a matter of branching from and PRing to `dev.major` instead of `master`?

Just branching from and PRing to `dev.major` -- the contribution guidelines are the same.
Roger that. Feel free to assign this to me then 😄 

Resolving patch:
diff --git a/qutip/qip.py b/qutip/qip.py
new file mode 100644
--- /dev/null
+++ b/qutip/qip.py
@@ -0,0 +1,13 @@
+"""Module replicating the qutip_qip package from within qutip."""
+import sys
+
+try:
+    import qutip_qip
+    del qutip_qip
+    sys.modules["qutip.qip"] = sys.modules["qutip_qip"]
+except ImportError:
+    raise ImportError(
+        "Importing 'qutip.qip' requires the 'qutip_qip' package. Install it "
+        "with `pip install qutip-qip` (for more details, go to "
+        "https://qutip-qip.readthedocs.io/)."
+    )


Test patch:
diff --git a/qutip/tests/test_qip.py b/qutip/tests/test_qip.py
new file mode 100644
--- /dev/null
+++ b/qutip/tests/test_qip.py
@@ -0,0 +1,46 @@
+import sys
+
+import pytest
+
+
+@pytest.fixture
+def without_qutip_qip(monkeypatch):
+    monkeypatch.setitem(sys.modules, "qutip_qip", None)
+    monkeypatch.delitem(sys.modules, "qutip.qip", raising=False)
+
+
+@pytest.fixture
+def with_qutip_qip_stub(tmp_path, monkeypatch):
+    pkg_dir = tmp_path / "qutip_qip"
+    pkg_dir.mkdir()
+    init_file = pkg_dir / "__init__.py"
+    init_file.write_text("__version__ = 'x.y.z'")
+    circuit_file = pkg_dir / "circuit.py"
+    circuit_file.write_text("class QubitCircuit:\n    pass")
+
+    monkeypatch.syspath_prepend(tmp_path)
+    # Make sure the stub modules is the one imported
+    monkeypatch.delitem(sys.modules, "qutip_qip", raising=False)
+    monkeypatch.delitem(sys.modules, "qutip.qip", raising=False)
+
+
+def test_failed_import(without_qutip_qip):
+    # Ensure 'qutip.qip' is not imported yet
+    assert "qutip.qip" not in sys.modules
+    with pytest.raises(
+        ImportError,
+        match="Importing 'qutip.qip' requires the 'qutip_qip' package.",
+    ):
+        import qutip.qip
+
+
+def test_with_qip(with_qutip_qip_stub):
+    import qutip.qip
+    import qutip.qip.circuit as circuit
+    from qutip.qip.circuit import QubitCircuit
+    import qutip_qip
+
+    assert qutip.qip.__version__ == "x.y.z"
+    assert qutip.qip is qutip_qip
+    assert circuit is qutip_qip.circuit
+    assert QubitCircuit is qutip_qip.circuit.QubitCircuit