Skip to content
This repository was archived by the owner on Jan 12, 2024. It is now read-only.
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
121 changes: 92 additions & 29 deletions src/AzureClient/AzureClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -17,19 +17,30 @@
using Microsoft.Azure.Quantum.Client.Models;
using Microsoft.Azure.Quantum.Storage;
using Microsoft.Azure.Quantum;
using Newtonsoft.Json;
using Newtonsoft.Json.Linq;

namespace Microsoft.Quantum.IQSharp.AzureClient
{
/// <inheritdoc/>
public class AzureClient : IAzureClient
{
private string ConnectionString { get; set; } = string.Empty;
private string ActiveTargetName { get; set; } = string.Empty;
private AzureExecutionTarget? ActiveTarget { get; set; }
private AuthenticationResult? AuthenticationResult { get; set; }
private IQuantumClient? QuantumClient { get; set; }
private IPage<ProviderStatus>? ProviderStatusList { get; set; }
private Azure.Quantum.IWorkspace? ActiveWorkspace { get; set; }
private string MostRecentJobId { get; set; } = string.Empty;
private IPage<ProviderStatus>? AvailableProviders { get; set; }
private IEnumerable<TargetStatus>? AvailableTargets { get => AvailableProviders?.SelectMany(provider => provider.Targets); }
private IEnumerable<TargetStatus>? ValidExecutionTargets { get => AvailableTargets?.Where(target => AzureExecutionTarget.IsValid(target.Id)); }
private string ValidExecutionTargetsDisplayText
{
get => ValidExecutionTargets == null
? "(no execution targets available)"
: string.Join(", ", ValidExecutionTargets.Select(target => target.Id));
}


/// <inheritdoc/>
public async Task<ExecutionResult> ConnectAsync(
Expand Down Expand Up @@ -112,7 +123,7 @@ public async Task<ExecutionResult> ConnectAsync(

try
{
ProviderStatusList = await QuantumClient.Providers.GetStatusAsync();
AvailableProviders = await QuantumClient.Providers.GetStatusAsync();
}
catch (Exception e)
{
Expand All @@ -122,22 +133,22 @@ public async Task<ExecutionResult> ConnectAsync(

channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}.");

// TODO: Add encoder for IPage<ProviderStatus> rather than calling ToJupyterTable() here directly.
return ProviderStatusList.ToJupyterTable().ToExecutionResult();
// TODO: Add encoder for IEnumerable<TargetStatus> rather than calling ToJupyterTable() here directly.
return ValidExecutionTargets.ToJupyterTable().ToExecutionResult();
}

/// <inheritdoc/>
public async Task<ExecutionResult> GetConnectionStatusAsync(IChannel channel)
{
if (QuantumClient == null || ProviderStatusList == null)
if (QuantumClient == null || AvailableProviders == null)
{
return AzureClientError.NotConnected.ToExecutionResult();
}

channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}.");

// TODO: Add encoder for IPage<ProviderStatus> rather than calling ToJupyterTable() here directly.
return ProviderStatusList.ToJupyterTable().ToExecutionResult();
// TODO: Add encoder for IEnumerable<TargetStatus> rather than calling ToJupyterTable() here directly.
return ValidExecutionTargets.ToJupyterTable().ToExecutionResult();
}

private async Task<ExecutionResult> SubmitOrExecuteJobAsync(
Expand All @@ -152,7 +163,7 @@ private async Task<ExecutionResult> SubmitOrExecuteJobAsync(
return AzureClientError.NotConnected.ToExecutionResult();
}

if (ActiveTargetName == null)
if (ActiveTarget == null)
{
channel.Stderr("Please call %azure.target before submitting a job.");
return AzureClientError.NoTarget.ToExecutionResult();
Expand All @@ -165,11 +176,12 @@ private async Task<ExecutionResult> SubmitOrExecuteJobAsync(
return AzureClientError.NoOperationName.ToExecutionResult();
}

var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTargetName, ConnectionString);
var machine = Azure.Quantum.QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetName, ConnectionString);
if (machine == null)
{
channel.Stderr($"Could not find an execution target for target {ActiveTargetName}.");
return AzureClientError.NoTarget.ToExecutionResult();
// We should never get here, since ActiveTarget should have already been validated at the time it was set.
channel.Stderr($"Unexpected error while preparing job for execution on target {ActiveTarget.TargetName}.");
return AzureClientError.InvalidTarget.ToExecutionResult();
}

var operationInfo = operationResolver.Resolve(operationName);
Expand All @@ -178,15 +190,21 @@ private async Task<ExecutionResult> SubmitOrExecuteJobAsync(

if (execute)
{
channel.Stdout($"Executing {operationName} on target {ActiveTarget.TargetName}...");
var output = await machine.ExecuteAsync(entryPointInfo, entryPointInput);
MostRecentJobId = output.Job.Id;
// TODO: Add encoder for IQuantumMachineOutput rather than returning the Histogram directly

// TODO: Add encoder to visualize IEnumerable<KeyValuePair<string, double>>
return output.Histogram.ToExecutionResult();
}
else
{
channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}...");
var job = await machine.SubmitAsync(entryPointInfo, entryPointInput);
channel.Stdout($"Job {job.Id} submitted successfully.");

MostRecentJobId = job.Id;

// TODO: Add encoder for IQuantumMachineJob rather than calling ToJupyterTable() here.
return job.ToJupyterTable().ToExecutionResult();
}
Expand All @@ -210,19 +228,60 @@ public async Task<ExecutionResult> ExecuteJobAsync(
public async Task<ExecutionResult> GetActiveTargetAsync(
IChannel channel)
{
// TODO: This should also print the list of available targets to the IChannel.
return ActiveTargetName.ToExecutionResult();
if (AvailableProviders == null)
{
channel.Stderr("Please call %azure.connect before getting the execution target.");
return AzureClientError.NotConnected.ToExecutionResult();
}

if (ActiveTarget == null)
{
channel.Stderr("No execution target has been specified. To specify one, run:\n%azure.target <target name>");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
return AzureClientError.NoTarget.ToExecutionResult();
}

channel.Stdout($"Current execution target: {ActiveTarget.TargetName}");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
return ActiveTarget.TargetName.ToExecutionResult();
}

/// <inheritdoc/>
public async Task<ExecutionResult> SetActiveTargetAsync(
IChannel channel,
IReferences references,
string targetName)
{
// TODO: Validate that this target name is valid in the workspace.
// TODO: Load the associated provider package.
ActiveTargetName = targetName;
return $"Active target is now {ActiveTargetName}".ToExecutionResult();
if (AvailableProviders == null)
{
channel.Stderr("Please call %azure.connect before setting an execution target.");
return AzureClientError.NotConnected.ToExecutionResult();
}

// Validate that this target name is valid in the workspace.
if (!AvailableTargets.Any(target => targetName == target.Id))
{
channel.Stderr($"Target name {targetName} is not available in the current Azure Quantum workspace.");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
return AzureClientError.InvalidTarget.ToExecutionResult();
}

// Validate that we know which package to load for this target name.
var executionTarget = AzureExecutionTarget.Create(targetName);
if (executionTarget == null)
{
channel.Stderr($"Target name {targetName} does not support executing Q# jobs.");
channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
return AzureClientError.InvalidTarget.ToExecutionResult();
}

// Set the active target and load the package.
ActiveTarget = executionTarget;

channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies...");
await references.AddPackage(ActiveTarget.PackageName);

return $"Active target is now {ActiveTarget.TargetName}".ToExecutionResult();
}

/// <inheritdoc/>
Expand Down Expand Up @@ -256,19 +315,23 @@ public async Task<ExecutionResult> GetJobResultAsync(

if (!job.Succeeded || string.IsNullOrEmpty(job.Details.OutputDataUri))
{
channel.Stderr($"Job ID {jobId} has not completed. Displaying the status instead.");
// TODO: Add encoder for CloudJob rather than calling ToJupyterTable() here directly.
return job.Details.ToJupyterTable().ToExecutionResult();
channel.Stderr($"Job ID {jobId} has not completed. To check the status, use:\n %azure.status {jobId}");
return AzureClientError.JobNotCompleted.ToExecutionResult();
}

var stream = new MemoryStream();
var protocol = await new JobStorageHelper(ConnectionString).DownloadJobOutputAsync(jobId, stream);
await new JobStorageHelper(ConnectionString).DownloadJobOutputAsync(jobId, stream);
stream.Seek(0, SeekOrigin.Begin);
var outputJson = new StreamReader(stream).ReadToEnd();
var output = new StreamReader(stream).ReadToEnd();
var deserializedOutput = JsonConvert.DeserializeObject<Dictionary<string, JToken>>(output);
var histogram = new Dictionary<string, double>();
foreach (var entry in deserializedOutput["histogram"] as JObject)
{
histogram[entry.Key] = entry.Value.ToObject<double>();
}

// TODO: Deserialize this once we have a way of getting the output type
// TODO: Add encoder for job output
return outputJson.ToExecutionResult();
// TODO: Add encoder to visualize IEnumerable<KeyValuePair<string, double>>
return histogram.ToExecutionResult();
}

/// <inheritdoc/>
Expand Down Expand Up @@ -300,8 +363,8 @@ public async Task<ExecutionResult> GetJobStatusAsync(
return AzureClientError.JobNotFound.ToExecutionResult();
}

// TODO: Add encoder for CloudJob rather than calling ToJupyterTable() here directly.
return job.Details.ToJupyterTable().ToExecutionResult();
// TODO: Add encoder for CloudJob which calls ToJupyterTable() for display.
return job.Details.ToExecutionResult();
}

/// <inheritdoc/>
Expand Down
1 change: 0 additions & 1 deletion src/AzureClient/AzureEnvironment.cs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@

#nullable enable

using Microsoft.Quantum.Simulation.Common;
using System;
using System.Collections.Generic;
using System.Linq;
Expand Down
44 changes: 44 additions & 0 deletions src/AzureClient/AzureExecutionTarget.cs
Original file line number Diff line number Diff line change
@@ -0,0 +1,44 @@
// Copyright (c) Microsoft Corporation. All rights reserved.
// Licensed under the MIT License.

#nullable enable

using System;

namespace Microsoft.Quantum.IQSharp.AzureClient
{
internal enum AzureProvider { IonQ, Honeywell, QCI }

internal class AzureExecutionTarget
{
public string TargetName { get; private set; }
public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetName)}";

public static bool IsValid(string targetName) => GetProvider(targetName) != null;

public static AzureExecutionTarget? Create(string targetName) =>
IsValid(targetName)
? new AzureExecutionTarget() { TargetName = targetName }
: null;

/// <summary>
/// Gets the Azure Quantum provider corresponding to the given execution target.
/// </summary>
/// <param name="targetName">The Azure Quantum execution target name.</param>
/// <returns>The <see cref="AzureProvider"/> enum value representing the provider.</returns>
/// <remarks>
/// Valid target names are structured as "provider.target".
/// For example, "ionq.simulator" or "honeywell.qpu".
/// </remarks>
private static AzureProvider? GetProvider(string targetName)
{
var parts = targetName.Split('.', 2);
if (Enum.TryParse(parts[0], true, out AzureProvider provider))
{
return provider;
}

return null;
}
}
}
5 changes: 2 additions & 3 deletions src/AzureClient/Extensions.cs
Original file line number Diff line number Diff line change
Expand Up @@ -90,7 +90,6 @@ internal static Table<IQuantumMachineJob> ToJupyterTable(this IQuantumMachineJob
{
("JobId", job => job.Id),
("JobStatus", job => job.Status),
("JobUri", job => job.Uri.ToString()),
},
Rows = new List<IQuantumMachineJob>() { job }
};
Expand All @@ -107,7 +106,7 @@ internal static Table<IQuantumClient> ToJupyterTable(this IQuantumClient quantum
Rows = new List<IQuantumClient>() { quantumClient }
};

internal static Table<TargetStatus> ToJupyterTable(this IEnumerable<ProviderStatus> providerStatusList) =>
internal static Table<TargetStatus> ToJupyterTable(this IEnumerable<TargetStatus> targets) =>
new Table<TargetStatus>
{
Columns = new List<(string, Func<TargetStatus, string>)>
Expand All @@ -117,7 +116,7 @@ internal static Table<TargetStatus> ToJupyterTable(this IEnumerable<ProviderStat
("AverageQueueTime", target => target.AverageQueueTime.ToString()),
("StatusPage", target => target.StatusPage),
},
Rows = providerStatusList.SelectMany(provider => provider.Targets).ToList()
Rows = targets.ToList()
};
}
}
27 changes: 20 additions & 7 deletions src/AzureClient/IAzureClient.cs
Original file line number Diff line number Diff line change
Expand Up @@ -18,43 +18,55 @@ public enum AzureClientError
/// Method completed with an unknown error.
/// </summary>
[Description(Resources.AzureClientErrorUnknownError)]
UnknownError = 0,
UnknownError,

