Skip to content

Commit

Permalink
Merge pull request #106 from ProvideQ/feature/solution-quality-evaluator
Browse files Browse the repository at this point in the history
Solution Quality Evaluator
  • Loading branch information
Elscrux authored Nov 27, 2024
2 parents b82e1d2 + dc926c3 commit aefaba6
Show file tree
Hide file tree
Showing 18 changed files with 252 additions and 13 deletions.
10 changes: 10 additions & 0 deletions src/main/java/edu/kit/provideq/toolbox/Bound.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package edu.kit.provideq.toolbox;

/**
* Represents a bound value with its type.
*
* @param value the estimated value
* @param boundType the type of the bound
*/
public record Bound(float value, BoundType boundType) {
}
15 changes: 15 additions & 0 deletions src/main/java/edu/kit/provideq/toolbox/BoundType.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
package edu.kit.provideq.toolbox;

/**
* Represents the bound type of bounds.
*/
public enum BoundType {
/**
* An upper bound.
*/
UPPER,
/**
* A lower bound.
*/
LOWER
}
10 changes: 10 additions & 0 deletions src/main/java/edu/kit/provideq/toolbox/BoundWithInfo.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package edu.kit.provideq.toolbox;

/**
* Represents a bound estimation for the solution of a problem.
*
* @param bound the value
* @param executionTime the time it took to estimate the value
*/
public record BoundWithInfo(Bound bound, long executionTime) {
}
13 changes: 13 additions & 0 deletions src/main/java/edu/kit/provideq/toolbox/api/BoundDto.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
package edu.kit.provideq.toolbox.api;

import edu.kit.provideq.toolbox.BoundType;
import edu.kit.provideq.toolbox.BoundWithInfo;

public record BoundDto(float bound, BoundType boundType, long executionTime) {
public BoundDto(BoundWithInfo boundWithInfo) {
this(
boundWithInfo.bound().value(),
boundWithInfo.bound().boundType(),
boundWithInfo.executionTime());
}
}
130 changes: 130 additions & 0 deletions src/main/java/edu/kit/provideq/toolbox/api/EstimationRouter.java
Original file line number Diff line number Diff line change
@@ -0,0 +1,130 @@
package edu.kit.provideq.toolbox.api;

import static org.springdoc.core.fn.builders.apiresponse.Builder.responseBuilder;
import static org.springdoc.core.fn.builders.content.Builder.contentBuilder;
import static org.springdoc.core.fn.builders.parameter.Builder.parameterBuilder;
import static org.springdoc.core.fn.builders.schema.Builder.schemaBuilder;
import static org.springdoc.webflux.core.fn.SpringdocRouteBuilder.route;
import static org.springframework.http.MediaType.APPLICATION_JSON;
import static org.springframework.http.MediaType.APPLICATION_JSON_VALUE;
import static org.springframework.web.reactive.function.server.RequestPredicates.accept;
import static org.springframework.web.reactive.function.server.ServerResponse.ok;

import edu.kit.provideq.toolbox.meta.Problem;
import edu.kit.provideq.toolbox.meta.ProblemManager;
import edu.kit.provideq.toolbox.meta.ProblemManagerProvider;
import edu.kit.provideq.toolbox.meta.ProblemType;
import io.swagger.v3.oas.annotations.enums.ParameterIn;
import java.util.NoSuchElementException;
import java.util.UUID;
import org.springdoc.core.fn.builders.operation.Builder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.ParameterizedTypeReference;
import org.springframework.http.HttpStatus;
import org.springframework.web.reactive.config.EnableWebFlux;
import org.springframework.web.reactive.function.server.RouterFunction;
import org.springframework.web.reactive.function.server.ServerRequest;
import org.springframework.web.reactive.function.server.ServerResponse;
import org.springframework.web.server.ResponseStatusException;
import reactor.core.publisher.Mono;

