Repository: qutip/qutip

Issue:
hardware_info fails (again) on M1 MacBook Pro running Big Sur 11.4
**Describe the bug**
qutip fails to import with the following error:
```
Python 3.9.6 (default, Jun 28 2021, 19:24:41) 
[Clang 12.0.5 (clang-1205.0.22.9)] on darwin
Type "help", "copyright", "credits" or "license" for more information.
>>> import qutip
Traceback (most recent call last):
  File "<stdin>", line 1, in <module>
  File "/opt/homebrew/lib/python3.9/site-packages/qutip/__init__.py", line 115, in <module>
    info = qutip.hardware_info.hardware_info()
  File "/opt/homebrew/lib/python3.9/site-packages/qutip/hardware_info.py", line 133, in hardware_info
    out = _mac_hardware_info()
  File "/opt/homebrew/lib/python3.9/site-packages/qutip/hardware_info.py", line 50, in _mac_hardware_info
    results.update({'cpu_freq': int(float(os.popen('sysctl hw.cpufrequency')
IndexError: list index out of range
```

This appears to be caused by Apple having removed hw.cpufrequency from the list of sysctl's (see below)

**To Reproduce**
Installed qutip using homebrew/pip3 (after using the workaround of specifying OPENBLAS properly before building scipy etc... so that part is all sorted). Then python3 and import qutip.

```python
from qutip import identity
print(identity(2))
```
The terminal output (aftrer I hacked up a workaround for this issue):
```
Quantum object: dims = [[2], [2]], shape = (2, 2), type = oper, isherm = True
Qobj data =
[[1. 0.]
 [0. 1.]]
```

**Expected behavior**
qutip successfully initializes :-)

**Your Environment**
Please use `qutip.about()` to get the information about your environment and paste it here.
```
>>> qutip.about()

QuTiP: Quantum Toolbox in Python
================================
Copyright (c) QuTiP team 2011 and later.
Current admin team: Alexander Pitchford, Nathan Shammah, Shahnawaz Ahmed, Neill Lambert, Eric Giguère, Boxi Li, Jake Lishman and Simon Cross.
Board members: Daniel Burgarth, Robert Johansson, Anton F. Kockum, Franco Nori and Will Zeng.
Original developers: R. J. Johansson & P. D. Nation.
Previous lead developers: Chris Granade & A. Grimsmo.
Currently developed through wide collaboration. See https://github.com/qutip for details.

QuTiP Version:      4.6.2
Numpy Version:      1.21.0
Scipy Version:      1.7.0
Cython Version:     0.29.23
Matplotlib Version: 3.4.2
Python Version:     3.9.6
Number of CPUs:     8
BLAS Info:          OPENBLAS
OPENMP Installed:   False
INTEL MKL Ext:      False
Platform Info:      Darwin (arm64)
Installation path:  /opt/homebrew/lib/python3.9/site-packages/qutip
================================================================================
Please cite QuTiP in your publication.
================================================================================
```

**Additional context**
 No sysctl hw.cpufrequency at all on this machine, so it blows up (some error handling in that function would be good :-)

```$ sysctl hw
hw.ncpu: 8
hw.byteorder: 1234
hw.memsize: 17179869184
hw.activecpu: 8
hw.optional.amx_version: 2
hw.optional.arm64: 1
hw.optional.armv8_1_atomics: 1
hw.optional.armv8_2_fhm: 1
hw.optional.armv8_2_sha3: 1
hw.optional.armv8_2_sha512: 1
hw.optional.armv8_crc32: 1
hw.optional.breakpoint: 6
hw.optional.floatingpoint: 1
hw.optional.neon: 1
hw.optional.neon_fp16: 1
hw.optional.neon_hpfp: 1
hw.optional.ucnormal_mem: 1
hw.optional.watchpoint: 4
hw.cacheconfig: 8 1 1 0 0 0 0 0 0 0
hw.cachelinesize: 128
hw.cachesize: 3616980992 65536 4194304 0 0 0 0 0 0 0
hw.cpu64bit_capable: 1
hw.cpufamily: 458787763
hw.cpusubfamily: 2
hw.cpusubtype: 2
hw.cputype: 16777228
hw.ephemeral_storage: 0
hw.l1dcachesize: 65536
hw.l1icachesize: 131072
hw.l2cachesize: 4194304
hw.logicalcpu: 8
hw.logicalcpu_max: 8
hw.osenvironment: 
hw.packages: 1
hw.pagesize: 16384
hw.pagesize32: 16384
hw.physicalcpu: 8
hw.physicalcpu_max: 8
hw.serialdebugmode: 0
hw.tbfrequency: 24000000
hw.use_kernelmanagerd: 1
hw.use_recovery_securityd: 0
hw.targettype: J293
```




Discussion:
@ozbenh Thanks for reporting this!

Do you perhaps know what is causing this on your M1? Is it something Apple forgot to add to the platform? Or is it that they're deprecating `hw.cpufrequency` entirely?

What was your fix?

Unfortunately we have no way to test on M1 ourselves at the moment. Any suggestions welcome. :|

