Repository: qutip/qutip

Issue:
Add the matrix logrithm method logm() to Qobj.
### Problem Description

I don't find matrix logrithm logm() in Qobj methods, so to compute matrix logrithm I call logm() in scipy.linalg, which changes the type from qobj to ndarray. And to continue the subsequent computation, I need to reinstantiate the resulting ndarray to Qobj, which is cumbersome.
It would be better if the matrix logrithm logm() is added to the Qobj

### Proposed Solution

Add the matrix logrithm method logm() to Qobj.

### Alternate Solutions

_No response_

### Additional Context

_No response_


Discussion:

Resolving patch:
diff --git a/qutip/core/data/expm.py b/qutip/core/data/expm.py
--- a/qutip/core/data/expm.py
+++ b/qutip/core/data/expm.py
@@ -1,5 +1,6 @@
 import numpy as np
 import scipy.sparse.linalg
+import scipy.linalg
 
 from .dense import Dense
 from .csr import CSR
@@ -8,7 +9,8 @@
 from .base import idxint_dtype
 
 __all__ = [
-    'expm', 'expm_csr', 'expm_csr_dense',
+    'expm', 'expm_csr', 'expm_csr_dense', 'expm_dense',
+    'logm', 'logm_dense',
 ]
 
 
@@ -41,6 +43,12 @@ def expm_csr_dense(matrix: CSR) -> Dense:
     return Dense(scipy.sparse.linalg.expm(matrix.to_array()))
 
 
+def expm_dense(matrix: Dense) -> Dense:
+    if matrix.shape[0] != matrix.shape[1]:
+        raise ValueError("can only exponentiate square matrix")
+    return Dense(scipy.linalg.expm(matrix.as_ndarray()), copy=False)
+
+
 from .dispatch import Dispatcher as _Dispatcher
 import inspect as _inspect
 
@@ -58,6 +66,28 @@ def expm_csr_dense(matrix: CSR) -> Dense:
 expm.add_specialisations([
     (CSR, CSR, expm_csr),
     (CSR, Dense, expm_csr_dense),
+    (Dense, Dense, expm_dense),
+], _defer=True)
+
+
+def logm_dense(matrix: Dense) -> Dense:
+    if matrix.shape[0] != matrix.shape[1]:
+        raise ValueError("can only compute logarithm square matrix")
+    return Dense(scipy.linalg.logm(matrix.as_ndarray()), copy=False)
+
+
+logm = _Dispatcher(
+    _inspect.Signature([
+        _inspect.Parameter('matrix', _inspect.Parameter.POSITIONAL_OR_KEYWORD),
+    ]),
+    name='logm',
+    module=__name__,
+    inputs=('matrix',),
+    out=True,
+)
+logm.__doc__ = """Matrix logarithm `ln(A)` for a matrix `A`."""
+logm.add_specialisations([
+    (Dense, Dense, logm_dense),
 ], _defer=True)
 
 del _inspect, _Dispatcher
diff --git a/qutip/core/qobj.py b/qutip/core/qobj.py
--- a/qutip/core/qobj.py
+++ b/qutip/core/qobj.py
@@ -869,6 +869,30 @@ def expm(self, dtype=_data.Dense):
                     isherm=self._isherm,
                     copy=False)
 
+    def logm(self):
+        """Matrix logarithm of quantum operator.
+
+        Input operator must be square.
+
+        Returns
+        -------
+        oper : :class:`qutip.Qobj`
+            Logarithm of the quantum operator.
+
+        Raises
+        ------
+        TypeError
+            Quantum operator is not square.
+        """
+        if self.dims[0] != self.dims[1]:
+            raise TypeError("expm is only valid for square operators")
+        return Qobj(_data.logm(self._data),
+                    dims=self.dims,
+                    type=self.type,
+                    superrep=self.superrep,
+                    isherm=self._isherm,
+                    copy=False)
+
     def check_herm(self):
         """Check if the quantum object is hermitian.
 


Test patch:
diff --git a/qutip/tests/core/data/test_mathematics.py b/qutip/tests/core/data/test_mathematics.py
--- a/qutip/tests/core/data/test_mathematics.py
+++ b/qutip/tests/core/data/test_mathematics.py
@@ -841,6 +841,18 @@ def op_numpy(self, matrix):
     specialisations = [
         pytest.param(data.expm_csr, CSR, CSR),
         pytest.param(data.expm_csr_dense, CSR, Dense),
+        pytest.param(data.expm_dense, Dense, Dense),
+    ]
+
+
+class TestLogm(UnaryOpMixin):
+    def op_numpy(self, matrix):
+        return scipy.linalg.logm(matrix)
+
+    shapes = shapes_square()
+    bad_shapes = shapes_not_square()
+    specialisations = [
+        pytest.param(data.logm_dense, Dense, Dense),
     ]
 
 
diff --git a/qutip/tests/core/test_qobj.py b/qutip/tests/core/test_qobj.py
--- a/qutip/tests/core/test_qobj.py
+++ b/qutip/tests/core/test_qobj.py
@@ -551,6 +551,29 @@ def test_QobjExpmZeroOper():
     assert B == qutip.qeye(5)
 
 
+def test_QobjLogm():
+    "qutip.Qobj expm (dense)"
+    data = _random_not_singular(15)
+    A = qutip.Qobj(data)
+    B = A.logm()
+    np.testing.assert_allclose(B.full(), scipy.linalg.logm(data), atol=1e-10)
+
+
+def test_QobjLogmExplicitlySparse():
+    "qutip.Qobj logm (sparse)"
+    data = _random_not_singular(15)
+    A = qutip.Qobj(data).to("csr")
+    B = A.logm()
+    np.testing.assert_allclose(B.full(), scipy.linalg.logm(data), atol=1e-10)
+
+
+def test_QobjLogmZeroOper():
+    "qutip.Qobj logm zero_oper (#493)"
+    A = qutip.qeye(5)
+    B = A.logm()
+    assert B == qutip.qzero(5)
+
+
 def test_Qobj_sqrtm():
     "qutip.Qobj sqrtm"
     data = _random_not_singular(5)