diff --git a/build/manifest.ps1 b/build/manifest.ps1
index fd8da939e7..3739c01e54 100644
--- a/build/manifest.ps1
+++ b/build/manifest.ps1
@@ -1,3 +1,6 @@
+# Copyright (c) Microsoft Corporation. All rights reserved.
+# Licensed under the MIT License.
+
#!/usr/bin/env pwsh
#Requires -PSEdition Core
@@ -11,6 +14,7 @@
);
Assemblies = @(
"./src/Tool/bin/$Env:BUILD_CONFIGURATION/netcoreapp3.1/Microsoft.Quantum.IQSharp.dll",
+ "./src/Tool/bin/$Env:BUILD_CONFIGURATION/netcoreapp3.1/Microsoft.Quantum.IQSharp.AzureClient.dll",
"./src/Tool/bin/$Env:BUILD_CONFIGURATION/netcoreapp3.1/Microsoft.Quantum.IQSharp.Core.dll",
"./src/Tool/bin/$Env:BUILD_CONFIGURATION/netcoreapp3.1/Microsoft.Quantum.IQSharp.Jupyter.dll",
"./src/Tool/bin/$Env:BUILD_CONFIGURATION/netcoreapp3.1/Microsoft.Quantum.IQSharp.Kernel.dll",
diff --git a/build/steps.yml b/build/steps.yml
index 951478c664..cbf77cecee 100644
--- a/build/steps.yml
+++ b/build/steps.yml
@@ -24,6 +24,11 @@ steps:
- pwsh: .\build.ps1
displayName: "Building IQ#"
workingDirectory: '$(System.DefaultWorkingDirectory)/build'
+
+- pwsh: .\manifest.ps1
+ displayName: "List built assemblies"
+ workingDirectory: '$(System.DefaultWorkingDirectory)/build'
+ condition: succeededOrFailed()
- pwsh: .\test.ps1
displayName: "Testing IQ#"
diff --git a/build/test.ps1 b/build/test.ps1
index 7a0c737a21..2dd318fe77 100644
--- a/build/test.ps1
+++ b/build/test.ps1
@@ -41,7 +41,7 @@ function Test-Python {
Write-Host "##[info]Testing Python inside $testFolder"
Push-Location (Join-Path $PSScriptRoot $testFolder)
python --version
- pytest -v
+ pytest -v --log-level=Debug
Pop-Location
if ($LastExitCode -ne 0) {
diff --git a/conda-recipes/iqsharp/build.ps1 b/conda-recipes/iqsharp/build.ps1
index f08bdeb1ee..6c76741f98 100644
--- a/conda-recipes/iqsharp/build.ps1
+++ b/conda-recipes/iqsharp/build.ps1
@@ -17,8 +17,6 @@ if ($IsWindows) {
$RepoRoot = Resolve-Path (Join-Path $PSScriptRoot "../..");
$ArtifactRoot = Join-Path $RepoRoot "drops";
$SelfContainedDirectory = Join-Path $ArtifactRoot (Join-Path "selfcontained" $RuntimeID)
-$NugetsDirectory = Join-Path $ArtifactRoot "nugets"
-$NugetConfig = Resolve-Path (Join-Path $PSScriptRoot "NuGet.config");
$TargetDirectory = (Join-Path (Join-Path $Env:PREFIX "opt") "iqsharp");
diff --git a/conda-recipes/iqsharp/test.ps1 b/conda-recipes/iqsharp/test.ps1
index e1f14d46f0..c5cf7bd421 100644
--- a/conda-recipes/iqsharp/test.ps1
+++ b/conda-recipes/iqsharp/test.ps1
@@ -5,6 +5,18 @@ $failed = $false;
$Env:IQSHARP_PACKAGE_SOURCE = "$Env:NUGET_OUTDIR"
+# Add the prerelease NuGet feed if this isn't a release build.
+if ("$Env:BUILD_RELEASETYPE" -ne "release") {
+ $NuGetDirectory = Resolve-Path ~
+ Write-Host "## Writing prerelease NuGet config to $NuGetDirectory ##"
+ "
+
+
+
+
+ " | Out-File -FilePath $NuGetDirectory/NuGet.Config -Encoding utf8
+}
+
# Check that iqsharp is installed as a Jupyter kernel.
$kernels = jupyter kernelspec list --json | ConvertFrom-Json;
if ($null -eq $kernels.kernelspecs.iqsharp) {
@@ -13,7 +25,7 @@ if ($null -eq $kernels.kernelspecs.iqsharp) {
jupyter kernelspec list
}
-
+# Run the kernel unit tests.
Push-Location $PSScriptRoot
python test.py
if ($LastExitCode -ne 0) {
diff --git a/iqsharp.sln b/iqsharp.sln
index 67bfcd937c..e2bf548fb7 100644
--- a/iqsharp.sln
+++ b/iqsharp.sln
@@ -13,6 +13,8 @@ Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Tool", "src\Tool\Tool.cspro
EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Web", "src\Web\Web.csproj", "{6431E92B-12AA-432C-8D53-C9A7A54BA21B}"
EndProject
+Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "AzureClient", "src\AzureClient\AzureClient.csproj", "{E7B60C94-B666-4024-B53E-D12C142DE8DC}"
+EndProject
Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "Jupyter", "src\Jupyter\Jupyter.csproj", "{19A9E2AB-8842-47E2-8E6A-6DD292B49E97}"
EndProject
Global
@@ -85,6 +87,18 @@ Global
{6431E92B-12AA-432C-8D53-C9A7A54BA21B}.Release|x64.Build.0 = Release|Any CPU
{6431E92B-12AA-432C-8D53-C9A7A54BA21B}.Release|x86.ActiveCfg = Release|Any CPU
{6431E92B-12AA-432C-8D53-C9A7A54BA21B}.Release|x86.Build.0 = Release|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|Any CPU.Build.0 = Debug|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|x64.ActiveCfg = Debug|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|x64.Build.0 = Debug|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|x86.ActiveCfg = Debug|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Debug|x86.Build.0 = Debug|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|Any CPU.ActiveCfg = Release|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|Any CPU.Build.0 = Release|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|x64.ActiveCfg = Release|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|x64.Build.0 = Release|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|x86.ActiveCfg = Release|Any CPU
+ {E7B60C94-B666-4024-B53E-D12C142DE8DC}.Release|x86.Build.0 = Release|Any CPU
{19A9E2AB-8842-47E2-8E6A-6DD292B49E97}.Debug|Any CPU.ActiveCfg = Debug|Any CPU
{19A9E2AB-8842-47E2-8E6A-6DD292B49E97}.Debug|Any CPU.Build.0 = Debug|Any CPU
{19A9E2AB-8842-47E2-8E6A-6DD292B49E97}.Debug|x64.ActiveCfg = Debug|Any CPU
diff --git a/src/AzureClient/AzureClient.cs b/src/AzureClient/AzureClient.cs
new file mode 100644
index 0000000000..0abb670a55
--- /dev/null
+++ b/src/AzureClient/AzureClient.cs
@@ -0,0 +1,406 @@
+// Copyright (c) Microsoft Corporation.
+// Licensed under the MIT License.
+
+#nullable enable
+
+using System;
+using System.Collections.Generic;
+using System.Linq;
+using System.Net;
+using System.Threading;
+using System.Threading.Tasks;
+using Microsoft.Azure.Quantum;
+using Microsoft.Azure.Quantum.Client.Models;
+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 IMetadataController MetadataController { get; }
+ private bool IsPythonUserAgent => MetadataController?.UserAgent?.StartsWith("qsharp.py") ?? false;
+ private string ConnectionString { get; set; } = string.Empty;
+ private AzureExecutionTarget? ActiveTarget { get; set; }
+ private string MostRecentJobId { get; set; } = string.Empty;
+ 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,
+ IMetadataController metadataController,
+ ILogger logger,
+ IEventService eventService)
+ {
+ References = references;
+ EntryPointGenerator = entryPointGenerator;
+ MetadataController = metadataController;
+ 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(IChannel channel,
+ string subscriptionId,
+ string resourceGroupName,
+ string workspaceName,
+ string storageAccountConnectionString,
+ bool refreshCredentials = false)
+ {
+ var azureEnvironment = AzureEnvironment.Create(subscriptionId);
+ IAzureWorkspace? workspace = null;
+ try
+ {
+ workspace = await azureEnvironment.GetAuthenticatedWorkspaceAsync(channel, resourceGroupName, workspaceName, refreshCredentials);
+ }
+ catch (Exception e)
+ {
+ channel.Stderr($"The connection to the Azure Quantum workspace could not be completed. Please check the provided parameters and try again.");
+ channel.Stderr($"Error details: {e.Message}");
+ return AzureClientError.WorkspaceNotFound.ToExecutionResult();
+ }
+
+ if (workspace == null)
+ {
+ return AzureClientError.AuthenticationFailed.ToExecutionResult();
+ }
+
+ var providers = await workspace.GetProvidersAsync();
+ if (providers == null)
+ {
+ return AzureClientError.WorkspaceNotFound.ToExecutionResult();
+ }
+
+ ActiveWorkspace = workspace;
+ AvailableProviders = providers;
+ ConnectionString = storageAccountConnectionString;
+ ActiveTarget = null;
+ MostRecentJobId = string.Empty;
+
+ channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}.");
+
+ if (ValidExecutionTargets.Count() == 0)
+ {
+ channel.Stderr($"No valid Q# execution targets found in Azure Quantum workspace {ActiveWorkspace.Name}.");
+ }
+
+ return ValidExecutionTargets.ToExecutionResult();
+ }
+
+ ///
+ public async Task GetConnectionStatusAsync(IChannel channel)
+ {
+ if (ActiveWorkspace == null || AvailableProviders == null)
+ {
+ return AzureClientError.NotConnected.ToExecutionResult();
+ }
+
+ channel.Stdout($"Connected to Azure Quantum workspace {ActiveWorkspace.Name}.");
+
+ return ValidExecutionTargets.ToExecutionResult();
+ }
+
+ private async Task SubmitOrExecuteJobAsync(
+ IChannel channel,
+ AzureSubmissionContext submissionContext,
+ bool execute,
+ CancellationToken cancellationToken)
+ {
+ if (ActiveWorkspace == null)
+ {
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before submitting a job.");
+ return AzureClientError.NotConnected.ToExecutionResult();
+ }
+
+ if (ActiveTarget == null)
+ {
+ channel.Stderr($"Please call {GetCommandDisplayName("target")} before submitting a job.");
+ return AzureClientError.NoTarget.ToExecutionResult();
+ }
+
+ if (string.IsNullOrEmpty(submissionContext.OperationName))
+ {
+ channel.Stderr($"Please pass a valid Q# operation name to {GetCommandDisplayName(execute ? "execute" : "submit")}.");
+ return AzureClientError.NoOperationName.ToExecutionResult();
+ }
+
+ 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.TargetId}.");
+ return AzureClientError.InvalidTarget.ToExecutionResult();
+ }
+
+ channel.Stdout($"Submitting {submissionContext.OperationName} to target {ActiveTarget.TargetId}...");
+
+ IEntryPoint? entryPoint = null;
+ try
+ {
+ 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();
+ }
+ catch (CompilationErrorsException e)
+ {
+ 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();
+ }
+
+ // If the command was not %azure.execute, simply return the job status.
+ if (!execute)
+ {
+ return await GetJobStatusAsync(channel, MostRecentJobId);
+ }
+
+ // If the command was %azure.execute, wait for the job to complete and return the job output.
+ channel.Stdout($"Waiting up to {submissionContext.ExecutionTimeout} seconds for Azure Quantum job to complete...");
+
+ using var executionTimeoutTokenSource = new CancellationTokenSource(TimeSpan.FromSeconds(submissionContext.ExecutionTimeout));
+ using var executionCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(executionTimeoutTokenSource.Token, cancellationToken);
+ {
+ try
+ {
+ CloudJob? cloudJob = null;
+ while (cloudJob == null || cloudJob.InProgress)
+ {
+ executionCancellationTokenSource.Token.ThrowIfCancellationRequested();
+ await Task.Delay(TimeSpan.FromSeconds(submissionContext.ExecutionPollingInterval), executionCancellationTokenSource.Token);
+ cloudJob = await ActiveWorkspace.GetJobAsync(MostRecentJobId);
+ channel.Stdout($"[{DateTime.Now.ToLongTimeString()}] Current job status: {cloudJob?.Status ?? "Unknown"}");
+ }
+ }
+ catch (Exception e) when (e is TaskCanceledException || e is OperationCanceledException)
+ {
+ Logger?.LogInformation($"Operation canceled while waiting for job execution to complete: {e.Message}");
+ }
+ }
+
+ return await GetJobResultAsync(channel, MostRecentJobId);
+ }
+
+ ///
+ public async Task SubmitJobAsync(IChannel channel, AzureSubmissionContext submissionContext, CancellationToken? cancellationToken = null) =>
+ await SubmitOrExecuteJobAsync(channel, submissionContext, execute: false, cancellationToken ?? CancellationToken.None);
+
+ ///
+ public async Task ExecuteJobAsync(IChannel channel, AzureSubmissionContext submissionContext, CancellationToken? cancellationToken = null) =>
+ await SubmitOrExecuteJobAsync(channel, submissionContext, execute: true, cancellationToken ?? CancellationToken.None);
+
+ ///
+ public async Task GetActiveTargetAsync(IChannel channel)
+ {
+ if (AvailableProviders == null)
+ {
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting the execution target.");
+ return AzureClientError.NotConnected.ToExecutionResult();
+ }
+
+ if (ActiveTarget == null)
+ {
+ channel.Stderr($"No execution target has been specified. To specify one, call {GetCommandDisplayName("target")} with the target ID.");
+ channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
+ return AzureClientError.NoTarget.ToExecutionResult();
+ }
+
+ channel.Stdout($"Current execution target: {ActiveTarget.TargetId}");
+ channel.Stdout($"Available execution targets: {ValidExecutionTargetsDisplayText}");
+
+ return AvailableTargets.First(target => target.Id == ActiveTarget.TargetId).ToExecutionResult();
+ }
+
+ ///
+ public async Task SetActiveTargetAsync(IChannel channel, string targetId)
+ {
+ if (AvailableProviders == null)
+ {
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before setting an execution target.");
+ return AzureClientError.NotConnected.ToExecutionResult();
+ }
+
+ // Validate that this target is valid in the workspace.
+ if (!AvailableTargets.Any(target => targetId == target.Id))
+ {
+ 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.
+ var executionTarget = AzureExecutionTarget.Create(targetId);
+ if (executionTarget == null)
+ {
+ channel.Stderr($"Target {targetId} 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);
+
+ 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)
+ {
+ if (ActiveWorkspace == null)
+ {
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting job results.");
+ return AzureClientError.NotConnected.ToExecutionResult();
+ }
+
+ if (string.IsNullOrEmpty(jobId))
+ {
+ if (string.IsNullOrEmpty(MostRecentJobId))
+ {
+ channel.Stderr("No job ID was specified. Please submit a job first or specify a job ID.");
+ return AzureClientError.JobNotFound.ToExecutionResult();
+ }
+
+ jobId = MostRecentJobId;
+ }
+
+ var job = await ActiveWorkspace.GetJobAsync(jobId);
+ if (job == null)
+ {
+ channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace.");
+ return AzureClientError.JobNotFound.ToExecutionResult();
+ }
+
+ if (!job.Succeeded || string.IsNullOrEmpty(job.Details.OutputDataUri))
+ {
+ channel.Stderr($"Job ID {jobId} has not completed. To check the status, call {GetCommandDisplayName("status")} with the job ID.");
+ return AzureClientError.JobNotCompleted.ToExecutionResult();
+ }
+
+ try
+ {
+ 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();
+ }
+ }
+
+ ///
+ public async Task GetJobStatusAsync(IChannel channel, string jobId)
+ {
+ if (ActiveWorkspace == null)
+ {
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before getting job status.");
+ return AzureClientError.NotConnected.ToExecutionResult();
+ }
+
+ if (string.IsNullOrEmpty(jobId))
+ {
+ if (string.IsNullOrEmpty(MostRecentJobId))
+ {
+ channel.Stderr("No job ID was specified. Please submit a job first or specify a job ID.");
+ return AzureClientError.JobNotFound.ToExecutionResult();
+ }
+
+ jobId = MostRecentJobId;
+ }
+
+ var job = await ActiveWorkspace.GetJobAsync(jobId);
+ if (job == null)
+ {
+ channel.Stderr($"Job ID {jobId} not found in current Azure Quantum workspace.");
+ return AzureClientError.JobNotFound.ToExecutionResult();
+ }
+
+ return job.ToExecutionResult();
+ }
+
+ ///
+ public async Task GetJobListAsync(IChannel channel, string filter)
+ {
+ if (ActiveWorkspace == null)
+ {
+ channel.Stderr($"Please call {GetCommandDisplayName("connect")} before listing jobs.");
+ return AzureClientError.NotConnected.ToExecutionResult();
+ }
+
+ var jobs = await ActiveWorkspace.ListJobsAsync() ?? new List();
+ if (jobs.Count() == 0)
+ {
+ channel.Stderr("No jobs found in current Azure Quantum workspace.");
+ }
+ else
+ {
+ jobs = jobs.Where(job => job.Matches(filter));
+ if (jobs.Count() == 0)
+ {
+ channel.Stderr($"No jobs matching \"{filter}\" found in current Azure Quantum workspace.");
+ }
+ }
+
+ return jobs.ToExecutionResult();
+ }
+
+ private string GetCommandDisplayName(string commandName) =>
+ IsPythonUserAgent ? $"qsharp.azure.{commandName}()" : $"%azure.{commandName}";
+ }
+}
diff --git a/src/AzureClient/AzureClient.csproj b/src/AzureClient/AzureClient.csproj
new file mode 100644
index 0000000000..8d7b029f32
--- /dev/null
+++ b/src/AzureClient/AzureClient.csproj
@@ -0,0 +1,27 @@
+
+
+
+ netstandard2.1
+ x64
+ Microsoft.Quantum.IQSharp.AzureClient
+ Microsoft.Quantum.IQSharp.AzureClient
+ true
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
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
new file mode 100644
index 0000000000..66dc1de62d
--- /dev/null
+++ b/src/AzureClient/AzureEnvironment.cs
@@ -0,0 +1,205 @@
+// 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.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, Mock };
+
+ internal class AzureEnvironment
+ {
+ 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 subscriptionId)
+ {
+ var azureEnvironmentName = System.Environment.GetEnvironmentVariable(EnvironmentVariableName);
+
+ if (Enum.TryParse(azureEnvironmentName, true, out AzureEnvironmentType environmentType))
+ {
+ switch (environmentType)
+ {
+ case AzureEnvironmentType.Production:
+ return Production(subscriptionId);
+ case AzureEnvironmentType.Canary:
+ return Canary(subscriptionId);
+ case AzureEnvironmentType.Dogfood:
+ return Dogfood(subscriptionId);
+ case AzureEnvironmentType.Mock:
+ return Mock();
+ default:
+ throw new InvalidOperationException("Unexpected EnvironmentType value.");
+ }
+ }
+
+ return Production(subscriptionId);
+ }
+
+ public async Task GetAuthenticatedWorkspaceAsync(IChannel channel, string resourceGroupName, string workspaceName, bool refreshCredentials)
+ {
+ if (Type == AzureEnvironmentType.Mock)
+ {
+ channel.Stdout("AZURE_QUANTUM_ENV set to Mock. Using mock Azure workspace rather than connecting to the real service.");
+ return new MockAzureWorkspace(workspaceName);
+ }
+
+ // Find the token cache folder
+ var cacheDirectoryEnvVarName = "AZURE_QUANTUM_TOKEN_CACHE";
+ var cacheDirectory = System.Environment.GetEnvironmentVariable(cacheDirectoryEnvVarName);
+ 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(string subscriptionId)
+ {
+ 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
+ {
+ var armBaseUrl = "https://api-dogfood.resources.windows-int.net";
+ var requestUrl = $"{armBaseUrl}/subscriptions/{subscriptionId}?api-version=2018-01-01";
+
+ WebResponse? response = null;
+ try
+ {
+ response = WebRequest.Create(requestUrl).GetResponse();
+ }
+ catch (WebException webException)
+ {
+ response = webException.Response;
+ }
+
+ var authHeader = response.Headers["WWW-Authenticate"];
+ var headerParts = authHeader.Substring("Bearer ".Length).Split(',');
+ foreach (var headerPart in headerParts)
+ {
+ var parts = headerPart.Split("=", 2);
+ if (parts[0] == "authorization_uri")
+ {
+ var quotedAuthority = parts[1];
+ return quotedAuthority[1..^1];
+ }
+ }
+
+ throw new InvalidOperationException($"Dogfood authority not found in ARM header response for subscription ID {subscriptionId}.");
+ }
+ catch (Exception ex)
+ {
+ throw new InvalidOperationException($"Failed to construct dogfood authority for subscription ID {subscriptionId}.", ex);
+ }
+ }
+ }
+}
diff --git a/src/AzureClient/AzureExecutionTarget.cs b/src/AzureClient/AzureExecutionTarget.cs
new file mode 100644
index 0000000000..f2cebf24f0
--- /dev/null
+++ b/src/AzureClient/AzureExecutionTarget.cs
@@ -0,0 +1,44 @@
+// Copyright (c) Microsoft Corporation.
+// 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 TargetId { get; private set; } = string.Empty;
+ public string PackageName => $"Microsoft.Quantum.Providers.{GetProvider(TargetId)}";
+
+ public static bool IsValid(string targetId) => GetProvider(targetId) != null;
+
+ 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 ID.
+ /// The enum value representing the provider.
+ ///
+ /// Valid target IDs are structured as "provider.target".
+ /// For example, "ionq.simulator" or "honeywell.qpu".
+ ///
+ private static AzureProvider? GetProvider(string targetId)
+ {
+ var parts = targetId.Split('.', 2);
+ if (Enum.TryParse(parts[0], true, out AzureProvider provider))
+ {
+ return provider;
+ }
+
+ return null;
+ }
+ }
+}
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