Repository: qutip/qutip

Issue:
Qobj.extract_states() doesn't preserve the isherm attribute
### Bug Description

The method extract_states() of the Qobj class doesn't preserve the isherm attribute of the operators it truncates. Maybe I'm forgetting an edge case, but it seems to me that taking any subset of states from an Hermitian operator should results in an Hermitian operator.

### Code to Reproduce the Bug

```shell
# Imports
import qutip as qt
import numpy as np
import scqubits as scq

# Maximum number of transmon states
k_t_max = 201
ncut = int((k_t_max-1)/2)
# Number of states to truncate to
k_t_trunc = 100


# Transmon Hamiltonian from scqubits
transmon = scq.Transmon(EJ=22.975, EC=0.194, ng=0, ncut=ncut)

# Form a qutip object
H_t = qt.Qobj(transmon.hamiltonian())

# At this stage, the Hamiltonian has the Hermitian attribute
print("Is the Hamiltonian Hermitian? Answer: {}".format(H_t.isherm))

# And the anti-Hermitian part is truly 0
diff = H_t - H_t.dag()
print("The norm of the anti-Hermitian part is {}".format(diff.norm()))

# Calculate the eigenvalues and eigenvectors
evals_t, evecs_t = H_t.eigenstates()

# Diagonalize the Hamiltonian
H_t = H_t.transform(evecs_t)

# At this stage, the Hamiltonian still has the Hermitian attribute
print("Is the Hamiltonian still Hermitian after diagonalization? Answer: {}".format(H_t.isherm))

# However, the transformation gave a residual non-Hermitian part
diff = H_t - H_t.dag()
print("The norm of the anti-Hermitian part after diagonalization is {}".format(diff.norm()))

# Truncate the Hamiltonian
H_t = H_t.extract_states(range(k_t_trunc))

# At this stage, the Hamiltonian has lost the Hermitian attribute
print("Is the Hamiltonian still Hermitian after truncation? Answer: {}".format(H_t.isherm))

# And it still has a non-zero Hermitian part
diff = H_t - H_t.dag()
print("The norm of the anti-Hermitian part after truncation is {}".format(diff.norm()))
```


### Code Output

```shell
Is the Hamiltonian Hermitian? Answer: True
The norm of the anti-Hermitian part is 0.0
Is the Hamiltonian still Hermitian after diagonalization? Answer: True
The norm of the anti-Hermitian part after diagonalization is 1.2427378357318259e-11
Is the Hamiltonian still Hermitian after truncation? Answer: False
The norm of the anti-Hermitian part after truncation is 6.2142895453943875e-12
```


### Expected Behaviour

Extracting a state of states from an Hermitian operator should yield an Hermitian operator.

### Your Environment

```shell
QuTiP Version:      4.7.2
Numpy Version:      1.24.3
Scipy Version:      1.10.1
Cython Version:     0.29.35
Matplotlib Version: 3.7.1
Python Version:     3.10.6
Number of CPUs:     12
BLAS Info:          OPENBLAS
OPENMP Installed:   False
INTEL MKL Ext:      False
Platform Info:      Darwin (arm64)
```


### Additional Context

_No response_


Discussion:
Thanks for reporting this issue. It does seem to be a bug. I will take a closer look at the `extract_states` method and propose a change.


Resolving patch:
diff --git a/qutip/qobj.py b/qutip/qobj.py
--- a/qutip/qobj.py
+++ b/qutip/qobj.py
@@ -1843,7 +1843,7 @@ def extract_states(self, states_inds, normalize=False):
 
         """
         if self.isoper:
-            q = Qobj(self.data[states_inds, :][:, states_inds])
+            q = Qobj(self.data[states_inds, :][:, states_inds], isherm=self._isherm or None)
         elif self.isket:
             q = Qobj(self.data[states_inds, :])
         elif self.isbra:
diff --git a/qutip/random_objects.py b/qutip/random_objects.py
--- a/qutip/random_objects.py
+++ b/qutip/random_objects.py
@@ -137,7 +137,7 @@ def rand_herm(N, density=0.75, dims=None, pos_def=False, seed=None):
             M = _rand_herm_dense(N, density, pos_def)
     else:
         raise TypeError('Input N must be an integer or array_like.')
-    return Qobj(M, dims=dims)
+    return Qobj(M, dims=dims, isherm=True)
 
 
 def _rand_herm_sparse(N, density, pos_def):


Test patch:
diff --git a/qutip/tests/test_qobj.py b/qutip/tests/test_qobj.py
--- a/qutip/tests/test_qobj.py
+++ b/qutip/tests/test_qobj.py
@@ -1012,6 +1012,14 @@ def test_overlap():
         assert np.allclose(Ad.overlap(Bd), ans)
         assert np.allclose(A.overlap(Bd), np.conj(ans))
 
+def test_extract_states_hermiticity():
+    """Test that hermiticity is preserved after extract_states."""
+
+    H_t = rand_herm(10, seed=0)
+    H_t_reduced = H_t.extract_states(range(3))
+
+    assert H_t_reduced._isherm == True
+
 
 def test_unit():
     """