Skip to content
This repository was archived by the owner on Jan 12, 2024. It is now read-only.

Wrap runtime failures in Python exceptions. #560

Open
wants to merge 5 commits into
base: main
Choose a base branch
from
Open
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 2 additions & 2 deletions src/Python/qsharp-core/qsharp/__init__.py
Original file line number Diff line number Diff line change
@@ -20,7 +20,7 @@
from distutils.version import LooseVersion

from qsharp.clients import _start_client
from qsharp.clients.iqsharp import IQSharpError
from qsharp.clients.iqsharp import IQSharpError, ExecutionFailedException
from qsharp.loader import QSharpCallable, QSharpModuleFinder
from qsharp.config import Config
from qsharp.packages import Packages
@@ -40,7 +40,7 @@
'config',
'packages',
'projects',
'IQSharpError',
'IQSharpError', 'ExecutionFailedException',
'Result', 'Pauli'
]

81 changes: 67 additions & 14 deletions src/Python/qsharp-core/qsharp/clients/iqsharp.py
Original file line number Diff line number Diff line change
@@ -65,6 +65,16 @@ def __init__(self, iqsharp_errors : List[str]):
])
super().__init__(error_msg.getvalue())

class ExecutionFailedException(RuntimeError):
"""
Represents when a Q# execution reached a fail statement.
"""
def __init__(self, message: str, stack_trace: List[str]):
self.message = message
self.stack_trace = stack_trace
formatted_trace = str.join('\n', stack_trace)
super().__init__(f"Q# execution failed: {message}\n{formatted_trace}")