/// <summary>
/// No connection has been made to any Azure Quantum workspace.
/// </summary>
[Description(Resources.AzureClientErrorNotConnected)]
NotConnected = 1,
NotConnected,

/// <summary>
/// A target has not yet been configured for job submission.
/// </summary>
[Description(Resources.AzureClientErrorNoTarget)]
NoTarget = 2,
NoTarget,

/// <summary>
/// The specified target is not valid for job submission.
/// </summary>
[Description(Resources.AzureClientErrorInvalidTarget)]
InvalidTarget,

/// <summary>
/// A job meeting the specified criteria was not found.
/// </summary>
[Description(Resources.AzureClientErrorJobNotFound)]
JobNotFound = 3,
JobNotFound,

/// <summary>
/// The result of a job was requested, but the job has not yet completed.
/// </summary>
[Description(Resources.AzureClientErrorJobNotCompleted)]
JobNotCompleted,

/// <summary>
/// No Q# operation name was provided where one was required.
/// </summary>
[Description(Resources.AzureClientErrorNoOperationName)]
NoOperationName = 4,
NoOperationName,

/// <summary>
/// Authentication with the Azure service failed.
/// </summary>
[Description(Resources.AzureClientErrorAuthenticationFailed)]
AuthenticationFailed = 5,
AuthenticationFailed,

/// <summary>
/// A workspace meeting the specified criteria was not found.
/// </summary>
[Description(Resources.AzureClientErrorWorkspaceNotFound)]
WorkspaceNotFound = 6,
WorkspaceNotFound,
}

/// <summary>
Expand Down Expand Up @@ -118,6 +130,7 @@ public Task<ExecutionResult> ExecuteJobAsync(
/// </returns>
public Task<ExecutionResult> SetActiveTargetAsync(
IChannel channel,
IReferences references,
string targetName);

/// <summary>
Expand Down
Loading