/**
* This router handles requests regarding {@link Problem} instance solution bound estimations.
* The /bound endpoint is only available for problem types that have an estimator.
*/
@Configuration
@EnableWebFlux
public class EstimationRouter {
public static final String PROBLEM_ID_PARAM_NAME = "problemId";
private ProblemManagerProvider managerProvider;

@Bean
RouterFunction<ServerResponse> getEstimationRoutes() {
return managerProvider.getProblemManagers().stream()
.filter(manager -> manager.getType().getEstimator().isPresent())
.map(this::defineGetRoute)
.reduce(RouterFunction::and)
.orElse(null);
}

/**
* Estimate Operation: GET /problems/TYPE/{problemId}/bound.
*/
private RouterFunction<ServerResponse> defineGetRoute(ProblemManager<?, ?> manager) {
return route().GET(
getEstimationRouteForProblemType(manager.getType()),
accept(APPLICATION_JSON),
req -> handleGet(manager, req),
ops -> handleGetDocumentation(manager, ops)
).build();
}

private <InputT, ResultT> Mono<ServerResponse> handleGet(
ProblemManager<InputT, ResultT> manager,
ServerRequest req
) {
var problemId = req.pathVariable(PROBLEM_ID_PARAM_NAME);
var problem = findProblemOrThrow(manager, problemId);

Mono<BoundDto> bound;
try {
problem.estimateBound();
bound = Mono.just(new BoundDto(problem.getBound().orElseThrow()));
} catch (IllegalStateException | NoSuchElementException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, e.getMessage());
}

return ok().body(bound, new ParameterizedTypeReference<>() {
});
}

private void handleGetDocumentation(
ProblemManager<?, ?> manager,
Builder ops
) {
ProblemType<?, ?> problemType = manager.getType();
ops
.operationId(getEstimationRouteForProblemType(problemType))
.tag(problemType.getId())
.description("Estimates the solution bound for the problem with the given ID.")
.parameter(parameterBuilder().in(ParameterIn.PATH).name(PROBLEM_ID_PARAM_NAME))
.response(responseBuilder()
.responseCode(String.valueOf(HttpStatus.OK.value()))
.content(getOkResponseContent())
);
}

private static org.springdoc.core.fn.builders.content.Builder getOkResponseContent() {
return contentBuilder()
.mediaType(APPLICATION_JSON_VALUE)
.schema(schemaBuilder().implementation(BoundDto.class));
}

private <InputT, ResultT> Problem<InputT, ResultT> findProblemOrThrow(
ProblemManager<InputT, ResultT> manager,
String id
) {
UUID uuid;
try {
uuid = UUID.fromString(id);
} catch (IllegalArgumentException e) {
throw new ResponseStatusException(HttpStatus.BAD_REQUEST, "Invalid problem ID");
}

return manager.findInstanceById(uuid)
.orElseThrow(() -> new ResponseStatusException(HttpStatus.NOT_FOUND,
"Could not find a problem for this type with this problem ID!"));
}

private String getEstimationRouteForProblemType(ProblemType<?, ?> type) {
return "/problems/%s/{%s}/bound".formatted(type.getId(), PROBLEM_ID_PARAM_NAME);
}

@Autowired
void setManagerProvider(ProblemManagerProvider managerProvider) {
this.managerProvider = managerProvider;
}

}
8 changes: 8 additions & 0 deletions src/main/java/edu/kit/provideq/toolbox/api/ProblemDto.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.kit.provideq.toolbox.api;

