From 89282dff3599b744e13c6d97e97d48fb11be56b4 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 28 May 2020 18:03:10 -0400 Subject: [PATCH 01/28] Updates to IQ# syntax highlighting --- src/Kernel/client/kernel.ts | 140 ++++++++++++++++++++++++------------ 1 file changed, 96 insertions(+), 44 deletions(-) diff --git a/src/Kernel/client/kernel.ts b/src/Kernel/client/kernel.ts index bb001eb7a8..d239a5af47 100644 --- a/src/Kernel/client/kernel.ts +++ b/src/Kernel/client/kernel.ts @@ -11,54 +11,106 @@ import { Telemetry, ClientInfo } from "./telemetry.js"; function defineQSharpMode() { console.log("Loading IQ# kernel-specific extension..."); + + let rules = [ + { + token: "comment", + regex: /(\/\/).*/, + beginWord: false, + }, + { + token: "string", + regex: String.raw`^\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)`, + beginWord: false, + }, + { + token: "keyword", + regex: String.raw`(namespace|open|as|operation|function|body|adjoint|newtype|controlled)\b`, + beginWord: true, + }, + { + token: "keyword", + regex: String.raw`(if|elif|else|repeat|until|fixup|for|in|return|fail|within|apply)\b`, + beginWord: true, + }, + { + token: "keyword", + regex: String.raw`(Adjoint|Controlled|Adj|Ctl|is|self|auto|distribute|invert|intrinsic)\b`, + beginWord: true, + }, + { + token: "keyword", + regex: String.raw`(let|set|w\/|new|not|and|or|using|borrowing|newtype|mutable)\b`, + beginWord: true, + }, + { + token: "meta", + regex: String.raw`(Int|BigInt|Double|Bool|Qubit|Pauli|Result|Range|String|Unit)\b`, + beginWord: true, + }, + { + token: "atom", + regex: String.raw`(true|false|Pauli(I|X|Y|Z)|One|Zero)\b`, + beginWord: true, + }, + { + token: "builtin", + regex: String.raw`(X|Y|Z|H|HY|S|T|SWAP|CNOT|CCNOT|MultiX|R|RFrac|Rx|Ry|Rz|R1|R1Frac|Exp|ExpFrac|Measure|M|MultiM)\b`, + beginWord: true, + }, + { + token: "builtin", + regex: String.raw`(Message|Length|Assert|AssertProb|AssertEqual)\b`, + beginWord: true, + }, + { + // built-in magic commands + token: "builtin", + regex: String.raw`(%(config|estimate|lsmagic|package|performance|simulate|toffoli|version|who|workspace))\b`, + beginWord: true, + }, + { + // Azure magic commands + token: "builtin", + regex: String.raw`(%azure\.(connect|execute|jobs|output|status|submit|target))\b`, + beginWord: true, + }, + { + // chemistry magic commands + token: "builtin", + regex: String.raw`(%chemistry\.(broombridge|encode|fh\.add_terms|fh\.load|inputstate\.load))\b`, + beginWord: true, + }, + { + // katas magic commands + token: "builtin", + regex: String.raw`(%(?:check_kata|kata))\b`, + beginWord: true, + }, + ]; + + let simpleRules = [] + for (let rule of rules) { + simpleRules.push({ + "token": rule.token, + "regex": new RegExp(rule.regex, "g"), + "sol": rule.beginWord + }); + if (rule.beginWord) { + // Need an additional rule due to the fact that CodeMirror simple mode doesn't work with ^ token + simpleRules.push({ + "token": rule.token, + "regex": new RegExp(String.raw`\W` + rule.regex, "g"), + "sol": false + }); + } + } + // NB: The TypeScript definitions for CodeMirror don't currently understand // the simple mode plugin. let codeMirror: any = window.CodeMirror; codeMirror.defineSimpleMode('qsharp', { - start: [ - { - token: "comment", - // include % to support kata special commands - regex: /(\/\/|%kata|%version|%simulate|%package|%workspace|%check_kata).*/ - }, - { - token: "string", - regex: /^\"(?:[^\"\\]|\\[\s\S])*(?:\"|$)/ - }, - { - // a group of keywords that can typically occur in the beginning of the line but not in the end of a phrase - token: "keyword", - regex: /(^|\W)(?:namespace|open|as|operation|function|body|adjoint|newtype|controlled)\b/ - }, - { - token: "keyword", - regex: /\W(?:if|elif|else|repeat|until|fixup|for|in|return|fail|within|apply)\b/ - }, - { - token: "keyword", - regex: /\W(?:Adjoint|Controlled|Adj|Ctl|is|self|auto|distribute|invert|intrinsic)\b/ - }, - { - token: "keyword", - regex: /\W(?:let|set|w\/|new|not|and|or|using|borrowing|newtype|mutable)\b/ - }, - { - token: "meta", - regex: /[^\w(\s]*(?:Int|BigInt|Double|Bool|Qubit|Pauli|Result|Range|String|Unit)\b/ - }, - { - token: "atom", - regex: /\W(?:true|false|Pauli(I|X|Y|Z)|One|Zero)\b/ - }, - { - token: "builtin", - regex: /(\\n|\W)(?:X|Y|Z|H|HY|S|T|SWAP|CNOT|CCNOT|MultiX|R|RFrac|Rx|Ry|Rz|R1|R1Frac|Exp|ExpFrac|Measure|M|MultiM)\b/ - }, - { - token: "builtin", - regex: /(\\n|\W)(?:Message|Length|Assert|AssertProb|AssertEqual)\b/ - } - ] + start: simpleRules }); codeMirror.defineMIME("text/x-qsharp", "qsharp"); } From f53eb9cbb5a1b8af6b6542e1c7543f13aaa195af Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 28 May 2020 18:05:22 -0400 Subject: [PATCH 02/28] Validate targets and load provider packages --- src/AzureClient/AzureClient.cs | 110 ++++++++++++++++++------ src/AzureClient/AzureEnvironment.cs | 1 - src/AzureClient/AzureExecutionTarget.cs | 43 +++++++++ src/AzureClient/Extensions.cs | 5 +- src/AzureClient/IAzureClient.cs | 21 +++-- src/AzureClient/Magic/TargetMagic.cs | 14 +-- src/AzureClient/Resources.cs | 3 + src/Tests/AzureClientMagicTests.cs | 11 ++- src/Tests/AzureClientTests.cs | 20 +++-- src/Tool/appsettings.json | 6 +- 10 files changed, 185 insertions(+), 49 deletions(-) create mode 100644 src/AzureClient/AzureExecutionTarget.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index c581d8e086..600080e5b9 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -17,6 +17,8 @@ using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Azure.Quantum.Storage; using Microsoft.Azure.Quantum; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -24,12 +26,21 @@ namespace Microsoft.Quantum.IQSharp.AzureClient public class AzureClient : IAzureClient { private string ConnectionString { get; set; } = string.Empty; - private string ActiveTargetName { get; set; } = string.Empty; + private AzureExecutionTarget? ActiveTarget { get; set; } private AuthenticationResult? AuthenticationResult { get; set; } private IQuantumClient? QuantumClient { get; set; } - private IPage? ProviderStatusList { get; set; } private Azure.Quantum.IWorkspace? ActiveWorkspace { get; set; } private string MostRecentJobId { get; set; } = string.Empty; + private IPage? AvailableProviders { get; set; } + private IEnumerable? AvailableTargets { get => AvailableProviders?.SelectMany(provider => provider.Targets); } + private IEnumerable? ValidExecutionTargets { get => AvailableTargets?.Where(target => AzureExecutionTarget.IsValid(target.Id)); } + private string ValidExecutionTargetsDisplayText + { + get => ValidExecutionTargets == null + ? "(no execution targets available)" + : string.Join(", ", ValidExecutionTargets.Select(target => target.Id)); + } + /// public async Task ConnectAsync( @@ -112,7 +123,7 @@ public async Task ConnectAsync( try { - ProviderStatusList = await QuantumClient.Providers.GetStatusAsync(); + AvailableProviders = await QuantumClient.Providers.GetStatusAsync(); } catch (Exception e) { @@ -122,22 +133,22 @@ public async Task ConnectAsync( channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}."); - // TODO: Add encoder for IPage rather than calling ToJupyterTable() here directly. - return ProviderStatusList.ToJupyterTable().ToExecutionResult(); + // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. + return ValidExecutionTargets.ToJupyterTable().ToExecutionResult(); } /// public async Task GetConnectionStatusAsync(IChannel channel) { - if (QuantumClient == null || ProviderStatusList == null) + if (QuantumClient == null || AvailableProviders == null) { return AzureClientError.NotConnected.ToExecutionResult(); } channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}."); - // TODO: Add encoder for IPage rather than calling ToJupyterTable() here directly. - return ProviderStatusList.ToJupyterTable().ToExecutionResult(); + // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. + return ValidExecutionTargets.ToJupyterTable().ToExecutionResult(); } private async Task SubmitOrExecuteJobAsync( @@ -152,7 +163,7 @@ private async Task SubmitOrExecuteJobAsync( return AzureClientError.NotConnected.ToExecutionResult(); } - if (ActiveTargetName == null) + if (ActiveTarget == null) { channel.Stderr("Please call %azure.target before submitting a job."); return AzureClientError.NoTarget.ToExecutionResult(); @@ -165,11 +176,12 @@ private async Task SubmitOrExecuteJobAsync( return AzureClientError.NoOperationName.ToExecutionResult(); } - var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTargetName, ConnectionString); + var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetName, ConnectionString); if (machine == null) { - channel.Stderr($"Could not find an execution target for target {ActiveTargetName}."); - return AzureClientError.NoTarget.ToExecutionResult(); + // We should never get here, since ActiveTarget should have already been validated at the time it was set. + channel.Stderr($"Unexpected error while preparing job for execution on target {ActiveTarget.TargetName}."); + return AzureClientError.InvalidTarget.ToExecutionResult(); } var operationInfo = operationResolver.Resolve(operationName); @@ -178,15 +190,22 @@ private async Task SubmitOrExecuteJobAsync( if (execute) { + channel.Stdout($"Executing {operationName} on target {ActiveTarget.TargetName}..."); var output = await machine.ExecuteAsync(entryPointInfo, entryPointInput); MostRecentJobId = output.Job.Id; - // TODO: Add encoder for IQuantumMachineOutput rather than returning the Histogram directly + + // TODO: Add encoder to visualize IEnumerable> return output.Histogram.ToExecutionResult(); } else { + channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); var job = await machine.SubmitAsync(entryPointInfo, entryPointInput); MostRecentJobId = job.Id; + channel.Stdout("Job submission successful."); + channel.Stdout($"To check the status, run:\n %azure.status {MostRecentJobId}"); + channel.Stdout($"To see the results, run:\n %azure.output {MostRecentJobId}"); + // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here. return job.ToJupyterTable().ToExecutionResult(); } @@ -210,19 +229,57 @@ public async Task ExecuteJobAsync( public async Task GetActiveTargetAsync( IChannel channel) { - // TODO: This should also print the list of available targets to the IChannel. - return ActiveTargetName.ToExecutionResult(); + if (AvailableProviders == null) + { + channel.Stderr("Please call %azure.connect before getting the execution target."); + return AzureClientError.NotConnected.ToExecutionResult(); + } + + if (ActiveTarget == null) + { + channel.Stderr("No execution target has been specified. To specify one, run:\n%azure.target "); + channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); + return AzureClientError.NoTarget.ToExecutionResult(); + } + + channel.Stdout($"Current execution target: {ActiveTarget.TargetName}"); + channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); + return ActiveTarget.TargetName.ToExecutionResult(); } /// public async Task SetActiveTargetAsync( IChannel channel, + IReferences references, string targetName) { - // TODO: Validate that this target name is valid in the workspace. - // TODO: Load the associated provider package. - ActiveTargetName = targetName; - return $"Active target is now {ActiveTargetName}".ToExecutionResult(); + if (AvailableProviders == null) + { + channel.Stderr("Please call %azure.connect before setting an execution target."); + return AzureClientError.NotConnected.ToExecutionResult(); + } + + // Validate that this target name is valid in the workspace. + if (!AvailableTargets.Any(target => targetName == target.Id)) + { + channel.Stderr($"Target name {targetName} is not available in the current Azure Quantum workspace."); + channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); + return AzureClientError.InvalidTarget.ToExecutionResult(); + } + + // Validate that we know which package to load for this target name. + if (!ValidExecutionTargets.Any(target => targetName == target.Id)) + { + channel.Stderr($"Target name {targetName} does not support executing Q# jobs."); + channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); + return AzureClientError.InvalidTarget.ToExecutionResult(); + } + + // Set the active target and load the package. + ActiveTarget = new AzureExecutionTarget(targetName); + await references.AddPackage(ActiveTarget.PackageName); + + return $"Active target is now {ActiveTarget.TargetName}".ToExecutionResult(); } /// @@ -262,13 +319,18 @@ public async Task GetJobResultAsync( } var stream = new MemoryStream(); - var protocol = await new JobStorageHelper(ConnectionString).DownloadJobOutputAsync(jobId, stream); + await new JobStorageHelper(ConnectionString).DownloadJobOutputAsync(jobId, stream); stream.Seek(0, SeekOrigin.Begin); - var outputJson = new StreamReader(stream).ReadToEnd(); + var output = new StreamReader(stream).ReadToEnd(); + var deserializedOutput = JsonConvert.DeserializeObject>(output); + var histogram = new Dictionary(); + foreach (var entry in deserializedOutput["histogram"] as JObject) + { + histogram[entry.Key] = entry.Value.ToObject(); + } - // TODO: Deserialize this once we have a way of getting the output type - // TODO: Add encoder for job output - return outputJson.ToExecutionResult(); + // TODO: Add encoder to visualize IEnumerable> + return histogram.ToExecutionResult(); } /// diff --git a/src/AzureClient/AzureEnvironment.cs b/src/AzureClient/AzureEnvironment.cs index d5c6ac2a2c..20db5920ae 100644 --- a/src/AzureClient/AzureEnvironment.cs +++ b/src/AzureClient/AzureEnvironment.cs @@ -3,7 +3,6 @@ #nullable enable -using Microsoft.Quantum.Simulation.Common; using System; using System.Collections.Generic; using System.Linq; diff --git a/src/AzureClient/AzureExecutionTarget.cs b/src/AzureClient/AzureExecutionTarget.cs new file mode 100644 index 0000000000..91de87dcdf --- /dev/null +++ b/src/AzureClient/AzureExecutionTarget.cs @@ -0,0 +1,43 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal enum AzureProvider { IonQ, Honeywell, QCI } + + internal class AzureExecutionTarget + { + public string TargetName { get; private set; } + public AzureProvider? Provider { get => GetProvider(TargetName); } + public string PackageName { get => $"Microsoft.Quantum.Providers.{Provider}"; } + + public static bool IsValid(string targetName) => GetProvider(targetName) != null; + + public AzureExecutionTarget(string targetName) + { + if (!IsValid(targetName)) + { + throw new InvalidOperationException($"{targetName} is not a valid target name."); + } + + TargetName = targetName; + } + + private static AzureProvider? GetProvider(string targetName) + { + var parts = targetName.Split('.', 2); + if (Enum.TryParse(parts[0], true, out AzureProvider provider)) + { + return provider; + } + + return null; + } + } +} diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 7a8e950951..116643480f 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -90,7 +90,6 @@ internal static Table ToJupyterTable(this IQuantumMachineJob { ("JobId", job => job.Id), ("JobStatus", job => job.Status), - ("JobUri", job => job.Uri.ToString()), }, Rows = new List() { job } }; @@ -107,7 +106,7 @@ internal static Table ToJupyterTable(this IQuantumClient quantum Rows = new List() { quantumClient } }; - internal static Table ToJupyterTable(this IEnumerable providerStatusList) => + internal static Table ToJupyterTable(this IEnumerable targets) => new Table { Columns = new List<(string, Func)> @@ -117,7 +116,7 @@ internal static Table ToJupyterTable(this IEnumerable target.AverageQueueTime.ToString()), ("StatusPage", target => target.StatusPage), }, - Rows = providerStatusList.SelectMany(provider => provider.Targets).ToList() + Rows = targets.ToList() }; } } diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 82bb3cb756..69a0177ade 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -18,43 +18,49 @@ public enum AzureClientError /// Method completed with an unknown error. /// [Description(Resources.AzureClientErrorUnknownError)] - UnknownError = 0, + UnknownError, /// /// No connection has been made to any Azure Quantum workspace. /// [Description(Resources.AzureClientErrorNotConnected)] - NotConnected = 1, + NotConnected, /// /// A target has not yet been configured for job submission. /// [Description(Resources.AzureClientErrorNoTarget)] - NoTarget = 2, + NoTarget, + + /// + /// The specified target is not valid for job submission. + /// + [Description(Resources.AzureClientErrorInvalidTarget)] + InvalidTarget, /// /// A job meeting the specified criteria was not found. /// [Description(Resources.AzureClientErrorJobNotFound)] - JobNotFound = 3, + JobNotFound, /// /// No Q# operation name was provided where one was required. /// [Description(Resources.AzureClientErrorNoOperationName)] - NoOperationName = 4, + NoOperationName, /// /// Authentication with the Azure service failed. /// [Description(Resources.AzureClientErrorAuthenticationFailed)] - AuthenticationFailed = 5, + AuthenticationFailed, /// /// A workspace meeting the specified criteria was not found. /// [Description(Resources.AzureClientErrorWorkspaceNotFound)] - WorkspaceNotFound = 6, + WorkspaceNotFound, } /// @@ -118,6 +124,7 @@ public Task ExecuteJobAsync( /// public Task SetActiveTargetAsync( IChannel channel, + IReferences references, string targetName); /// diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index b17eb0ab3e..7e5e2a4151 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -19,13 +19,18 @@ public class TargetMagic : AzureClientMagicBase { private const string ParameterNameTargetName = "name"; + private IReferences? References { get; set; } + /// /// Initializes a new instance of the class. /// /// /// The object to use for Azure functionality. /// - public TargetMagic(IAzureClient azureClient) + /// + /// The object to use for loading target-specific packages. + /// + public TargetMagic(IAzureClient azureClient, IReferences references) : base( azureClient, "azure.target", @@ -57,9 +62,8 @@ available in the workspace. ``` ".Dedent(), }, - }) - { - } + }) => + References = references; /// /// Sets or views the target for job submission to the current Azure Quantum workspace. @@ -70,7 +74,7 @@ public override async Task RunAsync(string input, IChannel chan if (inputParameters.ContainsKey(ParameterNameTargetName)) { string targetName = inputParameters.DecodeParameter(ParameterNameTargetName); - return await AzureClient.SetActiveTargetAsync(channel, targetName); + return await AzureClient.SetActiveTargetAsync(channel, References, targetName); } return await AzureClient.GetActiveTargetAsync(channel); diff --git a/src/AzureClient/Resources.cs b/src/AzureClient/Resources.cs index 6c3de47cc0..06703703d0 100644 --- a/src/AzureClient/Resources.cs +++ b/src/AzureClient/Resources.cs @@ -16,6 +16,9 @@ internal static class Resources public const string AzureClientErrorNoTarget = "No execution target has been configured for Azure Quantum job submission."; + public const string AzureClientErrorInvalidTarget = + "The specified execution target is not valid for Q# job submission in the current Azure Quantum workspace."; + public const string AzureClientErrorJobNotFound = "No job with the given ID was found in the current Azure Quantum workspace."; diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 0eddabee2d..fbbc880253 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -10,6 +10,7 @@ using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp; using Microsoft.Quantum.IQSharp.AzureClient; +using Microsoft.Extensions.DependencyInjection; namespace Tests.IQSharp { @@ -140,15 +141,19 @@ public void TestJobsMagic() [TestMethod] public void TestTargetMagic() { + var workspace = "Workspace"; + var services = Startup.CreateServiceProvider(workspace); + var references = services.GetService(); + // single argument - should set active target var azureClient = new MockAzureClient(); - var targetMagic = new TargetMagic(azureClient); + var targetMagic = new TargetMagic(azureClient, references); targetMagic.Test(targetName); Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); // no arguments - should print active target azureClient = new MockAzureClient(); - targetMagic = new TargetMagic(azureClient); + targetMagic = new TargetMagic(azureClient, references); targetMagic.Test(string.Empty); Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetActiveTarget); } @@ -177,7 +182,7 @@ public class MockAzureClient : IAzureClient internal List SubmittedJobs = new List(); internal List ExecutedJobs = new List(); - public async Task SetActiveTargetAsync(IChannel channel, string targetName) + public async Task SetActiveTargetAsync(IChannel channel, IReferences references, string targetName) { LastAction = AzureClientAction.SetActiveTarget; ActiveTargetName = targetName; diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index c9258af1af..425563f5a4 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -6,7 +6,9 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Linq; using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp; using Microsoft.Quantum.IQSharp.AzureClient; +using Microsoft.Extensions.DependencyInjection; namespace Tests.IQSharp { @@ -23,18 +25,26 @@ public class AzureClientTests private readonly string storageAccountConnectionString = "TEST_CONNECTION_STRING"; private readonly string jobId = "TEST_JOB_ID"; private readonly string operationName = "TEST_OPERATION_NAME"; - private readonly string targetName = "TEST_TARGET_NAME"; [TestMethod] public void TestTargets() { - var azureClient = new AzureClient(); + var workspace = "Workspace"; + var services = Startup.CreateServiceProvider(workspace); + var references = services.GetService(); + var azureClient = services.GetService(); - var result = azureClient.SetActiveTargetAsync(new MockChannel(), targetName).GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == ExecuteStatus.Ok); + // SetActiveTargetAsync with recognized target name, but not yet connected + var result = azureClient.SetActiveTargetAsync(new MockChannel(), references, "ionq.simulator").GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == ExecuteStatus.Error); + // SetActiveTargetAsync with unrecognized target name + result = azureClient.SetActiveTargetAsync(new MockChannel(), references, "contoso.qpu").GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == ExecuteStatus.Error); + + // GetActiveTargetAsync, but not yet connected result = azureClient.GetActiveTargetAsync(new MockChannel()).GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == ExecuteStatus.Ok); + Assert.IsTrue(result.Status == ExecuteStatus.Error); } } } diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index 52d295f3e3..f692e0f440 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -18,6 +18,10 @@ "Microsoft.Quantum.Chemistry.Jupyter::0.11.2005.1924-beta", "Microsoft.Quantum.Numerics::0.11.2005.1924-beta", - "Microsoft.Quantum.Research::0.11.2005.1924-beta" + "Microsoft.Quantum.Research::0.11.2005.1924-beta", + + "Microsoft.Quantum.Providers.IonQ::0.11.2005.1924-beta", + "Microsoft.Quantum.Providers.Honeywell::0.11.2005.1924-beta", + "Microsoft.Quantum.Providers.QCI::0.11.2005.1924-beta", ] } From 75a8e357d71442aceb77da9755652ccb56f2190b Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 29 May 2020 09:36:40 -0400 Subject: [PATCH 03/28] Update Python interface for Azure commands --- src/Python/qsharp/azure.py | 22 +++++++++++++++++----- 1 file changed, 17 insertions(+), 5 deletions(-) diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index 2182a20b30..f69fb83a00 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -29,19 +29,31 @@ 'connect', 'target', 'submit', - 'status' + 'execute', + 'status', + 'output', + 'jobs' ] ## FUNCTIONS ## def connect(**params) -> Any: - return qsharp.client._execute_magic(f"connect", raise_on_stderr=False, **params) + return qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params) def target(name : str = '', **params) -> Any: - return qsharp.client._execute_magic(f"target {name}", raise_on_stderr=False, **params) + return qsharp.client._execute_magic(f"azure.target {name}", raise_on_stderr=False, **params) def submit(op, **params) -> Any: - return qsharp.client._execute_callable_magic("submit", op, raise_on_stderr=False, **params) + return qsharp.client._execute_callable_magic("azure.submit", op, raise_on_stderr=False, **params) + +def execute(op, **params) -> Any: + return qsharp.client._execute_callable_magic("azure.execute", op, raise_on_stderr=False, **params) def status(jobId : str = '', **params) -> Any: - return qsharp.client._execute_magic(f"status {jobId}", raise_on_stderr=False, **params) + return qsharp.client._execute_magic(f"azure.status {jobId}", raise_on_stderr=False, **params) + +def output(jobId : str = '', **params) -> Any: + return qsharp.client._execute_magic(f"azure.output {jobId}", raise_on_stderr=False, **params) + +def jobs(**params) -> Any: + return qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params) From 52098e6412cfc5df8aba9948cdfc8f02d6efba7b Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 1 Jun 2020 10:54:53 -0400 Subject: [PATCH 04/28] Simplify AzureExecutionTarget class --- src/AzureClient/AzureClient.cs | 5 +++-- src/AzureClient/AzureExecutionTarget.cs | 18 +++++------------- 2 files changed, 8 insertions(+), 15 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 600080e5b9..797a953132 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -268,7 +268,8 @@ public async Task SetActiveTargetAsync( } // Validate that we know which package to load for this target name. - if (!ValidExecutionTargets.Any(target => targetName == target.Id)) + var executionTarget = AzureExecutionTarget.Create(targetName); + if (executionTarget == null) { channel.Stderr($"Target name {targetName} does not support executing Q# jobs."); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); @@ -276,7 +277,7 @@ public async Task SetActiveTargetAsync( } // Set the active target and load the package. - ActiveTarget = new AzureExecutionTarget(targetName); + ActiveTarget = executionTarget; await references.AddPackage(ActiveTarget.PackageName); return $"Active target is now {ActiveTarget.TargetName}".ToExecutionResult(); diff --git a/src/AzureClient/AzureExecutionTarget.cs b/src/AzureClient/AzureExecutionTarget.cs index 91de87dcdf..4881049818 100644 --- a/src/AzureClient/AzureExecutionTarget.cs +++ b/src/AzureClient/AzureExecutionTarget.cs @@ -4,8 +4,6 @@ #nullable enable using System; -using System.Collections.Generic; -using System.Text; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -14,20 +12,14 @@ internal enum AzureProvider { IonQ, Honeywell, QCI } internal class AzureExecutionTarget { public string TargetName { get; private set; } - public AzureProvider? Provider { get => GetProvider(TargetName); } - public string PackageName { get => $"Microsoft.Quantum.Providers.{Provider}"; } + public string PackageName { get => $"Microsoft.Quantum.Providers.{GetProvider(TargetName)}"; } public static bool IsValid(string targetName) => GetProvider(targetName) != null; - public AzureExecutionTarget(string targetName) - { - if (!IsValid(targetName)) - { - throw new InvalidOperationException($"{targetName} is not a valid target name."); - } - - TargetName = targetName; - } + public static AzureExecutionTarget? Create(string targetName) => + IsValid(targetName) + ? new AzureExecutionTarget() { TargetName = targetName } + : null; private static AzureProvider? GetProvider(string targetName) { From 53fd8e92c6a93022714bcda2393fba858d6e04cf Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 1 Jun 2020 19:52:27 -0400 Subject: [PATCH 05/28] Generate EntryPoint and compile into new assembly --- src/AzureClient/AzureClient.cs | 184 ++++++++++++++++++-------- src/AzureClient/IAzureClient.cs | 36 ++--- src/AzureClient/Magic/ExecuteMagic.cs | 17 +-- src/AzureClient/Magic/SubmitMagic.cs | 17 +-- src/AzureClient/Magic/TargetMagic.cs | 13 +- src/Core/Compiler/CompilerService.cs | 25 +++- src/Core/Compiler/ICompilerService.cs | 6 + src/Tests/AzureClientMagicTests.cs | 20 +-- src/Tests/AzureClientTests.cs | 5 +- 9 files changed, 187 insertions(+), 136 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 797a953132..ca94091cc8 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -5,18 +5,25 @@ using System; using System.Collections.Generic; -using System.Linq; using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; using System.Threading.Tasks; +using Microsoft.Azure.Quantum; using Microsoft.Azure.Quantum.Client; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Azure.Quantum.Storage; +using Microsoft.Extensions.Logging; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.IQSharp.Jupyter; +using Microsoft.Quantum.QsCompiler.SyntaxTree; +using Microsoft.Quantum.Runtime; using Microsoft.Quantum.Simulation.Core; using Microsoft.Rest.Azure; -using Microsoft.Azure.Quantum.Client.Models; -using Microsoft.Azure.Quantum.Storage; -using Microsoft.Azure.Quantum; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -25,6 +32,15 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class AzureClient : IAzureClient { + // TODO: Factor compilation and EntryPoint-related properties and code to a separate class. + private ICompilerService Compiler { get; } + private IOperationResolver OperationResolver { get; } + private IWorkspace Workspace { get; } + private ISnippets Snippets { get; } + private IReferences References { get; } + private ILogger Logger { get; } + private Lazy CompilerMetadata { get; set; } + private AssemblyInfo EntryPointAssembly { get; set; } = new AssemblyInfo(null); private string ConnectionString { get; set; } = string.Empty; private AzureExecutionTarget? ActiveTarget { get; set; } private AuthenticationResult? AuthenticationResult { get; set; } @@ -41,10 +57,66 @@ private string ValidExecutionTargetsDisplayText : string.Join(", ", ValidExecutionTargets.Select(target => target.Id)); } + public AzureClient( + ICompilerService compiler, + IOperationResolver operationResolver, + IWorkspace workspace, + ISnippets snippets, + IReferences references, + ILogger logger, + IEventService eventService) + { + Compiler = compiler; + OperationResolver = operationResolver; + Workspace = workspace; + Snippets = snippets; + References = references; + Logger = logger; + CompilerMetadata = new Lazy(LoadCompilerMetadata); + + Workspace.Reloaded += OnWorkspaceReloaded; + References.PackageLoaded += OnGlobalReferencesPackageLoaded; + + AssemblyLoadContext.Default.Resolving += Resolve; + + eventService?.TriggerServiceInitialized(this); + } + + private void OnGlobalReferencesPackageLoaded(object sender, PackageLoadedEventArgs e) => + CompilerMetadata = new Lazy(LoadCompilerMetadata); + + private void OnWorkspaceReloaded(object sender, ReloadedEventArgs e) => + CompilerMetadata = new Lazy(LoadCompilerMetadata); + + private CompilerMetadata LoadCompilerMetadata() => + Workspace.HasErrors + ? References?.CompilerMetadata.WithAssemblies(Snippets.AssemblyInfo) + : References?.CompilerMetadata.WithAssemblies(Snippets.AssemblyInfo, Workspace.AssemblyInfo); + + /// + /// Because the assemblies are loaded into memory, we need to provide this method to the AssemblyLoadContext + /// such that the Workspace assembly or this assembly is correctly resolved when it is executed for simulation. + /// + public Assembly Resolve(AssemblyLoadContext context, AssemblyName name) + { + if (name.Name == Path.GetFileNameWithoutExtension(EntryPointAssembly?.Location)) + { + return EntryPointAssembly.Assembly; + } + if (name.Name == Path.GetFileNameWithoutExtension(Snippets?.AssemblyInfo?.Location)) + { + return Snippets.AssemblyInfo.Assembly; + } + else if (name.Name == Path.GetFileNameWithoutExtension(Workspace?.AssemblyInfo?.Location)) + { + return Workspace.AssemblyInfo.Assembly; + } + + return null; + } /// - public async Task ConnectAsync( - IChannel channel, + public async Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, @@ -151,11 +223,7 @@ public async Task GetConnectionStatusAsync(IChannel channel) return ValidExecutionTargets.ToJupyterTable().ToExecutionResult(); } - private async Task SubmitOrExecuteJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName, - bool execute) + private async Task SubmitOrExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters, bool execute) { if (ActiveWorkspace == null) { @@ -176,7 +244,7 @@ private async Task SubmitOrExecuteJobAsync( return AzureClientError.NoOperationName.ToExecutionResult(); } - var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetName, ConnectionString); + var machine = QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetName, ConnectionString); if (machine == null) { // We should never get here, since ActiveTarget should have already been validated at the time it was set. @@ -184,50 +252,58 @@ private async Task SubmitOrExecuteJobAsync( return AzureClientError.InvalidTarget.ToExecutionResult(); } - var operationInfo = operationResolver.Resolve(operationName); - var entryPointInfo = new EntryPointInfo(operationInfo.RoslynType); - var entryPointInput = QVoid.Instance; + // TODO: Factor compilation and EntryPoint-related properties and code to a separate class. + var operationInfo = OperationResolver.Resolve(operationName); + var logger = new QSharpLogger(Logger); + EntryPointAssembly = Compiler.BuildEntryPoint(operationInfo, CompilerMetadata.Value, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__.dll")); + var entryPointOperationInfo = EntryPointAssembly.Operations.Single(); - if (execute) - { - channel.Stdout($"Executing {operationName} on target {ActiveTarget.TargetName}..."); - var output = await machine.ExecuteAsync(entryPointInfo, entryPointInput); - MostRecentJobId = output.Job.Id; + // TODO: Need these two lines to construct the Type objects correctly. + Type entryPointInputType = entryPointOperationInfo.RoslynParameters.Select(p => p.ParameterType).DefaultIfEmpty(typeof(QVoid)).First(); // .Header.Signature.ArgumentType.GetType(); + Type entryPointOutputType = typeof(Result); // entryPointOperationInfo.Header.Signature.ReturnType.GetType(); - // TODO: Add encoder to visualize IEnumerable> - return output.Histogram.ToExecutionResult(); - } - else + var entryPointInputOutputTypes = new Type[] { entryPointInputType, entryPointOutputType }; + Type entryPointInfoType = typeof(EntryPointInfo<,>).MakeGenericType(entryPointInputOutputTypes); + var entryPointInfo = entryPointInfoType.GetConstructor( + new Type[] { typeof(Type) }).Invoke(new object[] { entryPointOperationInfo.RoslynType }); + + var typedParameters = new List(); + foreach (var parameter in entryPointOperationInfo.RoslynParameters) { - channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); - var job = await machine.SubmitAsync(entryPointInfo, entryPointInput); - MostRecentJobId = job.Id; - channel.Stdout("Job submission successful."); - channel.Stdout($"To check the status, run:\n %azure.status {MostRecentJobId}"); - channel.Stdout($"To see the results, run:\n %azure.output {MostRecentJobId}"); - - // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here. - return job.ToJupyterTable().ToExecutionResult(); + typedParameters.Add(System.Convert.ChangeType(inputParameters.DecodeParameter(parameter.Name), parameter.ParameterType)); } + + // TODO: Need to use all of the typed parameters, not just the first one. + var entryPointInput = typedParameters.DefaultIfEmpty(QVoid.Instance).First(); + + channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); + + var method = typeof(IQuantumMachine).GetMethod("SubmitAsync").MakeGenericMethod(entryPointInputOutputTypes); + var job = await (method.Invoke(machine, new object[] { entryPointInfo, entryPointInput }) as Task); + MostRecentJobId = job.Id; + channel.Stdout("Job submission successful."); + channel.Stdout($"To check the status, run:\n %azure.status {MostRecentJobId}"); + channel.Stdout($"To see the results, run:\n %azure.output {MostRecentJobId}"); + + //if (execute) + //{ + // // TODO: wait for job completion + //} + + // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here. + return job.ToJupyterTable().ToExecutionResult(); } /// - public async Task SubmitJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName) => - await SubmitOrExecuteJobAsync(channel, operationResolver, operationName, execute: false); + public async Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters) => + await SubmitOrExecuteJobAsync(channel, operationName, inputParameters, execute: false); /// - public async Task ExecuteJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName) => - await SubmitOrExecuteJobAsync(channel, operationResolver, operationName, execute: true); + public async Task ExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters) => + await SubmitOrExecuteJobAsync(channel, operationName, inputParameters, execute: true); /// - public async Task GetActiveTargetAsync( - IChannel channel) + public async Task GetActiveTargetAsync(IChannel channel) { if (AvailableProviders == null) { @@ -248,10 +324,7 @@ public async Task GetActiveTargetAsync( } /// - public async Task SetActiveTargetAsync( - IChannel channel, - IReferences references, - string targetName) + public async Task SetActiveTargetAsync(IChannel channel, string targetName) { if (AvailableProviders == null) { @@ -278,15 +351,13 @@ public async Task SetActiveTargetAsync( // Set the active target and load the package. ActiveTarget = executionTarget; - await references.AddPackage(ActiveTarget.PackageName); + await References.AddPackage(ActiveTarget.PackageName); return $"Active target is now {ActiveTarget.TargetName}".ToExecutionResult(); } /// - public async Task GetJobResultAsync( - IChannel channel, - string jobId) + public async Task GetJobResultAsync(IChannel channel, string jobId) { if (ActiveWorkspace == null) { @@ -335,9 +406,7 @@ public async Task GetJobResultAsync( } /// - public async Task GetJobStatusAsync( - IChannel channel, - string jobId) + public async Task GetJobStatusAsync(IChannel channel, string jobId) { if (ActiveWorkspace == null) { @@ -368,8 +437,7 @@ public async Task GetJobStatusAsync( } /// - public async Task GetJobListAsync( - IChannel channel) + public async Task GetJobListAsync(IChannel channel) { if (ActiveWorkspace == null) { diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 69a0177ade..0cbccb706f 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -3,6 +3,7 @@ #nullable enable +using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; using Microsoft.Jupyter.Core; @@ -75,8 +76,7 @@ public interface IAzureClient /// /// The list of execution targets available in the Azure Quantum workspace. /// - public Task ConnectAsync( - IChannel channel, + public Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, @@ -90,8 +90,7 @@ public Task ConnectAsync( /// The list of execution targets available in the Azure Quantum workspace, /// or an error if the Azure Quantum workspace connection has not yet been created. /// - public Task GetConnectionStatusAsync( - IChannel channel); + public Task GetConnectionStatusAsync(IChannel channel); /// /// Submits the specified Q# operation as a job to the currently active target. @@ -99,10 +98,7 @@ public Task GetConnectionStatusAsync( /// /// Details of the submitted job, or an error if submission failed. /// - public Task SubmitJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName); + public Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters); /// /// Executes the specified Q# operation as a job to the currently active target @@ -111,10 +107,7 @@ public Task SubmitJobAsync( /// /// The result of the executed job, or an error if execution failed. /// - public Task ExecuteJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName); + public Task ExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters); /// /// Sets the specified target for job submission. @@ -122,10 +115,7 @@ public Task ExecuteJobAsync( /// /// Success if the target is valid, or an error if the target cannot be set. /// - public Task SetActiveTargetAsync( - IChannel channel, - IReferences references, - string targetName); + public Task SetActiveTargetAsync(IChannel channel, string targetName); /// /// Gets the currently specified target for job submission. @@ -133,8 +123,7 @@ public Task SetActiveTargetAsync( /// /// The target name. /// - public Task GetActiveTargetAsync( - IChannel channel); + public Task GetActiveTargetAsync(IChannel channel); /// /// Gets the result of a specified job. @@ -143,9 +132,7 @@ public Task GetActiveTargetAsync( /// The job result corresponding to the given job ID, /// or for the most recently-submitted job if no job ID is provided. /// - public Task GetJobResultAsync( - IChannel channel, - string jobId); + public Task GetJobResultAsync(IChannel channel, string jobId); /// /// Gets the status of a specified job. @@ -154,9 +141,7 @@ public Task GetJobResultAsync( /// The job status corresponding to the given job ID, /// or for the most recently-submitted job if no job ID is provided. /// - public Task GetJobStatusAsync( - IChannel channel, - string jobId); + public Task GetJobStatusAsync(IChannel channel, string jobId); /// /// Gets a list of all jobs in the current Azure Quantum workspace. @@ -164,7 +149,6 @@ public Task GetJobStatusAsync( /// /// A list of all jobs in the current workspace. /// - public Task GetJobListAsync( - IChannel channel); + public Task GetJobListAsync(IChannel channel); } } diff --git a/src/AzureClient/Magic/ExecuteMagic.cs b/src/AzureClient/Magic/ExecuteMagic.cs index 8f69532952..0fd3da1706 100644 --- a/src/AzureClient/Magic/ExecuteMagic.cs +++ b/src/AzureClient/Magic/ExecuteMagic.cs @@ -19,22 +19,13 @@ public class ExecuteMagic : AzureClientMagicBase { private const string ParameterNameOperationName = "operationName"; - /// - /// Gets the symbol resolver used by this magic command to find - /// operations or functions to be simulated. - /// - public IOperationResolver OperationResolver { get; } - /// /// Initializes a new instance of the class. /// - /// - /// The object used to find and resolve operations. - /// /// /// The object to use for Azure functionality. /// - public ExecuteMagic(IOperationResolver operationResolver, IAzureClient azureClient) + public ExecuteMagic(IAzureClient azureClient) : base( azureClient, "azure.execute", @@ -60,8 +51,8 @@ The Azure Quantum workspace must previously have been initialized ``` ".Dedent(), }, - }) => - this.OperationResolver = operationResolver; + }) + { } /// /// Executes a new job in an Azure Quantum workspace given a Q# operation @@ -72,7 +63,7 @@ public override async Task RunAsync(string input, IChannel chan { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); - return await AzureClient.ExecuteJobAsync(channel, OperationResolver, operationName); + return await AzureClient.ExecuteJobAsync(channel, operationName, inputParameters); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index 8d779646d5..10b570d2e2 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -19,22 +19,13 @@ public class SubmitMagic : AzureClientMagicBase { private const string ParameterNameOperationName = "operationName"; - /// - /// Gets the symbol resolver used by this magic command to find - /// operations or functions to be simulated. - /// - public IOperationResolver OperationResolver { get; } - /// /// Initializes a new instance of the class. /// - /// - /// The object used to find and resolve operations. - /// /// /// The object to use for Azure functionality. /// - public SubmitMagic(IOperationResolver operationResolver, IAzureClient azureClient) + public SubmitMagic(IAzureClient azureClient) : base( azureClient, "azure.submit", @@ -58,8 +49,8 @@ The Azure Quantum workspace must previously have been initialized ``` ".Dedent(), }, - }) => - this.OperationResolver = operationResolver; + }) + { } /// /// Submits a new job to an Azure Quantum workspace given a Q# operation @@ -69,7 +60,7 @@ public override async Task RunAsync(string input, IChannel chan { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); - return await AzureClient.SubmitJobAsync(channel, OperationResolver, operationName); + return await AzureClient.SubmitJobAsync(channel, operationName, inputParameters); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 7e5e2a4151..2a5cf36c93 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -19,18 +19,13 @@ public class TargetMagic : AzureClientMagicBase { private const string ParameterNameTargetName = "name"; - private IReferences? References { get; set; } - /// /// Initializes a new instance of the class. /// /// /// The object to use for Azure functionality. /// - /// - /// The object to use for loading target-specific packages. - /// - public TargetMagic(IAzureClient azureClient, IReferences references) + public TargetMagic(IAzureClient azureClient) : base( azureClient, "azure.target", @@ -62,8 +57,8 @@ available in the workspace. ``` ".Dedent(), }, - }) => - References = references; + }) + { } /// /// Sets or views the target for job submission to the current Azure Quantum workspace. @@ -74,7 +69,7 @@ public override async Task RunAsync(string input, IChannel chan if (inputParameters.ContainsKey(ParameterNameTargetName)) { string targetName = inputParameters.DecodeParameter(ParameterNameTargetName); - return await AzureClient.SetActiveTargetAsync(channel, References, targetName); + return await AzureClient.SetActiveTargetAsync(channel, targetName); } return await AzureClient.GetActiveTargetAsync(channel); diff --git a/src/Core/Compiler/CompilerService.cs b/src/Core/Compiler/CompilerService.cs index 2869592270..eccd023871 100644 --- a/src/Core/Compiler/CompilerService.cs +++ b/src/Core/Compiler/CompilerService.cs @@ -17,8 +17,10 @@ using Microsoft.Quantum.QsCompiler.CsharpGeneration; using Microsoft.Quantum.QsCompiler.DataTypes; using Microsoft.Quantum.QsCompiler.Serialization; +using Microsoft.Quantum.QsCompiler.SyntaxProcessing; using Microsoft.Quantum.QsCompiler.SyntaxTree; using Microsoft.Quantum.QsCompiler.Transformations.BasicTransformations; +using Microsoft.Quantum.QsCompiler.Transformations.QsCodeOutput; using Newtonsoft.Json.Bson; using QsReferences = Microsoft.Quantum.QsCompiler.CompilationBuilder.References; @@ -67,6 +69,27 @@ private QsCompilation UpdateCompilation(ImmutableDictionary sources return loaded.CompilationOutput; } + /// + public AssemblyInfo BuildEntryPoint(OperationInfo operation, CompilerMetadata metadatas, QSharpLogger logger, string dllName) + { + var signature = operation.Header.PrintSignature(); + var argumentTuple = SyntaxTreeToQsharp.ArgumentTuple(operation.Header.ArgumentTuple, type => type.ToString(), symbolsOnly: true); + + var entryPointUri = new Uri(Path.GetFullPath(Path.Combine("/", $"entrypoint.qs"))); + var entryPointSnippet = @$"namespace ENTRYPOINT + {{ + open {operation.Header.QualifiedName.Namespace.Value}; + @{BuiltIn.EntryPoint.FullName}() + operation {signature} + {{ + return {operation.Header.QualifiedName}{argumentTuple}; + }} + }}"; + + var sources = new Dictionary() {{ entryPointUri, entryPointSnippet }}.ToImmutableDictionary(); + return BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: true); + } + /// /// Builds the corresponding .net core assembly from the code in the given Q# Snippets. /// Each snippet code is wrapped inside the 'SNIPPETS_NAMESPACE' namespace and processed as a file @@ -103,7 +126,7 @@ private AssemblyInfo BuildAssembly(ImmutableDictionary sources, Com try { // Generate C# simulation code from Q# syntax tree and convert it into C# syntax trees: - var trees = new List(); + var trees = new List(); NonNullable GetFileId(Uri uri) => CompilationUnitManager.TryGetFileId(uri, out var id) ? id : NonNullable.New(uri.AbsolutePath); foreach (var file in sources.Keys) { diff --git a/src/Core/Compiler/ICompilerService.cs b/src/Core/Compiler/ICompilerService.cs index 7a9548a61f..7fde4a2228 100644 --- a/src/Core/Compiler/ICompilerService.cs +++ b/src/Core/Compiler/ICompilerService.cs @@ -12,6 +12,12 @@ namespace Microsoft.Quantum.IQSharp /// public interface ICompilerService { + /// + /// Builds an executable assembly with an entry point that invokes the Q# operation specified + /// by the provided object. + /// + AssemblyInfo BuildEntryPoint(OperationInfo operation, CompilerMetadata metadatas, QSharpLogger logger, string dllName); + /// /// Builds the corresponding .net core assembly from the code in the given Q# Snippets. /// diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index fbbc880253..a45f1eb6c0 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -85,8 +85,7 @@ public void TestSubmitMagic() { // no arguments var azureClient = new MockAzureClient(); - var operationResolver = new MockOperationResolver(); - var submitMagic = new SubmitMagic(operationResolver, azureClient); + var submitMagic = new SubmitMagic(azureClient); submitMagic.Test(string.Empty); Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); @@ -101,8 +100,7 @@ public void TestExecuteMagic() { // no arguments var azureClient = new MockAzureClient(); - var operationResolver = new MockOperationResolver(); - var executeMagic = new ExecuteMagic(operationResolver, azureClient); + var executeMagic = new ExecuteMagic(azureClient); executeMagic.Test(string.Empty); Assert.AreEqual(azureClient.LastAction, AzureClientAction.ExecuteJob); @@ -141,19 +139,15 @@ public void TestJobsMagic() [TestMethod] public void TestTargetMagic() { - var workspace = "Workspace"; - var services = Startup.CreateServiceProvider(workspace); - var references = services.GetService(); - // single argument - should set active target var azureClient = new MockAzureClient(); - var targetMagic = new TargetMagic(azureClient, references); + var targetMagic = new TargetMagic(azureClient); targetMagic.Test(targetName); Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); // no arguments - should print active target azureClient = new MockAzureClient(); - targetMagic = new TargetMagic(azureClient, references); + targetMagic = new TargetMagic(azureClient); targetMagic.Test(string.Empty); Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetActiveTarget); } @@ -182,7 +176,7 @@ public class MockAzureClient : IAzureClient internal List SubmittedJobs = new List(); internal List ExecutedJobs = new List(); - public async Task SetActiveTargetAsync(IChannel channel, IReferences references, string targetName) + public async Task SetActiveTargetAsync(IChannel channel, string targetName) { LastAction = AzureClientAction.SetActiveTarget; ActiveTargetName = targetName; @@ -194,14 +188,14 @@ public async Task GetActiveTargetAsync(IChannel channel) return ActiveTargetName.ToExecutionResult(); } - public async Task SubmitJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName) + public async Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters) { LastAction = AzureClientAction.SubmitJob; SubmittedJobs.Add(operationName); return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task ExecuteJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName) + public async Task ExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters) { LastAction = AzureClientAction.ExecuteJob; ExecutedJobs.Add(operationName); diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index 425563f5a4..2329ac834f 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -31,15 +31,14 @@ public void TestTargets() { var workspace = "Workspace"; var services = Startup.CreateServiceProvider(workspace); - var references = services.GetService(); var azureClient = services.GetService(); // SetActiveTargetAsync with recognized target name, but not yet connected - var result = azureClient.SetActiveTargetAsync(new MockChannel(), references, "ionq.simulator").GetAwaiter().GetResult(); + var result = azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator").GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Error); // SetActiveTargetAsync with unrecognized target name - result = azureClient.SetActiveTargetAsync(new MockChannel(), references, "contoso.qpu").GetAwaiter().GetResult(); + result = azureClient.SetActiveTargetAsync(new MockChannel(), "contoso.qpu").GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Error); // GetActiveTargetAsync, but not yet connected From a33a2c9fa36c653b8dc310e39e5074804ff60a12 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Tue, 2 Jun 2020 14:04:48 -0400 Subject: [PATCH 06/28] Changes for JobNotCompleted case --- src/AzureClient/AzureClient.cs | 14 ++++++-------- src/AzureClient/IAzureClient.cs | 6 ++++++ src/AzureClient/Resources.cs | 3 +++ 3 files changed, 15 insertions(+), 8 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 797a953132..1f01dc737f 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -201,10 +201,9 @@ private async Task SubmitOrExecuteJobAsync( { channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); var job = await machine.SubmitAsync(entryPointInfo, entryPointInput); + channel.Stdout($"Job {job.Id} submitted successfully."); + MostRecentJobId = job.Id; - channel.Stdout("Job submission successful."); - channel.Stdout($"To check the status, run:\n %azure.status {MostRecentJobId}"); - channel.Stdout($"To see the results, run:\n %azure.output {MostRecentJobId}"); // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here. return job.ToJupyterTable().ToExecutionResult(); @@ -314,9 +313,8 @@ public async Task GetJobResultAsync( if (!job.Succeeded || string.IsNullOrEmpty(job.Details.OutputDataUri)) { - channel.Stderr($"Job ID {jobId} has not completed. Displaying the status instead."); - // TODO: Add encoder for CloudJob rather than calling ToJupyterTable() here directly. - return job.Details.ToJupyterTable().ToExecutionResult(); + channel.Stderr($"Job ID {jobId} has not completed. To check the status, use:\n %azure.status {jobId}"); + return AzureClientError.JobNotCompleted.ToExecutionResult(); } var stream = new MemoryStream(); @@ -363,8 +361,8 @@ public async Task GetJobStatusAsync( return AzureClientError.JobNotFound.ToExecutionResult(); } - // TODO: Add encoder for CloudJob rather than calling ToJupyterTable() here directly. - return job.Details.ToJupyterTable().ToExecutionResult(); + // TODO: Add encoder for CloudJob which calls ToJupyterTable() for display. + return job.Details.ToExecutionResult(); } /// diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 69a0177ade..f7df675652 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -44,6 +44,12 @@ public enum AzureClientError [Description(Resources.AzureClientErrorJobNotFound)] JobNotFound, + /// + /// The result of a job was requested, but the job has not yet completed. + /// + [Description(Resources.AzureClientErrorJobNotCompleted)] + JobNotCompleted, + /// /// No Q# operation name was provided where one was required. /// diff --git a/src/AzureClient/Resources.cs b/src/AzureClient/Resources.cs index 06703703d0..1fedce95f2 100644 --- a/src/AzureClient/Resources.cs +++ b/src/AzureClient/Resources.cs @@ -22,6 +22,9 @@ internal static class Resources public const string AzureClientErrorJobNotFound = "No job with the given ID was found in the current Azure Quantum workspace."; + public const string AzureClientErrorJobNotCompleted = + "The specified Azure Quantum job has not yet completed."; + public const string AzureClientErrorNoOperationName = "No Q# operation name was specified for Azure Quantum job submission."; From 44490405a02e2186020e46b447b91ac0592ab7ef Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Tue, 2 Jun 2020 15:31:52 -0400 Subject: [PATCH 07/28] Refactor entry point code into new classes --- src/AzureClient/AzureClient.cs | 96 ++-------------- src/AzureClient/EntryPoint/EntryPoint.cs | 53 +++++++++ .../EntryPoint/EntryPointGenerator.cs | 105 ++++++++++++++++++ src/AzureClient/EntryPoint/IEntryPoint.cs | 28 +++++ .../EntryPoint/IEntryPointGenerator.cs | 26 +++++ src/AzureClient/Extensions.cs | 1 + src/AzureClient/Magic/ExecuteMagic.cs | 11 +- src/AzureClient/Magic/SubmitMagic.cs | 11 +- 8 files changed, 236 insertions(+), 95 deletions(-) create mode 100644 src/AzureClient/EntryPoint/EntryPoint.cs create mode 100644 src/AzureClient/EntryPoint/EntryPointGenerator.cs create mode 100644 src/AzureClient/EntryPoint/IEntryPoint.cs create mode 100644 src/AzureClient/EntryPoint/IEntryPointGenerator.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index a433d4ab4b..1b917b1d57 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -7,8 +7,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Reflection; -using System.Runtime.Loader; using System.Threading.Tasks; using Microsoft.Azure.Quantum; using Microsoft.Azure.Quantum.Client; @@ -18,11 +16,6 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; using Microsoft.Jupyter.Core; -using Microsoft.Quantum.IQSharp.Common; -using Microsoft.Quantum.IQSharp.Jupyter; -using Microsoft.Quantum.QsCompiler.SyntaxTree; -using Microsoft.Quantum.Runtime; -using Microsoft.Quantum.Simulation.Core; using Microsoft.Rest.Azure; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -32,15 +25,9 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class AzureClient : IAzureClient { - // TODO: Factor compilation and EntryPoint-related properties and code to a separate class. - private ICompilerService Compiler { get; } - private IOperationResolver OperationResolver { get; } - private IWorkspace Workspace { get; } - private ISnippets Snippets { get; } - private IReferences References { get; } private ILogger Logger { get; } - private Lazy CompilerMetadata { get; set; } - private AssemblyInfo EntryPointAssembly { get; set; } = new AssemblyInfo(null); + private IReferences References { get; } + private IEntryPointGenerator EntryPointGenerator { get; } private string ConnectionString { get; set; } = string.Empty; private AzureExecutionTarget? ActiveTarget { get; set; } private AuthenticationResult? AuthenticationResult { get; set; } @@ -58,63 +45,17 @@ private string ValidExecutionTargetsDisplayText } public AzureClient( - ICompilerService compiler, - IOperationResolver operationResolver, - IWorkspace workspace, - ISnippets snippets, IReferences references, + IEntryPointGenerator entryPointGenerator, ILogger logger, IEventService eventService) { - Compiler = compiler; - OperationResolver = operationResolver; - Workspace = workspace; - Snippets = snippets; References = references; + EntryPointGenerator = entryPointGenerator; Logger = logger; - CompilerMetadata = new Lazy(LoadCompilerMetadata); - - Workspace.Reloaded += OnWorkspaceReloaded; - References.PackageLoaded += OnGlobalReferencesPackageLoaded; - - AssemblyLoadContext.Default.Resolving += Resolve; - eventService?.TriggerServiceInitialized(this); } - private void OnGlobalReferencesPackageLoaded(object sender, PackageLoadedEventArgs e) => - CompilerMetadata = new Lazy(LoadCompilerMetadata); - - private void OnWorkspaceReloaded(object sender, ReloadedEventArgs e) => - CompilerMetadata = new Lazy(LoadCompilerMetadata); - - private CompilerMetadata LoadCompilerMetadata() => - Workspace.HasErrors - ? References?.CompilerMetadata.WithAssemblies(Snippets.AssemblyInfo) - : References?.CompilerMetadata.WithAssemblies(Snippets.AssemblyInfo, Workspace.AssemblyInfo); - - /// - /// Because the assemblies are loaded into memory, we need to provide this method to the AssemblyLoadContext - /// such that the Workspace assembly or this assembly is correctly resolved when it is executed for simulation. - /// - public Assembly Resolve(AssemblyLoadContext context, AssemblyName name) - { - if (name.Name == Path.GetFileNameWithoutExtension(EntryPointAssembly?.Location)) - { - return EntryPointAssembly.Assembly; - } - if (name.Name == Path.GetFileNameWithoutExtension(Snippets?.AssemblyInfo?.Location)) - { - return Snippets.AssemblyInfo.Assembly; - } - else if (name.Name == Path.GetFileNameWithoutExtension(Workspace?.AssemblyInfo?.Location)) - { - return Workspace.AssemblyInfo.Assembly; - } - - return null; - } - /// public async Task ConnectAsync(IChannel channel, string subscriptionId, @@ -252,34 +193,11 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st return AzureClientError.InvalidTarget.ToExecutionResult(); } - // TODO: Factor compilation and EntryPoint-related properties and code to a separate class. - var operationInfo = OperationResolver.Resolve(operationName); - var logger = new QSharpLogger(Logger); - EntryPointAssembly = Compiler.BuildEntryPoint(operationInfo, CompilerMetadata.Value, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__.dll")); - var entryPointOperationInfo = EntryPointAssembly.Operations.Single(); - - // TODO: Need these two lines to construct the Type objects correctly. - Type entryPointInputType = entryPointOperationInfo.RoslynParameters.Select(p => p.ParameterType).DefaultIfEmpty(typeof(QVoid)).First(); // .Header.Signature.ArgumentType.GetType(); - Type entryPointOutputType = typeof(Result); // entryPointOperationInfo.Header.Signature.ReturnType.GetType(); - - var entryPointInputOutputTypes = new Type[] { entryPointInputType, entryPointOutputType }; - Type entryPointInfoType = typeof(EntryPointInfo<,>).MakeGenericType(entryPointInputOutputTypes); - var entryPointInfo = entryPointInfoType.GetConstructor( - new Type[] { typeof(Type) }).Invoke(new object[] { entryPointOperationInfo.RoslynType }); - - var typedParameters = new List(); - foreach (var parameter in entryPointOperationInfo.RoslynParameters) - { - typedParameters.Add(System.Convert.ChangeType(inputParameters.DecodeParameter(parameter.Name), parameter.ParameterType)); - } - - // TODO: Need to use all of the typed parameters, not just the first one. - var entryPointInput = typedParameters.DefaultIfEmpty(QVoid.Instance).First(); - channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); - var method = typeof(IQuantumMachine).GetMethod("SubmitAsync").MakeGenericMethod(entryPointInputOutputTypes); - var job = await (method.Invoke(machine, new object[] { entryPointInfo, entryPointInput }) as Task); + var entryPoint = EntryPointGenerator.Generate(operationName); + var job = await entryPoint.SubmitAsync(machine, inputParameters); + channel.Stdout($"Job {job.Id} submitted successfully."); MostRecentJobId = job.Id; diff --git a/src/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs new file mode 100644 index 0000000000..a9be64b0b9 --- /dev/null +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -0,0 +1,53 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Quantum.Runtime; +using Microsoft.Quantum.Simulation.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + internal class EntryPoint : IEntryPoint + { + private readonly object entryPointInfo; + private readonly Type[] entryPointInputOutputTypes; + private readonly OperationInfo entryPointOperationInfo; + + /// + /// Creates an object used to submit jobs to Azure Quantum. + /// + /// Must be an object with type + /// parameters specified by the types in the entryPointInputOutputTypes argument. + /// Specifies the type parameters for the + /// object provided as the entryPointInfo argument. + /// Information about the Q# operation to be used as the entry point. + public EntryPoint(object entryPointInfo, Type[] entryPointInputOutputTypes, OperationInfo entryPointOperationInfo) + { + this.entryPointInfo = entryPointInfo; + this.entryPointInputOutputTypes = entryPointInputOutputTypes; + this.entryPointOperationInfo = entryPointOperationInfo; + } + + /// + public Task SubmitAsync(IQuantumMachine machine, Dictionary inputParameters) + { + var typedParameters = new List(); + foreach (var parameter in entryPointOperationInfo.RoslynParameters) + { + typedParameters.Add(System.Convert.ChangeType(inputParameters[parameter.Name], parameter.ParameterType)); + } + + // TODO: Need to use all of the typed parameters, not just the first one. + var entryPointInput = typedParameters.DefaultIfEmpty(QVoid.Instance).First(); + + var method = typeof(IQuantumMachine).GetMethod("SubmitAsync").MakeGenericMethod(entryPointInputOutputTypes); + return method.Invoke(machine, new object[] { entryPointInfo, entryPointInput }) as Task; + } + } +} diff --git a/src/AzureClient/EntryPoint/EntryPointGenerator.cs b/src/AzureClient/EntryPoint/EntryPointGenerator.cs new file mode 100644 index 0000000000..edf37c7736 --- /dev/null +++ b/src/AzureClient/EntryPoint/EntryPointGenerator.cs @@ -0,0 +1,105 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.IO; +using System.Linq; +using System.Reflection; +using System.Runtime.Loader; +using Microsoft.Extensions.Logging; +using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.Simulation.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + internal class EntryPointGenerator : IEntryPointGenerator + { + private ICompilerService Compiler { get; } + private IOperationResolver OperationResolver { get; } + private IWorkspace Workspace { get; } + private ISnippets Snippets { get; } + private IReferences References { get; } + private ILogger Logger { get; } + private Lazy CompilerMetadata { get; set; } + private AssemblyInfo AssemblyInfo { get; set; } = new AssemblyInfo(null); + + public EntryPointGenerator( + ICompilerService compiler, + IOperationResolver operationResolver, + IWorkspace workspace, + ISnippets snippets, + IReferences references, + ILogger logger, + IEventService eventService) + { + Compiler = compiler; + OperationResolver = operationResolver; + Workspace = workspace; + Snippets = snippets; + References = references; + Logger = logger; + CompilerMetadata = new Lazy(LoadCompilerMetadata); + + Workspace.Reloaded += OnWorkspaceReloaded; + References.PackageLoaded += OnGlobalReferencesPackageLoaded; + + AssemblyLoadContext.Default.Resolving += Resolve; + + eventService?.TriggerServiceInitialized(this); + } + private void OnGlobalReferencesPackageLoaded(object sender, PackageLoadedEventArgs e) => + CompilerMetadata = new Lazy(LoadCompilerMetadata); + + private void OnWorkspaceReloaded(object sender, ReloadedEventArgs e) => + CompilerMetadata = new Lazy(LoadCompilerMetadata); + + private CompilerMetadata LoadCompilerMetadata() => + Workspace.HasErrors + ? References?.CompilerMetadata.WithAssemblies(Snippets.AssemblyInfo) + : References?.CompilerMetadata.WithAssemblies(Snippets.AssemblyInfo, Workspace.AssemblyInfo); + + /// + /// Because the assemblies are loaded into memory, we need to provide this method to the AssemblyLoadContext + /// such that the Workspace assembly or this assembly is correctly resolved when it is executed for simulation. + /// + public Assembly Resolve(AssemblyLoadContext context, AssemblyName name) + { + if (name.Name == Path.GetFileNameWithoutExtension(AssemblyInfo?.Location)) + { + return AssemblyInfo.Assembly; + } + if (name.Name == Path.GetFileNameWithoutExtension(Snippets?.AssemblyInfo?.Location)) + { + return Snippets.AssemblyInfo.Assembly; + } + else if (name.Name == Path.GetFileNameWithoutExtension(Workspace?.AssemblyInfo?.Location)) + { + return Workspace.AssemblyInfo.Assembly; + } + + return null; + } + + public IEntryPoint Generate(string operationName) + { + var operationInfo = OperationResolver.Resolve(operationName); + var logger = new QSharpLogger(Logger); + AssemblyInfo = Compiler.BuildEntryPoint(operationInfo, CompilerMetadata.Value, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__.dll")); + var entryPointOperationInfo = AssemblyInfo.Operations.Single(); + + // TODO: Need these two lines to construct the Type objects correctly. + Type entryPointInputType = entryPointOperationInfo.RoslynParameters.Select(p => p.ParameterType).DefaultIfEmpty(typeof(QVoid)).First(); // .Header.Signature.ArgumentType.GetType(); + Type entryPointOutputType = typeof(Result); // entryPointOperationInfo.Header.Signature.ReturnType.GetType(); + + var entryPointInputOutputTypes = new Type[] { entryPointInputType, entryPointOutputType }; + Type entryPointInfoType = typeof(EntryPointInfo<,>).MakeGenericType(entryPointInputOutputTypes); + var entryPointInfo = entryPointInfoType.GetConstructor( + new Type[] { typeof(Type) }).Invoke(new object[] { entryPointOperationInfo.RoslynType }); + + return new EntryPoint(entryPointInfo, entryPointInputOutputTypes, entryPointOperationInfo); + } + } +} diff --git a/src/AzureClient/EntryPoint/IEntryPoint.cs b/src/AzureClient/EntryPoint/IEntryPoint.cs new file mode 100644 index 0000000000..f2eeab219b --- /dev/null +++ b/src/AzureClient/EntryPoint/IEntryPoint.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; +using System.Threading.Tasks; +using Microsoft.Quantum.Runtime; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// Represents a Q# entry point that can be submitted + /// for execution to Azure Quantum. + /// + public interface IEntryPoint + { + /// + /// Submits the entry point for execution to Azure Quantum. + /// + /// The object representing the job submission target. + /// The provided input parameters to the entry point operation. + /// The details of the submitted job. + public Task SubmitAsync(IQuantumMachine machine, Dictionary inputParameters); + } +} diff --git a/src/AzureClient/EntryPoint/IEntryPointGenerator.cs b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs new file mode 100644 index 0000000000..f9cd307e4b --- /dev/null +++ b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs @@ -0,0 +1,26 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// This service is capable of generating entry points for + /// job submission to Azure Quantum. + /// + public interface IEntryPointGenerator + { + /// + /// Compiles an assembly and returns the object + /// representing an entry point that wraps the specified operation. + /// + /// The name of the operation to wrap in an entry point. + /// The generated entry point. + public IEntryPoint Generate(string operationName); + } +} diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 116643480f..8bc768ec7f 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -27,6 +27,7 @@ public static class Extensions public static void AddAzureClient(this IServiceCollection services) { services.AddSingleton(); + services.AddSingleton(); } /// diff --git a/src/AzureClient/Magic/ExecuteMagic.cs b/src/AzureClient/Magic/ExecuteMagic.cs index 0fd3da1706..8a2ecd45ec 100644 --- a/src/AzureClient/Magic/ExecuteMagic.cs +++ b/src/AzureClient/Magic/ExecuteMagic.cs @@ -3,9 +3,7 @@ #nullable enable -using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -63,7 +61,14 @@ public override async Task RunAsync(string input, IChannel chan { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); - return await AzureClient.ExecuteJobAsync(channel, operationName, inputParameters); + + var remainingParameters = new Dictionary(); + foreach (var key in inputParameters.Keys) + { + remainingParameters[key] = inputParameters.DecodeParameter(key); + } + + return await AzureClient.ExecuteJobAsync(channel, operationName, remainingParameters); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index 10b570d2e2..835acff7bb 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -3,9 +3,7 @@ #nullable enable -using System; using System.Collections.Generic; -using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -60,7 +58,14 @@ public override async Task RunAsync(string input, IChannel chan { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); - return await AzureClient.SubmitJobAsync(channel, operationName, inputParameters); + + var remainingParameters = new Dictionary(); + foreach (var key in inputParameters.Keys) + { + remainingParameters[key] = inputParameters.DecodeParameter(key); + } + + return await AzureClient.SubmitJobAsync(channel, operationName, remainingParameters); } } } \ No newline at end of file From 6f3a6fe5cd350215deb083fe8bcc4de10d99e68d Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Tue, 2 Jun 2020 17:26:10 -0400 Subject: [PATCH 08/28] Use correct input and output types --- src/AzureClient/EntryPoint/EntryPoint.cs | 47 +++++++++++-------- .../EntryPoint/EntryPointGenerator.cs | 15 +++--- src/Core/OperationInfo.cs | 8 ++++ 3 files changed, 45 insertions(+), 25 deletions(-) diff --git a/src/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs index a9be64b0b9..be08f7833c 100644 --- a/src/AzureClient/EntryPoint/EntryPoint.cs +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -15,39 +15,48 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// internal class EntryPoint : IEntryPoint { - private readonly object entryPointInfo; - private readonly Type[] entryPointInputOutputTypes; - private readonly OperationInfo entryPointOperationInfo; + private object EntryPointInfo { get; } + private Type InputType { get; } + private Type OutputType { get; } + private OperationInfo OperationInfo { get; } /// /// Creates an object used to submit jobs to Azure Quantum. /// - /// Must be an object with type - /// parameters specified by the types in the entryPointInputOutputTypes argument. - /// Specifies the type parameters for the - /// object provided as the entryPointInfo argument. - /// Information about the Q# operation to be used as the entry point. - public EntryPoint(object entryPointInfo, Type[] entryPointInputOutputTypes, OperationInfo entryPointOperationInfo) + /// Must be an object with type + /// parameters specified by the types in the entryPointInputbeginWords argument. + /// Specifies the input parameter type for the + /// object provided as the entryPointInfo argument. + /// Specifies the output parameter type for the + /// object provided as the entryPointInfo argument. + /// Information about the Q# operation to be used as the entry point. + public EntryPoint(object entryPointInfo, Type inputType, Type outputType, OperationInfo operationInfo) { - this.entryPointInfo = entryPointInfo; - this.entryPointInputOutputTypes = entryPointInputOutputTypes; - this.entryPointOperationInfo = entryPointOperationInfo; + EntryPointInfo = entryPointInfo; + InputType = inputType; + OutputType = outputType; + OperationInfo = operationInfo; } /// public Task SubmitAsync(IQuantumMachine machine, Dictionary inputParameters) { - var typedParameters = new List(); - foreach (var parameter in entryPointOperationInfo.RoslynParameters) + var parameterTypes = new List(); + var parameterValues = new List(); + foreach (var parameter in OperationInfo.RoslynParameters) { - typedParameters.Add(System.Convert.ChangeType(inputParameters[parameter.Name], parameter.ParameterType)); + parameterTypes.Add(parameter.ParameterType); + parameterValues.Add(System.Convert.ChangeType(inputParameters[parameter.Name], parameter.ParameterType)); } - // TODO: Need to use all of the typed parameters, not just the first one. - var entryPointInput = typedParameters.DefaultIfEmpty(QVoid.Instance).First(); + var entryPointInput = + parameterValues.Count == 0 ? QVoid.Instance : + parameterValues.Count == 1 ? parameterValues.Single() : + InputType.GetConstructor(parameterTypes.ToArray()).Invoke(parameterValues.ToArray()); - var method = typeof(IQuantumMachine).GetMethod("SubmitAsync").MakeGenericMethod(entryPointInputOutputTypes); - return method.Invoke(machine, new object[] { entryPointInfo, entryPointInput }) as Task; + var submitMethod = typeof(IQuantumMachine).GetMethod("SubmitAsync").MakeGenericMethod(new Type[] { InputType, OutputType }); + var submitParameters = new object[] { EntryPointInfo, entryPointInput }; + return submitMethod.Invoke(machine, submitParameters) as Task; } } } diff --git a/src/AzureClient/EntryPoint/EntryPointGenerator.cs b/src/AzureClient/EntryPoint/EntryPointGenerator.cs index edf37c7736..97bbae03bb 100644 --- a/src/AzureClient/EntryPoint/EntryPointGenerator.cs +++ b/src/AzureClient/EntryPoint/EntryPointGenerator.cs @@ -90,16 +90,19 @@ public IEntryPoint Generate(string operationName) AssemblyInfo = Compiler.BuildEntryPoint(operationInfo, CompilerMetadata.Value, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__.dll")); var entryPointOperationInfo = AssemblyInfo.Operations.Single(); - // TODO: Need these two lines to construct the Type objects correctly. - Type entryPointInputType = entryPointOperationInfo.RoslynParameters.Select(p => p.ParameterType).DefaultIfEmpty(typeof(QVoid)).First(); // .Header.Signature.ArgumentType.GetType(); - Type entryPointOutputType = typeof(Result); // entryPointOperationInfo.Header.Signature.ReturnType.GetType(); + var parameterTypes = entryPointOperationInfo.RoslynParameters.Select(p => p.ParameterType).ToArray(); + var typeCount = parameterTypes.Length; + Type entryPointInputType = + typeCount == 0 ? typeof(QVoid) : + typeCount == 1 ? parameterTypes.Single() : + PartialMapper.TupleTypes[typeCount].MakeGenericType(parameterTypes); + Type entryPointOutputType = entryPointOperationInfo.ReturnType; - var entryPointInputOutputTypes = new Type[] { entryPointInputType, entryPointOutputType }; - Type entryPointInfoType = typeof(EntryPointInfo<,>).MakeGenericType(entryPointInputOutputTypes); + Type entryPointInfoType = typeof(EntryPointInfo<,>).MakeGenericType(new Type[] { entryPointInputType, entryPointOutputType }); var entryPointInfo = entryPointInfoType.GetConstructor( new Type[] { typeof(Type) }).Invoke(new object[] { entryPointOperationInfo.RoslynType }); - return new EntryPoint(entryPointInfo, entryPointInputOutputTypes, entryPointOperationInfo); + return new EntryPoint(entryPointInfo, entryPointInputType, entryPointOutputType, entryPointOperationInfo); } } } diff --git a/src/Core/OperationInfo.cs b/src/Core/OperationInfo.cs index caaeddbd5b..ebb9141f04 100644 --- a/src/Core/OperationInfo.cs +++ b/src/Core/OperationInfo.cs @@ -26,12 +26,14 @@ public class OperationInfo { private Lazy> _params; private Lazy _roslynParams; + private Lazy _returnType; internal OperationInfo(Type roslynType, CallableDeclarationHeader header) { this.Header = header ?? throw new ArgumentNullException(nameof(header)); RoslynType = roslynType; _roslynParams = new Lazy(() => RoslynType?.GetMethod("Run").GetParameters().Skip(1).ToArray()); + _returnType = new Lazy(() => RoslynType?.GetMethod("Run").ReturnType.GenericTypeArguments.Single()); _params = new Lazy>(() => RoslynParameters?.ToDictionary(p => p.Name, p => p.ParameterType.Name)); } @@ -60,6 +62,12 @@ internal OperationInfo(Type roslynType, CallableDeclarationHeader header) [JsonIgnore] public ParameterInfo[] RoslynParameters => _roslynParams.Value; + /// + /// The return type for the underlying compiled .NET Type for this Q# operation + /// + [JsonIgnore] + public Type ReturnType => _returnType.Value; + public override string ToString() => FullName; } From d47fddd9d2295240fab002a676ed562dea3132c3 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Tue, 2 Jun 2020 17:35:11 -0400 Subject: [PATCH 09/28] Simplify property syntax Co-authored-by: Chris Granade --- src/AzureClient/AzureExecutionTarget.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureClient/AzureExecutionTarget.cs b/src/AzureClient/AzureExecutionTarget.cs index 4881049818..67803e3ae1 100644 --- a/src/AzureClient/AzureExecutionTarget.cs +++ b/src/AzureClient/AzureExecutionTarget.cs @@ -12,7 +12,7 @@ internal enum AzureProvider { IonQ, Honeywell, QCI } internal class AzureExecutionTarget { public string TargetName { get; private set; } - public string PackageName { get => $"Microsoft.Quantum.Providers.{GetProvider(TargetName)}"; } + public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetName)}"; public static bool IsValid(string targetName) => GetProvider(targetName) != null; From 521515546c0afcd43c43530d095c71f448469cf0 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Tue, 2 Jun 2020 17:46:24 -0400 Subject: [PATCH 10/28] Add simple tests for AzureExecutionTarget class --- src/AzureClient/Properties/AssemblyInfo.cs | 3 +++ src/Tests/AzureClientTests.cs | 26 ++++++++++++++++++++++ 2 files changed, 29 insertions(+) create mode 100644 src/AzureClient/Properties/AssemblyInfo.cs diff --git a/src/AzureClient/Properties/AssemblyInfo.cs b/src/AzureClient/Properties/AssemblyInfo.cs new file mode 100644 index 0000000000..bb672ac9dd --- /dev/null +++ b/src/AzureClient/Properties/AssemblyInfo.cs @@ -0,0 +1,3 @@ +using System.Runtime.CompilerServices; + +[assembly: InternalsVisibleTo("Tests.IQsharp" + SigningConstants.PUBLIC_KEY)] diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index 425563f5a4..af75ade1b8 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -46,5 +46,31 @@ public void TestTargets() result = azureClient.GetActiveTargetAsync(new MockChannel()).GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Error); } + + [TestMethod] + public void TestAzureExecutionTarget() + { + var targetName = "invalidname"; + var executionTarget = AzureExecutionTarget.Create(targetName); + Assert.IsNull(executionTarget); + + targetName = "ionq.targetname"; + executionTarget = AzureExecutionTarget.Create(targetName); + Assert.IsNotNull(executionTarget); + Assert.AreEqual(executionTarget.TargetName, targetName); + Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.IonQ"); + + targetName = "HonEYWEll.targetname"; + executionTarget = AzureExecutionTarget.Create(targetName); + Assert.IsNotNull(executionTarget); + Assert.AreEqual(executionTarget.TargetName, targetName); + Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.Honeywell"); + + targetName = "qci.target.name.qpu"; + executionTarget = AzureExecutionTarget.Create(targetName); + Assert.IsNotNull(executionTarget); + Assert.AreEqual(executionTarget.TargetName, targetName); + Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.QCI"); + } } } From 5e62413892022541259af6b6080c8f4516752d31 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 3 Jun 2020 09:16:22 -0400 Subject: [PATCH 11/28] Recompile everything for specified execution target --- src/AzureClient/AzureClient.cs | 2 +- .../EntryPoint/EntryPointGenerator.cs | 71 ++++++++++++------- .../EntryPoint/IEntryPointGenerator.cs | 3 +- src/Core/Compiler/CompilerService.cs | 27 ++++--- src/Core/Compiler/ICompilerService.cs | 6 +- src/Core/Loggers/QsharpLogger.cs | 5 +- src/Core/Snippets/ISnippets.cs | 5 ++ src/Core/Snippets/Snippets.cs | 8 +-- src/Core/Workspace/IWorkspace.cs | 5 ++ src/Core/Workspace/Workspace.cs | 9 ++- 10 files changed, 90 insertions(+), 51 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 1b917b1d57..ee17ab66d6 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -195,7 +195,7 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); - var entryPoint = EntryPointGenerator.Generate(operationName); + var entryPoint = EntryPointGenerator.Generate(operationName, ActiveTarget.TargetName); var job = await entryPoint.SubmitAsync(machine, inputParameters); channel.Stdout($"Job {job.Id} submitted successfully."); diff --git a/src/AzureClient/EntryPoint/EntryPointGenerator.cs b/src/AzureClient/EntryPoint/EntryPointGenerator.cs index 97bbae03bb..21a0645648 100644 --- a/src/AzureClient/EntryPoint/EntryPointGenerator.cs +++ b/src/AzureClient/EntryPoint/EntryPointGenerator.cs @@ -23,8 +23,9 @@ internal class EntryPointGenerator : IEntryPointGenerator private ISnippets Snippets { get; } private IReferences References { get; } private ILogger Logger { get; } - private Lazy CompilerMetadata { get; set; } - private AssemblyInfo AssemblyInfo { get; set; } = new AssemblyInfo(null); + private AssemblyInfo WorkspaceAssemblyInfo { get; set; } = new AssemblyInfo(null); + private AssemblyInfo SnippetsAssemblyInfo { get; set; } = new AssemblyInfo(null); + private AssemblyInfo EntryPointAssemblyInfo { get; set; } = new AssemblyInfo(null); public EntryPointGenerator( ICompilerService compiler, @@ -41,55 +42,71 @@ public EntryPointGenerator( Snippets = snippets; References = references; Logger = logger; - CompilerMetadata = new Lazy(LoadCompilerMetadata); - - Workspace.Reloaded += OnWorkspaceReloaded; - References.PackageLoaded += OnGlobalReferencesPackageLoaded; AssemblyLoadContext.Default.Resolving += Resolve; eventService?.TriggerServiceInitialized(this); } - private void OnGlobalReferencesPackageLoaded(object sender, PackageLoadedEventArgs e) => - CompilerMetadata = new Lazy(LoadCompilerMetadata); - - private void OnWorkspaceReloaded(object sender, ReloadedEventArgs e) => - CompilerMetadata = new Lazy(LoadCompilerMetadata); - - private CompilerMetadata LoadCompilerMetadata() => - Workspace.HasErrors - ? References?.CompilerMetadata.WithAssemblies(Snippets.AssemblyInfo) - : References?.CompilerMetadata.WithAssemblies(Snippets.AssemblyInfo, Workspace.AssemblyInfo); - + /// /// Because the assemblies are loaded into memory, we need to provide this method to the AssemblyLoadContext /// such that the Workspace assembly or this assembly is correctly resolved when it is executed for simulation. /// public Assembly Resolve(AssemblyLoadContext context, AssemblyName name) { - if (name.Name == Path.GetFileNameWithoutExtension(AssemblyInfo?.Location)) + if (name.Name == Path.GetFileNameWithoutExtension(EntryPointAssemblyInfo?.Location)) { - return AssemblyInfo.Assembly; + return EntryPointAssemblyInfo.Assembly; } - if (name.Name == Path.GetFileNameWithoutExtension(Snippets?.AssemblyInfo?.Location)) + if (name.Name == Path.GetFileNameWithoutExtension(SnippetsAssemblyInfo?.Location)) { - return Snippets.AssemblyInfo.Assembly; + return SnippetsAssemblyInfo.Assembly; } - else if (name.Name == Path.GetFileNameWithoutExtension(Workspace?.AssemblyInfo?.Location)) + else if (name.Name == Path.GetFileNameWithoutExtension(WorkspaceAssemblyInfo?.Location)) { - return Workspace.AssemblyInfo.Assembly; + return WorkspaceAssemblyInfo.Assembly; } return null; } - public IEntryPoint Generate(string operationName) + public IEntryPoint Generate(string operationName, string executionTarget) { - var operationInfo = OperationResolver.Resolve(operationName); var logger = new QSharpLogger(Logger); - AssemblyInfo = Compiler.BuildEntryPoint(operationInfo, CompilerMetadata.Value, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__.dll")); - var entryPointOperationInfo = AssemblyInfo.Operations.Single(); + var compilerMetadata = References.CompilerMetadata; + + // Clear references to previously-built assemblies + WorkspaceAssemblyInfo = null; + SnippetsAssemblyInfo = null; + EntryPointAssemblyInfo = null; + + // Compile the workspace against the provided execution target + var workspaceFiles = Workspace.SourceFiles.ToArray(); + if (workspaceFiles.Any()) + { + Logger?.LogDebug($"{workspaceFiles.Length} files found in workspace. Compiling."); + WorkspaceAssemblyInfo = Compiler.BuildFiles( + workspaceFiles, compilerMetadata, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__workspace__.dll"), executionTarget); + compilerMetadata = compilerMetadata.WithAssemblies(WorkspaceAssemblyInfo); + } + + // Compile the snippets against the provided execution target + var snippets = Snippets.Items.ToArray(); + if (snippets.Any()) + { + Logger?.LogDebug($"{snippets.Length} items found in snippets. Compiling."); + SnippetsAssemblyInfo = Compiler.BuildSnippets( + snippets, compilerMetadata, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__snippets__.dll"), executionTarget); + compilerMetadata = compilerMetadata.WithAssemblies(SnippetsAssemblyInfo); + } + + // Build the entry point assembly + var operationInfo = OperationResolver.Resolve(operationName); + EntryPointAssemblyInfo = Compiler.BuildEntryPoint( + operationInfo, compilerMetadata, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__.dll"), executionTarget); + var entryPointOperationInfo = EntryPointAssemblyInfo.Operations.Single(); + // Construct the EntryPointInfo<,> object var parameterTypes = entryPointOperationInfo.RoslynParameters.Select(p => p.ParameterType).ToArray(); var typeCount = parameterTypes.Length; Type entryPointInputType = diff --git a/src/AzureClient/EntryPoint/IEntryPointGenerator.cs b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs index f9cd307e4b..69a7bfd996 100644 --- a/src/AzureClient/EntryPoint/IEntryPointGenerator.cs +++ b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs @@ -20,7 +20,8 @@ public interface IEntryPointGenerator /// representing an entry point that wraps the specified operation. /// /// The name of the operation to wrap in an entry point. + /// The intended execution target for the compiled entry point. /// The generated entry point. - public IEntryPoint Generate(string operationName); + public IEntryPoint Generate(string operationName, string executionTarget); } } diff --git a/src/Core/Compiler/CompilerService.cs b/src/Core/Compiler/CompilerService.cs index eccd023871..b22dbf9fb5 100644 --- a/src/Core/Compiler/CompilerService.cs +++ b/src/Core/Compiler/CompilerService.cs @@ -16,6 +16,7 @@ using Microsoft.Quantum.QsCompiler.CompilationBuilder; using Microsoft.Quantum.QsCompiler.CsharpGeneration; using Microsoft.Quantum.QsCompiler.DataTypes; +using Microsoft.Quantum.QsCompiler.ReservedKeywords; using Microsoft.Quantum.QsCompiler.Serialization; using Microsoft.Quantum.QsCompiler.SyntaxProcessing; using Microsoft.Quantum.QsCompiler.SyntaxTree; @@ -70,7 +71,7 @@ private QsCompilation UpdateCompilation(ImmutableDictionary sources } /// - public AssemblyInfo BuildEntryPoint(OperationInfo operation, CompilerMetadata metadatas, QSharpLogger logger, string dllName) + public AssemblyInfo BuildEntryPoint(OperationInfo operation, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null) { var signature = operation.Header.PrintSignature(); var argumentTuple = SyntaxTreeToQsharp.ArgumentTuple(operation.Header.ArgumentTuple, type => type.ToString(), symbolsOnly: true); @@ -87,7 +88,7 @@ public AssemblyInfo BuildEntryPoint(OperationInfo operation, CompilerMetadata me }}"; var sources = new Dictionary() {{ entryPointUri, entryPointSnippet }}.ToImmutableDictionary(); - return BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: true); + return BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: true, executionTarget: executionTarget); } /// @@ -95,28 +96,35 @@ public AssemblyInfo BuildEntryPoint(OperationInfo operation, CompilerMetadata me /// Each snippet code is wrapped inside the 'SNIPPETS_NAMESPACE' namespace and processed as a file /// with the same name as the snippet id. /// - public AssemblyInfo BuildSnippets(Snippet[] snippets, CompilerMetadata metadatas, QSharpLogger logger, string dllName) + public AssemblyInfo BuildSnippets(Snippet[] snippets, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null) { string WrapInNamespace(Snippet s) => $"namespace {Snippets.SNIPPETS_NAMESPACE} {{ open Microsoft.Quantum.Intrinsic; open Microsoft.Quantum.Canon; {s.code} }}"; + // Ignore any @EntryPoint() attributes found in snippets. + logger.ErrorCodesToIgnore.Append(QsCompiler.Diagnostics.ErrorCode.EntryPointInLibrary); + var sources = snippets.ToImmutableDictionary(s => s.Uri, WrapInNamespace); - return BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: false); + var assembly = BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: false, executionTarget: executionTarget); + + logger.ErrorCodesToIgnore.Remove(QsCompiler.Diagnostics.ErrorCode.EntryPointInLibrary); + + return assembly; } /// /// Builds the corresponding .net core assembly from the code in the given files. /// - public AssemblyInfo BuildFiles(string[] files, CompilerMetadata metadatas, QSharpLogger logger, string dllName) + public AssemblyInfo BuildFiles(string[] files, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null) { var sources = ProjectManager.LoadSourceFiles(files, d => logger?.Log(d), ex => logger?.Log(ex)); - return BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: true); + return BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: true, executionTarget: executionTarget); } /// /// Builds the corresponding .net core assembly from the Q# syntax tree. /// - private AssemblyInfo BuildAssembly(ImmutableDictionary sources, CompilerMetadata metadata, QSharpLogger logger, string dllName, bool compileAsExecutable) + private AssemblyInfo BuildAssembly(ImmutableDictionary sources, CompilerMetadata metadata, QSharpLogger logger, string dllName, bool compileAsExecutable, string executionTarget) { logger.LogDebug($"Compiling the following Q# files: {string.Join(",", sources.Keys.Select(f => f.LocalPath))}"); @@ -131,7 +139,10 @@ private AssemblyInfo BuildAssembly(ImmutableDictionary sources, Com foreach (var file in sources.Keys) { var sourceFile = GetFileId(file); - var code = SimulationCode.generate(sourceFile, CodegenContext.Create(qsCompilation.Namespaces)); + var codegenContext = string.IsNullOrEmpty(executionTarget) + ? CodegenContext.Create(qsCompilation.Namespaces) + : CodegenContext.Create(qsCompilation.Namespaces, new Dictionary() { { AssemblyConstants.ExecutionTarget, executionTarget } }); + var code = SimulationCode.generate(sourceFile, codegenContext); var tree = CSharpSyntaxTree.ParseText(code, encoding: UTF8Encoding.UTF8); trees.Add(tree); logger.LogDebug($"Generated the following C# code for {sourceFile.Value}:\n=============\n{code}\n=============\n"); diff --git a/src/Core/Compiler/ICompilerService.cs b/src/Core/Compiler/ICompilerService.cs index 7fde4a2228..76f95f6d0c 100644 --- a/src/Core/Compiler/ICompilerService.cs +++ b/src/Core/Compiler/ICompilerService.cs @@ -16,17 +16,17 @@ public interface ICompilerService /// Builds an executable assembly with an entry point that invokes the Q# operation specified /// by the provided object. /// - AssemblyInfo BuildEntryPoint(OperationInfo operation, CompilerMetadata metadatas, QSharpLogger logger, string dllName); + AssemblyInfo BuildEntryPoint(OperationInfo operation, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null); /// /// Builds the corresponding .net core assembly from the code in the given Q# Snippets. /// - AssemblyInfo BuildSnippets(Snippet[] snippets, CompilerMetadata metadatas, QSharpLogger logger, string dllName); + AssemblyInfo BuildSnippets(Snippet[] snippets, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null); /// /// Builds the corresponding .net core assembly from the code in the given files. /// - AssemblyInfo BuildFiles(string[] files, CompilerMetadata metadatas, QSharpLogger logger, string dllName); + AssemblyInfo BuildFiles(string[] files, CompilerMetadata metadatas, QSharpLogger logger, string dllName, string executionTarget = null); /// /// Returns the names of all declared callables and types. diff --git a/src/Core/Loggers/QsharpLogger.cs b/src/Core/Loggers/QsharpLogger.cs index 247227fc72..6afbb7ebb9 100644 --- a/src/Core/Loggers/QsharpLogger.cs +++ b/src/Core/Loggers/QsharpLogger.cs @@ -22,13 +22,12 @@ public class QSharpLogger : QsCompiler.Diagnostics.LogTracker public List Logs { get; } - public List ErrorCodesToIgnore { get; } + public List ErrorCodesToIgnore { get; } = new List(); - public QSharpLogger(ILogger logger, List errorCodesToIgnore = null) + public QSharpLogger(ILogger logger) { this.Logger = logger; this.Logs = new List(); - this.ErrorCodesToIgnore = errorCodesToIgnore ?? new List(); } public static LogLevel MapLevel(LSP.DiagnosticSeverity original) diff --git a/src/Core/Snippets/ISnippets.cs b/src/Core/Snippets/ISnippets.cs index f8393cb6b3..60e52fb2ea 100644 --- a/src/Core/Snippets/ISnippets.cs +++ b/src/Core/Snippets/ISnippets.cs @@ -55,6 +55,11 @@ public interface ISnippets /// AssemblyInfo AssemblyInfo { get; } + /// + /// The list of currently available snippets. + /// + IEnumerable Items { get; set; } + /// /// Adds or updates a snippet of code. If successful, this updates the AssemblyInfo /// with the new operations found in the Snippet and returns a new Snippet diff --git a/src/Core/Snippets/Snippets.cs b/src/Core/Snippets/Snippets.cs index bf3f0cf189..f6afa05204 100644 --- a/src/Core/Snippets/Snippets.cs +++ b/src/Core/Snippets/Snippets.cs @@ -104,7 +104,7 @@ private void OnWorkspaceReloaded(object sender, ReloadedEventArgs e) /// /// The list of currently available snippets. /// - internal IEnumerable Items { get; set; } + public IEnumerable Items { get; set; } /// /// The list of Q# operations available across all snippets. @@ -144,11 +144,7 @@ public Snippet Compile(string code) if (string.IsNullOrWhiteSpace(code)) throw new ArgumentNullException(nameof(code)); var duration = Stopwatch.StartNew(); - var errorCodesToIgnore = new List() - { - QsCompiler.Diagnostics.ErrorCode.EntryPointInLibrary, // Ignore any @EntryPoint() attributes found in snippets. - }; - var logger = new QSharpLogger(Logger, errorCodesToIgnore); + var logger = new QSharpLogger(Logger); try { diff --git a/src/Core/Workspace/IWorkspace.cs b/src/Core/Workspace/IWorkspace.cs index 4bc0e806ca..f950f1ca79 100644 --- a/src/Core/Workspace/IWorkspace.cs +++ b/src/Core/Workspace/IWorkspace.cs @@ -64,6 +64,11 @@ public interface IWorkspace /// string Root { get; } + /// + /// Gets the source files to be built for this Workspace. + /// + public IEnumerable SourceFiles { get; } + /// /// The folder where the assembly is permanently saved for cache. /// diff --git a/src/Core/Workspace/Workspace.cs b/src/Core/Workspace/Workspace.cs index 2c5d7bad21..daa7516faa 100644 --- a/src/Core/Workspace/Workspace.cs +++ b/src/Core/Workspace/Workspace.cs @@ -77,6 +77,11 @@ public string CacheFolder /// public string Root { get; set; } + /// + /// Gets the source files to be built for this Workspace. + /// + public IEnumerable SourceFiles => Directory.EnumerateFiles(Root, "*.qs", SearchOption.TopDirectoryOnly); + /// /// Information about the assembly built from this Workspace. /// @@ -200,7 +205,7 @@ private bool IsCacheFresh() if (!File.Exists(CacheDll)) return false; var last = File.GetLastWriteTime(CacheDll); - foreach (var f in Directory.EnumerateFiles(Root, "*.qs", SearchOption.AllDirectories)) + foreach (var f in SourceFiles) { if (File.GetLastWriteTime(f) > last) { @@ -230,7 +235,7 @@ public void Reload() if (File.Exists(CacheDll)) { File.Delete(CacheDll); } - files = Directory.EnumerateFiles(Root, "*.qs", SearchOption.TopDirectoryOnly).ToArray(); + files = SourceFiles.ToArray(); if (files.Length > 0) { From 26a79ea771a49e675bf8ce2b727cc1f2f74d015b Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 3 Jun 2020 16:15:13 -0400 Subject: [PATCH 12/28] Add tests and error handling --- src/AzureClient/AzureClient.cs | 56 +++++++++--- src/AzureClient/EntryPoint/EntryPoint.cs | 18 +++- .../EntryPoint/EntryPointGenerator.cs | 44 +++++++--- .../EntryPoint/EntryPointOperationResolver.cs | 32 +++++++ .../EntryPoint/IEntryPointGenerator.cs | 22 ++++- src/AzureClient/IAzureClient.cs | 18 ++++ src/AzureClient/Resources.cs | 9 ++ src/Core/Compiler/CompilerService.cs | 2 +- src/Core/Resolver/OperationResolver.cs | 7 +- src/Tests/AzureClientEntryPointTests.cs | 87 +++++++++++++++++++ src/Tests/SNIPPETS.cs | 9 ++ 11 files changed, 278 insertions(+), 26 deletions(-) create mode 100644 src/AzureClient/EntryPoint/EntryPointOperationResolver.cs create mode 100644 src/Tests/AzureClientEntryPointTests.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index ee17ab66d6..b9755ba326 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -16,6 +16,9 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.Runtime; +using Microsoft.Quantum.Simulation.Common; using Microsoft.Rest.Azure; using Newtonsoft.Json; using Newtonsoft.Json.Linq; @@ -195,20 +198,53 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); - var entryPoint = EntryPointGenerator.Generate(operationName, ActiveTarget.TargetName); - var job = await entryPoint.SubmitAsync(machine, inputParameters); + IEntryPoint? entryPoint = null; + try + { + entryPoint = EntryPointGenerator.Generate(operationName, ActiveTarget.TargetName); + } + catch (UnsupportedOperationException e) + { + channel.Stderr($"{operationName} is not a recognized Q# operation name."); + return AzureClientError.UnrecognizedOperationName.ToExecutionResult(); + } + catch (CompilationErrorsException e) + { + channel.Stderr($"The Q# operation {operationName} could not be compiled as an entry point for job execution."); + foreach (var message in e.Errors) channel.Stderr(message); + return AzureClientError.InvalidEntryPoint.ToExecutionResult(); + } - channel.Stdout($"Job {job.Id} submitted successfully."); + IQuantumMachineJob? job = null; + try + { + job = await entryPoint.SubmitAsync(machine, inputParameters); + channel.Stdout($"Job {job.Id} submitted successfully."); + MostRecentJobId = job.Id; + } + catch (ArgumentException e) + { + channel.Stderr($"Failed to parse all expected parameters for Q# operation {operationName}."); + channel.Stderr(e.Message); + return AzureClientError.JobSubmissionFailed.ToExecutionResult(); + } + catch (Exception e) + { + channel.Stderr($"Failed to submit Q# operation {operationName} for execution."); + channel.Stderr(e.InnerException?.Message ?? e.Message); + return AzureClientError.JobSubmissionFailed.ToExecutionResult(); + } - MostRecentJobId = job.Id; + if (!execute) + { + // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here. + return job.ToJupyterTable().ToExecutionResult(); + } - //if (execute) - //{ - // // TODO: wait for job completion - //} + Logger?.LogDebug($"Waiting for Azure Quantum job {job.Id} to complete..."); - // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here. - return job.ToJupyterTable().ToExecutionResult(); + // TODO: Actually wait for job completion before calling GetJobResultAsync + return await GetJobResultAsync(channel, job.Id); } /// diff --git a/src/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs index be08f7833c..b72f11c66d 100644 --- a/src/AzureClient/EntryPoint/EntryPoint.cs +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -45,8 +45,24 @@ public Task SubmitAsync(IQuantumMachine machine, Dictionary< var parameterValues = new List(); foreach (var parameter in OperationInfo.RoslynParameters) { + if (!inputParameters.ContainsKey(parameter.Name)) + { + throw new ArgumentException($"Required parameter {parameter.Name} was not specified."); + } + + string rawParameterValue = inputParameters[parameter.Name]; + object? parameterValue = null; + try + { + parameterValue = System.Convert.ChangeType(rawParameterValue, parameter.ParameterType); + } + catch (Exception e) + { + throw new ArgumentException($"The value {rawParameterValue} provided for parameter {parameter.Name} could not be converted to the expected type."); + } + parameterTypes.Add(parameter.ParameterType); - parameterValues.Add(System.Convert.ChangeType(inputParameters[parameter.Name], parameter.ParameterType)); + parameterValues.Add(parameterValue); } var entryPointInput = diff --git a/src/AzureClient/EntryPoint/EntryPointGenerator.cs b/src/AzureClient/EntryPoint/EntryPointGenerator.cs index 21a0645648..ef626791cf 100644 --- a/src/AzureClient/EntryPoint/EntryPointGenerator.cs +++ b/src/AzureClient/EntryPoint/EntryPointGenerator.cs @@ -10,6 +10,7 @@ using System.Runtime.Loader; using Microsoft.Extensions.Logging; using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.Simulation.Common; using Microsoft.Quantum.Simulation.Core; namespace Microsoft.Quantum.IQSharp.AzureClient @@ -18,18 +19,16 @@ namespace Microsoft.Quantum.IQSharp.AzureClient internal class EntryPointGenerator : IEntryPointGenerator { private ICompilerService Compiler { get; } - private IOperationResolver OperationResolver { get; } + private ILogger Logger { get; } private IWorkspace Workspace { get; } private ISnippets Snippets { get; } - private IReferences References { get; } - private ILogger Logger { get; } - private AssemblyInfo WorkspaceAssemblyInfo { get; set; } = new AssemblyInfo(null); - private AssemblyInfo SnippetsAssemblyInfo { get; set; } = new AssemblyInfo(null); - private AssemblyInfo EntryPointAssemblyInfo { get; set; } = new AssemblyInfo(null); + public IReferences References { get; } + public AssemblyInfo WorkspaceAssemblyInfo { get; set; } = new AssemblyInfo(null); + public AssemblyInfo SnippetsAssemblyInfo { get; set; } = new AssemblyInfo(null); + public AssemblyInfo EntryPointAssemblyInfo { get; set; } = new AssemblyInfo(null); public EntryPointGenerator( ICompilerService compiler, - IOperationResolver operationResolver, IWorkspace workspace, ISnippets snippets, IReferences references, @@ -37,7 +36,6 @@ public EntryPointGenerator( IEventService eventService) { Compiler = compiler; - OperationResolver = operationResolver; Workspace = workspace; Snippets = snippets; References = references; @@ -70,8 +68,10 @@ public Assembly Resolve(AssemblyLoadContext context, AssemblyName name) return null; } - public IEntryPoint Generate(string operationName, string executionTarget) + public IEntryPoint Generate(string operationName, string? executionTarget) { + Logger?.LogDebug($"Generating entry point: operationName={operationName}, executionTarget={executionTarget}"); + var logger = new QSharpLogger(Logger); var compilerMetadata = References.CompilerMetadata; @@ -87,6 +87,12 @@ public IEntryPoint Generate(string operationName, string executionTarget) Logger?.LogDebug($"{workspaceFiles.Length} files found in workspace. Compiling."); WorkspaceAssemblyInfo = Compiler.BuildFiles( workspaceFiles, compilerMetadata, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__workspace__.dll"), executionTarget); + if (WorkspaceAssemblyInfo == null || logger.HasErrors) + { + Logger?.LogError($"Error compiling workspace."); + throw new CompilationErrorsException(logger.Errors.ToArray()); + } + compilerMetadata = compilerMetadata.WithAssemblies(WorkspaceAssemblyInfo); } @@ -97,13 +103,31 @@ public IEntryPoint Generate(string operationName, string executionTarget) Logger?.LogDebug($"{snippets.Length} items found in snippets. Compiling."); SnippetsAssemblyInfo = Compiler.BuildSnippets( snippets, compilerMetadata, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__snippets__.dll"), executionTarget); + if (SnippetsAssemblyInfo == null || logger.HasErrors) + { + Logger?.LogError($"Error compiling snippets."); + throw new CompilationErrorsException(logger.Errors.ToArray()); + } + compilerMetadata = compilerMetadata.WithAssemblies(SnippetsAssemblyInfo); } // Build the entry point assembly - var operationInfo = OperationResolver.Resolve(operationName); + var operationInfo = new EntryPointOperationResolver(this).Resolve(operationName); + if (operationInfo == null) + { + Logger?.LogError($"{operationName} is not a recognized Q# operation name."); + throw new UnsupportedOperationException(operationName); + } + EntryPointAssemblyInfo = Compiler.BuildEntryPoint( operationInfo, compilerMetadata, logger, Path.Combine(Workspace.CacheFolder, "__entrypoint__.dll"), executionTarget); + if (EntryPointAssemblyInfo == null || logger.HasErrors) + { + Logger?.LogError($"Error compiling entry point for operation {operationName}."); + throw new CompilationErrorsException(logger.Errors.ToArray()); + } + var entryPointOperationInfo = EntryPointAssemblyInfo.Operations.Single(); // Construct the EntryPointInfo<,> object diff --git a/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs b/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs new file mode 100644 index 0000000000..1bacca1af6 --- /dev/null +++ b/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Text; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class EntryPointOperationResolver : IOperationResolver + { + private IEntryPointGenerator EntryPointGenerator { get; } + + public EntryPointOperationResolver(IEntryPointGenerator entryPointGenerator) => + EntryPointGenerator = entryPointGenerator; + + public OperationInfo Resolve(string name) => OperationResolver.ResolveFromAssemblies(name, RelevantAssemblies()); + + private IEnumerable RelevantAssemblies() + { + if (EntryPointGenerator.SnippetsAssemblyInfo != null) yield return EntryPointGenerator.SnippetsAssemblyInfo; + if (EntryPointGenerator.WorkspaceAssemblyInfo != null) yield return EntryPointGenerator.WorkspaceAssemblyInfo; + + foreach (var asm in EntryPointGenerator.References.Assemblies) + { + yield return asm; + } + } + } +} diff --git a/src/AzureClient/EntryPoint/IEntryPointGenerator.cs b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs index 69a7bfd996..1428284266 100644 --- a/src/AzureClient/EntryPoint/IEntryPointGenerator.cs +++ b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs @@ -15,6 +15,26 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public interface IEntryPointGenerator { + /// + /// Gets the compiled workspace assembly for the most recently-generated entry point. + /// + public AssemblyInfo WorkspaceAssemblyInfo { get; } + + /// + /// Gets the compiled snippets assembly for the most recently-generated entry point. + /// + public AssemblyInfo SnippetsAssemblyInfo { get; } + + /// + /// Gets the compiled entry point assembly for the most recently-generated entry point. + /// + public AssemblyInfo EntryPointAssemblyInfo { get; } + + /// + /// Gets the references used for compilation of the entry point assembly. + /// + public IReferences References { get; } + /// /// Compiles an assembly and returns the object /// representing an entry point that wraps the specified operation. @@ -22,6 +42,6 @@ public interface IEntryPointGenerator /// The name of the operation to wrap in an entry point. /// The intended execution target for the compiled entry point. /// The generated entry point. - public IEntryPoint Generate(string operationName, string executionTarget); + public IEntryPoint Generate(string operationName, string? executionTarget); } } diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index b87b98d9c7..952c28d796 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -57,6 +57,24 @@ public enum AzureClientError [Description(Resources.AzureClientErrorNoOperationName)] NoOperationName, + /// + /// The specified Q# operation name is not recognized. + /// + [Description(Resources.AzureClientErrorUnrecognizedOperationName)] + UnrecognizedOperationName, + + /// + /// The specified Q# operation cannot be used as an entry point. + /// + [Description(Resources.AzureClientErrorInvalidEntryPoint)] + InvalidEntryPoint, + + /// + /// The Azure Quantum job submission failed. + /// + [Description(Resources.AzureClientErrorJobSubmissionFailed)] + JobSubmissionFailed, + /// /// Authentication with the Azure service failed. /// diff --git a/src/AzureClient/Resources.cs b/src/AzureClient/Resources.cs index 1fedce95f2..a85a3cf463 100644 --- a/src/AzureClient/Resources.cs +++ b/src/AzureClient/Resources.cs @@ -28,6 +28,15 @@ internal static class Resources public const string AzureClientErrorNoOperationName = "No Q# operation name was specified for Azure Quantum job submission."; + public const string AzureClientErrorUnrecognizedOperationName = + "The specified Q# operation name was not recognized."; + + public const string AzureClientErrorInvalidEntryPoint = + "The specified Q# operation cannot be used as an entry point for Azure Quantum job submission."; + + public const string AzureClientErrorJobSubmissionFailed = + "Failed to submit the job to the Azure Quantum workspace."; + public const string AzureClientErrorAuthenticationFailed = "Failed to authenticate to the specified Azure Quantum workspace."; diff --git a/src/Core/Compiler/CompilerService.cs b/src/Core/Compiler/CompilerService.cs index b22dbf9fb5..6ac40cdd4b 100644 --- a/src/Core/Compiler/CompilerService.cs +++ b/src/Core/Compiler/CompilerService.cs @@ -102,7 +102,7 @@ string WrapInNamespace(Snippet s) => $"namespace {Snippets.SNIPPETS_NAMESPACE} {{ open Microsoft.Quantum.Intrinsic; open Microsoft.Quantum.Canon; {s.code} }}"; // Ignore any @EntryPoint() attributes found in snippets. - logger.ErrorCodesToIgnore.Append(QsCompiler.Diagnostics.ErrorCode.EntryPointInLibrary); + logger.ErrorCodesToIgnore.Add(QsCompiler.Diagnostics.ErrorCode.EntryPointInLibrary); var sources = snippets.ToImmutableDictionary(s => s.Uri, WrapInNamespace); var assembly = BuildAssembly(sources, metadatas, logger, dllName, compileAsExecutable: false, executionTarget: executionTarget); diff --git a/src/Core/Resolver/OperationResolver.cs b/src/Core/Resolver/OperationResolver.cs index 8be04f2933..2bf52f5b16 100644 --- a/src/Core/Resolver/OperationResolver.cs +++ b/src/Core/Resolver/OperationResolver.cs @@ -71,11 +71,12 @@ private IEnumerable RelevantAssemblies() /// Symbol names without a dot are resolved to the first symbol /// whose base name matches the given name. /// - public OperationInfo Resolve(string name) + public OperationInfo Resolve(string name) => ResolveFromAssemblies(name, RelevantAssemblies()); + + public static OperationInfo ResolveFromAssemblies(string name, IEnumerable assemblies) { var isQualified = name.Contains('.'); - var relevant = RelevantAssemblies(); - foreach (var operation in relevant.SelectMany(asm => asm.Operations)) + foreach (var operation in assemblies.SelectMany(asm => asm.Operations)) { if (name == (isQualified ? operation.FullName : operation.Header.QualifiedName.Name.Value)) { diff --git a/src/Tests/AzureClientEntryPointTests.cs b/src/Tests/AzureClientEntryPointTests.cs new file mode 100644 index 0000000000..42249f7e00 --- /dev/null +++ b/src/Tests/AzureClientEntryPointTests.cs @@ -0,0 +1,87 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Extensions.DependencyInjection; +using Microsoft.Quantum.IQSharp; +using Microsoft.Quantum.IQSharp.AzureClient; +using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.Simulation.Common; +using Microsoft.VisualStudio.TestTools.UnitTesting; + +namespace Tests.IQSharp +{ + [TestClass] + public class AzureClientEntryPointTests + { + private IEntryPointGenerator Init(string workspace, IEnumerable? codeSnippets = null) + { + var services = Startup.CreateServiceProvider(workspace); + + if (codeSnippets != null) + { + var snippets = services.GetService(); + snippets.Items = codeSnippets.Select(codeSnippet => new Snippet() { code = codeSnippet }); + } + + return services.GetService(); + } + + [TestMethod] + public async Task FromSnippet() + { + var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.HelloQ }); + var entryPoint = entryPointGenerator.Generate("HelloQ", null); + Assert.IsNotNull(entryPoint); + } + + [TestMethod] + public async Task FromBrokenSnippet() + { + var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.TwoErrors }); + Assert.ThrowsException(() => entryPointGenerator.Generate("TwoErrors", null)); + } + + [TestMethod] + public async Task FromWorkspace() + { + var entryPointGenerator = Init("Workspace"); + var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null); + Assert.IsNotNull(entryPoint); + } + + [TestMethod] + public async Task FromBrokenWorkspace() + { + var entryPointGenerator = Init("Workspace.Broken"); + Assert.ThrowsException(() => entryPointGenerator.Generate("Tests.qss.HelloAgain", null)); + } + + [TestMethod] + public async Task FromSnippetDependsOnWorkspace() + { + var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.DependsOnWorkspace }); + var entryPoint = entryPointGenerator.Generate("DependsOnWorkspace", null); + Assert.IsNotNull(entryPoint); + } + + [TestMethod] + public async Task InvalidOperationName() + { + var entryPointGenerator = Init("Workspace"); + Assert.ThrowsException(() => entryPointGenerator.Generate("InvalidOperationName", null)); + } + + [TestMethod] + public async Task InvalidEntryPointOperation() + { + var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.InvalidEntryPoint }); + Assert.ThrowsException(() => entryPointGenerator.Generate("InvalidEntryPoint", null)); + } + } +} diff --git a/src/Tests/SNIPPETS.cs b/src/Tests/SNIPPETS.cs index a16f637329..bfef89e38d 100644 --- a/src/Tests/SNIPPETS.cs +++ b/src/Tests/SNIPPETS.cs @@ -190,6 +190,15 @@ operation InvalidFunctor(q: Qubit) : Unit { } "; + public static string InvalidEntryPoint = +@" + /// # Summary + /// This script has an operation that is not valid to be marked as an entry point. + operation InvalidEntryPoint(q : Qubit) : Unit { + H(q); + } +"; + public static string Reverse = @" /// # Summary From 69a4fe680324d83081df4b9ac42a8c5c8e0b8ebc Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 3 Jun 2020 16:21:59 -0400 Subject: [PATCH 13/28] Improve variable names --- src/AzureClient/Magic/ExecuteMagic.cs | 6 +++--- src/AzureClient/Magic/SubmitMagic.cs | 6 +++--- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/src/AzureClient/Magic/ExecuteMagic.cs b/src/AzureClient/Magic/ExecuteMagic.cs index 8a2ecd45ec..a5f317688d 100644 --- a/src/AzureClient/Magic/ExecuteMagic.cs +++ b/src/AzureClient/Magic/ExecuteMagic.cs @@ -62,13 +62,13 @@ public override async Task RunAsync(string input, IChannel chan var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); - var remainingParameters = new Dictionary(); + var decodedParameters = new Dictionary(); foreach (var key in inputParameters.Keys) { - remainingParameters[key] = inputParameters.DecodeParameter(key); + decodedParameters[key] = inputParameters.DecodeParameter(key); } - return await AzureClient.ExecuteJobAsync(channel, operationName, remainingParameters); + return await AzureClient.ExecuteJobAsync(channel, operationName, decodedParameters); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index 835acff7bb..cb646a9903 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -59,13 +59,13 @@ public override async Task RunAsync(string input, IChannel chan var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); - var remainingParameters = new Dictionary(); + var decodedParameters = new Dictionary(); foreach (var key in inputParameters.Keys) { - remainingParameters[key] = inputParameters.DecodeParameter(key); + decodedParameters[key] = inputParameters.DecodeParameter(key); } - return await AzureClient.SubmitJobAsync(channel, operationName, remainingParameters); + return await AzureClient.SubmitJobAsync(channel, operationName, decodedParameters); } } } \ No newline at end of file From c63a006bc1264f468bd02d7be2fb9a4a68cfe01b Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 3 Jun 2020 16:59:20 -0400 Subject: [PATCH 14/28] Add status message while loading provider package --- src/AzureClient/AzureClient.cs | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 1f01dc737f..ee639ec3e8 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -277,6 +277,8 @@ public async Task SetActiveTargetAsync( // Set the active target and load the package. ActiveTarget = executionTarget; + + channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies..."); await references.AddPackage(ActiveTarget.PackageName); return $"Active target is now {ActiveTarget.TargetName}".ToExecutionResult(); From 43c07ae1e4ebf59f6cdd22467b76687748c25dd4 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 4 Jun 2020 20:35:54 -0400 Subject: [PATCH 15/28] Add documentation to AzureExecutionTarget.GetProvider --- src/AzureClient/AzureExecutionTarget.cs | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/AzureClient/AzureExecutionTarget.cs b/src/AzureClient/AzureExecutionTarget.cs index 67803e3ae1..2f0d5c89d1 100644 --- a/src/AzureClient/AzureExecutionTarget.cs +++ b/src/AzureClient/AzureExecutionTarget.cs @@ -21,6 +21,15 @@ internal class AzureExecutionTarget ? new AzureExecutionTarget() { TargetName = targetName } : null; + /// + /// Gets the Azure Quantum provider corresponding to the given execution target. + /// + /// The Azure Quantum execution target name. + /// The enum value representing the provider. + /// + /// Valid target names are structured as "provider.target". + /// For example, "ionq.simulator" or "honeywell.qpu". + /// private static AzureProvider? GetProvider(string targetName) { var parts = targetName.Split('.', 2); From 52c481d6af79e75226c8b08f67357ec14dcdbf96 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 5 Jun 2020 08:43:52 -0400 Subject: [PATCH 16/28] Extend tests to call EntryPoint.SubmitAsync --- src/AzureClient/EntryPoint/EntryPoint.cs | 10 +- src/Tests/AzureClientEntryPointTests.cs | 117 ++++++++++++++++++++++- 2 files changed, 122 insertions(+), 5 deletions(-) diff --git a/src/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs index b72f11c66d..f07fed4216 100644 --- a/src/AzureClient/EntryPoint/EntryPoint.cs +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -70,7 +70,15 @@ public Task SubmitAsync(IQuantumMachine machine, Dictionary< parameterValues.Count == 1 ? parameterValues.Single() : InputType.GetConstructor(parameterTypes.ToArray()).Invoke(parameterValues.ToArray()); - var submitMethod = typeof(IQuantumMachine).GetMethod("SubmitAsync").MakeGenericMethod(new Type[] { InputType, OutputType }); + var submitMethod = typeof(IQuantumMachine) + .GetMethods() + .Single(method => + method.Name == "SubmitAsync" + && method.IsGenericMethodDefinition + && method.GetParameters().Length == 2 + && method.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == EntryPointInfo.GetType().GetGenericTypeDefinition() + && method.GetParameters()[1].ParameterType.IsGenericMethodParameter) + .MakeGenericMethod(new Type[] { InputType, OutputType }); var submitParameters = new object[] { EntryPointInfo, entryPointInput }; return submitMethod.Invoke(machine, submitParameters) as Task; } diff --git a/src/Tests/AzureClientEntryPointTests.cs b/src/Tests/AzureClientEntryPointTests.cs index 42249f7e00..e9b3bbbe8e 100644 --- a/src/Tests/AzureClientEntryPointTests.cs +++ b/src/Tests/AzureClientEntryPointTests.cs @@ -6,12 +6,15 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Extensions.DependencyInjection; using Microsoft.Quantum.IQSharp; using Microsoft.Quantum.IQSharp.AzureClient; using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.Runtime; using Microsoft.Quantum.Simulation.Common; +using Microsoft.Quantum.Simulation.Core; using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Tests.IQSharp @@ -38,13 +41,17 @@ public async Task FromSnippet() var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.HelloQ }); var entryPoint = entryPointGenerator.Generate("HelloQ", null); Assert.IsNotNull(entryPoint); + + var job = await entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary()); + Assert.IsNotNull(job); } [TestMethod] public async Task FromBrokenSnippet() { var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.TwoErrors }); - Assert.ThrowsException(() => entryPointGenerator.Generate("TwoErrors", null)); + Assert.ThrowsException(() => + entryPointGenerator.Generate("TwoErrors", null)); } [TestMethod] @@ -53,13 +60,39 @@ public async Task FromWorkspace() var entryPointGenerator = Init("Workspace"); var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null); Assert.IsNotNull(entryPoint); + + var job = await entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary() { { "count", "2" }, { "name", "test" } } ); + Assert.IsNotNull(job); + } + + [TestMethod] + public async Task FromWorkspaceMissingArgument() + { + var entryPointGenerator = Init("Workspace"); + var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null); + Assert.IsNotNull(entryPoint); + + Assert.ThrowsException(() => + entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary() { { "count", "2" } })); + } + + [TestMethod] + public async Task FromWorkspaceIncorrectArgumentType() + { + var entryPointGenerator = Init("Workspace"); + var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null); + Assert.IsNotNull(entryPoint); + + Assert.ThrowsException(() => + entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary() { { "count", "NaN" }, { "name", "test" } })); } [TestMethod] public async Task FromBrokenWorkspace() { var entryPointGenerator = Init("Workspace.Broken"); - Assert.ThrowsException(() => entryPointGenerator.Generate("Tests.qss.HelloAgain", null)); + Assert.ThrowsException(() => + entryPointGenerator.Generate("Tests.qss.HelloAgain", null)); } [TestMethod] @@ -68,20 +101,96 @@ public async Task FromSnippetDependsOnWorkspace() var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.DependsOnWorkspace }); var entryPoint = entryPointGenerator.Generate("DependsOnWorkspace", null); Assert.IsNotNull(entryPoint); + + var job = await entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary()); + Assert.IsNotNull(job); } [TestMethod] public async Task InvalidOperationName() { var entryPointGenerator = Init("Workspace"); - Assert.ThrowsException(() => entryPointGenerator.Generate("InvalidOperationName", null)); + Assert.ThrowsException(() => + entryPointGenerator.Generate("InvalidOperationName", null)); } [TestMethod] public async Task InvalidEntryPointOperation() { var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.InvalidEntryPoint }); - Assert.ThrowsException(() => entryPointGenerator.Generate("InvalidEntryPoint", null)); + Assert.ThrowsException(() => + entryPointGenerator.Generate("InvalidEntryPoint", null)); + } + } + + public class MockQuantumMachine : IQuantumMachine + { + public string ProviderId => throw new NotImplementedException(); + + public string Target => throw new NotImplementedException(); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input) + => ExecuteAsync(info, input, null as IQuantumMachineSubmissionContext); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext) + => ExecuteAsync(info, input, submissionContext, null as IQuantumMachine.ConfigureJob); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachine.ConfigureJob configureJobCallback) + => ExecuteAsync(info, input, submissionContext, null, configureJobCallback); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext executionContext) + => ExecuteAsync(info, input, null as IQuantumMachineExecutionContext); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext executionContext, IQuantumMachine.ConfigureJob configureJobCallback) + => ExecuteAsync(info, input, executionContext, null as IQuantumMachine.ConfigureJob); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachineExecutionContext executionContext) + => ExecuteAsync(info, input, submissionContext, executionContext, null); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachineExecutionContext executionContext, IQuantumMachine.ConfigureJob configureJobCallback) + { + throw new NotImplementedException(); + } + + public Task SubmitAsync(EntryPointInfo info, TInput input) + => SubmitAsync(info, input, null); + + public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext) + => SubmitAsync(info, input, null, null); + + public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachine.ConfigureJob configureJobCallback) + { + return Task.FromResult(new MockQuantumMachineJob() as IQuantumMachineJob); + } + + public (bool IsValid, string Message) Validate(EntryPointInfo info, TInput input) + { + throw new NotImplementedException(); + } + } + + public class MockQuantumMachineJob : IQuantumMachineJob + { + public bool Failed => throw new NotImplementedException(); + + public string Id => throw new NotImplementedException(); + + public bool InProgress => throw new NotImplementedException(); + + public string Status => throw new NotImplementedException(); + + public bool Succeeded => throw new NotImplementedException(); + + public Uri Uri => throw new NotImplementedException(); + + public Task CancelAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); + } + + public Task RefreshAsync(CancellationToken cancellationToken = default) + { + throw new NotImplementedException(); } } } From a470662463a084e8a5e0cfadb741f6b248ce115c Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 5 Jun 2020 09:11:38 -0400 Subject: [PATCH 17/28] Wait for job completion on %azure.execute --- src/AzureClient/AzureClient.cs | 15 ++++++++++++--- src/AzureClient/Extensions.cs | 21 +++++++++++---------- 2 files changed, 23 insertions(+), 13 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 3ca03a3968..519b8e2543 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -241,7 +241,16 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st return job.ToJupyterTable().ToExecutionResult(); } - Logger?.LogDebug($"Waiting for Azure Quantum job {job.Id} to complete..."); + channel.Stdout($"Waiting for Azure Quantum job to complete..."); + + CloudJob? cloudJob = null; + do + { + await Task.Delay(TimeSpan.FromSeconds(5)); + cloudJob = (await GetJobStatusAsync(channel, job.Id)).Output as CloudJob; + channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob.Status}"); + } + while (cloudJob != null && !cloudJob.Succeeded && !cloudJob.Failed); // TODO: Actually wait for job completion before calling GetJobResultAsync return await GetJobResultAsync(channel, job.Id); @@ -387,7 +396,7 @@ public async Task GetJobStatusAsync(IChannel channel, string jo } // TODO: Add encoder for CloudJob which calls ToJupyterTable() for display. - return job.Details.ToExecutionResult(); + return job.ToExecutionResult(); } /// @@ -407,7 +416,7 @@ public async Task GetJobListAsync(IChannel channel) } // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return jobs.Select(job => job.Details).ToJupyterTable().ToExecutionResult(); + return jobs.ToJupyterTable().ToExecutionResult(); } } } diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 8bc768ec7f..0727e8aaca 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -8,6 +8,7 @@ using System.ComponentModel; using System.Linq; using System.Threading.Tasks; +using Microsoft.Azure.Quantum; using Microsoft.Azure.Quantum.Client; using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Extensions.DependencyInjection; @@ -67,19 +68,19 @@ public static string ToDescription(this AzureClientError azureClientError) public static async Task ToExecutionResult(this Task task) => (await task).ToExecutionResult(); - internal static Table ToJupyterTable(this JobDetails jobDetails) => - new List { jobDetails }.ToJupyterTable(); + internal static Table ToJupyterTable(this CloudJob cloudJob) => + new List { cloudJob }.ToJupyterTable(); - internal static Table ToJupyterTable(this IEnumerable jobsList) => - new Table + internal static Table ToJupyterTable(this IEnumerable jobsList) => + new Table { - Columns = new List<(string, Func)> + Columns = new List<(string, Func)> { - ("JobId", jobDetails => jobDetails.Id), - ("JobName", jobDetails => jobDetails.Name), - ("JobStatus", jobDetails => jobDetails.Status), - ("Provider", jobDetails => jobDetails.ProviderId), - ("Target", jobDetails => jobDetails.Target), + ("JobId", cloudJob => cloudJob.Id), + ("JobName", cloudJob => cloudJob.Details.Name), + ("JobStatus", cloudJob => cloudJob.Status), + ("Provider", cloudJob => cloudJob.Details.ProviderId), + ("Target", cloudJob => cloudJob.Details.Target), }, Rows = jobsList.ToList() }; From 354d826069d755e0f1e6b597965bed2967b8a211 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 5 Jun 2020 09:13:23 -0400 Subject: [PATCH 18/28] Update comment --- src/AzureClient/AzureClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 519b8e2543..620d313fa3 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -246,13 +246,13 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st CloudJob? cloudJob = null; do { + // TODO: Allow Jupyter kernel interrupt to break out of this loop await Task.Delay(TimeSpan.FromSeconds(5)); cloudJob = (await GetJobStatusAsync(channel, job.Id)).Output as CloudJob; channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob.Status}"); } while (cloudJob != null && !cloudJob.Succeeded && !cloudJob.Failed); - // TODO: Actually wait for job completion before calling GetJobResultAsync return await GetJobResultAsync(channel, job.Id); } From 9041701fb8dba97996b0c0739050d4224d1efca8 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 5 Jun 2020 10:42:17 -0400 Subject: [PATCH 19/28] Add timeout for %azure.execute --- src/AzureClient/AzureClient.cs | 26 +++++++++++++++++--------- 1 file changed, 17 insertions(+), 9 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 620d313fa3..e81338e1e6 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Quantum; using Microsoft.Azure.Quantum.Client; @@ -241,17 +242,24 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st return job.ToJupyterTable().ToExecutionResult(); } - channel.Stdout($"Waiting for Azure Quantum job to complete..."); - - CloudJob? cloudJob = null; - do + var timeoutInSeconds = 30; + channel.Stdout($"Waiting up to {timeoutInSeconds} seconds for Azure Quantum job to complete..."); + + using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30))) { - // TODO: Allow Jupyter kernel interrupt to break out of this loop - await Task.Delay(TimeSpan.FromSeconds(5)); - cloudJob = (await GetJobStatusAsync(channel, job.Id)).Output as CloudJob; - channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob.Status}"); + CloudJob? cloudJob = null; + do + { + // TODO: Once jupyter-core supports interrupt requests (https://github.com/microsoft/jupyter-core/issues/55), + // handle Jupyter kernel interrupt here and break out of this loop + var pollingIntervalInSeconds = 5; + await Task.Delay(TimeSpan.FromSeconds(pollingIntervalInSeconds), cts.Token); + if (cts.IsCancellationRequested) break; + cloudJob = (await GetJobStatusAsync(channel, job.Id)).Output as CloudJob; + channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob.Status}"); + } + while (cloudJob != null && !cloudJob.Succeeded && !cloudJob.Failed); } - while (cloudJob != null && !cloudJob.Succeeded && !cloudJob.Failed); return await GetJobResultAsync(channel, job.Id); } From 0300ecda6f9e9a5b3be8a02602994fccca660211 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 5 Jun 2020 10:51:42 -0400 Subject: [PATCH 20/28] Minor fixes in AzureClient --- src/AzureClient/AzureClient.cs | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index e81338e1e6..859f405408 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -253,7 +253,7 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st // TODO: Once jupyter-core supports interrupt requests (https://github.com/microsoft/jupyter-core/issues/55), // handle Jupyter kernel interrupt here and break out of this loop var pollingIntervalInSeconds = 5; - await Task.Delay(TimeSpan.FromSeconds(pollingIntervalInSeconds), cts.Token); + await Task.Delay(TimeSpan.FromSeconds(pollingIntervalInSeconds)); if (cts.IsCancellationRequested) break; cloudJob = (await GetJobStatusAsync(channel, job.Id)).Output as CloudJob; channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob.Status}"); @@ -403,8 +403,8 @@ public async Task GetJobStatusAsync(IChannel channel, string jo return AzureClientError.JobNotFound.ToExecutionResult(); } - // TODO: Add encoder for CloudJob which calls ToJupyterTable() for display. - return job.ToExecutionResult(); + // TODO: Add encoder for CloudJob rather than calling ToJupyterTable() here directly. + return job.ToJupyterTable().ToExecutionResult(); } /// From 8ce9c6369f0b677ffdbb11b813019b6a396bd1c6 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 5 Jun 2020 11:00:15 -0400 Subject: [PATCH 21/28] Minor formatting and comment tweaks --- src/AzureClient/EntryPoint/EntryPoint.cs | 2 ++ src/AzureClient/EntryPoint/EntryPointGenerator.cs | 4 ++-- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs index f07fed4216..49108f3022 100644 --- a/src/AzureClient/EntryPoint/EntryPoint.cs +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -70,6 +70,8 @@ public Task SubmitAsync(IQuantumMachine machine, Dictionary< parameterValues.Count == 1 ? parameterValues.Single() : InputType.GetConstructor(parameterTypes.ToArray()).Invoke(parameterValues.ToArray()); + // Find and invoke the method on IQuantumMachine that is declared as: + // Task SubmitAsync(EntryPointInfo info, TInput input) var submitMethod = typeof(IQuantumMachine) .GetMethods() .Single(method => diff --git a/src/AzureClient/EntryPoint/EntryPointGenerator.cs b/src/AzureClient/EntryPoint/EntryPointGenerator.cs index ef626791cf..0cf9ae3946 100644 --- a/src/AzureClient/EntryPoint/EntryPointGenerator.cs +++ b/src/AzureClient/EntryPoint/EntryPointGenerator.cs @@ -140,8 +140,8 @@ public IEntryPoint Generate(string operationName, string? executionTarget) Type entryPointOutputType = entryPointOperationInfo.ReturnType; Type entryPointInfoType = typeof(EntryPointInfo<,>).MakeGenericType(new Type[] { entryPointInputType, entryPointOutputType }); - var entryPointInfo = entryPointInfoType.GetConstructor( - new Type[] { typeof(Type) }).Invoke(new object[] { entryPointOperationInfo.RoslynType }); + var entryPointInfo = entryPointInfoType.GetConstructor(new Type[] { typeof(Type) }) + .Invoke(new object[] { entryPointOperationInfo.RoslynType }); return new EntryPoint(entryPointInfo, entryPointInputType, entryPointOutputType, entryPointOperationInfo); } From e3e7bccb842ed48198cf211eb5a7354b3339d531 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 5 Jun 2020 11:03:03 -0400 Subject: [PATCH 22/28] Style improvements in test code --- src/Tests/AzureClientEntryPointTests.cs | 22 +++++----------------- 1 file changed, 5 insertions(+), 17 deletions(-) diff --git a/src/Tests/AzureClientEntryPointTests.cs b/src/Tests/AzureClientEntryPointTests.cs index e9b3bbbe8e..3aba6eb27a 100644 --- a/src/Tests/AzureClientEntryPointTests.cs +++ b/src/Tests/AzureClientEntryPointTests.cs @@ -148,9 +148,7 @@ public Task> ExecuteAsync(EntryP => ExecuteAsync(info, input, submissionContext, executionContext, null); public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachineExecutionContext executionContext, IQuantumMachine.ConfigureJob configureJobCallback) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); public Task SubmitAsync(EntryPointInfo info, TInput input) => SubmitAsync(info, input, null); @@ -159,14 +157,10 @@ public Task SubmitAsync(EntryPointInfo SubmitAsync(info, input, null, null); public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachine.ConfigureJob configureJobCallback) - { - return Task.FromResult(new MockQuantumMachineJob() as IQuantumMachineJob); - } + => Task.FromResult(new MockQuantumMachineJob() as IQuantumMachineJob); public (bool IsValid, string Message) Validate(EntryPointInfo info, TInput input) - { - throw new NotImplementedException(); - } + => throw new NotImplementedException(); } public class MockQuantumMachineJob : IQuantumMachineJob @@ -183,14 +177,8 @@ public class MockQuantumMachineJob : IQuantumMachineJob public Uri Uri => throw new NotImplementedException(); - public Task CancelAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public Task CancelAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); - public Task RefreshAsync(CancellationToken cancellationToken = default) - { - throw new NotImplementedException(); - } + public Task RefreshAsync(CancellationToken cancellationToken = default) => throw new NotImplementedException(); } } From f8077238fb2511ed637e7b525ae55e1394ad8b8e Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 5 Jun 2020 13:32:37 -0400 Subject: [PATCH 23/28] More consistent handling of job objects --- src/AzureClient/AzureClient.cs | 24 ++++++++++-------------- 1 file changed, 10 insertions(+), 14 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 859f405408..8d875ce234 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -7,7 +7,6 @@ using System.Collections.Generic; using System.IO; using System.Linq; -using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Quantum; using Microsoft.Azure.Quantum.Client; @@ -18,7 +17,6 @@ using Microsoft.Identity.Client.Extensions.Msal; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Common; -using Microsoft.Quantum.Runtime; using Microsoft.Quantum.Simulation.Common; using Microsoft.Rest.Azure; using Newtonsoft.Json; @@ -216,10 +214,9 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st return AzureClientError.InvalidEntryPoint.ToExecutionResult(); } - IQuantumMachineJob? job = null; try { - job = await entryPoint.SubmitAsync(machine, inputParameters); + var job = await entryPoint.SubmitAsync(machine, inputParameters); channel.Stdout($"Job {job.Id} submitted successfully."); MostRecentJobId = job.Id; } @@ -238,14 +235,13 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st if (!execute) { - // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here. - return job.ToJupyterTable().ToExecutionResult(); + return await GetJobStatusAsync(channel, MostRecentJobId); } var timeoutInSeconds = 30; channel.Stdout($"Waiting up to {timeoutInSeconds} seconds for Azure Quantum job to complete..."); - using (var cts = new CancellationTokenSource(TimeSpan.FromSeconds(30))) + using (var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30))) { CloudJob? cloudJob = null; do @@ -255,13 +251,13 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st var pollingIntervalInSeconds = 5; await Task.Delay(TimeSpan.FromSeconds(pollingIntervalInSeconds)); if (cts.IsCancellationRequested) break; - cloudJob = (await GetJobStatusAsync(channel, job.Id)).Output as CloudJob; - channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob.Status}"); + cloudJob = await ActiveWorkspace.GetJobAsync(MostRecentJobId); + channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}"); } - while (cloudJob != null && !cloudJob.Succeeded && !cloudJob.Failed); + while (cloudJob == null || cloudJob.InProgress); } - return await GetJobResultAsync(channel, job.Id); + return await GetJobResultAsync(channel, MostRecentJobId); } /// @@ -348,7 +344,7 @@ public async Task GetJobResultAsync(IChannel channel, string jo jobId = MostRecentJobId; } - var job = ActiveWorkspace.GetJob(jobId); + var job = await ActiveWorkspace.GetJobAsync(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); @@ -396,7 +392,7 @@ public async Task GetJobStatusAsync(IChannel channel, string jo jobId = MostRecentJobId; } - var job = ActiveWorkspace.GetJob(jobId); + var job = await ActiveWorkspace.GetJobAsync(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); @@ -416,7 +412,7 @@ public async Task GetJobListAsync(IChannel channel) return AzureClientError.NotConnected.ToExecutionResult(); } - var jobs = ActiveWorkspace.ListJobs(); + var jobs = await ActiveWorkspace.ListJobsAsync(); if (jobs == null || jobs.Count() == 0) { channel.Stderr("No jobs found in current Azure Quantum workspace."); From 14092138fed9e89d875b99de17987d564ed36707 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 5 Jun 2020 15:10:16 -0400 Subject: [PATCH 24/28] Consistent error handling for IWorkspace calls --- src/AzureClient/AzureClient.cs | 38 +++++++++++++++++++++++++++++----- 1 file changed, 33 insertions(+), 5 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 8d875ce234..a282d18788 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -142,7 +142,7 @@ public async Task ConnectAsync(IChannel channel, } catch (Exception e) { - channel.Stderr(e.ToString()); + Logger?.LogError(e, $"Failed to download providers list from Azure Quantum workspace: {e.Message}"); return AzureClientError.WorkspaceNotFound.ToExecutionResult(); } @@ -251,7 +251,7 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st var pollingIntervalInSeconds = 5; await Task.Delay(TimeSpan.FromSeconds(pollingIntervalInSeconds)); if (cts.IsCancellationRequested) break; - cloudJob = await ActiveWorkspace.GetJobAsync(MostRecentJobId); + cloudJob = await GetCloudJob(MostRecentJobId); channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}"); } while (cloudJob == null || cloudJob.InProgress); @@ -344,7 +344,7 @@ public async Task GetJobResultAsync(IChannel channel, string jo jobId = MostRecentJobId; } - var job = await ActiveWorkspace.GetJobAsync(jobId); + var job = await GetCloudJob(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); @@ -392,7 +392,7 @@ public async Task GetJobStatusAsync(IChannel channel, string jo jobId = MostRecentJobId; } - var job = await ActiveWorkspace.GetJobAsync(jobId); + var job = await GetCloudJob(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); @@ -412,7 +412,7 @@ public async Task GetJobListAsync(IChannel channel) return AzureClientError.NotConnected.ToExecutionResult(); } - var jobs = await ActiveWorkspace.ListJobsAsync(); + var jobs = await GetCloudJobs(); if (jobs == null || jobs.Count() == 0) { channel.Stderr("No jobs found in current Azure Quantum workspace."); @@ -422,5 +422,33 @@ public async Task GetJobListAsync(IChannel channel) // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. return jobs.ToJupyterTable().ToExecutionResult(); } + + private async Task GetCloudJob(string jobId) + { + try + { + return await ActiveWorkspace.GetJobAsync(jobId); + } + catch (Exception e) + { + Logger?.LogError(e, $"Failed to retrieve the specified Azure Quantum job: {e.Message}"); + } + + return null; + } + + private async Task?> GetCloudJobs() + { + try + { + return await ActiveWorkspace.ListJobsAsync(); + } + catch (Exception e) + { + Logger?.LogError(e, $"Failed to retrieve the list of jobs from the Azure Quantum workspace: {e.Message}"); + } + + return null; + } } } From 78196ff8f865d2c01b5f6fb4ba74adba3dc7b771 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 5 Jun 2020 16:22:31 -0400 Subject: [PATCH 25/28] Update to latest QDK released version --- src/AzureClient/AzureClient.csproj | 2 +- src/Core/Core.csproj | 6 +++--- src/Tool/appsettings.json | 28 ++++++++++++++-------------- 3 files changed, 18 insertions(+), 18 deletions(-) diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index b2fc4b4554..995ae372bb 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index a0b687d1ba..11460fc08e 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,9 +34,9 @@ - - - + + + diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index 3edf302854..f6975e9c6c 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -6,24 +6,24 @@ }, "AllowedHosts": "*", "DefaultPackageVersions": [ - "Microsoft.Quantum.Compiler::0.11.2006.207", + "Microsoft.Quantum.Compiler::0.11.2006.403", - "Microsoft.Quantum.CsharpGeneration::0.11.2006.207", - "Microsoft.Quantum.Development.Kit::0.11.2006.207", - "Microsoft.Quantum.Simulators::0.11.2006.207", - "Microsoft.Quantum.Xunit::0.11.2006.207", + "Microsoft.Quantum.CsharpGeneration::0.11.2006.403", + "Microsoft.Quantum.Development.Kit::0.11.2006.403", + "Microsoft.Quantum.Simulators::0.11.2006.403", + "Microsoft.Quantum.Xunit::0.11.2006.403", - "Microsoft.Quantum.Standard::0.11.2006.207", - "Microsoft.Quantum.Chemistry::0.11.2006.207", - "Microsoft.Quantum.Chemistry.Jupyter::0.11.2006.207", - "Microsoft.Quantum.Numerics::0.11.2006.207", + "Microsoft.Quantum.Standard::0.11.2006.403", + "Microsoft.Quantum.Chemistry::0.11.2006.403", + "Microsoft.Quantum.Chemistry.Jupyter::0.11.2006.403", + "Microsoft.Quantum.Numerics::0.11.2006.403", - "Microsoft.Quantum.Katas::0.11.2006.207", + "Microsoft.Quantum.Katas::0.11.2006.403", - "Microsoft.Quantum.Research::0.11.2006.207", + "Microsoft.Quantum.Research::0.11.2006.403", - "Microsoft.Quantum.Providers.IonQ::0.11.2006.207", - "Microsoft.Quantum.Providers.Honeywell::0.11.2006.207", - "Microsoft.Quantum.Providers.QCI::0.11.2006.207", + "Microsoft.Quantum.Providers.IonQ::0.11.2006.403", + "Microsoft.Quantum.Providers.Honeywell::0.11.2006.403", + "Microsoft.Quantum.Providers.QCI::0.11.2006.403", ] } From 9b3b74b231815a3ab3f5db847e5583a0a7d49202 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 10 Jun 2020 19:52:22 -0400 Subject: [PATCH 26/28] Use switch syntax for entryPointInput Co-authored-by: Chris Granade --- src/AzureClient/EntryPoint/EntryPoint.cs | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs index 49108f3022..1d5bdf626c 100644 --- a/src/AzureClient/EntryPoint/EntryPoint.cs +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -65,10 +65,12 @@ public Task SubmitAsync(IQuantumMachine machine, Dictionary< parameterValues.Add(parameterValue); } - var entryPointInput = - parameterValues.Count == 0 ? QVoid.Instance : - parameterValues.Count == 1 ? parameterValues.Single() : - InputType.GetConstructor(parameterTypes.ToArray()).Invoke(parameterValues.ToArray()); + var entryPointInput = parameterValues.Count switch + { + 0 => QVoid.Instance, + 1 => parameterValues.Single(), + _ => InputType.GetConstructor(parameterTypes.ToArray()).Invoke(parameterValues.ToArray()) + }; // Find and invoke the method on IQuantumMachine that is declared as: // Task SubmitAsync(EntryPointInfo info, TInput input) From eb8de489d1ad9f7972757519f4b10fcc4711ac2f Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 10 Jun 2020 19:53:51 -0400 Subject: [PATCH 27/28] Remove 'All rights reserved.' Co-authored-by: Chris Granade --- src/AzureClient/EntryPoint/EntryPointGenerator.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureClient/EntryPoint/EntryPointGenerator.cs b/src/AzureClient/EntryPoint/EntryPointGenerator.cs index 0cf9ae3946..a7b42af189 100644 --- a/src/AzureClient/EntryPoint/EntryPointGenerator.cs +++ b/src/AzureClient/EntryPoint/EntryPointGenerator.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable From 04296350e1813603fc01f99b7813ec63407540c8 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 10 Jun 2020 20:04:13 -0400 Subject: [PATCH 28/28] Addressing PR feedback and compiler warnings --- src/AzureClient/EntryPoint/EntryPoint.cs | 12 +++--- .../EntryPoint/EntryPointGenerator.cs | 40 ++++++++----------- .../EntryPoint/EntryPointOperationResolver.cs | 2 +- src/AzureClient/EntryPoint/IEntryPoint.cs | 2 +- .../EntryPoint/IEntryPointGenerator.cs | 8 ++-- 5 files changed, 28 insertions(+), 36 deletions(-) diff --git a/src/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs index 1d5bdf626c..407f29d0d5 100644 --- a/src/AzureClient/EntryPoint/EntryPoint.cs +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -23,12 +23,12 @@ internal class EntryPoint : IEntryPoint /// /// Creates an object used to submit jobs to Azure Quantum. /// - /// Must be an object with type + /// Must be an object with type /// parameters specified by the types in the entryPointInputbeginWords argument. /// Specifies the input parameter type for the - /// object provided as the entryPointInfo argument. + /// object provided as the entryPointInfo argument. /// Specifies the output parameter type for the - /// object provided as the entryPointInfo argument. + /// object provided as the entryPointInfo argument. /// Information about the Q# operation to be used as the entry point. public EntryPoint(object entryPointInfo, Type inputType, Type outputType, OperationInfo operationInfo) { @@ -58,7 +58,7 @@ public Task SubmitAsync(IQuantumMachine machine, Dictionary< } catch (Exception e) { - throw new ArgumentException($"The value {rawParameterValue} provided for parameter {parameter.Name} could not be converted to the expected type."); + throw new ArgumentException($"The value {rawParameterValue} provided for parameter {parameter.Name} could not be converted to the expected type: {e.Message}"); } parameterTypes.Add(parameter.ParameterType); @@ -84,7 +84,7 @@ public Task SubmitAsync(IQuantumMachine machine, Dictionary< && method.GetParameters()[1].ParameterType.IsGenericMethodParameter) .MakeGenericMethod(new Type[] { InputType, OutputType }); var submitParameters = new object[] { EntryPointInfo, entryPointInput }; - return submitMethod.Invoke(machine, submitParameters) as Task; + return (Task)submitMethod.Invoke(machine, submitParameters); } } } diff --git a/src/AzureClient/EntryPoint/EntryPointGenerator.cs b/src/AzureClient/EntryPoint/EntryPointGenerator.cs index a7b42af189..31f997f40f 100644 --- a/src/AzureClient/EntryPoint/EntryPointGenerator.cs +++ b/src/AzureClient/EntryPoint/EntryPointGenerator.cs @@ -23,9 +23,9 @@ internal class EntryPointGenerator : IEntryPointGenerator private IWorkspace Workspace { get; } private ISnippets Snippets { get; } public IReferences References { get; } - public AssemblyInfo WorkspaceAssemblyInfo { get; set; } = new AssemblyInfo(null); - public AssemblyInfo SnippetsAssemblyInfo { get; set; } = new AssemblyInfo(null); - public AssemblyInfo EntryPointAssemblyInfo { get; set; } = new AssemblyInfo(null); + public AssemblyInfo? WorkspaceAssemblyInfo { get; set; } + public AssemblyInfo? SnippetsAssemblyInfo { get; set; } + public AssemblyInfo? EntryPointAssemblyInfo { get; set; } public EntryPointGenerator( ICompilerService compiler, @@ -45,28 +45,18 @@ public EntryPointGenerator( eventService?.TriggerServiceInitialized(this); } - + /// /// Because the assemblies are loaded into memory, we need to provide this method to the AssemblyLoadContext /// such that the Workspace assembly or this assembly is correctly resolved when it is executed for simulation. /// - public Assembly Resolve(AssemblyLoadContext context, AssemblyName name) + public Assembly? Resolve(AssemblyLoadContext context, AssemblyName name) => name.Name switch { - if (name.Name == Path.GetFileNameWithoutExtension(EntryPointAssemblyInfo?.Location)) - { - return EntryPointAssemblyInfo.Assembly; - } - if (name.Name == Path.GetFileNameWithoutExtension(SnippetsAssemblyInfo?.Location)) - { - return SnippetsAssemblyInfo.Assembly; - } - else if (name.Name == Path.GetFileNameWithoutExtension(WorkspaceAssemblyInfo?.Location)) - { - return WorkspaceAssemblyInfo.Assembly; - } - - return null; - } + var s when s == Path.GetFileNameWithoutExtension(EntryPointAssemblyInfo?.Location) => EntryPointAssemblyInfo?.Assembly, + var s when s == Path.GetFileNameWithoutExtension(SnippetsAssemblyInfo?.Location) => SnippetsAssemblyInfo?.Assembly, + var s when s == Path.GetFileNameWithoutExtension(WorkspaceAssemblyInfo?.Location) => WorkspaceAssemblyInfo?.Assembly, + _ => null + }; public IEntryPoint Generate(string operationName, string? executionTarget) { @@ -133,10 +123,12 @@ public IEntryPoint Generate(string operationName, string? executionTarget) // Construct the EntryPointInfo<,> object var parameterTypes = entryPointOperationInfo.RoslynParameters.Select(p => p.ParameterType).ToArray(); var typeCount = parameterTypes.Length; - Type entryPointInputType = - typeCount == 0 ? typeof(QVoid) : - typeCount == 1 ? parameterTypes.Single() : - PartialMapper.TupleTypes[typeCount].MakeGenericType(parameterTypes); + Type entryPointInputType = typeCount switch + { + 0 => typeof(QVoid), + 1 => parameterTypes.Single(), + _ => PartialMapper.TupleTypes[typeCount].MakeGenericType(parameterTypes) + }; Type entryPointOutputType = entryPointOperationInfo.ReturnType; Type entryPointInfoType = typeof(EntryPointInfo<,>).MakeGenericType(new Type[] { entryPointInputType, entryPointOutputType }); diff --git a/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs b/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs index 1bacca1af6..28dcda3f4c 100644 --- a/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs +++ b/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable diff --git a/src/AzureClient/EntryPoint/IEntryPoint.cs b/src/AzureClient/EntryPoint/IEntryPoint.cs index f2eeab219b..4cc6e063d7 100644 --- a/src/AzureClient/EntryPoint/IEntryPoint.cs +++ b/src/AzureClient/EntryPoint/IEntryPoint.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable diff --git a/src/AzureClient/EntryPoint/IEntryPointGenerator.cs b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs index 1428284266..1ed8b2a87a 100644 --- a/src/AzureClient/EntryPoint/IEntryPointGenerator.cs +++ b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -18,17 +18,17 @@ public interface IEntryPointGenerator /// /// Gets the compiled workspace assembly for the most recently-generated entry point. /// - public AssemblyInfo WorkspaceAssemblyInfo { get; } + public AssemblyInfo? WorkspaceAssemblyInfo { get; } /// /// Gets the compiled snippets assembly for the most recently-generated entry point. /// - public AssemblyInfo SnippetsAssemblyInfo { get; } + public AssemblyInfo? SnippetsAssemblyInfo { get; } /// /// Gets the compiled entry point assembly for the most recently-generated entry point. /// - public AssemblyInfo EntryPointAssemblyInfo { get; } + public AssemblyInfo? EntryPointAssemblyInfo { get; } /// /// Gets the references used for compilation of the entry point assembly.