Perhaps the best we can do at the moment is harden QuTiP against these breakages so that it doesn't fail to import if some of the hardware information is missing on the M1. 
It appears that they have removed hw.cpufrequency on these things (About this mac doesn't show a freq. either). I don't know what is their policy in this regard but I did find these two related issues: https://github.com/giampaolo/psutil/issues/1892 and https://github.com/shirou/gopsutil/pull/999.

My workaround was to comment out the code and hard-wire a random value in there :-) A quick grep didn't seem to show any use of cpu_freq elsewhere in the code but then I didn't look too much in depth. I don't personally use qutip, my partner does, she's the physicist, I'm just the geek helping hand :)

It does look like this code could use some error handling and put some "reasonable" default values if the sysctl's fail indeed. Happy to test a patch and report. She's currently away so there will be some latency (I can occasionally ssh into her laptop but it takes preparation), so I can't really submit a PR myself at this point.
Can confirm both the error and the workaround. As a physicist, I just did this to lines 48 and 49 of `hardware_info.py`
```
#results.update({'cpu_freq': int(float(os.popen('sysctl -n machdep.cpu.brand_string')
                            #.readlines()[0].split('@')[1][:-4])*1000)})
results.update({'cpu_freq': 3000})
```
Works fine now.

Resolving patch:
diff --git a/qutip/hardware_info.py b/qutip/hardware_info.py
--- a/qutip/hardware_info.py
+++ b/qutip/hardware_info.py
@@ -8,15 +8,27 @@
 
 
 def _mac_hardware_info():
-    info = dict()
-    results = dict()
-    for l in [l.split(':') for l in os.popen('sysctl hw').readlines()[1:]]:
-        info[l[0].strip(' "').replace(' ', '_').lower().strip('hw.')] = \
-            l[1].strip('.\n ')
+    info = {}
+    results = {}
+    with os.popen('sysctl hw') as f:
+        lines = f.readlines()
+    for line in lines[1:]:
+        key, _, value = line.partition(':')
+        key = key.strip(' "').replace(' ', '_').lower().strip('hw.')
+        value = value.strip('.\n ')
+        info[key] = value
     results.update({'cpus': int(info['physicalcpu'])})
-    results.update({'cpu_freq': int(float(os.popen('sysctl hw.cpufrequency')
-                                          .readlines()[0].split(':')[
-                                              1]) / 1000000)})
+    # Mac OS currently doesn't not provide hw.cpufrequency on the M1
+    with os.popen('sysctl hw.cpufrequency') as f:
+        cpu_freq_lines = f.readlines()
+    if cpu_freq_lines:
+        # Yay, hw.cpufrequency present
+        results.update({
+            'cpu_freq': float(cpu_freq_lines[0].split(':')[1]) / 1000000,
+        })
+    else:
+        # No hw.cpufrequency, assume Apple M1 CPU (all are 3.2 GHz currently)
+        results['cpu_freq'] = 3.2
     results.update({'memsize': int(int(info['memsize']) / (1024 ** 2))})
     # add OS information
     results.update({'os': 'Mac OSX'})
@@ -32,19 +44,19 @@ def _linux_hardware_info():
     with open("/proc/cpuinfo") as f:
         for l in [l.split(':') for l in f.readlines()]:
             if (l[0].strip() == "physical id"):
-                sockets = np.maximum(sockets,int(l[1].strip())+1)
+                sockets = np.maximum(sockets, int(l[1].strip()) + 1)
             if (l[0].strip() == "cpu cores"):
                 cores_per_socket = int(l[1].strip())
             if (l[0].strip() == "cpu MHz"):
                 frequency = float(l[1].strip()) / 1000.
-    results.update({'cpus': sockets * cores_per_socket})
+    results.update({'cpus': int(sockets * cores_per_socket)})
     # get cpu frequency directly (bypasses freq scaling)
     try:
         with open(
                 "/sys/devices/system/cpu/cpu0/cpufreq/cpuinfo_max_freq") as f:
             line = f.readlines()[0]
         frequency = float(line.strip('\n')) / 1000000.
-    except:
+    except Exception:
         pass
     results.update({'cpu_freq': frequency})
 
@@ -78,7 +90,7 @@ def _win_hardware_info():
         ncpus = 0
         for cpu in cpus:
             ncpus += int(cpu.Properties_['NumberOfCores'].Value)
-    except:
+    except Exception:
         ncpus = int(multiprocessing.cpu_count())
     return {'os': 'Windows', 'cpus': ncpus}
 


Test patch:
diff --git a/qutip/tests/test_hardware_info.py b/qutip/tests/test_hardware_info.py
new file mode 100644
--- /dev/null
+++ b/qutip/tests/test_hardware_info.py
@@ -0,0 +1,15 @@
+"""
+Tests for qutip.hardware_info.
+"""
+
+from qutip.hardware_info import hardware_info
+
+
+def test_hardware_info():
+    info = hardware_info()
+    assert info["os"] in ("Windows", "FreeBSD", "Linux", "Mac OSX")
+    assert info["cpus"] > 0
+    assert isinstance(info["cpus"], int)
+    cpu_freq = info.get("cpu_freq", 0.0)
+    assert cpu_freq >= 0.
+    assert isinstance(cpu_freq, float)