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 ee639ec3e8..9c63cf334a 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 @@ -6,41 +6,59 @@ using System; using System.Collections.Generic; using System.Linq; -using System.IO; +using System.Net; using System.Threading.Tasks; -using Microsoft.Azure.Quantum.Client; -using Microsoft.Identity.Client; -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; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Extensions.Logging; +using Microsoft.Jupyter.Core; +using Microsoft.Quantum.IQSharp.Common; +using Microsoft.Quantum.Simulation.Common; 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 AuthenticationResult? AuthenticationResult { get; set; } - private IQuantumClient? QuantumClient { 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 + 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, + IReferences references, + IEntryPointGenerator entryPointGenerator, + ILogger logger, + IEventService eventService) { - get => ValidExecutionTargets == null - ? "(no execution targets available)" - : string.Join(", ", ValidExecutionTargets.Select(target => target.Id)); - } + References = references; + 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()); + baseEngine.RegisterDisplayEncoder(new AzureClientErrorToHtmlEncoder()); + baseEngine.RegisterDisplayEncoder(new AzureClientErrorToTextEncoder()); + } + } /// public async Task ConnectAsync( @@ -53,109 +71,43 @@ public async Task ConnectAsync( { 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) + var azureEnvironment = AzureEnvironment.Create(subscriptionId); + ActiveWorkspace = await azureEnvironment.GetAuthenticatedWorkspaceAsync(channel, resourceGroupName, workspaceName, refreshCredentials); + if (ActiveWorkspace == null) { - AuthenticationResult = await msalApp.AcquireTokenWithDeviceCode( - azureEnvironment.Scopes, - deviceCodeResult => - { - channel.Stdout(deviceCodeResult.Message); - return Task.FromResult(0); - }).WithAuthority(msalApp.Authority).ExecuteAsync(); + return AzureClientError.AuthenticationFailed.ToExecutionResult(); } - if (AuthenticationResult == null) + AvailableProviders = await ActiveWorkspace.GetProvidersAsync(); + if (AvailableProviders == null) { - return AzureClientError.AuthenticationFailed.ToExecutionResult(); + return AzureClientError.WorkspaceNotFound.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); + channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}."); - try + if (ValidExecutionTargets.Count() == 0) { - AvailableProviders = await QuantumClient.Providers.GetStatusAsync(); + channel.Stderr($"No valid Q# execution targets found in Azure Quantum workspace {ActiveWorkspace.Name}."); } - catch (Exception e) - { - channel.Stderr(e.ToString()); - return AzureClientError.WorkspaceNotFound.ToExecutionResult(); - } - - 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(); } /// 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}."); - // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return ValidExecutionTargets.ToJupyterTable().ToExecutionResult(); + return ValidExecutionTargets.ToExecutionResult(); } - private async Task SubmitOrExecuteJobAsync( - IChannel channel, - IOperationResolver operationResolver, - string operationName, - bool execute) + private async Task SubmitOrExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext, bool execute) { if (ActiveWorkspace == null) { @@ -169,64 +121,96 @@ private async Task SubmitOrExecuteJobAsync( 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}."); return AzureClientError.NoOperationName.ToExecutionResult(); } - var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetName, 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. - 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(); } - var operationInfo = operationResolver.Resolve(operationName); - var entryPointInfo = new EntryPointInfo(operationInfo.RoslynType); - var entryPointInput = QVoid.Instance; + channel.Stdout($"Submitting {submissionContext.OperationName} to target {ActiveTarget.TargetId}..."); - 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(submissionContext.OperationName, ActiveTarget.TargetId); + } + catch (UnsupportedOperationException e) + { + channel.Stderr($"{submissionContext.OperationName} is not a recognized Q# operation name."); + return AzureClientError.UnrecognizedOperationName.ToExecutionResult(); } - else + catch (CompilationErrorsException 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($"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, 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 {submissionContext.OperationName}."); + channel.Stderr(e.Message); + return AzureClientError.JobSubmissionFailed.ToExecutionResult(); + } + catch (Exception e) + { + channel.Stderr($"Failed to submit Q# operation {submissionContext.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); } + + 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))) + { + 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 + 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"}"); + } + 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, AzureSubmissionContext submissionContext) => + await SubmitOrExecuteJobAsync(channel, submissionContext, 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, AzureSubmissionContext submissionContext) => + await SubmitOrExecuteJobAsync(channel, submissionContext, execute: true); /// - public async Task GetActiveTargetAsync( - IChannel channel) + public async Task GetActiveTargetAsync(IChannel channel) { if (AvailableProviders == null) { @@ -236,21 +220,19 @@ public async Task GetActiveTargetAsync( 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 AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult(); } /// - public async Task SetActiveTargetAsync( - IChannel channel, - IReferences references, - string targetName) + public async Task SetActiveTargetAsync(IChannel channel, string targetId) { if (AvailableProviders == null) { @@ -258,19 +240,19 @@ public async Task SetActiveTargetAsync( 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(); } @@ -279,15 +261,15 @@ 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(); + channel.Stdout($"Active target is now {ActiveTarget.TargetId}"); + + return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult(); } /// - public async Task GetJobResultAsync( - IChannel channel, - string jobId) + public async Task GetJobResultAsync(IChannel channel, string jobId) { if (ActiveWorkspace == null) { @@ -306,7 +288,7 @@ public async Task GetJobResultAsync( jobId = MostRecentJobId; } - var job = ActiveWorkspace.GetJob(jobId); + var job = await ActiveWorkspace.GetJobAsync(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); @@ -319,25 +301,22 @@ public async Task GetJobResultAsync( 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(); } /// - public async Task GetJobStatusAsync( - IChannel channel, - string jobId) + public async Task GetJobStatusAsync(IChannel channel, string jobId) { if (ActiveWorkspace == null) { @@ -356,20 +335,18 @@ public async Task GetJobStatusAsync( jobId = MostRecentJobId; } - var job = ActiveWorkspace.GetJob(jobId); + var job = await ActiveWorkspace.GetJobAsync(jobId); if (job == null) { channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace."); return AzureClientError.JobNotFound.ToExecutionResult(); } - // TODO: Add encoder for CloudJob which calls ToJupyterTable() for display. - return job.Details.ToExecutionResult(); + return job.ToExecutionResult(); } /// - public async Task GetJobListAsync( - IChannel channel) + public async Task GetJobListAsync(IChannel channel) { if (ActiveWorkspace == null) { @@ -377,15 +354,13 @@ public async Task GetJobListAsync( return AzureClientError.NotConnected.ToExecutionResult(); } - var jobs = ActiveWorkspace.ListJobs(); - 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(); } - - // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return jobs.Select(job => job.Details).ToJupyterTable().ToExecutionResult(); + + return jobs.ToExecutionResult(); } } } 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/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 20db5920ae..66dc1de62d 100644 --- a/src/AzureClient/AzureEnvironment.cs +++ b/src/AzureClient/AzureEnvironment.cs @@ -1,74 +1,170 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable 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 { - internal enum AzureEnvironmentType { Production, Canary, Dogfood }; + internal enum AzureEnvironmentType { Production, Canary, Dogfood, Mock }; 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 static string EnvironmentVariableName => "AZURE_QUANTUM_ENV"; + 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 azureEnvironmentName = System.Environment.GetEnvironmentVariable(EnvironmentVariableName); + + 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); + case AzureEnvironmentType.Mock: + return Mock(); default: throw new InvalidOperationException("Unexpected EnvironmentType value."); } } - return Production(); + return Production(subscriptionId); } - private static AzureEnvironment Production() => + 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); + 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(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; } + 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 2f0d5c89d1..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,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; } = string.Empty; + 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/AzureSubmissionContext.cs b/src/AzureClient/AzureSubmissionContext.cs new file mode 100644 index 0000000000..f299d90694 --- /dev/null +++ b/src/AzureClient/AzureSubmissionContext.cs @@ -0,0 +1,92 @@ +// Copyright (c) Microsoft Corporation. +// 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/AzureWorkspace.cs b/src/AzureClient/AzureWorkspace.cs new file mode 100644 index 0000000000..a5083c442b --- /dev/null +++ b/src/AzureClient/AzureWorkspace.cs @@ -0,0 +1,78 @@ +// Copyright (c) Microsoft Corporation. +// 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/EntryPoint/EntryPoint.cs b/src/AzureClient/EntryPoint/EntryPoint.cs new file mode 100644 index 0000000000..17d10806ed --- /dev/null +++ b/src/AzureClient/EntryPoint/EntryPoint.cs @@ -0,0 +1,91 @@ +// 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, AzureSubmissionContext submissionContext) + { + var parameterTypes = new List(); + var parameterValues = new List(); + foreach (var parameter in OperationInfo.RoslynParameters) + { + if (!submissionContext.InputParameters.ContainsKey(parameter.Name)) + { + throw new ArgumentException($"Required parameter {parameter.Name} was not specified."); + } + + string rawParameterValue = submissionContext.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, SubmissionContext context) + var submitMethod = typeof(IQuantumMachine) + .GetMethods() + .Single(method => + method.Name == "SubmitAsync" + && method.IsGenericMethodDefinition + && method.GetParameters().Length == 3 + && method.GetParameters()[0].ParameterType.GetGenericTypeDefinition() == EntryPointInfo.GetType().GetGenericTypeDefinition() + && method.GetParameters()[1].ParameterType.IsGenericMethodParameter + && method.GetParameters()[2].ParameterType == typeof(IQuantumMachineSubmissionContext)) + .MakeGenericMethod(new Type[] { InputType, OutputType }); + var submitParameters = new object[] { EntryPointInfo, entryPointInput, submissionContext }; + 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..aa3f78caa5 --- /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 object representing the submission context for the job. + /// The details of the submitted job. + public Task SubmitAsync(IQuantumMachine machine, AzureSubmissionContext submissionContext); + } +} 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..6e4cc96c56 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -1,18 +1,15 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #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.Client; -using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Jupyter.Core; -using Microsoft.Quantum.Runtime; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -27,6 +24,7 @@ public static class Extensions public static void AddAzureClient(this IServiceCollection services) { services.AddSingleton(); + services.AddSingleton(); } /// @@ -35,88 +33,34 @@ 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, - Output = azureClientError.ToDescription() + Output = azureClientError, }; - /// - /// 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) => + internal 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), - }, - 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 f7df675652..441abc7bff 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -1,74 +1,15 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. #nullable enable +using System.Collections.Generic; 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, - - /// - /// 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, - - /// - /// No Q# operation name was provided where one was required. - /// - [Description(Resources.AzureClientErrorNoOperationName)] - NoOperationName, - - /// - /// 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. @@ -81,8 +22,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 +36,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 +44,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, AzureSubmissionContext submissionContext); /// /// Executes the specified Q# operation as a job to the currently active target @@ -117,10 +53,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, AzureSubmissionContext submissionContext); /// /// Sets the specified target for job submission. @@ -128,19 +61,15 @@ 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 targetId); /// /// Gets the currently specified target for job submission. /// /// - /// The target name. + /// The target ID. /// - public Task GetActiveTargetAsync( - IChannel channel); + public Task GetActiveTargetAsync(IChannel channel); /// /// Gets the result of a specified job. @@ -149,9 +78,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 +87,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 +95,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/IAzureWorkspace.cs b/src/AzureClient/IAzureWorkspace.cs new file mode 100644 index 0000000000..1c2c71b813 --- /dev/null +++ b/src/AzureClient/IAzureWorkspace.cs @@ -0,0 +1,23 @@ +// Copyright (c) Microsoft Corporation. +// 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 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 8f69532952..122882abe5 100644 --- a/src/AzureClient/Magic/ExecuteMagic.cs +++ b/src/AzureClient/Magic/ExecuteMagic.cs @@ -1,11 +1,10 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -17,24 +16,13 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// 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 +48,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 @@ -70,9 +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); - return await AzureClient.ExecuteJobAsync(channel, OperationResolver, operationName); + return await AzureClient.ExecuteJobAsync(channel, AzureSubmissionContext.Parse(input)); } } } \ No newline at end of file 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 f4b722f556..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 @@ -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..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 @@ -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 8d779646d5..3c7bcfccdd 100644 --- a/src/AzureClient/Magic/SubmitMagic.cs +++ b/src/AzureClient/Magic/SubmitMagic.cs @@ -1,11 +1,9 @@ -// Copyright (c) Microsoft Corporation. All rights reserved. +// 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.Jupyter.Core; using Microsoft.Quantum.IQSharp.Jupyter; @@ -17,24 +15,13 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// 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 +45,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 @@ -67,9 +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); - return await AzureClient.SubmitJobAsync(channel, OperationResolver, operationName); + return await AzureClient.SubmitJobAsync(channel, AzureSubmissionContext.Parse(input)); } } } \ No newline at end of file diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 7e5e2a4151..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 @@ -17,9 +17,7 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class TargetMagic : AzureClientMagicBase { - private const string ParameterNameTargetName = "name"; - - private IReferences? References { get; set; } + private const string ParameterNameTargetId = "id"; /// /// Initializes a new instance of the class. @@ -27,10 +25,7 @@ public class TargetMagic : AzureClientMagicBase /// /// 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", @@ -50,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(), @" @@ -62,19 +57,19 @@ available in the workspace. ``` ".Dedent(), }, - }) => - References = references; + }) + { } /// /// 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)) + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameTargetId); + if (inputParameters.ContainsKey(ParameterNameTargetId)) { - string targetName = inputParameters.DecodeParameter(ParameterNameTargetName); - return await AzureClient.SetActiveTargetAsync(channel, References, targetName); + string targetId = inputParameters.DecodeParameter(ParameterNameTargetId); + return await AzureClient.SetActiveTargetAsync(channel, targetId); } return await AzureClient.GetActiveTargetAsync(channel); 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/Resources.cs b/src/AzureClient/Resources.cs index 1fedce95f2..f77ad445d9 100644 --- a/src/AzureClient/Resources.cs +++ b/src/AzureClient/Resources.cs @@ -25,9 +25,21 @@ 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."; + 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/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 new file mode 100644 index 0000000000..d4b20ced6f --- /dev/null +++ b/src/AzureClient/Visualization/CloudJobEncoders.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. +// 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, + ["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) => + 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..41f3f0698b --- /dev/null +++ b/src/AzureClient/Visualization/HistogramEncoders.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. +// 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..394370155a --- /dev/null +++ b/src/AzureClient/Visualization/JsonConverters.cs @@ -0,0 +1,28 @@ +// Copyright (c) Microsoft Corporation. +// 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(), + new AzureClientErrorJsonConverter() + ); + + 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..6925163fcb --- /dev/null +++ b/src/AzureClient/Visualization/TargetStatusEncoders.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. +// 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, + ["current_availability"] = target.CurrentAvailability, + ["average_queue_time"] = 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/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/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/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/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/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 new file mode 100644 index 0000000000..a4a121c994 --- /dev/null +++ b/src/Tests/AzureClientEntryPointTests.cs @@ -0,0 +1,135 @@ +// 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 AzureSubmissionContext()); + 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 AzureSubmissionContext() { InputParameters = 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 AzureSubmissionContext() { InputParameters = 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 AzureSubmissionContext() { InputParameters = 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 AzureSubmissionContext()); + 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)); + } + } +} diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index fbbc880253..b06833d336 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() @@ -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); - targetMagic.Test(targetName); + var targetMagic = new TargetMagic(azureClient); + targetMagic.Test(targetId); 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); } @@ -178,33 +172,33 @@ 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, IReferences references, 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, IOperationResolver operationResolver, string operationName) + 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, IOperationResolver operationResolver, string operationName) + public async Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext) { LastAction = AzureClientAction.ExecuteJob; - ExecutedJobs.Add(operationName); + ExecutedJobs.Add(submissionContext.OperationName); return ExecuteStatus.Ok.ToExecutionResult(); } diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index af75ade1b8..099c251268 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -3,74 +3,266 @@ #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 references = services.GetService(); - 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 name, but not yet connected - var result = azureClient.SetActiveTargetAsync(new MockChannel(), references, "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 name - result = azureClient.SetActiveTargetAsync(new MockChannel(), references, "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] 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); - 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"); + targetId = "ionq.targetId"; + executionTarget = AzureExecutionTarget.Create(targetId); + Assert.AreEqual(targetId, executionTarget?.TargetId); + Assert.AreEqual("Microsoft.Quantum.Providers.IonQ", executionTarget?.PackageName); + + targetId = "HonEYWEll.targetId"; + executionTarget = AzureExecutionTarget.Create(targetId); + Assert.AreEqual(targetId, executionTarget?.TargetId); + Assert.AreEqual("Microsoft.Quantum.Providers.Honeywell", executionTarget?.PackageName); + + targetId = "qci.target.name.qpu"; + executionTarget = AzureExecutionTarget.Create(targetId); + 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); } } } 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", ] }