Skip to content

Commit

Permalink
feature: Quantagonia Qubo solver via PlanQK
Browse files Browse the repository at this point in the history
  • Loading branch information
Elscrux committed Nov 25, 2024
1 parent eb6aa14 commit ef5b3f0
Show file tree
Hide file tree
Showing 7 changed files with 550 additions and 1 deletion.
2 changes: 2 additions & 0 deletions build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -11,13 +11,15 @@ sourceCompatibility = '17'

repositories {
mavenCentral()
maven { url 'https://jitpack.io' }
}

dependencies {
implementation 'org.springframework.boot:spring-boot-starter-webflux'
implementation 'org.springframework.boot:spring-boot-starter-validation'
implementation 'org.springdoc:springdoc-openapi-starter-webflux-ui:2.0.4'
implementation 'com.bpodgursky:jbool_expressions:1.24'
implementation 'com.github.ProvideQ.jplex:input:8267d8be09'
implementation files('lib/de.ovgu.featureide.lib.fm-v3.9.1.jar', 'lib/uvl-parser.jar')
testImplementation 'org.springframework.boot:spring-boot-starter-test'
testImplementation 'org.mockito:mockito-core:5.+'
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
package edu.kit.provideq.toolbox.exception;

public class AuthenticationException extends Exception {
public AuthenticationException(String message) {
super(message);
}

public AuthenticationException(String message, Throwable cause) {
super(message, cause);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,235 @@
package edu.kit.provideq.toolbox.integration.planqk;

import edu.kit.provideq.toolbox.integration.planqk.exception.PlanQkJobFailedException;
import edu.kit.provideq.toolbox.integration.planqk.exception.PlanQkJobPendingException;
import java.time.Duration;
import java.util.Arrays;
import java.util.Date;
import java.util.function.Function;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.web.reactive.function.BodyInserters;
import org.springframework.web.reactive.function.client.WebClient;
import reactor.core.publisher.Mono;
import reactor.util.retry.Retry;

@Component
public class PlanQkApi {
private static final String BASE_URL = "https://gateway.platform.planqk.de";

/**
* Calls a service using the PlanQk API.
*
* @param <ProblemT> The type of the problem.
* @param <StatusT> The type of the status.
* @param <ResultT> The type of the result.
* @param service The url path of the service to use.
* @param problem The data of the problem to solve.
* @param authenticationToken The authentication token to use.
* @param problemProperties The properties of the problem.
* @param statusProperties The properties of the status.
* @param resultProperties The properties of the result.
* @return The result to the problem.
*/
public <ProblemT, StatusT, ResultT> Mono<ResultT> call(
String service,
ProblemT problem,
String authenticationToken,
ProblemProperties problemProperties,
StatusProperties<StatusT> statusProperties,
ResultProperties<ResultT> resultProperties) {
var webClientBuilder = WebClient.builder();
if (authenticationToken != null) {
webClientBuilder.defaultHeader(HttpHeaders.AUTHORIZATION, "Bearer " + authenticationToken);
}

WebClient webClient = webClientBuilder.build();

// Define the URL for the token endpoint
final String serviceEndpoint = BASE_URL + service;

// Make the POST request to obtain the access token with client_credentials grant type
return webClient.post()
.uri(serviceEndpoint + problemProperties.createJobEndpoint())
.contentType(MediaType.APPLICATION_JSON)
.body(BodyInserters.fromValue(problem))
.retrieve()
.bodyToMono(String.class)
.flatMap(jobId -> webClient.get()
.uri(serviceEndpoint + statusProperties.jobStatusEndpoint().formatted(jobId))
.exchangeToMono(response -> {
// Handle different kinds of status responses
// Usually the JobStatus is returned as json object
// but some might use just an enum in plain text
MediaType contentType = response.headers().contentType().orElseThrow();
if (MediaType.TEXT_PLAIN.equals(contentType)) {
return response.bodyToMono(String.class)
.map(status -> parseEnumStatus(statusProperties, status));
}

// Otherwise, assume the content type is application/json
return response.bodyToMono(statusProperties.statusClass());
})
.map(customStatus -> statusProperties.statusMapper().apply(customStatus))
.flatMap(status -> {
switch (status) {
case SUCCEEDED -> {
return Mono.just(status);
}
case FAILED -> {
return Mono.error(new PlanQkJobFailedException());
}
default -> {
return Mono.error(new PlanQkJobPendingException());
}
}
})
.retryWhen(Retry.backoff(100, Duration.ofSeconds(1))
.filter(PlanQkJobPendingException.class::isInstance))
.flatMap(succeededJobStatus ->
// The job finished successfully, so get the result
webClient.get()
.uri(serviceEndpoint
+ resultProperties.jobResultsEndpoint().formatted(jobId))
.retrieve()
.bodyToMono(resultProperties.jobResultsClass())
)
);
}

private static <StatusT> StatusT parseEnumStatus(
StatusProperties<StatusT> statusProperties,
String status) throws IllegalArgumentException {
if (!statusProperties.statusClass().isEnum()) {
throw new IllegalArgumentException("Not an enum: " + statusProperties.statusClass());
}

return Arrays.stream(statusProperties
.statusClass()
.getEnumConstants())
.filter(enumValue -> enumValue.toString().equalsIgnoreCase(status))
.findFirst()
.orElseThrow(() -> new IllegalArgumentException("Invalid status: " + status));
}

public enum JobStatus {
UNKNOWN,
PENDING,
RUNNING,
SUCCEEDED,
FAILED
}

public static class JobInfo {
private String id;
private JobStatus status;
private Date createdAt;
private Date startedAt;
private Date endedAt;

/**
* Job ID.
*/
public String getId() {
return id;
}

public void setId(String id) {
this.id = id;
}

/**
* Job status.
*/
public JobStatus getStatus() {
return status;
}

public void setStatus(JobStatus status) {
this.status = status;
}

/**
* Date of creation.
*/
public Date getCreatedAt() {
return createdAt;
}

public void setCreatedAt(Date createdAt) {
this.createdAt = createdAt;
}

/**
* Date of starting.
*/
public Date getStartedAt() {
return startedAt;
}

public void setStartedAt(Date startedAt) {
this.startedAt = startedAt;
}

/**
* Date of ending.
*/
public Date getEndedAt() {
return endedAt;
}

public void setEndedAt(Date endedAt) {
this.endedAt = endedAt;
}
}

/**
* Properties for the problem.
*
* @param createJobEndpoint The POST endpoint to create a job.
* Must contain %s placeholder for the job ID.
*/
public record ProblemProperties(
String createJobEndpoint) {
public static PlanQkApi.ProblemProperties defaultProblemProperties() {
return new PlanQkApi.ProblemProperties("/jobs");
}
}

/**
* Properties for the status.
*
* @param statusClass The class of the custom status.
* @param jobStatusEndpoint The endpoint to get the status of a job.
* Must contain %s placeholder for the job ID.
* @param statusMapper Maps the custom status to a status of the PlanQK API.
* @param <StatusT> The type of the custom status.
*/
public record StatusProperties<StatusT>(
Class<StatusT> statusClass,
String jobStatusEndpoint,
Function<StatusT, JobStatus> statusMapper) {
public static PlanQkApi.StatusProperties<JobInfo> defaultStatusProperties() {
return new PlanQkApi.StatusProperties<>(
PlanQkApi.JobInfo.class,
"/jobs/%s",
PlanQkApi.JobInfo::getStatus);
}
}

/**
* Properties for the result.
*
* @param jobResultsClass The class of the custom result.
* @param jobResultsEndpoint The endpoint to get the result of a job.
*/
public record ResultProperties<ResultT>(
Class<ResultT> jobResultsClass,
String jobResultsEndpoint) {
public static <ResultT> PlanQkApi.ResultProperties<ResultT> defaultResultProperties(
Class<ResultT> resultClass) {
return new PlanQkApi.ResultProperties<>(resultClass, "/jobs/%s/results");
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package edu.kit.provideq.toolbox.integration.planqk.exception;

public class PlanQkJobFailedException extends Exception {
public PlanQkJobFailedException() {
super("PlanQK job failed");
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,7 @@
package edu.kit.provideq.toolbox.integration.planqk.exception;

public class PlanQkJobPendingException extends Exception {
public PlanQkJobPendingException() {
super("PlanQK job is still pending");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@
import edu.kit.provideq.toolbox.qubo.solvers.DwaveQuboSolver;
import edu.kit.provideq.toolbox.qubo.solvers.QiskitQuboSolver;
import edu.kit.provideq.toolbox.qubo.solvers.QrispQuboSolver;
import edu.kit.provideq.toolbox.qubo.solvers.QuantagoniaQuboSolver;
import java.io.IOException;
import java.util.Objects;
import java.util.Set;
Expand Down Expand Up @@ -36,11 +37,12 @@ ProblemManager<String, String> getQuboManager(
QiskitQuboSolver qiskitSolver,
DwaveQuboSolver dwaveSolver,
QrispQuboSolver qrispSolver,
QuantagoniaQuboSolver quantagoniaQuboSolver,
ResourceProvider resourceProvider
) {
return new ProblemManager<>(
QUBO,
Set.of(qiskitSolver, dwaveSolver, qrispSolver),
Set.of(qiskitSolver, dwaveSolver, qrispSolver, quantagoniaQuboSolver),
loadExampleProblems(resourceProvider)
);
}
Expand Down
Loading

0 comments on commit ef5b3f0

Please sign in to comment.