Repository: XanaduAI/strawberryfields

Issue:
Error when printing Result for stateless engine
#### Issue description
* *Expected behavior:*
The result (samples) is printed without the state being included.

* *Actual behavior:*
The following error raised:
```python
Traceback (most recent call last):
  File "remote_engine_example.py", line 27, in <module>
    print(result)
  File "/strawberryfields/strawberryfields/api/result.py", line 113, in __str__
    len(self.samples), self.state, self.samples
  File "/strawberryfields/strawberryfields/api/result.py", line 107, in state
    raise AttributeError("The state is undefined for a stateless computation.")
AttributeError: The state is undefined for a stateless computation.
```

* *Reproduces how often:*
Every time.

* *System information:*
```python
Strawberry Fields: a Python library for continuous-variable quantum circuits.
Copyright 2018-2020 Xanadu Quantum Technologies Inc.

Python version:            3.7.6
Platform info:             Linux-4.19.11-041911-generic-x86_64-with-debian-buster-sid
Installation path:        /strawberryfields/strawberryfields
Strawberry Fields version: 0.13.0.rc0
Numpy version:             1.18.1
Scipy version:             1.3.2
SymPy version:             1.5.1
NetworkX version:          2.3
The Walrus version:        0.12.0
Blackbird version:         0.2.3
TensorFlow version:        2.0.1
```
#### Source code and tracebacks

```python
import strawberryfields as sf 
from strawberryfields import ops 
from strawberryfields.utils import random_interferometer 

con = sf.api.Connection() 
eng = sf.RemoteEngine("X8", connection=con) 
prog = sf.Program(8) 
U = random_interferometer(4) 

with prog.context as q: 
    ops.S2gate(1.0) | (q[0], q[4]) 
    ops.S2gate(1.0) | (q[1], q[5]) 
    ops.S2gate(1.0) | (q[2], q[6]) 
    ops.S2gate(1.0) | (q[3], q[7]) 
    ops.Interferometer(U) | q[:4] 
    ops.Interferometer(U) | q[4:] 
    ops.MeasureFock() | q 

result = eng.run(prog, shots=1000)
print(result)
```

#### Additional information

The error simply comes from the fact that the `state` property raises an error in the stateless case. It is, however, also used in the definition for string representation in `Result`, therefore `print(result)` will indeed result in an error for stateless engines.

A possible solution could be creating a branching logic inside of `__str__` without calling `state` in the stateless case.


Discussion:
A case for subclassing the result object? I feel like there may be other things that would also differentiate the "result" obtained via simulation versus that obtained remotely
That's a great idea, as it would be an even cleaner solution! Could also help us if we expect that other factors would further differentiate the two types of `Result`s.
To be honest, it doesn't makes sense to print the state and the samples when simply `print(results)` is called.

Typically when you print an object in Python, you should just get high level metadata of the object, e.g.,

```python
>>> print(results)
<Result: modes=4, backend=fock, origin=local, state_vector=True, shots=10> 
```

The state and the sample can then be printed separately using `print(result.state)` and `print(result.sample)`.

Trying to print all data stored in the class at once, especially when some of the data can be exceptionally large, seems weird, and likely has a lot edge cases (e.g., unexpected bugs like this, the whole state might inadvertently show up in log files, the state could show up unexpectedly in Jupyter notebook environments, etc.)

> A case for subclassing the result object?

I would argue no (for now), there isn't a clear logical separation between the two (yet), and I would rather we wait to see what that looks like before designing subclasses and abstract base classes. 

Currently, local simulators can return either states and samples, just states, or just samples, depending on the program and the backend options, so this bug also affects the `LocalEngine` equally.

> A possible solution could be creating a branching logic inside of __str__ without calling state in the stateless case.

I would recommend just not printing states and samples when printing the top level results object.

If I recall the lineage of this `__str__` function correctly, this existed before the initial `StarshipEngine` refactor (during which the `AttributeError` was added for stateless results and this bug was introduced, so my bad :slightly_smiling_face:), and this issue was specifically brought up in that PR - but I don't exactly remember why it stuck around. I'm not sure if anyone really wants it at this point, and in hindsight I think it makes perfect sense to remove states and samples here.

Resolving patch:
diff --git a/strawberryfields/api/connection.py b/strawberryfields/api/connection.py
--- a/strawberryfields/api/connection.py
+++ b/strawberryfields/api/connection.py
@@ -227,7 +227,15 @@ def get_job_result(self, job_id: str) -> Result:
             with io.BytesIO() as buf:
                 buf.write(response.content)
                 buf.seek(0)
+
                 samples = np.load(buf, allow_pickle=False)
+
+                if np.issubdtype(samples.dtype, np.integer):
+                    # Samples represent photon numbers.
+                    # Convert to int64, to avoid unexpected behaviour
+                    # when users postprocess these samples.
+                    samples = samples.astype(np.int64)
+
             return Result(samples, is_stateful=False)
         raise RequestFailedError(
             "Failed to get job result: {}".format(self._format_error_message(response))
diff --git a/strawberryfields/api/result.py b/strawberryfields/api/result.py
--- a/strawberryfields/api/result.py
+++ b/strawberryfields/api/result.py
@@ -107,8 +107,9 @@ def state(self):
             raise AttributeError("The state is undefined for a stateless computation.")
         return self._state
 
-    def __str__(self):
+    def __repr__(self):
         """String representation."""
-        return "Result: {} subsystems, state: {}\n samples: {}".format(
-            len(self.samples), self.state, self.samples
+        shots, modes = self.samples.shape
+        return "<Result: num_modes={}, shots={}, contains state={}>".format(
+            modes, shots, self._is_stateful
         )


Test patch:
diff --git a/tests/api/test_result.py b/tests/api/test_result.py
--- a/tests/api/test_result.py
+++ b/tests/api/test_result.py
@@ -14,6 +14,7 @@
 """
 Unit tests for strawberryfields.api.result
 """
+import numpy as np
 import pytest
 
 from strawberryfields.api import Result
@@ -29,9 +30,27 @@ class TestResult:
     def test_stateless_result_raises_on_state_access(self):
         """Tests that `result.state` raises an error for a stateless result.
         """
-        result = Result([[1, 2], [3, 4]], is_stateful=False)
+        result = Result(np.array([[1, 2], [3, 4]]), is_stateful=False)
 
         with pytest.raises(
             AttributeError, match="The state is undefined for a stateless computation."
         ):
             result.state
+
+    def test_stateless_print(self, capfd):
+        """Test that printing a result object with no state provides the correct output."""
+        result = Result(np.array([[1, 2], [3, 4], [5, 6]]), is_stateful=False)
+        print(result)
+        out, err = capfd.readouterr()
+        assert "modes=2" in out
+        assert "shots=3" in out
+        assert "contains state=False" in out
+
+    def test_state_print(self, capfd):
+        """Test that printing a result object with a state provides the correct output."""
+        result = Result(np.array([[1, 2], [3, 4], [5, 6]]), is_stateful=True)
+        print(result)
+        out, err = capfd.readouterr()
+        assert "modes=2" in out
+        assert "shots=3" in out
+        assert "contains state=True" in out