class AlreadyExecutingError(IOError):
"""
Raised when the IQ# client is already executing a command and cannot safely
@@ -171,15 +181,15 @@ def get_projects(self) -> List[str]:

def simulate(self, op, **kwargs) -> Any:
kwargs.setdefault('_timeout_', None)
return self._execute_callable_magic('simulate', op, **kwargs)
return self._execute_callable_magic('simulate', op, _fail_as_exceptions_=True, **kwargs)

def toffoli_simulate(self, op, **kwargs) -> Any:
kwargs.setdefault('_timeout_', None)
return self._execute_callable_magic('toffoli', op, **kwargs)
return self._execute_callable_magic('toffoli', op, _fail_as_exceptions_=True, **kwargs)

def estimate(self, op, **kwargs) -> Dict[str, int]:
kwargs.setdefault('_timeout_', None)
raw_counts = self._execute_callable_magic('estimate', op, **kwargs)
raw_counts = self._execute_callable_magic('estimate', op, _fail_as_exceptions_=True, **kwargs)
# Note that raw_counts will have the form:
# [
# {"Metric": "<name>", "Sum": "<value>"},
@@ -214,12 +224,7 @@ def capture(msg):
def capture_diagnostics(self, passthrough: bool) -> List[Any]:
captured_data = []
def callback(msg):
msg_data = (
# Check both the old and new MIME types used by the IQ#
# kernel.
json.loads(msg['content']['data'].get('application/json', "null")) or
json.loads(msg['content']['data'].get('application/x-qsharp-data', "null"))
)
msg_data = _extract_data_from(msg)
if msg_data is not None:
captured_data.append(msg_data)
return passthrough
@@ -242,7 +247,7 @@ def callback(msg):

def _simulate_noise(self, op, **kwargs) -> Any:
kwargs.setdefault('_timeout_', None)
return self._execute_callable_magic('experimental.simulate_noise', op, **kwargs)
return self._execute_callable_magic('experimental.simulate_noise', op, _fail_as_exceptions_=True, **kwargs)

def _get_noise_model(self) -> str:
return self._execute(f'%experimental.noise_model')
@@ -273,11 +278,14 @@ def _get_qsharp_data(message_content):
return message_content["data"]["application/json"]
return None

def _execute_magic(self, magic : str, raise_on_stderr : bool = False, _quiet_ : bool = False, **kwargs) -> Any:
def _execute_magic(self, magic : str, raise_on_stderr : bool = False, _quiet_ : bool = False, _fail_as_exceptions_ : bool = False, **kwargs) -> Any:
_timeout_ = kwargs.pop('_timeout_', DEFAULT_TIMEOUT)
return self._execute(
f'%{magic} {json.dumps(map_tuples(kwargs))}',
raise_on_stderr=raise_on_stderr, _quiet_=_quiet_, _timeout_=_timeout_
raise_on_stderr=raise_on_stderr,
_quiet_=_quiet_,
_timeout_=_timeout_,
_fail_as_exceptions_=_fail_as_exceptions_
)

def _execute_callable_magic(self, magic : str, op,
@@ -306,7 +314,16 @@ def _handle_message(self, msg, handlers=None, error_callback=None, fallback_hook
else:
fallback_hook(msg)

def _execute(self, input, return_full_result=False, raise_on_stderr : bool = False, output_hook=None, display_data_handler=None, _timeout_=DEFAULT_TIMEOUT, _quiet_ : bool = False, **kwargs):
def _execute(self, input,
return_full_result=False,
raise_on_stderr : bool = False,
output_hook=None,
display_data_handler=None,
_timeout_=DEFAULT_TIMEOUT,
_quiet_ : bool = False,
_fail_as_exceptions_ : bool = False,
**kwargs):

logger.debug(f"sending:\n{input}")
logger.debug(f"timeout: {_timeout_}")

@@ -337,7 +354,7 @@ def log_error(msg):
lambda msg: display_raw(msg['content']['data'])
)

# Finish setting up handlers by allowing the display_data_callback
# Continue setting up handlers by allowing the display_data_callback
# to intercept display data first, only sending messages through to
# other handlers if it returns True.
if self.display_data_callback is not None:
@@ -349,6 +366,32 @@ def filter_display_data(msg):

handlers['display_data'] = filter_display_data

# Finally, we want to make sure that if we're handling Q# failures by
# converting them to Python exceptions, we set up that handler last
# so that it has highest priority.
if _fail_as_exceptions_:
success_handler = handlers['display_data']

def convert_exceptions(msg):
msg_data = _extract_data_from(msg)
# Check if the message data looks like a C# exception
# serialized into JSON.
if (
isinstance(msg_data, dict) and len(msg_data) == 3 and
all(field in msg_data for field in ('Exception', 'StackTrace', 'Header'))
):
raise ExecutionFailedException(
msg_data['Exception']['Message'],
[
frame.strip()
for frame in
msg_data['Exception']['StackTrace'].split('\n')
],
)
return success_handler(msg)

handlers['display_data'] = convert_exceptions

_output_hook = partial(
self._handle_message,
error_callback=log_error if raise_on_stderr else None,
@@ -388,3 +431,13 @@ def filter_display_data(msg):
return (obj, content) if return_full_result else obj
else:
return None

## UTILITY FUNCTIONS ##

def _extract_data_from(msg):
return (
# Check both the old and new MIME types used by the IQ#
# kernel.
json.loads(msg['content']['data'].get('application/json', "null")) or
json.loads(msg['content']['data'].get('application/x-qsharp-data', "null"))
)
9 changes: 7 additions & 2 deletions src/Python/qsharp-core/qsharp/tests/Operations.qs
Original file line number Diff line number Diff line change
@@ -10,8 +10,7 @@ namespace Microsoft.Quantum.SanityTests {

/// # Summary
/// The simplest program. Just generate a debug Message on the console.
operation HelloQ() : Unit
{
operation HelloQ() : Unit {
Message($"Hello from quantum world!");
}

@@ -98,4 +97,10 @@ namespace Microsoft.Quantum.SanityTests {
}
return -1;
}

operation MeasureOne() : Result {
use q = Qubit();
X(q);
return MResetZ(q);
}
}
38 changes: 21 additions & 17 deletions src/Python/qsharp-core/qsharp/tests/test_iqsharp.py
Original file line number Diff line number Diff line change
@@ -62,19 +62,26 @@ def test_simulate():
assert HelloAgain(
count=1, name="Ada") == HelloAgain.simulate(count=1, name="Ada")


def test_toffoli_simulate():
foo = qsharp.compile("""
open Microsoft.Quantum.Measurement;

operation Foo() : Result {
using (q = Qubit()) {
X(q);
return MResetZ(q);
}
def test_failing_simulate():
"""
Checks that fail statements in Q# operations are translated into Python
exceptions.
"""
print(qsharp)
fails = qsharp.compile("""
function Fails() : Unit {
fail "Failure message.";
}
""")
assert foo.toffoli_simulate() == 1
with pytest.raises(qsharp.ExecutionFailedException) as exc_info:
fails()
assert exc_info.type is qsharp.ExecutionFailedException
assert exc_info.value.args[0].split('\n')[0] == "Q# execution failed: Failure message."

@skip_if_no_workspace
def test_toffoli_simulate():
from Microsoft.Quantum.SanityTests import MeasureOne
assert MeasureOne.toffoli_simulate() == 1

@skip_if_no_workspace
def test_tuples():
@@ -192,8 +199,7 @@ def test_simple_compile():
Verifies that compile works
"""
op = qsharp.compile( """
operation HelloQ() : Result
{
operation HelloQ() : Result {
Message($"Hello from quantum world!");
return Zero;
}
@@ -208,14 +214,12 @@ def test_multi_compile():
are returned in the correct order
"""
ops = qsharp.compile( """
operation HelloQ() : Result
{
operation HelloQ() : Result {
Message($"Hello from quantum world!");
return One;
}

operation Hello2() : Result
{
operation Hello2() : Result {
Message($"Will call hello.");
return HelloQ();
}