import edu.kit.provideq.toolbox.BoundWithInfo;
import edu.kit.provideq.toolbox.Solution;
import edu.kit.provideq.toolbox.meta.Problem;
import edu.kit.provideq.toolbox.meta.ProblemSolver;
Expand All @@ -16,6 +17,7 @@ public class ProblemDto<InputT, ResultT> {
private String typeId;
private InputT input;
private Solution<ResultT> solution;
private BoundWithInfo bound;
private ProblemState state;
private String solverId;
private List<SolverSetting> solverSettings;
Expand All @@ -38,6 +40,7 @@ public static <InputT, ResultT> ProblemDto<InputT, ResultT> fromProblem(
dto.typeId = problem.getType().getId();
dto.input = problem.getInput().orElse(null);
dto.solution = problem.getSolution();
dto.bound = problem.getBound().orElse(null);
dto.state = problem.getState();
dto.solverId = problem.getSolver()
.map(ProblemSolver::getId)
Expand Down Expand Up @@ -67,6 +70,10 @@ public Solution<ResultT> getSolution() {
return solution;
}

public BoundWithInfo getBound() {
return bound;
}

public ProblemState getState() {
return state;
}
Expand Down Expand Up @@ -100,6 +107,7 @@ public String toString() {
+ ", solverId=" + solverId
+ ", input=" + input
+ ", solution=" + solution
+ ", value=" + bound
+ ", solverSettings=" + solverSettings
+ ", subProblems=" + subProblems
+ '}';
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,7 +16,8 @@ public class DemonstratorConfiguration {
public static final ProblemType<String, String> DEMONSTRATOR = new ProblemType<>(
"demonstrator",
String.class,
String.class
String.class,
null
);

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public class DeadFeatureConfiguration {
public static final ProblemType<String, String> FEATURE_MODEL_ANOMALY_DEAD = new ProblemType<>(
"feature-model-anomaly-dead",
String.class,
String.class
String.class,
null
);

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -26,7 +26,8 @@ public class VoidModelConfiguration {
public static final ProblemType<String, String> FEATURE_MODEL_ANOMALY_VOID = new ProblemType<>(
"feature-model-anomaly-void",
String.class,
String.class
String.class,
null
);

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public class KnapsackConfiguration {
public static final ProblemType<String, String> KNAPSACK = new ProblemType<>(
"knapsack",
String.class,
String.class
String.class,
null
);

@Bean
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,8 @@ public class MaxCutConfiguration {
public static final ProblemType<String, String> MAX_CUT = new ProblemType<>(
"max-cut",
String.class,
String.class
String.class,
null
);

@Bean
Expand Down
26 changes: 26 additions & 0 deletions src/main/java/edu/kit/provideq/toolbox/meta/Problem.java
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
package edu.kit.provideq.toolbox.meta;

import edu.kit.provideq.toolbox.BoundWithInfo;
import edu.kit.provideq.toolbox.Solution;
import edu.kit.provideq.toolbox.meta.setting.SolverSetting;
import java.util.Collections;
Expand Down Expand Up @@ -29,6 +30,7 @@ public class Problem<InputT, ResultT> {

private InputT input;
private Solution<ResultT> solution;
private BoundWithInfo bound;
private ProblemState state;
private ProblemSolver<InputT, ResultT> solver;
private List<SolverSetting> solverSettings;
Expand Down Expand Up @@ -82,6 +84,26 @@ public Mono<Solution<ResultT>> solve() {
});
}

public void estimateBound() {
if (this.input == null) {
throw new IllegalStateException("Cannot estimate value without input!");
}

var optionalEstimator = this.type.getEstimator();
if (optionalEstimator.isEmpty()) {
throw new IllegalStateException("Cannot estimate value without an estimator!");
}
var estimator = optionalEstimator.get();

long start = System.currentTimeMillis();

var estimatedBound = estimator.apply(this.input);
long finish = System.currentTimeMillis();
var executionTime = finish - start;

this.bound = new BoundWithInfo(estimatedBound, executionTime);
}

public UUID getId() {
return id;
}
Expand Down Expand Up @@ -208,4 +230,8 @@ public String toString() {
+ ", solver=" + solver
+ '}';
}

public Optional<BoundWithInfo> getBound() {
return Optional.ofNullable(bound);
}
}
23 changes: 20 additions & 3 deletions src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java
Original file line number Diff line number Diff line change
@@ -1,28 +1,37 @@
package edu.kit.provideq.toolbox.meta;

import edu.kit.provideq.toolbox.Bound;
import java.util.Optional;
import java.util.function.Function;

/**
* The type of problem to solve.
*/
public class ProblemType<InputT, ResultT> {
private final String id;
private final Class<InputT> inputClass;
private final Class<ResultT> resultClass;
private final Function<InputT, Bound> estimator;

/**
* Defines a new problem type.
*
* @param id a unique string identifier for this type of problem.
* @param inputClass the Java class object corresponding to the {@link InputT} type parameter.
* @param id a unique string identifier for this type of problem.
* @param inputClass the Java class object corresponding to the {@link InputT} type parameter.
* @param resultClass the Java class object corresponding to the {@link ResultT} type parameter.
* @param estimator the bound estimator for this problem type.
* null if estimation is not supported.
*/
public ProblemType(
String id,
Class<InputT> inputClass,
Class<ResultT> resultClass
Class<ResultT> resultClass,
Function<InputT, Bound> estimator
) {
this.id = id;
this.inputClass = inputClass;
this.resultClass = resultClass;
this.estimator = estimator;
}

/**
Expand All @@ -46,12 +55,20 @@ public Class<ResultT> getResultClass() {
return resultClass;
}

/**
* Returns the bound estimator for this problem type.
*/
public Optional<Function<InputT, Bound>> getEstimator() {
return Optional.ofNullable(estimator);
}

@Override
public String toString() {
return "ProblemType{"
+ "id='%s'".formatted(id)
+ ", inputClass=%s".formatted(inputClass)
+ ", resultClass=%s".formatted(resultClass)
+ ", estimator?=%s".formatted(estimator != null)
+ '}';
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,8 @@ public class QuboConfiguration {
public static final ProblemType<String, String> QUBO = new ProblemType<>(
"qubo",
String.class,
String.class
String.class,
null
);

@Bean
Expand Down
Loading

0 comments on commit aefaba6

Please sign in to comment.