diff --git a/build/test.ps1 b/build/test.ps1 index 88bf8583a3..b259698eca 100644 --- a/build/test.ps1 +++ b/build/test.ps1 @@ -41,7 +41,7 @@ function Test-Python { Write-Host "##[info]Testing Python inside $testFolder" Push-Location (Join-Path $PSScriptRoot $testFolder) python --version - pytest --log-level=DEBUG + pytest -v --log-level=DEBUG Pop-Location if ($LastExitCode -ne 0) { diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs index a282d18788..4ec62ed001 100644 --- a/src/AzureClient/AzureClient.cs +++ b/src/AzureClient/AzureClient.cs @@ -7,11 +7,11 @@ using System.Collections.Generic; using System.IO; using System.Linq; +using System.Net; using System.Threading.Tasks; using Microsoft.Azure.Quantum; using Microsoft.Azure.Quantum.Client; using Microsoft.Azure.Quantum.Client.Models; -using Microsoft.Azure.Quantum.Storage; using Microsoft.Extensions.Logging; using Microsoft.Identity.Client; using Microsoft.Identity.Client.Extensions.Msal; @@ -19,8 +19,6 @@ using Microsoft.Quantum.IQSharp.Common; using Microsoft.Quantum.Simulation.Common; using Microsoft.Rest.Azure; -using Newtonsoft.Json; -using Newtonsoft.Json.Linq; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -47,6 +45,7 @@ private string ValidExecutionTargetsDisplayText } public AzureClient( + IExecutionEngine engine, IReferences references, IEntryPointGenerator entryPointGenerator, ILogger logger, @@ -56,6 +55,16 @@ public AzureClient( EntryPointGenerator = entryPointGenerator; Logger = logger; eventService?.TriggerServiceInitialized(this); + + if (engine is BaseEngine baseEngine) + { + baseEngine.RegisterDisplayEncoder(new CloudJobToHtmlEncoder()); + baseEngine.RegisterDisplayEncoder(new CloudJobToTextEncoder()); + baseEngine.RegisterDisplayEncoder(new TargetStatusToHtmlEncoder()); + baseEngine.RegisterDisplayEncoder(new TargetStatusToTextEncoder()); + baseEngine.RegisterDisplayEncoder(new HistogramToHtmlEncoder()); + baseEngine.RegisterDisplayEncoder(new HistogramToTextEncoder()); + } } /// @@ -148,8 +157,7 @@ public async Task ConnectAsync(IChannel channel, channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}."); - // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return ValidExecutionTargets.ToJupyterTable().ToExecutionResult(); + return ValidExecutionTargets.ToExecutionResult(); } /// @@ -162,8 +170,7 @@ public async Task GetConnectionStatusAsync(IChannel channel) channel.Stdout($"Connected to Azure Quantum workspace {QuantumClient.WorkspaceName}."); - // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return ValidExecutionTargets.ToJupyterTable().ToExecutionResult(); + return ValidExecutionTargets.ToExecutionResult(); } private async Task SubmitOrExecuteJobAsync(IChannel channel, string operationName, Dictionary inputParameters, bool execute) @@ -187,20 +194,20 @@ private async Task SubmitOrExecuteJobAsync(IChannel channel, st return AzureClientError.NoOperationName.ToExecutionResult(); } - var machine = QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetName, ConnectionString); + var machine = QuantumMachineFactory.CreateMachine(ActiveWorkspace, ActiveTarget.TargetId, ConnectionString); if (machine == null) { // We should never get here, since ActiveTarget should have already been validated at the time it was set. - channel.Stderr($"Unexpected error while preparing job for execution on target {ActiveTarget.TargetName}."); + channel.Stderr($"Unexpected error while preparing job for execution on target {ActiveTarget.TargetId}."); return AzureClientError.InvalidTarget.ToExecutionResult(); } - channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetName}..."); + channel.Stdout($"Submitting {operationName} to target {ActiveTarget.TargetId}..."); IEntryPoint? entryPoint = null; try { - entryPoint = EntryPointGenerator.Generate(operationName, ActiveTarget.TargetName); + entryPoint = EntryPointGenerator.Generate(operationName, ActiveTarget.TargetId); } catch (UnsupportedOperationException e) { @@ -279,18 +286,18 @@ public async Task GetActiveTargetAsync(IChannel channel) if (ActiveTarget == null) { - channel.Stderr("No execution target has been specified. To specify one, run:\n%azure.target "); + channel.Stderr("No execution target has been specified. To specify one, run:\n%azure.target "); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); return AzureClientError.NoTarget.ToExecutionResult(); } - channel.Stdout($"Current execution target: {ActiveTarget.TargetName}"); + channel.Stdout($"Current execution target: {ActiveTarget.TargetId}"); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); - return ActiveTarget.TargetName.ToExecutionResult(); + return ActiveTarget.TargetId.ToExecutionResult(); } /// - public async Task SetActiveTargetAsync(IChannel channel, string targetName) + public async Task SetActiveTargetAsync(IChannel channel, string targetId) { if (AvailableProviders == null) { @@ -298,19 +305,19 @@ public async Task SetActiveTargetAsync(IChannel channel, string return AzureClientError.NotConnected.ToExecutionResult(); } - // Validate that this target name is valid in the workspace. - if (!AvailableTargets.Any(target => targetName == target.Id)) + // Validate that this target is valid in the workspace. + if (!AvailableTargets.Any(target => targetId == target.Id)) { - channel.Stderr($"Target name {targetName} is not available in the current Azure Quantum workspace."); + channel.Stderr($"Target {targetId} is not available in the current Azure Quantum workspace."); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); return AzureClientError.InvalidTarget.ToExecutionResult(); } - // Validate that we know which package to load for this target name. - var executionTarget = AzureExecutionTarget.Create(targetName); + // Validate that we know which package to load for this target. + var executionTarget = AzureExecutionTarget.Create(targetId); if (executionTarget == null) { - channel.Stderr($"Target name {targetName} does not support executing Q# jobs."); + channel.Stderr($"Target {targetId} does not support executing Q# jobs."); channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}"); return AzureClientError.InvalidTarget.ToExecutionResult(); } @@ -321,7 +328,7 @@ public async Task SetActiveTargetAsync(IChannel channel, string channel.Stdout($"Loading package {ActiveTarget.PackageName} and dependencies..."); await References.AddPackage(ActiveTarget.PackageName); - return $"Active target is now {ActiveTarget.TargetName}".ToExecutionResult(); + return $"Active target is now {ActiveTarget.TargetId}".ToExecutionResult(); } /// @@ -357,19 +364,18 @@ public async Task GetJobResultAsync(IChannel channel, string jo return AzureClientError.JobNotCompleted.ToExecutionResult(); } - var stream = new MemoryStream(); - await new JobStorageHelper(ConnectionString).DownloadJobOutputAsync(jobId, stream); - stream.Seek(0, SeekOrigin.Begin); - var output = new StreamReader(stream).ReadToEnd(); - var deserializedOutput = JsonConvert.DeserializeObject>(output); - var histogram = new Dictionary(); - foreach (var entry in deserializedOutput["histogram"] as JObject) + try { - histogram[entry.Key] = entry.Value.ToObject(); + var request = WebRequest.Create(job.Details.OutputDataUri); + using var responseStream = request.GetResponse().GetResponseStream(); + return responseStream.ToHistogram().ToExecutionResult(); + } + catch (Exception e) + { + channel.Stderr($"Failed to retrieve results for job ID {jobId}."); + Logger?.LogError(e, $"Failed to download the job output for the specified Azure Quantum job: {e.Message}"); + return AzureClientError.JobOutputDownloadFailed.ToExecutionResult(); } - - // TODO: Add encoder to visualize IEnumerable> - return histogram.ToExecutionResult(); } /// @@ -399,8 +405,7 @@ public async Task GetJobStatusAsync(IChannel channel, string jo return AzureClientError.JobNotFound.ToExecutionResult(); } - // TODO: Add encoder for CloudJob rather than calling ToJupyterTable() here directly. - return job.ToJupyterTable().ToExecutionResult(); + return job.ToExecutionResult(); } /// @@ -419,8 +424,7 @@ public async Task GetJobListAsync(IChannel channel) return AzureClientError.JobNotFound.ToExecutionResult(); } - // TODO: Add encoder for IEnumerable rather than calling ToJupyterTable() here directly. - return jobs.ToJupyterTable().ToExecutionResult(); + return jobs.ToExecutionResult(); } private async Task GetCloudJob(string jobId) diff --git a/src/AzureClient/AzureExecutionTarget.cs b/src/AzureClient/AzureExecutionTarget.cs index 2f0d5c89d1..d56064efa5 100644 --- a/src/AzureClient/AzureExecutionTarget.cs +++ b/src/AzureClient/AzureExecutionTarget.cs @@ -11,28 +11,28 @@ internal enum AzureProvider { IonQ, Honeywell, QCI } internal class AzureExecutionTarget { - public string TargetName { get; private set; } - public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetName)}"; + public string TargetId { get; private set; } + public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetId)}"; - public static bool IsValid(string targetName) => GetProvider(targetName) != null; + public static bool IsValid(string targetId) => GetProvider(targetId) != null; - public static AzureExecutionTarget? Create(string targetName) => - IsValid(targetName) - ? new AzureExecutionTarget() { TargetName = targetName } + public static AzureExecutionTarget? Create(string targetId) => + IsValid(targetId) + ? new AzureExecutionTarget() { TargetId = targetId } : null; /// /// Gets the Azure Quantum provider corresponding to the given execution target. /// - /// The Azure Quantum execution target name. + /// The Azure Quantum execution target ID. /// The enum value representing the provider. /// - /// Valid target names are structured as "provider.target". + /// Valid target IDs are structured as "provider.target". /// For example, "ionq.simulator" or "honeywell.qpu". /// - private static AzureProvider? GetProvider(string targetName) + private static AzureProvider? GetProvider(string targetId) { - var parts = targetName.Split('.', 2); + var parts = targetId.Split('.', 2); if (Enum.TryParse(parts[0], true, out AzureProvider provider)) { return provider; diff --git a/src/AzureClient/Extensions.cs b/src/AzureClient/Extensions.cs index 0727e8aaca..1fda56014e 100644 --- a/src/AzureClient/Extensions.cs +++ b/src/AzureClient/Extensions.cs @@ -4,16 +4,12 @@ #nullable enable using System; +using System.Collections; using System.Collections.Generic; using System.ComponentModel; -using System.Linq; using System.Threading.Tasks; -using Microsoft.Azure.Quantum; -using Microsoft.Azure.Quantum.Client; -using Microsoft.Azure.Quantum.Client.Models; using Microsoft.Extensions.DependencyInjection; using Microsoft.Jupyter.Core; -using Microsoft.Quantum.Runtime; namespace Microsoft.Quantum.IQSharp.AzureClient { @@ -37,7 +33,7 @@ public static void AddAzureClient(this IServiceCollection services) /// /// The result of an IAzureClient API call. /// - public static ExecutionResult ToExecutionResult(this AzureClientError azureClientError) => + internal static ExecutionResult ToExecutionResult(this AzureClientError azureClientError) => new ExecutionResult { Status = ExecuteStatus.Error, @@ -50,7 +46,7 @@ public static ExecutionResult ToExecutionResult(this AzureClientError azureClien /// /// /// - public static string ToDescription(this AzureClientError azureClientError) + internal static string ToDescription(this AzureClientError azureClientError) { var attributes = azureClientError .GetType() @@ -65,60 +61,21 @@ public static string ToDescription(this AzureClientError azureClientError) /// /// A task which will return the result of an IAzureClient API call. /// - public static async Task ToExecutionResult(this Task task) => + internal static async Task ToExecutionResult(this Task task) => (await task).ToExecutionResult(); - internal static Table ToJupyterTable(this CloudJob cloudJob) => - new List { cloudJob }.ToJupyterTable(); - - internal static Table ToJupyterTable(this IEnumerable jobsList) => - new Table - { - Columns = new List<(string, Func)> - { - ("JobId", cloudJob => cloudJob.Id), - ("JobName", cloudJob => cloudJob.Details.Name), - ("JobStatus", cloudJob => cloudJob.Status), - ("Provider", cloudJob => cloudJob.Details.ProviderId), - ("Target", cloudJob => cloudJob.Details.Target), - }, - Rows = jobsList.ToList() - }; - - internal static Table ToJupyterTable(this IQuantumMachineJob job) => - new Table - { - Columns = new List<(string, Func)> - { - ("JobId", job => job.Id), - ("JobStatus", job => job.Status), - }, - Rows = new List() { job } - }; - - internal static Table ToJupyterTable(this IQuantumClient quantumClient) => - new Table - { - Columns = new List<(string, Func)> - { - ("SubscriptionId", quantumClient => quantumClient.SubscriptionId), - ("ResourceGroupName", quantumClient => quantumClient.ResourceGroupName), - ("WorkspaceName", quantumClient => quantumClient.WorkspaceName), - }, - Rows = new List() { quantumClient } - }; - - internal static Table ToJupyterTable(this IEnumerable targets) => - new Table - { - Columns = new List<(string, Func)> - { - ("TargetId", target => target.Id), - ("CurrentAvailability", target => target.CurrentAvailability), - ("AverageQueueTime", target => target.AverageQueueTime.ToString()), - ("StatusPage", target => target.StatusPage), - }, - Rows = targets.ToList() - }; + /// + /// Returns the provided argument as an enumeration of the specified type. + /// + /// + /// If the argument is already an of the specified type, + /// the argument is returned. If the argument is of type T, then an + /// enumeration is returned with this argument as the only element. + /// Otherwise, null is returned. + /// + internal static IEnumerable? AsEnumerableOf(this object? source) => + source is T singleton ? new List { singleton } : + source is IEnumerable collection ? collection : + null; } } diff --git a/src/AzureClient/IAzureClient.cs b/src/AzureClient/IAzureClient.cs index 952c28d796..62937522c8 100644 --- a/src/AzureClient/IAzureClient.cs +++ b/src/AzureClient/IAzureClient.cs @@ -51,6 +51,12 @@ public enum AzureClientError [Description(Resources.AzureClientErrorJobNotCompleted)] JobNotCompleted, + /// + /// The job output failed to be downloaded from the Azure storage location. + /// + [Description(Resources.AzureClientErrorJobOutputDownloadFailed)] + JobOutputDownloadFailed, + /// /// No Q# operation name was provided where one was required. /// @@ -139,13 +145,13 @@ public Task ConnectAsync(IChannel channel, /// /// Success if the target is valid, or an error if the target cannot be set. /// - public Task SetActiveTargetAsync(IChannel channel, string targetName); + public Task SetActiveTargetAsync(IChannel channel, string targetId); /// /// Gets the currently specified target for job submission. /// /// - /// The target name. + /// The target ID. /// public Task GetActiveTargetAsync(IChannel channel); diff --git a/src/AzureClient/Magic/TargetMagic.cs b/src/AzureClient/Magic/TargetMagic.cs index 2a5cf36c93..778f88ab74 100644 --- a/src/AzureClient/Magic/TargetMagic.cs +++ b/src/AzureClient/Magic/TargetMagic.cs @@ -17,7 +17,7 @@ namespace Microsoft.Quantum.IQSharp.AzureClient /// public class TargetMagic : AzureClientMagicBase { - private const string ParameterNameTargetName = "name"; + private const string ParameterNameTargetId = "id"; /// /// Initializes a new instance of the class. @@ -45,8 +45,8 @@ available in the workspace. @" Set the current target for job submission: ``` - In []: %azure.target TARGET_NAME - Out[]: Active target is now TARGET_NAME + In []: %azure.target TARGET_ID + Out[]: Active target is now TARGET_ID ``` ".Dedent(), @" @@ -65,11 +65,11 @@ available in the workspace. /// public override async Task RunAsync(string input, IChannel channel) { - var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameTargetName); - if (inputParameters.ContainsKey(ParameterNameTargetName)) + var inputParameters = ParseInputParameters(input, firstParameterInferredName: ParameterNameTargetId); + if (inputParameters.ContainsKey(ParameterNameTargetId)) { - string targetName = inputParameters.DecodeParameter(ParameterNameTargetName); - return await AzureClient.SetActiveTargetAsync(channel, targetName); + string targetId = inputParameters.DecodeParameter(ParameterNameTargetId); + return await AzureClient.SetActiveTargetAsync(channel, targetId); } return await AzureClient.GetActiveTargetAsync(channel); diff --git a/src/AzureClient/Resources.cs b/src/AzureClient/Resources.cs index a85a3cf463..f77ad445d9 100644 --- a/src/AzureClient/Resources.cs +++ b/src/AzureClient/Resources.cs @@ -25,6 +25,9 @@ internal static class Resources public const string AzureClientErrorJobNotCompleted = "The specified Azure Quantum job has not yet completed."; + public const string AzureClientErrorJobOutputDownloadFailed = + "Failed to download results for the specified Azure Quantum job."; + public const string AzureClientErrorNoOperationName = "No Q# operation name was specified for Azure Quantum job submission."; diff --git a/src/AzureClient/Visualization/CloudJobEncoders.cs b/src/AzureClient/Visualization/CloudJobEncoders.cs new file mode 100644 index 0000000000..8e5f9b5610 --- /dev/null +++ b/src/AzureClient/Visualization/CloudJobEncoders.cs @@ -0,0 +1,99 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text; +using Microsoft.Azure.Quantum; +using Microsoft.Jupyter.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal static class CloudJobExtensions + { + private static DateTime? ToDateTime(this string serializedDateTime) => + DateTime.TryParse(serializedDateTime, CultureInfo.InvariantCulture, DateTimeStyles.None, out DateTime dateTime) + ? dateTime + : null as DateTime?; + + internal static Dictionary ToDictionary(this CloudJob cloudJob) => + new Dictionary() + { + // TODO: add cloudJob.Uri after https://github.com/microsoft/qsharp-runtime/issues/236 is fixed. + ["id"] = cloudJob.Id, + ["name"] = cloudJob.Details.Name, + ["status"] = cloudJob.Status, + ["provider"] = cloudJob.Details.ProviderId, + ["target"] = cloudJob.Details.Target, + ["creationTime"] = cloudJob.Details.CreationTime.ToDateTime()?.ToUniversalTime(), + ["beginExecutionTime"] = cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToUniversalTime(), + ["endExecutionTime"] = cloudJob.Details.EndExecutionTime.ToDateTime()?.ToUniversalTime(), + }; + + internal static Table ToJupyterTable(this IEnumerable jobsList) => + new Table + { + Columns = new List<(string, Func)> + { + // TODO: add cloudJob.Uri after https://github.com/microsoft/qsharp-runtime/issues/236 is fixed. + ("Job ID", cloudJob => cloudJob.Id), + ("Job Name", cloudJob => cloudJob.Details.Name), + ("Job Status", cloudJob => cloudJob.Status), + ("Provider", cloudJob => cloudJob.Details.ProviderId), + ("Target", cloudJob => cloudJob.Details.Target), + ("Creation Time", cloudJob => cloudJob.Details.CreationTime.ToDateTime()?.ToString()), + ("Begin Execution Time", cloudJob => cloudJob.Details.BeginExecutionTime.ToDateTime()?.ToString()), + ("End Execution Time", cloudJob => cloudJob.Details.EndExecutionTime.ToDateTime()?.ToString()), + }, + Rows = jobsList.OrderByDescending(job => job.Details.CreationTime).ToList() + }; + } + + public class CloudJobToHtmlEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToHtmlDisplayEncoder(); + + public string MimeType => MimeTypes.Html; + + public EncodedData? Encode(object displayable) => + displayable.AsEnumerableOf() is IEnumerable jobs + ? tableEncoder.Encode(jobs.ToJupyterTable()) + : null; + } + + public class CloudJobToTextEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToTextDisplayEncoder(); + + public string MimeType => MimeTypes.PlainText; + + public EncodedData? Encode(object displayable) => + displayable.AsEnumerableOf() is IEnumerable jobs + ? tableEncoder.Encode(jobs.ToJupyterTable()) + : null; + } + + public class CloudJobJsonConverter : JsonConverter + { + public override CloudJob ReadJson(JsonReader reader, Type objectType, CloudJob existingValue, bool hasExistingValue, JsonSerializer serializer) + => throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, CloudJob value, JsonSerializer serializer) => + JToken.FromObject(value.ToDictionary()).WriteTo(writer); + } + + public class CloudJobListJsonConverter : JsonConverter> + { + public override IEnumerable ReadJson(JsonReader reader, Type objectType, IEnumerable existingValue, bool hasExistingValue, JsonSerializer serializer) + => throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, IEnumerable value, JsonSerializer serializer) => + JToken.FromObject(value.Select(job => job.ToDictionary())).WriteTo(writer); + } +} diff --git a/src/AzureClient/Visualization/HistogramEncoders.cs b/src/AzureClient/Visualization/HistogramEncoders.cs new file mode 100644 index 0000000000..befbbf095b --- /dev/null +++ b/src/AzureClient/Visualization/HistogramEncoders.cs @@ -0,0 +1,120 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.IO; +using System.Linq; +using Microsoft.Jupyter.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal class Histogram : Dictionary + { + } + + internal static class HistogramExtensions + { + internal static Histogram ToHistogram(this Stream stream) + { + var output = new StreamReader(stream).ReadToEnd(); + var deserializedOutput = JsonConvert.DeserializeObject>(output); + var deserializedHistogram = deserializedOutput["Histogram"] as JArray; + + var histogram = new Histogram(); + for (var i = 0; i < deserializedHistogram.Count - 1; i += 2) + { + var key = deserializedHistogram[i].ToObject(); + var value = deserializedHistogram[i + 1].ToObject(); + histogram[key] = value; + } + + return histogram; + } + + internal static Table> ToJupyterTable(this Histogram histogram) => + new Table> + { + Columns = new List<(string, Func, string>)> + { + ("Result", entry => entry.Key), + ("Frequency", entry => entry.Value.ToString()), + }, + Rows = histogram.ToList() + }; + } + + public class HistogramToHtmlEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToHtmlDisplayEncoder(); + + public string MimeType => MimeTypes.Html; + + public EncodedData? Encode(object displayable) + { + if (displayable is Histogram histogram) + { + var style = "text-align: left"; + var columnStyle = $"{style}; width: 25ch"; + var lastColumnStyle = $"{style}; width: calc(100% - 25ch - 25ch)"; + + // Make the HTML table body by formatting everything as individual rows. + var formattedData = string.Join("\n", + histogram.Select(entry => + { + var result = entry.Key; + var frequency = entry.Value; + + return FormattableString.Invariant($@" + + {result} + {frequency} + + + + + "); + }) + ); + + // Construct and return the table. + var outputTable = $@" + + + + + + + + + + {formattedData} + +
ResultFrequencyHistogram
+ "; + return outputTable.ToEncodedData(); + } + else return null; + } + } + + public class HistogramToTextEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToTextDisplayEncoder(); + + public string MimeType => MimeTypes.PlainText; + + public EncodedData? Encode(object displayable) => + displayable is Histogram histogram + ? tableEncoder.Encode(histogram.ToJupyterTable()) + : null; + } +} diff --git a/src/AzureClient/Visualization/JsonConverters.cs b/src/AzureClient/Visualization/JsonConverters.cs new file mode 100644 index 0000000000..24f26f2515 --- /dev/null +++ b/src/AzureClient/Visualization/JsonConverters.cs @@ -0,0 +1,27 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Collections.Immutable; +using System.Linq; +using System.Text; + +using Newtonsoft.Json; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + public static class JsonConverters + { + private static readonly ImmutableList allConverters = ImmutableList.Create( + new CloudJobJsonConverter(), + new CloudJobListJsonConverter(), + new TargetStatusJsonConverter(), + new TargetStatusListJsonConverter() + ); + + public static JsonConverter[] AllConverters => allConverters.ToArray(); + } +} diff --git a/src/AzureClient/Visualization/TargetStatusEncoders.cs b/src/AzureClient/Visualization/TargetStatusEncoders.cs new file mode 100644 index 0000000000..1de24e7bc0 --- /dev/null +++ b/src/AzureClient/Visualization/TargetStatusEncoders.cs @@ -0,0 +1,81 @@ +// Copyright (c) Microsoft Corporation. All rights reserved. +// Licensed under the MIT License. + +#nullable enable + +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using Microsoft.Azure.Quantum.Client.Models; +using Microsoft.Jupyter.Core; +using Newtonsoft.Json; +using Newtonsoft.Json.Linq; + +namespace Microsoft.Quantum.IQSharp.AzureClient +{ + internal static class TargetStatusExtensions + { + internal static Dictionary ToDictionary(this TargetStatus target) => + new Dictionary() + { + ["id"] = target.Id, + ["currentAvailability"] = target.CurrentAvailability, + ["averageQueueTime"] = target.AverageQueueTime, + }; + + internal static Table ToJupyterTable(this IEnumerable targets) => + new Table + { + Columns = new List<(string, Func)> + { + ("Target ID", target => target.Id), + ("Current Availability", target => target.CurrentAvailability), + ("Average Queue Time (Seconds)", target => target.AverageQueueTime.ToString()), + }, + Rows = targets.ToList() + }; + } + + public class TargetStatusToHtmlEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToHtmlDisplayEncoder(); + + public string MimeType => MimeTypes.Html; + + public EncodedData? Encode(object displayable) => + displayable.AsEnumerableOf() is IEnumerable targets + ? tableEncoder.Encode(targets.ToJupyterTable()) + : null; + } + + public class TargetStatusToTextEncoder : IResultEncoder + { + private static readonly IResultEncoder tableEncoder = new TableToTextDisplayEncoder(); + + public string MimeType => MimeTypes.PlainText; + + public EncodedData? Encode(object displayable) => + displayable.AsEnumerableOf() is IEnumerable targets + ? tableEncoder.Encode(targets.ToJupyterTable()) + : null; + } + + public class TargetStatusJsonConverter : JsonConverter + { + public override TargetStatus ReadJson(JsonReader reader, Type objectType, TargetStatus existingValue, bool hasExistingValue, JsonSerializer serializer) + => throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, TargetStatus value, JsonSerializer serializer) => + JToken.FromObject(value.ToDictionary()).WriteTo(writer); + } + + public class TargetStatusListJsonConverter : JsonConverter> + { + public override IEnumerable ReadJson(JsonReader reader, Type objectType, IEnumerable existingValue, bool hasExistingValue, JsonSerializer serializer) + => throw new NotImplementedException(); + + public override void WriteJson(JsonWriter writer, IEnumerable value, JsonSerializer serializer) => + JToken.FromObject(value.Select(job => job.ToDictionary())).WriteTo(writer); + } +} diff --git a/src/Kernel/IQSharpEngine.cs b/src/Kernel/IQSharpEngine.cs index 759b89114b..88122f5271 100644 --- a/src/Kernel/IQSharpEngine.cs +++ b/src/Kernel/IQSharpEngine.cs @@ -60,7 +60,11 @@ IMagicSymbolResolver magicSymbolResolver RegisterDisplayEncoder(new DataTableToTextEncoder()); RegisterDisplayEncoder(new DisplayableExceptionToHtmlEncoder()); RegisterDisplayEncoder(new DisplayableExceptionToTextEncoder()); - RegisterJsonEncoder(JsonConverters.AllConverters); + + RegisterJsonEncoder( + JsonConverters.AllConverters + .Concat(AzureClient.JsonConverters.AllConverters) + .ToArray()); RegisterSymbolResolver(this.SymbolsResolver); RegisterSymbolResolver(this.MagicResolver); diff --git a/src/Tests/AzureClientMagicTests.cs b/src/Tests/AzureClientMagicTests.cs index a45f1eb6c0..6e91ad9491 100644 --- a/src/Tests/AzureClientMagicTests.cs +++ b/src/Tests/AzureClientMagicTests.cs @@ -32,7 +32,7 @@ public class AzureClientMagicTests private readonly string storageAccountConnectionString = "TEST_CONNECTION_STRING"; private readonly string jobId = "TEST_JOB_ID"; private readonly string operationName = "TEST_OPERATION_NAME"; - private readonly string targetName = "TEST_TARGET_NAME"; + private readonly string targetId = "TEST_TARGET_ID"; [TestMethod] public void TestConnectMagic() @@ -142,7 +142,7 @@ public void TestTargetMagic() // single argument - should set active target var azureClient = new MockAzureClient(); var targetMagic = new TargetMagic(azureClient); - targetMagic.Test(targetName); + targetMagic.Test(targetId); Assert.AreEqual(azureClient.LastAction, AzureClientAction.SetActiveTarget); // no arguments - should print active target @@ -172,20 +172,20 @@ public class MockAzureClient : IAzureClient internal AzureClientAction LastAction = AzureClientAction.None; internal string ConnectionString = string.Empty; internal bool RefreshCredentials = false; - internal string ActiveTargetName = string.Empty; + internal string ActiveTargetId = string.Empty; internal List SubmittedJobs = new List(); internal List ExecutedJobs = new List(); - public async Task SetActiveTargetAsync(IChannel channel, string targetName) + public async Task SetActiveTargetAsync(IChannel channel, string targetId) { LastAction = AzureClientAction.SetActiveTarget; - ActiveTargetName = targetName; + ActiveTargetId = targetId; return ExecuteStatus.Ok.ToExecutionResult(); } public async Task GetActiveTargetAsync(IChannel channel) { LastAction = AzureClientAction.GetActiveTarget; - return ActiveTargetName.ToExecutionResult(); + return ActiveTargetId.ToExecutionResult(); } public async Task SubmitJobAsync(IChannel channel, string operationName, Dictionary inputParameters) diff --git a/src/Tests/AzureClientTests.cs b/src/Tests/AzureClientTests.cs index 2044b2116a..a115085104 100644 --- a/src/Tests/AzureClientTests.cs +++ b/src/Tests/AzureClientTests.cs @@ -33,11 +33,11 @@ public void TestTargets() var services = Startup.CreateServiceProvider(workspace); var azureClient = services.GetService(); - // SetActiveTargetAsync with recognized target name, but not yet connected + // SetActiveTargetAsync with recognized target ID, but not yet connected var result = azureClient.SetActiveTargetAsync(new MockChannel(), "ionq.simulator").GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Error); - // SetActiveTargetAsync with unrecognized target name + // SetActiveTargetAsync with unrecognized target ID result = azureClient.SetActiveTargetAsync(new MockChannel(), "contoso.qpu").GetAwaiter().GetResult(); Assert.IsTrue(result.Status == ExecuteStatus.Error); @@ -49,26 +49,26 @@ public void TestTargets() [TestMethod] public void TestAzureExecutionTarget() { - var targetName = "invalidname"; - var executionTarget = AzureExecutionTarget.Create(targetName); + var targetId = "invalidname"; + var executionTarget = AzureExecutionTarget.Create(targetId); Assert.IsNull(executionTarget); - targetName = "ionq.targetname"; - executionTarget = AzureExecutionTarget.Create(targetName); + targetId = "ionq.targetId"; + executionTarget = AzureExecutionTarget.Create(targetId); Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetName, targetName); + Assert.AreEqual(executionTarget.TargetId, targetId); Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.IonQ"); - targetName = "HonEYWEll.targetname"; - executionTarget = AzureExecutionTarget.Create(targetName); + targetId = "HonEYWEll.targetId"; + executionTarget = AzureExecutionTarget.Create(targetId); Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetName, targetName); + Assert.AreEqual(executionTarget.TargetId, targetId); Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.Honeywell"); - targetName = "qci.target.name.qpu"; - executionTarget = AzureExecutionTarget.Create(targetName); + targetId = "qci.target.name.qpu"; + executionTarget = AzureExecutionTarget.Create(targetId); Assert.IsNotNull(executionTarget); - Assert.AreEqual(executionTarget.TargetName, targetName); + Assert.AreEqual(executionTarget.TargetId, targetId); Assert.AreEqual(executionTarget.PackageName, "Microsoft.Quantum.Providers.QCI"); } }