diff --git a/src/main/java/edu/kit/provideq/toolbox/Bound.java b/src/main/java/edu/kit/provideq/toolbox/Bound.java new file mode 100644 index 00000000..d59b15e6 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/Bound.java @@ -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) { +} diff --git a/src/main/java/edu/kit/provideq/toolbox/BoundType.java b/src/main/java/edu/kit/provideq/toolbox/BoundType.java new file mode 100644 index 00000000..3cf2a81a --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/BoundType.java @@ -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 +} diff --git a/src/main/java/edu/kit/provideq/toolbox/BoundWithInfo.java b/src/main/java/edu/kit/provideq/toolbox/BoundWithInfo.java new file mode 100644 index 00000000..9aacc7e9 --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/BoundWithInfo.java @@ -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) { +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/BoundDto.java b/src/main/java/edu/kit/provideq/toolbox/api/BoundDto.java new file mode 100644 index 00000000..e23356fa --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/BoundDto.java @@ -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()); + } +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/EstimationRouter.java b/src/main/java/edu/kit/provideq/toolbox/api/EstimationRouter.java new file mode 100644 index 00000000..08e6b34b --- /dev/null +++ b/src/main/java/edu/kit/provideq/toolbox/api/EstimationRouter.java @@ -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 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 defineGetRoute(ProblemManager manager) { + return route().GET( + getEstimationRouteForProblemType(manager.getType()), + accept(APPLICATION_JSON), + req -> handleGet(manager, req), + ops -> handleGetDocumentation(manager, ops) + ).build(); + } + + private Mono handleGet( + ProblemManager manager, + ServerRequest req + ) { + var problemId = req.pathVariable(PROBLEM_ID_PARAM_NAME); + var problem = findProblemOrThrow(manager, problemId); + + Mono 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 Problem findProblemOrThrow( + ProblemManager 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; + } + +} diff --git a/src/main/java/edu/kit/provideq/toolbox/api/ProblemDto.java b/src/main/java/edu/kit/provideq/toolbox/api/ProblemDto.java index ca5ffa72..e53db2f5 100644 --- a/src/main/java/edu/kit/provideq/toolbox/api/ProblemDto.java +++ b/src/main/java/edu/kit/provideq/toolbox/api/ProblemDto.java @@ -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; @@ -16,6 +17,7 @@ public class ProblemDto { private String typeId; private InputT input; private Solution solution; + private BoundWithInfo bound; private ProblemState state; private String solverId; private List solverSettings; @@ -38,6 +40,7 @@ public static ProblemDto 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) @@ -67,6 +70,10 @@ public Solution getSolution() { return solution; } + public BoundWithInfo getBound() { + return bound; + } + public ProblemState getState() { return state; } @@ -100,6 +107,7 @@ public String toString() { + ", solverId=" + solverId + ", input=" + input + ", solution=" + solution + + ", value=" + bound + ", solverSettings=" + solverSettings + ", subProblems=" + subProblems + '}'; diff --git a/src/main/java/edu/kit/provideq/toolbox/demonstrators/DemonstratorConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/demonstrators/DemonstratorConfiguration.java index 5767fe52..f1875365 100644 --- a/src/main/java/edu/kit/provideq/toolbox/demonstrators/DemonstratorConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/demonstrators/DemonstratorConfiguration.java @@ -16,7 +16,8 @@ public class DemonstratorConfiguration { public static final ProblemType DEMONSTRATOR = new ProblemType<>( "demonstrator", String.class, - String.class + String.class, + null ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureConfiguration.java index 54f15dad..9b29ca0b 100644 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/dead/DeadFeatureConfiguration.java @@ -26,7 +26,8 @@ public class DeadFeatureConfiguration { public static final ProblemType FEATURE_MODEL_ANOMALY_DEAD = new ProblemType<>( "feature-model-anomaly-dead", String.class, - String.class + String.class, + null ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidModelConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidModelConfiguration.java index cc07a31a..d1edc2a3 100644 --- a/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidModelConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/featuremodel/anomaly/voidmodel/VoidModelConfiguration.java @@ -26,7 +26,8 @@ public class VoidModelConfiguration { public static final ProblemType FEATURE_MODEL_ANOMALY_VOID = new ProblemType<>( "feature-model-anomaly-void", String.class, - String.class + String.class, + null ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/knapsack/KnapsackConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/knapsack/KnapsackConfiguration.java index 309558e4..f7bf9c72 100644 --- a/src/main/java/edu/kit/provideq/toolbox/knapsack/KnapsackConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/knapsack/KnapsackConfiguration.java @@ -27,7 +27,8 @@ public class KnapsackConfiguration { public static final ProblemType KNAPSACK = new ProblemType<>( "knapsack", String.class, - String.class + String.class, + null ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutConfiguration.java index bb3fc354..1117f38b 100644 --- a/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/maxcut/MaxCutConfiguration.java @@ -27,7 +27,8 @@ public class MaxCutConfiguration { public static final ProblemType MAX_CUT = new ProblemType<>( "max-cut", String.class, - String.class + String.class, + null ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/Problem.java b/src/main/java/edu/kit/provideq/toolbox/meta/Problem.java index f26ed241..8d33afef 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/Problem.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/Problem.java @@ -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; @@ -29,6 +30,7 @@ public class Problem { private InputT input; private Solution solution; + private BoundWithInfo bound; private ProblemState state; private ProblemSolver solver; private List solverSettings; @@ -82,6 +84,26 @@ public Mono> 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; } @@ -208,4 +230,8 @@ public String toString() { + ", solver=" + solver + '}'; } + + public Optional getBound() { + return Optional.ofNullable(bound); + } } diff --git a/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java b/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java index a3278f44..17edd9ab 100644 --- a/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java +++ b/src/main/java/edu/kit/provideq/toolbox/meta/ProblemType.java @@ -1,5 +1,9 @@ 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. */ @@ -7,22 +11,27 @@ public class ProblemType { private final String id; private final Class inputClass; private final Class resultClass; + private final Function 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 inputClass, - Class resultClass + Class resultClass, + Function estimator ) { this.id = id; this.inputClass = inputClass; this.resultClass = resultClass; + this.estimator = estimator; } /** @@ -46,12 +55,20 @@ public Class getResultClass() { return resultClass; } + /** + * Returns the bound estimator for this problem type. + */ + public Optional> 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) + '}'; } } diff --git a/src/main/java/edu/kit/provideq/toolbox/qubo/QuboConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/qubo/QuboConfiguration.java index 6c58be28..9681f1ff 100644 --- a/src/main/java/edu/kit/provideq/toolbox/qubo/QuboConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/qubo/QuboConfiguration.java @@ -28,7 +28,8 @@ public class QuboConfiguration { public static final ProblemType QUBO = new ProblemType<>( "qubo", String.class, - String.class + String.class, + null ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/sat/SatConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/sat/SatConfiguration.java index dc17ea49..6d819683 100644 --- a/src/main/java/edu/kit/provideq/toolbox/sat/SatConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/sat/SatConfiguration.java @@ -25,7 +25,8 @@ public class SatConfiguration { public static final ProblemType SAT = new ProblemType<>( "sat", String.class, - DimacsCnfSolution.class + DimacsCnfSolution.class, + null ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java index da9d146b..4bc63594 100644 --- a/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/tsp/TspConfiguration.java @@ -25,7 +25,8 @@ public class TspConfiguration { public static final ProblemType TSP = new ProblemType<>( "tsp", String.class, - String.class + String.class, + null ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java index ca47c505..d2499741 100644 --- a/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/VrpConfiguration.java @@ -26,7 +26,8 @@ public class VrpConfiguration { public static final ProblemType VRP = new ProblemType<>( "vrp", String.class, - String.class + String.class, + null ); @Bean diff --git a/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClustererConfiguration.java b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClustererConfiguration.java index 79ce124f..e7918160 100644 --- a/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClustererConfiguration.java +++ b/src/main/java/edu/kit/provideq/toolbox/vrp/clusterer/VrpClustererConfiguration.java @@ -19,7 +19,8 @@ public class VrpClustererConfiguration { public static final ProblemType CLUSTER_VRP = new ProblemType<>( "cluster-vrp", String.class, - String.class + String.class, + null ); @Bean