From c804d4f7a79aa4e0306541d799281f6954af2146 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 13 Apr 2020 13:07:50 -0700 Subject: [PATCH 01/44] Fix case consistency in test namespace --- src/Tests/HttpServerIntegrationTests.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/Tests/HttpServerIntegrationTests.cs b/src/Tests/HttpServerIntegrationTests.cs index 61eddced03..10534f5a4f 100644 --- a/src/Tests/HttpServerIntegrationTests.cs +++ b/src/Tests/HttpServerIntegrationTests.cs @@ -9,9 +9,8 @@ using System.Collections.Generic; using System.Linq; using System.Threading.Tasks; -using Tests.IQSharp; -namespace Tests.IQsharp +namespace Tests.IQSharp { [TestClass] public class HttpServerIntegrationTests From d9afad7d2dcc6fbf55c7c00e44855573b8d35ff7 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 13 Apr 2020 13:09:03 -0700 Subject: [PATCH 02/44] Enforce exact match for %magic command names --- src/Jupyter/Magic/Resolution/MagicResolver.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jupyter/Magic/Resolution/MagicResolver.cs b/src/Jupyter/Magic/Resolution/MagicResolver.cs index 3335c59dec..4c624d884f 100644 --- a/src/Jupyter/Magic/Resolution/MagicResolver.cs +++ b/src/Jupyter/Magic/Resolution/MagicResolver.cs @@ -80,7 +80,7 @@ public MagicSymbol Resolve(string symbolName) foreach (var magic in FindAllMagicSymbols()) { - if (symbolName.StartsWith(magic.Name)) + if (symbolName == magic.Name) { this.logger.LogDebug($"Using magic {magic.Name}"); return magic; From c757aa65c2ed9dabbc6ad8ae1972fd8bb8aa72e7 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 13 Apr 2020 13:10:05 -0700 Subject: [PATCH 03/44] Make Extensions.Dedent() publicly accessible --- src/Jupyter/Extensions.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Jupyter/Extensions.cs b/src/Jupyter/Extensions.cs index a4ccf50cd6..2b53b49399 100644 --- a/src/Jupyter/Extensions.cs +++ b/src/Jupyter/Extensions.cs @@ -175,7 +175,7 @@ public static T WithStackTraceDisplay(this T simulator, IChannel channel) /// Removes common indents from each line in a string, /// similarly to Python's textwrap.dedent() function. /// - internal static string Dedent(this string text) + public static string Dedent(this string text) { // First, start by finding the length of common indents, // disregarding lines that are only whitespace. From 8b931bb20516a2a1566a43dbcda63d4c8e271d86 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 13 Apr 2020 14:25:00 -0700 Subject: [PATCH 04/44] Move Extensions.Dedent() to core project --- src/Core/Extensions/String.cs | 38 +++++++++++++++++++++++++++++++++++ src/Jupyter/Extensions.cs | 27 ------------------------- 2 files changed, 38 insertions(+), 27 deletions(-) create mode 100644 src/Core/Extensions/String.cs diff --git a/src/Core/Extensions/String.cs b/src/Core/Extensions/String.cs new file mode 100644 index 0000000000..ea85828f63 --- /dev/null +++ b/src/Core/Extensions/String.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +using System; +using System.Text.RegularExpressions; + +namespace Microsoft.Quantum.IQSharp +{ + public static partial class Extensions + { + /// + /// Removes common indents from each line in a string, + /// similarly to Python's textwrap.dedent() function. + /// + public static string Dedent(this string text) + { + // First, start by finding the length of common indents, + // disregarding lines that are only whitespace. + var leadingWhitespaceRegex = new Regex(@"^[ \t]*"); + var minWhitespace = int.MaxValue; + foreach (var line in text.Split("\n")) + { + if (!string.IsNullOrWhiteSpace(line)) + { + var match = leadingWhitespaceRegex.Match(line); + minWhitespace = match.Success + ? System.Math.Min(minWhitespace, match.Value.Length) + : minWhitespace = 0; + } + } + + // We can use that to build a new regex that strips + // out common indenting. + var leftTrimRegex = new Regex(@$"^[ \t]{{{minWhitespace}}}", RegexOptions.Multiline); + return leftTrimRegex.Replace(text, ""); + } + } +} diff --git a/src/Jupyter/Extensions.cs b/src/Jupyter/Extensions.cs index 2b53b49399..4c03f98166 100644 --- a/src/Jupyter/Extensions.cs +++ b/src/Jupyter/Extensions.cs @@ -170,32 +170,5 @@ public static T WithStackTraceDisplay(this T simulator, IChannel channel) }; return simulator; } - - /// - /// Removes common indents from each line in a string, - /// similarly to Python's textwrap.dedent() function. - /// - public static string Dedent(this string text) - { - // First, start by finding the length of common indents, - // disregarding lines that are only whitespace. - var leadingWhitespaceRegex = new Regex(@"^[ \t]*"); - var minWhitespace = int.MaxValue; - foreach (var line in text.Split("\n")) - { - if (!string.IsNullOrWhiteSpace(line)) - { - var match = leadingWhitespaceRegex.Match(line); - minWhitespace = match.Success - ? System.Math.Min(minWhitespace, match.Value.Length) - : minWhitespace = 0; - } - } - - // We can use that to build a new regex that strips - // out common indenting. - var leftTrimRegex = new Regex(@$"^[ \t]{{{minWhitespace}}}", RegexOptions.Multiline); - return leftTrimRegex.Replace(text, ""); - } } } From 7e5f3f62417f0e7cc5cf46abbe518a6cc7ec6144 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 13 Apr 2020 15:14:00 -0700 Subject: [PATCH 05/44] Add AzureClient project to iqsharp solution --- iqsharp.sln | 37 ++++++++++++++++++++++-------- src/AzureClient/AzureClient.csproj | 28 ++++++++++++++++++++++ 2 files changed, 55 insertions(+), 10 deletions(-) create mode 100644 src/AzureClient/AzureClient.csproj diff --git a/iqsharp.sln b/iqsharp.sln index 2d4bdef234..952bf41100 100644 --- a/iqsharp.sln +++ b/iqsharp.sln @@ -1,17 +1,19 @@  Microsoft Visual Studio Solution File, Format Version 12.00 -# Visual Studio 15 -VisualStudioVersion = 15.0.26124.0 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.29920.165 MinimumVisualStudioVersion = 15.0.26124.0 -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Jupyter", "src\Jupyter\Jupyter.csproj", "{B6F42099-DACD-472F-866C-BEF92DDDF754}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jupyter", "src\Jupyter\Jupyter.csproj", "{B6F42099-DACD-472F-866C-BEF92DDDF754}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Core", "src\Core\Core.csproj", "{0ACA57A6-A8F6-497C-85D3-D547433BFACB}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Core", "src\Core\Core.csproj", "{0ACA57A6-A8F6-497C-85D3-D547433BFACB}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tests.IQsharp", "src\Tests\Tests.IQsharp.csproj", "{756BE082-2E89-47F0-A48A-D7E762B0A82E}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tests.IQsharp", "src\Tests\Tests.IQsharp.csproj", "{756BE082-2E89-47F0-A48A-D7E762B0A82E}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Tool", "src\Tool\Tool.csproj", "{7EB9C7E8-7D40-432E-9857-1DD6301B9F4A}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tool", "src\Tool\Tool.csproj", "{7EB9C7E8-7D40-432E-9857-1DD6301B9F4A}" EndProject -Project("{FAE04EC0-301F-11D3-BF4B-00C04F79EFBC}") = "Web", "src\Web\Web.csproj", "{6431E92B-12AA-432C-8D53-C9A7A54BA21B}" +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{6431E92B-12AA-432C-8D53-C9A7A54BA21B}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureClient", "src\AzureClient\AzureClient.csproj", "{E7B60C94-B666-4024-B53E-D12C142DE8DC}" EndProject Global GlobalSection(SolutionConfigurationPlatforms) = preSolution @@ -22,9 +24,6 @@ Global Release|x64 = Release|x64 Release|x86 = Release|x86 EndGlobalSection - GlobalSection(SolutionProperties) = preSolution - HideSolutionNode = FALSE - EndGlobalSection GlobalSection(ProjectConfigurationPlatforms) = postSolution {B6F42099-DACD-472F-866C-BEF92DDDF754}.Debug|Any CPU.ActiveCfg = Debug|Any CPU {B6F42099-DACD-472F-866C-BEF92DDDF754}.Debug|Any CPU.Build.0 = Debug|Any CPU @@ -86,5 +85,23 @@ Global {6431E92B-12AA-432C-8D53-C9A7A54BA21B}.Release|x64.Build.0 = Release|Any CPU {6431E92B-12AA-432C-8D53-C9A7A54BA21B}.Release|x86.ActiveCfg = Release|Any CPU {6431E92B-12AA-432C-8D53-C9A7A54BA21B}.Release|x86.Build.0 = Release|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|x64.ActiveCfg = Debug|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|x64.Build.0 = Debug|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|x86.ActiveCfg = Debug|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|x86.Build.0 = Debug|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|Any CPU.Build.0 = Release|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|x64.ActiveCfg = Release|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|x64.Build.0 = Release|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|x86.ActiveCfg = Release|Any CPU + {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|x86.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {9B439141-9EFD-47B2-BD10-41BD0C96316A} EndGlobalSection EndGlobal diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj new file mode 100644 index 0000000000..830944146b --- /dev/null +++ b/src/AzureClient/AzureClient.csproj @@ -0,0 +1,28 @@ + + + + netstandard2.1 + x64 + Microsoft.Quantum.IQSharp.AzureClient + Microsoft.Quantum.IQSharp.AzureClient + true + + + + + + + + + + + + + + + + + + + + From a0023f2f6bbee985054adf62a3e0eaa9018c9acb Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 13 Apr 2020 15:15:12 -0700 Subject: [PATCH 06/44] Add AzureClient project dependencies and update package versions to be consistent --- src/Core/Core.csproj | 6 +++--- src/Jupyter/Jupyter.csproj | 3 ++- src/Tool/Tool.csproj | 1 + src/Tool/appsettings.json | 20 ++++++++++---------- 4 files changed, 16 insertions(+), 14 deletions(-) diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 3e8942e5af..dba9a8f51a 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,9 +34,9 @@ - - - + + + diff --git a/src/Jupyter/Jupyter.csproj b/src/Jupyter/Jupyter.csproj index 4fd43c8606..78d0fe8e13 100644 --- a/src/Jupyter/Jupyter.csproj +++ b/src/Jupyter/Jupyter.csproj @@ -22,11 +22,12 @@ - + + diff --git a/src/Tool/Tool.csproj b/src/Tool/Tool.csproj index ea89459038..14df53a843 100644 --- a/src/Tool/Tool.csproj +++ b/src/Tool/Tool.csproj @@ -36,6 +36,7 @@ + diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index 68275089e3..71f5f2f0b5 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -6,18 +6,18 @@ }, "AllowedHosts": "*", "DefaultPackageVersions": [ - "Microsoft.Quantum.Compiler::0.10.2003.1102-beta", + "Microsoft.Quantum.Compiler::0.11.2004.1014-beta", - "Microsoft.Quantum.CsharpGeneration::0.10.2003.1102-beta", - "Microsoft.Quantum.Development.Kit::0.10.2003.1102-beta", - "Microsoft.Quantum.Simulators::0.10.2003.1102-beta", - "Microsoft.Quantum.Xunit::0.10.2003.1102-beta", + "Microsoft.Quantum.CsharpGeneration::0.11.2004.1014-beta", + "Microsoft.Quantum.SDK::0.11.2004.1014-beta", + "Microsoft.Quantum.Simulators::0.11.2004.1014-beta", + "Microsoft.Quantum.Xunit::0.11.2004.1014-beta", - "Microsoft.Quantum.Standard::0.10.2003.1102-beta", - "Microsoft.Quantum.Chemistry::0.10.2003.1102-beta", - "Microsoft.Quantum.Chemistry.Jupyter::0.10.2003.1102-beta", - "Microsoft.Quantum.Numerics::0.10.2003.1102-beta", + "Microsoft.Quantum.Standard::0.11.2004.1014-beta", + "Microsoft.Quantum.Chemistry::0.11.2004.1014-beta", + "Microsoft.Quantum.Chemistry.Jupyter::0.11.2004.1014-beta", + "Microsoft.Quantum.Numerics::0.11.2004.1014-beta", - "Microsoft.Quantum.Research::0.10.2003.1102-beta" + "Microsoft.Quantum.Research::0.11.2004.1014-beta" ] } From e424eeddf64d189775fa989ed3db265c5a81e017 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 13 Apr 2020 15:17:03 -0700 Subject: [PATCH 07/44] Add AzureClient class and register as magic symbol provider --- src/AzureClient/AzureClient.cs | 23 +++++++++++++++++++ src/AzureClient/Extensions.cs | 21 +++++++++++++++++ src/AzureClient/IAzureClient.cs | 16 +++++++++++++ src/Jupyter/Magic/Resolution/MagicResolver.cs | 13 ++++++++--- src/Tool/Startup.cs | 2 ++ 5 files changed, 72 insertions(+), 3 deletions(-) create mode 100644 src/AzureClient/AzureClient.cs create mode 100644 src/AzureClient/Extensions.cs create mode 100644 src/AzureClient/IAzureClient.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs new file mode 100644 index 0000000000..8293fd7b65 --- /dev/null +++ b/src/AzureClient/AzureClient.cs @@ -0,0 +1,23 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Jupyter.Core; +using System.Threading.Tasks; + +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensions.Msal; +using Microsoft.WindowsAzure.Storage; +using System.Linq; +using System.IO; +using Microsoft.Azure.Quantum; +using Microsoft.Azure.Quantum.Client; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Quantum.Runtime; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + public class AzureClient : IAzureClient + { + } +} diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs new file mode 100644 index 0000000000..11453c042d --- /dev/null +++ b/src/AzureClient/Extensions.cs @@ -0,0 +1,21 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Extensions.DependencyInjection; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// Extension methods to be used with various IQ# and AzureClient objects. + /// + public static class Extensions + { + /// + /// Adds services required for the AzureClient to a given service collection. + /// + public static void AddAzureClient(this IServiceCollection services) + { + services.AddSingleton(); + } + } +} diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs new file mode 100644 index 0000000000..c4dc183ea4 --- /dev/null +++ b/src/AzureClient/IAzureClient.cs @@ -0,0 +1,16 @@ +using System; +using System.Collections.Generic; +using System.Text; +using Microsoft.Jupyter.Core; +using System.Threading.Tasks; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// This service is capable of connecting to Azure Quantum workspace + /// and submitting jobs. + /// + public interface IAzureClient + { + } +} diff --git a/src/Jupyter/Magic/Resolution/MagicResolver.cs b/src/Jupyter/Magic/Resolution/MagicResolver.cs index 4c624d884f..d183ead5c9 100644 --- a/src/Jupyter/Magic/Resolution/MagicResolver.cs +++ b/src/Jupyter/Magic/Resolution/MagicResolver.cs @@ -8,6 +8,7 @@ using Microsoft.Extensions.DependencyInjection; using Microsoft.Extensions.Logging; using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.AzureClient; using Microsoft.Quantum.IQSharp.Common; using Newtonsoft.Json; @@ -22,7 +23,7 @@ namespace Microsoft.Quantum.IQSharp.Jupyter /// public class MagicSymbolResolver : IMagicSymbolResolver { - private AssemblyInfo kernelAssembly; + private AssemblyInfo[] kernelAssemblies; private Dictionary cache; private IServiceProvider services; private IReferences references; @@ -38,7 +39,10 @@ public MagicSymbolResolver(IServiceProvider services, ILogger(); this.logger = logger; - this.kernelAssembly = new AssemblyInfo(typeof(MagicSymbolResolver).Assembly); + this.kernelAssemblies = new[] { + new AssemblyInfo(typeof(MagicSymbolResolver).Assembly), + new AssemblyInfo(typeof(AzureClient.AzureClient).Assembly) + }; this.services = services; this.references = services.GetService(); } @@ -50,7 +54,10 @@ public MagicSymbolResolver(IServiceProvider services, ILogger private IEnumerable RelevantAssemblies() { - yield return this.kernelAssembly; + foreach (var asm in this.kernelAssemblies) + { + yield return asm; + } foreach (var asm in references.Assemblies) { diff --git a/src/Tool/Startup.cs b/src/Tool/Startup.cs index cb4cfa4150..c278f30269 100644 --- a/src/Tool/Startup.cs +++ b/src/Tool/Startup.cs @@ -5,6 +5,7 @@ using Microsoft.AspNetCore.Hosting; using Microsoft.Extensions.DependencyInjection; using Microsoft.Quantum.IQSharp.Jupyter; +using Microsoft.Quantum.IQSharp.AzureClient; using System; namespace Microsoft.Quantum.IQSharp @@ -24,6 +25,7 @@ public virtual void ConfigureServices(IServiceCollection services) services.AddSingleton(typeof(ITelemetryService), GetTelemetryServiceType()); services.AddIQSharp(); services.AddIQSharpKernel(); + services.AddAzureClient(); var assembly = typeof(PackagesController).Assembly; services.AddControllers() From dfc62ab9a6ecc8a93bbc2fb324e3717e6b5e9006 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 15 Apr 2020 13:27:29 -0700 Subject: [PATCH 08/44] Add #nullable enable to new files --- src/AzureClient/AzureClient.cs | 7 ++++++- src/AzureClient/Extensions.cs | 7 ++++++- src/AzureClient/IAzureClient.cs | 7 ++++++- 3 files changed, 18 insertions(+), 3 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 8293fd7b65..99626db950 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -1,4 +1,9 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; using System.Collections.Generic; using System.Text; using Microsoft.Jupyter.Core; diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 11453c042d..fbfe0a4a8e 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -1,4 +1,9 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; using System.Collections.Generic; using System.Text; using Microsoft.Extensions.DependencyInjection; diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index c4dc183ea4..19da6a27df 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -1,4 +1,9 @@ -using System; +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; using System.Collections.Generic; using System.Text; using Microsoft.Jupyter.Core; From 7d7bcc3fb08ae0b8b19a7d1760cd314dfa2b7e50 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 15 Apr 2020 16:23:39 -0700 Subject: [PATCH 09/44] Add empty AzureClientTests --- src/Tests/AzureClientTests.cs | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) create mode 100644 src/Tests/AzureClientTests.cs diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs new file mode 100644 index 0000000000..ba18985f1c --- /dev/null +++ b/src/Tests/AzureClientTests.cs @@ -0,0 +1,22 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using Microsoft.Quantum.IQSharp.AzureClient; +using System.Linq; +using System.Collections.Generic; + +namespace Tests.IQSharp +{ + [TestClass] + public class AzureClientTests + { + [TestMethod] + public void TestNothing() + { + Assert.IsTrue(true); + } + } +} From 8d2b0eb99e4d89fac116fa62f6d204c2c8eecd58 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 16 Apr 2020 16:34:31 -0700 Subject: [PATCH 10/44] Update a few package references --- src/AzureClient/AzureClient.cs | 1 - src/AzureClient/AzureClient.csproj | 3 +-- src/Core/Core.csproj | 4 ++-- src/Jupyter/Jupyter.csproj | 2 +- 4 files changed, 4 insertions(+), 6 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 99626db950..64faa81fda 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -11,7 +11,6 @@ using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; -using Microsoft.WindowsAzure.Storage; using System.Linq; using System.IO; using Microsoft.Azure.Quantum; diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index 830944146b..b8613eec35 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -16,9 +16,8 @@ - + - diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index dba9a8f51a..e5e7581f63 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -32,8 +32,8 @@ - - + + diff --git a/src/Jupyter/Jupyter.csproj b/src/Jupyter/Jupyter.csproj index 78d0fe8e13..87afc0d1ea 100644 --- a/src/Jupyter/Jupyter.csproj +++ b/src/Jupyter/Jupyter.csproj @@ -22,7 +22,7 @@ - + From 0c5681ac6d7cd94abc6b3ab4c1af8b0bd8ffd842 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 16 Apr 2020 17:39:30 -0700 Subject: [PATCH 11/44] Update IAzureClient API docs Co-Authored-By: Chris Granade --- src/AzureClient/IAzureClient.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 19da6a27df..8efd69e5a7 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -12,7 +12,7 @@ namespace Microsoft.Quantum.IQSharp.AzureClient { /// - /// This service is capable of connecting to Azure Quantum workspace + /// This service is capable of connecting to Azure Quantum workspaces /// and submitting jobs. /// public interface IAzureClient From 65ccb8c3af00c37ed788360adeb4b11128bebf62 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 16 Apr 2020 17:40:07 -0700 Subject: [PATCH 12/44] Update new array syntax Co-Authored-By: Chris Granade --- src/Jupyter/Magic/Resolution/MagicResolver.cs | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/Jupyter/Magic/Resolution/MagicResolver.cs b/src/Jupyter/Magic/Resolution/MagicResolver.cs index d183ead5c9..d96b95fb1a 100644 --- a/src/Jupyter/Magic/Resolution/MagicResolver.cs +++ b/src/Jupyter/Magic/Resolution/MagicResolver.cs @@ -39,7 +39,8 @@ public MagicSymbolResolver(IServiceProvider services, ILogger(); this.logger = logger; - this.kernelAssemblies = new[] { + this.kernelAssemblies = new[] + { new AssemblyInfo(typeof(MagicSymbolResolver).Assembly), new AssemblyInfo(typeof(AzureClient.AzureClient).Assembly) }; From ae883cb8bfeb852ab6ffd4c2364280fc1ec71ef1 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 16 Apr 2020 17:44:02 -0700 Subject: [PATCH 13/44] Use Microsoft.Quantum.Development.Kit package --- src/Tool/appsettings.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index 71f5f2f0b5..ca473b2635 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -9,7 +9,7 @@ "Microsoft.Quantum.Compiler::0.11.2004.1014-beta", "Microsoft.Quantum.CsharpGeneration::0.11.2004.1014-beta", - "Microsoft.Quantum.SDK::0.11.2004.1014-beta", + "Microsoft.Quantum.Development.Kit::0.11.2004.1014-beta", "Microsoft.Quantum.Simulators::0.11.2004.1014-beta", "Microsoft.Quantum.Xunit::0.11.2004.1014-beta", From abe204f4a9d00790346755604dc15a4fc0d7f176 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 8 May 2020 11:21:02 -0400 Subject: [PATCH 14/44] Update package references --- src/AzureClient/AzureClient.cs | 3 --- src/AzureClient/AzureClient.csproj | 3 +-- 2 files changed, 1 insertion(+), 5 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 64faa81fda..3d941f5233 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -13,9 +13,6 @@ using Microsoft.Identity.Client.Extensions.Msal; using System.Linq; using System.IO; -using Microsoft.Azure.Quantum; -using Microsoft.Azure.Quantum.Client; -using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Quantum.Runtime; namespace Microsoft.Quantum.IQSharp.AzureClient diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index b8613eec35..f518fde626 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -13,15 +13,14 @@ - - + From e6e582d6f3fa54e60f3a91520a5623fed43b71d4 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 13 May 2020 17:51:50 -0400 Subject: [PATCH 15/44] Add AzureClient assembly to manifest (#141) * Add AzureClient assembly to manifest * Add manifest.ps1 to build steps * Run manifest after build completes * Fix manifest.ps1 script error * Fix to manifest.ps1 * Run manifest.ps1 from inside build.ps1 * Fix case error in manifest.ps1, undo 192a631314a8b74f4755f6cacd00e7892640018a * Move back to using pwsh in manifest.ps1 --- build/manifest.ps1 | 14 +++++++++----- build/steps.yml | 5 +++++ 2 files changed, 14 insertions(+), 5 deletions(-) diff --git a/build/manifest.ps1 b/build/manifest.ps1 index d767d026f6..4dbf01ac04 100644 --- a/build/manifest.ps1 +++ b/build/manifest.ps1 @@ -1,3 +1,6 @@ +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. + #!/usr/bin/env pwsh #Requires -PSEdition Core @@ -10,10 +13,11 @@ "Microsoft.Quantum.IQSharp" ); Assemblies = @( - ".\src\tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.dll", - ".\src\tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Core.dll", - ".\src\tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Jupyter.dll", - ".\src\tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Kernel.dll", - ".\src\tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Web.dll" + ".\src\Tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.dll", + ".\src\Tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.AzureClient.dll", + ".\src\Tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Core.dll", + ".\src\Tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Jupyter.dll", + ".\src\Tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Kernel.dll", + ".\src\Tool\bin\$Env:BUILD_CONFIGURATION\netcoreapp3.1\Microsoft.Quantum.IQSharp.Web.dll" ) | ForEach-Object { Get-Item (Join-Path $PSScriptRoot ".." $_) }; } | Write-Output; \ No newline at end of file diff --git a/build/steps.yml b/build/steps.yml index 1649ffc7ac..16cb7f94e0 100644 --- a/build/steps.yml +++ b/build/steps.yml @@ -24,6 +24,11 @@ steps: - pwsh: .\build.ps1 displayName: "Building IQ#" workingDirectory: '$(System.DefaultWorkingDirectory)/build' + +- pwsh: .\manifest.ps1 + displayName: "List built assemblies" + workingDirectory: '$(System.DefaultWorkingDirectory)/build' + condition: succeededOrFailed() - pwsh: .\test.ps1 displayName: "Testing IQ#" From ce2327f2f4e726235077f2da808eb35001df8c00 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Tue, 19 May 2020 12:55:48 -0400 Subject: [PATCH 16/44] Update Microsoft.Identity.Client package versions --- src/AzureClient/AzureClient.csproj | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index f518fde626..e4da5c48b2 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -13,8 +13,8 @@ - - + + From 3c4803fee46e069aa59aa509d729524b25d38b57 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Tue, 19 May 2020 20:29:47 -0400 Subject: [PATCH 17/44] Initial IQ# magic command implementation for Azure Quantum (#144) * Add IAzureClient definition and initial magic commands * Add AzureClient assembly to manifest * Add base class with common functionality for AzureClient magic commands * Initial implementation of AzureClient methods * Move AbstractMagic to Jupyter project * Add key=value input parameter parsing to AbstractMagic * Initial qsharp.azure Python implementation * Begin to unify magic command parsing * Minor updates to a few magics * Integrate latest Azure.Quantum.Client package * Minor cleanup * Improvements to address PR comments * Minor change to fix syntax highlighting --- src/AzureClient/AzureClient.cs | 212 +++++++++++++++++- src/AzureClient/AzureClient.csproj | 7 +- src/AzureClient/Extensions.cs | 101 ++++++++- src/AzureClient/IAzureClient.cs | 106 ++++++++- src/AzureClient/Magic/AzureClientMagicBase.cs | 45 ++++ src/AzureClient/Magic/ConnectMagic.cs | 107 +++++++++ src/AzureClient/Magic/StatusMagic.cs | 74 ++++++ src/AzureClient/Magic/SubmitMagic.cs | 66 ++++++ src/AzureClient/Magic/TargetMagic.cs | 76 +++++++ src/AzureClient/Resources.cs | 31 +++ src/Core/Core.csproj | 6 +- src/Jupyter/Extensions.cs | 15 ++ .../Magic/AbstractMagic.cs | 68 +++++- src/Kernel/Magic/LsMagicMagic.cs | 2 +- src/Kernel/Magic/Simulate.cs | 8 +- src/Kernel/Magic/WhoMagic.cs | 2 +- src/Kernel/Magic/WorkspaceMagic.cs | 2 +- src/Python/qsharp/azure.py | 47 ++++ src/Tests/AzureClientMagicTests.cs | 181 +++++++++++++++ src/Tests/AzureClientTests.cs | 26 ++- src/Tests/IQsharpEngineTests.cs | 7 + src/Tests/Mocks.cs | 8 + src/Tests/Startup.cs | 2 + src/Tool/appsettings.json | 20 +- 24 files changed, 1178 insertions(+), 41 deletions(-) create mode 100644 src/AzureClient/Magic/AzureClientMagicBase.cs create mode 100644 src/AzureClient/Magic/ConnectMagic.cs create mode 100644 src/AzureClient/Magic/StatusMagic.cs create mode 100644 src/AzureClient/Magic/SubmitMagic.cs create mode 100644 src/AzureClient/Magic/TargetMagic.cs create mode 100644 src/AzureClient/Resources.cs rename src/{Kernel => Jupyter}/Magic/AbstractMagic.cs (51%) create mode 100644 src/Python/qsharp/azure.py create mode 100644 src/Tests/AzureClientMagicTests.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 3d941f5233..371ab20c95 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -1,24 +1,222 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #nullable enable using System; using System.Collections.Generic; -using System.Text; -using Microsoft.Jupyter.Core; +using System.Linq; +using System.IO; using System.Threading.Tasks; - +using Microsoft.Azure.Quantum.Client; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; -using System.Linq; -using System.IO; -using Microsoft.Quantum.Runtime; +using Microsoft.Jupyter.Core; +using Microsoft.Quantum.Simulation.Core; namespace Microsoft.Quantum.IQSharp.AzureClient { /// public class AzureClient : IAzureClient { + private string ConnectionString { get; set; } = string.Empty; + private string ActiveTargetName { get; set; } = string.Empty; + private AuthenticationResult? AuthenticationResult { get; set; } + private IQuantumClient? QuantumClient { get; set; } + private Azure.Quantum.Workspace? ActiveWorkspace { get; set; } + + /// + public async Task ConnectAsync( + IChannel channel, + string subscriptionId, + string resourceGroupName, + string workspaceName, + string storageAccountConnectionString, + bool forceLoginPrompt = false) + { + ConnectionString = storageAccountConnectionString; + + var clientId = "84ba0947-6c53-4dd2-9ca9-b3694761521b"; // Microsoft Quantum Development Kit + var authority = "https://login.microsoftonline.com/common"; + var msalApp = PublicClientApplicationBuilder.Create(clientId).WithAuthority(authority).Build(); + + // Register the token cache for serialization + var cacheFileName = "aad.bin"; + var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE"; + var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName); + if (string.IsNullOrEmpty(cacheDirectory)) + { + cacheDirectory = Path.Join(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".azure-quantum"); + } + + var storageCreationProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, clientId).Build(); + var cacheHelper = await MsalCacheHelper.CreateAsync(storageCreationProperties); + cacheHelper.RegisterCache(msalApp.UserTokenCache); + + var scopes = new List() { "https://quantum.microsoft.com/Jobs.ReadWrite" }; + + bool shouldShowLoginPrompt = forceLoginPrompt; + if (!shouldShowLoginPrompt) + { + try + { + var accounts = await msalApp.GetAccountsAsync(); + AuthenticationResult = await msalApp.AcquireTokenSilent( + scopes, accounts.FirstOrDefault()).WithAuthority(msalApp.Authority).ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + shouldShowLoginPrompt = true; + } + } + + if (shouldShowLoginPrompt) + { + AuthenticationResult = await msalApp.AcquireTokenWithDeviceCode(scopes, + deviceCodeResult => + { + channel.Stdout(deviceCodeResult.Message); + return Task.FromResult(0); + }).WithAuthority(msalApp.Authority).ExecuteAsync(); + } + + if (AuthenticationResult == null) + { + return AzureClientError.AuthenticationFailed.ToExecutionResult(); + } + + var credentials = new Rest.TokenCredentials(AuthenticationResult.AccessToken); + QuantumClient = new QuantumClient(credentials) + { + SubscriptionId = subscriptionId, + ResourceGroupName = resourceGroupName, + WorkspaceName = workspaceName + }; + ActiveWorkspace = new Azure.Quantum.Workspace( + QuantumClient.SubscriptionId, QuantumClient.ResourceGroupName, + QuantumClient.WorkspaceName, AuthenticationResult?.AccessToken); + + try + { + var jobsList = await QuantumClient.Jobs.ListAsync(); + channel.Stdout($"Successfully connected to Azure Quantum workspace {workspaceName}."); + } + catch (Exception e) + { + channel.Stderr(e.ToString()); + return AzureClientError.WorkspaceNotFound.ToExecutionResult(); + } + + return QuantumClient.ToJupyterTable().ToExecutionResult(); + } + + /// + public async Task PrintConnectionStatusAsync(IChannel channel) => + QuantumClient == null + ? AzureClientError.NotConnected.ToExecutionResult() + : QuantumClient.ToJupyterTable().ToExecutionResult(); + + /// + public async Task SubmitJobAsync( + IChannel channel, + IOperationResolver operationResolver, + string operationName) + { + if (ActiveWorkspace == null) + { + channel.Stderr("Please call %connect before submitting a job."); + return AzureClientError.NotConnected.ToExecutionResult(); + } + + if (ActiveTargetName == null) + { + channel.Stderr("Please call %target before submitting a job."); + return AzureClientError.NoTarget.ToExecutionResult(); + } + + if (string.IsNullOrEmpty(operationName)) + { + channel.Stderr("Please pass a valid Q# operation name to %submit."); + return AzureClientError.NoOperationName.ToExecutionResult(); + } + + var operationInfo = operationResolver.Resolve(operationName); + var entryPointInfo = new EntryPointInfo(operationInfo.RoslynType); + var entryPointInput = QVoid.Instance; + var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTargetName, ConnectionString); + if (machine == null) + { + channel.Stderr($"Could not find an execution target for target {ActiveTargetName}."); + return AzureClientError.NoTarget.ToExecutionResult(); + } + + var job = await machine.SubmitAsync(entryPointInfo, entryPointInput); + return job.ToJupyterTable().ToExecutionResult(); + } + + /// + public async Task SetActiveTargetAsync( + IChannel channel, + string targetName) + { + // TODO: Validate that this target name is valid in the workspace. + ActiveTargetName = targetName; + return $"Active target is now {ActiveTargetName}".ToExecutionResult(); + } + + /// + public async Task PrintTargetListAsync( + IChannel channel) + { + if (QuantumClient == null) + { + channel.Stderr("Please call %connect before listing targets."); + return AzureClientError.NotConnected.ToExecutionResult(); + } + + var providersStatus = await QuantumClient.Providers.GetStatusAsync(); + return providersStatus.ToJupyterTable().ToExecutionResult(); + } + + /// + public async Task PrintJobStatusAsync( + IChannel channel, + string jobId) + { + if (QuantumClient == null) + { + channel.Stderr("Please call %connect before getting job status."); + return AzureClientError.NotConnected.ToExecutionResult(); + } + + var jobDetails = await QuantumClient.Jobs.GetAsync(jobId); + if (jobDetails == null) + { + channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); + return AzureClientError.JobNotFound.ToExecutionResult(); + } + + return jobDetails.ToJupyterTable().ToExecutionResult(); + } + + /// + public async Task PrintJobListAsync( + IChannel channel) + { + if (QuantumClient == null) + { + channel.Stderr("Please call %connect before listing jobs."); + return AzureClientError.NotConnected.ToExecutionResult(); + } + + var jobsList = await QuantumClient.Jobs.ListAsync(); + if (jobsList == null || jobsList.Count() == 0) + { + channel.Stderr("No jobs found in current Azure Quantum workspace."); + return AzureClientError.JobNotFound.ToExecutionResult(); + } + + return jobsList.ToJupyterTable().ToExecutionResult(); + } } } diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index e4da5c48b2..3b9d29e070 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -1,4 +1,4 @@ - + netstandard2.1 @@ -13,8 +13,9 @@ - - + + + diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index fbfe0a4a8e..7a8e950951 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -1,12 +1,18 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.ComponentModel; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.Quantum.Client; +using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Extensions.DependencyInjection; +using Microsoft.Jupyter.Core; +using Microsoft.Quantum.Runtime; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -22,5 +28,96 @@ public static void AddAzureClient(this IServiceCollection services) { services.AddSingleton(); } + + /// + /// Encapsulates a given as the result of an execution. + /// + /// + /// The result of an IAzureClient API call. + /// + public static ExecutionResult ToExecutionResult(this AzureClientError azureClientError) => + new ExecutionResult + { + Status = ExecuteStatus.Error, + Output = azureClientError.ToDescription() + }; + + /// + /// Returns the string value of the for the given + /// enumeration value. + /// + /// + /// + public static string ToDescription(this AzureClientError azureClientError) + { + var attributes = azureClientError + .GetType() + .GetField(azureClientError.ToString()) + .GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[]; + return attributes?.Length > 0 ? attributes[0].Description : string.Empty; + } + + /// + /// Encapsulates a given as the result of an execution. + /// + /// + /// A task which will return the result of an IAzureClient API call. + /// + 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 IEnumerable jobsList) => + new Table + { + 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), + }, + Rows = jobsList.ToList() + }; + + internal static Table ToJupyterTable(this IQuantumMachineJob job) => + new Table + { + Columns = new List<(string, Func)> + { + ("JobId", job => job.Id), + ("JobStatus", job => job.Status), + ("JobUri", job => job.Uri.ToString()), + }, + Rows = new List() { job } + }; + + internal static Table ToJupyterTable(this IQuantumClient quantumClient) => + new Table + { + Columns = new List<(string, Func)> + { + ("SubscriptionId", quantumClient => quantumClient.SubscriptionId), + ("ResourceGroupName", quantumClient => quantumClient.ResourceGroupName), + ("WorkspaceName", quantumClient => quantumClient.WorkspaceName), + }, + Rows = new List() { quantumClient } + }; + + internal static Table ToJupyterTable(this IEnumerable providerStatusList) => + new Table + { + Columns = new List<(string, Func)> + { + ("TargetId", target => target.Id), + ("CurrentAvailability", target => target.CurrentAvailability), + ("AverageQueueTime", target => target.AverageQueueTime.ToString()), + ("StatusPage", target => target.StatusPage), + }, + Rows = providerStatusList.SelectMany(provider => provider.Targets).ToList() + }; } } diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 8efd69e5a7..d98c420431 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -1,21 +1,117 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. #nullable enable -using System; -using System.Collections.Generic; -using System.Text; -using Microsoft.Jupyter.Core; +using System.ComponentModel; using System.Threading.Tasks; +using Microsoft.Jupyter.Core; namespace Microsoft.Quantum.IQSharp.AzureClient { + /// + /// Describes possible error results from methods. + /// + public enum AzureClientError + { + /// + /// Method completed with an unknown error. + /// + [Description(Resources.AzureClientErrorUnknownError)] + UnknownError = 0, + + /// + /// No connection has been made to any Azure Quantum workspace. + /// + [Description(Resources.AzureClientErrorNotConnected)] + NotConnected = 1, + + /// + /// A target has not yet been configured for job submission. + /// + [Description(Resources.AzureClientErrorNoTarget)] + NoTarget = 2, + + /// + /// A job meeting the specified criteria was not found. + /// + [Description(Resources.AzureClientErrorJobNotFound)] + JobNotFound = 3, + + /// + /// No Q# operation name was provided where one was required. + /// + [Description(Resources.AzureClientErrorNoOperationName)] + NoOperationName = 4, + + /// + /// Authentication with the Azure service failed. + /// + [Description(Resources.AzureClientErrorAuthenticationFailed)] + AuthenticationFailed = 5, + + /// + /// A workspace meeting the specified criteria was not found. + /// + [Description(Resources.AzureClientErrorWorkspaceNotFound)] + WorkspaceNotFound = 6, + } + /// /// This service is capable of connecting to Azure Quantum workspaces /// and submitting jobs. /// public interface IAzureClient { + /// + /// Connects to the specified Azure Quantum workspace, first logging into Azure if necessary. + /// + public Task ConnectAsync( + IChannel channel, + string subscriptionId, + string resourceGroupName, + string workspaceName, + string storageAccountConnectionString, + bool forceLogin = false); + + /// + /// Prints a string describing the current connection status. + /// + public Task PrintConnectionStatusAsync( + IChannel channel); + + /// + /// Submits the specified Q# operation as a job to the currently active target. + /// + public Task SubmitJobAsync( + IChannel channel, + IOperationResolver operationResolver, + string operationName); + + /// + /// Sets the specified target for job submission. + /// + public Task SetActiveTargetAsync( + IChannel channel, + string targetName); + + /// + /// Prints the list of targets currently provisioned in the current workspace. + /// + public Task PrintTargetListAsync( + IChannel channel); + + /// + /// Prints the job status corresponding to the given job ID. + /// + public Task PrintJobStatusAsync( + IChannel channel, + string jobId); + + /// + /// Prints a list of all jobs in the current workspace. + /// + public Task PrintJobListAsync( + IChannel channel); } } diff --git a/src/AzureClient/Magic/AzureClientMagicBase.cs b/src/AzureClient/Magic/AzureClientMagicBase.cs new file mode 100644 index 0000000000..f7eac3b5ce --- /dev/null +++ b/src/AzureClient/Magic/AzureClientMagicBase.cs @@ -0,0 +1,45 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Threading.Tasks; +using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// Base class used for Azure Client magic commands. + /// + public abstract class AzureClientMagicBase : AbstractMagic + { + /// + /// The object used by this magic command to interact with Azure. + /// + public IAzureClient AzureClient { get; } + + /// + /// Constructs the Azure Client magic command with the specified keyword + /// and documentation. + /// + /// The object used to interact with Azure. + /// The name used to invoke the magic command. + /// Documentation describing the usage of this magic command. + public AzureClientMagicBase(IAzureClient azureClient, string keyword, Documentation docs): + base(keyword, docs) + { + this.AzureClient = azureClient; + } + + /// + public override ExecutionResult Run(string input, IChannel channel) => + RunAsync(input, channel).GetAwaiter().GetResult(); + + /// + /// Executes the magic command functionality for the given input. + /// + public abstract Task RunAsync(string input, IChannel channel); + } +} diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs new file mode 100644 index 0000000000..4a325a923b --- /dev/null +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -0,0 +1,107 @@ +// 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.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to connect to an Azure workspace. + /// + public class ConnectMagic : AzureClientMagicBase + { + private const string + ParameterNameLogin = "login", + ParameterNameStorageAccountConnectionString = "storageAccountConnectionString", + ParameterNameSubscriptionId = "subscriptionId", + ParameterNameResourceGroupName = "resourceGroupName", + ParameterNameWorkspaceName = "workspaceName"; + + /// + /// Constructs a new magic command given an IAzureClient object. + /// + public ConnectMagic(IAzureClient azureClient) : + base(azureClient, + "connect", + new Documentation + { + Summary = "Connects to an Azure workspace or displays current connection status.", + Description = @" + This magic command allows for connecting to an Azure Quantum workspace + as specified by a valid subscription ID, resource group name, workspace name, + and storage account connection string. + ".Dedent(), + Examples = new[] + { + @" + Print information about the current connection: + ``` + In []: %connect + Out[]: Connected to WORKSPACE_NAME + ``` + ".Dedent(), + + $@" + Connect to an Azure Quantum workspace: + ``` + In []: %connect {ParameterNameSubscriptionId}=SUBSCRIPTION_ID + {ParameterNameResourceGroupName}=RESOURCE_GROUP_NAME + {ParameterNameWorkspaceName}=WORKSPACE_NAME + {ParameterNameStorageAccountConnectionString}=CONNECTION_STRING + Out[]: Connected to WORKSPACE_NAME + ``` + ".Dedent(), + + $@" + Connect to an Azure Quantum workspace and force a credential prompt: + ``` + In []: %connect {ParameterNameLogin} + {ParameterNameSubscriptionId}=SUBSCRIPTION_ID + {ParameterNameResourceGroupName}=RESOURCE_GROUP_NAME + {ParameterNameWorkspaceName}=WORKSPACE_NAME + {ParameterNameStorageAccountConnectionString}=CONNECTION_STRING + Out[]: To sign in, use a web browser to open the page https://microsoft.com/devicelogin + and enter the code [login code] to authenticate. + Connected to WORKSPACE_NAME + ``` + Use the `{ParameterNameLogin}` option if you want to bypass any saved or cached + credentials when connecting to Azure. + ".Dedent() + } + }) {} + + /// + /// Connects to an Azure workspace given a subscription ID, resource group name, + /// workspace name, and connection string as a JSON-encoded object. + /// + public override async Task RunAsync(string input, IChannel channel) + { + var inputParameters = ParseInputParameters(input); + + var storageAccountConnectionString = inputParameters.DecodeParameter(ParameterNameStorageAccountConnectionString); + if (string.IsNullOrEmpty(storageAccountConnectionString)) + { + return await AzureClient.PrintConnectionStatusAsync(channel); + } + + var subscriptionId = inputParameters.DecodeParameter(ParameterNameSubscriptionId); + var resourceGroupName = inputParameters.DecodeParameter(ParameterNameResourceGroupName); + var workspaceName = inputParameters.DecodeParameter(ParameterNameWorkspaceName); + var forceLogin = inputParameters.DecodeParameter(ParameterNameLogin, defaultValue: false); + return await AzureClient.ConnectAsync( + channel, + subscriptionId, + resourceGroupName, + workspaceName, + storageAccountConnectionString, + forceLogin); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs new file mode 100644 index 0000000000..d39c6927da --- /dev/null +++ b/src/AzureClient/Magic/StatusMagic.cs @@ -0,0 +1,74 @@ +// 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.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to connect to an Azure workspace. + /// + public class StatusMagic : AzureClientMagicBase + { + private const string + ParameterNameJobId = "jobId"; + + /// + /// Constructs a new magic command given an IAzureClient object. + /// + public StatusMagic(IAzureClient azureClient) : + base(azureClient, + "status", + new Documentation + { + Summary = "Displays status for jobs in the current Azure Quantum workspace.", + Description = @" + This magic command allows for displaying status of jobs in the current + Azure Quantum workspace. If a valid job ID is provided as an argument, the + detailed status of that job will be displayed; otherwise, a list of all jobs + created in the current session will be displayed. + ".Dedent(), + Examples = new[] + { + @" + Print status about a specific job: + ``` + In []: %status JOB_ID + Out[]: JOB_ID: + ``` + ".Dedent(), + + @" + Print status about all jobs created in the current session: + ``` + In []: %status + Out[]: + ``` + ".Dedent() + } + }) {} + + /// + /// Displays the status corresponding to a given job ID, if provided, + /// or all jobs in the active workspace. + /// + public override async Task RunAsync(string input, IChannel channel) + { + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameJobId); + if (inputParameters.ContainsKey(ParameterNameJobId)) + { + string jobId = inputParameters.DecodeParameter(ParameterNameJobId); + return await AzureClient.PrintJobStatusAsync(channel, jobId); + } + + return await AzureClient.PrintJobListAsync(channel); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs new file mode 100644 index 0000000000..d89da233b3 --- /dev/null +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -0,0 +1,66 @@ +// 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.Jupyter.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to submit jobs to an Azure Quantum workspace. + /// + public class SubmitMagic : AzureClientMagicBase + { + /// + /// The symbol resolver used by this magic command to find + /// operations or functions to be simulated. + /// + public IOperationResolver OperationResolver { get; } + + /// + /// Constructs a new magic command given a resolver used to find + /// operations and functions and an IAzureClient object. + /// + public SubmitMagic(IOperationResolver operationResolver, IAzureClient azureClient) : + base(azureClient, + "submit", + new Documentation + { + Summary = "Submits a job to an Azure Quantum workspace.", + Description = @" + This magic command allows for submitting a job to an Azure Quantum workspace + corresponding to the Q# operation provided as an argument. + + The Azure Quantum workspace must previously have been initialized + using the %connect magic command. + ".Dedent(), + Examples = new[] + { + @" + Submit an operation as a new job to the current Azure Quantum workspace: + ``` + In []: %submit OPERATION_NAME + Out[]: Submitted job JOB_ID + ``` + ".Dedent(), + } + }) => + this.OperationResolver = operationResolver; + + /// + /// Submits a new job to an Azure Quantum workspace given a Q# operation + /// name that is present in the current Q# Jupyter workspace. + /// + public override async Task RunAsync(string input, IChannel channel) + { + Dictionary keyValuePairs = ParseInputParameters(input); + var operationName = keyValuePairs.Keys.FirstOrDefault(); + return await AzureClient.SubmitJobAsync(channel, OperationResolver, operationName); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs new file mode 100644 index 0000000000..4108009cc6 --- /dev/null +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -0,0 +1,76 @@ +// 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.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to view or set target information for an Azure Quantum workspace. + /// + public class TargetMagic : AzureClientMagicBase + { + private const string + ParameterNameTargetName = "name"; + + /// + /// Constructs a new magic command given an IAzureClient object. + /// + public TargetMagic(IAzureClient azureClient) : + base(azureClient, + "target", + new Documentation + { + Summary = "Views or sets the target for job submission to an Azure Quantum workspace.", + Description = @" + This magic command allows for specifying a target for job submission + to an Azure Quantum workspace, or viewing the list of all available targets. + + The Azure Quantum workspace must previously have been initialized + using the %connect magic command, and the specified target must be + available in the workspace. + ".Dedent(), + Examples = new[] + { + @" + Set the current target for job submission: + ``` + In []: %target TARGET_NAME + Out[]: Active target is now TARGET_NAME + ``` + ".Dedent(), + @" + View the current target and all available targets in the current Azure Quantum workspace: + ``` + In []: %target + Out[]: + ``` + ".Dedent(), + } + }) + { + } + + /// + /// Sets or views the target for job submission to the current Azure Quantum workspace. + /// + public override async Task RunAsync(string input, IChannel channel) + { + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameTargetName); + if (inputParameters.ContainsKey(ParameterNameTargetName)) + { + string targetName = inputParameters.DecodeParameter(ParameterNameTargetName); + return await AzureClient.SetActiveTargetAsync(channel, targetName); + } + + return await AzureClient.PrintTargetListAsync(channel); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Resources.cs b/src/AzureClient/Resources.cs new file mode 100644 index 0000000000..6c3de47cc0 --- /dev/null +++ b/src/AzureClient/Resources.cs @@ -0,0 +1,31 @@ +#nullable enable + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// This class contains resources that will eventually be exposed to localization. + /// + internal static class Resources + { + public const string AzureClientErrorUnknownError = + "An unknown error occurred."; + + public const string AzureClientErrorNotConnected = + "Not connected to any Azure Quantum workspace."; + + public const string AzureClientErrorNoTarget = + "No execution target has been configured for Azure Quantum job submission."; + + public const string AzureClientErrorJobNotFound = + "No job with the given ID was found in the current Azure Quantum workspace."; + + public const string AzureClientErrorNoOperationName = + "No Q# operation name was specified for Azure Quantum job submission."; + + public const string AzureClientErrorAuthenticationFailed = + "Failed to authenticate to the specified Azure Quantum workspace."; + + public const string AzureClientErrorWorkspaceNotFound = + "No Azure Quantum workspace was found that matches the specified criteria."; + } +} diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 249d061dcd..b67e57c334 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,9 +34,9 @@ - - - + + + diff --git a/src/Jupyter/Extensions.cs b/src/Jupyter/Extensions.cs index 5a602d89d4..b016d9c328 100644 --- a/src/Jupyter/Extensions.cs +++ b/src/Jupyter/Extensions.cs @@ -1,6 +1,8 @@ // Copyright (c) Microsoft Corporation. All rights reserved. // Licensed under the MIT License. +#nullable enable + using System; using System.Collections.Generic; using System.Collections.Immutable; @@ -9,6 +11,7 @@ using Microsoft.Quantum.Simulation.Common; using Microsoft.Quantum.Simulation.Core; using Microsoft.Quantum.Simulation.Simulators; +using Newtonsoft.Json; namespace Microsoft.Quantum.IQSharp.Jupyter { @@ -174,5 +177,17 @@ public static string Dedent(this string text) var leftTrimRegex = new Regex(@$"^[ \t]{{{minWhitespace}}}", RegexOptions.Multiline); return leftTrimRegex.Replace(text, ""); } + + /// + /// Retrieves and JSON-decodes the value for the given parameter name. + /// + public static T DecodeParameter(this Dictionary parameters, string parameterName, T defaultValue = default) + { + if (!parameters.TryGetValue(parameterName, out string parameterValue)) + { + return defaultValue; + } + return (T)(JsonConvert.DeserializeObject(parameterValue)) ?? defaultValue; + } } } diff --git a/src/Kernel/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs similarity index 51% rename from src/Kernel/Magic/AbstractMagic.cs rename to src/Jupyter/Magic/AbstractMagic.cs index 9babaf2ca1..1b0cd24c3a 100644 --- a/src/Kernel/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -3,12 +3,15 @@ using System; using System.Collections.Generic; +using System.IO; +using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Common; -using Microsoft.Quantum.IQSharp.Kernel; +using Microsoft.Quantum.QsCompiler.Serialization; +using Newtonsoft.Json.Linq; -namespace Microsoft.Quantum.IQSharp.Kernel +namespace Microsoft.Quantum.IQSharp.Jupyter { /// /// Abstract base class for IQ# magic symbols. @@ -78,6 +81,67 @@ public static (string, Dictionary) ParseInput(string input) return (name, args); } + /// + /// Parses the input to a magic command, interpreting the input as + /// a name followed by a JSON-serialized dictionary. + /// + public static Dictionary JsonToDict(string input) => + !string.IsNullOrEmpty(input) ? JsonConverters.JsonToDict(input) : new Dictionary { }; + + /// + /// Parses the input parameters for a given magic symbol and returns a + /// Dictionary with the names and values of the parameters, + /// where the values of the Dictionary are JSON-serialized objects. + /// + public Dictionary ParseInputParameters(string input, string firstParameterInferredName = "") + { + Dictionary inputParameters = new Dictionary(); + + var args = input.Split(null as char[], StringSplitOptions.RemoveEmptyEntries); + + // If we are expecting a first inferred-name parameter, see if it exists. + // If so, serialize it to the dictionary as JSON and remove it from the list of args. + if (args.Length > 0 && + !args[0].StartsWith("{") && + !args[0].Contains("=") && + !string.IsNullOrEmpty(firstParameterInferredName)) + { + using (var writer = new StringWriter()) + { + Json.Serializer.Serialize(writer, args[0]); + inputParameters[firstParameterInferredName] = writer.ToString(); + } + args = args.Where((_, index) => index != 0).ToArray(); + } + + // See if the remaining arguments look like JSON. If so, try to parse as JSON. + // Otherwise, try to parse as key=value pairs and serialize into the dictionary as JSON. + if (args.Length > 0 && args[0].StartsWith("{")) + { + var jsonArgs = JsonToDict(string.Join(" ", args)); + foreach (var (key, jsonValue) in jsonArgs) + { + inputParameters[key] = jsonValue; + } + } + else + { + foreach (string arg in args) + { + var tokens = arg.Split("=", 2); + var key = tokens[0].Trim(); + var value = (tokens.Length == 1) ? true as object : tokens[1].Trim() as object; + using (var writer = new StringWriter()) + { + Json.Serializer.Serialize(writer, value); + inputParameters[key] = writer.ToString(); + } + } + } + + return inputParameters; + } + /// /// A method to be run when the magic command is executed. /// diff --git a/src/Kernel/Magic/LsMagicMagic.cs b/src/Kernel/Magic/LsMagicMagic.cs index 891d2f92b6..05e1037e00 100644 --- a/src/Kernel/Magic/LsMagicMagic.cs +++ b/src/Kernel/Magic/LsMagicMagic.cs @@ -5,7 +5,7 @@ using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp; -using Microsoft.Quantum.IQSharp.Kernel; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.Kernel { diff --git a/src/Kernel/Magic/Simulate.cs b/src/Kernel/Magic/Simulate.cs index d3cea8b17a..2b94eb9b06 100644 --- a/src/Kernel/Magic/Simulate.cs +++ b/src/Kernel/Magic/Simulate.cs @@ -18,6 +18,9 @@ namespace Microsoft.Quantum.IQSharp.Kernel /// public class SimulateMagic : AbstractMagic { + private const string + ParameterNameOperationName = "operationName"; + /// /// Constructs a new magic command given a resolver used to find /// operations and functions, and a configuration source used to set @@ -55,15 +58,16 @@ public override ExecutionResult Run(string input, IChannel channel) => /// public async Task RunAsync(string input, IChannel channel) { - var (name, args) = ParseInput(input); + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); + var name = inputParameters.DecodeParameter(ParameterNameOperationName); var symbol = SymbolResolver.Resolve(name) as IQSharpSymbol; if (symbol == null) throw new InvalidOperationException($"Invalid operation name: {name}"); using var qsim = new QuantumSimulator() .WithJupyterDisplay(channel, ConfigurationSource) .WithStackTraceDisplay(channel); - var value = await symbol.Operation.RunAsync(qsim, args); + var value = await symbol.Operation.RunAsync(qsim, inputParameters); return value.ToExecutionResult(); } } diff --git a/src/Kernel/Magic/WhoMagic.cs b/src/Kernel/Magic/WhoMagic.cs index 7efadeddb5..9a8c514f4d 100644 --- a/src/Kernel/Magic/WhoMagic.cs +++ b/src/Kernel/Magic/WhoMagic.cs @@ -5,7 +5,7 @@ using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp; -using Microsoft.Quantum.IQSharp.Kernel; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.Kernel { diff --git a/src/Kernel/Magic/WorkspaceMagic.cs b/src/Kernel/Magic/WorkspaceMagic.cs index 0f347295d3..7ef9ee2996 100644 --- a/src/Kernel/Magic/WorkspaceMagic.cs +++ b/src/Kernel/Magic/WorkspaceMagic.cs @@ -5,7 +5,7 @@ using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Common; -using Microsoft.Quantum.IQSharp.Kernel; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.Kernel { diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py new file mode 100644 index 0000000000..2182a20b30 --- /dev/null +++ b/src/Python/qsharp/azure.py @@ -0,0 +1,47 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +## +# azure.py: enables using Q# quantum execution on Azure Quantum from Python. +## +# Copyright (c) Microsoft Corporation. All rights reserved. +# Licensed under the MIT License. +## + +## IMPORTS ## + +import qsharp +import json +import typing +from typing import List, Dict, Callable, Any + +from qsharp.serialization import map_tuples +from typing import List, Tuple, Dict, Iterable +from enum import Enum + +## LOGGING ## + +import logging +logger = logging.getLogger(__name__) + +## EXPORTS ## + +__all__ = [ + 'connect', + 'target', + 'submit', + 'status' +] + +## FUNCTIONS ## + +def connect(**params) -> Any: + return qsharp.client._execute_magic(f"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) + +def submit(op, **params) -> Any: + return qsharp.client._execute_callable_magic("submit", 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) diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs new file mode 100644 index 0000000000..730554ede8 --- /dev/null +++ b/src/Tests/AzureClientMagicTests.cs @@ -0,0 +1,181 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp; +using Microsoft.Quantum.IQSharp.AzureClient; + +namespace Tests.IQSharp +{ + public static class AzureClientMagicTestExtensions + { + public static void Test(this MagicSymbol magic, string input, ExecuteStatus expected = ExecuteStatus.Ok) + { + var result = magic.Execute(input, new MockChannel()).GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == expected); + } + } + + [TestClass] + public class AzureClientMagicTests + { + private readonly string subscriptionId = "TEST_SUBSCRIPTION_ID"; + private readonly string resourceGroupName = "TEST_RESOURCE_GROUP_NAME"; + private readonly string workspaceName = "TEST_WORKSPACE_NAME"; + 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 TestConnectMagic() + { + var azureClient = new MockAzureClient(); + var connectMagic = new ConnectMagic(azureClient); + + // unrecognized input + connectMagic.Test($"invalid"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintConnectionStatus); + + // valid input + connectMagic.Test( + @$"subscriptionId={subscriptionId} + resourceGroupName={resourceGroupName} + workspaceName={workspaceName} + storageAccountConnectionString={storageAccountConnectionString}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.Connect); + Assert.IsFalse(azureClient.ForceLogin); + Assert.AreEqual(azureClient.ConnectionString, storageAccountConnectionString); + + // valid input with forced login + connectMagic.Test( + @$"login subscriptionId={subscriptionId} + resourceGroupName={resourceGroupName} + workspaceName={workspaceName} + storageAccountConnectionString={storageAccountConnectionString}"); + + Assert.IsTrue(azureClient.ForceLogin); + } + + [TestMethod] + public void TestStatusMagic() + { + // no arguments - should print job list + var azureClient = new MockAzureClient(); + var statusMagic = new StatusMagic(azureClient); + statusMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintJobList); + + // single argument - should print job status + azureClient = new MockAzureClient(); + statusMagic = new StatusMagic(azureClient); + statusMagic.Test($"{jobId}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintJobStatus); + } + + [TestMethod] + public void TestSubmitMagic() + { + // no arguments + var azureClient = new MockAzureClient(); + var operationResolver = new MockOperationResolver(); + var submitMagic = new SubmitMagic(operationResolver, azureClient); + submitMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); + + // single argument + submitMagic.Test($"{operationName}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); + Assert.IsTrue(azureClient.SubmittedJobs.Contains(operationName)); + } + + [TestMethod] + public void TestTargetMagic() + { + // no arguments - should print target list + var azureClient = new MockAzureClient(); + var targetMagic = new TargetMagic(azureClient); + targetMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintTargetList); + + // single argument - should set active target + azureClient = new MockAzureClient(); + targetMagic = new TargetMagic(azureClient); + targetMagic.Test(targetName); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); + } + } + + internal enum AzureClientAction + { + None, + Connect, + SetActiveTarget, + SubmitJob, + PrintConnectionStatus, + PrintJobList, + PrintJobStatus, + PrintTargetList, + } + + public class MockAzureClient : IAzureClient + { + internal AzureClientAction LastAction = AzureClientAction.None; + internal string ConnectionString = string.Empty; + internal bool ForceLogin = false; + internal string ActiveTargetName = string.Empty; + internal List SubmittedJobs = new List(); + + public async Task SetActiveTargetAsync(IChannel channel, string targetName) + { + LastAction = AzureClientAction.SetActiveTarget; + ActiveTargetName = targetName; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task SubmitJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName) + { + LastAction = AzureClientAction.SubmitJob; + SubmittedJobs.Add(operationName); + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, string storageAccountConnectionString, bool forceLogin) + { + LastAction = AzureClientAction.Connect; + ConnectionString = storageAccountConnectionString; + ForceLogin = forceLogin; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task PrintConnectionStatusAsync(IChannel channel) + { + LastAction = AzureClientAction.PrintConnectionStatus; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task PrintJobListAsync(IChannel channel) + { + LastAction = AzureClientAction.PrintJobList; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task PrintJobStatusAsync(IChannel channel, string jobId) + { + LastAction = AzureClientAction.PrintJobStatus; + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task PrintTargetListAsync(IChannel channel) + { + LastAction = AzureClientAction.PrintTargetList; + return ExecuteStatus.Ok.ToExecutionResult(); + } + } +} diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index ba18985f1c..6c5eb88334 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -4,19 +4,37 @@ #nullable enable using Microsoft.VisualStudio.TestTools.UnitTesting; -using Microsoft.Quantum.IQSharp.AzureClient; using System.Linq; -using System.Collections.Generic; +using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.AzureClient; namespace Tests.IQSharp { + public static class AzureClientTestExtensions + { + } + [TestClass] public class AzureClientTests { + private readonly string subscriptionId = "TEST_SUBSCRIPTION_ID"; + private readonly string resourceGroupName = "TEST_RESOURCE_GROUP_NAME"; + private readonly string workspaceName = "TEST_WORKSPACE_NAME"; + 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 TestNothing() + public void TestTargets() { - Assert.IsTrue(true); + var azureClient = new AzureClient(); + + var result = azureClient.SetActiveTargetAsync(new MockChannel(), targetName).GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == ExecuteStatus.Ok); + + result = azureClient.PrintTargetListAsync(new MockChannel()).GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == ExecuteStatus.Error); } } } diff --git a/src/Tests/IQsharpEngineTests.cs b/src/Tests/IQsharpEngineTests.cs index bd9d9b0f64..a4449ada95 100644 --- a/src/Tests/IQsharpEngineTests.cs +++ b/src/Tests/IQsharpEngineTests.cs @@ -15,6 +15,7 @@ using Newtonsoft.Json; using System.Data; +using Microsoft.Quantum.IQSharp.AzureClient; #pragma warning disable VSTHRD200 // Use "Async" suffix for async methods @@ -447,6 +448,12 @@ public void TestResolveMagic() symbol = resolver.Resolve("%foo"); Assert.IsNull(symbol); + + // AzureClient-provided commands + Assert.IsNotNull(resolver.Resolve("%connect")); + Assert.IsNotNull(resolver.Resolve("%status")); + Assert.IsNotNull(resolver.Resolve("%submit")); + Assert.IsNotNull(resolver.Resolve("%target")); } } } diff --git a/src/Tests/Mocks.cs b/src/Tests/Mocks.cs index 11fcb4f645..84d06abe64 100644 --- a/src/Tests/Mocks.cs +++ b/src/Tests/Mocks.cs @@ -117,4 +117,12 @@ public IUpdatableDisplay DisplayUpdatable(object displayable) public void Stdout(string message) => msgs.Add(message); } + + public class MockOperationResolver : IOperationResolver + { + public OperationInfo Resolve(string input) + { + return new OperationInfo(null, null); + } + } } diff --git a/src/Tests/Startup.cs b/src/Tests/Startup.cs index c370d67d10..bc6ac12d15 100644 --- a/src/Tests/Startup.cs +++ b/src/Tests/Startup.cs @@ -9,6 +9,7 @@ using Microsoft.Extensions.Options; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp; +using Microsoft.Quantum.IQSharp.AzureClient; using Microsoft.Quantum.IQSharp.Kernel; namespace Tests.IQSharp @@ -34,6 +35,7 @@ internal static ServiceProvider CreateServiceProvider(string workspaceFolder) services.AddTelemetry(); services.AddIQSharp(); services.AddIQSharpKernel(); + services.AddAzureClient(); var serviceProvider = services.BuildServiceProvider(); serviceProvider.GetRequiredService(); diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index 44d378812c..5c8675d093 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -6,18 +6,18 @@ }, "AllowedHosts": "*", "DefaultPackageVersions": [ - "Microsoft.Quantum.Compiler::0.11.2004.2825", + "Microsoft.Quantum.Compiler::0.11.2005.1420-beta", - "Microsoft.Quantum.CsharpGeneration::0.11.2004.2825", - "Microsoft.Quantum.Development.Kit::0.11.2004.2825", - "Microsoft.Quantum.Simulators::0.11.2004.2825", - "Microsoft.Quantum.Xunit::0.11.2004.2825", + "Microsoft.Quantum.CsharpGeneration::0.11.2005.1420-beta", + "Microsoft.Quantum.Development.Kit::0.11.2005.1420-beta", + "Microsoft.Quantum.Simulators::0.11.2005.1420-beta", + "Microsoft.Quantum.Xunit::0.11.2005.1420-beta", - "Microsoft.Quantum.Standard::0.11.2004.2825", - "Microsoft.Quantum.Chemistry::0.11.2004.2825", - "Microsoft.Quantum.Chemistry.Jupyter::0.11.2004.2825", - "Microsoft.Quantum.Numerics::0.11.2004.2825", + "Microsoft.Quantum.Standard::0.11.2005.1420-beta", + "Microsoft.Quantum.Chemistry::0.11.2005.1420-beta", + "Microsoft.Quantum.Chemistry.Jupyter::0.11.2005.1420-beta", + "Microsoft.Quantum.Numerics::0.11.2005.1420-beta", - "Microsoft.Quantum.Research::0.11.2004.2825" + "Microsoft.Quantum.Research::0.11.2005.1420-beta" ] } From 79cb1ee7d78f72c2deec0559a726f49e554cbb54 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 28 May 2020 17:50:00 -0400 Subject: [PATCH 18/44] Add remaining AzureClient magic commands (#150) * Add more magics and clean up IAzureClient * Update magic behaviors, add dogfood support * Minor documentation fix * Update tests to match new functionality * Clean up AzureEnvironment class * Work around syntax highlighting error * Improve exception handling and documentation * Use Enum.TryParse() in AzureEnvironment.Create() --- src/AzureClient/AzureClient.cs | 208 +++++++++++++++++++------- src/AzureClient/AzureClient.csproj | 2 +- src/AzureClient/AzureEnvironment.cs | 110 ++++++++++++++ src/AzureClient/IAzureClient.cs | 64 ++++++-- src/AzureClient/Magic/ConnectMagic.cs | 53 ++++--- src/AzureClient/Magic/ExecuteMagic.cs | 78 ++++++++++ src/AzureClient/Magic/JobsMagic.cs | 58 +++++++ src/AzureClient/Magic/OutputMagic.cs | 78 ++++++++++ src/AzureClient/Magic/StatusMagic.cs | 50 ++++--- src/AzureClient/Magic/SubmitMagic.cs | 31 ++-- src/AzureClient/Magic/TargetMagic.cs | 27 ++-- src/Core/Core.csproj | 6 +- src/Tests/AzureClientMagicTests.cs | 113 ++++++++++---- src/Tests/AzureClientTests.cs | 4 +- src/Tests/IQsharpEngineTests.cs | 11 +- src/Tool/appsettings.json | 20 +-- 16 files changed, 736 insertions(+), 177 deletions(-) create mode 100644 src/AzureClient/AzureEnvironment.cs create mode 100644 src/AzureClient/Magic/ExecuteMagic.cs create mode 100644 src/AzureClient/Magic/JobsMagic.cs create mode 100644 src/AzureClient/Magic/OutputMagic.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 371ab20c95..c581d8e086 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -13,6 +13,10 @@ using Microsoft.Identity.Client.Extensions.Msal; using Microsoft.Jupyter.Core; using Microsoft.Quantum.Simulation.Core; +using Microsoft.Rest.Azure; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Azure.Quantum.Storage; +using Microsoft.Azure.Quantum; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -23,7 +27,9 @@ public class AzureClient : IAzureClient private string ActiveTargetName { get; set; } = string.Empty; private AuthenticationResult? AuthenticationResult { get; set; } private IQuantumClient? QuantumClient { get; set; } - private Azure.Quantum.Workspace? ActiveWorkspace { get; set; } + private IPage? ProviderStatusList { get; set; } + private Azure.Quantum.IWorkspace? ActiveWorkspace { get; set; } + private string MostRecentJobId { get; set; } = string.Empty; /// public async Task ConnectAsync( @@ -32,13 +38,18 @@ public async Task ConnectAsync( string resourceGroupName, string workspaceName, string storageAccountConnectionString, - bool forceLoginPrompt = false) + bool refreshCredentials = false) { ConnectionString = storageAccountConnectionString; - var clientId = "84ba0947-6c53-4dd2-9ca9-b3694761521b"; // Microsoft Quantum Development Kit - var authority = "https://login.microsoftonline.com/common"; - var msalApp = PublicClientApplicationBuilder.Create(clientId).WithAuthority(authority).Build(); + var azureEnvironmentEnvVarName = "AZURE_QUANTUM_ENV"; + var azureEnvironmentName = System.Environment.GetEnvironmentVariable(azureEnvironmentEnvVarName); + var azureEnvironment = AzureEnvironment.Create(azureEnvironmentName, subscriptionId); + + var msalApp = PublicClientApplicationBuilder + .Create(azureEnvironment.ClientId) + .WithAuthority(azureEnvironment.Authority) + .Build(); // Register the token cache for serialization var cacheFileName = "aad.bin"; @@ -49,20 +60,18 @@ public async Task ConnectAsync( cacheDirectory = Path.Join(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".azure-quantum"); } - var storageCreationProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, clientId).Build(); + var storageCreationProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, azureEnvironment.ClientId).Build(); var cacheHelper = await MsalCacheHelper.CreateAsync(storageCreationProperties); cacheHelper.RegisterCache(msalApp.UserTokenCache); - var scopes = new List() { "https://quantum.microsoft.com/Jobs.ReadWrite" }; - - bool shouldShowLoginPrompt = forceLoginPrompt; + bool shouldShowLoginPrompt = refreshCredentials; if (!shouldShowLoginPrompt) - { + { try { var accounts = await msalApp.GetAccountsAsync(); AuthenticationResult = await msalApp.AcquireTokenSilent( - scopes, accounts.FirstOrDefault()).WithAuthority(msalApp.Authority).ExecuteAsync(); + azureEnvironment.Scopes, accounts.FirstOrDefault()).WithAuthority(msalApp.Authority).ExecuteAsync(); } catch (MsalUiRequiredException) { @@ -72,7 +81,8 @@ public async Task ConnectAsync( if (shouldShowLoginPrompt) { - AuthenticationResult = await msalApp.AcquireTokenWithDeviceCode(scopes, + AuthenticationResult = await msalApp.AcquireTokenWithDeviceCode( + azureEnvironment.Scopes, deviceCodeResult => { channel.Stdout(deviceCodeResult.Message); @@ -90,16 +100,19 @@ public async Task ConnectAsync( { SubscriptionId = subscriptionId, ResourceGroupName = resourceGroupName, - WorkspaceName = workspaceName + WorkspaceName = workspaceName, + BaseUri = azureEnvironment.BaseUri, }; ActiveWorkspace = new Azure.Quantum.Workspace( - QuantumClient.SubscriptionId, QuantumClient.ResourceGroupName, - QuantumClient.WorkspaceName, AuthenticationResult?.AccessToken); + QuantumClient.SubscriptionId, + QuantumClient.ResourceGroupName, + QuantumClient.WorkspaceName, + AuthenticationResult?.AccessToken, + azureEnvironment.BaseUri); try { - var jobsList = await QuantumClient.Jobs.ListAsync(); - channel.Stdout($"Successfully connected to Azure Quantum workspace {workspaceName}."); + ProviderStatusList = await QuantumClient.Providers.GetStatusAsync(); } catch (Exception e) { @@ -107,42 +120,51 @@ public async Task ConnectAsync( return AzureClientError.WorkspaceNotFound.ToExecutionResult(); } - return QuantumClient.ToJupyterTable().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(); } /// - public async Task PrintConnectionStatusAsync(IChannel channel) => - QuantumClient == null - ? AzureClientError.NotConnected.ToExecutionResult() - : QuantumClient.ToJupyterTable().ToExecutionResult(); + public async Task GetConnectionStatusAsync(IChannel channel) + { + if (QuantumClient == null || ProviderStatusList == null) + { + return AzureClientError.NotConnected.ToExecutionResult(); + } - /// - public async Task SubmitJobAsync( + channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}."); + + // TODO: Add encoder for IPage rather than calling ToJupyterTable() here directly. + return ProviderStatusList.ToJupyterTable().ToExecutionResult(); + } + + private async Task SubmitOrExecuteJobAsync( IChannel channel, IOperationResolver operationResolver, - string operationName) + string operationName, + bool execute) { if (ActiveWorkspace == null) { - channel.Stderr("Please call %connect before submitting a job."); + channel.Stderr("Please call %azure.connect before submitting a job."); return AzureClientError.NotConnected.ToExecutionResult(); } if (ActiveTargetName == null) { - channel.Stderr("Please call %target before submitting a job."); + channel.Stderr("Please call %azure.target before submitting a job."); return AzureClientError.NoTarget.ToExecutionResult(); } if (string.IsNullOrEmpty(operationName)) { - channel.Stderr("Please pass a valid Q# operation name to %submit."); + var commandName = execute ? "%azure.execute" : "%azure.submit"; + channel.Stderr($"Please pass a valid Q# operation name to {commandName}."); return AzureClientError.NoOperationName.ToExecutionResult(); } - var operationInfo = operationResolver.Resolve(operationName); - var entryPointInfo = new EntryPointInfo(operationInfo.RoslynType); - var entryPointInput = QVoid.Instance; var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTargetName, ConnectionString); if (machine == null) { @@ -150,8 +172,46 @@ public async Task SubmitJobAsync( return AzureClientError.NoTarget.ToExecutionResult(); } - var job = await machine.SubmitAsync(entryPointInfo, entryPointInput); - return job.ToJupyterTable().ToExecutionResult(); + var operationInfo = operationResolver.Resolve(operationName); + var entryPointInfo = new EntryPointInfo(operationInfo.RoslynType); + var entryPointInput = QVoid.Instance; + + if (execute) + { + var output = await machine.ExecuteAsync(entryPointInfo, entryPointInput); + MostRecentJobId = output.Job.Id; + // TODO: Add encoder for IQuantumMachineOutput rather than returning the Histogram directly + return output.Histogram.ToExecutionResult(); + } + else + { + var job = await machine.SubmitAsync(entryPointInfo, entryPointInput); + MostRecentJobId = job.Id; + // 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 ExecuteJobAsync( + IChannel channel, + IOperationResolver operationResolver, + string operationName) => + await SubmitOrExecuteJobAsync(channel, operationResolver, operationName, execute: true); + + /// + public async Task GetActiveTargetAsync( + IChannel channel) + { + // TODO: This should also print the list of available targets to the IChannel. + return ActiveTargetName.ToExecutionResult(); } /// @@ -160,63 +220,109 @@ public async Task SetActiveTargetAsync( 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(); } /// - public async Task PrintTargetListAsync( - IChannel channel) + public async Task GetJobResultAsync( + IChannel channel, + string jobId) { - if (QuantumClient == null) + if (ActiveWorkspace == null) { - channel.Stderr("Please call %connect before listing targets."); + channel.Stderr("Please call %azure.connect before getting job results."); return AzureClientError.NotConnected.ToExecutionResult(); } - var providersStatus = await QuantumClient.Providers.GetStatusAsync(); - return providersStatus.ToJupyterTable().ToExecutionResult(); + if (string.IsNullOrEmpty(jobId)) + { + if (string.IsNullOrEmpty(MostRecentJobId)) + { + channel.Stderr("No job ID was specified. Please submit a job first or specify a job ID."); + return AzureClientError.JobNotFound.ToExecutionResult(); + } + + jobId = MostRecentJobId; + } + + var job = ActiveWorkspace.GetJob(jobId); + if (job == null) + { + channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); + return AzureClientError.JobNotFound.ToExecutionResult(); + } + + 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(); + } + + var stream = new MemoryStream(); + var protocol = await new JobStorageHelper(ConnectionString).DownloadJobOutputAsync(jobId, stream); + stream.Seek(0, SeekOrigin.Begin); + var outputJson = new StreamReader(stream).ReadToEnd(); + + // TODO: Deserialize this once we have a way of getting the output type + // TODO: Add encoder for job output + return outputJson.ToExecutionResult(); } /// - public async Task PrintJobStatusAsync( + public async Task GetJobStatusAsync( IChannel channel, string jobId) { - if (QuantumClient == null) + if (ActiveWorkspace == null) { - channel.Stderr("Please call %connect before getting job status."); + channel.Stderr("Please call %azure.connect before getting job status."); return AzureClientError.NotConnected.ToExecutionResult(); } - var jobDetails = await QuantumClient.Jobs.GetAsync(jobId); - if (jobDetails == null) + if (string.IsNullOrEmpty(jobId)) + { + if (string.IsNullOrEmpty(MostRecentJobId)) + { + channel.Stderr("No job ID was specified. Please submit a job first or specify a job ID."); + return AzureClientError.JobNotFound.ToExecutionResult(); + } + + jobId = MostRecentJobId; + } + + var job = ActiveWorkspace.GetJob(jobId); + if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); return AzureClientError.JobNotFound.ToExecutionResult(); } - return jobDetails.ToJupyterTable().ToExecutionResult(); + // TODO: Add encoder for CloudJob rather than calling ToJupyterTable() here directly. + return job.Details.ToJupyterTable().ToExecutionResult(); } /// - public async Task PrintJobListAsync( + public async Task GetJobListAsync( IChannel channel) { - if (QuantumClient == null) + if (ActiveWorkspace == null) { - channel.Stderr("Please call %connect before listing jobs."); + channel.Stderr("Please call %azure.connect before listing jobs."); return AzureClientError.NotConnected.ToExecutionResult(); } - var jobsList = await QuantumClient.Jobs.ListAsync(); - if (jobsList == null || jobsList.Count() == 0) + var jobs = ActiveWorkspace.ListJobs(); + if (jobs == null || jobs.Count() == 0) { channel.Stderr("No jobs found in current Azure Quantum workspace."); return AzureClientError.JobNotFound.ToExecutionResult(); } - return jobsList.ToJupyterTable().ToExecutionResult(); + // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. + return jobs.Select(job => job.Details).ToJupyterTable().ToExecutionResult(); } } } diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index 3b9d29e070..81010d9370 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/AzureClient/AzureEnvironment.cs b/src/AzureClient/AzureEnvironment.cs new file mode 100644 index 0000000000..d5c6ac2a2c --- /dev/null +++ b/src/AzureClient/AzureEnvironment.cs @@ -0,0 +1,110 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.Quantum.Simulation.Common; +using System; +using System.Collections.Generic; +using System.Linq; +using System.Net; +using System.Text; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal enum AzureEnvironmentType { Production, Canary, Dogfood }; + + internal class AzureEnvironment + { + public string ClientId { get; private set; } = string.Empty; + public string Authority { get; private set; } = string.Empty; + public List Scopes { get; private set; } = new List(); + public Uri? BaseUri { get; private set; } + + private AzureEnvironment() + { + } + + public static AzureEnvironment Create(string environment, string subscriptionId) + { + if (Enum.TryParse(environment, true, out AzureEnvironmentType environmentType)) + { + switch (environmentType) + { + case AzureEnvironmentType.Production: + return Production(); + case AzureEnvironmentType.Canary: + return Canary(); + case AzureEnvironmentType.Dogfood: + return Dogfood(subscriptionId); + default: + throw new InvalidOperationException("Unexpected EnvironmentType value."); + } + } + + return Production(); + } + + private static AzureEnvironment Production() => + new AzureEnvironment() + { + ClientId = "84ba0947-6c53-4dd2-9ca9-b3694761521b", // QDK client ID + Authority = "https://login.microsoftonline.com/common", + Scopes = new List() { "https://quantum.microsoft.com/Jobs.ReadWrite" }, + BaseUri = new Uri("https://app-jobscheduler-prod.azurewebsites.net/"), + }; + + private static AzureEnvironment Dogfood(string subscriptionId) => + new AzureEnvironment() + { + ClientId = "46a998aa-43d0-4281-9cbb-5709a507ac36", // QDK dogfood client ID + Authority = GetDogfoodAuthority(subscriptionId), + Scopes = new List() { "api://dogfood.azure-quantum/Jobs.ReadWrite" }, + BaseUri = new Uri("https://app-jobscheduler-test.azurewebsites.net/"), + }; + + private static AzureEnvironment Canary() + { + var canary = Production(); + canary.BaseUri = new Uri("https://app-jobs-canarysouthcentralus.azurewebsites.net/"); + return canary; + } + + private static string GetDogfoodAuthority(string subscriptionId) + { + try + { + var armBaseUrl = "https://api-dogfood.resources.windows-int.net"; + var requestUrl = $"{armBaseUrl}/subscriptions/{subscriptionId}?api-version=2018-01-01"; + + WebResponse? response = null; + try + { + response = WebRequest.Create(requestUrl).GetResponse(); + } + catch (WebException webException) + { + response = webException.Response; + } + + var authHeader = response.Headers["WWW-Authenticate"]; + var headerParts = authHeader.Substring("Bearer ".Length).Split(','); + foreach (var headerPart in headerParts) + { + var parts = headerPart.Split("=", 2); + if (parts[0] == "authorization_uri") + { + var quotedAuthority = parts[1]; + return quotedAuthority[1..^1]; + } + } + + throw new InvalidOperationException($"Dogfood authority not found in ARM header response for subscription ID {subscriptionId}."); + } + catch (Exception ex) + { + throw new InvalidOperationException($"Failed to construct dogfood authority for subscription ID {subscriptionId}.", ex); + } + } + } +} diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index d98c420431..82bb3cb756 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -66,52 +66,98 @@ public interface IAzureClient /// /// Connects to the specified Azure Quantum workspace, first logging into Azure if necessary. /// + /// + /// The list of execution targets available in the Azure Quantum workspace. + /// public Task ConnectAsync( IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, string storageAccountConnectionString, - bool forceLogin = false); + bool refreshCredentials = false); /// - /// Prints a string describing the current connection status. + /// Gets the current connection status to an Azure Quantum workspace. /// - public Task PrintConnectionStatusAsync( + /// + /// 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); /// /// Submits the specified Q# operation as a job to the currently active target. /// + /// + /// Details of the submitted job, or an error if submission failed. + /// public Task SubmitJobAsync( IChannel channel, IOperationResolver operationResolver, string operationName); + /// + /// Executes the specified Q# operation as a job to the currently active target + /// and waits for execution to complete before returning. + /// + /// + /// The result of the executed job, or an error if execution failed. + /// + public Task ExecuteJobAsync( + IChannel channel, + IOperationResolver operationResolver, + string operationName); + /// /// Sets the specified target for job submission. /// + /// + /// Success if the target is valid, or an error if the target cannot be set. + /// public Task SetActiveTargetAsync( IChannel channel, string targetName); /// - /// Prints the list of targets currently provisioned in the current workspace. + /// Gets the currently specified target for job submission. /// - public Task PrintTargetListAsync( + /// + /// The target name. + /// + public Task GetActiveTargetAsync( IChannel channel); /// - /// Prints the job status corresponding to the given job ID. + /// Gets the result of a specified job. + /// + /// + /// 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); + + /// + /// Gets the status of a specified job. /// - public Task PrintJobStatusAsync( + /// + /// 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); /// - /// Prints a list of all jobs in the current workspace. + /// Gets a list of all jobs in the current Azure Quantum workspace. /// - public Task PrintJobListAsync( + /// + /// A list of all jobs in the current workspace. + /// + public Task GetJobListAsync( IChannel channel); } } diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 4a325a923b..559c04e3e7 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -17,19 +17,22 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class ConnectMagic : AzureClientMagicBase { - private const string - ParameterNameLogin = "login", - ParameterNameStorageAccountConnectionString = "storageAccountConnectionString", - ParameterNameSubscriptionId = "subscriptionId", - ParameterNameResourceGroupName = "resourceGroupName", - ParameterNameWorkspaceName = "workspaceName"; + private const string ParameterNameRefresh = "refresh"; + private const string ParameterNameStorageAccountConnectionString = "storageAccountConnectionString"; + private const string ParameterNameSubscriptionId = "subscriptionId"; + private const string ParameterNameResourceGroupName = "resourceGroupName"; + private const string ParameterNameWorkspaceName = "workspaceName"; /// - /// Constructs a new magic command given an IAzureClient object. + /// Initializes a new instance of the class. /// - public ConnectMagic(IAzureClient azureClient) : - base(azureClient, - "connect", + /// + /// The object to use for Azure functionality. + /// + public ConnectMagic(IAzureClient azureClient) + : base( + azureClient, + "azure.connect", new Documentation { Summary = "Connects to an Azure workspace or displays current connection status.", @@ -37,44 +40,50 @@ public ConnectMagic(IAzureClient azureClient) : This magic command allows for connecting to an Azure Quantum workspace as specified by a valid subscription ID, resource group name, workspace name, and storage account connection string. + + If the connection is successful, a list of the available execution targets + in the Azure Quantum workspace will be displayed. ".Dedent(), Examples = new[] { @" Print information about the current connection: ``` - In []: %connect - Out[]: Connected to WORKSPACE_NAME + In []: %azure.connect + Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. + ``` ".Dedent(), $@" Connect to an Azure Quantum workspace: ``` - In []: %connect {ParameterNameSubscriptionId}=SUBSCRIPTION_ID + In []: %azure.connect {ParameterNameSubscriptionId}=SUBSCRIPTION_ID {ParameterNameResourceGroupName}=RESOURCE_GROUP_NAME {ParameterNameWorkspaceName}=WORKSPACE_NAME {ParameterNameStorageAccountConnectionString}=CONNECTION_STRING - Out[]: Connected to WORKSPACE_NAME + Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. + ``` ".Dedent(), $@" Connect to an Azure Quantum workspace and force a credential prompt: ``` - In []: %connect {ParameterNameLogin} + In []: %azure.connect {ParameterNameRefresh} {ParameterNameSubscriptionId}=SUBSCRIPTION_ID {ParameterNameResourceGroupName}=RESOURCE_GROUP_NAME {ParameterNameWorkspaceName}=WORKSPACE_NAME {ParameterNameStorageAccountConnectionString}=CONNECTION_STRING Out[]: To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code [login code] to authenticate. - Connected to WORKSPACE_NAME + Connected to Azure Quantum workspace WORKSPACE_NAME. + ``` - Use the `{ParameterNameLogin}` option if you want to bypass any saved or cached + Use the `{ParameterNameRefresh}` option if you want to bypass any saved or cached credentials when connecting to Azure. - ".Dedent() - } + ".Dedent(), + }, }) {} /// @@ -88,20 +97,20 @@ public override async Task RunAsync(string input, IChannel chan var storageAccountConnectionString = inputParameters.DecodeParameter(ParameterNameStorageAccountConnectionString); if (string.IsNullOrEmpty(storageAccountConnectionString)) { - return await AzureClient.PrintConnectionStatusAsync(channel); + return await AzureClient.GetConnectionStatusAsync(channel); } var subscriptionId = inputParameters.DecodeParameter(ParameterNameSubscriptionId); var resourceGroupName = inputParameters.DecodeParameter(ParameterNameResourceGroupName); var workspaceName = inputParameters.DecodeParameter(ParameterNameWorkspaceName); - var forceLogin = inputParameters.DecodeParameter(ParameterNameLogin, defaultValue: false); + var refreshCredentials = inputParameters.DecodeParameter(ParameterNameRefresh, defaultValue: false); return await AzureClient.ConnectAsync( channel, subscriptionId, resourceGroupName, workspaceName, storageAccountConnectionString, - forceLogin); + refreshCredentials); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/ExecuteMagic.cs b/src/AzureClient/Magic/ExecuteMagic.cs new file mode 100644 index 0000000000..8f69532952 --- /dev/null +++ b/src/AzureClient/Magic/ExecuteMagic.cs @@ -0,0 +1,78 @@ +// 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.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to submit jobs to an Azure Quantum workspace. + /// + 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) + : base( + azureClient, + "azure.execute", + new Documentation + { + Summary = "Executes a job in an Azure Quantum workspace.", + Description = @" + This magic command allows for executing a job in an Azure Quantum workspace + corresponding to the Q# operation provided as an argument, and it waits + for the job to complete before returning. + + The Azure Quantum workspace must previously have been initialized + using the %azure.connect magic command. + ".Dedent(), + Examples = new[] + { + @" + Execute an operation in the current Azure Quantum workspace: + ``` + In []: %azure.execute OPERATION_NAME + Out[]: Executing job on target TARGET_NAME... + + ``` + ".Dedent(), + }, + }) => + this.OperationResolver = operationResolver; + + /// + /// Executes a new job in an Azure Quantum workspace given a Q# operation + /// name that is present in the current Q# Jupyter workspace, and + /// waits for the job to complete before returning. + /// + public override async Task RunAsync(string input, IChannel channel) + { + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); + var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); + return await AzureClient.ExecuteJobAsync(channel, OperationResolver, operationName); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Magic/JobsMagic.cs b/src/AzureClient/Magic/JobsMagic.cs new file mode 100644 index 0000000000..f23708d9ea --- /dev/null +++ b/src/AzureClient/Magic/JobsMagic.cs @@ -0,0 +1,58 @@ +// 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.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to list jobs in an Azure Quantum workspace. + /// + public class JobsMagic : AzureClientMagicBase + { + /// + /// Initializes a new instance of the class. + /// + /// + /// The object to use for Azure functionality. + /// + public JobsMagic(IAzureClient azureClient) + : base( + azureClient, + "azure.jobs", + new Documentation + { + Summary = "Displays a list of jobs in the current Azure Quantum workspace.", + Description = @" + This magic command allows for displaying the list of jobs in the current + Azure Quantum workspace. + + The Azure Quantum workspace must previously have been initialized + using the %azure.connect magic command. + ".Dedent(), + Examples = new[] + { + @" + Print the list of jobs: + ``` + In []: %azure.jobs + Out[]: + ``` + ".Dedent(), + }, + }) {} + + /// + /// Lists all jobs in the active workspace. + /// + public override async Task RunAsync(string input, IChannel channel) => + await AzureClient.GetJobListAsync(channel); + } +} \ No newline at end of file diff --git a/src/AzureClient/Magic/OutputMagic.cs b/src/AzureClient/Magic/OutputMagic.cs new file mode 100644 index 0000000000..f4b722f556 --- /dev/null +++ b/src/AzureClient/Magic/OutputMagic.cs @@ -0,0 +1,78 @@ +// 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.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// A magic command that can be used to connect to display the results of an Azure Quantum job. + /// + public class OutputMagic : AzureClientMagicBase + { + private const string ParameterNameJobId = "jobId"; + + /// + /// Initializes a new instance of the class. + /// + /// + /// The object to use for Azure functionality. + /// + public OutputMagic(IAzureClient azureClient) + : base( + azureClient, + "azure.output", + new Documentation + { + Summary = "Displays results for jobs in the current Azure Quantum workspace.", + Description = @" + This magic command allows for displaying results of jobs in the current + Azure Quantum workspace. If a valid job ID is provided as an argument, and the + job has completed, the output of that job will be displayed. If no job ID is + provided, the job ID from the most recent call to `%azure.submit` or + `%azure.execute` will be used. + + If the job has not yet completed, an error message will be displayed. + + The Azure Quantum workspace must previously have been initialized + using the %azure.connect magic command. + ".Dedent(), + Examples = new[] + { + @" + Print results of a specific job: + ``` + In []: %azure.output JOB_ID + Out[]: + ``` + ".Dedent(), + + @" + Print results of the most recently-submitted job: + ``` + In []: %azure.output + Out[]: + ``` + ".Dedent(), + }, + }) {} + + /// + /// Displays the output of a given completed job ID, if provided, + /// or all jobs submitted in the current session. + /// + public override async Task RunAsync(string input, IChannel channel) + { + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameJobId); + string jobId = inputParameters.DecodeParameter(ParameterNameJobId); + return await AzureClient.GetJobResultAsync(channel, jobId); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs index d39c6927da..e3ee13d092 100644 --- a/src/AzureClient/Magic/StatusMagic.cs +++ b/src/AzureClient/Magic/StatusMagic.cs @@ -17,58 +17,60 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class StatusMagic : AzureClientMagicBase { - private const string - ParameterNameJobId = "jobId"; + private const string ParameterNameJobId = "jobId"; /// - /// Constructs a new magic command given an IAzureClient object. + /// Initializes a new instance of the class. /// - public StatusMagic(IAzureClient azureClient) : - base(azureClient, - "status", + /// + /// The object to use for Azure functionality. + /// + public StatusMagic(IAzureClient azureClient) + : base( + azureClient, + "azure.status", new Documentation { Summary = "Displays status for jobs in the current Azure Quantum workspace.", Description = @" This magic command allows for displaying status of jobs in the current Azure Quantum workspace. If a valid job ID is provided as an argument, the - detailed status of that job will be displayed; otherwise, a list of all jobs - created in the current session will be displayed. + detailed status of that job will be displayed. If no job ID is + provided, the job ID from the most recent call to `%azure.submit` or + `%azure.execute` will be used. + + The Azure Quantum workspace must previously have been initialized + using the %azure.connect magic command. ".Dedent(), Examples = new[] { @" - Print status about a specific job: + Print status of a specific job: ``` - In []: %status JOB_ID - Out[]: JOB_ID: + In []: %azure.status JOB_ID + Out[]: ``` ".Dedent(), @" - Print status about all jobs created in the current session: + Print status of the most recently-submitted job: ``` - In []: %status - Out[]: + In []: %azure.status + Out[]: ``` - ".Dedent() - } + ".Dedent(), + }, }) {} /// /// Displays the status corresponding to a given job ID, if provided, - /// or all jobs in the active workspace. + /// or the most recently-submitted job in the current session. /// public override async Task RunAsync(string input, IChannel channel) { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameJobId); - if (inputParameters.ContainsKey(ParameterNameJobId)) - { - string jobId = inputParameters.DecodeParameter(ParameterNameJobId); - return await AzureClient.PrintJobStatusAsync(channel, jobId); - } - - return await AzureClient.PrintJobListAsync(channel); + string jobId = inputParameters.DecodeParameter(ParameterNameJobId); + return await AzureClient.GetJobStatusAsync(channel, jobId); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index d89da233b3..8d779646d5 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -8,6 +8,7 @@ using System.Linq; using System.Threading.Tasks; using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Jupyter; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -16,19 +17,27 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class SubmitMagic : AzureClientMagicBase { + private const string ParameterNameOperationName = "operationName"; + /// - /// The symbol resolver used by this magic command to find + /// Gets the symbol resolver used by this magic command to find /// operations or functions to be simulated. /// public IOperationResolver OperationResolver { get; } /// - /// Constructs a new magic command given a resolver used to find - /// operations and functions and an IAzureClient object. + /// Initializes a new instance of the class. /// - public SubmitMagic(IOperationResolver operationResolver, IAzureClient azureClient) : - base(azureClient, - "submit", + /// + /// The object used to find and resolve operations. + /// + /// + /// The object to use for Azure functionality. + /// + public SubmitMagic(IOperationResolver operationResolver, IAzureClient azureClient) + : base( + azureClient, + "azure.submit", new Documentation { Summary = "Submits a job to an Azure Quantum workspace.", @@ -37,18 +46,18 @@ public SubmitMagic(IOperationResolver operationResolver, IAzureClient azureClien corresponding to the Q# operation provided as an argument. The Azure Quantum workspace must previously have been initialized - using the %connect magic command. + using the %azure.connect magic command. ".Dedent(), Examples = new[] { @" Submit an operation as a new job to the current Azure Quantum workspace: ``` - In []: %submit OPERATION_NAME + In []: %azure.submit OPERATION_NAME Out[]: Submitted job JOB_ID ``` ".Dedent(), - } + }, }) => this.OperationResolver = operationResolver; @@ -58,8 +67,8 @@ The Azure Quantum workspace must previously have been initialized /// public override async Task RunAsync(string input, IChannel channel) { - Dictionary keyValuePairs = ParseInputParameters(input); - var operationName = keyValuePairs.Keys.FirstOrDefault(); + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); + var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); return await AzureClient.SubmitJobAsync(channel, OperationResolver, operationName); } } diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 4108009cc6..b17eb0ab3e 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -17,15 +17,18 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class TargetMagic : AzureClientMagicBase { - private const string - ParameterNameTargetName = "name"; + private const string ParameterNameTargetName = "name"; /// - /// Constructs a new magic command given an IAzureClient object. + /// Initializes a new instance of the class. /// - public TargetMagic(IAzureClient azureClient) : - base(azureClient, - "target", + /// + /// The object to use for Azure functionality. + /// + public TargetMagic(IAzureClient azureClient) + : base( + azureClient, + "azure.target", new Documentation { Summary = "Views or sets the target for job submission to an Azure Quantum workspace.", @@ -34,7 +37,7 @@ public TargetMagic(IAzureClient azureClient) : to an Azure Quantum workspace, or viewing the list of all available targets. The Azure Quantum workspace must previously have been initialized - using the %connect magic command, and the specified target must be + using the %azure.connect magic command, and the specified target must be available in the workspace. ".Dedent(), Examples = new[] @@ -42,18 +45,18 @@ available in the workspace. @" Set the current target for job submission: ``` - In []: %target TARGET_NAME + In []: %azure.target TARGET_NAME Out[]: Active target is now TARGET_NAME ``` ".Dedent(), @" View the current target and all available targets in the current Azure Quantum workspace: ``` - In []: %target - Out[]: + In []: %azure.target + Out[]: ``` ".Dedent(), - } + }, }) { } @@ -70,7 +73,7 @@ public override async Task RunAsync(string input, IChannel chan return await AzureClient.SetActiveTargetAsync(channel, targetName); } - return await AzureClient.PrintTargetListAsync(channel); + return await AzureClient.GetActiveTargetAsync(channel); } } } \ No newline at end of file diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index b67e57c334..9d38c271c7 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,9 +34,9 @@ - - - + + + diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 730554ede8..0eddabee2d 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -41,7 +41,7 @@ public void TestConnectMagic() // unrecognized input connectMagic.Test($"invalid"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintConnectionStatus); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetConnectionStatus); // valid input connectMagic.Test( @@ -50,33 +50,33 @@ public void TestConnectMagic() workspaceName={workspaceName} storageAccountConnectionString={storageAccountConnectionString}"); Assert.AreEqual(azureClient.LastAction, AzureClientAction.Connect); - Assert.IsFalse(azureClient.ForceLogin); + Assert.IsFalse(azureClient.RefreshCredentials); Assert.AreEqual(azureClient.ConnectionString, storageAccountConnectionString); // valid input with forced login connectMagic.Test( - @$"login subscriptionId={subscriptionId} + @$"refresh subscriptionId={subscriptionId} resourceGroupName={resourceGroupName} workspaceName={workspaceName} storageAccountConnectionString={storageAccountConnectionString}"); - Assert.IsTrue(azureClient.ForceLogin); + Assert.IsTrue(azureClient.RefreshCredentials); } [TestMethod] public void TestStatusMagic() { - // no arguments - should print job list + // no arguments - should print job status of most recent job var azureClient = new MockAzureClient(); var statusMagic = new StatusMagic(azureClient); statusMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintJobList); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetJobStatus); // single argument - should print job status azureClient = new MockAzureClient(); statusMagic = new StatusMagic(azureClient); statusMagic.Test($"{jobId}"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintJobStatus); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetJobStatus); } [TestMethod] @@ -95,20 +95,62 @@ public void TestSubmitMagic() Assert.IsTrue(azureClient.SubmittedJobs.Contains(operationName)); } + [TestMethod] + public void TestExecuteMagic() + { + // no arguments + var azureClient = new MockAzureClient(); + var operationResolver = new MockOperationResolver(); + var executeMagic = new ExecuteMagic(operationResolver, azureClient); + executeMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.ExecuteJob); + + // single argument + executeMagic.Test($"{operationName}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.ExecuteJob); + Assert.IsTrue(azureClient.ExecutedJobs.Contains(operationName)); + } + + [TestMethod] + public void TestOutputMagic() + { + // no arguments - should print job result of most recent job + var azureClient = new MockAzureClient(); + var outputMagic = new OutputMagic(azureClient); + outputMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetJobResult); + + // single argument - should print job status + azureClient = new MockAzureClient(); + outputMagic = new OutputMagic(azureClient); + outputMagic.Test($"{jobId}"); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetJobResult); + } + + [TestMethod] + public void TestJobsMagic() + { + // no arguments - should print job status of all jobs + var azureClient = new MockAzureClient(); + var jobsMagic = new JobsMagic(azureClient); + jobsMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetJobList); + } + [TestMethod] public void TestTargetMagic() { - // no arguments - should print target list + // single argument - should set active target var azureClient = new MockAzureClient(); var targetMagic = new TargetMagic(azureClient); - targetMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.PrintTargetList); + targetMagic.Test(targetName); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); - // single argument - should set active target + // no arguments - should print active target azureClient = new MockAzureClient(); targetMagic = new TargetMagic(azureClient); - targetMagic.Test(targetName); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); + targetMagic.Test(string.Empty); + Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetActiveTarget); } } @@ -117,20 +159,23 @@ internal enum AzureClientAction None, Connect, SetActiveTarget, + GetActiveTarget, SubmitJob, - PrintConnectionStatus, - PrintJobList, - PrintJobStatus, - PrintTargetList, + ExecuteJob, + GetConnectionStatus, + GetJobList, + GetJobStatus, + GetJobResult, } public class MockAzureClient : IAzureClient { internal AzureClientAction LastAction = AzureClientAction.None; internal string ConnectionString = string.Empty; - internal bool ForceLogin = false; + internal bool RefreshCredentials = false; internal string ActiveTargetName = string.Empty; internal List SubmittedJobs = new List(); + internal List ExecutedJobs = new List(); public async Task SetActiveTargetAsync(IChannel channel, string targetName) { @@ -138,6 +183,11 @@ public async Task SetActiveTargetAsync(IChannel channel, string ActiveTargetName = targetName; return ExecuteStatus.Ok.ToExecutionResult(); } + public async Task GetActiveTargetAsync(IChannel channel) + { + LastAction = AzureClientAction.GetActiveTarget; + return ActiveTargetName.ToExecutionResult(); + } public async Task SubmitJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName) { @@ -146,35 +196,42 @@ public async Task SubmitJobAsync(IChannel channel, IOperationRe return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, string storageAccountConnectionString, bool forceLogin) + public async Task ExecuteJobAsync(IChannel channel, IOperationResolver operationResolver, string operationName) + { + LastAction = AzureClientAction.ExecuteJob; + ExecutedJobs.Add(operationName); + return ExecuteStatus.Ok.ToExecutionResult(); + } + + public async Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, string storageAccountConnectionString, bool refreshCredentials) { LastAction = AzureClientAction.Connect; ConnectionString = storageAccountConnectionString; - ForceLogin = forceLogin; + RefreshCredentials = refreshCredentials; return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task PrintConnectionStatusAsync(IChannel channel) + public async Task GetConnectionStatusAsync(IChannel channel) { - LastAction = AzureClientAction.PrintConnectionStatus; + LastAction = AzureClientAction.GetConnectionStatus; return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task PrintJobListAsync(IChannel channel) + public async Task GetJobListAsync(IChannel channel) { - LastAction = AzureClientAction.PrintJobList; + LastAction = AzureClientAction.GetJobList; return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task PrintJobStatusAsync(IChannel channel, string jobId) + public async Task GetJobStatusAsync(IChannel channel, string jobId) { - LastAction = AzureClientAction.PrintJobStatus; + LastAction = AzureClientAction.GetJobStatus; return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task PrintTargetListAsync(IChannel channel) + public async Task GetJobResultAsync(IChannel channel, string jobId) { - LastAction = AzureClientAction.PrintTargetList; + LastAction = AzureClientAction.GetJobResult; return ExecuteStatus.Ok.ToExecutionResult(); } } diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index 6c5eb88334..c9258af1af 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -33,8 +33,8 @@ public void TestTargets() var result = azureClient.SetActiveTargetAsync(new MockChannel(), targetName).GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Ok); - result = azureClient.PrintTargetListAsync(new MockChannel()).GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == ExecuteStatus.Error); + result = azureClient.GetActiveTargetAsync(new MockChannel()).GetAwaiter().GetResult(); + Assert.IsTrue(result.Status == ExecuteStatus.Ok); } } } diff --git a/src/Tests/IQsharpEngineTests.cs b/src/Tests/IQsharpEngineTests.cs index a4449ada95..2d800b3bb9 100644 --- a/src/Tests/IQsharpEngineTests.cs +++ b/src/Tests/IQsharpEngineTests.cs @@ -450,10 +450,13 @@ public void TestResolveMagic() Assert.IsNull(symbol); // AzureClient-provided commands - Assert.IsNotNull(resolver.Resolve("%connect")); - Assert.IsNotNull(resolver.Resolve("%status")); - Assert.IsNotNull(resolver.Resolve("%submit")); - Assert.IsNotNull(resolver.Resolve("%target")); + Assert.IsNotNull(resolver.Resolve("%azure.connect")); + Assert.IsNotNull(resolver.Resolve("%azure.target")); + Assert.IsNotNull(resolver.Resolve("%azure.submit")); + Assert.IsNotNull(resolver.Resolve("%azure.execute")); + Assert.IsNotNull(resolver.Resolve("%azure.status")); + Assert.IsNotNull(resolver.Resolve("%azure.output")); + Assert.IsNotNull(resolver.Resolve("%azure.jobs")); } } } diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index 5c8675d093..52d295f3e3 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -6,18 +6,18 @@ }, "AllowedHosts": "*", "DefaultPackageVersions": [ - "Microsoft.Quantum.Compiler::0.11.2005.1420-beta", + "Microsoft.Quantum.Compiler::0.11.2005.1924-beta", - "Microsoft.Quantum.CsharpGeneration::0.11.2005.1420-beta", - "Microsoft.Quantum.Development.Kit::0.11.2005.1420-beta", - "Microsoft.Quantum.Simulators::0.11.2005.1420-beta", - "Microsoft.Quantum.Xunit::0.11.2005.1420-beta", + "Microsoft.Quantum.CsharpGeneration::0.11.2005.1924-beta", + "Microsoft.Quantum.Development.Kit::0.11.2005.1924-beta", + "Microsoft.Quantum.Simulators::0.11.2005.1924-beta", + "Microsoft.Quantum.Xunit::0.11.2005.1924-beta", - "Microsoft.Quantum.Standard::0.11.2005.1420-beta", - "Microsoft.Quantum.Chemistry::0.11.2005.1420-beta", - "Microsoft.Quantum.Chemistry.Jupyter::0.11.2005.1420-beta", - "Microsoft.Quantum.Numerics::0.11.2005.1420-beta", + "Microsoft.Quantum.Standard::0.11.2005.1924-beta", + "Microsoft.Quantum.Chemistry::0.11.2005.1924-beta", + "Microsoft.Quantum.Chemistry.Jupyter::0.11.2005.1924-beta", + "Microsoft.Quantum.Numerics::0.11.2005.1924-beta", - "Microsoft.Quantum.Research::0.11.2005.1420-beta" + "Microsoft.Quantum.Research::0.11.2005.1924-beta" ] } From 8e118b659470258d0ad479166d9ff882aefb3418 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 28 May 2020 18:03:10 -0400 Subject: [PATCH 19/44] 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 2891f4f6fa9f304637fc012f3cb9f0167ef50de4 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 29 May 2020 12:44:44 -0400 Subject: [PATCH 20/44] Remove azure magics from CodeMirror rules --- src/Kernel/client/kernel.ts | 6 ------ 1 file changed, 6 deletions(-) diff --git a/src/Kernel/client/kernel.ts b/src/Kernel/client/kernel.ts index d239a5af47..db24014383 100644 --- a/src/Kernel/client/kernel.ts +++ b/src/Kernel/client/kernel.ts @@ -69,12 +69,6 @@ function defineQSharpMode() { 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", From e76dc50107bb85295770866b2fcbf713d670cc23 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 29 May 2020 12:55:25 -0400 Subject: [PATCH 21/44] Minor tweak for consistency --- src/Kernel/client/kernel.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kernel/client/kernel.ts b/src/Kernel/client/kernel.ts index db24014383..3be307a020 100644 --- a/src/Kernel/client/kernel.ts +++ b/src/Kernel/client/kernel.ts @@ -78,7 +78,7 @@ function defineQSharpMode() { { // katas magic commands token: "builtin", - regex: String.raw`(%(?:check_kata|kata))\b`, + regex: String.raw`(%(check_kata|kata))\b`, beginWord: true, }, ]; From 0282eec4387066f5f53acd2d0250dacd5cedc228 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 8 Jun 2020 10:02:23 -0400 Subject: [PATCH 22/44] Validate and load packages for Azure execution targets (#152) * Updates to IQ# syntax highlighting * Validate targets and load provider packages * Update Python interface for Azure commands * Simplify AzureExecutionTarget class * Changes for JobNotCompleted case * Simplify property syntax Co-authored-by: Chris Granade * Add simple tests for AzureExecutionTarget class * Add status message while loading provider package * Add documentation to AzureExecutionTarget.GetProvider Co-authored-by: Chris Granade --- src/AzureClient/AzureClient.cs | 121 ++++++++++++++++----- src/AzureClient/AzureEnvironment.cs | 1 - src/AzureClient/AzureExecutionTarget.cs | 44 ++++++++ src/AzureClient/Extensions.cs | 5 +- src/AzureClient/IAzureClient.cs | 27 +++-- src/AzureClient/Magic/TargetMagic.cs | 14 ++- src/AzureClient/Properties/AssemblyInfo.cs | 3 + src/AzureClient/Resources.cs | 6 + src/Kernel/client/kernel.ts | 6 + src/Python/qsharp/azure.py | 22 +++- src/Tests/AzureClientMagicTests.cs | 11 +- src/Tests/AzureClientTests.cs | 46 +++++++- src/Tool/appsettings.json | 6 +- 13 files changed, 253 insertions(+), 59 deletions(-) create mode 100644 src/AzureClient/AzureExecutionTarget.cs create mode 100644 src/AzureClient/Properties/AssemblyInfo.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index c581d8e086..ee639ec3e8 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,21 @@ 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); + channel.Stdout($"Job {job.Id} submitted successfully."); + MostRecentJobId = job.Id; + // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here. return job.ToJupyterTable().ToExecutionResult(); } @@ -210,19 +228,60 @@ 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. + 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}"); + return AzureClientError.InvalidTarget.ToExecutionResult(); + } + + // 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(); } /// @@ -256,19 +315,23 @@ 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(); - 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(); } /// @@ -300,8 +363,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/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..2f0d5c89d1 --- /dev/null +++ b/src/AzureClient/AzureExecutionTarget.cs @@ -0,0 +1,44 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal enum AzureProvider { IonQ, Honeywell, QCI } + + internal class AzureExecutionTarget + { + public string TargetName { get; private set; } + public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetName)}"; + + public static bool IsValid(string targetName) => GetProvider(targetName) != null; + + public static AzureExecutionTarget? Create(string targetName) => + IsValid(targetName) + ? 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); + 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..f7df675652 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -18,43 +18,55 @@ 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, + + /// + /// 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. /// [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 +130,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/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/AzureClient/Resources.cs b/src/AzureClient/Resources.cs index 6c3de47cc0..1fedce95f2 100644 --- a/src/AzureClient/Resources.cs +++ b/src/AzureClient/Resources.cs @@ -16,9 +16,15 @@ 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."; + 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."; diff --git a/src/Kernel/client/kernel.ts b/src/Kernel/client/kernel.ts index 3be307a020..7590ddc03b 100644 --- a/src/Kernel/client/kernel.ts +++ b/src/Kernel/client/kernel.ts @@ -69,6 +69,12 @@ function defineQSharpMode() { 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", 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) 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..af75ade1b8 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,52 @@ 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); + } + + [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"); } } } diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index 31834fd153..3edf302854 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -20,6 +20,10 @@ "Microsoft.Quantum.Katas::0.11.2006.207", - "Microsoft.Quantum.Research::0.11.2006.207" + "Microsoft.Quantum.Research::0.11.2006.207", + + "Microsoft.Quantum.Providers.IonQ::0.11.2006.207", + "Microsoft.Quantum.Providers.Honeywell::0.11.2006.207", + "Microsoft.Quantum.Providers.QCI::0.11.2006.207", ] } From 1c1768727a26372cff541f7bf7f54e184ae2ef80 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 12 Jun 2020 15:31:29 -0400 Subject: [PATCH 23/44] Auto-generate entry point for Azure job submission (#156) * Updates to IQ# syntax highlighting * Validate targets and load provider packages * Update Python interface for Azure commands * Simplify AzureExecutionTarget class * Generate EntryPoint and compile into new assembly * Changes for JobNotCompleted case * Refactor entry point code into new classes * Use correct input and output types * Simplify property syntax Co-authored-by: Chris Granade * Add simple tests for AzureExecutionTarget class * Recompile everything for specified execution target * Add tests and error handling * Improve variable names * Add status message while loading provider package * Add documentation to AzureExecutionTarget.GetProvider * Extend tests to call EntryPoint.SubmitAsync * Wait for job completion on %azure.execute * Update comment * Add timeout for %azure.execute * Minor fixes in AzureClient * Minor formatting and comment tweaks * Style improvements in test code * More consistent handling of job objects * Consistent error handling for IWorkspace calls * Update to latest QDK released version * Use switch syntax for entryPointInput Co-authored-by: Chris Granade * Remove 'All rights reserved.' Co-authored-by: Chris Granade * Addressing PR feedback and compiler warnings Co-authored-by: Chris Granade --- src/AzureClient/AzureClient.cs | 185 ++++++++++++------ src/AzureClient/AzureClient.csproj | 2 +- src/AzureClient/EntryPoint/EntryPoint.cs | 90 +++++++++ .../EntryPoint/EntryPointGenerator.cs | 141 +++++++++++++ .../EntryPoint/EntryPointOperationResolver.cs | 32 +++ src/AzureClient/EntryPoint/IEntryPoint.cs | 28 +++ .../EntryPoint/IEntryPointGenerator.cs | 47 +++++ src/AzureClient/Extensions.cs | 22 ++- src/AzureClient/IAzureClient.cs | 54 ++--- src/AzureClient/Magic/ExecuteMagic.cs | 26 ++- src/AzureClient/Magic/SubmitMagic.cs | 26 ++- src/AzureClient/Magic/TargetMagic.cs | 13 +- src/AzureClient/Resources.cs | 9 + src/Core/Compiler/CompilerService.cs | 48 ++++- src/Core/Compiler/ICompilerService.cs | 10 +- src/Core/Core.csproj | 6 +- src/Core/Loggers/QsharpLogger.cs | 5 +- src/Core/OperationInfo.cs | 8 + src/Core/Resolver/OperationResolver.cs | 7 +- src/Core/Snippets/ISnippets.cs | 5 + src/Core/Snippets/Snippets.cs | 8 +- src/Core/Workspace/IWorkspace.cs | 5 + src/Tests/AzureClientEntryPointTests.cs | 184 +++++++++++++++++ src/Tests/AzureClientMagicTests.cs | 20 +- src/Tests/AzureClientTests.cs | 5 +- src/Tests/SNIPPETS.cs | 9 + src/Tool/appsettings.json | 28 +-- 27 files changed, 832 insertions(+), 191 deletions(-) create mode 100644 src/AzureClient/EntryPoint/EntryPoint.cs create mode 100644 src/AzureClient/EntryPoint/EntryPointGenerator.cs create mode 100644 src/AzureClient/EntryPoint/EntryPointOperationResolver.cs create mode 100644 src/AzureClient/EntryPoint/IEntryPoint.cs create mode 100644 src/AzureClient/EntryPoint/IEntryPointGenerator.cs create mode 100644 src/Tests/AzureClientEntryPointTests.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index ee639ec3e8..a282d18788 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -5,18 +5,20 @@ using System; using System.Collections.Generic; -using System.Linq; using System.IO; +using System.Linq; 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.Simulation.Core; +using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.Simulation.Common; 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 +27,9 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class AzureClient : IAzureClient { + private ILogger Logger { get; } + 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; } @@ -41,10 +46,20 @@ private string ValidExecutionTargetsDisplayText : string.Join(", ", ValidExecutionTargets.Select(target => target.Id)); } + public AzureClient( + IReferences references, + IEntryPointGenerator entryPointGenerator, + ILogger logger, + IEventService eventService) + { + References = references; + EntryPointGenerator = entryPointGenerator; + Logger = logger; + eventService?.TriggerServiceInitialized(this); + } /// - public async Task ConnectAsync( - IChannel channel, + public async Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, @@ -127,7 +142,7 @@ public async Task ConnectAsync( } 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(); } @@ -151,11 +166,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 +187,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,49 +195,81 @@ private async Task SubmitOrExecuteJobAsync( return AzureClientError.InvalidTarget.ToExecutionResult(); } - var operationInfo = operationResolver.Resolve(operationName); - var entryPointInfo = new EntryPointInfo(operationInfo.RoslynType); - var entryPointInput = QVoid.Instance; + channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); - if (execute) + IEntryPoint? entryPoint = null; + try { - channel.Stdout($"Executing {operationName} on target {ActiveTarget.TargetName}..."); - var output = await machine.ExecuteAsync(entryPointInfo, entryPointInput); - MostRecentJobId = output.Job.Id; - - // TODO: Add encoder to visualize IEnumerable> - return output.Histogram.ToExecutionResult(); + entryPoint = EntryPointGenerator.Generate(operationName, ActiveTarget.TargetName); } - else + catch (UnsupportedOperationException e) { - channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); - var job = await machine.SubmitAsync(entryPointInfo, entryPointInput); - channel.Stdout($"Job {job.Id} submitted successfully."); + 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(); + } + try + { + var 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(); + } - // TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here. - return job.ToJupyterTable().ToExecutionResult(); + if (!execute) + { + 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 System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30))) + { + 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)); + if (cts.IsCancellationRequested) break; + cloudJob = await GetCloudJob(MostRecentJobId); + channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}"); + } + while (cloudJob == null || cloudJob.InProgress); + } + + return await GetJobResultAsync(channel, MostRecentJobId); } /// - 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) { @@ -247,10 +290,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) { @@ -279,15 +319,13 @@ public async Task SetActiveTargetAsync( ActiveTarget = executionTarget; channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies..."); - 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) { @@ -306,7 +344,7 @@ public async Task GetJobResultAsync( jobId = MostRecentJobId; } - var job = ActiveWorkspace.GetJob(jobId); + var job = await GetCloudJob(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); @@ -335,9 +373,7 @@ public async Task GetJobResultAsync( } /// - public async Task GetJobStatusAsync( - IChannel channel, - string jobId) + public async Task GetJobStatusAsync(IChannel channel, string jobId) { if (ActiveWorkspace == null) { @@ -356,20 +392,19 @@ public async Task GetJobStatusAsync( jobId = MostRecentJobId; } - var job = ActiveWorkspace.GetJob(jobId); + var job = await GetCloudJob(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); return AzureClientError.JobNotFound.ToExecutionResult(); } - // TODO: Add encoder for CloudJob which calls ToJupyterTable() for display. - return job.Details.ToExecutionResult(); + // TODO: Add encoder for CloudJob rather than calling ToJupyterTable() here directly. + return job.ToJupyterTable().ToExecutionResult(); } /// - public async Task GetJobListAsync( - IChannel channel) + public async Task GetJobListAsync(IChannel channel) { if (ActiveWorkspace == null) { @@ -377,7 +412,7 @@ public async Task GetJobListAsync( return AzureClientError.NotConnected.ToExecutionResult(); } - var jobs = ActiveWorkspace.ListJobs(); + var jobs = await GetCloudJobs(); if (jobs == null || jobs.Count() == 0) { channel.Stderr("No jobs found in current Azure Quantum workspace."); @@ -385,7 +420,35 @@ public async Task GetJobListAsync( } // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return jobs.Select(job => job.Details).ToJupyterTable().ToExecutionResult(); + 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; } } } 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/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs new file mode 100644 index 0000000000..407f29d0d5 --- /dev/null +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -0,0 +1,90 @@ +// Copyright (c) Microsoft Corporation. +// 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 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 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) + { + EntryPointInfo = entryPointInfo; + InputType = inputType; + OutputType = outputType; + OperationInfo = operationInfo; + } + + /// + public Task SubmitAsync(IQuantumMachine machine, Dictionary inputParameters) + { + var parameterTypes = new List(); + 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: {e.Message}"); + } + + parameterTypes.Add(parameter.ParameterType); + parameterValues.Add(parameterValue); + } + + 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) + 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 (Task)submitMethod.Invoke(machine, submitParameters); + } + } +} diff --git a/src/AzureClient/EntryPoint/EntryPointGenerator.cs b/src/AzureClient/EntryPoint/EntryPointGenerator.cs new file mode 100644 index 0000000000..31f997f40f --- /dev/null +++ b/src/AzureClient/EntryPoint/EntryPointGenerator.cs @@ -0,0 +1,141 @@ +// Copyright (c) Microsoft Corporation. +// 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.Common; +using Microsoft.Quantum.Simulation.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + internal class EntryPointGenerator : IEntryPointGenerator + { + private ICompilerService Compiler { get; } + private ILogger Logger { get; } + private IWorkspace Workspace { get; } + private ISnippets Snippets { get; } + public IReferences References { get; } + public AssemblyInfo? WorkspaceAssemblyInfo { get; set; } + public AssemblyInfo? SnippetsAssemblyInfo { get; set; } + public AssemblyInfo? EntryPointAssemblyInfo { get; set; } + + public EntryPointGenerator( + ICompilerService compiler, + IWorkspace workspace, + ISnippets snippets, + IReferences references, + ILogger logger, + IEventService eventService) + { + Compiler = compiler; + Workspace = workspace; + Snippets = snippets; + References = references; + Logger = logger; + + AssemblyLoadContext.Default.Resolving += Resolve; + + 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) => name.Name switch + { + 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) + { + Logger?.LogDebug($"Generating entry point: operationName={operationName}, executionTarget={executionTarget}"); + + var logger = new QSharpLogger(Logger); + 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); + if (WorkspaceAssemblyInfo == null || logger.HasErrors) + { + Logger?.LogError($"Error compiling workspace."); + throw new CompilationErrorsException(logger.Errors.ToArray()); + } + + 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); + 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 = 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 + var parameterTypes = entryPointOperationInfo.RoslynParameters.Select(p => p.ParameterType).ToArray(); + var typeCount = parameterTypes.Length; + 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 }); + var entryPointInfo = entryPointInfoType.GetConstructor(new Type[] { typeof(Type) }) + .Invoke(new object[] { entryPointOperationInfo.RoslynType }); + + return new EntryPoint(entryPointInfo, entryPointInputType, entryPointOutputType, entryPointOperationInfo); + } + } +} diff --git a/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs b/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs new file mode 100644 index 0000000000..28dcda3f4c --- /dev/null +++ b/src/AzureClient/EntryPoint/EntryPointOperationResolver.cs @@ -0,0 +1,32 @@ +// Copyright (c) Microsoft Corporation. +// 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/IEntryPoint.cs b/src/AzureClient/EntryPoint/IEntryPoint.cs new file mode 100644 index 0000000000..4cc6e063d7 --- /dev/null +++ b/src/AzureClient/EntryPoint/IEntryPoint.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// 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..1ed8b2a87a --- /dev/null +++ b/src/AzureClient/EntryPoint/IEntryPointGenerator.cs @@ -0,0 +1,47 @@ +// Copyright (c) Microsoft Corporation. +// 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 + { + /// + /// 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. + /// + /// 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); + } +} diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 116643480f..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; @@ -27,6 +28,7 @@ public static class Extensions public static void AddAzureClient(this IServiceCollection services) { services.AddSingleton(); + services.AddSingleton(); } /// @@ -66,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() }; diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index f7df675652..952c28d796 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; @@ -56,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. /// @@ -81,8 +100,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, @@ -96,8 +114,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. @@ -105,10 +122,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 @@ -117,10 +131,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. @@ -128,10 +139,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. @@ -139,8 +147,7 @@ public Task SetActiveTargetAsync( /// /// The target name. /// - public Task GetActiveTargetAsync( - IChannel channel); + public Task GetActiveTargetAsync(IChannel channel); /// /// Gets the result of a specified job. @@ -149,9 +156,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. @@ -160,9 +165,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. @@ -170,7 +173,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..a5f317688d 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; @@ -19,22 +17,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 +49,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 +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, OperationResolver, operationName); + + var decodedParameters = new Dictionary(); + foreach (var key in inputParameters.Keys) + { + decodedParameters[key] = inputParameters.DecodeParameter(key); + } + + 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 8d779646d5..cb646a9903 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; @@ -19,22 +17,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 +47,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 +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, OperationResolver, operationName); + + var decodedParameters = new Dictionary(); + foreach (var key in inputParameters.Keys) + { + decodedParameters[key] = inputParameters.DecodeParameter(key); + } + + return await AzureClient.SubmitJobAsync(channel, operationName, decodedParameters); } } } \ 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/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 2869592270..6ac40cdd4b 100644 --- a/src/Core/Compiler/CompilerService.cs +++ b/src/Core/Compiler/CompilerService.cs @@ -16,9 +16,12 @@ 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; using Microsoft.Quantum.QsCompiler.Transformations.BasicTransformations; +using Microsoft.Quantum.QsCompiler.Transformations.QsCodeOutput; using Newtonsoft.Json.Bson; using QsReferences = Microsoft.Quantum.QsCompiler.CompilationBuilder.References; @@ -67,33 +70,61 @@ private QsCompilation UpdateCompilation(ImmutableDictionary sources return loaded.CompilationOutput; } + /// + 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); + + 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, executionTarget: executionTarget); + } + /// /// 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 /// 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.Add(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))}"); @@ -103,12 +134,15 @@ 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) { 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 7a9548a61f..76f95f6d0c 100644 --- a/src/Core/Compiler/ICompilerService.cs +++ b/src/Core/Compiler/ICompilerService.cs @@ -12,15 +12,21 @@ 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, 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/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/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/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; } 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/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/Tests/AzureClientEntryPointTests.cs b/src/Tests/AzureClientEntryPointTests.cs new file mode 100644 index 0000000000..3aba6eb27a --- /dev/null +++ b/src/Tests/AzureClientEntryPointTests.cs @@ -0,0 +1,184 @@ +// 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; +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 +{ + [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); + + 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)); + } + + [TestMethod] + 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)); + } + + [TestMethod] + 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)); + } + + [TestMethod] + public async Task InvalidEntryPointOperation() + { + var entryPointGenerator = Init("Workspace", new string[] { SNIPPETS.InvalidEntryPoint }); + 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) + => 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(); + } +} 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 af75ade1b8..2044b2116a 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 diff --git a/src/Tests/SNIPPETS.cs b/src/Tests/SNIPPETS.cs index 8aff3f3214..ef29cf6628 100644 --- a/src/Tests/SNIPPETS.cs +++ b/src/Tests/SNIPPETS.cs @@ -207,6 +207,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 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 8d6819300d54086fe19fcc3059eaf3b1564d094e Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 12 Jun 2020 15:47:53 -0400 Subject: [PATCH 24/44] Add simple display encoders for CloudJob, TargetStatus, and Histogram (#160) * Updates to IQ# syntax highlighting * Validate targets and load provider packages * Update Python interface for Azure commands * Simplify AzureExecutionTarget class * Generate EntryPoint and compile into new assembly * Changes for JobNotCompleted case * Refactor entry point code into new classes * Use correct input and output types * Simplify property syntax Co-authored-by: Chris Granade * Add simple tests for AzureExecutionTarget class * Recompile everything for specified execution target * Add tests and error handling * Improve variable names * Add status message while loading provider package * Add documentation to AzureExecutionTarget.GetProvider * Extend tests to call EntryPoint.SubmitAsync * Wait for job completion on %azure.execute * Update comment * Add timeout for %azure.execute * Minor fixes in AzureClient * Minor formatting and comment tweaks * Style improvements in test code * More consistent handling of job objects * Consistent error handling for IWorkspace calls * Update to latest QDK released version * Add encoders for CloudJob and TargetStatus * Move extension methods into encoder files * Change signature of CloudJob.ToDictionary * Update histogram deserialization and add encoders * Use JsonConverter classes directly * Small cleanup * Register single JsonEncoder * Add a simple HTML histogram display based on StateVectorToHtmlResultEncoder * Various improvements from PR suggestions * Use UTC for dates returned to Python * Use switch syntax for entryPointInput Co-authored-by: Chris Granade * Remove 'All rights reserved.' Co-authored-by: Chris Granade * Addressing PR feedback and compiler warnings Co-authored-by: Chris Granade --- build/test.ps1 | 2 +- src/AzureClient/AzureClient.cs | 78 ++++++------ src/AzureClient/AzureExecutionTarget.cs | 20 +-- src/AzureClient/Extensions.cs | 77 +++-------- src/AzureClient/IAzureClient.cs | 10 +- src/AzureClient/Magic/TargetMagic.cs | 14 +- src/AzureClient/Resources.cs | 3 + .../Visualization/CloudJobEncoders.cs | 99 +++++++++++++++ .../Visualization/HistogramEncoders.cs | 120 ++++++++++++++++++ .../Visualization/JsonConverters.cs | 27 ++++ .../Visualization/TargetStatusEncoders.cs | 81 ++++++++++++ src/Kernel/IQSharpEngine.cs | 6 +- src/Tests/AzureClientMagicTests.cs | 12 +- src/Tests/AzureClientTests.cs | 26 ++-- 14 files changed, 438 insertions(+), 137 deletions(-) create mode 100644 src/AzureClient/Visualization/CloudJobEncoders.cs create mode 100644 src/AzureClient/Visualization/HistogramEncoders.cs create mode 100644 src/AzureClient/Visualization/JsonConverters.cs create mode 100644 src/AzureClient/Visualization/TargetStatusEncoders.cs diff --git a/build/test.ps1 b/build/test.ps1 index 88bf8583a3..b259698eca 100644 --- a/build/test.ps1 +++ b/build/test.ps1 @@ -41,7 +41,7 @@ function Test-Python { Write-Host "##[info]Testing Python inside $testFolder" Push-Location (Join-Path $PSScriptRoot $testFolder) python --version - pytest --log-level=DEBUG + pytest -v --log-level=DEBUG Pop-Location if ($LastExitCode -ne 0) { diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index a282d18788..4ec62ed001 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -7,11 +7,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; 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; @@ -19,8 +19,6 @@ using Microsoft.Quantum.IQSharp.Common; using Microsoft.Quantum.Simulation.Common; using Microsoft.Rest.Azure; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -47,6 +45,7 @@ private string ValidExecutionTargetsDisplayText } public AzureClient( + IExecutionEngine engine, IReferences references, IEntryPointGenerator entryPointGenerator, ILogger logger, @@ -56,6 +55,16 @@ public AzureClient( EntryPointGenerator = entryPointGenerator; Logger = logger; eventService?.TriggerServiceInitialized(this); + + if (engine is BaseEngine baseEngine) + { + baseEngine.RegisterDisplayEncoder(new CloudJobToHtmlEncoder()); + baseEngine.RegisterDisplayEncoder(new CloudJobToTextEncoder()); + baseEngine.RegisterDisplayEncoder(new TargetStatusToHtmlEncoder()); + baseEngine.RegisterDisplayEncoder(new TargetStatusToTextEncoder()); + baseEngine.RegisterDisplayEncoder(new HistogramToHtmlEncoder()); + baseEngine.RegisterDisplayEncoder(new HistogramToTextEncoder()); + } } /// @@ -148,8 +157,7 @@ public async Task ConnectAsync(IChannel channel, channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}."); - // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return ValidExecutionTargets.ToJupyterTable().ToExecutionResult(); + return ValidExecutionTargets.ToExecutionResult(); } /// @@ -162,8 +170,7 @@ public async Task GetConnectionStatusAsync(IChannel channel) channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}."); - // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return ValidExecutionTargets.ToJupyterTable().ToExecutionResult(); + return ValidExecutionTargets.ToExecutionResult(); } private async Task SubmitOrExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters, bool execute) @@ -187,20 +194,20 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st return AzureClientError.NoOperationName.ToExecutionResult(); } - var machine = QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetName, ConnectionString); + var machine = QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetId, ConnectionString); if (machine == null) { // 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}."); + channel.Stderr($"Unexpected error while preparing job for execution on target {ActiveTarget.TargetId}."); return AzureClientError.InvalidTarget.ToExecutionResult(); } - channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); + channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetId}..."); IEntryPoint? entryPoint = null; try { - entryPoint = EntryPointGenerator.Generate(operationName, ActiveTarget.TargetName); + entryPoint = EntryPointGenerator.Generate(operationName, ActiveTarget.TargetId); } catch (UnsupportedOperationException e) { @@ -279,18 +286,18 @@ public async Task GetActiveTargetAsync(IChannel channel) if (ActiveTarget == null) { - channel.Stderr("No execution target has been specified. To specify one, run:\n%azure.target "); + 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($"Current execution target: {ActiveTarget.TargetId}"); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); - return ActiveTarget.TargetName.ToExecutionResult(); + return ActiveTarget.TargetId.ToExecutionResult(); } /// - public async Task SetActiveTargetAsync(IChannel channel, string targetName) + public async Task SetActiveTargetAsync(IChannel channel, string targetId) { if (AvailableProviders == null) { @@ -298,19 +305,19 @@ public async Task SetActiveTargetAsync(IChannel channel, string return AzureClientError.NotConnected.ToExecutionResult(); } - // Validate that this target name is valid in the workspace. - if (!AvailableTargets.Any(target => targetName == target.Id)) + // Validate that this target is valid in the workspace. + if (!AvailableTargets.Any(target => targetId == target.Id)) { - channel.Stderr($"Target name {targetName} is not available in the current Azure Quantum workspace."); + channel.Stderr($"Target {targetId} 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. - var executionTarget = AzureExecutionTarget.Create(targetName); + // Validate that we know which package to load for this target. + var executionTarget = AzureExecutionTarget.Create(targetId); if (executionTarget == null) { - channel.Stderr($"Target name {targetName} does not support executing Q# jobs."); + channel.Stderr($"Target {targetId} does not support executing Q# jobs."); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); return AzureClientError.InvalidTarget.ToExecutionResult(); } @@ -321,7 +328,7 @@ public async Task SetActiveTargetAsync(IChannel channel, string channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies..."); await References.AddPackage(ActiveTarget.PackageName); - return $"Active target is now {ActiveTarget.TargetName}".ToExecutionResult(); + return $"Active target is now {ActiveTarget.TargetId}".ToExecutionResult(); } /// @@ -357,19 +364,18 @@ public async Task GetJobResultAsync(IChannel channel, string jo return AzureClientError.JobNotCompleted.ToExecutionResult(); } - var stream = new MemoryStream(); - await new JobStorageHelper(ConnectionString).DownloadJobOutputAsync(jobId, stream); - stream.Seek(0, SeekOrigin.Begin); - var output = new StreamReader(stream).ReadToEnd(); - var deserializedOutput = JsonConvert.DeserializeObject>(output); - var histogram = new Dictionary(); - foreach (var entry in deserializedOutput["histogram"] as JObject) + try { - histogram[entry.Key] = entry.Value.ToObject(); + var request = WebRequest.Create(job.Details.OutputDataUri); + using var responseStream = request.GetResponse().GetResponseStream(); + return responseStream.ToHistogram().ToExecutionResult(); + } + catch (Exception e) + { + channel.Stderr($"Failed to retrieve results for job ID {jobId}."); + Logger?.LogError(e, $"Failed to download the job output for the specified Azure Quantum job: {e.Message}"); + return AzureClientError.JobOutputDownloadFailed.ToExecutionResult(); } - - // TODO: Add encoder to visualize IEnumerable> - return histogram.ToExecutionResult(); } /// @@ -399,8 +405,7 @@ public async Task GetJobStatusAsync(IChannel channel, string jo return AzureClientError.JobNotFound.ToExecutionResult(); } - // TODO: Add encoder for CloudJob rather than calling ToJupyterTable() here directly. - return job.ToJupyterTable().ToExecutionResult(); + return job.ToExecutionResult(); } /// @@ -419,8 +424,7 @@ public async Task GetJobListAsync(IChannel channel) return AzureClientError.JobNotFound.ToExecutionResult(); } - // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return jobs.ToJupyterTable().ToExecutionResult(); + return jobs.ToExecutionResult(); } private async Task GetCloudJob(string jobId) diff --git a/src/AzureClient/AzureExecutionTarget.cs b/src/AzureClient/AzureExecutionTarget.cs index 2f0d5c89d1..d56064efa5 100644 --- a/src/AzureClient/AzureExecutionTarget.cs +++ b/src/AzureClient/AzureExecutionTarget.cs @@ -11,28 +11,28 @@ internal enum AzureProvider { IonQ, Honeywell, QCI } internal class AzureExecutionTarget { - public string TargetName { get; private set; } - public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetName)}"; + public string TargetId { get; private set; } + public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetId)}"; - public static bool IsValid(string targetName) => GetProvider(targetName) != null; + public static bool IsValid(string targetId) => GetProvider(targetId) != null; - public static AzureExecutionTarget? Create(string targetName) => - IsValid(targetName) - ? new AzureExecutionTarget() { TargetName = targetName } + public static AzureExecutionTarget? Create(string targetId) => + IsValid(targetId) + ? new AzureExecutionTarget() { TargetId = targetId } : null; /// /// Gets the Azure Quantum provider corresponding to the given execution target. /// - /// The Azure Quantum execution target name. + /// The Azure Quantum execution target ID. /// The enum value representing the provider. /// - /// Valid target names are structured as "provider.target". + /// Valid target IDs are structured as "provider.target". /// For example, "ionq.simulator" or "honeywell.qpu". /// - private static AzureProvider? GetProvider(string targetName) + private static AzureProvider? GetProvider(string targetId) { - var parts = targetName.Split('.', 2); + var parts = targetId.Split('.', 2); if (Enum.TryParse(parts[0], true, out AzureProvider provider)) { return provider; diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 0727e8aaca..1fda56014e 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -4,16 +4,12 @@ #nullable enable using System; +using System.Collections; using System.Collections.Generic; 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; using Microsoft.Jupyter.Core; -using Microsoft.Quantum.Runtime; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -37,7 +33,7 @@ public static void AddAzureClient(this IServiceCollection services) /// /// The result of an IAzureClient API call. /// - public static ExecutionResult ToExecutionResult(this AzureClientError azureClientError) => + internal static ExecutionResult ToExecutionResult(this AzureClientError azureClientError) => new ExecutionResult { Status = ExecuteStatus.Error, @@ -50,7 +46,7 @@ public static ExecutionResult ToExecutionResult(this AzureClientError azureClien /// /// /// - public static string ToDescription(this AzureClientError azureClientError) + internal static string ToDescription(this AzureClientError azureClientError) { var attributes = azureClientError .GetType() @@ -65,60 +61,21 @@ public static string ToDescription(this AzureClientError azureClientError) /// /// A task which will return the result of an IAzureClient API call. /// - public static async Task ToExecutionResult(this Task task) => + internal static async Task ToExecutionResult(this Task task) => (await task).ToExecutionResult(); - internal static Table ToJupyterTable(this CloudJob cloudJob) => - new List { cloudJob }.ToJupyterTable(); - - internal static Table ToJupyterTable(this IEnumerable jobsList) => - new Table - { - Columns = new List<(string, Func)> - { - ("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() - }; - - internal static Table ToJupyterTable(this IQuantumMachineJob job) => - new Table - { - Columns = new List<(string, Func)> - { - ("JobId", job => job.Id), - ("JobStatus", job => job.Status), - }, - Rows = new List() { job } - }; - - internal static Table ToJupyterTable(this IQuantumClient quantumClient) => - new Table - { - Columns = new List<(string, Func)> - { - ("SubscriptionId", quantumClient => quantumClient.SubscriptionId), - ("ResourceGroupName", quantumClient => quantumClient.ResourceGroupName), - ("WorkspaceName", quantumClient => quantumClient.WorkspaceName), - }, - Rows = new List() { quantumClient } - }; - - internal static Table ToJupyterTable(this IEnumerable targets) => - new Table - { - Columns = new List<(string, Func)> - { - ("TargetId", target => target.Id), - ("CurrentAvailability", target => target.CurrentAvailability), - ("AverageQueueTime", target => target.AverageQueueTime.ToString()), - ("StatusPage", target => target.StatusPage), - }, - Rows = targets.ToList() - }; + /// + /// Returns the provided argument as an enumeration of the specified type. + /// + /// + /// If the argument is already an of the specified type, + /// the argument is returned. If the argument is of type T, then an + /// enumeration is returned with this argument as the only element. + /// Otherwise, null is returned. + /// + internal static IEnumerable? AsEnumerableOf(this object? source) => + source is T singleton ? new List { singleton } : + source is IEnumerable collection ? collection : + null; } } diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 952c28d796..62937522c8 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -51,6 +51,12 @@ public enum AzureClientError [Description(Resources.AzureClientErrorJobNotCompleted)] JobNotCompleted, + /// + /// The job output failed to be downloaded from the Azure storage location. + /// + [Description(Resources.AzureClientErrorJobOutputDownloadFailed)] + JobOutputDownloadFailed, + /// /// No Q# operation name was provided where one was required. /// @@ -139,13 +145,13 @@ public Task ConnectAsync(IChannel channel, /// /// Success if the target is valid, or an error if the target cannot be set. /// - public Task SetActiveTargetAsync(IChannel channel, string targetName); + public Task SetActiveTargetAsync(IChannel channel, string targetId); /// /// Gets the currently specified target for job submission. /// /// - /// The target name. + /// The target ID. /// public Task GetActiveTargetAsync(IChannel channel); diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 2a5cf36c93..778f88ab74 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -17,7 +17,7 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class TargetMagic : AzureClientMagicBase { - private const string ParameterNameTargetName = "name"; + private const string ParameterNameTargetId = "id"; /// /// Initializes a new instance of the class. @@ -45,8 +45,8 @@ available in the workspace. @" Set the current target for job submission: ``` - In []: %azure.target TARGET_NAME - Out[]: Active target is now TARGET_NAME + In []: %azure.target TARGET_ID + Out[]: Active target is now TARGET_ID ``` ".Dedent(), @" @@ -65,11 +65,11 @@ available in the workspace. /// public override async Task RunAsync(string input, IChannel channel) { - var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameTargetName); - if (inputParameters.ContainsKey(ParameterNameTargetName)) + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameTargetId); + if (inputParameters.ContainsKey(ParameterNameTargetId)) { - string targetName = inputParameters.DecodeParameter(ParameterNameTargetName); - return await AzureClient.SetActiveTargetAsync(channel, targetName); + string targetId = inputParameters.DecodeParameter(ParameterNameTargetId); + return await AzureClient.SetActiveTargetAsync(channel, targetId); } return await AzureClient.GetActiveTargetAsync(channel); diff --git a/src/AzureClient/Resources.cs b/src/AzureClient/Resources.cs index a85a3cf463..f77ad445d9 100644 --- a/src/AzureClient/Resources.cs +++ b/src/AzureClient/Resources.cs @@ -25,6 +25,9 @@ internal static class Resources public const string AzureClientErrorJobNotCompleted = "The specified Azure Quantum job has not yet completed."; + public const string AzureClientErrorJobOutputDownloadFailed = + "Failed to download results for the specified Azure Quantum job."; + public const string AzureClientErrorNoOperationName = "No Q# operation name was specified for Azure Quantum job submission."; diff --git a/src/AzureClient/Visualization/CloudJobEncoders.cs b/src/AzureClient/Visualization/CloudJobEncoders.cs new file mode 100644 index 0000000000..8e5f9b5610 --- /dev/null +++ b/src/AzureClient/Visualization/CloudJobEncoders.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Microsoft.Azure.Quantum; +using Microsoft.Jupyter.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal static class CloudJobExtensions + { + private static DateTime? ToDateTime(this string serializedDateTime) => + DateTime.TryParse(serializedDateTime, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dateTime) + ? dateTime + : null as DateTime?; + + internal static Dictionary ToDictionary(this CloudJob cloudJob) => + new Dictionary() + { + // TODO: add cloudJob.Uri after https://github.com/microsoft/qsharp-runtime/issues/236 is fixed. + ["id"] = cloudJob.Id, + ["name"] = cloudJob.Details.Name, + ["status"] = cloudJob.Status, + ["provider"] = cloudJob.Details.ProviderId, + ["target"] = cloudJob.Details.Target, + ["creationTime"] = cloudJob.Details.CreationTime.ToDateTime()?.ToUniversalTime(), + ["beginExecutionTime"] = cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToUniversalTime(), + ["endExecutionTime"] = cloudJob.Details.EndExecutionTime.ToDateTime()?.ToUniversalTime(), + }; + + internal static Table ToJupyterTable(this IEnumerable jobsList) => + new Table + { + Columns = new List<(string, Func)> + { + // TODO: add cloudJob.Uri after https://github.com/microsoft/qsharp-runtime/issues/236 is fixed. + ("Job ID", cloudJob => cloudJob.Id), + ("Job Name", cloudJob => cloudJob.Details.Name), + ("Job Status", cloudJob => cloudJob.Status), + ("Provider", cloudJob => cloudJob.Details.ProviderId), + ("Target", cloudJob => cloudJob.Details.Target), + ("Creation Time", cloudJob => cloudJob.Details.CreationTime.ToDateTime()?.ToString()), + ("Begin Execution Time", cloudJob => cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToString()), + ("End Execution Time", cloudJob => cloudJob.Details.EndExecutionTime.ToDateTime()?.ToString()), + }, + Rows = jobsList.OrderByDescending(job => job.Details.CreationTime).ToList() + }; + } + + public class CloudJobToHtmlEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToHtmlDisplayEncoder(); + + public string MimeType => MimeTypes.Html; + + public EncodedData? Encode(object displayable) => + displayable.AsEnumerableOf() is IEnumerable jobs + ? tableEncoder.Encode(jobs.ToJupyterTable()) + : null; + } + + public class CloudJobToTextEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToTextDisplayEncoder(); + + public string MimeType => MimeTypes.PlainText; + + public EncodedData? Encode(object displayable) => + displayable.AsEnumerableOf() is IEnumerable jobs + ? tableEncoder.Encode(jobs.ToJupyterTable()) + : null; + } + + public class CloudJobJsonConverter : JsonConverter + { + public override CloudJob ReadJson(JsonReader reader, Type objectType, CloudJob existingValue, bool hasExistingValue, JsonSerializer serializer) + => throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, CloudJob value, JsonSerializer serializer) => + JToken.FromObject(value.ToDictionary()).WriteTo(writer); + } + + public class CloudJobListJsonConverter : JsonConverter> + { + public override IEnumerable ReadJson(JsonReader reader, Type objectType, IEnumerable existingValue, bool hasExistingValue, JsonSerializer serializer) + => throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, IEnumerable value, JsonSerializer serializer) => + JToken.FromObject(value.Select(job => job.ToDictionary())).WriteTo(writer); + } +} diff --git a/src/AzureClient/Visualization/HistogramEncoders.cs b/src/AzureClient/Visualization/HistogramEncoders.cs new file mode 100644 index 0000000000..befbbf095b --- /dev/null +++ b/src/AzureClient/Visualization/HistogramEncoders.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Jupyter.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class Histogram : Dictionary + { + } + + internal static class HistogramExtensions + { + internal static Histogram ToHistogram(this Stream stream) + { + var output = new StreamReader(stream).ReadToEnd(); + var deserializedOutput = JsonConvert.DeserializeObject>(output); + var deserializedHistogram = deserializedOutput["Histogram"] as JArray; + + var histogram = new Histogram(); + for (var i = 0; i < deserializedHistogram.Count - 1; i += 2) + { + var key = deserializedHistogram[i].ToObject(); + var value = deserializedHistogram[i + 1].ToObject(); + histogram[key] = value; + } + + return histogram; + } + + internal static Table> ToJupyterTable(this Histogram histogram) => + new Table> + { + Columns = new List<(string, Func, string>)> + { + ("Result", entry => entry.Key), + ("Frequency", entry => entry.Value.ToString()), + }, + Rows = histogram.ToList() + }; + } + + public class HistogramToHtmlEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToHtmlDisplayEncoder(); + + public string MimeType => MimeTypes.Html; + + public EncodedData? Encode(object displayable) + { + if (displayable is Histogram histogram) + { + var style = "text-align: left"; + var columnStyle = $"{style}; width: 25ch"; + var lastColumnStyle = $"{style}; width: calc(100% - 25ch - 25ch)"; + + // Make the HTML table body by formatting everything as individual rows. + var formattedData = string.Join("\n", + histogram.Select(entry => + { + var result = entry.Key; + var frequency = entry.Value; + + return FormattableString.Invariant($@" + + {result} + {frequency} + + + + + "); + }) + ); + + // Construct and return the table. + var outputTable = $@" + + + + + + + + + + {formattedData} + +
ResultFrequencyHistogram
+ "; + return outputTable.ToEncodedData(); + } + else return null; + } + } + + public class HistogramToTextEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToTextDisplayEncoder(); + + public string MimeType => MimeTypes.PlainText; + + public EncodedData? Encode(object displayable) => + displayable is Histogram histogram + ? tableEncoder.Encode(histogram.ToJupyterTable()) + : null; + } +} diff --git a/src/AzureClient/Visualization/JsonConverters.cs b/src/AzureClient/Visualization/JsonConverters.cs new file mode 100644 index 0000000000..24f26f2515 --- /dev/null +++ b/src/AzureClient/Visualization/JsonConverters.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; + +using Newtonsoft.Json; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + public static class JsonConverters + { + private static readonly ImmutableList allConverters = ImmutableList.Create( + new CloudJobJsonConverter(), + new CloudJobListJsonConverter(), + new TargetStatusJsonConverter(), + new TargetStatusListJsonConverter() + ); + + public static JsonConverter[] AllConverters => allConverters.ToArray(); + } +} diff --git a/src/AzureClient/Visualization/TargetStatusEncoders.cs b/src/AzureClient/Visualization/TargetStatusEncoders.cs new file mode 100644 index 0000000000..1de24e7bc0 --- /dev/null +++ b/src/AzureClient/Visualization/TargetStatusEncoders.cs @@ -0,0 +1,81 @@ +// 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.Text; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Jupyter.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal static class TargetStatusExtensions + { + internal static Dictionary ToDictionary(this TargetStatus target) => + new Dictionary() + { + ["id"] = target.Id, + ["currentAvailability"] = target.CurrentAvailability, + ["averageQueueTime"] = target.AverageQueueTime, + }; + + internal static Table ToJupyterTable(this IEnumerable targets) => + new Table + { + Columns = new List<(string, Func)> + { + ("Target ID", target => target.Id), + ("Current Availability", target => target.CurrentAvailability), + ("Average Queue Time (Seconds)", target => target.AverageQueueTime.ToString()), + }, + Rows = targets.ToList() + }; + } + + public class TargetStatusToHtmlEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToHtmlDisplayEncoder(); + + public string MimeType => MimeTypes.Html; + + public EncodedData? Encode(object displayable) => + displayable.AsEnumerableOf() is IEnumerable targets + ? tableEncoder.Encode(targets.ToJupyterTable()) + : null; + } + + public class TargetStatusToTextEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToTextDisplayEncoder(); + + public string MimeType => MimeTypes.PlainText; + + public EncodedData? Encode(object displayable) => + displayable.AsEnumerableOf() is IEnumerable targets + ? tableEncoder.Encode(targets.ToJupyterTable()) + : null; + } + + public class TargetStatusJsonConverter : JsonConverter + { + public override TargetStatus ReadJson(JsonReader reader, Type objectType, TargetStatus existingValue, bool hasExistingValue, JsonSerializer serializer) + => throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, TargetStatus value, JsonSerializer serializer) => + JToken.FromObject(value.ToDictionary()).WriteTo(writer); + } + + public class TargetStatusListJsonConverter : JsonConverter> + { + public override IEnumerable ReadJson(JsonReader reader, Type objectType, IEnumerable existingValue, bool hasExistingValue, JsonSerializer serializer) + => throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, IEnumerable value, JsonSerializer serializer) => + JToken.FromObject(value.Select(job => job.ToDictionary())).WriteTo(writer); + } +} diff --git a/src/Kernel/IQSharpEngine.cs b/src/Kernel/IQSharpEngine.cs index 759b89114b..88122f5271 100644 --- a/src/Kernel/IQSharpEngine.cs +++ b/src/Kernel/IQSharpEngine.cs @@ -60,7 +60,11 @@ IMagicSymbolResolver magicSymbolResolver RegisterDisplayEncoder(new DataTableToTextEncoder()); RegisterDisplayEncoder(new DisplayableExceptionToHtmlEncoder()); RegisterDisplayEncoder(new DisplayableExceptionToTextEncoder()); - RegisterJsonEncoder(JsonConverters.AllConverters); + + RegisterJsonEncoder( + JsonConverters.AllConverters + .Concat(AzureClient.JsonConverters.AllConverters) + .ToArray()); RegisterSymbolResolver(this.SymbolsResolver); RegisterSymbolResolver(this.MagicResolver); diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index a45f1eb6c0..6e91ad9491 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -32,7 +32,7 @@ public class AzureClientMagicTests 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"; + private readonly string targetId = "TEST_TARGET_ID"; [TestMethod] public void TestConnectMagic() @@ -142,7 +142,7 @@ public void TestTargetMagic() // single argument - should set active target var azureClient = new MockAzureClient(); var targetMagic = new TargetMagic(azureClient); - targetMagic.Test(targetName); + targetMagic.Test(targetId); Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); // no arguments - should print active target @@ -172,20 +172,20 @@ public class MockAzureClient : IAzureClient internal AzureClientAction LastAction = AzureClientAction.None; internal string ConnectionString = string.Empty; internal bool RefreshCredentials = false; - internal string ActiveTargetName = string.Empty; + internal string ActiveTargetId = string.Empty; internal List SubmittedJobs = new List(); internal List ExecutedJobs = new List(); - public async Task SetActiveTargetAsync(IChannel channel, string targetName) + public async Task SetActiveTargetAsync(IChannel channel, string targetId) { LastAction = AzureClientAction.SetActiveTarget; - ActiveTargetName = targetName; + ActiveTargetId = targetId; return ExecuteStatus.Ok.ToExecutionResult(); } public async Task GetActiveTargetAsync(IChannel channel) { LastAction = AzureClientAction.GetActiveTarget; - return ActiveTargetName.ToExecutionResult(); + return ActiveTargetId.ToExecutionResult(); } public async Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters) diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index 2044b2116a..a115085104 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -33,11 +33,11 @@ public void TestTargets() var services = Startup.CreateServiceProvider(workspace); var azureClient = services.GetService(); - // SetActiveTargetAsync with recognized target name, but not yet connected + // SetActiveTargetAsync with recognized target ID, but not yet connected var result = azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator").GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Error); - // SetActiveTargetAsync with unrecognized target name + // SetActiveTargetAsync with unrecognized target ID result = azureClient.SetActiveTargetAsync(new MockChannel(), "contoso.qpu").GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Error); @@ -49,26 +49,26 @@ public void TestTargets() [TestMethod] public void TestAzureExecutionTarget() { - var targetName = "invalidname"; - var executionTarget = AzureExecutionTarget.Create(targetName); + var targetId = "invalidname"; + var executionTarget = AzureExecutionTarget.Create(targetId); Assert.IsNull(executionTarget); - targetName = "ionq.targetname"; - executionTarget = AzureExecutionTarget.Create(targetName); + targetId = "ionq.targetId"; + executionTarget = AzureExecutionTarget.Create(targetId); Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetName, targetName); + Assert.AreEqual(executionTarget.TargetId, targetId); Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.IonQ"); - targetName = "HonEYWEll.targetname"; - executionTarget = AzureExecutionTarget.Create(targetName); + targetId = "HonEYWEll.targetId"; + executionTarget = AzureExecutionTarget.Create(targetId); Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetName, targetName); + Assert.AreEqual(executionTarget.TargetId, targetId); Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.Honeywell"); - targetName = "qci.target.name.qpu"; - executionTarget = AzureExecutionTarget.Create(targetName); + targetId = "qci.target.name.qpu"; + executionTarget = AzureExecutionTarget.Create(targetId); Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetName, targetName); + Assert.AreEqual(executionTarget.TargetId, targetId); Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.QCI"); } } From 79cf9ca7a510548a869fca6f0d60c53b80aa6d34 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 12 Jun 2020 17:05:56 -0400 Subject: [PATCH 25/44] Support several parameters for IQ# Azure Quantum job execution (#161) * Updates to IQ# syntax highlighting * Validate targets and load provider packages * Update Python interface for Azure commands * Simplify AzureExecutionTarget class * Generate EntryPoint and compile into new assembly * Changes for JobNotCompleted case * Refactor entry point code into new classes * Use correct input and output types * Simplify property syntax Co-authored-by: Chris Granade * Add simple tests for AzureExecutionTarget class * Recompile everything for specified execution target * Add tests and error handling * Improve variable names * Add status message while loading provider package * Add documentation to AzureExecutionTarget.GetProvider * Extend tests to call EntryPoint.SubmitAsync * Wait for job completion on %azure.execute * Update comment * Add timeout for %azure.execute * Minor fixes in AzureClient * Minor formatting and comment tweaks * Style improvements in test code * More consistent handling of job objects * Consistent error handling for IWorkspace calls * Update to latest QDK released version * Add encoders for CloudJob and TargetStatus * Move extension methods into encoder files * Change signature of CloudJob.ToDictionary * Update histogram deserialization and add encoders * Use JsonConverter classes directly * Small cleanup * Register single JsonEncoder * Add a simple HTML histogram display based on StateVectorToHtmlResultEncoder * Various improvements from PR suggestions * Use UTC for dates returned to Python * Support jobName and shots parameters * Unify submission and execution code * Add some documentation to AzureSubmissionContext * Documentation fix * Add timeout and pollingInterval parameters * Use switch syntax for entryPointInput Co-authored-by: Chris Granade * Remove 'All rights reserved.' Co-authored-by: Chris Granade * Addressing PR feedback and compiler warnings * Use LINQ for decodedParameters * Update parameter names for consistency Co-authored-by: Chris Granade --- src/AzureClient/AzureClient.cs | 38 +++++----- src/AzureClient/AzureSubmissionContext.cs | 92 +++++++++++++++++++++++ src/AzureClient/EntryPoint/EntryPoint.cs | 15 ++-- src/AzureClient/EntryPoint/IEntryPoint.cs | 4 +- src/AzureClient/IAzureClient.cs | 4 +- src/AzureClient/Magic/ExecuteMagic.cs | 14 +--- src/AzureClient/Magic/OutputMagic.cs | 2 +- src/AzureClient/Magic/StatusMagic.cs | 2 +- src/AzureClient/Magic/SubmitMagic.cs | 13 +--- src/Jupyter/Extensions.cs | 2 +- src/Jupyter/Magic/AbstractMagic.cs | 2 +- src/Kernel/Magic/EstimateMagic.cs | 7 +- src/Kernel/Magic/Simulate.cs | 3 +- src/Kernel/Magic/ToffoliMagic.cs | 7 +- src/Tests/AzureClientEntryPointTests.cs | 26 +++++-- src/Tests/AzureClientMagicTests.cs | 8 +- 16 files changed, 163 insertions(+), 76 deletions(-) create mode 100644 src/AzureClient/AzureSubmissionContext.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 4ec62ed001..27ebd616f1 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -173,7 +173,7 @@ public async Task GetConnectionStatusAsync(IChannel channel) return ValidExecutionTargets.ToExecutionResult(); } - private async Task SubmitOrExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters, bool execute) + private async Task SubmitOrExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext, bool execute) { if (ActiveWorkspace == null) { @@ -187,7 +187,7 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st return AzureClientError.NoTarget.ToExecutionResult(); } - if (string.IsNullOrEmpty(operationName)) + if (string.IsNullOrEmpty(submissionContext.OperationName)) { var commandName = execute ? "%azure.execute" : "%azure.submit"; channel.Stderr($"Please pass a valid Q# operation name to {commandName}."); @@ -202,40 +202,42 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st return AzureClientError.InvalidTarget.ToExecutionResult(); } - channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetId}..."); + channel.Stdout($"Submitting {submissionContext.OperationName} to target {ActiveTarget.TargetId}..."); IEntryPoint? entryPoint = null; try { - entryPoint = EntryPointGenerator.Generate(operationName, ActiveTarget.TargetId); + entryPoint = EntryPointGenerator.Generate(submissionContext.OperationName, ActiveTarget.TargetId); } catch (UnsupportedOperationException e) { - channel.Stderr($"{operationName} is not a recognized Q# operation name."); + channel.Stderr($"{submissionContext.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."); + channel.Stderr($"The Q# operation {submissionContext.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(); } try { - var job = await entryPoint.SubmitAsync(machine, inputParameters); - channel.Stdout($"Job {job.Id} submitted successfully."); + var job = await entryPoint.SubmitAsync(machine, submissionContext); + channel.Stdout($"Job successfully submitted for {submissionContext.Shots} shots."); + channel.Stdout($" Job name: {submissionContext.FriendlyName}"); + channel.Stdout($" Job ID: {job.Id}"); MostRecentJobId = job.Id; } catch (ArgumentException e) { - channel.Stderr($"Failed to parse all expected parameters for Q# operation {operationName}."); + channel.Stderr($"Failed to parse all expected parameters for Q# operation {submissionContext.OperationName}."); channel.Stderr(e.Message); return AzureClientError.JobSubmissionFailed.ToExecutionResult(); } catch (Exception e) { - channel.Stderr($"Failed to submit Q# operation {operationName} for execution."); + channel.Stderr($"Failed to submit Q# operation {submissionContext.OperationName} for execution."); channel.Stderr(e.InnerException?.Message ?? e.Message); return AzureClientError.JobSubmissionFailed.ToExecutionResult(); } @@ -245,18 +247,16 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st return await GetJobStatusAsync(channel, MostRecentJobId); } - var timeoutInSeconds = 30; - channel.Stdout($"Waiting up to {timeoutInSeconds} seconds for Azure Quantum job to complete..."); + channel.Stdout($"Waiting up to {submissionContext.ExecutionTimeout} seconds for Azure Quantum job to complete..."); - using (var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(30))) + using (var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(submissionContext.ExecutionTimeout))) { 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)); + await Task.Delay(TimeSpan.FromSeconds(submissionContext.ExecutionPollingInterval)); if (cts.IsCancellationRequested) break; cloudJob = await GetCloudJob(MostRecentJobId); channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}"); @@ -268,12 +268,12 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st } /// - public async Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters) => - await SubmitOrExecuteJobAsync(channel, operationName, inputParameters, execute: false); + public async Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext) => + await SubmitOrExecuteJobAsync(channel, submissionContext, execute: false); /// - public async Task ExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters) => - await SubmitOrExecuteJobAsync(channel, operationName, inputParameters, execute: true); + public async Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext) => + await SubmitOrExecuteJobAsync(channel, submissionContext, execute: true); /// public async Task GetActiveTargetAsync(IChannel channel) diff --git a/src/AzureClient/AzureSubmissionContext.cs b/src/AzureClient/AzureSubmissionContext.cs new file mode 100644 index 0000000000..bfe023e9fc --- /dev/null +++ b/src/AzureClient/AzureSubmissionContext.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using Microsoft.Quantum.IQSharp.Jupyter; +using Microsoft.Quantum.Runtime; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// Represents the configuration settings for a job submission to Azure Quantum. + /// + public sealed class AzureSubmissionContext : IQuantumMachineSubmissionContext + { + private static readonly int DefaultShots = 500; + private static readonly int DefaultExecutionTimeoutInSeconds = 30; + private static readonly int DefaultExecutionPollingIntervalInSeconds = 5; + + internal static readonly string ParameterNameOperationName = "__operationName__"; + internal static readonly string ParameterNameJobName = "jobName"; + internal static readonly string ParameterNameShots = "shots"; + internal static readonly string ParameterNameTimeout = "timeout"; + internal static readonly string ParameterNamePollingInterval = "poll"; + + /// + public string FriendlyName { get; set; } = string.Empty; + + /// + public int Shots { get; set; } = DefaultShots; + + /// + /// The Q# operation name to be executed as part of this job. + /// + public string OperationName { get; set; } = string.Empty; + + /// + /// The input parameters to be provided to the specified Q# operation. + /// + public Dictionary InputParameters { get; set; } = new Dictionary(); + + /// + /// The execution timeout for the job, expressed in seconds. + /// + /// + /// This setting only applies to %azure.execute. It is ignored for %azure.submit. + /// The timeout determines how long the IQ# kernel will wait for the job to complete; + /// the Azure Quantum job itself will continue to execute until it is completed. + /// + public int ExecutionTimeout { get; set; } = DefaultExecutionTimeoutInSeconds; + + /// + /// The polling interval, in seconds, to check for job status updates + /// while waiting for an Azure Quantum job to complete execution. + /// + /// + /// This setting only applies to %azure.execute. It is ignored for %azure.submit. + /// + public int ExecutionPollingInterval { get; set; } = DefaultExecutionPollingIntervalInSeconds; + + /// + /// Parses the input from a magic command into an object + /// suitable for job submission via . + /// + public static AzureSubmissionContext Parse(string inputCommand) + { + var inputParameters = AbstractMagic.ParseInputParameters(inputCommand, firstParameterInferredName: ParameterNameOperationName); + var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); + var jobName = inputParameters.DecodeParameter(ParameterNameJobName, defaultValue: operationName); + var shots = inputParameters.DecodeParameter(ParameterNameShots, defaultValue: DefaultShots); + var timeout = inputParameters.DecodeParameter(ParameterNameTimeout, defaultValue: DefaultExecutionTimeoutInSeconds); + var pollingInterval = inputParameters.DecodeParameter(ParameterNamePollingInterval, defaultValue: DefaultExecutionPollingIntervalInSeconds); + + var decodedParameters = inputParameters.ToDictionary( + item => item.Key, + item => inputParameters.DecodeParameter(item.Key)); + + return new AzureSubmissionContext() + { + FriendlyName = jobName, + Shots = shots, + OperationName = operationName, + InputParameters = decodedParameters, + ExecutionTimeout = timeout, + ExecutionPollingInterval = pollingInterval, + }; + } + } +} diff --git a/src/AzureClient/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs index 407f29d0d5..17d10806ed 100644 --- a/src/AzureClient/EntryPoint/EntryPoint.cs +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -39,18 +39,18 @@ public EntryPoint(object entryPointInfo, Type inputType, Type outputType, Operat } /// - public Task SubmitAsync(IQuantumMachine machine, Dictionary inputParameters) + public Task SubmitAsync(IQuantumMachine machine, AzureSubmissionContext submissionContext) { var parameterTypes = new List(); var parameterValues = new List(); foreach (var parameter in OperationInfo.RoslynParameters) { - if (!inputParameters.ContainsKey(parameter.Name)) + if (!submissionContext.InputParameters.ContainsKey(parameter.Name)) { throw new ArgumentException($"Required parameter {parameter.Name} was not specified."); } - string rawParameterValue = inputParameters[parameter.Name]; + string rawParameterValue = submissionContext.InputParameters[parameter.Name]; object? parameterValue = null; try { @@ -73,17 +73,18 @@ public Task SubmitAsync(IQuantumMachine machine, Dictionary< }; // Find and invoke the method on IQuantumMachine that is declared as: - // Task SubmitAsync(EntryPointInfo info, TInput input) + // Task SubmitAsync(EntryPointInfo info, TInput input, SubmissionContext context) var submitMethod = typeof(IQuantumMachine) .GetMethods() .Single(method => method.Name == "SubmitAsync" && method.IsGenericMethodDefinition - && method.GetParameters().Length == 2 + && method.GetParameters().Length == 3 && method.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == EntryPointInfo.GetType().GetGenericTypeDefinition() - && method.GetParameters()[1].ParameterType.IsGenericMethodParameter) + && method.GetParameters()[1].ParameterType.IsGenericMethodParameter + && method.GetParameters()[2].ParameterType == typeof(IQuantumMachineSubmissionContext)) .MakeGenericMethod(new Type[] { InputType, OutputType }); - var submitParameters = new object[] { EntryPointInfo, entryPointInput }; + var submitParameters = new object[] { EntryPointInfo, entryPointInput, submissionContext }; return (Task)submitMethod.Invoke(machine, submitParameters); } } diff --git a/src/AzureClient/EntryPoint/IEntryPoint.cs b/src/AzureClient/EntryPoint/IEntryPoint.cs index 4cc6e063d7..aa3f78caa5 100644 --- a/src/AzureClient/EntryPoint/IEntryPoint.cs +++ b/src/AzureClient/EntryPoint/IEntryPoint.cs @@ -21,8 +21,8 @@ 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 object representing the submission context for the job. /// The details of the submitted job. - public Task SubmitAsync(IQuantumMachine machine, Dictionary inputParameters); + public Task SubmitAsync(IQuantumMachine machine, AzureSubmissionContext submissionContext); } } diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 62937522c8..d6c67678ff 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -128,7 +128,7 @@ public Task ConnectAsync(IChannel channel, /// /// Details of the submitted job, or an error if submission failed. /// - public Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters); + public Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext); /// /// Executes the specified Q# operation as a job to the currently active target @@ -137,7 +137,7 @@ public Task ConnectAsync(IChannel channel, /// /// The result of the executed job, or an error if execution failed. /// - public Task ExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters); + public Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext); /// /// Sets the specified target for job submission. diff --git a/src/AzureClient/Magic/ExecuteMagic.cs b/src/AzureClient/Magic/ExecuteMagic.cs index a5f317688d..6a3080d274 100644 --- a/src/AzureClient/Magic/ExecuteMagic.cs +++ b/src/AzureClient/Magic/ExecuteMagic.cs @@ -3,6 +3,7 @@ #nullable enable +using System; using System.Collections.Generic; using System.Threading.Tasks; using Microsoft.Jupyter.Core; @@ -15,8 +16,6 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class ExecuteMagic : AzureClientMagicBase { - private const string ParameterNameOperationName = "operationName"; - /// /// Initializes a new instance of the class. /// @@ -59,16 +58,7 @@ The Azure Quantum workspace must previously have been initialized /// public override async Task RunAsync(string input, IChannel channel) { - var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); - var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); - - var decodedParameters = new Dictionary(); - foreach (var key in inputParameters.Keys) - { - decodedParameters[key] = inputParameters.DecodeParameter(key); - } - - return await AzureClient.ExecuteJobAsync(channel, operationName, decodedParameters); + return await AzureClient.ExecuteJobAsync(channel, AzureSubmissionContext.Parse(input)); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/OutputMagic.cs b/src/AzureClient/Magic/OutputMagic.cs index f4b722f556..17ca257a46 100644 --- a/src/AzureClient/Magic/OutputMagic.cs +++ b/src/AzureClient/Magic/OutputMagic.cs @@ -17,7 +17,7 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class OutputMagic : AzureClientMagicBase { - private const string ParameterNameJobId = "jobId"; + private const string ParameterNameJobId = "id"; /// /// Initializes a new instance of the class. diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs index e3ee13d092..e7eaa56d6c 100644 --- a/src/AzureClient/Magic/StatusMagic.cs +++ b/src/AzureClient/Magic/StatusMagic.cs @@ -17,7 +17,7 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class StatusMagic : AzureClientMagicBase { - private const string ParameterNameJobId = "jobId"; + private const string ParameterNameJobId = "id"; /// /// Initializes a new instance of the class. diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index cb646a9903..516646382e 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -15,8 +15,6 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class SubmitMagic : AzureClientMagicBase { - private const string ParameterNameOperationName = "operationName"; - /// /// Initializes a new instance of the class. /// @@ -56,16 +54,7 @@ The Azure Quantum workspace must previously have been initialized /// public override async Task RunAsync(string input, IChannel channel) { - var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); - var operationName = inputParameters.DecodeParameter(ParameterNameOperationName); - - var decodedParameters = new Dictionary(); - foreach (var key in inputParameters.Keys) - { - decodedParameters[key] = inputParameters.DecodeParameter(key); - } - - return await AzureClient.SubmitJobAsync(channel, operationName, decodedParameters); + return await AzureClient.SubmitJobAsync(channel, AzureSubmissionContext.Parse(input)); } } } \ No newline at end of file diff --git a/src/Jupyter/Extensions.cs b/src/Jupyter/Extensions.cs index b016d9c328..7b2c1bb9bf 100644 --- a/src/Jupyter/Extensions.cs +++ b/src/Jupyter/Extensions.cs @@ -187,7 +187,7 @@ public static T DecodeParameter(this Dictionary parameters, s { return defaultValue; } - return (T)(JsonConvert.DeserializeObject(parameterValue)) ?? defaultValue; + return (T)System.Convert.ChangeType(JsonConvert.DeserializeObject(parameterValue), typeof(T)) ?? defaultValue; } } } diff --git a/src/Jupyter/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs index 1b0cd24c3a..09a3df31b8 100644 --- a/src/Jupyter/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -93,7 +93,7 @@ public static Dictionary JsonToDict(string input) => /// Dictionary with the names and values of the parameters, /// where the values of the Dictionary are JSON-serialized objects. /// - public Dictionary ParseInputParameters(string input, string firstParameterInferredName = "") + public static Dictionary ParseInputParameters(string input, string firstParameterInferredName = "") { Dictionary inputParameters = new Dictionary(); diff --git a/src/Kernel/Magic/EstimateMagic.cs b/src/Kernel/Magic/EstimateMagic.cs index 7e227003b7..54e58f81d0 100644 --- a/src/Kernel/Magic/EstimateMagic.cs +++ b/src/Kernel/Magic/EstimateMagic.cs @@ -19,6 +19,8 @@ namespace Microsoft.Quantum.IQSharp.Kernel /// public class EstimateMagic : AbstractMagic { + private const string ParameterNameOperationName = "__operationName__"; + /// /// Given a symbol resolver that can be used to locate operations, /// constructs a new magic command that performs resource estimation @@ -52,15 +54,16 @@ public override ExecutionResult Run(string input, IChannel channel) => /// public async Task RunAsync(string input, IChannel channel) { - var (name, args) = ParseInput(input); + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); + var name = inputParameters.DecodeParameter(ParameterNameOperationName); var symbol = SymbolResolver.Resolve(name) as IQSharpSymbol; if (symbol == null) throw new InvalidOperationException($"Invalid operation name: {name}"); var qsim = new ResourcesEstimator().WithStackTraceDisplay(channel); qsim.DisableLogToConsole(); - await symbol.Operation.RunAsync(qsim, args); + await symbol.Operation.RunAsync(qsim, inputParameters); return qsim.Data.ToExecutionResult(); } diff --git a/src/Kernel/Magic/Simulate.cs b/src/Kernel/Magic/Simulate.cs index 2b94eb9b06..5e552541b8 100644 --- a/src/Kernel/Magic/Simulate.cs +++ b/src/Kernel/Magic/Simulate.cs @@ -18,8 +18,7 @@ namespace Microsoft.Quantum.IQSharp.Kernel /// public class SimulateMagic : AbstractMagic { - private const string - ParameterNameOperationName = "operationName"; + private const string ParameterNameOperationName = "__operationName__"; /// /// Constructs a new magic command given a resolver used to find diff --git a/src/Kernel/Magic/ToffoliMagic.cs b/src/Kernel/Magic/ToffoliMagic.cs index bb46a50e8f..48160c2bef 100644 --- a/src/Kernel/Magic/ToffoliMagic.cs +++ b/src/Kernel/Magic/ToffoliMagic.cs @@ -16,6 +16,8 @@ namespace Microsoft.Quantum.IQSharp.Kernel /// public class ToffoliMagic : AbstractMagic { + private const string ParameterNameOperationName = "__operationName__"; + /// /// Default constructor. /// @@ -43,8 +45,9 @@ public override ExecutionResult Run(string input, IChannel channel) => /// public async Task RunAsync(string input, IChannel channel) { - var (name, args) = ParseInput(input); + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameOperationName); + var name = inputParameters.DecodeParameter(ParameterNameOperationName); var symbol = SymbolResolver.Resolve(name) as IQSharpSymbol; if (symbol == null) throw new InvalidOperationException($"Invalid operation name: {name}"); @@ -52,7 +55,7 @@ public async Task RunAsync(string input, IChannel channel) qsim.DisableLogToConsole(); qsim.OnLog += channel.Stdout; - var value = await symbol.Operation.RunAsync(qsim, args); + var value = await symbol.Operation.RunAsync(qsim, inputParameters); return value.ToExecutionResult(); } diff --git a/src/Tests/AzureClientEntryPointTests.cs b/src/Tests/AzureClientEntryPointTests.cs index 3aba6eb27a..48d0416c61 100644 --- a/src/Tests/AzureClientEntryPointTests.cs +++ b/src/Tests/AzureClientEntryPointTests.cs @@ -42,7 +42,9 @@ public async Task FromSnippet() var entryPoint = entryPointGenerator.Generate("HelloQ", null); Assert.IsNotNull(entryPoint); - var job = await entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary()); + var job = await entryPoint.SubmitAsync( + new MockQuantumMachine(), + new AzureSubmissionContext()); Assert.IsNotNull(job); } @@ -61,7 +63,9 @@ public async Task FromWorkspace() var entryPoint = entryPointGenerator.Generate("Tests.qss.HelloAgain", null); Assert.IsNotNull(entryPoint); - var job = await entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary() { { "count", "2" }, { "name", "test" } } ); + var job = await entryPoint.SubmitAsync( + new MockQuantumMachine(), + new AzureSubmissionContext() { InputParameters = new Dictionary() { ["count"] = "2", ["name"] = "test" } }); Assert.IsNotNull(job); } @@ -73,7 +77,9 @@ public async Task FromWorkspaceMissingArgument() Assert.IsNotNull(entryPoint); Assert.ThrowsException(() => - entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary() { { "count", "2" } })); + entryPoint.SubmitAsync( + new MockQuantumMachine(), + new AzureSubmissionContext() { InputParameters = new Dictionary() { ["count"] = "2" } })); } [TestMethod] @@ -84,7 +90,9 @@ public async Task FromWorkspaceIncorrectArgumentType() Assert.IsNotNull(entryPoint); Assert.ThrowsException(() => - entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary() { { "count", "NaN" }, { "name", "test" } })); + entryPoint.SubmitAsync( + new MockQuantumMachine(), + new AzureSubmissionContext() { InputParameters = new Dictionary() { ["count"] = "NaN", ["name"] = "test" } })); } [TestMethod] @@ -102,7 +110,9 @@ public async Task FromSnippetDependsOnWorkspace() var entryPoint = entryPointGenerator.Generate("DependsOnWorkspace", null); Assert.IsNotNull(entryPoint); - var job = await entryPoint.SubmitAsync(new MockQuantumMachine(), new Dictionary()); + var job = await entryPoint.SubmitAsync( + new MockQuantumMachine(), + new AzureSubmissionContext()); Assert.IsNotNull(job); } @@ -139,10 +149,10 @@ public Task> ExecuteAsync(EntryP => ExecuteAsync(info, input, submissionContext, null, configureJobCallback); public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext executionContext) - => ExecuteAsync(info, input, null as IQuantumMachineExecutionContext); + => ExecuteAsync(info, input, executionContext, null); public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext executionContext, IQuantumMachine.ConfigureJob configureJobCallback) - => ExecuteAsync(info, input, executionContext, null as IQuantumMachine.ConfigureJob); + => ExecuteAsync(info, input, null, executionContext); public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachineExecutionContext executionContext) => ExecuteAsync(info, input, submissionContext, executionContext, null); @@ -154,7 +164,7 @@ public Task SubmitAsync(EntryPointInfo SubmitAsync(info, input, null); public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext) - => SubmitAsync(info, input, null, null); + => SubmitAsync(info, input, submissionContext, null); public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachine.ConfigureJob configureJobCallback) => Task.FromResult(new MockQuantumMachineJob() as IQuantumMachineJob); diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 6e91ad9491..b06833d336 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -188,17 +188,17 @@ public async Task GetActiveTargetAsync(IChannel channel) return ActiveTargetId.ToExecutionResult(); } - public async Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters) + public async Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext) { LastAction = AzureClientAction.SubmitJob; - SubmittedJobs.Add(operationName); + SubmittedJobs.Add(submissionContext.OperationName); return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task ExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters) + public async Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext) { LastAction = AzureClientAction.ExecuteJob; - ExecutedJobs.Add(operationName); + ExecutedJobs.Add(submissionContext.OperationName); return ExecuteStatus.Ok.ToExecutionResult(); } From fba06c95246736562d305b8c43fb14de6ad1ca62 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 12 Jun 2020 17:39:21 -0400 Subject: [PATCH 26/44] Refactor Azure authentication and service calls (#164) * Refactor authentication and service calls --- src/AzureClient/AzureClient.cs | 144 ++++------------------------ src/AzureClient/AzureEnvironment.cs | 111 ++++++++++++++++++--- src/AzureClient/AzureWorkspace.cs | 78 +++++++++++++++ src/AzureClient/IAzureWorkspace.cs | 23 +++++ 4 files changed, 220 insertions(+), 136 deletions(-) create mode 100644 src/AzureClient/AzureWorkspace.cs create mode 100644 src/AzureClient/IAzureWorkspace.cs diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 27ebd616f1..c03ad4a61f 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -5,20 +5,15 @@ using System; using System.Collections.Generic; -using System.IO; using System.Linq; using System.Net; using System.Threading.Tasks; using Microsoft.Azure.Quantum; -using Microsoft.Azure.Quantum.Client; using Microsoft.Azure.Quantum.Client.Models; 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.Simulation.Common; -using Microsoft.Rest.Azure; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -30,19 +25,15 @@ public class AzureClient : IAzureClient private IEntryPointGenerator EntryPointGenerator { get; } private string ConnectionString { get; set; } = string.Empty; private AzureExecutionTarget? ActiveTarget { get; set; } - private AuthenticationResult? AuthenticationResult { get; set; } - private IQuantumClient? QuantumClient { get; set; } - private Azure.Quantum.IWorkspace? ActiveWorkspace { get; set; } + private IAzureWorkspace? 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)); - } + private IEnumerable? AvailableProviders { get; set; } + private IEnumerable? AvailableTargets => AvailableProviders?.SelectMany(provider => provider.Targets); + private IEnumerable? ValidExecutionTargets => AvailableTargets?.Where(target => AzureExecutionTarget.IsValid(target.Id)); + private string ValidExecutionTargetsDisplayText => + ValidExecutionTargets == null + ? "(no execution targets available)" + : string.Join(", ", ValidExecutionTargets.Select(target => target.Id)); public AzureClient( IExecutionEngine engine, @@ -77,85 +68,20 @@ public async Task ConnectAsync(IChannel channel, { ConnectionString = storageAccountConnectionString; - var azureEnvironmentEnvVarName = "AZURE_QUANTUM_ENV"; - var azureEnvironmentName = System.Environment.GetEnvironmentVariable(azureEnvironmentEnvVarName); - var azureEnvironment = AzureEnvironment.Create(azureEnvironmentName, subscriptionId); - - var msalApp = PublicClientApplicationBuilder - .Create(azureEnvironment.ClientId) - .WithAuthority(azureEnvironment.Authority) - .Build(); - - // Register the token cache for serialization - var cacheFileName = "aad.bin"; - var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE"; - var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName); - if (string.IsNullOrEmpty(cacheDirectory)) - { - cacheDirectory = Path.Join(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".azure-quantum"); - } - - var storageCreationProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, azureEnvironment.ClientId).Build(); - var cacheHelper = await MsalCacheHelper.CreateAsync(storageCreationProperties); - cacheHelper.RegisterCache(msalApp.UserTokenCache); - - bool shouldShowLoginPrompt = refreshCredentials; - if (!shouldShowLoginPrompt) - { - try - { - var accounts = await msalApp.GetAccountsAsync(); - AuthenticationResult = await msalApp.AcquireTokenSilent( - azureEnvironment.Scopes, accounts.FirstOrDefault()).WithAuthority(msalApp.Authority).ExecuteAsync(); - } - catch (MsalUiRequiredException) - { - shouldShowLoginPrompt = true; - } - } - - if (shouldShowLoginPrompt) - { - AuthenticationResult = await msalApp.AcquireTokenWithDeviceCode( - azureEnvironment.Scopes, - deviceCodeResult => - { - channel.Stdout(deviceCodeResult.Message); - return Task.FromResult(0); - }).WithAuthority(msalApp.Authority).ExecuteAsync(); - } - - if (AuthenticationResult == null) + var azureEnvironment = AzureEnvironment.Create(subscriptionId); + ActiveWorkspace = await azureEnvironment.GetAuthenticatedWorkspaceAsync(channel, resourceGroupName, workspaceName, refreshCredentials); + if (ActiveWorkspace == null) { return AzureClientError.AuthenticationFailed.ToExecutionResult(); } - var credentials = new Rest.TokenCredentials(AuthenticationResult.AccessToken); - QuantumClient = new QuantumClient(credentials) - { - SubscriptionId = subscriptionId, - ResourceGroupName = resourceGroupName, - WorkspaceName = workspaceName, - BaseUri = azureEnvironment.BaseUri, - }; - ActiveWorkspace = new Azure.Quantum.Workspace( - QuantumClient.SubscriptionId, - QuantumClient.ResourceGroupName, - QuantumClient.WorkspaceName, - AuthenticationResult?.AccessToken, - azureEnvironment.BaseUri); - - try - { - AvailableProviders = await QuantumClient.Providers.GetStatusAsync(); - } - catch (Exception e) + AvailableProviders = await ActiveWorkspace.GetProvidersAsync(); + if (AvailableProviders == null) { - Logger?.LogError(e, $"Failed to download providers list from Azure Quantum workspace: {e.Message}"); return AzureClientError.WorkspaceNotFound.ToExecutionResult(); } - channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}."); + channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}."); return ValidExecutionTargets.ToExecutionResult(); } @@ -163,12 +89,12 @@ public async Task ConnectAsync(IChannel channel, /// public async Task GetConnectionStatusAsync(IChannel channel) { - if (QuantumClient == null || AvailableProviders == null) + if (ActiveWorkspace == null || AvailableProviders == null) { return AzureClientError.NotConnected.ToExecutionResult(); } - channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}."); + channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}."); return ValidExecutionTargets.ToExecutionResult(); } @@ -194,7 +120,7 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, Az return AzureClientError.NoOperationName.ToExecutionResult(); } - var machine = QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetId, ConnectionString); + var machine = ActiveWorkspace.CreateQuantumMachine(ActiveTarget.TargetId, ConnectionString); if (machine == null) { // We should never get here, since ActiveTarget should have already been validated at the time it was set. @@ -258,7 +184,7 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, Az // handle Jupyter kernel interrupt here and break out of this loop await Task.Delay(TimeSpan.FromSeconds(submissionContext.ExecutionPollingInterval)); if (cts.IsCancellationRequested) break; - cloudJob = await GetCloudJob(MostRecentJobId); + cloudJob = await ActiveWorkspace.GetJobAsync(MostRecentJobId); channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}"); } while (cloudJob == null || cloudJob.InProgress); @@ -351,7 +277,7 @@ public async Task GetJobResultAsync(IChannel channel, string jo jobId = MostRecentJobId; } - var job = await GetCloudJob(jobId); + var job = await ActiveWorkspace.GetJobAsync(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); @@ -398,7 +324,7 @@ public async Task GetJobStatusAsync(IChannel channel, string jo jobId = MostRecentJobId; } - var job = await GetCloudJob(jobId); + var job = await ActiveWorkspace.GetJobAsync(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); @@ -417,7 +343,7 @@ public async Task GetJobListAsync(IChannel channel) return AzureClientError.NotConnected.ToExecutionResult(); } - var jobs = await GetCloudJobs(); + var jobs = await ActiveWorkspace.ListJobsAsync(); if (jobs == null || jobs.Count() == 0) { channel.Stderr("No jobs found in current Azure Quantum workspace."); @@ -426,33 +352,5 @@ public async Task GetJobListAsync(IChannel channel) return jobs.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; - } } } diff --git a/src/AzureClient/AzureEnvironment.cs b/src/AzureClient/AzureEnvironment.cs index 20db5920ae..729a56ef59 100644 --- a/src/AzureClient/AzureEnvironment.cs +++ b/src/AzureClient/AzureEnvironment.cs @@ -5,9 +5,14 @@ using System; using System.Collections.Generic; +using System.IO; using System.Linq; using System.Net; -using System.Text; +using System.Threading.Tasks; +using Microsoft.Azure.Quantum.Client; +using Microsoft.Identity.Client; +using Microsoft.Identity.Client.Extensions.Msal; +using Microsoft.Jupyter.Core; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -15,25 +20,31 @@ internal enum AzureEnvironmentType { Production, Canary, Dogfood }; internal class AzureEnvironment { - public string ClientId { get; private set; } = string.Empty; - public string Authority { get; private set; } = string.Empty; - public List Scopes { get; private set; } = new List(); - public Uri? BaseUri { get; private set; } + public AzureEnvironmentType Type { get; private set; } + + private string SubscriptionId { get; set; } = string.Empty; + private string ClientId { get; set; } = string.Empty; + private string Authority { get; set; } = string.Empty; + private List Scopes { get; set; } = new List(); + private Uri? BaseUri { get; set; } private AzureEnvironment() { } - public static AzureEnvironment Create(string environment, string subscriptionId) + public static AzureEnvironment Create(string subscriptionId) { - if (Enum.TryParse(environment, true, out AzureEnvironmentType environmentType)) + var azureEnvironmentEnvVarName = "AZURE_QUANTUM_ENV"; + var azureEnvironmentName = System.Environment.GetEnvironmentVariable(azureEnvironmentEnvVarName); + + if (Enum.TryParse(azureEnvironmentName, true, out AzureEnvironmentType environmentType)) { switch (environmentType) { case AzureEnvironmentType.Production: - return Production(); + return Production(subscriptionId); case AzureEnvironmentType.Canary: - return Canary(); + return Canary(subscriptionId); case AzureEnvironmentType.Dogfood: return Dogfood(subscriptionId); default: @@ -41,30 +52,104 @@ public static AzureEnvironment Create(string environment, string subscriptionId) } } - return Production(); + return Production(subscriptionId); + } + + public async Task GetAuthenticatedWorkspaceAsync(IChannel channel, string resourceGroupName, string workspaceName, bool refreshCredentials) + { + // Find the token cache folder + var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE"; + var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName); + if (string.IsNullOrEmpty(cacheDirectory)) + { + cacheDirectory = Path.Join(System.Environment.GetFolderPath(System.Environment.SpecialFolder.UserProfile), ".azure-quantum"); + } + + // Register the token cache for serialization + var cacheFileName = "aad.bin"; + var storageCreationProperties = new StorageCreationPropertiesBuilder(cacheFileName, cacheDirectory, ClientId).Build(); + var cacheHelper = await MsalCacheHelper.CreateAsync(storageCreationProperties); + var msalApp = PublicClientApplicationBuilder.Create(ClientId).WithAuthority(Authority).Build(); + cacheHelper.RegisterCache(msalApp.UserTokenCache); + + // Perform the authentication + bool shouldShowLoginPrompt = refreshCredentials; + AuthenticationResult? authenticationResult = null; + if (!shouldShowLoginPrompt) + { + try + { + var accounts = await msalApp.GetAccountsAsync(); + authenticationResult = await msalApp.AcquireTokenSilent( + Scopes, accounts.FirstOrDefault()).WithAuthority(msalApp.Authority).ExecuteAsync(); + } + catch (MsalUiRequiredException) + { + shouldShowLoginPrompt = true; + } + } + + if (shouldShowLoginPrompt) + { + authenticationResult = await msalApp.AcquireTokenWithDeviceCode( + Scopes, + deviceCodeResult => + { + channel.Stdout(deviceCodeResult.Message); + return Task.FromResult(0); + }).WithAuthority(msalApp.Authority).ExecuteAsync(); + } + + if (authenticationResult == null) + { + return null; + } + + // Construct and return the AzureWorkspace object + var credentials = new Rest.TokenCredentials(authenticationResult.AccessToken); + var azureQuantumClient = new QuantumClient(credentials) + { + SubscriptionId = SubscriptionId, + ResourceGroupName = resourceGroupName, + WorkspaceName = workspaceName, + BaseUri = BaseUri, + }; + var azureQuantumWorkspace = new Azure.Quantum.Workspace( + azureQuantumClient.SubscriptionId, + azureQuantumClient.ResourceGroupName, + azureQuantumClient.WorkspaceName, + authenticationResult?.AccessToken, + BaseUri); + + return new AzureWorkspace(azureQuantumClient, azureQuantumWorkspace); } - private static AzureEnvironment Production() => + private static AzureEnvironment Production(string subscriptionId) => new AzureEnvironment() { + Type = AzureEnvironmentType.Production, ClientId = "84ba0947-6c53-4dd2-9ca9-b3694761521b", // QDK client ID Authority = "https://login.microsoftonline.com/common", Scopes = new List() { "https://quantum.microsoft.com/Jobs.ReadWrite" }, BaseUri = new Uri("https://app-jobscheduler-prod.azurewebsites.net/"), + SubscriptionId = subscriptionId, }; private static AzureEnvironment Dogfood(string subscriptionId) => new AzureEnvironment() { + Type = AzureEnvironmentType.Dogfood, ClientId = "46a998aa-43d0-4281-9cbb-5709a507ac36", // QDK dogfood client ID Authority = GetDogfoodAuthority(subscriptionId), Scopes = new List() { "api://dogfood.azure-quantum/Jobs.ReadWrite" }, BaseUri = new Uri("https://app-jobscheduler-test.azurewebsites.net/"), + SubscriptionId = subscriptionId, }; - private static AzureEnvironment Canary() + private static AzureEnvironment Canary(string subscriptionId) { - var canary = Production(); + var canary = Production(subscriptionId); + canary.Type = AzureEnvironmentType.Canary; canary.BaseUri = new Uri("https://app-jobs-canarysouthcentralus.azurewebsites.net/"); return canary; } diff --git a/src/AzureClient/AzureWorkspace.cs b/src/AzureClient/AzureWorkspace.cs new file mode 100644 index 0000000000..54fb782abc --- /dev/null +++ b/src/AzureClient/AzureWorkspace.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Azure.Quantum; +using Microsoft.Azure.Quantum.Client; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Quantum.Runtime; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class AzureWorkspace : IAzureWorkspace + { + public string? Name => AzureQuantumClient?.WorkspaceName; + + private Azure.Quantum.IWorkspace AzureQuantumWorkspace { get; set; } + private QuantumClient AzureQuantumClient { get; set; } + private ILogger Logger { get; } = new LoggerFactory().CreateLogger(); + + public AzureWorkspace(QuantumClient azureQuantumClient, Azure.Quantum.Workspace azureQuantumWorkspace) + { + AzureQuantumClient = azureQuantumClient; + AzureQuantumWorkspace = azureQuantumWorkspace; + } + + public async Task?> GetProvidersAsync() + { + try + { + return await AzureQuantumClient.Providers.GetStatusAsync(); + } + catch (Exception e) + { + Logger.LogError(e, $"Failed to retrieve the providers list from the Azure Quantum workspace: {e.Message}"); + } + + return null; + } + + public async Task GetJobAsync(string jobId) + { + try + { + return await AzureQuantumWorkspace.GetJobAsync(jobId); + } + catch (Exception e) + { + Logger.LogError(e, $"Failed to retrieve the specified Azure Quantum job: {e.Message}"); + } + + return null; + } + + public async Task?> ListJobsAsync() + { + try + { + return await AzureQuantumWorkspace.ListJobsAsync(); + } + catch (Exception e) + { + Logger.LogError(e, $"Failed to retrieve the list of jobs from the Azure Quantum workspace: {e.Message}"); + } + + return null; + } + + public IQuantumMachine? CreateQuantumMachine(string targetId, string storageAccountConnectionString) + { + return QuantumMachineFactory.CreateMachine(AzureQuantumWorkspace, targetId, storageAccountConnectionString); + } + } +} diff --git a/src/AzureClient/IAzureWorkspace.cs b/src/AzureClient/IAzureWorkspace.cs new file mode 100644 index 0000000000..a08152b3b7 --- /dev/null +++ b/src/AzureClient/IAzureWorkspace.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System.Collections.Generic; +using System.Threading.Tasks; +using Microsoft.Azure.Quantum; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Quantum.Runtime; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal interface IAzureWorkspace + { + public string Name { get; } + + public Task> GetProvidersAsync(); + public Task GetJobAsync(string jobId); + public Task> ListJobsAsync(); + public IQuantumMachine? CreateQuantumMachine(string targetId, string storageAccountConnectionString); + } +} \ No newline at end of file From 4e70a76c620e97bd84955d5c913815b4abb733cc Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 12 Jun 2020 17:47:48 -0400 Subject: [PATCH 27/44] Mock AzureEnvironment and end-to-end tests --- src/AzureClient/AzureClient.cs | 25 +- src/AzureClient/AzureClientError.cs | 93 +++++++ src/AzureClient/AzureEnvironment.cs | 19 +- src/AzureClient/AzureExecutionTarget.cs | 4 +- src/AzureClient/AzureSubmissionContext.cs | 2 +- src/AzureClient/AzureWorkspace.cs | 2 +- src/AzureClient/Extensions.cs | 19 +- src/AzureClient/IAzureClient.cs | 86 +----- src/AzureClient/IAzureWorkspace.cs | 10 +- src/AzureClient/Magic/AzureClientMagicBase.cs | 2 +- src/AzureClient/Magic/ConnectMagic.cs | 2 +- src/AzureClient/Magic/ExecuteMagic.cs | 2 +- src/AzureClient/Magic/JobsMagic.cs | 2 +- src/AzureClient/Magic/OutputMagic.cs | 2 +- src/AzureClient/Magic/StatusMagic.cs | 2 +- src/AzureClient/Magic/SubmitMagic.cs | 2 +- src/AzureClient/Magic/TargetMagic.cs | 2 +- src/AzureClient/Mocks/MockAzureWorkspace.cs | 63 +++++ src/AzureClient/Mocks/MockCloudJob.cs | 38 +++ src/AzureClient/Mocks/MockQuantumMachine.cs | 61 ++++ .../Visualization/AzureClientErrorEncoders.cs | 64 +++++ .../Visualization/CloudJobEncoders.cs | 8 +- .../Visualization/HistogramEncoders.cs | 2 +- .../Visualization/JsonConverters.cs | 5 +- .../Visualization/TargetStatusEncoders.cs | 6 +- src/Python/qsharp/azure.py | 99 +++++-- src/Python/qsharp/tests/test_azure.py | 102 +++++++ src/Tests/AzureClientEntryPointTests.cs | 59 ---- src/Tests/AzureClientTests.cs | 263 +++++++++++++++--- 29 files changed, 792 insertions(+), 254 deletions(-) create mode 100644 src/AzureClient/AzureClientError.cs create mode 100644 src/AzureClient/Mocks/MockAzureWorkspace.cs create mode 100644 src/AzureClient/Mocks/MockCloudJob.cs create mode 100644 src/AzureClient/Mocks/MockQuantumMachine.cs create mode 100644 src/AzureClient/Visualization/AzureClientErrorEncoders.cs create mode 100644 src/Python/qsharp/tests/test_azure.py diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index c03ad4a61f..1622cb0ccd 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -20,12 +20,12 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class AzureClient : IAzureClient { + internal IAzureWorkspace? ActiveWorkspace { get; private set; } private ILogger Logger { get; } private IReferences References { get; } private IEntryPointGenerator EntryPointGenerator { get; } private string ConnectionString { get; set; } = string.Empty; private AzureExecutionTarget? ActiveTarget { get; set; } - private IAzureWorkspace? ActiveWorkspace { get; set; } private string MostRecentJobId { get; set; } = string.Empty; private IEnumerable? AvailableProviders { get; set; } private IEnumerable? AvailableTargets => AvailableProviders?.SelectMany(provider => provider.Targets); @@ -55,6 +55,8 @@ public AzureClient( baseEngine.RegisterDisplayEncoder(new TargetStatusToTextEncoder()); baseEngine.RegisterDisplayEncoder(new HistogramToHtmlEncoder()); baseEngine.RegisterDisplayEncoder(new HistogramToTextEncoder()); + baseEngine.RegisterDisplayEncoder(new AzureClientErrorToHtmlEncoder()); + baseEngine.RegisterDisplayEncoder(new AzureClientErrorToTextEncoder()); } } @@ -83,6 +85,11 @@ public async Task ConnectAsync(IChannel channel, channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}."); + if (ValidExecutionTargets.Count() == 0) + { + channel.Stderr($"No valid Q# execution targets found in Azure Quantum workspace {ActiveWorkspace.Name}."); + } + return ValidExecutionTargets.ToExecutionResult(); } @@ -219,7 +226,8 @@ public async Task GetActiveTargetAsync(IChannel channel) channel.Stdout($"Current execution target: {ActiveTarget.TargetId}"); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); - return ActiveTarget.TargetId.ToExecutionResult(); + + return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult(); } /// @@ -254,7 +262,9 @@ public async Task SetActiveTargetAsync(IChannel channel, string channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies..."); await References.AddPackage(ActiveTarget.PackageName); - return $"Active target is now {ActiveTarget.TargetId}".ToExecutionResult(); + channel.Stdout($"Active target is now {ActiveTarget.TargetId}"); + + return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult(); } /// @@ -343,13 +353,12 @@ public async Task GetJobListAsync(IChannel channel) return AzureClientError.NotConnected.ToExecutionResult(); } - var jobs = await ActiveWorkspace.ListJobsAsync(); - if (jobs == null || jobs.Count() == 0) + var jobs = await ActiveWorkspace.ListJobsAsync() ?? new List(); + if (jobs.Count() == 0) { channel.Stderr("No jobs found in current Azure Quantum workspace."); - return AzureClientError.JobNotFound.ToExecutionResult(); } - + return jobs.ToExecutionResult(); } } diff --git a/src/AzureClient/AzureClientError.cs b/src/AzureClient/AzureClientError.cs new file mode 100644 index 0000000000..a5ac286870 --- /dev/null +++ b/src/AzureClient/AzureClientError.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.ComponentModel; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// Describes possible error results from methods. + /// + public enum AzureClientError + { + /// + /// Method completed with an unknown error. + /// + [Description(Resources.AzureClientErrorUnknownError)] + UnknownError = 1000, + + /// + /// No connection has been made to any Azure Quantum workspace. + /// + [Description(Resources.AzureClientErrorNotConnected)] + NotConnected, + + /// + /// A target has not yet been configured for job submission. + /// + [Description(Resources.AzureClientErrorNoTarget)] + 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, + + /// + /// The result of a job was requested, but the job has not yet completed. + /// + [Description(Resources.AzureClientErrorJobNotCompleted)] + JobNotCompleted, + + /// + /// The job output failed to be downloaded from the Azure storage location. + /// + [Description(Resources.AzureClientErrorJobOutputDownloadFailed)] + JobOutputDownloadFailed, + + /// + /// No Q# operation name was provided where one was required. + /// + [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. + /// + [Description(Resources.AzureClientErrorAuthenticationFailed)] + AuthenticationFailed, + + /// + /// A workspace meeting the specified criteria was not found. + /// + [Description(Resources.AzureClientErrorWorkspaceNotFound)] + WorkspaceNotFound, + } +} diff --git a/src/AzureClient/AzureEnvironment.cs b/src/AzureClient/AzureEnvironment.cs index 729a56ef59..66dc1de62d 100644 --- a/src/AzureClient/AzureEnvironment.cs +++ b/src/AzureClient/AzureEnvironment.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -16,10 +16,11 @@ namespace Microsoft.Quantum.IQSharp.AzureClient { - internal enum AzureEnvironmentType { Production, Canary, Dogfood }; + internal enum AzureEnvironmentType { Production, Canary, Dogfood, Mock }; internal class AzureEnvironment { + public static string EnvironmentVariableName => "AZURE_QUANTUM_ENV"; public AzureEnvironmentType Type { get; private set; } private string SubscriptionId { get; set; } = string.Empty; @@ -34,8 +35,7 @@ private AzureEnvironment() public static AzureEnvironment Create(string subscriptionId) { - var azureEnvironmentEnvVarName = "AZURE_QUANTUM_ENV"; - var azureEnvironmentName = System.Environment.GetEnvironmentVariable(azureEnvironmentEnvVarName); + var azureEnvironmentName = System.Environment.GetEnvironmentVariable(EnvironmentVariableName); if (Enum.TryParse(azureEnvironmentName, true, out AzureEnvironmentType environmentType)) { @@ -47,6 +47,8 @@ public static AzureEnvironment Create(string subscriptionId) return Canary(subscriptionId); case AzureEnvironmentType.Dogfood: return Dogfood(subscriptionId); + case AzureEnvironmentType.Mock: + return Mock(); default: throw new InvalidOperationException("Unexpected EnvironmentType value."); } @@ -57,6 +59,12 @@ public static AzureEnvironment Create(string subscriptionId) public async Task GetAuthenticatedWorkspaceAsync(IChannel channel, string resourceGroupName, string workspaceName, bool refreshCredentials) { + if (Type == AzureEnvironmentType.Mock) + { + channel.Stdout("AZURE_QUANTUM_ENV set to Mock. Using mock Azure workspace rather than connecting to the real service."); + return new MockAzureWorkspace(workspaceName); + } + // Find the token cache folder var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE"; var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName); @@ -154,6 +162,9 @@ private static AzureEnvironment Canary(string subscriptionId) return canary; } + private static AzureEnvironment Mock() => + new AzureEnvironment() { Type = AzureEnvironmentType.Mock }; + private static string GetDogfoodAuthority(string subscriptionId) { try diff --git a/src/AzureClient/AzureExecutionTarget.cs b/src/AzureClient/AzureExecutionTarget.cs index d56064efa5..f2cebf24f0 100644 --- a/src/AzureClient/AzureExecutionTarget.cs +++ b/src/AzureClient/AzureExecutionTarget.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -11,7 +11,7 @@ internal enum AzureProvider { IonQ, Honeywell, QCI } internal class AzureExecutionTarget { - public string TargetId { get; private set; } + public string TargetId { get; private set; } = string.Empty; public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetId)}"; public static bool IsValid(string targetId) => GetProvider(targetId) != null; diff --git a/src/AzureClient/AzureSubmissionContext.cs b/src/AzureClient/AzureSubmissionContext.cs index bfe023e9fc..f299d90694 100644 --- a/src/AzureClient/AzureSubmissionContext.cs +++ b/src/AzureClient/AzureSubmissionContext.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/AzureWorkspace.cs b/src/AzureClient/AzureWorkspace.cs index 54fb782abc..a5083c442b 100644 --- a/src/AzureClient/AzureWorkspace.cs +++ b/src/AzureClient/AzureWorkspace.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/Extensions.cs b/src/AzureClient/Extensions.cs index 1fda56014e..6e4cc96c56 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -37,24 +37,9 @@ internal static ExecutionResult ToExecutionResult(this AzureClientError azureCli new ExecutionResult { Status = ExecuteStatus.Error, - Output = azureClientError.ToDescription() + Output = azureClientError, }; - /// - /// Returns the string value of the for the given - /// enumeration value. - /// - /// - /// - internal static string ToDescription(this AzureClientError azureClientError) - { - var attributes = azureClientError - .GetType() - .GetField(azureClientError.ToString()) - .GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[]; - return attributes?.Length > 0 ? attributes[0].Description : string.Empty; - } - /// /// Encapsulates a given as the result of an execution. /// diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index d6c67678ff..441abc7bff 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -10,90 +10,6 @@ namespace Microsoft.Quantum.IQSharp.AzureClient { - /// - /// Describes possible error results from methods. - /// - public enum AzureClientError - { - /// - /// Method completed with an unknown error. - /// - [Description(Resources.AzureClientErrorUnknownError)] - UnknownError, - - /// - /// No connection has been made to any Azure Quantum workspace. - /// - [Description(Resources.AzureClientErrorNotConnected)] - NotConnected, - - /// - /// A target has not yet been configured for job submission. - /// - [Description(Resources.AzureClientErrorNoTarget)] - 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, - - /// - /// The result of a job was requested, but the job has not yet completed. - /// - [Description(Resources.AzureClientErrorJobNotCompleted)] - JobNotCompleted, - - /// - /// The job output failed to be downloaded from the Azure storage location. - /// - [Description(Resources.AzureClientErrorJobOutputDownloadFailed)] - JobOutputDownloadFailed, - - /// - /// No Q# operation name was provided where one was required. - /// - [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. - /// - [Description(Resources.AzureClientErrorAuthenticationFailed)] - AuthenticationFailed, - - /// - /// A workspace meeting the specified criteria was not found. - /// - [Description(Resources.AzureClientErrorWorkspaceNotFound)] - WorkspaceNotFound, - } - /// /// This service is capable of connecting to Azure Quantum workspaces /// and submitting jobs. diff --git a/src/AzureClient/IAzureWorkspace.cs b/src/AzureClient/IAzureWorkspace.cs index a08152b3b7..1c2c71b813 100644 --- a/src/AzureClient/IAzureWorkspace.cs +++ b/src/AzureClient/IAzureWorkspace.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -13,11 +13,11 @@ namespace Microsoft.Quantum.IQSharp.AzureClient { internal interface IAzureWorkspace { - public string Name { get; } + public string? Name { get; } - public Task> GetProvidersAsync(); - public Task GetJobAsync(string jobId); - public Task> ListJobsAsync(); + public Task?> GetProvidersAsync(); + public Task GetJobAsync(string jobId); + public Task?> ListJobsAsync(); public IQuantumMachine? CreateQuantumMachine(string targetId, string storageAccountConnectionString); } } \ No newline at end of file diff --git a/src/AzureClient/Magic/AzureClientMagicBase.cs b/src/AzureClient/Magic/AzureClientMagicBase.cs index f7eac3b5ce..823554c48f 100644 --- a/src/AzureClient/Magic/AzureClientMagicBase.cs +++ b/src/AzureClient/Magic/AzureClientMagicBase.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/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 559c04e3e7..ffdceff925 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.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/Magic/ExecuteMagic.cs b/src/AzureClient/Magic/ExecuteMagic.cs index 6a3080d274..122882abe5 100644 --- a/src/AzureClient/Magic/ExecuteMagic.cs +++ b/src/AzureClient/Magic/ExecuteMagic.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/Magic/JobsMagic.cs b/src/AzureClient/Magic/JobsMagic.cs index f23708d9ea..590e77b418 100644 --- a/src/AzureClient/Magic/JobsMagic.cs +++ b/src/AzureClient/Magic/JobsMagic.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/Magic/OutputMagic.cs b/src/AzureClient/Magic/OutputMagic.cs index 17ca257a46..dd542d7329 100644 --- a/src/AzureClient/Magic/OutputMagic.cs +++ b/src/AzureClient/Magic/OutputMagic.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/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs index e7eaa56d6c..80a2a85733 100644 --- a/src/AzureClient/Magic/StatusMagic.cs +++ b/src/AzureClient/Magic/StatusMagic.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/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index 516646382e..3c7bcfccdd 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.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/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 778f88ab74..d0c93be7e1 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.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/Mocks/MockAzureWorkspace.cs b/src/AzureClient/Mocks/MockAzureWorkspace.cs new file mode 100644 index 0000000000..212d605714 --- /dev/null +++ b/src/AzureClient/Mocks/MockAzureWorkspace.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.Quantum; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Quantum.Runtime; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class MockAzureWorkspace : IAzureWorkspace + { + public const string NameWithMockProviders = "WorkspaceNameWithMockProviders"; + + public string Name { get; private set; } + + public List Providers { get; } = new List(); + + public List Jobs { get; } = new List(); + + public MockAzureWorkspace(string workspaceName) + { + Name = workspaceName; + if (Name == NameWithMockProviders) + { + // add a mock target for each provider: "ionq.mock", "honeywell.mock", etc. + AddMockTargets( + Enum.GetNames(typeof(AzureProvider)) + .Select(provider => $"{provider.ToLowerInvariant()}.mock") + .ToArray()); + } + } + + public async Task GetJobAsync(string jobId) => Jobs.FirstOrDefault(job => job.Id == jobId); + + public async Task?> GetProvidersAsync() => Providers; + + public async Task?> ListJobsAsync() => Jobs; + + public IQuantumMachine? CreateQuantumMachine(string targetId, string storageAccountConnectionString) => new MockQuantumMachine(this); + + public void AddMockJobs(params string[] jobIds) + { + foreach (var jobId in jobIds) + { + var mockJob = new MockCloudJob(); + mockJob.Details.Id = jobId; + Jobs.Add(mockJob); + } + } + + public void AddMockTargets(params string[] targetIds) + { + var targets = targetIds.Select(id => new TargetStatus(id)).ToList(); + Providers.Add(new ProviderStatus(null, null, targets)); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Mocks/MockCloudJob.cs b/src/AzureClient/Mocks/MockCloudJob.cs new file mode 100644 index 0000000000..73d6cc789d --- /dev/null +++ b/src/AzureClient/Mocks/MockCloudJob.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.Azure.Quantum; +using Microsoft.Azure.Quantum.Client.Models; +using System; +using System.IO; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class MockCloudJob : CloudJob + { + public MockCloudJob() + : base( + new Azure.Quantum.Workspace("mockSubscriptionId", "mockResourceGroupName", "mockWorkspaceName"), + new JobDetails( + containerUri: null, + inputDataFormat: null, + providerId: null, + target: null, + id: Guid.NewGuid().ToString(), + status: "Succeeded", + outputDataUri: CreateMockOutputFileUri() + )) + { + } + + private static string CreateMockOutputFileUri() + { + var tempFilePath = Path.GetTempFileName(); + using var outputFile = new StreamWriter(tempFilePath); + outputFile.WriteLine(@"{'Histogram':['0',0.5,'1',0.5]}"); + return new Uri(tempFilePath).AbsoluteUri; + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Mocks/MockQuantumMachine.cs b/src/AzureClient/Mocks/MockQuantumMachine.cs new file mode 100644 index 0000000000..9e6c28411f --- /dev/null +++ b/src/AzureClient/Mocks/MockQuantumMachine.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Quantum.Runtime; +using Microsoft.Quantum.Simulation.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class MockQuantumMachine : IQuantumMachine + { + public string ProviderId => throw new NotImplementedException(); + + public string Target => throw new NotImplementedException(); + + private MockAzureWorkspace? Workspace { get; } + + public MockQuantumMachine(MockAzureWorkspace? workspace = null) => Workspace = workspace; + + 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, executionContext, null); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext? executionContext, IQuantumMachine.ConfigureJob? configureJobCallback) + => ExecuteAsync(info, input, null, executionContext); + + 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, submissionContext, null); + + public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext? submissionContext, IQuantumMachine.ConfigureJob? configureJobCallback) + { + var job = new MockCloudJob(); + Workspace?.AddMockJobs(job.Id); + return Task.FromResult(job as IQuantumMachineJob); + } + + public (bool IsValid, string Message) Validate(EntryPointInfo info, TInput input) + => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/AzureClient/Visualization/AzureClientErrorEncoders.cs b/src/AzureClient/Visualization/AzureClientErrorEncoders.cs new file mode 100644 index 0000000000..4848d7a8a3 --- /dev/null +++ b/src/AzureClient/Visualization/AzureClientErrorEncoders.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.Jupyter.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal static class AzureClientErrorExtensions + { + /// + /// Returns the string value of the for the given + /// enumeration value. + /// + internal static string ToDescription(this AzureClientError error) + { + var attributes = error + .GetType() + .GetField(error.ToString()) + .GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[]; + return attributes?.Length > 0 ? attributes[0].Description : string.Empty; + } + + /// + /// Returns a dictionary representing the properties of the . + /// + internal static Dictionary ToDictionary(this AzureClientError error) => + new Dictionary() + { + ["error_code"] = System.Convert.ToInt32(error), + ["error_name"] = error.ToString(), + ["error_description"] = error.ToDescription(), + }; + } + + public class AzureClientErrorToHtmlEncoder : IResultEncoder + { + public string MimeType => MimeTypes.Html; + + public EncodedData? Encode(object displayable) => (displayable as AzureClientError?)?.ToDescription().ToEncodedData(); + } + + public class AzureClientErrorToTextEncoder : IResultEncoder + { + public string MimeType => MimeTypes.PlainText; + + public EncodedData? Encode(object displayable) => (displayable as AzureClientError?)?.ToDescription().ToEncodedData(); + } + + public class AzureClientErrorJsonConverter : JsonConverter + { + public override AzureClientError ReadJson(JsonReader reader, Type objectType, AzureClientError existingValue, bool hasExistingValue, JsonSerializer serializer) => + throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, AzureClientError value, JsonSerializer serializer) => + JToken.FromObject(value.ToDictionary()).WriteTo(writer); + } +} diff --git a/src/AzureClient/Visualization/CloudJobEncoders.cs b/src/AzureClient/Visualization/CloudJobEncoders.cs index 8e5f9b5610..d4b20ced6f 100644 --- a/src/AzureClient/Visualization/CloudJobEncoders.cs +++ b/src/AzureClient/Visualization/CloudJobEncoders.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -31,9 +31,9 @@ internal static Dictionary ToDictionary(this CloudJob cloudJob) ["status"] = cloudJob.Status, ["provider"] = cloudJob.Details.ProviderId, ["target"] = cloudJob.Details.Target, - ["creationTime"] = cloudJob.Details.CreationTime.ToDateTime()?.ToUniversalTime(), - ["beginExecutionTime"] = cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToUniversalTime(), - ["endExecutionTime"] = cloudJob.Details.EndExecutionTime.ToDateTime()?.ToUniversalTime(), + ["creation_time"] = cloudJob.Details.CreationTime.ToDateTime()?.ToUniversalTime(), + ["begin_execution_time"] = cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToUniversalTime(), + ["end_execution_time"] = cloudJob.Details.EndExecutionTime.ToDateTime()?.ToUniversalTime(), }; internal static Table ToJupyterTable(this IEnumerable jobsList) => diff --git a/src/AzureClient/Visualization/HistogramEncoders.cs b/src/AzureClient/Visualization/HistogramEncoders.cs index befbbf095b..41f3f0698b 100644 --- a/src/AzureClient/Visualization/HistogramEncoders.cs +++ b/src/AzureClient/Visualization/HistogramEncoders.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/Visualization/JsonConverters.cs b/src/AzureClient/Visualization/JsonConverters.cs index 24f26f2515..394370155a 100644 --- a/src/AzureClient/Visualization/JsonConverters.cs +++ b/src/AzureClient/Visualization/JsonConverters.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -19,7 +19,8 @@ public static class JsonConverters new CloudJobJsonConverter(), new CloudJobListJsonConverter(), new TargetStatusJsonConverter(), - new TargetStatusListJsonConverter() + new TargetStatusListJsonConverter(), + new AzureClientErrorJsonConverter() ); public static JsonConverter[] AllConverters => allConverters.ToArray(); diff --git a/src/AzureClient/Visualization/TargetStatusEncoders.cs b/src/AzureClient/Visualization/TargetStatusEncoders.cs index 1de24e7bc0..6925163fcb 100644 --- a/src/AzureClient/Visualization/TargetStatusEncoders.cs +++ b/src/AzureClient/Visualization/TargetStatusEncoders.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -20,8 +20,8 @@ internal static Dictionary ToDictionary(this TargetStatus target new Dictionary() { ["id"] = target.Id, - ["currentAvailability"] = target.CurrentAvailability, - ["averageQueueTime"] = target.AverageQueueTime, + ["current_availability"] = target.CurrentAvailability, + ["average_queue_time"] = target.AverageQueueTime, }; internal static Table ToJupyterTable(this IEnumerable targets) => diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index f69fb83a00..e9c9abe633 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -11,11 +11,7 @@ import qsharp import json -import typing -from typing import List, Dict, Callable, Any - -from qsharp.serialization import map_tuples -from typing import List, Tuple, Dict, Iterable +from typing import List, Dict, Callable, Any, Union from enum import Enum ## LOGGING ## @@ -33,27 +29,92 @@ 'status', 'output', 'jobs' + 'AzureTarget', + 'AzureJob', + 'AzureError' ] +## CLASSES ## + +class AzureTarget(object): + """ + Represents an instance of an Azure Quantum execution target for Q# job submission. + """ + def __init__(self, data: Dict): + self.__dict__ = data + self.id = data["id"] + self.current_availability = data["current_availability"] + self.average_queue_time = data["average_queue_time"] + + def __eq__(self, other): + if not isinstance(other, AzureTarget): + # don't attempt to compare against unrelated types + return NotImplemented + return self.__dict__ == other.__dict__ + +class AzureJob(object): + """ + Represents an instance of an Azure Quantum job. + """ + def __init__(self, data: Dict): + self.__dict__ = data + self.id = data["id"] + self.name = data["name"] + self.status = data["status"] + self.provider = data["provider"] + self.target = data["target"] + self.creation_time = data["creation_time"] + self.begin_execution_time = data["begin_execution_time"] + self.end_execution_time = data["end_execution_time"] + + def __eq__(self, other): + if not isinstance(other, AzureJob): + # don't attempt to compare against unrelated types + return NotImplemented + return self.__dict__ == other.__dict__ + +class AzureError(object): + """ + Contains error information resulting from an attempt to interact with Azure. + """ + def __init__(self, data: Dict): + self.__dict__ = data + self.error_code = data["error_code"] + self.error_name = data["error_name"] + self.error_description = data["error_description"] + + def __eq__(self, other): + if not isinstance(other, AzureError): + # don't attempt to compare against unrelated types + return NotImplemented + return self.__dict__ == other.__dict__ + ## FUNCTIONS ## -def connect(**params) -> Any: - return qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params) +def connect(**params) -> Union[List[AzureTarget], AzureError]: + result = qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params) + return AzureError(result) if "error_code" in result else [AzureTarget(target) for target in result] -def target(name : str = '', **params) -> Any: - return qsharp.client._execute_magic(f"azure.target {name}", raise_on_stderr=False, **params) +def target(name : str = '', **params) -> Union[AzureTarget, AzureError]: + result = qsharp.client._execute_magic(f"azure.target {name}", raise_on_stderr=False, **params) + return AzureError(result) if "error_code" in result else AzureTarget(result) -def submit(op, **params) -> Any: - return qsharp.client._execute_callable_magic("azure.submit", op, raise_on_stderr=False, **params) +def submit(op, **params) -> Union[AzureJob, AzureError]: + result = qsharp.client._execute_callable_magic("azure.submit", op, raise_on_stderr=False, **params) + return AzureError(result) if "error_code" in result else AzureJob(result) -def execute(op, **params) -> Any: - return qsharp.client._execute_callable_magic("azure.execute", op, raise_on_stderr=False, **params) +def execute(op, **params) -> Union[Dict, AzureError]: + result = qsharp.client._execute_callable_magic("azure.execute", op, raise_on_stderr=False, **params) + return AzureError(result) if "error_code" in result else result -def status(jobId : str = '', **params) -> Any: - return qsharp.client._execute_magic(f"azure.status {jobId}", raise_on_stderr=False, **params) +def status(jobId : str = '', **params) -> Union[AzureJob, AzureError]: + result = qsharp.client._execute_magic(f"azure.status {jobId}", raise_on_stderr=False, **params) + return AzureError(result) if "error_code" in result else AzureJob(result) -def output(jobId : str = '', **params) -> Any: - return qsharp.client._execute_magic(f"azure.output {jobId}", raise_on_stderr=False, **params) +def output(jobId : str = '', **params) -> Union[Dict, AzureError]: + result = qsharp.client._execute_magic(f"azure.output {jobId}", raise_on_stderr=False, **params) + return AzureError(result) if "error_code" in result else result -def jobs(**params) -> Any: - return qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params) +def jobs(**params) -> Union[List[AzureJob], AzureError]: + result = qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params) + return AzureError(result) if "error_code" in result else [AzureJob(job) for job in result] diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py new file mode 100644 index 0000000000..4e195e4f88 --- /dev/null +++ b/src/Python/qsharp/tests/test_azure.py @@ -0,0 +1,102 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +## +# test_azure.py: Tests Azure Quantum functionality against a mock workspace. +## +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +## + +## IMPORTS ## + +import importlib +import os +import pytest +import qsharp +from qsharp.azure import AzureError, AzureJob, AzureTarget +import sys + +## SETUP ## + +@pytest.fixture(scope="session", autouse=True) +def set_environment_variables(): + # Need to restart the IQ# kernel after setting the environment variable + os.environ["AZURE_QUANTUM_ENV"] = "mock" + importlib.reload(qsharp) + if "qsharp.chemistry" in sys.modules: + importlib.reload(qsharp.chemistry) + +## TESTS ## + +def test_empty_workspace(monkeypatch): + """ + Tests behavior of a mock workspace with no providers. + """ + targets = qsharp.azure.connect( + storageAccountConnectionString="test", + subscriptionId="test", + resourceGroupName="test", + workspaceName="test" + ) + assert targets == [] + + result = qsharp.azure.target("invalid.target") + assert isinstance(result, AzureError) + + jobs = qsharp.azure.jobs() + assert jobs == [] + +def test_workspace_with_providers(): + """ + Tests behavior of a mock workspace with mock providers. + """ + result = qsharp.azure.target() + assert isinstance(result, AzureError) + + targets = qsharp.azure.connect( + storageAccountConnectionString="test", + subscriptionId="test", + resourceGroupName="test", + workspaceName="WorkspaceNameWithMockProviders" + ) + assert isinstance(targets, list) + assert len(targets) > 0 + + for target in targets: + active_target = qsharp.azure.target(target.id) + assert isinstance(active_target, AzureTarget) + assert active_target == target + + # Submit a snippet operation without parameters + op = qsharp.compile(""" + operation HelloQ() : Result + { + Message($"Hello from quantum world!"); + return Zero; + } + """) + + job = qsharp.azure.submit(op) + assert isinstance(job, AzureJob) + + retrieved_job = qsharp.azure.status(job.id) + assert isinstance(retrieved_job, AzureJob) + assert job.id == retrieved_job.id + + # Execute a workspace operation with parameters + op = qsharp.QSharpCallable("Microsoft.Quantum.SanityTests.HelloAgain", None) + + result = qsharp.azure.execute(op) # missing parameters + assert isinstance(result, AzureError) + + histogram = qsharp.azure.execute(op, count=3, name="test") + assert isinstance(histogram, dict) + + retrieved_histogram = qsharp.azure.output() + assert isinstance(retrieved_histogram, dict) + assert histogram == retrieved_histogram + + # Check that both submitted jobs exist in the workspace + jobs = qsharp.azure.jobs() + assert isinstance(jobs, list) + assert len(jobs) == 2 diff --git a/src/Tests/AzureClientEntryPointTests.cs b/src/Tests/AzureClientEntryPointTests.cs index 48d0416c61..a4a121c994 100644 --- a/src/Tests/AzureClientEntryPointTests.cs +++ b/src/Tests/AzureClientEntryPointTests.cs @@ -132,63 +132,4 @@ public async Task InvalidEntryPointOperation() 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, executionContext, null); - - public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext executionContext, IQuantumMachine.ConfigureJob configureJobCallback) - => ExecuteAsync(info, input, null, executionContext); - - 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, submissionContext, null); - - public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachine.ConfigureJob configureJobCallback) - => 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(); - } } diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index a115085104..099c251268 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -3,47 +3,84 @@ #nullable enable -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.Quantum; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Jupyter.Core; -using Microsoft.Quantum.IQSharp; using Microsoft.Quantum.IQSharp.AzureClient; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Tests.IQSharp { - public static class AzureClientTestExtensions - { - } - [TestClass] public class AzureClientTests { - private readonly string subscriptionId = "TEST_SUBSCRIPTION_ID"; - private readonly string resourceGroupName = "TEST_RESOURCE_GROUP_NAME"; - private readonly string workspaceName = "TEST_WORKSPACE_NAME"; - private readonly string storageAccountConnectionString = "TEST_CONNECTION_STRING"; - private readonly string jobId = "TEST_JOB_ID"; - private readonly string operationName = "TEST_OPERATION_NAME"; + private string originalEnvironmentName = string.Empty; + + [TestInitialize] + public void SetMockEnvironment() + { + originalEnvironmentName = Environment.GetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName) ?? string.Empty; + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Mock.ToString()); + } + + [TestCleanup] + public void RestoreEnvironment() + { + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, originalEnvironmentName); + } + + private T ExpectSuccess(Task task) + { + var result = task.GetAwaiter().GetResult(); + Assert.AreEqual(ExecuteStatus.Ok, result.Status); + Assert.IsInstanceOfType(result.Output, typeof(T)); + return (T)result.Output; + } + + private void ExpectError(AzureClientError expectedError, Task task) + { + var result = task.GetAwaiter().GetResult(); + Assert.AreEqual(ExecuteStatus.Error, result.Status); + Assert.IsInstanceOfType(result.Output, typeof(AzureClientError)); + Assert.AreEqual(expectedError, (AzureClientError)result.Output); + } + + private Task ConnectToWorkspaceAsync(IAzureClient azureClient, string workspaceName = "TEST_WORKSPACE_NAME") + { + return azureClient.ConnectAsync( + new MockChannel(), + "TEST_SUBSCRIPTION_ID", + "TEST_RESOURCE_GROUP_NAME", + workspaceName, + "TEST_CONNECTION_STRING"); + } [TestMethod] - public void TestTargets() + public void TestAzureEnvironment() { - var workspace = "Workspace"; - var services = Startup.CreateServiceProvider(workspace); - var azureClient = services.GetService(); + // Production environment + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Production.ToString()); + var environment = AzureEnvironment.Create("TEST_SUBSCRIPTION_ID"); + Assert.AreEqual(AzureEnvironmentType.Production, environment.Type); - // SetActiveTargetAsync with recognized target ID, but not yet connected - var result = azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator").GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == ExecuteStatus.Error); + // Dogfood environment cannot be created in test because it requires a service call + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Dogfood.ToString()); + Assert.ThrowsException(() => AzureEnvironment.Create("TEST_SUBSCRIPTION_ID")); - // SetActiveTargetAsync with unrecognized target ID - result = azureClient.SetActiveTargetAsync(new MockChannel(), "contoso.qpu").GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == ExecuteStatus.Error); + // Canary environment + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Canary.ToString()); + environment = AzureEnvironment.Create("TEST_SUBSCRIPTION_ID"); + Assert.AreEqual(AzureEnvironmentType.Canary, environment.Type); - // GetActiveTargetAsync, but not yet connected - result = azureClient.GetActiveTargetAsync(new MockChannel()).GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == ExecuteStatus.Error); + // Mock environment + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Mock.ToString()); + environment = AzureEnvironment.Create("TEST_SUBSCRIPTION_ID"); + Assert.AreEqual(AzureEnvironmentType.Mock, environment.Type); } [TestMethod] @@ -55,21 +92,177 @@ public void TestAzureExecutionTarget() targetId = "ionq.targetId"; executionTarget = AzureExecutionTarget.Create(targetId); - Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetId, targetId); - Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.IonQ"); + Assert.AreEqual(targetId, executionTarget?.TargetId); + Assert.AreEqual("Microsoft.Quantum.Providers.IonQ", executionTarget?.PackageName); targetId = "HonEYWEll.targetId"; executionTarget = AzureExecutionTarget.Create(targetId); - Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetId, targetId); - Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.Honeywell"); + Assert.AreEqual(targetId, executionTarget?.TargetId); + Assert.AreEqual("Microsoft.Quantum.Providers.Honeywell", executionTarget?.PackageName); targetId = "qci.target.name.qpu"; executionTarget = AzureExecutionTarget.Create(targetId); - Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetId, targetId); - Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.QCI"); + Assert.AreEqual(targetId, executionTarget?.TargetId); + Assert.AreEqual("Microsoft.Quantum.Providers.QCI", executionTarget?.PackageName); + } + + [TestMethod] + public void TestJobStatus() + { + var services = Startup.CreateServiceProvider("Workspace"); + var azureClient = (AzureClient)services.GetService(); + + // not connected + ExpectError(AzureClientError.NotConnected, azureClient.GetJobStatusAsync(new MockChannel(), "JOB_ID_1")); + + // connect + var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient)); + Assert.IsFalse(targets.Any()); + + // set up the mock workspace + var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace; + Assert.IsNotNull(azureWorkspace); + azureWorkspace?.AddMockJobs("JOB_ID_1", "JOB_ID_2"); + + // valid job ID + var job = ExpectSuccess(azureClient.GetJobStatusAsync(new MockChannel(), "JOB_ID_1")); + Assert.AreEqual("JOB_ID_1", job.Id); + + // invalid job ID + ExpectError(AzureClientError.JobNotFound, azureClient.GetJobStatusAsync(new MockChannel(), "JOB_ID_3")); + + // jobs list + var jobs = ExpectSuccess>(azureClient.GetJobListAsync(new MockChannel())); + Assert.AreEqual(2, jobs.Count()); + } + + [TestMethod] + public void TestManualTargets() + { + var services = Startup.CreateServiceProvider("Workspace"); + var azureClient = (AzureClient)services.GetService(); + + // SetActiveTargetAsync with recognized target ID, but not yet connected + ExpectError(AzureClientError.NotConnected, azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator")); + + // GetActiveTargetAsync, but not yet connected + ExpectError(AzureClientError.NotConnected, azureClient.GetActiveTargetAsync(new MockChannel())); + + // connect + var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient)); + Assert.IsFalse(targets.Any()); + + // set up the mock workspace + var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace; + Assert.IsNotNull(azureWorkspace); + azureWorkspace?.AddMockTargets("ionq.simulator", "honeywell.qpu", "unrecognized.target"); + + // get connection status to verify list of targets + targets = ExpectSuccess>(azureClient.GetConnectionStatusAsync(new MockChannel())); + Assert.AreEqual(2, targets.Count()); // only 2 valid quantum execution targets + + // GetActiveTargetAsync, but no active target set yet + ExpectError(AzureClientError.NoTarget, azureClient.GetActiveTargetAsync(new MockChannel())); + + // SetActiveTargetAsync with target ID not valid for quantum execution + ExpectError(AzureClientError.InvalidTarget, azureClient.SetActiveTargetAsync(new MockChannel(), "unrecognized.target")); + + // SetActiveTargetAsync with valid target ID + var target = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator")); + Assert.AreEqual("ionq.simulator", target.Id); + + // GetActiveTargetAsync + target = ExpectSuccess(azureClient.GetActiveTargetAsync(new MockChannel())); + Assert.AreEqual("ionq.simulator", target.Id); + } + + [TestMethod] + public void TestAllTargets() + { + var services = Startup.CreateServiceProvider("Workspace"); + var azureClient = (AzureClient)services.GetService(); + + // connect to mock workspace with all providers + var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient, MockAzureWorkspace.NameWithMockProviders)); + Assert.AreEqual(Enum.GetNames(typeof(AzureProvider)).Length, targets.Count()); + + // set each target, which will load the corresponding package + foreach (var target in targets) + { + var returnedTarget = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), target.Id)); + Assert.AreEqual(target.Id, returnedTarget.Id); + } + } + + [TestMethod] + public void TestJobSubmission() + { + var services = Startup.CreateServiceProvider("Workspace"); + var azureClient = (AzureClient)services.GetService(); + var submissionContext = new AzureSubmissionContext(); + + // not yet connected + ExpectError(AzureClientError.NotConnected, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + + // connect + var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient)); + Assert.IsFalse(targets.Any()); + + // no target yet + ExpectError(AzureClientError.NoTarget, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + + // add a target + var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace; + Assert.IsNotNull(azureWorkspace); + azureWorkspace?.AddMockTargets("ionq.simulator"); + + // set the active target + var target = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator")); + Assert.AreEqual("ionq.simulator", target.Id); + + // no operation name specified + ExpectError(AzureClientError.NoOperationName, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + + // specify an operation name, but have missing parameters + submissionContext.OperationName = "Tests.qss.HelloAgain"; + ExpectError(AzureClientError.JobSubmissionFailed, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + + // specify input parameters and verify that the job was submitted + submissionContext.InputParameters = new Dictionary() { ["count"] = "3", ["name"] = "testing" }; + var job = ExpectSuccess(azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + var retrievedJob = ExpectSuccess(azureClient.GetJobStatusAsync(new MockChannel(), job.Id)); + Assert.AreEqual(job.Id, retrievedJob.Id); + } + + [TestMethod] + public void TestJobExecution() + { + var services = Startup.CreateServiceProvider("Workspace"); + var azureClient = (AzureClient)services.GetService(); + + // connect + var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient)); + Assert.IsFalse(targets.Any()); + + // add a target + var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace; + Assert.IsNotNull(azureWorkspace); + azureWorkspace?.AddMockTargets("ionq.simulator"); + + // set the active target + var target = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator")); + Assert.AreEqual("ionq.simulator", target.Id); + + // execute the job and verify that the results are retrieved successfully + var submissionContext = new AzureSubmissionContext() + { + OperationName = "Tests.qss.HelloAgain", + InputParameters = new Dictionary() { ["count"] = "3", ["name"] = "testing" }, + ExecutionTimeout = 5, + ExecutionPollingInterval = 1, + }; + var histogram = ExpectSuccess(azureClient.ExecuteJobAsync(new MockChannel(), submissionContext)); + Assert.IsNotNull(histogram); } } } From fb9e21834b897d3855e2d4293444782041340157 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Sun, 14 Jun 2020 08:26:23 -0700 Subject: [PATCH 28/44] Raise Python exceptions rather than returning error objects --- src/Python/qsharp/azure.py | 37 ++++++++++++++++----------- src/Python/qsharp/tests/test_azure.py | 21 ++++++++++----- 2 files changed, 36 insertions(+), 22 deletions(-) diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index e9c9abe633..f2632e8d32 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -73,7 +73,7 @@ def __eq__(self, other): return NotImplemented return self.__dict__ == other.__dict__ -class AzureError(object): +class AzureError(Exception): """ Contains error information resulting from an attempt to interact with Azure. """ @@ -91,30 +91,37 @@ def __eq__(self, other): ## FUNCTIONS ## -def connect(**params) -> Union[List[AzureTarget], AzureError]: +def connect(**params) -> List[AzureTarget]: result = qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params) - return AzureError(result) if "error_code" in result else [AzureTarget(target) for target in result] + if "error_code" in result: raise AzureError(result) + return [AzureTarget(target) for target in result] -def target(name : str = '', **params) -> Union[AzureTarget, AzureError]: +def target(name : str = '', **params) -> AzureTarget: result = qsharp.client._execute_magic(f"azure.target {name}", raise_on_stderr=False, **params) - return AzureError(result) if "error_code" in result else AzureTarget(result) + if "error_code" in result: raise AzureError(result) + return AzureTarget(result) -def submit(op, **params) -> Union[AzureJob, AzureError]: +def submit(op, **params) -> AzureJob: result = qsharp.client._execute_callable_magic("azure.submit", op, raise_on_stderr=False, **params) - return AzureError(result) if "error_code" in result else AzureJob(result) + if "error_code" in result: raise AzureError(result) + return AzureJob(result) -def execute(op, **params) -> Union[Dict, AzureError]: +def execute(op, **params) -> Dict: result = qsharp.client._execute_callable_magic("azure.execute", op, raise_on_stderr=False, **params) - return AzureError(result) if "error_code" in result else result + if "error_code" in result: raise AzureError(result) + return result -def status(jobId : str = '', **params) -> Union[AzureJob, AzureError]: +def status(jobId : str = '', **params) -> AzureJob: result = qsharp.client._execute_magic(f"azure.status {jobId}", raise_on_stderr=False, **params) - return AzureError(result) if "error_code" in result else AzureJob(result) + if "error_code" in result: raise AzureError(result) + return AzureJob(result) -def output(jobId : str = '', **params) -> Union[Dict, AzureError]: +def output(jobId : str = '', **params) -> Dict: result = qsharp.client._execute_magic(f"azure.output {jobId}", raise_on_stderr=False, **params) - return AzureError(result) if "error_code" in result else result + if "error_code" in result: raise AzureError(result) + return result -def jobs(**params) -> Union[List[AzureJob], AzureError]: +def jobs(**params) -> List[AzureJob]: result = qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params) - return AzureError(result) if "error_code" in result else [AzureJob(job) for job in result] + if "error_code" in result: raise AzureError(result) + return [AzureJob(job) for job in result] diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py index 4e195e4f88..07bd7e6cf9 100644 --- a/src/Python/qsharp/tests/test_azure.py +++ b/src/Python/qsharp/tests/test_azure.py @@ -32,6 +32,10 @@ def test_empty_workspace(monkeypatch): """ Tests behavior of a mock workspace with no providers. """ + with pytest.raises(AzureError) as exception_info: + qsharp.azure.target() # not yet connected + assert exception_info.value.error_name == "NotConnected" + targets = qsharp.azure.connect( storageAccountConnectionString="test", subscriptionId="test", @@ -40,8 +44,9 @@ def test_empty_workspace(monkeypatch): ) assert targets == [] - result = qsharp.azure.target("invalid.target") - assert isinstance(result, AzureError) + with pytest.raises(AzureError) as exception_info: + qsharp.azure.target("invalid.target") # invalid target + assert exception_info.value.error_name == "InvalidTarget" jobs = qsharp.azure.jobs() assert jobs == [] @@ -50,9 +55,6 @@ def test_workspace_with_providers(): """ Tests behavior of a mock workspace with mock providers. """ - result = qsharp.azure.target() - assert isinstance(result, AzureError) - targets = qsharp.azure.connect( storageAccountConnectionString="test", subscriptionId="test", @@ -62,6 +64,10 @@ def test_workspace_with_providers(): assert isinstance(targets, list) assert len(targets) > 0 + with pytest.raises(AzureError) as exception_info: + qsharp.azure.target() # no target specified yet + assert exception_info.value.error_name == "NoTarget" + for target in targets: active_target = qsharp.azure.target(target.id) assert isinstance(active_target, AzureTarget) @@ -86,8 +92,9 @@ def test_workspace_with_providers(): # Execute a workspace operation with parameters op = qsharp.QSharpCallable("Microsoft.Quantum.SanityTests.HelloAgain", None) - result = qsharp.azure.execute(op) # missing parameters - assert isinstance(result, AzureError) + with pytest.raises(AzureError) as exception_info: + qsharp.azure.execute(op) # missing parameters + assert exception_info.value.error_name == "JobSubmissionFailed" histogram = qsharp.azure.execute(op, count=3, name="test") assert isinstance(histogram, dict) From a6391c7199061b216d58718b2affc88b225925d8 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 15 Jun 2020 07:42:26 -0700 Subject: [PATCH 29/44] Fix nullable warnings --- src/AzureClient/Visualization/CloudJobEncoders.cs | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/src/AzureClient/Visualization/CloudJobEncoders.cs b/src/AzureClient/Visualization/CloudJobEncoders.cs index d4b20ced6f..4d8d9c8a3a 100644 --- a/src/AzureClient/Visualization/CloudJobEncoders.cs +++ b/src/AzureClient/Visualization/CloudJobEncoders.cs @@ -22,8 +22,8 @@ internal static class CloudJobExtensions ? dateTime : null as DateTime?; - internal static Dictionary ToDictionary(this CloudJob cloudJob) => - new Dictionary() + internal static Dictionary ToDictionary(this CloudJob cloudJob) => + new Dictionary() { // TODO: add cloudJob.Uri after https://github.com/microsoft/qsharp-runtime/issues/236 is fixed. ["id"] = cloudJob.Id, @@ -47,11 +47,11 @@ internal static Table ToJupyterTable(this IEnumerable jobsLi ("Job Status", cloudJob => cloudJob.Status), ("Provider", cloudJob => cloudJob.Details.ProviderId), ("Target", cloudJob => cloudJob.Details.Target), - ("Creation Time", cloudJob => cloudJob.Details.CreationTime.ToDateTime()?.ToString()), - ("Begin Execution Time", cloudJob => cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToString()), - ("End Execution Time", cloudJob => cloudJob.Details.EndExecutionTime.ToDateTime()?.ToString()), + ("Creation Time", cloudJob => cloudJob.Details.CreationTime.ToDateTime()?.ToString() ?? string.Empty), + ("Begin Execution Time", cloudJob => cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToString() ?? string.Empty), + ("End Execution Time", cloudJob => cloudJob.Details.EndExecutionTime.ToDateTime()?.ToString() ?? string.Empty), }, - Rows = jobsList.OrderByDescending(job => job.Details.CreationTime).ToList() + Rows = jobsList.OrderByDescending(job => job.Details.CreationTime).ToList(), }; } From 2fed6476b65356ae8096202cda68a40ef2ac6e8d Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 15 Jun 2020 14:09:17 -0700 Subject: [PATCH 30/44] Minor cleanup --- src/Python/qsharp/tests/test_azure.py | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py index 07bd7e6cf9..f626a759c6 100644 --- a/src/Python/qsharp/tests/test_azure.py +++ b/src/Python/qsharp/tests/test_azure.py @@ -28,12 +28,12 @@ def set_environment_variables(): ## TESTS ## -def test_empty_workspace(monkeypatch): +def test_empty_workspace(): """ Tests behavior of a mock workspace with no providers. """ with pytest.raises(AzureError) as exception_info: - qsharp.azure.target() # not yet connected + qsharp.azure.target() assert exception_info.value.error_name == "NotConnected" targets = qsharp.azure.connect( @@ -45,7 +45,7 @@ def test_empty_workspace(monkeypatch): assert targets == [] with pytest.raises(AzureError) as exception_info: - qsharp.azure.target("invalid.target") # invalid target + qsharp.azure.target("invalid.target") assert exception_info.value.error_name == "InvalidTarget" jobs = qsharp.azure.jobs() @@ -65,7 +65,7 @@ def test_workspace_with_providers(): assert len(targets) > 0 with pytest.raises(AzureError) as exception_info: - qsharp.azure.target() # no target specified yet + qsharp.azure.target() assert exception_info.value.error_name == "NoTarget" for target in targets: @@ -93,7 +93,7 @@ def test_workspace_with_providers(): op = qsharp.QSharpCallable("Microsoft.Quantum.SanityTests.HelloAgain", None) with pytest.raises(AzureError) as exception_info: - qsharp.azure.execute(op) # missing parameters + qsharp.azure.execute(op) assert exception_info.value.error_name == "JobSubmissionFailed" histogram = qsharp.azure.execute(op, count=3, name="test") From 8771325ff3a9e1ad0349614223dfbd320bab03b0 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 15 Jun 2020 15:29:28 -0700 Subject: [PATCH 31/44] Improvements to magic command argument parsing (#170) * Use updated parsing method everywhere * Improve handling of quotes and spaces * Small improvement to switch statement * Add some tests for the new parsing --- src/Jupyter/Magic/AbstractMagic.cs | 86 +++++++++++++++--------------- src/Kernel/Magic/PackageMagic.cs | 5 +- src/Kernel/Magic/WorkspaceMagic.cs | 5 +- src/Tests/AzureClientMagicTests.cs | 68 +++++++++++++++++------ 4 files changed, 104 insertions(+), 60 deletions(-) diff --git a/src/Jupyter/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs index 09a3df31b8..64a1326266 100644 --- a/src/Jupyter/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Text.RegularExpressions; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Common; @@ -63,24 +64,6 @@ public Func> SafeExecute(Func - /// Parses the input to a magic command, interpreting the input as - /// a name followed by a JSON-serialized dictionary. - /// - public static (string, Dictionary) ParseInput(string input) - { - if (input == null) return (string.Empty, new Dictionary { }); - var BLANK_SPACE = new char[1] { ' ' }; - - var inputParts = input.Split(BLANK_SPACE, 2, StringSplitOptions.RemoveEmptyEntries); - var name = inputParts.Length > 0 ? inputParts[0] : string.Empty; - var args = inputParts.Length > 1 - ? JsonConverters.JsonToDict(inputParts[1]) - : new Dictionary { }; - - return (name, args); - } - /// /// Parses the input to a magic command, interpreting the input as /// a name followed by a JSON-serialized dictionary. @@ -97,46 +80,63 @@ public static Dictionary ParseInputParameters(string input, stri { Dictionary inputParameters = new Dictionary(); - var args = input.Split(null as char[], StringSplitOptions.RemoveEmptyEntries); + // This regex looks for four types of matches: + // 1. (\{.*\}) + // Matches anything enclosed in matching curly braces. + // 2. [^\s"]+(?:\s*=\s*)(?:"[^"]*"|[^\s"]*)* + // Matches things that look like key=value, allowing whitespace around the equals sign, + // and allowing value to be a quoted string, e.g., key="value". + // 3. [^\s"]+(?:"[^"]*"[^\s"]*)* + // Matches things that are single words, not inside quotes. + // 4. (?:"[^"]*"[^\s"]*)+ + // Matches quoted strings. + var regex = new Regex(@"(\{.*\})|[^\s""]+(?:\s*=\s*)(?:""[^""]*""|[^\s""]*)*|[^\s""]+(?:""[^""]*""[^\s""]*)*|(?:""[^""]*""[^\s""]*)+"); + var args = regex.Matches(input).Select(match => match.Value); // If we are expecting a first inferred-name parameter, see if it exists. // If so, serialize it to the dictionary as JSON and remove it from the list of args. - if (args.Length > 0 && - !args[0].StartsWith("{") && - !args[0].Contains("=") && + if (args.Any() && + !args.First().StartsWith("{") && + !args.First().Contains("=") && !string.IsNullOrEmpty(firstParameterInferredName)) { - using (var writer = new StringWriter()) - { - Json.Serializer.Serialize(writer, args[0]); - inputParameters[firstParameterInferredName] = writer.ToString(); - } - args = args.Where((_, index) => index != 0).ToArray(); + using var writer = new StringWriter(); + Json.Serializer.Serialize(writer, args.First()); + inputParameters[firstParameterInferredName] = writer.ToString(); + args = args.Skip(1); } - // See if the remaining arguments look like JSON. If so, try to parse as JSON. - // Otherwise, try to parse as key=value pairs and serialize into the dictionary as JSON. - if (args.Length > 0 && args[0].StartsWith("{")) + // See if the remaining arguments look like JSON. If so, parse as JSON. + if (args.Any() && args.First().StartsWith("{")) { - var jsonArgs = JsonToDict(string.Join(" ", args)); + var jsonArgs = JsonToDict(args.First()); foreach (var (key, jsonValue) in jsonArgs) { inputParameters[key] = jsonValue; } + + return inputParameters; } - else + + // Otherwise, try to parse as key=value pairs and serialize into the dictionary as JSON. + foreach (string arg in args) { - foreach (string arg in args) + var tokens = arg.Split("=", 2); + var key = tokens[0].Trim(); + var value = tokens.Length switch { - var tokens = arg.Split("=", 2); - var key = tokens[0].Trim(); - var value = (tokens.Length == 1) ? true as object : tokens[1].Trim() as object; - using (var writer = new StringWriter()) - { - Json.Serializer.Serialize(writer, value); - inputParameters[key] = writer.ToString(); - } - } + // If there was no value provided explicitly, treat it as an implicit "true" value + 1 => true as object, + + // Trim whitespace and also enclosing single-quotes or double-quotes before returning + 2 => Regex.Replace(tokens[1].Trim(), @"^['""]|['""]$", string.Empty) as object, + + // We called arg.Split("=", 2), so there should never be more than 2 + _ => throw new InvalidOperationException() + }; + using var writer = new StringWriter(); + Json.Serializer.Serialize(writer, value); + inputParameters[key] = writer.ToString(); } return inputParameters; diff --git a/src/Kernel/Magic/PackageMagic.cs b/src/Kernel/Magic/PackageMagic.cs index 11a635e462..8ca8aa2d43 100644 --- a/src/Kernel/Magic/PackageMagic.cs +++ b/src/Kernel/Magic/PackageMagic.cs @@ -17,6 +17,8 @@ namespace Microsoft.Quantum.IQSharp.Kernel /// public class PackageMagic : AbstractMagic { + private const string ParameterNamePackageName = "__packageName__"; + /// /// Constructs a new magic command that adds package references to /// a given references collection. @@ -39,7 +41,8 @@ public PackageMagic(IReferences references) : base( /// public override ExecutionResult Run(string input, IChannel channel) { - var (name, _) = ParseInput(input); + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNamePackageName); + var name = inputParameters.DecodeParameter(ParameterNamePackageName); var status = new Jupyter.TaskStatus($"Adding package {name}"); var statusUpdater = channel.DisplayUpdatable(status); void Update() => statusUpdater.Update(status); diff --git a/src/Kernel/Magic/WorkspaceMagic.cs b/src/Kernel/Magic/WorkspaceMagic.cs index 7ef9ee2996..22b089f7b7 100644 --- a/src/Kernel/Magic/WorkspaceMagic.cs +++ b/src/Kernel/Magic/WorkspaceMagic.cs @@ -14,6 +14,8 @@ namespace Microsoft.Quantum.IQSharp.Kernel /// public class WorkspaceMagic : AbstractMagic { + private const string ParameterNameCommand = "__command__"; + /// /// Given a workspace, constructs a new magic symbol to control /// that workspace. @@ -51,7 +53,8 @@ public void CheckIfReady() /// public override ExecutionResult Run(string input, IChannel channel) { - var (command, _) = ParseInput(input); + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameCommand); + var command = inputParameters.DecodeParameter(ParameterNameCommand); if (string.IsNullOrWhiteSpace(command)) { diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index b06833d336..51763fdbac 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -50,9 +50,25 @@ public void TestConnectMagic() resourceGroupName={resourceGroupName} workspaceName={workspaceName} storageAccountConnectionString={storageAccountConnectionString}"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.Connect); + Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); Assert.IsFalse(azureClient.RefreshCredentials); - Assert.AreEqual(azureClient.ConnectionString, storageAccountConnectionString); + Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); + Assert.AreEqual(resourceGroupName, azureClient.ResourceGroupName); + Assert.AreEqual(workspaceName, azureClient.WorkspaceName); + Assert.AreEqual(storageAccountConnectionString, azureClient.ConnectionString); + + // valid input with extra whitespace and quotes + connectMagic.Test( + @$"subscriptionId = {subscriptionId} + resourceGroupName= ""{resourceGroupName}"" + workspaceName ={workspaceName} + storageAccountConnectionString = '{storageAccountConnectionString}'"); + Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); + Assert.IsFalse(azureClient.RefreshCredentials); + Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); + Assert.AreEqual(resourceGroupName, azureClient.ResourceGroupName); + Assert.AreEqual(workspaceName, azureClient.WorkspaceName); + Assert.AreEqual(storageAccountConnectionString, azureClient.ConnectionString); // valid input with forced login connectMagic.Test( @@ -60,7 +76,6 @@ public void TestConnectMagic() resourceGroupName={resourceGroupName} workspaceName={workspaceName} storageAccountConnectionString={storageAccountConnectionString}"); - Assert.IsTrue(azureClient.RefreshCredentials); } @@ -71,13 +86,19 @@ public void TestStatusMagic() var azureClient = new MockAzureClient(); var statusMagic = new StatusMagic(azureClient); statusMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetJobStatus); + Assert.AreEqual(AzureClientAction.GetJobStatus, azureClient.LastAction); // single argument - should print job status azureClient = new MockAzureClient(); statusMagic = new StatusMagic(azureClient); statusMagic.Test($"{jobId}"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetJobStatus); + Assert.AreEqual(AzureClientAction.GetJobStatus, azureClient.LastAction); + + // single argument with quotes - should print job status + azureClient = new MockAzureClient(); + statusMagic = new StatusMagic(azureClient); + statusMagic.Test($"\"{jobId}\""); + Assert.AreEqual(AzureClientAction.GetJobStatus, azureClient.LastAction); } [TestMethod] @@ -87,11 +108,11 @@ public void TestSubmitMagic() var azureClient = new MockAzureClient(); var submitMagic = new SubmitMagic(azureClient); submitMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); + Assert.AreEqual(AzureClientAction.SubmitJob, azureClient.LastAction); // single argument submitMagic.Test($"{operationName}"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.SubmitJob); + Assert.AreEqual(AzureClientAction.SubmitJob, azureClient.LastAction); Assert.IsTrue(azureClient.SubmittedJobs.Contains(operationName)); } @@ -102,11 +123,11 @@ public void TestExecuteMagic() var azureClient = new MockAzureClient(); var executeMagic = new ExecuteMagic(azureClient); executeMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.ExecuteJob); + Assert.AreEqual(AzureClientAction.ExecuteJob, azureClient.LastAction); // single argument executeMagic.Test($"{operationName}"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.ExecuteJob); + Assert.AreEqual(AzureClientAction.ExecuteJob, azureClient.LastAction); Assert.IsTrue(azureClient.ExecutedJobs.Contains(operationName)); } @@ -117,13 +138,19 @@ public void TestOutputMagic() var azureClient = new MockAzureClient(); var outputMagic = new OutputMagic(azureClient); outputMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetJobResult); + Assert.AreEqual(AzureClientAction.GetJobResult, azureClient.LastAction); - // single argument - should print job status + // single argument - should print job result azureClient = new MockAzureClient(); outputMagic = new OutputMagic(azureClient); outputMagic.Test($"{jobId}"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetJobResult); + Assert.AreEqual(AzureClientAction.GetJobResult, azureClient.LastAction); + + // single argument with quotes - should print job result + azureClient = new MockAzureClient(); + outputMagic = new OutputMagic(azureClient); + outputMagic.Test($"'{jobId}'"); + Assert.AreEqual(AzureClientAction.GetJobResult, azureClient.LastAction); } [TestMethod] @@ -133,7 +160,7 @@ public void TestJobsMagic() var azureClient = new MockAzureClient(); var jobsMagic = new JobsMagic(azureClient); jobsMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetJobList); + Assert.AreEqual(AzureClientAction.GetJobList, azureClient.LastAction); } [TestMethod] @@ -143,13 +170,18 @@ public void TestTargetMagic() var azureClient = new MockAzureClient(); var targetMagic = new TargetMagic(azureClient); targetMagic.Test(targetId); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); + Assert.AreEqual(AzureClientAction.SetActiveTarget, azureClient.LastAction); + + // single argument with quotes - should set active target + targetMagic = new TargetMagic(azureClient); + targetMagic.Test($"\"{targetId}\""); + Assert.AreEqual(AzureClientAction.SetActiveTarget, azureClient.LastAction); // no arguments - should print active target azureClient = new MockAzureClient(); targetMagic = new TargetMagic(azureClient); targetMagic.Test(string.Empty); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetActiveTarget); + Assert.AreEqual(AzureClientAction.GetActiveTarget, azureClient.LastAction); } } @@ -170,6 +202,9 @@ internal enum AzureClientAction public class MockAzureClient : IAzureClient { internal AzureClientAction LastAction = AzureClientAction.None; + internal string SubscriptionId = string.Empty; + internal string ResourceGroupName = string.Empty; + internal string WorkspaceName = string.Empty; internal string ConnectionString = string.Empty; internal bool RefreshCredentials = false; internal string ActiveTargetId = string.Empty; @@ -205,6 +240,9 @@ public async Task ExecuteJobAsync(IChannel channel, AzureSubmis public async Task ConnectAsync(IChannel channel, string subscriptionId, string resourceGroupName, string workspaceName, string storageAccountConnectionString, bool refreshCredentials) { LastAction = AzureClientAction.Connect; + SubscriptionId = subscriptionId; + ResourceGroupName = resourceGroupName; + WorkspaceName = workspaceName; ConnectionString = storageAccountConnectionString; RefreshCredentials = refreshCredentials; return ExecuteStatus.Ok.ToExecutionResult(); From 17c24e80d9628cc6a5f581590368097e85b4ec24 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Tue, 16 Jun 2020 16:51:29 -0700 Subject: [PATCH 32/44] Add missing Dedent() in ConfigMagic --- src/Kernel/Magic/ConfigMagic.cs | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/Kernel/Magic/ConfigMagic.cs b/src/Kernel/Magic/ConfigMagic.cs index 6a020af780..586a8a6cbb 100644 --- a/src/Kernel/Magic/ConfigMagic.cs +++ b/src/Kernel/Magic/ConfigMagic.cs @@ -45,7 +45,7 @@ save those options to a JSON file in the current working dump.basisStateLabelingConvention ""BigEndian"" dump.truncateSmallAmplitudes true ``` - ", + ".Dedent(), @" Configure the `DumpMachine` and `DumpRegister` callables From 5164a0e159b76be73ee13ac063187bb9ba8f0a13 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 17 Jun 2020 14:55:48 -0700 Subject: [PATCH 33/44] Add filter for %azure.jobs command --- src/AzureClient/AzureClient.cs | 9 +++++++-- src/AzureClient/IAzureClient.cs | 6 ++++-- src/AzureClient/Magic/JobsMagic.cs | 23 +++++++++++++++++++---- src/Tests/AzureClientMagicTests.cs | 2 +- 4 files changed, 31 insertions(+), 9 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index c03ad4a61f..1187a5ef0f 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -335,7 +335,7 @@ public async Task GetJobStatusAsync(IChannel channel, string jo } /// - public async Task GetJobListAsync(IChannel channel) + public async Task GetJobListAsync(IChannel channel, string filter) { if (ActiveWorkspace == null) { @@ -350,7 +350,12 @@ public async Task GetJobListAsync(IChannel channel) return AzureClientError.JobNotFound.ToExecutionResult(); } - return jobs.ToExecutionResult(); + return jobs + .Where(job => + job.Id.Contains(filter, StringComparison.OrdinalIgnoreCase) || + job.Details.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || + job.Details.Target.Contains(filter, StringComparison.OrdinalIgnoreCase)) + .ToExecutionResult(); } } } diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index d6c67678ff..a10712c9ab 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -177,8 +177,10 @@ public Task ConnectAsync(IChannel channel, /// Gets a list of all jobs in the current Azure Quantum workspace. /// /// - /// A list of all jobs in the current workspace. + /// A list of all jobs in the current workspace, optionally filtered + /// to jobs with fields containing filter using a case-insensitive + /// comparison. /// - public Task GetJobListAsync(IChannel channel); + public Task GetJobListAsync(IChannel channel, string filter); } } diff --git a/src/AzureClient/Magic/JobsMagic.cs b/src/AzureClient/Magic/JobsMagic.cs index f23708d9ea..b5605a98d1 100644 --- a/src/AzureClient/Magic/JobsMagic.cs +++ b/src/AzureClient/Magic/JobsMagic.cs @@ -17,6 +17,8 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class JobsMagic : AzureClientMagicBase { + private const string ParameterNameFilter = "__filter__"; + /// /// Initializes a new instance of the class. /// @@ -32,7 +34,8 @@ public JobsMagic(IAzureClient azureClient) Summary = "Displays a list of jobs in the current Azure Quantum workspace.", Description = @" This magic command allows for displaying the list of jobs in the current - Azure Quantum workspace. + Azure Quantum workspace, optionally filtering the list to jobs which + have an ID, name, or target containing the provided filter parameter. The Azure Quantum workspace must previously have been initialized using the %azure.connect magic command. @@ -46,13 +49,25 @@ The Azure Quantum workspace must previously have been initialized Out[]: ``` ".Dedent(), + + @" + Print the list of jobs whose names contain ""MyJob"": + ``` + In []: %azure.jobs ""MyJob"" + Out[]: + ``` + ".Dedent(), }, }) {} /// - /// Lists all jobs in the active workspace. + /// Lists all jobs in the active workspace, optionally filtered by a provided parameter. /// - public override async Task RunAsync(string input, IChannel channel) => - await AzureClient.GetJobListAsync(channel); + public override async Task RunAsync(string input, IChannel channel) + { + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameFilter); + var filter = inputParameters.DecodeParameter(ParameterNameFilter, defaultValue: string.Empty); + return await AzureClient.GetJobListAsync(channel, filter); + } } } \ No newline at end of file diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 51763fdbac..4e1f3483a6 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -254,7 +254,7 @@ public async Task GetConnectionStatusAsync(IChannel channel) return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task GetJobListAsync(IChannel channel) + public async Task GetJobListAsync(IChannel channel, string filter) { LastAction = AzureClientAction.GetJobList; return ExecuteStatus.Ok.ToExecutionResult(); From 59741996d2e0c123449fc39cd6c3d83826b9a42c Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Wed, 17 Jun 2020 15:11:45 -0700 Subject: [PATCH 34/44] Move filter logic into extension function --- src/AzureClient/AzureClient.cs | 7 +------ src/AzureClient/Extensions.cs | 9 +++++++++ 2 files changed, 10 insertions(+), 6 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 1187a5ef0f..ce96907640 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -350,12 +350,7 @@ public async Task GetJobListAsync(IChannel channel, string filt return AzureClientError.JobNotFound.ToExecutionResult(); } - return jobs - .Where(job => - job.Id.Contains(filter, StringComparison.OrdinalIgnoreCase) || - job.Details.Name.Contains(filter, StringComparison.OrdinalIgnoreCase) || - job.Details.Target.Contains(filter, StringComparison.OrdinalIgnoreCase)) - .ToExecutionResult(); + return jobs.Where(job => job.Matches(filter)).ToExecutionResult(); } } } diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 1fda56014e..26662458b3 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -8,6 +8,7 @@ using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; +using Microsoft.Azure.Quantum; using Microsoft.Extensions.DependencyInjection; using Microsoft.Jupyter.Core; @@ -77,5 +78,13 @@ internal static async Task ToExecutionResult(this Task { singleton } : source is IEnumerable collection ? collection : null; + + /// + /// Determines whether the given matches the given filter. + /// + internal static bool Matches(this CloudJob job, string filter) => + (job.Id != null && job.Id.Contains(filter, StringComparison.OrdinalIgnoreCase)) || + (job.Details.Name != null && job.Id.Contains(filter, StringComparison.OrdinalIgnoreCase)) || + (job.Details.Target != null && job.Id.Contains(filter, StringComparison.OrdinalIgnoreCase)); } } From f1647383f133b2e3ec53b14a61ac6d2de58518bc Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Thu, 18 Jun 2020 09:22:22 -0700 Subject: [PATCH 35/44] Fix typo --- src/AzureClient/Extensions.cs | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 26662458b3..954bcda3e7 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -84,7 +84,7 @@ internal static async Task ToExecutionResult(this Task internal static bool Matches(this CloudJob job, string filter) => (job.Id != null && job.Id.Contains(filter, StringComparison.OrdinalIgnoreCase)) || - (job.Details.Name != null && job.Id.Contains(filter, StringComparison.OrdinalIgnoreCase)) || - (job.Details.Target != null && job.Id.Contains(filter, StringComparison.OrdinalIgnoreCase)); + (job.Details.Name != null && job.Details.Name.Contains(filter, StringComparison.OrdinalIgnoreCase)) || + (job.Details.Target != null && job.Details.Target.Contains(filter, StringComparison.OrdinalIgnoreCase)); } } From 83fa8f90323f2280cd603ad74e92fc2f98c19703 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 19 Jun 2020 07:12:45 -0700 Subject: [PATCH 36/44] Tests and Python support --- src/AzureClient/AzureClient.cs | 12 ++++++++++-- src/AzureClient/Magic/JobsMagic.cs | 2 +- src/Python/qsharp/azure.py | 4 ++-- src/Python/qsharp/tests/test_azure.py | 6 ++++++ src/Tests/AzureClientMagicTests.cs | 6 ++++++ src/Tests/AzureClientTests.cs | 8 ++++++-- 6 files changed, 31 insertions(+), 7 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index e392b0c1a9..3cee0e827b 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -358,8 +358,16 @@ public async Task GetJobListAsync(IChannel channel, string filt { channel.Stderr("No jobs found in current Azure Quantum workspace."); } - - return jobs.Where(job => job.Matches(filter)).ToExecutionResult(); + else + { + jobs = jobs.Where(job => job.Matches(filter)); + if (jobs.Count() == 0) + { + channel.Stderr($"No jobs matching \"{filter}\" found in current Azure Quantum workspace."); + } + } + + return jobs.ToExecutionResult(); } } } diff --git a/src/AzureClient/Magic/JobsMagic.cs b/src/AzureClient/Magic/JobsMagic.cs index 9c3038b9e4..0d2edf4380 100644 --- a/src/AzureClient/Magic/JobsMagic.cs +++ b/src/AzureClient/Magic/JobsMagic.cs @@ -51,7 +51,7 @@ The Azure Quantum workspace must previously have been initialized ".Dedent(), @" - Print the list of jobs whose names contain ""MyJob"": + Print the list of jobs whose ID, name, or target contains ""MyJob"": ``` In []: %azure.jobs ""MyJob"" Out[]: diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index f2632e8d32..0d1865fc23 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -121,7 +121,7 @@ def output(jobId : str = '', **params) -> Dict: if "error_code" in result: raise AzureError(result) return result -def jobs(**params) -> List[AzureJob]: - result = qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params) +def jobs(filter : str = '', **params) -> List[AzureJob]: + result = qsharp.client._execute_magic(f"azure.jobs {filter}", raise_on_stderr=False, **params) if "error_code" in result: raise AzureError(result) return [AzureJob(job) for job in result] diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py index f626a759c6..613aa0b42a 100644 --- a/src/Python/qsharp/tests/test_azure.py +++ b/src/Python/qsharp/tests/test_azure.py @@ -107,3 +107,9 @@ def test_workspace_with_providers(): jobs = qsharp.azure.jobs() assert isinstance(jobs, list) assert len(jobs) == 2 + + # Check that job filtering works + jobs = qsharp.azure.jobs(job.id) + print(job.id) + assert isinstance(jobs, list) + assert len(jobs) == 1 diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 4e1f3483a6..2836671a7b 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -161,6 +161,12 @@ public void TestJobsMagic() var jobsMagic = new JobsMagic(azureClient); jobsMagic.Test(string.Empty); Assert.AreEqual(AzureClientAction.GetJobList, azureClient.LastAction); + + // with arguments - should still print job status + azureClient = new MockAzureClient(); + jobsMagic = new JobsMagic(azureClient); + jobsMagic.Test($"{jobId}"); + Assert.AreEqual(AzureClientAction.GetJobList, azureClient.LastAction); } [TestMethod] diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index 099c251268..4117b0a237 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -131,9 +131,13 @@ public void TestJobStatus() // invalid job ID ExpectError(AzureClientError.JobNotFound, azureClient.GetJobStatusAsync(new MockChannel(), "JOB_ID_3")); - // jobs list - var jobs = ExpectSuccess>(azureClient.GetJobListAsync(new MockChannel())); + // jobs list with no filter + var jobs = ExpectSuccess>(azureClient.GetJobListAsync(new MockChannel(), string.Empty)); Assert.AreEqual(2, jobs.Count()); + + // jobs list with filter + jobs = ExpectSuccess>(azureClient.GetJobListAsync(new MockChannel(), "JOB_ID_1")); + Assert.AreEqual(1, jobs.Count()); } [TestMethod] From f328aa5ea8446c757c45484e7e5d2f866eaeaed0 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Fri, 19 Jun 2020 13:29:23 -0700 Subject: [PATCH 37/44] Few improvements to output when using Python --- src/AzureClient/AzureClient.cs | 28 +++++++++++++++++----------- src/Python/qsharp/azure.py | 15 ++++++++++++--- 2 files changed, 29 insertions(+), 14 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 1622cb0ccd..ed059d99f9 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -24,6 +24,8 @@ public class AzureClient : IAzureClient private ILogger Logger { get; } private IReferences References { get; } private IEntryPointGenerator EntryPointGenerator { get; } + private IMetadataController MetadataController { get; } + private bool IsPythonUserAgent => MetadataController?.UserAgent?.StartsWith("qsharp.py") ?? false; private string ConnectionString { get; set; } = string.Empty; private AzureExecutionTarget? ActiveTarget { get; set; } private string MostRecentJobId { get; set; } = string.Empty; @@ -39,11 +41,13 @@ public AzureClient( IExecutionEngine engine, IReferences references, IEntryPointGenerator entryPointGenerator, + IMetadataController metadataController, ILogger logger, IEventService eventService) { References = references; EntryPointGenerator = entryPointGenerator; + MetadataController = metadataController; Logger = logger; eventService?.TriggerServiceInitialized(this); @@ -110,20 +114,19 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, Az { if (ActiveWorkspace == null) { - channel.Stderr("Please call %azure.connect before submitting a job."); + channel.Stderr($"Please call {GetCommandDisplayName("connect")} before submitting a job."); return AzureClientError.NotConnected.ToExecutionResult(); } if (ActiveTarget == null) { - channel.Stderr("Please call %azure.target before submitting a job."); + channel.Stderr($"Please call {GetCommandDisplayName("target")} before submitting a job."); return AzureClientError.NoTarget.ToExecutionResult(); } if (string.IsNullOrEmpty(submissionContext.OperationName)) { - var commandName = execute ? "%azure.execute" : "%azure.submit"; - channel.Stderr($"Please pass a valid Q# operation name to {commandName}."); + channel.Stderr($"Please pass a valid Q# operation name to {GetCommandDisplayName(execute ? "execute" : "submit")}."); return AzureClientError.NoOperationName.ToExecutionResult(); } @@ -213,13 +216,13 @@ public async Task GetActiveTargetAsync(IChannel channel) { if (AvailableProviders == null) { - channel.Stderr("Please call %azure.connect before getting the execution target."); + channel.Stderr($"Please call {GetCommandDisplayName("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.Stderr($"No execution target has been specified. To specify one, call {GetCommandDisplayName("target")} with the target ID."); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); return AzureClientError.NoTarget.ToExecutionResult(); } @@ -235,7 +238,7 @@ public async Task SetActiveTargetAsync(IChannel channel, string { if (AvailableProviders == null) { - channel.Stderr("Please call %azure.connect before setting an execution target."); + channel.Stderr($"Please call {GetCommandDisplayName("connect")} before setting an execution target."); return AzureClientError.NotConnected.ToExecutionResult(); } @@ -272,7 +275,7 @@ public async Task GetJobResultAsync(IChannel channel, string jo { if (ActiveWorkspace == null) { - channel.Stderr("Please call %azure.connect before getting job results."); + channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting job results."); return AzureClientError.NotConnected.ToExecutionResult(); } @@ -296,7 +299,7 @@ public async Task GetJobResultAsync(IChannel channel, string jo if (!job.Succeeded || string.IsNullOrEmpty(job.Details.OutputDataUri)) { - channel.Stderr($"Job ID {jobId} has not completed. To check the status, use:\n %azure.status {jobId}"); + channel.Stderr($"Job ID {jobId} has not completed. To check the status, call {GetCommandDisplayName("status")} with the job ID."); return AzureClientError.JobNotCompleted.ToExecutionResult(); } @@ -319,7 +322,7 @@ public async Task GetJobStatusAsync(IChannel channel, string jo { if (ActiveWorkspace == null) { - channel.Stderr("Please call %azure.connect before getting job status."); + channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting job status."); return AzureClientError.NotConnected.ToExecutionResult(); } @@ -349,7 +352,7 @@ public async Task GetJobListAsync(IChannel channel) { if (ActiveWorkspace == null) { - channel.Stderr("Please call %azure.connect before listing jobs."); + channel.Stderr($"Please call {GetCommandDisplayName("connect")} before listing jobs."); return AzureClientError.NotConnected.ToExecutionResult(); } @@ -361,5 +364,8 @@ public async Task GetJobListAsync(IChannel channel) return jobs.ToExecutionResult(); } + + private string GetCommandDisplayName(string commandName) => + IsPythonUserAgent ? $"qsharp.azure.{commandName}()" : $"%azure.{commandName}"; } } diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index f2632e8d32..afbe53c3fb 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -46,7 +46,10 @@ def __init__(self, data: Dict): self.current_availability = data["current_availability"] self.average_queue_time = data["average_queue_time"] - def __eq__(self, other): + def __repr__(self) -> str: + return self.__dict__.__repr__() + + def __eq__(self, other) -> bool: if not isinstance(other, AzureTarget): # don't attempt to compare against unrelated types return NotImplemented @@ -67,7 +70,10 @@ def __init__(self, data: Dict): self.begin_execution_time = data["begin_execution_time"] self.end_execution_time = data["end_execution_time"] - def __eq__(self, other): + def __repr__(self) -> str: + return self.__dict__.__repr__() + + def __eq__(self, other) -> bool: if not isinstance(other, AzureJob): # don't attempt to compare against unrelated types return NotImplemented @@ -83,7 +89,10 @@ def __init__(self, data: Dict): self.error_name = data["error_name"] self.error_description = data["error_description"] - def __eq__(self, other): + def __repr__(self) -> str: + return self.__dict__.__repr__() + + def __eq__(self, other) -> bool: if not isinstance(other, AzureError): # don't attempt to compare against unrelated types return NotImplemented From ca4b4472e77e6b370973064dacce929ce000536c Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 11:48:26 -0700 Subject: [PATCH 38/44] Small change to CloudJob encoder --- src/AzureClient/Visualization/CloudJobEncoders.cs | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/AzureClient/Visualization/CloudJobEncoders.cs b/src/AzureClient/Visualization/CloudJobEncoders.cs index 4d8d9c8a3a..a8e66af5ce 100644 --- a/src/AzureClient/Visualization/CloudJobEncoders.cs +++ b/src/AzureClient/Visualization/CloudJobEncoders.cs @@ -42,10 +42,9 @@ internal static Table ToJupyterTable(this IEnumerable jobsLi Columns = new List<(string, Func)> { // TODO: add cloudJob.Uri after https://github.com/microsoft/qsharp-runtime/issues/236 is fixed. - ("Job ID", cloudJob => cloudJob.Id), ("Job Name", cloudJob => cloudJob.Details.Name), + ("Job ID", cloudJob => cloudJob.Id), ("Job Status", cloudJob => cloudJob.Status), - ("Provider", cloudJob => cloudJob.Details.ProviderId), ("Target", cloudJob => cloudJob.Details.Target), ("Creation Time", cloudJob => cloudJob.Details.CreationTime.ToDateTime()?.ToString() ?? string.Empty), ("Begin Execution Time", cloudJob => cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToString() ?? string.Empty), From 4896d9ae0af8ed54a1b4889a2345606980db256d Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 16:34:11 -0700 Subject: [PATCH 39/44] Add mock Azure classes, improve qsharp.azure Python API, implement end-to-end C# and Python tests (#167) * Mock AzureEnvironment and end-to-end tests * Raise Python exceptions rather than returning error objects * Fix nullable warnings * Minor cleanup * Add missing Dedent() in ConfigMagic * Few improvements to output when using Python --- src/AzureClient/AzureClient.cs | 53 ++-- src/AzureClient/AzureClientError.cs | 93 +++++++ src/AzureClient/AzureEnvironment.cs | 19 +- src/AzureClient/AzureExecutionTarget.cs | 4 +- src/AzureClient/AzureSubmissionContext.cs | 2 +- src/AzureClient/AzureWorkspace.cs | 2 +- src/AzureClient/Extensions.cs | 19 +- src/AzureClient/IAzureClient.cs | 86 +----- src/AzureClient/IAzureWorkspace.cs | 10 +- src/AzureClient/Magic/AzureClientMagicBase.cs | 2 +- src/AzureClient/Magic/ConnectMagic.cs | 2 +- src/AzureClient/Magic/ExecuteMagic.cs | 2 +- src/AzureClient/Magic/JobsMagic.cs | 2 +- src/AzureClient/Magic/OutputMagic.cs | 2 +- src/AzureClient/Magic/StatusMagic.cs | 2 +- src/AzureClient/Magic/SubmitMagic.cs | 2 +- src/AzureClient/Magic/TargetMagic.cs | 2 +- src/AzureClient/Mocks/MockAzureWorkspace.cs | 63 +++++ src/AzureClient/Mocks/MockCloudJob.cs | 38 +++ src/AzureClient/Mocks/MockQuantumMachine.cs | 61 ++++ .../Visualization/AzureClientErrorEncoders.cs | 64 +++++ .../Visualization/CloudJobEncoders.cs | 20 +- .../Visualization/HistogramEncoders.cs | 2 +- .../Visualization/JsonConverters.cs | 5 +- .../Visualization/TargetStatusEncoders.cs | 6 +- src/Kernel/Magic/ConfigMagic.cs | 2 +- src/Python/qsharp/azure.py | 115 ++++++-- src/Python/qsharp/tests/test_azure.py | 109 ++++++++ src/Tests/AzureClientEntryPointTests.cs | 59 ---- src/Tests/AzureClientTests.cs | 263 +++++++++++++++--- 30 files changed, 839 insertions(+), 272 deletions(-) create mode 100644 src/AzureClient/AzureClientError.cs create mode 100644 src/AzureClient/Mocks/MockAzureWorkspace.cs create mode 100644 src/AzureClient/Mocks/MockCloudJob.cs create mode 100644 src/AzureClient/Mocks/MockQuantumMachine.cs create mode 100644 src/AzureClient/Visualization/AzureClientErrorEncoders.cs create mode 100644 src/Python/qsharp/tests/test_azure.py diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index c03ad4a61f..ed059d99f9 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -20,12 +20,14 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class AzureClient : IAzureClient { + internal IAzureWorkspace? ActiveWorkspace { get; private set; } private ILogger Logger { get; } private IReferences References { get; } private IEntryPointGenerator EntryPointGenerator { get; } + private IMetadataController MetadataController { get; } + private bool IsPythonUserAgent => MetadataController?.UserAgent?.StartsWith("qsharp.py") ?? false; private string ConnectionString { get; set; } = string.Empty; private AzureExecutionTarget? ActiveTarget { get; set; } - private IAzureWorkspace? ActiveWorkspace { get; set; } private string MostRecentJobId { get; set; } = string.Empty; private IEnumerable? AvailableProviders { get; set; } private IEnumerable? AvailableTargets => AvailableProviders?.SelectMany(provider => provider.Targets); @@ -39,11 +41,13 @@ public AzureClient( IExecutionEngine engine, IReferences references, IEntryPointGenerator entryPointGenerator, + IMetadataController metadataController, ILogger logger, IEventService eventService) { References = references; EntryPointGenerator = entryPointGenerator; + MetadataController = metadataController; Logger = logger; eventService?.TriggerServiceInitialized(this); @@ -55,6 +59,8 @@ public AzureClient( baseEngine.RegisterDisplayEncoder(new TargetStatusToTextEncoder()); baseEngine.RegisterDisplayEncoder(new HistogramToHtmlEncoder()); baseEngine.RegisterDisplayEncoder(new HistogramToTextEncoder()); + baseEngine.RegisterDisplayEncoder(new AzureClientErrorToHtmlEncoder()); + baseEngine.RegisterDisplayEncoder(new AzureClientErrorToTextEncoder()); } } @@ -83,6 +89,11 @@ public async Task ConnectAsync(IChannel channel, channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}."); + if (ValidExecutionTargets.Count() == 0) + { + channel.Stderr($"No valid Q# execution targets found in Azure Quantum workspace {ActiveWorkspace.Name}."); + } + return ValidExecutionTargets.ToExecutionResult(); } @@ -103,20 +114,19 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, Az { if (ActiveWorkspace == null) { - channel.Stderr("Please call %azure.connect before submitting a job."); + channel.Stderr($"Please call {GetCommandDisplayName("connect")} before submitting a job."); return AzureClientError.NotConnected.ToExecutionResult(); } if (ActiveTarget == null) { - channel.Stderr("Please call %azure.target before submitting a job."); + channel.Stderr($"Please call {GetCommandDisplayName("target")} before submitting a job."); return AzureClientError.NoTarget.ToExecutionResult(); } if (string.IsNullOrEmpty(submissionContext.OperationName)) { - var commandName = execute ? "%azure.execute" : "%azure.submit"; - channel.Stderr($"Please pass a valid Q# operation name to {commandName}."); + channel.Stderr($"Please pass a valid Q# operation name to {GetCommandDisplayName(execute ? "execute" : "submit")}."); return AzureClientError.NoOperationName.ToExecutionResult(); } @@ -206,20 +216,21 @@ public async Task GetActiveTargetAsync(IChannel channel) { if (AvailableProviders == null) { - channel.Stderr("Please call %azure.connect before getting the execution target."); + channel.Stderr($"Please call {GetCommandDisplayName("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.Stderr($"No execution target has been specified. To specify one, call {GetCommandDisplayName("target")} with the target ID."); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); return AzureClientError.NoTarget.ToExecutionResult(); } channel.Stdout($"Current execution target: {ActiveTarget.TargetId}"); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); - return ActiveTarget.TargetId.ToExecutionResult(); + + return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult(); } /// @@ -227,7 +238,7 @@ public async Task SetActiveTargetAsync(IChannel channel, string { if (AvailableProviders == null) { - channel.Stderr("Please call %azure.connect before setting an execution target."); + channel.Stderr($"Please call {GetCommandDisplayName("connect")} before setting an execution target."); return AzureClientError.NotConnected.ToExecutionResult(); } @@ -254,7 +265,9 @@ public async Task SetActiveTargetAsync(IChannel channel, string channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies..."); await References.AddPackage(ActiveTarget.PackageName); - return $"Active target is now {ActiveTarget.TargetId}".ToExecutionResult(); + channel.Stdout($"Active target is now {ActiveTarget.TargetId}"); + + return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult(); } /// @@ -262,7 +275,7 @@ public async Task GetJobResultAsync(IChannel channel, string jo { if (ActiveWorkspace == null) { - channel.Stderr("Please call %azure.connect before getting job results."); + channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting job results."); return AzureClientError.NotConnected.ToExecutionResult(); } @@ -286,7 +299,7 @@ public async Task GetJobResultAsync(IChannel channel, string jo if (!job.Succeeded || string.IsNullOrEmpty(job.Details.OutputDataUri)) { - channel.Stderr($"Job ID {jobId} has not completed. To check the status, use:\n %azure.status {jobId}"); + channel.Stderr($"Job ID {jobId} has not completed. To check the status, call {GetCommandDisplayName("status")} with the job ID."); return AzureClientError.JobNotCompleted.ToExecutionResult(); } @@ -309,7 +322,7 @@ public async Task GetJobStatusAsync(IChannel channel, string jo { if (ActiveWorkspace == null) { - channel.Stderr("Please call %azure.connect before getting job status."); + channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting job status."); return AzureClientError.NotConnected.ToExecutionResult(); } @@ -339,18 +352,20 @@ public async Task GetJobListAsync(IChannel channel) { if (ActiveWorkspace == null) { - channel.Stderr("Please call %azure.connect before listing jobs."); + channel.Stderr($"Please call {GetCommandDisplayName("connect")} before listing jobs."); return AzureClientError.NotConnected.ToExecutionResult(); } - var jobs = await ActiveWorkspace.ListJobsAsync(); - if (jobs == null || jobs.Count() == 0) + var jobs = await ActiveWorkspace.ListJobsAsync() ?? new List(); + if (jobs.Count() == 0) { channel.Stderr("No jobs found in current Azure Quantum workspace."); - return AzureClientError.JobNotFound.ToExecutionResult(); } - + return jobs.ToExecutionResult(); } + + private string GetCommandDisplayName(string commandName) => + IsPythonUserAgent ? $"qsharp.azure.{commandName}()" : $"%azure.{commandName}"; } } diff --git a/src/AzureClient/AzureClientError.cs b/src/AzureClient/AzureClientError.cs new file mode 100644 index 0000000000..a5ac286870 --- /dev/null +++ b/src/AzureClient/AzureClientError.cs @@ -0,0 +1,93 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System.ComponentModel; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + /// + /// Describes possible error results from methods. + /// + public enum AzureClientError + { + /// + /// Method completed with an unknown error. + /// + [Description(Resources.AzureClientErrorUnknownError)] + UnknownError = 1000, + + /// + /// No connection has been made to any Azure Quantum workspace. + /// + [Description(Resources.AzureClientErrorNotConnected)] + NotConnected, + + /// + /// A target has not yet been configured for job submission. + /// + [Description(Resources.AzureClientErrorNoTarget)] + 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, + + /// + /// The result of a job was requested, but the job has not yet completed. + /// + [Description(Resources.AzureClientErrorJobNotCompleted)] + JobNotCompleted, + + /// + /// The job output failed to be downloaded from the Azure storage location. + /// + [Description(Resources.AzureClientErrorJobOutputDownloadFailed)] + JobOutputDownloadFailed, + + /// + /// No Q# operation name was provided where one was required. + /// + [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. + /// + [Description(Resources.AzureClientErrorAuthenticationFailed)] + AuthenticationFailed, + + /// + /// A workspace meeting the specified criteria was not found. + /// + [Description(Resources.AzureClientErrorWorkspaceNotFound)] + WorkspaceNotFound, + } +} diff --git a/src/AzureClient/AzureEnvironment.cs b/src/AzureClient/AzureEnvironment.cs index 729a56ef59..66dc1de62d 100644 --- a/src/AzureClient/AzureEnvironment.cs +++ b/src/AzureClient/AzureEnvironment.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -16,10 +16,11 @@ namespace Microsoft.Quantum.IQSharp.AzureClient { - internal enum AzureEnvironmentType { Production, Canary, Dogfood }; + internal enum AzureEnvironmentType { Production, Canary, Dogfood, Mock }; internal class AzureEnvironment { + public static string EnvironmentVariableName => "AZURE_QUANTUM_ENV"; public AzureEnvironmentType Type { get; private set; } private string SubscriptionId { get; set; } = string.Empty; @@ -34,8 +35,7 @@ private AzureEnvironment() public static AzureEnvironment Create(string subscriptionId) { - var azureEnvironmentEnvVarName = "AZURE_QUANTUM_ENV"; - var azureEnvironmentName = System.Environment.GetEnvironmentVariable(azureEnvironmentEnvVarName); + var azureEnvironmentName = System.Environment.GetEnvironmentVariable(EnvironmentVariableName); if (Enum.TryParse(azureEnvironmentName, true, out AzureEnvironmentType environmentType)) { @@ -47,6 +47,8 @@ public static AzureEnvironment Create(string subscriptionId) return Canary(subscriptionId); case AzureEnvironmentType.Dogfood: return Dogfood(subscriptionId); + case AzureEnvironmentType.Mock: + return Mock(); default: throw new InvalidOperationException("Unexpected EnvironmentType value."); } @@ -57,6 +59,12 @@ public static AzureEnvironment Create(string subscriptionId) public async Task GetAuthenticatedWorkspaceAsync(IChannel channel, string resourceGroupName, string workspaceName, bool refreshCredentials) { + if (Type == AzureEnvironmentType.Mock) + { + channel.Stdout("AZURE_QUANTUM_ENV set to Mock. Using mock Azure workspace rather than connecting to the real service."); + return new MockAzureWorkspace(workspaceName); + } + // Find the token cache folder var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE"; var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName); @@ -154,6 +162,9 @@ private static AzureEnvironment Canary(string subscriptionId) return canary; } + private static AzureEnvironment Mock() => + new AzureEnvironment() { Type = AzureEnvironmentType.Mock }; + private static string GetDogfoodAuthority(string subscriptionId) { try diff --git a/src/AzureClient/AzureExecutionTarget.cs b/src/AzureClient/AzureExecutionTarget.cs index d56064efa5..f2cebf24f0 100644 --- a/src/AzureClient/AzureExecutionTarget.cs +++ b/src/AzureClient/AzureExecutionTarget.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -11,7 +11,7 @@ internal enum AzureProvider { IonQ, Honeywell, QCI } internal class AzureExecutionTarget { - public string TargetId { get; private set; } + public string TargetId { get; private set; } = string.Empty; public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetId)}"; public static bool IsValid(string targetId) => GetProvider(targetId) != null; diff --git a/src/AzureClient/AzureSubmissionContext.cs b/src/AzureClient/AzureSubmissionContext.cs index bfe023e9fc..f299d90694 100644 --- a/src/AzureClient/AzureSubmissionContext.cs +++ b/src/AzureClient/AzureSubmissionContext.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/AzureWorkspace.cs b/src/AzureClient/AzureWorkspace.cs index 54fb782abc..a5083c442b 100644 --- a/src/AzureClient/AzureWorkspace.cs +++ b/src/AzureClient/AzureWorkspace.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/Extensions.cs b/src/AzureClient/Extensions.cs index 1fda56014e..6e4cc96c56 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -37,24 +37,9 @@ internal static ExecutionResult ToExecutionResult(this AzureClientError azureCli new ExecutionResult { Status = ExecuteStatus.Error, - Output = azureClientError.ToDescription() + Output = azureClientError, }; - /// - /// Returns the string value of the for the given - /// enumeration value. - /// - /// - /// - internal static string ToDescription(this AzureClientError azureClientError) - { - var attributes = azureClientError - .GetType() - .GetField(azureClientError.ToString()) - .GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[]; - return attributes?.Length > 0 ? attributes[0].Description : string.Empty; - } - /// /// Encapsulates a given as the result of an execution. /// diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index d6c67678ff..441abc7bff 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -10,90 +10,6 @@ namespace Microsoft.Quantum.IQSharp.AzureClient { - /// - /// Describes possible error results from methods. - /// - public enum AzureClientError - { - /// - /// Method completed with an unknown error. - /// - [Description(Resources.AzureClientErrorUnknownError)] - UnknownError, - - /// - /// No connection has been made to any Azure Quantum workspace. - /// - [Description(Resources.AzureClientErrorNotConnected)] - NotConnected, - - /// - /// A target has not yet been configured for job submission. - /// - [Description(Resources.AzureClientErrorNoTarget)] - 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, - - /// - /// The result of a job was requested, but the job has not yet completed. - /// - [Description(Resources.AzureClientErrorJobNotCompleted)] - JobNotCompleted, - - /// - /// The job output failed to be downloaded from the Azure storage location. - /// - [Description(Resources.AzureClientErrorJobOutputDownloadFailed)] - JobOutputDownloadFailed, - - /// - /// No Q# operation name was provided where one was required. - /// - [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. - /// - [Description(Resources.AzureClientErrorAuthenticationFailed)] - AuthenticationFailed, - - /// - /// A workspace meeting the specified criteria was not found. - /// - [Description(Resources.AzureClientErrorWorkspaceNotFound)] - WorkspaceNotFound, - } - /// /// This service is capable of connecting to Azure Quantum workspaces /// and submitting jobs. diff --git a/src/AzureClient/IAzureWorkspace.cs b/src/AzureClient/IAzureWorkspace.cs index a08152b3b7..1c2c71b813 100644 --- a/src/AzureClient/IAzureWorkspace.cs +++ b/src/AzureClient/IAzureWorkspace.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -13,11 +13,11 @@ namespace Microsoft.Quantum.IQSharp.AzureClient { internal interface IAzureWorkspace { - public string Name { get; } + public string? Name { get; } - public Task> GetProvidersAsync(); - public Task GetJobAsync(string jobId); - public Task> ListJobsAsync(); + public Task?> GetProvidersAsync(); + public Task GetJobAsync(string jobId); + public Task?> ListJobsAsync(); public IQuantumMachine? CreateQuantumMachine(string targetId, string storageAccountConnectionString); } } \ No newline at end of file diff --git a/src/AzureClient/Magic/AzureClientMagicBase.cs b/src/AzureClient/Magic/AzureClientMagicBase.cs index f7eac3b5ce..823554c48f 100644 --- a/src/AzureClient/Magic/AzureClientMagicBase.cs +++ b/src/AzureClient/Magic/AzureClientMagicBase.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/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 559c04e3e7..ffdceff925 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.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/Magic/ExecuteMagic.cs b/src/AzureClient/Magic/ExecuteMagic.cs index 6a3080d274..122882abe5 100644 --- a/src/AzureClient/Magic/ExecuteMagic.cs +++ b/src/AzureClient/Magic/ExecuteMagic.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/Magic/JobsMagic.cs b/src/AzureClient/Magic/JobsMagic.cs index f23708d9ea..590e77b418 100644 --- a/src/AzureClient/Magic/JobsMagic.cs +++ b/src/AzureClient/Magic/JobsMagic.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/Magic/OutputMagic.cs b/src/AzureClient/Magic/OutputMagic.cs index 17ca257a46..dd542d7329 100644 --- a/src/AzureClient/Magic/OutputMagic.cs +++ b/src/AzureClient/Magic/OutputMagic.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/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs index e7eaa56d6c..80a2a85733 100644 --- a/src/AzureClient/Magic/StatusMagic.cs +++ b/src/AzureClient/Magic/StatusMagic.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/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index 516646382e..3c7bcfccdd 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.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/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 778f88ab74..d0c93be7e1 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.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/Mocks/MockAzureWorkspace.cs b/src/AzureClient/Mocks/MockAzureWorkspace.cs new file mode 100644 index 0000000000..212d605714 --- /dev/null +++ b/src/AzureClient/Mocks/MockAzureWorkspace.cs @@ -0,0 +1,63 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.Quantum; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Quantum.Runtime; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class MockAzureWorkspace : IAzureWorkspace + { + public const string NameWithMockProviders = "WorkspaceNameWithMockProviders"; + + public string Name { get; private set; } + + public List Providers { get; } = new List(); + + public List Jobs { get; } = new List(); + + public MockAzureWorkspace(string workspaceName) + { + Name = workspaceName; + if (Name == NameWithMockProviders) + { + // add a mock target for each provider: "ionq.mock", "honeywell.mock", etc. + AddMockTargets( + Enum.GetNames(typeof(AzureProvider)) + .Select(provider => $"{provider.ToLowerInvariant()}.mock") + .ToArray()); + } + } + + public async Task GetJobAsync(string jobId) => Jobs.FirstOrDefault(job => job.Id == jobId); + + public async Task?> GetProvidersAsync() => Providers; + + public async Task?> ListJobsAsync() => Jobs; + + public IQuantumMachine? CreateQuantumMachine(string targetId, string storageAccountConnectionString) => new MockQuantumMachine(this); + + public void AddMockJobs(params string[] jobIds) + { + foreach (var jobId in jobIds) + { + var mockJob = new MockCloudJob(); + mockJob.Details.Id = jobId; + Jobs.Add(mockJob); + } + } + + public void AddMockTargets(params string[] targetIds) + { + var targets = targetIds.Select(id => new TargetStatus(id)).ToList(); + Providers.Add(new ProviderStatus(null, null, targets)); + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Mocks/MockCloudJob.cs b/src/AzureClient/Mocks/MockCloudJob.cs new file mode 100644 index 0000000000..73d6cc789d --- /dev/null +++ b/src/AzureClient/Mocks/MockCloudJob.cs @@ -0,0 +1,38 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using Microsoft.Azure.Quantum; +using Microsoft.Azure.Quantum.Client.Models; +using System; +using System.IO; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class MockCloudJob : CloudJob + { + public MockCloudJob() + : base( + new Azure.Quantum.Workspace("mockSubscriptionId", "mockResourceGroupName", "mockWorkspaceName"), + new JobDetails( + containerUri: null, + inputDataFormat: null, + providerId: null, + target: null, + id: Guid.NewGuid().ToString(), + status: "Succeeded", + outputDataUri: CreateMockOutputFileUri() + )) + { + } + + private static string CreateMockOutputFileUri() + { + var tempFilePath = Path.GetTempFileName(); + using var outputFile = new StreamWriter(tempFilePath); + outputFile.WriteLine(@"{'Histogram':['0',0.5,'1',0.5]}"); + return new Uri(tempFilePath).AbsoluteUri; + } + } +} \ No newline at end of file diff --git a/src/AzureClient/Mocks/MockQuantumMachine.cs b/src/AzureClient/Mocks/MockQuantumMachine.cs new file mode 100644 index 0000000000..9e6c28411f --- /dev/null +++ b/src/AzureClient/Mocks/MockQuantumMachine.cs @@ -0,0 +1,61 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Threading; +using System.Threading.Tasks; +using Microsoft.Quantum.Runtime; +using Microsoft.Quantum.Simulation.Core; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class MockQuantumMachine : IQuantumMachine + { + public string ProviderId => throw new NotImplementedException(); + + public string Target => throw new NotImplementedException(); + + private MockAzureWorkspace? Workspace { get; } + + public MockQuantumMachine(MockAzureWorkspace? workspace = null) => Workspace = workspace; + + 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, executionContext, null); + + public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext? executionContext, IQuantumMachine.ConfigureJob? configureJobCallback) + => ExecuteAsync(info, input, null, executionContext); + + 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, submissionContext, null); + + public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext? submissionContext, IQuantumMachine.ConfigureJob? configureJobCallback) + { + var job = new MockCloudJob(); + Workspace?.AddMockJobs(job.Id); + return Task.FromResult(job as IQuantumMachineJob); + } + + public (bool IsValid, string Message) Validate(EntryPointInfo info, TInput input) + => throw new NotImplementedException(); + } +} \ No newline at end of file diff --git a/src/AzureClient/Visualization/AzureClientErrorEncoders.cs b/src/AzureClient/Visualization/AzureClientErrorEncoders.cs new file mode 100644 index 0000000000..4848d7a8a3 --- /dev/null +++ b/src/AzureClient/Visualization/AzureClientErrorEncoders.cs @@ -0,0 +1,64 @@ +// Copyright (c) Microsoft Corporation. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.ComponentModel; +using Microsoft.Jupyter.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal static class AzureClientErrorExtensions + { + /// + /// Returns the string value of the for the given + /// enumeration value. + /// + internal static string ToDescription(this AzureClientError error) + { + var attributes = error + .GetType() + .GetField(error.ToString()) + .GetCustomAttributes(typeof(DescriptionAttribute), false) as DescriptionAttribute[]; + return attributes?.Length > 0 ? attributes[0].Description : string.Empty; + } + + /// + /// Returns a dictionary representing the properties of the . + /// + internal static Dictionary ToDictionary(this AzureClientError error) => + new Dictionary() + { + ["error_code"] = System.Convert.ToInt32(error), + ["error_name"] = error.ToString(), + ["error_description"] = error.ToDescription(), + }; + } + + public class AzureClientErrorToHtmlEncoder : IResultEncoder + { + public string MimeType => MimeTypes.Html; + + public EncodedData? Encode(object displayable) => (displayable as AzureClientError?)?.ToDescription().ToEncodedData(); + } + + public class AzureClientErrorToTextEncoder : IResultEncoder + { + public string MimeType => MimeTypes.PlainText; + + public EncodedData? Encode(object displayable) => (displayable as AzureClientError?)?.ToDescription().ToEncodedData(); + } + + public class AzureClientErrorJsonConverter : JsonConverter + { + public override AzureClientError ReadJson(JsonReader reader, Type objectType, AzureClientError existingValue, bool hasExistingValue, JsonSerializer serializer) => + throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, AzureClientError value, JsonSerializer serializer) => + JToken.FromObject(value.ToDictionary()).WriteTo(writer); + } +} diff --git a/src/AzureClient/Visualization/CloudJobEncoders.cs b/src/AzureClient/Visualization/CloudJobEncoders.cs index 8e5f9b5610..4d8d9c8a3a 100644 --- a/src/AzureClient/Visualization/CloudJobEncoders.cs +++ b/src/AzureClient/Visualization/CloudJobEncoders.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -22,8 +22,8 @@ internal static class CloudJobExtensions ? dateTime : null as DateTime?; - internal static Dictionary ToDictionary(this CloudJob cloudJob) => - new Dictionary() + internal static Dictionary ToDictionary(this CloudJob cloudJob) => + new Dictionary() { // TODO: add cloudJob.Uri after https://github.com/microsoft/qsharp-runtime/issues/236 is fixed. ["id"] = cloudJob.Id, @@ -31,9 +31,9 @@ internal static Dictionary ToDictionary(this CloudJob cloudJob) ["status"] = cloudJob.Status, ["provider"] = cloudJob.Details.ProviderId, ["target"] = cloudJob.Details.Target, - ["creationTime"] = cloudJob.Details.CreationTime.ToDateTime()?.ToUniversalTime(), - ["beginExecutionTime"] = cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToUniversalTime(), - ["endExecutionTime"] = cloudJob.Details.EndExecutionTime.ToDateTime()?.ToUniversalTime(), + ["creation_time"] = cloudJob.Details.CreationTime.ToDateTime()?.ToUniversalTime(), + ["begin_execution_time"] = cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToUniversalTime(), + ["end_execution_time"] = cloudJob.Details.EndExecutionTime.ToDateTime()?.ToUniversalTime(), }; internal static Table ToJupyterTable(this IEnumerable jobsList) => @@ -47,11 +47,11 @@ internal static Table ToJupyterTable(this IEnumerable jobsLi ("Job Status", cloudJob => cloudJob.Status), ("Provider", cloudJob => cloudJob.Details.ProviderId), ("Target", cloudJob => cloudJob.Details.Target), - ("Creation Time", cloudJob => cloudJob.Details.CreationTime.ToDateTime()?.ToString()), - ("Begin Execution Time", cloudJob => cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToString()), - ("End Execution Time", cloudJob => cloudJob.Details.EndExecutionTime.ToDateTime()?.ToString()), + ("Creation Time", cloudJob => cloudJob.Details.CreationTime.ToDateTime()?.ToString() ?? string.Empty), + ("Begin Execution Time", cloudJob => cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToString() ?? string.Empty), + ("End Execution Time", cloudJob => cloudJob.Details.EndExecutionTime.ToDateTime()?.ToString() ?? string.Empty), }, - Rows = jobsList.OrderByDescending(job => job.Details.CreationTime).ToList() + Rows = jobsList.OrderByDescending(job => job.Details.CreationTime).ToList(), }; } diff --git a/src/AzureClient/Visualization/HistogramEncoders.cs b/src/AzureClient/Visualization/HistogramEncoders.cs index befbbf095b..41f3f0698b 100644 --- a/src/AzureClient/Visualization/HistogramEncoders.cs +++ b/src/AzureClient/Visualization/HistogramEncoders.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/Visualization/JsonConverters.cs b/src/AzureClient/Visualization/JsonConverters.cs index 24f26f2515..394370155a 100644 --- a/src/AzureClient/Visualization/JsonConverters.cs +++ b/src/AzureClient/Visualization/JsonConverters.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -19,7 +19,8 @@ public static class JsonConverters new CloudJobJsonConverter(), new CloudJobListJsonConverter(), new TargetStatusJsonConverter(), - new TargetStatusListJsonConverter() + new TargetStatusListJsonConverter(), + new AzureClientErrorJsonConverter() ); public static JsonConverter[] AllConverters => allConverters.ToArray(); diff --git a/src/AzureClient/Visualization/TargetStatusEncoders.cs b/src/AzureClient/Visualization/TargetStatusEncoders.cs index 1de24e7bc0..6925163fcb 100644 --- a/src/AzureClient/Visualization/TargetStatusEncoders.cs +++ b/src/AzureClient/Visualization/TargetStatusEncoders.cs @@ -1,4 +1,4 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable @@ -20,8 +20,8 @@ internal static Dictionary ToDictionary(this TargetStatus target new Dictionary() { ["id"] = target.Id, - ["currentAvailability"] = target.CurrentAvailability, - ["averageQueueTime"] = target.AverageQueueTime, + ["current_availability"] = target.CurrentAvailability, + ["average_queue_time"] = target.AverageQueueTime, }; internal static Table ToJupyterTable(this IEnumerable targets) => diff --git a/src/Kernel/Magic/ConfigMagic.cs b/src/Kernel/Magic/ConfigMagic.cs index 6a020af780..586a8a6cbb 100644 --- a/src/Kernel/Magic/ConfigMagic.cs +++ b/src/Kernel/Magic/ConfigMagic.cs @@ -45,7 +45,7 @@ save those options to a JSON file in the current working dump.basisStateLabelingConvention ""BigEndian"" dump.truncateSmallAmplitudes true ``` - ", + ".Dedent(), @" Configure the `DumpMachine` and `DumpRegister` callables diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index f69fb83a00..afbe53c3fb 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -11,11 +11,7 @@ import qsharp import json -import typing -from typing import List, Dict, Callable, Any - -from qsharp.serialization import map_tuples -from typing import List, Tuple, Dict, Iterable +from typing import List, Dict, Callable, Any, Union from enum import Enum ## LOGGING ## @@ -33,27 +29,108 @@ 'status', 'output', 'jobs' + 'AzureTarget', + 'AzureJob', + 'AzureError' ] +## CLASSES ## + +class AzureTarget(object): + """ + Represents an instance of an Azure Quantum execution target for Q# job submission. + """ + def __init__(self, data: Dict): + self.__dict__ = data + self.id = data["id"] + self.current_availability = data["current_availability"] + self.average_queue_time = data["average_queue_time"] + + def __repr__(self) -> str: + return self.__dict__.__repr__() + + def __eq__(self, other) -> bool: + if not isinstance(other, AzureTarget): + # don't attempt to compare against unrelated types + return NotImplemented + return self.__dict__ == other.__dict__ + +class AzureJob(object): + """ + Represents an instance of an Azure Quantum job. + """ + def __init__(self, data: Dict): + self.__dict__ = data + self.id = data["id"] + self.name = data["name"] + self.status = data["status"] + self.provider = data["provider"] + self.target = data["target"] + self.creation_time = data["creation_time"] + self.begin_execution_time = data["begin_execution_time"] + self.end_execution_time = data["end_execution_time"] + + def __repr__(self) -> str: + return self.__dict__.__repr__() + + def __eq__(self, other) -> bool: + if not isinstance(other, AzureJob): + # don't attempt to compare against unrelated types + return NotImplemented + return self.__dict__ == other.__dict__ + +class AzureError(Exception): + """ + Contains error information resulting from an attempt to interact with Azure. + """ + def __init__(self, data: Dict): + self.__dict__ = data + self.error_code = data["error_code"] + self.error_name = data["error_name"] + self.error_description = data["error_description"] + + def __repr__(self) -> str: + return self.__dict__.__repr__() + + def __eq__(self, other) -> bool: + if not isinstance(other, AzureError): + # don't attempt to compare against unrelated types + return NotImplemented + return self.__dict__ == other.__dict__ + ## FUNCTIONS ## -def connect(**params) -> Any: - return qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params) +def connect(**params) -> List[AzureTarget]: + result = qsharp.client._execute_magic(f"azure.connect", raise_on_stderr=False, **params) + if "error_code" in result: raise AzureError(result) + return [AzureTarget(target) for target in result] -def target(name : str = '', **params) -> Any: - return qsharp.client._execute_magic(f"azure.target {name}", raise_on_stderr=False, **params) +def target(name : str = '', **params) -> AzureTarget: + result = qsharp.client._execute_magic(f"azure.target {name}", raise_on_stderr=False, **params) + if "error_code" in result: raise AzureError(result) + return AzureTarget(result) -def submit(op, **params) -> Any: - return qsharp.client._execute_callable_magic("azure.submit", op, raise_on_stderr=False, **params) +def submit(op, **params) -> AzureJob: + result = qsharp.client._execute_callable_magic("azure.submit", op, raise_on_stderr=False, **params) + if "error_code" in result: raise AzureError(result) + return AzureJob(result) -def execute(op, **params) -> Any: - return qsharp.client._execute_callable_magic("azure.execute", op, raise_on_stderr=False, **params) +def execute(op, **params) -> Dict: + result = qsharp.client._execute_callable_magic("azure.execute", op, raise_on_stderr=False, **params) + if "error_code" in result: raise AzureError(result) + return result -def status(jobId : str = '', **params) -> Any: - return qsharp.client._execute_magic(f"azure.status {jobId}", raise_on_stderr=False, **params) +def status(jobId : str = '', **params) -> AzureJob: + result = qsharp.client._execute_magic(f"azure.status {jobId}", raise_on_stderr=False, **params) + if "error_code" in result: raise AzureError(result) + return AzureJob(result) -def output(jobId : str = '', **params) -> Any: - return qsharp.client._execute_magic(f"azure.output {jobId}", raise_on_stderr=False, **params) +def output(jobId : str = '', **params) -> Dict: + result = qsharp.client._execute_magic(f"azure.output {jobId}", raise_on_stderr=False, **params) + if "error_code" in result: raise AzureError(result) + return result -def jobs(**params) -> Any: - return qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params) +def jobs(**params) -> List[AzureJob]: + result = qsharp.client._execute_magic(f"azure.jobs", raise_on_stderr=False, **params) + if "error_code" in result: raise AzureError(result) + return [AzureJob(job) for job in result] diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py new file mode 100644 index 0000000000..f626a759c6 --- /dev/null +++ b/src/Python/qsharp/tests/test_azure.py @@ -0,0 +1,109 @@ +#!/bin/env python +# -*- coding: utf-8 -*- +## +# test_azure.py: Tests Azure Quantum functionality against a mock workspace. +## +# Copyright (c) Microsoft Corporation. +# Licensed under the MIT License. +## + +## IMPORTS ## + +import importlib +import os +import pytest +import qsharp +from qsharp.azure import AzureError, AzureJob, AzureTarget +import sys + +## SETUP ## + +@pytest.fixture(scope="session", autouse=True) +def set_environment_variables(): + # Need to restart the IQ# kernel after setting the environment variable + os.environ["AZURE_QUANTUM_ENV"] = "mock" + importlib.reload(qsharp) + if "qsharp.chemistry" in sys.modules: + importlib.reload(qsharp.chemistry) + +## TESTS ## + +def test_empty_workspace(): + """ + Tests behavior of a mock workspace with no providers. + """ + with pytest.raises(AzureError) as exception_info: + qsharp.azure.target() + assert exception_info.value.error_name == "NotConnected" + + targets = qsharp.azure.connect( + storageAccountConnectionString="test", + subscriptionId="test", + resourceGroupName="test", + workspaceName="test" + ) + assert targets == [] + + with pytest.raises(AzureError) as exception_info: + qsharp.azure.target("invalid.target") + assert exception_info.value.error_name == "InvalidTarget" + + jobs = qsharp.azure.jobs() + assert jobs == [] + +def test_workspace_with_providers(): + """ + Tests behavior of a mock workspace with mock providers. + """ + targets = qsharp.azure.connect( + storageAccountConnectionString="test", + subscriptionId="test", + resourceGroupName="test", + workspaceName="WorkspaceNameWithMockProviders" + ) + assert isinstance(targets, list) + assert len(targets) > 0 + + with pytest.raises(AzureError) as exception_info: + qsharp.azure.target() + assert exception_info.value.error_name == "NoTarget" + + for target in targets: + active_target = qsharp.azure.target(target.id) + assert isinstance(active_target, AzureTarget) + assert active_target == target + + # Submit a snippet operation without parameters + op = qsharp.compile(""" + operation HelloQ() : Result + { + Message($"Hello from quantum world!"); + return Zero; + } + """) + + job = qsharp.azure.submit(op) + assert isinstance(job, AzureJob) + + retrieved_job = qsharp.azure.status(job.id) + assert isinstance(retrieved_job, AzureJob) + assert job.id == retrieved_job.id + + # Execute a workspace operation with parameters + op = qsharp.QSharpCallable("Microsoft.Quantum.SanityTests.HelloAgain", None) + + with pytest.raises(AzureError) as exception_info: + qsharp.azure.execute(op) + assert exception_info.value.error_name == "JobSubmissionFailed" + + histogram = qsharp.azure.execute(op, count=3, name="test") + assert isinstance(histogram, dict) + + retrieved_histogram = qsharp.azure.output() + assert isinstance(retrieved_histogram, dict) + assert histogram == retrieved_histogram + + # Check that both submitted jobs exist in the workspace + jobs = qsharp.azure.jobs() + assert isinstance(jobs, list) + assert len(jobs) == 2 diff --git a/src/Tests/AzureClientEntryPointTests.cs b/src/Tests/AzureClientEntryPointTests.cs index 48d0416c61..a4a121c994 100644 --- a/src/Tests/AzureClientEntryPointTests.cs +++ b/src/Tests/AzureClientEntryPointTests.cs @@ -132,63 +132,4 @@ public async Task InvalidEntryPointOperation() 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, executionContext, null); - - public Task> ExecuteAsync(EntryPointInfo info, TInput input, IQuantumMachineExecutionContext executionContext, IQuantumMachine.ConfigureJob configureJobCallback) - => ExecuteAsync(info, input, null, executionContext); - - 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, submissionContext, null); - - public Task SubmitAsync(EntryPointInfo info, TInput input, IQuantumMachineSubmissionContext submissionContext, IQuantumMachine.ConfigureJob configureJobCallback) - => 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(); - } } diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index a115085104..099c251268 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -3,47 +3,84 @@ #nullable enable -using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Collections.Generic; using System.Linq; +using System.Threading.Tasks; +using Microsoft.Azure.Quantum; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Extensions.DependencyInjection; using Microsoft.Jupyter.Core; -using Microsoft.Quantum.IQSharp; using Microsoft.Quantum.IQSharp.AzureClient; -using Microsoft.Extensions.DependencyInjection; +using Microsoft.VisualStudio.TestTools.UnitTesting; namespace Tests.IQSharp { - public static class AzureClientTestExtensions - { - } - [TestClass] public class AzureClientTests { - private readonly string subscriptionId = "TEST_SUBSCRIPTION_ID"; - private readonly string resourceGroupName = "TEST_RESOURCE_GROUP_NAME"; - private readonly string workspaceName = "TEST_WORKSPACE_NAME"; - private readonly string storageAccountConnectionString = "TEST_CONNECTION_STRING"; - private readonly string jobId = "TEST_JOB_ID"; - private readonly string operationName = "TEST_OPERATION_NAME"; + private string originalEnvironmentName = string.Empty; + + [TestInitialize] + public void SetMockEnvironment() + { + originalEnvironmentName = Environment.GetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName) ?? string.Empty; + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Mock.ToString()); + } + + [TestCleanup] + public void RestoreEnvironment() + { + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, originalEnvironmentName); + } + + private T ExpectSuccess(Task task) + { + var result = task.GetAwaiter().GetResult(); + Assert.AreEqual(ExecuteStatus.Ok, result.Status); + Assert.IsInstanceOfType(result.Output, typeof(T)); + return (T)result.Output; + } + + private void ExpectError(AzureClientError expectedError, Task task) + { + var result = task.GetAwaiter().GetResult(); + Assert.AreEqual(ExecuteStatus.Error, result.Status); + Assert.IsInstanceOfType(result.Output, typeof(AzureClientError)); + Assert.AreEqual(expectedError, (AzureClientError)result.Output); + } + + private Task ConnectToWorkspaceAsync(IAzureClient azureClient, string workspaceName = "TEST_WORKSPACE_NAME") + { + return azureClient.ConnectAsync( + new MockChannel(), + "TEST_SUBSCRIPTION_ID", + "TEST_RESOURCE_GROUP_NAME", + workspaceName, + "TEST_CONNECTION_STRING"); + } [TestMethod] - public void TestTargets() + public void TestAzureEnvironment() { - var workspace = "Workspace"; - var services = Startup.CreateServiceProvider(workspace); - var azureClient = services.GetService(); + // Production environment + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Production.ToString()); + var environment = AzureEnvironment.Create("TEST_SUBSCRIPTION_ID"); + Assert.AreEqual(AzureEnvironmentType.Production, environment.Type); - // SetActiveTargetAsync with recognized target ID, but not yet connected - var result = azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator").GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == ExecuteStatus.Error); + // Dogfood environment cannot be created in test because it requires a service call + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Dogfood.ToString()); + Assert.ThrowsException(() => AzureEnvironment.Create("TEST_SUBSCRIPTION_ID")); - // SetActiveTargetAsync with unrecognized target ID - result = azureClient.SetActiveTargetAsync(new MockChannel(), "contoso.qpu").GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == ExecuteStatus.Error); + // Canary environment + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Canary.ToString()); + environment = AzureEnvironment.Create("TEST_SUBSCRIPTION_ID"); + Assert.AreEqual(AzureEnvironmentType.Canary, environment.Type); - // GetActiveTargetAsync, but not yet connected - result = azureClient.GetActiveTargetAsync(new MockChannel()).GetAwaiter().GetResult(); - Assert.IsTrue(result.Status == ExecuteStatus.Error); + // Mock environment + Environment.SetEnvironmentVariable(AzureEnvironment.EnvironmentVariableName, AzureEnvironmentType.Mock.ToString()); + environment = AzureEnvironment.Create("TEST_SUBSCRIPTION_ID"); + Assert.AreEqual(AzureEnvironmentType.Mock, environment.Type); } [TestMethod] @@ -55,21 +92,177 @@ public void TestAzureExecutionTarget() targetId = "ionq.targetId"; executionTarget = AzureExecutionTarget.Create(targetId); - Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetId, targetId); - Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.IonQ"); + Assert.AreEqual(targetId, executionTarget?.TargetId); + Assert.AreEqual("Microsoft.Quantum.Providers.IonQ", executionTarget?.PackageName); targetId = "HonEYWEll.targetId"; executionTarget = AzureExecutionTarget.Create(targetId); - Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetId, targetId); - Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.Honeywell"); + Assert.AreEqual(targetId, executionTarget?.TargetId); + Assert.AreEqual("Microsoft.Quantum.Providers.Honeywell", executionTarget?.PackageName); targetId = "qci.target.name.qpu"; executionTarget = AzureExecutionTarget.Create(targetId); - Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetId, targetId); - Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.QCI"); + Assert.AreEqual(targetId, executionTarget?.TargetId); + Assert.AreEqual("Microsoft.Quantum.Providers.QCI", executionTarget?.PackageName); + } + + [TestMethod] + public void TestJobStatus() + { + var services = Startup.CreateServiceProvider("Workspace"); + var azureClient = (AzureClient)services.GetService(); + + // not connected + ExpectError(AzureClientError.NotConnected, azureClient.GetJobStatusAsync(new MockChannel(), "JOB_ID_1")); + + // connect + var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient)); + Assert.IsFalse(targets.Any()); + + // set up the mock workspace + var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace; + Assert.IsNotNull(azureWorkspace); + azureWorkspace?.AddMockJobs("JOB_ID_1", "JOB_ID_2"); + + // valid job ID + var job = ExpectSuccess(azureClient.GetJobStatusAsync(new MockChannel(), "JOB_ID_1")); + Assert.AreEqual("JOB_ID_1", job.Id); + + // invalid job ID + ExpectError(AzureClientError.JobNotFound, azureClient.GetJobStatusAsync(new MockChannel(), "JOB_ID_3")); + + // jobs list + var jobs = ExpectSuccess>(azureClient.GetJobListAsync(new MockChannel())); + Assert.AreEqual(2, jobs.Count()); + } + + [TestMethod] + public void TestManualTargets() + { + var services = Startup.CreateServiceProvider("Workspace"); + var azureClient = (AzureClient)services.GetService(); + + // SetActiveTargetAsync with recognized target ID, but not yet connected + ExpectError(AzureClientError.NotConnected, azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator")); + + // GetActiveTargetAsync, but not yet connected + ExpectError(AzureClientError.NotConnected, azureClient.GetActiveTargetAsync(new MockChannel())); + + // connect + var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient)); + Assert.IsFalse(targets.Any()); + + // set up the mock workspace + var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace; + Assert.IsNotNull(azureWorkspace); + azureWorkspace?.AddMockTargets("ionq.simulator", "honeywell.qpu", "unrecognized.target"); + + // get connection status to verify list of targets + targets = ExpectSuccess>(azureClient.GetConnectionStatusAsync(new MockChannel())); + Assert.AreEqual(2, targets.Count()); // only 2 valid quantum execution targets + + // GetActiveTargetAsync, but no active target set yet + ExpectError(AzureClientError.NoTarget, azureClient.GetActiveTargetAsync(new MockChannel())); + + // SetActiveTargetAsync with target ID not valid for quantum execution + ExpectError(AzureClientError.InvalidTarget, azureClient.SetActiveTargetAsync(new MockChannel(), "unrecognized.target")); + + // SetActiveTargetAsync with valid target ID + var target = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator")); + Assert.AreEqual("ionq.simulator", target.Id); + + // GetActiveTargetAsync + target = ExpectSuccess(azureClient.GetActiveTargetAsync(new MockChannel())); + Assert.AreEqual("ionq.simulator", target.Id); + } + + [TestMethod] + public void TestAllTargets() + { + var services = Startup.CreateServiceProvider("Workspace"); + var azureClient = (AzureClient)services.GetService(); + + // connect to mock workspace with all providers + var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient, MockAzureWorkspace.NameWithMockProviders)); + Assert.AreEqual(Enum.GetNames(typeof(AzureProvider)).Length, targets.Count()); + + // set each target, which will load the corresponding package + foreach (var target in targets) + { + var returnedTarget = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), target.Id)); + Assert.AreEqual(target.Id, returnedTarget.Id); + } + } + + [TestMethod] + public void TestJobSubmission() + { + var services = Startup.CreateServiceProvider("Workspace"); + var azureClient = (AzureClient)services.GetService(); + var submissionContext = new AzureSubmissionContext(); + + // not yet connected + ExpectError(AzureClientError.NotConnected, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + + // connect + var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient)); + Assert.IsFalse(targets.Any()); + + // no target yet + ExpectError(AzureClientError.NoTarget, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + + // add a target + var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace; + Assert.IsNotNull(azureWorkspace); + azureWorkspace?.AddMockTargets("ionq.simulator"); + + // set the active target + var target = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator")); + Assert.AreEqual("ionq.simulator", target.Id); + + // no operation name specified + ExpectError(AzureClientError.NoOperationName, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + + // specify an operation name, but have missing parameters + submissionContext.OperationName = "Tests.qss.HelloAgain"; + ExpectError(AzureClientError.JobSubmissionFailed, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + + // specify input parameters and verify that the job was submitted + submissionContext.InputParameters = new Dictionary() { ["count"] = "3", ["name"] = "testing" }; + var job = ExpectSuccess(azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + var retrievedJob = ExpectSuccess(azureClient.GetJobStatusAsync(new MockChannel(), job.Id)); + Assert.AreEqual(job.Id, retrievedJob.Id); + } + + [TestMethod] + public void TestJobExecution() + { + var services = Startup.CreateServiceProvider("Workspace"); + var azureClient = (AzureClient)services.GetService(); + + // connect + var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient)); + Assert.IsFalse(targets.Any()); + + // add a target + var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace; + Assert.IsNotNull(azureWorkspace); + azureWorkspace?.AddMockTargets("ionq.simulator"); + + // set the active target + var target = ExpectSuccess(azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator")); + Assert.AreEqual("ionq.simulator", target.Id); + + // execute the job and verify that the results are retrieved successfully + var submissionContext = new AzureSubmissionContext() + { + OperationName = "Tests.qss.HelloAgain", + InputParameters = new Dictionary() { ["count"] = "3", ["name"] = "testing" }, + ExecutionTimeout = 5, + ExecutionPollingInterval = 1, + }; + var histogram = ExpectSuccess(azureClient.ExecuteJobAsync(new MockChannel(), submissionContext)); + Assert.IsNotNull(histogram); } } } From 66742a955790fdd8dbe07b354831e711766d5b35 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 18:30:39 -0700 Subject: [PATCH 40/44] Support kernel interrupt for %azure.execute (#176) * Mock AzureEnvironment and end-to-end tests * Raise Python exceptions rather than returning error objects * Fix nullable warnings * Minor cleanup * Add missing Dedent() in ConfigMagic * Support kernel interrupt during job execution * Add #nullable enable * Properly dispose CancellationTokenSource * Consume CancellationToken functionality from jupyter-core * Few improvements to output when using Python * Derive AbstractMagic from CancellableMagicSymbol * Whitespace fixes * Updates for latest jupyter-core changes * Improve readability * Improve readability correctly * Microsoft.Jupyter.Core version * Use Jupyter.Core CI package temporarily * Update to CI package version from master * Whitespace * Apply suggestions from code review Co-authored-by: Chris Granade * Default value for CancellationToken in IAzureClient * Use released Microsoft.Jupyter.Core version Co-authored-by: Chris Granade --- src/AzureClient/AzureClient.cs | 42 +++++++----- src/AzureClient/IAzureClient.cs | 5 +- src/AzureClient/Magic/AzureClientMagicBase.cs | 9 ++- src/AzureClient/Magic/ConnectMagic.cs | 3 +- src/AzureClient/Magic/ExecuteMagic.cs | 5 +- src/AzureClient/Magic/JobsMagic.cs | 3 +- src/AzureClient/Magic/OutputMagic.cs | 3 +- src/AzureClient/Magic/StatusMagic.cs | 3 +- src/AzureClient/Magic/SubmitMagic.cs | 5 +- src/AzureClient/Magic/TargetMagic.cs | 3 +- src/Jupyter/Jupyter.csproj | 2 +- src/Jupyter/Magic/AbstractMagic.cs | 67 +++++++++++-------- src/Kernel/Kernel.csproj | 1 - src/Tests/AzureClientMagicTests.cs | 5 +- src/Tests/AzureClientTests.cs | 13 ++-- 15 files changed, 104 insertions(+), 65 deletions(-) diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index ed059d99f9..749e674d22 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -7,6 +7,7 @@ using System.Collections.Generic; using System.Linq; using System.Net; +using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Quantum; using Microsoft.Azure.Quantum.Client.Models; @@ -110,7 +111,11 @@ public async Task GetConnectionStatusAsync(IChannel channel) return ValidExecutionTargets.ToExecutionResult(); } - private async Task SubmitOrExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext, bool execute) + private async Task SubmitOrExecuteJobAsync( + IChannel channel, + AzureSubmissionContext submissionContext, + bool execute, + CancellationToken cancellationToken) { if (ActiveWorkspace == null) { @@ -178,38 +183,45 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, Az return AzureClientError.JobSubmissionFailed.ToExecutionResult(); } + // If the command was not %azure.execute, simply return the job status. if (!execute) { return await GetJobStatusAsync(channel, MostRecentJobId); } + // If the command was %azure.execute, wait for the job to complete and return the job output. channel.Stdout($"Waiting up to {submissionContext.ExecutionTimeout} seconds for Azure Quantum job to complete..."); - using (var cts = new System.Threading.CancellationTokenSource(TimeSpan.FromSeconds(submissionContext.ExecutionTimeout))) + using var executionTimeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(submissionContext.ExecutionTimeout)); + using var executionCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(executionTimeoutTokenSource.Token, cancellationToken); { - CloudJob? cloudJob = null; - do + try { - // 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 - await Task.Delay(TimeSpan.FromSeconds(submissionContext.ExecutionPollingInterval)); - if (cts.IsCancellationRequested) break; - cloudJob = await ActiveWorkspace.GetJobAsync(MostRecentJobId); - channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}"); + CloudJob? cloudJob = null; + while (cloudJob == null || cloudJob.InProgress) + { + executionCancellationTokenSource.Token.ThrowIfCancellationRequested(); + await Task.Delay(TimeSpan.FromSeconds(submissionContext.ExecutionPollingInterval), executionCancellationTokenSource.Token); + cloudJob = await ActiveWorkspace.GetJobAsync(MostRecentJobId); + channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}"); + } + } + catch (Exception e) when (e is TaskCanceledException || e is OperationCanceledException) + { + Logger?.LogInformation($"Operation canceled while waiting for job execution to complete: {e.Message}"); } - while (cloudJob == null || cloudJob.InProgress); } return await GetJobResultAsync(channel, MostRecentJobId); } /// - public async Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext) => - await SubmitOrExecuteJobAsync(channel, submissionContext, execute: false); + public async Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext, CancellationToken? cancellationToken = null) => + await SubmitOrExecuteJobAsync(channel, submissionContext, execute: false, cancellationToken ?? CancellationToken.None); /// - public async Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext) => - await SubmitOrExecuteJobAsync(channel, submissionContext, execute: true); + public async Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext, CancellationToken? cancellationToken = null) => + await SubmitOrExecuteJobAsync(channel, submissionContext, execute: true, cancellationToken ?? CancellationToken.None); /// public async Task GetActiveTargetAsync(IChannel channel) diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 441abc7bff..c820564c3e 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -5,6 +5,7 @@ using System.Collections.Generic; using System.ComponentModel; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; @@ -44,7 +45,7 @@ public Task ConnectAsync(IChannel channel, /// /// Details of the submitted job, or an error if submission failed. /// - public Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext); + public Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext, CancellationToken? token); /// /// Executes the specified Q# operation as a job to the currently active target @@ -53,7 +54,7 @@ public Task ConnectAsync(IChannel channel, /// /// The result of the executed job, or an error if execution failed. /// - public Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext); + public Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext, CancellationToken? token); /// /// Sets the specified target for job submission. diff --git a/src/AzureClient/Magic/AzureClientMagicBase.cs b/src/AzureClient/Magic/AzureClientMagicBase.cs index 823554c48f..da17815756 100644 --- a/src/AzureClient/Magic/AzureClientMagicBase.cs +++ b/src/AzureClient/Magic/AzureClientMagicBase.cs @@ -4,6 +4,7 @@ #nullable enable using System; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -35,11 +36,15 @@ public AzureClientMagicBase(IAzureClient azureClient, string keyword, Documentat /// public override ExecutionResult Run(string input, IChannel channel) => - RunAsync(input, channel).GetAwaiter().GetResult(); + RunCancellable(input, channel, CancellationToken.None); + + /// + public override ExecutionResult RunCancellable(string input, IChannel channel, CancellationToken cancellationToken) => + RunAsync(input, channel, cancellationToken).GetAwaiter().GetResult(); /// /// Executes the magic command functionality for the given input. /// - public abstract Task RunAsync(string input, IChannel channel); + public abstract Task RunAsync(string input, IChannel channel, CancellationToken cancellationToken); } } diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index ffdceff925..867815d2cd 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -90,7 +91,7 @@ credentials when connecting to Azure. /// Connects to an Azure workspace given a subscription ID, resource group name, /// workspace name, and connection string as a JSON-encoded object. /// - public override async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel, CancellationToken cancellationToken) { var inputParameters = ParseInputParameters(input); diff --git a/src/AzureClient/Magic/ExecuteMagic.cs b/src/AzureClient/Magic/ExecuteMagic.cs index 122882abe5..6a675e9f3e 100644 --- a/src/AzureClient/Magic/ExecuteMagic.cs +++ b/src/AzureClient/Magic/ExecuteMagic.cs @@ -5,6 +5,7 @@ using System; using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -56,9 +57,9 @@ The Azure Quantum workspace must previously have been initialized /// name that is present in the current Q# Jupyter workspace, and /// waits for the job to complete before returning. /// - public override async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel, CancellationToken cancellationToken) { - return await AzureClient.ExecuteJobAsync(channel, AzureSubmissionContext.Parse(input)); + return await AzureClient.ExecuteJobAsync(channel, AzureSubmissionContext.Parse(input), cancellationToken); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/JobsMagic.cs b/src/AzureClient/Magic/JobsMagic.cs index 590e77b418..1ac082350c 100644 --- a/src/AzureClient/Magic/JobsMagic.cs +++ b/src/AzureClient/Magic/JobsMagic.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -52,7 +53,7 @@ The Azure Quantum workspace must previously have been initialized /// /// Lists all jobs in the active workspace. /// - public override async Task RunAsync(string input, IChannel channel) => + public override async Task RunAsync(string input, IChannel channel, CancellationToken cancellationToken) => await AzureClient.GetJobListAsync(channel); } } \ No newline at end of file diff --git a/src/AzureClient/Magic/OutputMagic.cs b/src/AzureClient/Magic/OutputMagic.cs index dd542d7329..624b4a5a89 100644 --- a/src/AzureClient/Magic/OutputMagic.cs +++ b/src/AzureClient/Magic/OutputMagic.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -68,7 +69,7 @@ The Azure Quantum workspace must previously have been initialized /// Displays the output of a given completed job ID, if provided, /// or all jobs submitted in the current session. /// - public override async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel, CancellationToken cancellationToken) { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameJobId); string jobId = inputParameters.DecodeParameter(ParameterNameJobId); diff --git a/src/AzureClient/Magic/StatusMagic.cs b/src/AzureClient/Magic/StatusMagic.cs index 80a2a85733..2ba8bcd0be 100644 --- a/src/AzureClient/Magic/StatusMagic.cs +++ b/src/AzureClient/Magic/StatusMagic.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -66,7 +67,7 @@ The Azure Quantum workspace must previously have been initialized /// Displays the status corresponding to a given job ID, if provided, /// or the most recently-submitted job in the current session. /// - public override async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel, CancellationToken cancellationToken) { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameJobId); string jobId = inputParameters.DecodeParameter(ParameterNameJobId); diff --git a/src/AzureClient/Magic/SubmitMagic.cs b/src/AzureClient/Magic/SubmitMagic.cs index 3c7bcfccdd..2a46a9c03b 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -4,6 +4,7 @@ #nullable enable using System.Collections.Generic; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -52,9 +53,9 @@ The Azure Quantum workspace must previously have been initialized /// Submits a new job to an Azure Quantum workspace given a Q# operation /// name that is present in the current Q# Jupyter workspace. /// - public override async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel, CancellationToken cancellationToken) { - return await AzureClient.SubmitJobAsync(channel, AzureSubmissionContext.Parse(input)); + return await AzureClient.SubmitJobAsync(channel, AzureSubmissionContext.Parse(input), cancellationToken); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index d0c93be7e1..d5c4f8b801 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -63,7 +64,7 @@ available in the workspace. /// /// Sets or views the target for job submission to the current Azure Quantum workspace. /// - public override async Task RunAsync(string input, IChannel channel) + public override async Task RunAsync(string input, IChannel channel, CancellationToken cancellationToken) { var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameTargetId); if (inputParameters.ContainsKey(ParameterNameTargetId)) diff --git a/src/Jupyter/Jupyter.csproj b/src/Jupyter/Jupyter.csproj index 67a9b01310..b092621bf2 100644 --- a/src/Jupyter/Jupyter.csproj +++ b/src/Jupyter/Jupyter.csproj @@ -35,7 +35,7 @@ - + diff --git a/src/Jupyter/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs index 64a1326266..9ddcd9e81c 100644 --- a/src/Jupyter/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -6,18 +6,18 @@ using System.IO; using System.Linq; using System.Text.RegularExpressions; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp.Common; using Microsoft.Quantum.QsCompiler.Serialization; -using Newtonsoft.Json.Linq; namespace Microsoft.Quantum.IQSharp.Jupyter { /// /// Abstract base class for IQ# magic symbols. /// - public abstract class AbstractMagic : MagicSymbol + public abstract class AbstractMagic : CancellableMagicSymbol { /// /// Constructs a new magic symbol given its name and documentation. @@ -28,7 +28,7 @@ public AbstractMagic(string keyword, Documentation docs) this.Documentation = docs; this.Kind = SymbolKind.Magic; - this.Execute = SafeExecute(this.Run); + this.ExecuteCancellable = this.SafeExecute(this.RunCancellable); } /// @@ -38,31 +38,32 @@ public AbstractMagic(string keyword, Documentation docs) /// returned execution function displays the given exceptions to its /// display channel. /// - public Func> SafeExecute(Func magic) => - async (input, channel) => - { - channel = channel.WithNewLines(); - - try - { - return magic(input, channel); - } - catch (InvalidWorkspaceException ws) + public Func> SafeExecute( + Func magic) => + async (input, channel, cancellationToken) => { - foreach (var m in ws.Errors) channel.Stderr(m); - return ExecuteStatus.Error.ToExecutionResult(); - } - catch (AggregateException agg) - { - foreach (var e in agg.InnerExceptions) channel.Stderr(e?.Message); - return ExecuteStatus.Error.ToExecutionResult(); - } - catch (Exception e) - { - channel.Stderr(e.Message); - return ExecuteStatus.Error.ToExecutionResult(); - } - }; + channel = channel.WithNewLines(); + + try + { + return magic(input, channel, cancellationToken); + } + catch (InvalidWorkspaceException ws) + { + foreach (var m in ws.Errors) channel.Stderr(m); + return ExecuteStatus.Error.ToExecutionResult(); + } + catch (AggregateException agg) + { + foreach (var e in agg.InnerExceptions) channel.Stderr(e?.Message); + return ExecuteStatus.Error.ToExecutionResult(); + } + catch (Exception e) + { + channel.Stderr(e.Message); + return ExecuteStatus.Error.ToExecutionResult(); + } + }; /// /// Parses the input to a magic command, interpreting the input as @@ -146,5 +147,17 @@ public static Dictionary ParseInputParameters(string input, stri /// A method to be run when the magic command is executed. /// public abstract ExecutionResult Run(string input, IChannel channel); + + /// + /// A method to be run when the magic command is executed, including a cancellation + /// token to use for requesting cancellation. + /// + /// + /// The default implementation in ignores the cancellation token. + /// Derived classes should override this method and monitor the cancellation token if they + /// wish to support cancellation. + /// + public virtual ExecutionResult RunCancellable(string input, IChannel channel, CancellationToken cancellationToken) => + Run(input, channel); } } diff --git a/src/Kernel/Kernel.csproj b/src/Kernel/Kernel.csproj index 242398b43b..b42ee4c7be 100644 --- a/src/Kernel/Kernel.csproj +++ b/src/Kernel/Kernel.csproj @@ -22,7 +22,6 @@ - diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 51763fdbac..19ebf98982 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -6,6 +6,7 @@ using Microsoft.VisualStudio.TestTools.UnitTesting; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; using Microsoft.Quantum.IQSharp; @@ -223,14 +224,14 @@ public async Task GetActiveTargetAsync(IChannel channel) return ActiveTargetId.ToExecutionResult(); } - public async Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext) + public async Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext, CancellationToken? token) { LastAction = AzureClientAction.SubmitJob; SubmittedJobs.Add(submissionContext.OperationName); return ExecuteStatus.Ok.ToExecutionResult(); } - public async Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext) + public async Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext, CancellationToken? token) { LastAction = AzureClientAction.ExecuteJob; ExecutedJobs.Add(submissionContext.OperationName); diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index 099c251268..773d7a6d8a 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Threading; using System.Threading.Tasks; using Microsoft.Azure.Quantum; using Microsoft.Azure.Quantum.Client.Models; @@ -202,14 +203,14 @@ public void TestJobSubmission() var submissionContext = new AzureSubmissionContext(); // not yet connected - ExpectError(AzureClientError.NotConnected, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + ExpectError(AzureClientError.NotConnected, azureClient.SubmitJobAsync(new MockChannel(), submissionContext, CancellationToken.None)); // connect var targets = ExpectSuccess>(ConnectToWorkspaceAsync(azureClient)); Assert.IsFalse(targets.Any()); // no target yet - ExpectError(AzureClientError.NoTarget, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + ExpectError(AzureClientError.NoTarget, azureClient.SubmitJobAsync(new MockChannel(), submissionContext, CancellationToken.None)); // add a target var azureWorkspace = azureClient.ActiveWorkspace as MockAzureWorkspace; @@ -221,15 +222,15 @@ public void TestJobSubmission() Assert.AreEqual("ionq.simulator", target.Id); // no operation name specified - ExpectError(AzureClientError.NoOperationName, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + ExpectError(AzureClientError.NoOperationName, azureClient.SubmitJobAsync(new MockChannel(), submissionContext, CancellationToken.None)); // specify an operation name, but have missing parameters submissionContext.OperationName = "Tests.qss.HelloAgain"; - ExpectError(AzureClientError.JobSubmissionFailed, azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + ExpectError(AzureClientError.JobSubmissionFailed, azureClient.SubmitJobAsync(new MockChannel(), submissionContext, CancellationToken.None)); // specify input parameters and verify that the job was submitted submissionContext.InputParameters = new Dictionary() { ["count"] = "3", ["name"] = "testing" }; - var job = ExpectSuccess(azureClient.SubmitJobAsync(new MockChannel(), submissionContext)); + var job = ExpectSuccess(azureClient.SubmitJobAsync(new MockChannel(), submissionContext, CancellationToken.None)); var retrievedJob = ExpectSuccess(azureClient.GetJobStatusAsync(new MockChannel(), job.Id)); Assert.AreEqual(job.Id, retrievedJob.Id); } @@ -261,7 +262,7 @@ public void TestJobExecution() ExecutionTimeout = 5, ExecutionPollingInterval = 1, }; - var histogram = ExpectSuccess(azureClient.ExecuteJobAsync(new MockChannel(), submissionContext)); + var histogram = ExpectSuccess(azureClient.ExecuteJobAsync(new MockChannel(), submissionContext, CancellationToken.None)); Assert.IsNotNull(histogram); } } From d76cd53130f97188455e4c8a61ab76942f969411 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 22:12:43 -0700 Subject: [PATCH 41/44] Support connection to Azure Quantum workspace via resource ID (#173) * Support resource ID in %azure.connect * Update parameter names to match standalone executable * Update Python API and tests * Use named parameter for resource ID * Avoid calling Regex.IsMatch Co-authored-by: Sarah Marshall <33814365+samarsha@users.noreply.github.com> * Address PR feedback and improve tests * Empty commit to re-trigger CI build * Speed up tests, enable debug logging * Make NuGet.config available to conda package * Add prerelease feed for non-release builds * Copy NuGet config during build step * Fix bad merge with job filter changes * Copy NuGet.config to home folder * Copy NuGet.config in both jobs * Write prerelease NuGet directly * Construct RegEx object once * Remove line breaks * Write prerelease NuGet into target directory * Put prerelease NuGet back in test.ps1 for now Co-authored-by: Sarah Marshall <33814365+samarsha@users.noreply.github.com> --- build/test.ps1 | 2 +- conda-recipes/iqsharp/build.ps1 | 2 - conda-recipes/iqsharp/test.ps1 | 9 ++- src/AzureClient/AzureClient.cs | 27 +++++-- src/AzureClient/AzureClient.csproj | 2 +- src/AzureClient/Magic/ConnectMagic.cs | 111 ++++++++++++++++++-------- src/Core/Core.csproj | 6 +- src/Jupyter/Magic/AbstractMagic.cs | 8 +- src/Python/qsharp/azure.py | 2 +- src/Python/qsharp/tests/test_azure.py | 61 +++++++++++--- src/Tests/AzureClientMagicTests.cs | 55 +++++++++---- src/Tool/appsettings.json | 30 +++---- 12 files changed, 221 insertions(+), 94 deletions(-) diff --git a/build/test.ps1 b/build/test.ps1 index 7a0c737a21..2dd318fe77 100644 --- a/build/test.ps1 +++ b/build/test.ps1 @@ -41,7 +41,7 @@ function Test-Python { Write-Host "##[info]Testing Python inside $testFolder" Push-Location (Join-Path $PSScriptRoot $testFolder) python --version - pytest -v + pytest -v --log-level=Debug Pop-Location if ($LastExitCode -ne 0) { diff --git a/conda-recipes/iqsharp/build.ps1 b/conda-recipes/iqsharp/build.ps1 index f08bdeb1ee..6c76741f98 100644 --- a/conda-recipes/iqsharp/build.ps1 +++ b/conda-recipes/iqsharp/build.ps1 @@ -17,8 +17,6 @@ if ($IsWindows) { $RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "../.."); $ArtifactRoot = Join-Path $RepoRoot "drops"; $SelfContainedDirectory = Join-Path $ArtifactRoot (Join-Path "selfcontained" $RuntimeID) -$NugetsDirectory = Join-Path $ArtifactRoot "nugets" -$NugetConfig = Resolve-Path (Join-Path $PSScriptRoot "NuGet.config"); $TargetDirectory = (Join-Path (Join-Path $Env:PREFIX "opt") "iqsharp"); diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1 index e1f14d46f0..3007f27e56 100644 --- a/conda-recipes/iqsharp/test.ps1 +++ b/conda-recipes/iqsharp/test.ps1 @@ -5,6 +5,13 @@ $failed = $false; $Env:IQSHARP_PACKAGE_SOURCE = "$Env:NUGET_OUTDIR" +# Add the prerelease NuGet feed if this isn't a release build. +if ("$Env:BUILD_RELEASETYPE" -ne "release") { + $NuGetDirectory = Resolve-Path ~ + Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##" + echo "" > $NuGetDirectory/NuGet.Config +} + # Check that iqsharp is installed as a Jupyter kernel. $kernels = jupyter kernelspec list --json | ConvertFrom-Json; if ($null -eq $kernels.kernelspecs.iqsharp) { @@ -13,7 +20,7 @@ if ($null -eq $kernels.kernelspecs.iqsharp) { jupyter kernelspec list } - +# Run the kernel unit tests. Push-Location $PSScriptRoot python test.py if ($LastExitCode -ne 0) { diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index 12ca9d130c..0abb670a55 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -73,21 +73,36 @@ public async Task ConnectAsync(IChannel channel, string storageAccountConnectionString, bool refreshCredentials = false) { - ConnectionString = storageAccountConnectionString; - var azureEnvironment = AzureEnvironment.Create(subscriptionId); - ActiveWorkspace = await azureEnvironment.GetAuthenticatedWorkspaceAsync(channel, resourceGroupName, workspaceName, refreshCredentials); - if (ActiveWorkspace == null) + IAzureWorkspace? workspace = null; + try + { + workspace = await azureEnvironment.GetAuthenticatedWorkspaceAsync(channel, resourceGroupName, workspaceName, refreshCredentials); + } + catch (Exception e) + { + channel.Stderr($"The connection to the Azure Quantum workspace could not be completed. Please check the provided parameters and try again."); + channel.Stderr($"Error details: {e.Message}"); + return AzureClientError.WorkspaceNotFound.ToExecutionResult(); + } + + if (workspace == null) { return AzureClientError.AuthenticationFailed.ToExecutionResult(); } - AvailableProviders = await ActiveWorkspace.GetProvidersAsync(); - if (AvailableProviders == null) + var providers = await workspace.GetProvidersAsync(); + if (providers == null) { return AzureClientError.WorkspaceNotFound.ToExecutionResult(); } + ActiveWorkspace = workspace; + AvailableProviders = providers; + ConnectionString = storageAccountConnectionString; + ActiveTarget = null; + MostRecentJobId = string.Empty; + channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}."); if (ValidExecutionTargets.Count() == 0) diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj index 995ae372bb..8d7b029f32 100644 --- a/src/AzureClient/AzureClient.csproj +++ b/src/AzureClient/AzureClient.csproj @@ -13,7 +13,7 @@ - + diff --git a/src/AzureClient/Magic/ConnectMagic.cs b/src/AzureClient/Magic/ConnectMagic.cs index 867815d2cd..12114f48fb 100644 --- a/src/AzureClient/Magic/ConnectMagic.cs +++ b/src/AzureClient/Magic/ConnectMagic.cs @@ -6,6 +6,7 @@ using System; using System.Collections.Generic; using System.Linq; +using System.Text.RegularExpressions; using System.Threading; using System.Threading.Tasks; using Microsoft.Jupyter.Core; @@ -19,10 +20,11 @@ namespace Microsoft.Quantum.IQSharp.AzureClient public class ConnectMagic : AzureClientMagicBase { private const string ParameterNameRefresh = "refresh"; - private const string ParameterNameStorageAccountConnectionString = "storageAccountConnectionString"; - private const string ParameterNameSubscriptionId = "subscriptionId"; - private const string ParameterNameResourceGroupName = "resourceGroupName"; - private const string ParameterNameWorkspaceName = "workspaceName"; + private const string ParameterNameStorageAccountConnectionString = "storage"; + private const string ParameterNameSubscriptionId = "subscription"; + private const string ParameterNameResourceGroupName = "resourceGroup"; + private const string ParameterNameWorkspaceName = "workspace"; + private const string ParameterNameResourceId = "resourceId"; /// /// Initializes a new instance of the class. @@ -36,54 +38,73 @@ public ConnectMagic(IAzureClient azureClient) "azure.connect", new Documentation { - Summary = "Connects to an Azure workspace or displays current connection status.", + Summary = "Connects to an Azure Quantum workspace or displays current connection status.", Description = @" This magic command allows for connecting to an Azure Quantum workspace - as specified by a valid subscription ID, resource group name, workspace name, - and storage account connection string. + as specified by the resource ID of the workspace or by a combination of + subscription ID, resource group name, and workspace name. - If the connection is successful, a list of the available execution targets + If the connection is successful, a list of the available Q# execution targets in the Azure Quantum workspace will be displayed. ".Dedent(), Examples = new[] { - @" - Print information about the current connection: + $@" + Connect to an Azure Quantum workspace using its resource ID: ``` - In []: %azure.connect + In []: %azure.connect {ParameterNameResourceId}=""/subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME"" Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. - + ``` ".Dedent(), $@" - Connect to an Azure Quantum workspace: + Connect to an Azure Quantum workspace using its resource ID and a storage account connection string, + which is required for workspaces that do not have a linked storage account: ``` - In []: %azure.connect {ParameterNameSubscriptionId}=SUBSCRIPTION_ID - {ParameterNameResourceGroupName}=RESOURCE_GROUP_NAME - {ParameterNameWorkspaceName}=WORKSPACE_NAME - {ParameterNameStorageAccountConnectionString}=CONNECTION_STRING + In []: %azure.connect {ParameterNameResourceId}=""/subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME"" + {ParameterNameStorageAccountConnectionString}=""STORAGE_ACCOUNT_CONNECTION_STRING"" Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. - + ``` ".Dedent(), $@" - Connect to an Azure Quantum workspace and force a credential prompt: + Connect to an Azure Quantum workspace using individual parameters: ``` - In []: %azure.connect {ParameterNameRefresh} - {ParameterNameSubscriptionId}=SUBSCRIPTION_ID - {ParameterNameResourceGroupName}=RESOURCE_GROUP_NAME - {ParameterNameWorkspaceName}=WORKSPACE_NAME - {ParameterNameStorageAccountConnectionString}=CONNECTION_STRING + In []: %azure.connect {ParameterNameSubscriptionId}=""SUBSCRIPTION_ID"" + {ParameterNameResourceGroupName}=""RESOURCE_GROUP_NAME"" + {ParameterNameWorkspaceName}=""WORKSPACE_NAME"" + {ParameterNameStorageAccountConnectionString}=""STORAGE_ACCOUNT_CONNECTION_STRING"" + Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. + + ``` + The `{ParameterNameStorageAccountConnectionString}` parameter is necessary only if the + specified Azure Quantum workspace was not linked to a storage account at creation time. + ".Dedent(), + + $@" + Connect to an Azure Quantum workspace and force a credential prompt using + the `{ParameterNameRefresh}` option: + ``` + In []: %azure.connect {ParameterNameRefresh} {ParameterNameResourceId}=""/subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME"" Out[]: To sign in, use a web browser to open the page https://microsoft.com/devicelogin and enter the code [login code] to authenticate. Connected to Azure Quantum workspace WORKSPACE_NAME. - + ``` - Use the `{ParameterNameRefresh}` option if you want to bypass any saved or cached + The `{ParameterNameRefresh}` option bypasses any saved or cached credentials when connecting to Azure. ".Dedent(), + + @" + Print information about the current connection: + ``` + In []: %azure.connect + Out[]: Connected to Azure Quantum workspace WORKSPACE_NAME. + + ``` + ".Dedent(), }, }) {} @@ -94,16 +115,40 @@ credentials when connecting to Azure. public override async Task RunAsync(string input, IChannel channel, CancellationToken cancellationToken) { var inputParameters = ParseInputParameters(input); - - var storageAccountConnectionString = inputParameters.DecodeParameter(ParameterNameStorageAccountConnectionString); - if (string.IsNullOrEmpty(storageAccountConnectionString)) + if (!inputParameters.Any()) { return await AzureClient.GetConnectionStatusAsync(channel); } - var subscriptionId = inputParameters.DecodeParameter(ParameterNameSubscriptionId); - var resourceGroupName = inputParameters.DecodeParameter(ParameterNameResourceGroupName); - var workspaceName = inputParameters.DecodeParameter(ParameterNameWorkspaceName); + var resourceId = inputParameters.DecodeParameter(ParameterNameResourceId, defaultValue: string.Empty); + var subscriptionId = string.Empty; + var resourceGroupName = string.Empty; + var workspaceName = string.Empty; + + // A valid resource ID looks like: + // /subscriptions/f846b2bd-d0e2-4a1d-8141-4c6944a9d387/resourceGroups/RESOURCE_GROUP_NAME/providers/Microsoft.Quantum/Workspaces/WORKSPACE_NAME + var match = Regex.Match(resourceId, + @"^/subscriptions/([a-fA-F0-9-]*)/resourceGroups/([^\s/]*)/providers/Microsoft\.Quantum/Workspaces/([^\s/]*)$"); + if (match.Success) + { + // match.Groups will be a GroupCollection containing four Group objects: + // -> match.Groups[0]: The full resource ID for the Azure Quantum workspace + // -> match.Groups[1]: The Azure subscription ID + // -> match.Groups[2]: The Azure resource group name + // -> match.Groups[3]: The Azure Quantum workspace name + subscriptionId = match.Groups[1].Value; + resourceGroupName = match.Groups[2].Value; + workspaceName = match.Groups[3].Value; + } + else + { + // look for each of the parameters individually + subscriptionId = inputParameters.DecodeParameter(ParameterNameSubscriptionId, defaultValue: string.Empty); + resourceGroupName = inputParameters.DecodeParameter(ParameterNameResourceGroupName, defaultValue: string.Empty); + workspaceName = inputParameters.DecodeParameter(ParameterNameWorkspaceName, defaultValue: string.Empty); + } + + var storageAccountConnectionString = inputParameters.DecodeParameter(ParameterNameStorageAccountConnectionString, defaultValue: string.Empty); var refreshCredentials = inputParameters.DecodeParameter(ParameterNameRefresh, defaultValue: false); return await AzureClient.ConnectAsync( channel, @@ -114,4 +159,4 @@ public override async Task RunAsync(string input, IChannel chan refreshCredentials); } } -} \ No newline at end of file +} diff --git a/src/Core/Core.csproj b/src/Core/Core.csproj index 11460fc08e..872d3a96af 100644 --- a/src/Core/Core.csproj +++ b/src/Core/Core.csproj @@ -34,9 +34,9 @@ - - - + + + diff --git a/src/Jupyter/Magic/AbstractMagic.cs b/src/Jupyter/Magic/AbstractMagic.cs index 9ddcd9e81c..5e53235dbe 100644 --- a/src/Jupyter/Magic/AbstractMagic.cs +++ b/src/Jupyter/Magic/AbstractMagic.cs @@ -94,6 +94,8 @@ public static Dictionary ParseInputParameters(string input, stri var regex = new Regex(@"(\{.*\})|[^\s""]+(?:\s*=\s*)(?:""[^""]*""|[^\s""]*)*|[^\s""]+(?:""[^""]*""[^\s""]*)*|(?:""[^""]*""[^\s""]*)+"); var args = regex.Matches(input).Select(match => match.Value); + var regexBeginEndQuotes = new Regex(@"^['""]|['""]$"); + // If we are expecting a first inferred-name parameter, see if it exists. // If so, serialize it to the dictionary as JSON and remove it from the list of args. if (args.Any() && @@ -102,7 +104,7 @@ public static Dictionary ParseInputParameters(string input, stri !string.IsNullOrEmpty(firstParameterInferredName)) { using var writer = new StringWriter(); - Json.Serializer.Serialize(writer, args.First()); + Json.Serializer.Serialize(writer, regexBeginEndQuotes.Replace(args.First(), string.Empty)); inputParameters[firstParameterInferredName] = writer.ToString(); args = args.Skip(1); } @@ -123,14 +125,14 @@ public static Dictionary ParseInputParameters(string input, stri foreach (string arg in args) { var tokens = arg.Split("=", 2); - var key = tokens[0].Trim(); + var key = regexBeginEndQuotes.Replace(tokens[0].Trim(), string.Empty); var value = tokens.Length switch { // If there was no value provided explicitly, treat it as an implicit "true" value 1 => true as object, // Trim whitespace and also enclosing single-quotes or double-quotes before returning - 2 => Regex.Replace(tokens[1].Trim(), @"^['""]|['""]$", string.Empty) as object, + 2 => regexBeginEndQuotes.Replace(tokens[1].Trim(), string.Empty) as object, // We called arg.Split("=", 2), so there should never be more than 2 _ => throw new InvalidOperationException() diff --git a/src/Python/qsharp/azure.py b/src/Python/qsharp/azure.py index 04a7644998..32036f528a 100644 --- a/src/Python/qsharp/azure.py +++ b/src/Python/qsharp/azure.py @@ -131,6 +131,6 @@ def output(jobId : str = '', **params) -> Dict: return result def jobs(filter : str = '', **params) -> List[AzureJob]: - result = qsharp.client._execute_magic(f"azure.jobs {filter}", raise_on_stderr=False, **params) + result = qsharp.client._execute_magic(f"azure.jobs \"{filter}\"", raise_on_stderr=False, **params) if "error_code" in result: raise AzureError(result) return [AzureJob(job) for job in result] diff --git a/src/Python/qsharp/tests/test_azure.py b/src/Python/qsharp/tests/test_azure.py index 613aa0b42a..13ba002958 100644 --- a/src/Python/qsharp/tests/test_azure.py +++ b/src/Python/qsharp/tests/test_azure.py @@ -37,10 +37,10 @@ def test_empty_workspace(): assert exception_info.value.error_name == "NotConnected" targets = qsharp.azure.connect( - storageAccountConnectionString="test", - subscriptionId="test", - resourceGroupName="test", - workspaceName="test" + storage="test", + subscription="test", + resourceGroup="test", + workspace="test" ) assert targets == [] @@ -51,23 +51,58 @@ def test_empty_workspace(): jobs = qsharp.azure.jobs() assert jobs == [] -def test_workspace_with_providers(): +def test_workspace_create_with_parameters(): """ - Tests behavior of a mock workspace with mock providers. + Tests behavior of a mock workspace with providers, using parameters to connect. """ targets = qsharp.azure.connect( - storageAccountConnectionString="test", - subscriptionId="test", - resourceGroupName="test", - workspaceName="WorkspaceNameWithMockProviders" + storage="test", + subscription="test", + resourceGroup="test", + workspace="WorkspaceNameWithMockProviders" ) assert isinstance(targets, list) assert len(targets) > 0 + _test_workspace_with_providers_after_connection() + +def test_workspace_create_with_resource_id(): + """ + Tests behavior of a mock workspace with providers, using resource ID to connect. + """ + subscriptionId = "f846b2bd-d0e2-4a1d-8141-4c6944a9d387" + resourceGroupName = "test" + workspaceName = "WorkspaceNameWithMockProviders" + targets = qsharp.azure.connect( + resourceId=f"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName}") + assert isinstance(targets, list) + assert len(targets) > 0 + + _test_workspace_with_providers_after_connection() + _test_workspace_job_execution() + +def test_workspace_create_with_resource_id_and_storage(): + """ + Tests behavior of a mock workspace with providers, using resource ID and storage connection string to connect. + """ + subscriptionId = "f846b2bd-d0e2-4a1d-8141-4c6944a9d387" + resourceGroupName = "test" + workspaceName = "WorkspaceNameWithMockProviders" + storageAccountConnectionString = "test" + targets = qsharp.azure.connect( + resourceId=f"/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName}", + storage=storageAccountConnectionString) + assert isinstance(targets, list) + assert len(targets) > 0 + + _test_workspace_with_providers_after_connection() + +def _test_workspace_with_providers_after_connection(): with pytest.raises(AzureError) as exception_info: qsharp.azure.target() assert exception_info.value.error_name == "NoTarget" + targets = qsharp.azure.connect() for target in targets: active_target = qsharp.azure.target(target.id) assert isinstance(active_target, AzureTarget) @@ -89,6 +124,7 @@ def test_workspace_with_providers(): assert isinstance(retrieved_job, AzureJob) assert job.id == retrieved_job.id +def _test_workspace_job_execution(): # Execute a workspace operation with parameters op = qsharp.QSharpCallable("Microsoft.Quantum.SanityTests.HelloAgain", None) @@ -96,7 +132,7 @@ def test_workspace_with_providers(): qsharp.azure.execute(op) assert exception_info.value.error_name == "JobSubmissionFailed" - histogram = qsharp.azure.execute(op, count=3, name="test") + histogram = qsharp.azure.execute(op, count=3, name="test", timeout=3, poll=0.5) assert isinstance(histogram, dict) retrieved_histogram = qsharp.azure.output() @@ -109,7 +145,6 @@ def test_workspace_with_providers(): assert len(jobs) == 2 # Check that job filtering works - jobs = qsharp.azure.jobs(job.id) - print(job.id) + jobs = qsharp.azure.jobs(jobs[0].id) assert isinstance(jobs, list) assert len(jobs) == 1 diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index 442cb75f27..e3ac7f2cf0 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -12,6 +12,7 @@ using Microsoft.Quantum.IQSharp; using Microsoft.Quantum.IQSharp.AzureClient; using Microsoft.Extensions.DependencyInjection; +using System; namespace Tests.IQSharp { @@ -27,7 +28,7 @@ public static void Test(this MagicSymbol magic, string input, ExecuteStatus expe [TestClass] public class AzureClientMagicTests { - private readonly string subscriptionId = "TEST_SUBSCRIPTION_ID"; + private readonly string subscriptionId = Guid.NewGuid().ToString(); private readonly string resourceGroupName = "TEST_RESOURCE_GROUP_NAME"; private readonly string workspaceName = "TEST_WORKSPACE_NAME"; private readonly string storageAccountConnectionString = "TEST_CONNECTION_STRING"; @@ -41,16 +42,40 @@ public void TestConnectMagic() var azureClient = new MockAzureClient(); var connectMagic = new ConnectMagic(azureClient); + // no input + connectMagic.Test(string.Empty); + Assert.AreEqual(AzureClientAction.GetConnectionStatus, azureClient.LastAction); + // unrecognized input connectMagic.Test($"invalid"); - Assert.AreEqual(azureClient.LastAction, AzureClientAction.GetConnectionStatus); + Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); + + // valid input with resource ID + connectMagic.Test($"resourceId=/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName}"); + Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); + Assert.IsFalse(azureClient.RefreshCredentials); + Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); + Assert.AreEqual(resourceGroupName, azureClient.ResourceGroupName); + Assert.AreEqual(workspaceName, azureClient.WorkspaceName); + Assert.AreEqual(string.Empty, azureClient.ConnectionString); + + // valid input with resource ID and storage account connection string + connectMagic.Test( + @$"resourceId=/subscriptions/{subscriptionId}/resourceGroups/{resourceGroupName}/providers/Microsoft.Quantum/Workspaces/{workspaceName} + storage={storageAccountConnectionString}"); + Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); + Assert.IsFalse(azureClient.RefreshCredentials); + Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); + Assert.AreEqual(resourceGroupName, azureClient.ResourceGroupName); + Assert.AreEqual(workspaceName, azureClient.WorkspaceName); + Assert.AreEqual(storageAccountConnectionString, azureClient.ConnectionString); - // valid input + // valid input with individual parameters connectMagic.Test( - @$"subscriptionId={subscriptionId} - resourceGroupName={resourceGroupName} - workspaceName={workspaceName} - storageAccountConnectionString={storageAccountConnectionString}"); + @$"subscription={subscriptionId} + resourceGroup={resourceGroupName} + workspace={workspaceName} + storage={storageAccountConnectionString}"); Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); Assert.IsFalse(azureClient.RefreshCredentials); Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); @@ -60,10 +85,10 @@ public void TestConnectMagic() // valid input with extra whitespace and quotes connectMagic.Test( - @$"subscriptionId = {subscriptionId} - resourceGroupName= ""{resourceGroupName}"" - workspaceName ={workspaceName} - storageAccountConnectionString = '{storageAccountConnectionString}'"); + @$"subscription = {subscriptionId} + resourceGroup= ""{resourceGroupName}"" + workspace ={workspaceName} + storage = '{storageAccountConnectionString}'"); Assert.AreEqual(AzureClientAction.Connect, azureClient.LastAction); Assert.IsFalse(azureClient.RefreshCredentials); Assert.AreEqual(subscriptionId, azureClient.SubscriptionId); @@ -73,10 +98,10 @@ public void TestConnectMagic() // valid input with forced login connectMagic.Test( - @$"refresh subscriptionId={subscriptionId} - resourceGroupName={resourceGroupName} - workspaceName={workspaceName} - storageAccountConnectionString={storageAccountConnectionString}"); + @$"refresh subscription={subscriptionId} + resourceGroup={resourceGroupName} + workspace={workspaceName} + storage={storageAccountConnectionString}"); Assert.IsTrue(azureClient.RefreshCredentials); } diff --git a/src/Tool/appsettings.json b/src/Tool/appsettings.json index 221dde45bd..d5cbc6ab1e 100644 --- a/src/Tool/appsettings.json +++ b/src/Tool/appsettings.json @@ -6,25 +6,25 @@ }, "AllowedHosts": "*", "DefaultPackageVersions": [ - "Microsoft.Quantum.Compiler::0.11.2006.403", + "Microsoft.Quantum.Compiler::0.11.2006.1615-beta", - "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.CsharpGeneration::0.11.2006.1615-beta", + "Microsoft.Quantum.Development.Kit::0.11.2006.1615-beta", + "Microsoft.Quantum.Simulators::0.11.2006.1615-beta", + "Microsoft.Quantum.Xunit::0.11.2006.1615-beta", - "Microsoft.Quantum.Standard::0.11.2006.403", - "Microsoft.Quantum.Chemistry::0.11.2006.403", - "Microsoft.Quantum.Chemistry.Jupyter::0.11.2006.403", - "Microsoft.Quantum.MachineLearning::0.11.2006.403", - "Microsoft.Quantum.Numerics::0.11.2006.403", + "Microsoft.Quantum.Standard::0.11.2006.1615-beta", + "Microsoft.Quantum.Chemistry::0.11.2006.1615-beta", + "Microsoft.Quantum.Chemistry.Jupyter::0.11.2006.1615-beta", + "Microsoft.Quantum.MachineLearning::0.11.2006.1615-beta", + "Microsoft.Quantum.Numerics::0.11.2006.1615-beta", - "Microsoft.Quantum.Katas::0.11.2006.403", + "Microsoft.Quantum.Katas::0.11.2006.1615-beta", - "Microsoft.Quantum.Research::0.11.2006.403", + "Microsoft.Quantum.Research::0.11.2006.1615-beta", - "Microsoft.Quantum.Providers.IonQ::0.11.2006.403", - "Microsoft.Quantum.Providers.Honeywell::0.11.2006.403", - "Microsoft.Quantum.Providers.QCI::0.11.2006.403", + "Microsoft.Quantum.Providers.IonQ::0.11.2006.1615-beta", + "Microsoft.Quantum.Providers.Honeywell::0.11.2006.1615-beta", + "Microsoft.Quantum.Providers.QCI::0.11.2006.1615-beta", ] } From 53c9a1c73243a53597e7e84354f5bdcd8d16179e Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 22:53:32 -0700 Subject: [PATCH 42/44] Add terminating character to NuGet.config --- conda-recipes/iqsharp/test.ps1 | 1 + 1 file changed, 1 insertion(+) diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1 index 3007f27e56..501d4bd978 100644 --- a/conda-recipes/iqsharp/test.ps1 +++ b/conda-recipes/iqsharp/test.ps1 @@ -10,6 +10,7 @@ if ("$Env:BUILD_RELEASETYPE" -ne "release") { $NuGetDirectory = Resolve-Path ~ Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##" echo "" > $NuGetDirectory/NuGet.Config + echo "" >> $NuGetDirectory/NuGet.Config } # Check that iqsharp is installed as a Jupyter kernel. From fcf567ac7abfa322197542d5f5a4d84e75cf3032 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 23:30:05 -0700 Subject: [PATCH 43/44] Use correct encoding --- conda-recipes/iqsharp/test.ps1 | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1 index 501d4bd978..f1648a6c10 100644 --- a/conda-recipes/iqsharp/test.ps1 +++ b/conda-recipes/iqsharp/test.ps1 @@ -9,8 +9,14 @@ $Env:IQSHARP_PACKAGE_SOURCE = "$Env:NUGET_OUTDIR" if ("$Env:BUILD_RELEASETYPE" -ne "release") { $NuGetDirectory = Resolve-Path ~ Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##" - echo "" > $NuGetDirectory/NuGet.Config - echo "" >> $NuGetDirectory/NuGet.Config + " + + + + + + + " | Out-File -FilePath $NuGetDirectory/NuGet.Config -Encoding utf8 } # Check that iqsharp is installed as a Jupyter kernel. From afaf38f34e0787020209cd554fd4dc40cbc4c054 Mon Sep 17 00:00:00 2001 From: Ryan Shaffer Date: Mon, 22 Jun 2020 23:31:31 -0700 Subject: [PATCH 44/44] Whitespace --- conda-recipes/iqsharp/test.ps1 | 8 +++----- 1 file changed, 3 insertions(+), 5 deletions(-) diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1 index f1648a6c10..c5cf7bd421 100644 --- a/conda-recipes/iqsharp/test.ps1 +++ b/conda-recipes/iqsharp/test.ps1 @@ -9,14 +9,12 @@ $Env:IQSHARP_PACKAGE_SOURCE = "$Env:NUGET_OUTDIR" if ("$Env:BUILD_RELEASETYPE" -ne "release") { $NuGetDirectory = Resolve-Path ~ Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##" - " - - + " + - - " | Out-File -FilePath $NuGetDirectory/NuGet.Config -Encoding utf8 + " | Out-File -FilePath $NuGetDirectory/NuGet.Config -Encoding utf8 } # Check that iqsharp is installed as a Jupyter kernel.