diff --git a/src/Jupyter/Visualization/DisplayableHtmlElementEncoder.cs b/src/Jupyter/Visualization/DisplayableHtmlElementEncoder.cs
new file mode 100644
index 0000000000..91a7034ad3
--- /dev/null
+++ b/src/Jupyter/Visualization/DisplayableHtmlElementEncoder.cs
@@ -0,0 +1,42 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+#nullable enable
+
+using Microsoft.Jupyter.Core;
+
+namespace Microsoft.Quantum.IQSharp.Jupyter
+{
+ ///
+ /// Represents an HTML string to be rendered as an HTML element.
+ ///
+ public class DisplayableHtmlElement
+ {
+ ///
+ /// Initializes with the given HTML string.
+ ///
+ public DisplayableHtmlElement(string html) => this.Html = html;
+
+ ///
+ /// HTML string to be rendered.
+ ///
+ public string Html { get; }
+ }
+
+ ///
+ /// Encodes instances as HTML elements.
+ ///
+ public class DisplayableHtmlElementEncoder : IResultEncoder
+ {
+ ///
+ public string MimeType => MimeTypes.Html;
+
+ ///
+ /// Checks if a given display object is an ,
+ /// and if so, returns its HTML element.
+ ///
+ public EncodedData? Encode(object displayable) =>
+ (displayable is DisplayableHtmlElement dis)
+ ? dis.Html.ToEncodedData() as EncodedData?
+ : null;
+ }
+}
diff --git a/src/Kernel/IQSharpEngine.cs b/src/Kernel/IQSharpEngine.cs
index a79b3d1894..54f27b2870 100644
--- a/src/Kernel/IQSharpEngine.cs
+++ b/src/Kernel/IQSharpEngine.cs
@@ -63,6 +63,7 @@ IReferences references
RegisterDisplayEncoder(new DataTableToTextEncoder());
RegisterDisplayEncoder(new DisplayableExceptionToHtmlEncoder());
RegisterDisplayEncoder(new DisplayableExceptionToTextEncoder());
+ RegisterDisplayEncoder(new DisplayableHtmlElementEncoder());
RegisterJsonEncoder(
JsonConverters.AllConverters
diff --git a/src/Kernel/Magic/TraceMagic.cs b/src/Kernel/Magic/TraceMagic.cs
new file mode 100644
index 0000000000..54cf44477c
--- /dev/null
+++ b/src/Kernel/Magic/TraceMagic.cs
@@ -0,0 +1,180 @@
+// Copyright (c) Microsoft Corporation. All rights reserved.
+// Licensed under the MIT License.
+
+using System;
+using System.Threading.Tasks;
+using Microsoft.Jupyter.Core;
+using Microsoft.Jupyter.Core.Protocol;
+using Microsoft.Quantum.IQSharp.Core.ExecutionPathTracer;
+using Microsoft.Quantum.IQSharp.Jupyter;
+using Microsoft.Quantum.Simulation.Simulators;
+using Newtonsoft.Json;
+using Newtonsoft.Json.Linq;
+
+namespace Microsoft.Quantum.IQSharp.Kernel
+{
+ ///
+ /// Contains the JSON representation of the
+ /// and the ID of the HTML div that will contain the visualization of the
+ /// execution path.
+ ///
+ public class ExecutionPathVisualizerContent : MessageContent
+ {
+ ///
+ /// Initializes with the
+ /// given (as a ) and HTML div ID.
+ ///
+ public ExecutionPathVisualizerContent(JToken executionPath, string id)
+ {
+ this.ExecutionPath = executionPath;
+ this.Id = id;
+ }
+
+ ///
+ /// The (as a ) to be rendered.
+ ///
+ [JsonProperty("executionPath")]
+ public JToken ExecutionPath { get; }
+
+ ///
+ /// ID of the HTML div that will contain the visualization.
+ ///
+ [JsonProperty("id")]
+ public string Id { get; }
+ }
+
+ ///
+ /// A magic command that can be used to visualize the execution
+ /// path of operations and functions traced out by the simulator.
+ ///
+ public class TraceMagic : AbstractMagic
+ {
+ private const string ParameterNameOperationName = "__operationName__";
+ private const string ParameterNameDepth = "depth";
+
+ ///
+ /// Constructs a new magic command given a resolver used to find
+ /// operations and functions, and a configuration source used to set
+ /// configuration options.
+ ///
+ public TraceMagic(ISymbolResolver resolver, IConfigurationSource configurationSource) : base(
+ "trace",
+ new Documentation
+ {
+ Summary = "Outputs the HTML-based visualization of an execution path of the given operation.",
+ Description = $@"
+ This magic command renders an HTML-based visualization of a runtime execution path of the
+ given operation using the QuantumSimulator.
+
+ #### Required parameters
+
+ - Q# operation or function name. This must be the first parameter, and must be a valid Q# operation
+ or function name that has been defined either in the notebook or in a Q# file in the same folder.
+ - Arguments for the Q# operation or function must also be specified as `key=value` pairs.
+
+ #### Optional parameters
+
+ - `{ParameterNameDepth}=` (default=1): The depth at which to render operations along
+ the execution path.
+ ".Dedent(),
+ Examples = new []
+ {
+ @"
+ Visualize the execution path of a Q# operation defined as `operation MyOperation() : Result`:
+ ```
+ In []: %trace MyOperation
+ Out[]:
+ ```
+ ".Dedent(),
+ @"
+ Visualize the execution path of a Q# operation defined as `operation MyOperation(a : Int, b : Int) : Result`:
+ ```
+ In []: %trace MyOperation a=5 b=10
+ Out[]:
+ ```
+ ".Dedent(),
+ $@"
+ Visualize operations at depth 2 on the execution path of a Q# operation defined
+ as `operation MyOperation() : Result`:
+ ```
+ In []: %trace MyOperation {ParameterNameDepth}=2
+ Out[]:
+ ```
+ ".Dedent(),
+ }
+ })
+ {
+ this.SymbolResolver = resolver;
+ this.ConfigurationSource = configurationSource;
+ }
+
+ ///
+ /// The symbol resolver used by this magic command to find the operation
+ /// to be visualized.
+ ///
+ public ISymbolResolver SymbolResolver { get; }
+
+ ///
+ /// The configuration source used by this magic command to control
+ /// visualization options.
+ ///
+ public IConfigurationSource ConfigurationSource { get; }
+
+ ///
+ public override ExecutionResult Run(string input, IChannel channel) =>
+ RunAsync(input, channel).Result;
+
+ ///
+ /// Outputs a visualization of a runtime execution path of an operation given
+ /// a string with its name and a JSON encoding of its arguments.
+ ///
+ public async Task RunAsync(string input, IChannel channel)
+ {
+ // Parse input parameters
+ var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName);
+
+ var name = inputParameters.DecodeParameter(ParameterNameOperationName);
+ var symbol = SymbolResolver.Resolve(name) as IQSharpSymbol;
+ if (symbol == null) throw new InvalidOperationException($"Invalid operation name: {name}");
+
+ var depth = inputParameters.DecodeParameter(ParameterNameDepth, defaultValue: 1);
+ if (depth <= 0) throw new ArgumentOutOfRangeException($"Invalid depth: {depth}. Must be >= 1.");
+
+ var tracer = new ExecutionPathTracer(depth);
+
+ // Simulate operation and attach `ExecutionPathTracer` to trace out operations performed
+ // in its execution path
+ using var qsim = new QuantumSimulator()
+ .WithJupyterDisplay(channel, ConfigurationSource)
+ .WithStackTraceDisplay(channel)
+ .WithExecutionPathTracer(tracer);
+ var value = await symbol.Operation.RunAsync(qsim, inputParameters);
+
+ // Retrieve the `ExecutionPath` traced out by the `ExecutionPathTracer`
+ var executionPath = tracer.GetExecutionPath();
+
+ // Convert executionPath to JToken for serialization
+ var executionPathJToken = JToken.FromObject(executionPath,
+ new JsonSerializer() { NullValueHandling = NullValueHandling.Ignore });
+
+ // Render empty div with unique ID as cell output
+ var divId = $"execution-path-container-{Guid.NewGuid().ToString()}";
+ var content = new ExecutionPathVisualizerContent(executionPathJToken, divId);
+ channel.DisplayUpdatable(new DisplayableHtmlElement($""));
+
+ // Send execution path to JavaScript via iopub for rendering
+ channel.SendIoPubMessage(
+ new Message
+ {
+ Header = new MessageHeader
+ {
+ MessageType = "render_execution_path"
+ },
+ Content = content,
+ }
+ );
+
+ return ExecuteStatus.Ok.ToExecutionResult();
+ }
+ }
+}
diff --git a/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts b/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts
index d110f6b716..6e8315a461 100644
--- a/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts
+++ b/src/Kernel/client/ExecutionPathVisualizer/pathVisualizer.ts
@@ -100,7 +100,7 @@ const style = `
*
* @returns HTML representation of circuit.
*/
-const jsonToHtml = (json: ExecutionPath): string => {
+const _jsonToHtml = (json: ExecutionPath): string => {
const { qubits, operations } = json;
const { qubitWires, registers, svgHeight } = formatInputs(qubits);
const { metadataList, svgWidth } = processOperations(operations, registers);
@@ -118,4 +118,18 @@ const jsonToHtml = (json: ExecutionPath): string => {
`;
};
-export default jsonToHtml;
+/**
+ * Renders the given `ExecutionPath` json into an HTML element and populates the div
+ * with the given `id` with it.
+ *
+ * @param json JSON received from simulator.
+ * @param id ID of div to populate.
+ */
+const renderExecutionPath = (json: ExecutionPath, id: string): void => {
+ const html: string = _jsonToHtml(json);
+ const container: HTMLElement = document.getElementById(id);
+ if (container == null) throw new Error(`Div with ID ${id} not found.`);
+ container.innerHTML = html;
+};
+
+export default renderExecutionPath;
diff --git a/src/Kernel/client/kernel.ts b/src/Kernel/client/kernel.ts
index 675387474e..c9d1b9dd38 100644
--- a/src/Kernel/client/kernel.ts
+++ b/src/Kernel/client/kernel.ts
@@ -8,7 +8,7 @@ import { IPython } from "./ipython";
declare var IPython: IPython;
import { Telemetry, ClientInfo } from "./telemetry.js";
-import jsonToHtml from "./ExecutionPathVisualizer/pathVisualizer.js";
+import renderExecutionPath from "./ExecutionPathVisualizer/pathVisualizer.js";
function defineQSharpMode() {
console.log("Loading IQ# kernel-specific extension...");
@@ -125,6 +125,7 @@ class Kernel {
IPython.notebook.kernel.events.on("kernel_ready.Kernel", args => {
this.requestEcho();
this.requestClientInfo();
+ this.initExecutionPathVisualizer();
});
}
@@ -225,6 +226,16 @@ class Kernel {
});
Telemetry.initAsync();
}
+
+ initExecutionPathVisualizer() {
+ IPython.notebook.kernel.register_iopub_handler(
+ "render_execution_path",
+ message => {
+ const { executionPath, id } = message.content;
+ renderExecutionPath(executionPath, id);
+ }
+ );
+ }
}
export function onload() {
@@ -232,4 +243,3 @@ export function onload() {
let kernel = new Kernel();
console.log("Loaded IQ# kernel-specific extension!");
}
-
diff --git a/src/Tests/ExecutionPathTracerTests.cs b/src/Tests/ExecutionPathTracerTests.cs
index fea5a8c3c4..17fa0e084f 100644
--- a/src/Tests/ExecutionPathTracerTests.cs
+++ b/src/Tests/ExecutionPathTracerTests.cs
@@ -34,7 +34,6 @@ public ExecutionPath GetExecutionPath(string name, int depth = 1)
return tracer.GetExecutionPath();
}
-
[TestMethod]
public void HTest()
{
@@ -50,7 +49,6 @@ public void HTest()
Gate = "H",
Targets = new List() { new QubitRegister(0) },
},
- // TODO: Remove Reset/ResetAll gates once we don't need to zero out qubits
new Operation()
{
Gate = "Reset",
@@ -360,10 +358,7 @@ public void UnusedQubitTest()
public void Depth2Test()
{
var path = GetExecutionPath("Depth2Circ", 2);
- var qubits = new QubitDeclaration[]
- {
- new QubitDeclaration(0, 1),
- };
+ var qubits = new QubitDeclaration[] { new QubitDeclaration(0) };
var operations = new Operation[]
{
new Operation()
@@ -381,12 +376,6 @@ public void Depth2Test()
Gate = "H",
Targets = new List() { new QubitRegister(0) },
},
- new Operation()
- {
- Gate = "measure",
- Controls = new List() { new QubitRegister(0) },
- Targets = new List() { new ClassicalRegister(0, 0) },
- },
};
var expected = new ExecutionPath(qubits, operations);
Assert.AreEqual(expected.ToJson(), path.ToJson());
@@ -440,6 +429,32 @@ public void EmptyTest()
var expected = new ExecutionPath(qubits, operations);
Assert.AreEqual(expected.ToJson(), path.ToJson());
}
+
+ [TestMethod]
+ public void NestedTest()
+ {
+ var path = GetExecutionPath("NestedCirc");
+ var qubits = new QubitDeclaration[] { new QubitDeclaration(0) };
+ var operations = new Operation[]
+ {
+ new Operation()
+ {
+ Gate = "H",
+ Targets = new List() { new QubitRegister(0) },
+ },
+ new Operation()
+ {
+ Gate = "HCirc",
+ },
+ new Operation()
+ {
+ Gate = "Reset",
+ Targets = new List() { new QubitRegister(0) },
+ },
+ };
+ var expected = new ExecutionPath(qubits, operations);
+ Assert.AreEqual(expected.ToJson(), path.ToJson());
+ }
[TestMethod]
public void BigTest()
diff --git a/src/Tests/IQsharpEngineTests.cs b/src/Tests/IQsharpEngineTests.cs
index 2d800b3bb9..54df19f1e1 100644
--- a/src/Tests/IQsharpEngineTests.cs
+++ b/src/Tests/IQsharpEngineTests.cs
@@ -10,12 +10,11 @@
using Microsoft.Quantum.IQSharp;
using Microsoft.Quantum.IQSharp.Jupyter;
using Microsoft.Quantum.IQSharp.Kernel;
-using Microsoft.Quantum.Simulation.Core;
+using Microsoft.Quantum.IQSharp.Core.ExecutionPathTracer;
using Microsoft.VisualStudio.TestTools.UnitTesting;
using Newtonsoft.Json;
using System.Data;
-using Microsoft.Quantum.IQSharp.AzureClient;
#pragma warning disable VSTHRD200 // Use "Async" suffix for async methods
@@ -86,6 +85,28 @@ public static async Task AssertEstimate(IQSharpEngine engine, string sni
return response.Output?.ToString();
}
+ private async Task AssertTrace(string name, ExecutionPath expectedPath)
+ {
+ var engine = Init("Workspace.ExecutionPathTracer");
+ var configSource = new ConfigurationSource(skipLoading: true);
+ var traceMagic = new TraceMagic(engine.SymbolsResolver, configSource);
+ var channel = new MockChannel();
+
+ var response = await traceMagic.Execute(name, channel);
+ Assert.AreEqual(ExecuteStatus.Ok, response.Status);
+
+ var message = channel.iopubMessages.ElementAtOrDefault(0);
+ Assert.IsNotNull(message);
+ Assert.AreEqual("render_execution_path", message.Header.MessageType);
+
+ var content = message.Content as ExecutionPathVisualizerContent;
+ Assert.IsNotNull(content);
+
+ var path = content.ExecutionPath.ToObject();
+ Assert.IsNotNull(path);
+ Assert.AreEqual(expectedPath.ToJson(), path.ToJson());
+ }
+
[TestMethod]
public async Task CompileOne()
{
@@ -458,6 +479,63 @@ public void TestResolveMagic()
Assert.IsNotNull(resolver.Resolve("%azure.output"));
Assert.IsNotNull(resolver.Resolve("%azure.jobs"));
}
+
+ [TestMethod]
+ public async Task TestTraceMagic()
+ {
+ await AssertTrace("HCirc", new ExecutionPath(
+ new QubitDeclaration[] { new QubitDeclaration(0) },
+ new Operation[]
+ {
+ new Operation()
+ {
+ Gate = "H",
+ Targets = new List() { new QubitRegister(0) },
+ },
+ new Operation()
+ {
+ Gate = "Reset",
+ Targets = new List() { new QubitRegister(0) },
+ },
+ }
+ ));
+
+ // Should only see depth-1 operations
+ await AssertTrace("Depth2Circ", new ExecutionPath(
+ new QubitDeclaration[] { new QubitDeclaration(0) },
+ new Operation[]
+ {
+ new Operation()
+ {
+ Gate = "FooBar",
+ Targets = new List() { new QubitRegister(0) },
+ },
+ }
+ ));
+
+ // Should see depth-2 operations
+ await AssertTrace("Depth2Circ depth=2", new ExecutionPath(
+ new QubitDeclaration[] { new QubitDeclaration(0) },
+ new Operation[]
+ {
+ new Operation()
+ {
+ Gate = "H",
+ Targets = new List() { new QubitRegister(0) },
+ },
+ new Operation()
+ {
+ Gate = "X",
+ Targets = new List() { new QubitRegister(0) },
+ },
+ new Operation()
+ {
+ Gate = "H",
+ Targets = new List() { new QubitRegister(0) },
+ },
+ }
+ ));
+ }
}
}
#pragma warning restore VSTHRD200 // Use "Async" suffix for async methods
diff --git a/src/Tests/Mocks.cs b/src/Tests/Mocks.cs
index 84d06abe64..87930ae423 100644
--- a/src/Tests/Mocks.cs
+++ b/src/Tests/Mocks.cs
@@ -102,6 +102,7 @@ public class MockChannel : IChannel
{
public List errors = new List();
public List msgs = new List();
+ public List iopubMessages = new List();
public void Display(object displayable)
{
@@ -113,6 +114,8 @@ public IUpdatableDisplay DisplayUpdatable(object displayable)
return new MockUpdatableDisplay();
}
+ public void SendIoPubMessage(Message message) => iopubMessages.Add(message);
+
public void Stderr(string message) => errors.Add(message);
public void Stdout(string message) => msgs.Add(message);
diff --git a/src/Tests/Workspace.ExecutionPathTracer/Intrinsic.qs b/src/Tests/Workspace.ExecutionPathTracer/Intrinsic.qs
index 3811173584..2796648a12 100644
--- a/src/Tests/Workspace.ExecutionPathTracer/Intrinsic.qs
+++ b/src/Tests/Workspace.ExecutionPathTracer/Intrinsic.qs
@@ -96,6 +96,14 @@ namespace Tests.ExecutionPathTracer {
}
}
+ operation NestedCirc() : Unit {
+ using (q = Qubit()) {
+ H(q);
+ HCirc();
+ Reset(q);
+ }
+ }
+
operation FooBar(q : Qubit) : Unit {
H(q);
X(q);
@@ -105,7 +113,6 @@ namespace Tests.ExecutionPathTracer {
operation Depth2Circ() : Unit {
using (q = Qubit()) {
FooBar(q);
- Reset(q);
}
}