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" ] }