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); } }