io.github.zabuzard.maglev.external.algorithms.ShortestPathComputationBuilder Maven / Gradle / Ivy
Show all versions of maglev Show documentation
package io.github.zabuzard.maglev.external.algorithms;
import io.github.zabuzard.maglev.external.graph.Edge;
import io.github.zabuzard.maglev.external.graph.Graph;
import io.github.zabuzard.maglev.internal.algorithms.metrics.GreedyFarthestLandmarkProvider;
import io.github.zabuzard.maglev.internal.algorithms.metrics.LandmarkMetric;
import io.github.zabuzard.maglev.internal.algorithms.metrics.RandomLandmarkProvider;
import io.github.zabuzard.maglev.internal.algorithms.shortestpath.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Objects;
import java.util.function.Predicate;
/**
* Builder for convenient construction of {@link ShortestPathComputation} instances.
*
* The builder offers highly customizable algorithms based on Dijkstra, called Module-Dijkstra. That is a regular
* Dijkstra algorithm which can be extended using extension modules that modify its behavior. Offered modules are:
*
* - {@code AbortAfterIfModule} - Aborts further computation as soon as a node that matches a given predicate has been settled
* - {@code AbortBeforeIfModule} - Aborts further computation as soon as a node that matches a given predicate would be settled
* - {@code AbortAfterRangeModule} - Only explores shortest paths up to the given range
* - {@code IgnoreEdgeIfModule} - Ignores exploring edges that match the given predicate
* - {@code AStarModule} - Optimization of the algorithm by utilizing a given heuristic metric
*
* It is also possible to add custom modules by simply implementing {@link DijkstraModule}.
* Modules can be added by using {@link #addModule(DijkstraModule)} and the other {@code addModuleXXX} methods.
*
* If the {@code AStarModule} was chosen, a heuristic metric must be given. Offered metrics are:
*
* - {@code LandmarkMetric} - Dynamic heuristic computed based on the underlying graph model (also known as ALT algorithm)
*
* It is also possible to use a custom metric by simply implementing {@link Metric}, for example a metric based on the Euclidean distance.
* A metric can be set by using {@link #setMetric(Metric)} and the other {@code setMetricXXX} methods.
*
* If the {@code LandmarkMetric} was chosen, the amount of landmarks must be set, which can be done using {@link #setAmountOfLandmarks(int)}.
* Additionally, a landmark provider must be given. Offered landmark providers are:
*
* - {@code GreedyFarthestLandmarkProvider} - Chooses landmarks that are optimally spread across the graph by dynamically utilizing its structure
* - {@code RandomLandmarkProvider} - Randomly selects nodes as landmarks
*
* It is also possible to use a custom landmark provider by simply implementing {@link LandmarkProvider}.
* A landmark provider can be set by using {@link #setLandmarkProvider(LandmarkProvider)} and the other {@code setLandmarkProviderXXX} methods.
*
* Finally, an algorithm using the selected properties can be created using {@link #build()}.
* The initial construction might take a while, depending on the graph size. Results are cached and further constructions
* will try to utilize the cache whenever possible.
*
* The default configuration of the builder is:
*
* - {@code AStarModule}
* - {@code LandmarkMetric}
* - 20 landmarks
* - {@code RandomLandmarkProvider}
*
* The method {@link #resetDefault()} can be used to restore the default settings.
* Likewise {@link #resetOrdinaryDijkstra()} can be used to get a configuration that just uses the
* ordinary Dijkstra algorithm without any modules.
*
* @param Type of node
* @param Type of edge
*
* @author Daniel Tischner {@literal }
*/
public final class ShortestPathComputationBuilder> {
/**
* The default amount of landmarks to use.
*/
private static final int DEFAULT_AMOUNT_OF_LANDMARKS = 20;
/**
* List of modules to use for {@link ModuleDijkstra}. Will use ordinary {@link Dijkstra} if empty.
*/
private final List> modules;
/**
* The graph to operate on.
*/
private Graph graph;
/**
* Whether to use an {@link AStarModule}. Will not be added to {@link #modules} but implicitly added during {@link
* #build()}.
*/
private boolean useAStarModule;
/**
* The metric to use, if {@link #useAStarModule} is set. Building a metric may be expensive, the result is cached as
* long as possible and will be reset to {@code null} whenever the cache needs to be invalidated.
*/
private Metric metric;
/**
* Whether to use a {@link LandmarkMetric}. Only interpreted if {@link #useAStarModule} is set. Will not directly be
* set to {@link #metric}, as computation might be expensive. The result is set later during {@link #build()}.
*/
private boolean useLandmarkMetric;
/**
* The landmark provider to use, if {@link #useLandmarkMetric} is set.
*/
private LandmarkProvider landmarkProvider;
/**
* The amount of landmarks to use, if {@link #useLandmarkMetric} is set.
*/
private int amountOfLandmarks;
/**
* Creates a new shortest path computation builder operating on the given graph with default settings:
*
* - {@code AStarModule}
* - {@code LandmarkMetric}
* - 20 landmarks
* - {@code RandomLandmarkProvider}
*
*
* @param graph The graph to operate on, not null
*/
public ShortestPathComputationBuilder(final Graph graph) {
this.graph = Objects.requireNonNull(graph);
modules = new ArrayList<>();
resetDefault();
}
/**
* Sets the graph to operate on.
*
* @param graph The graph to operate on, not null
*
* @return This builder instance
*/
public ShortestPathComputationBuilder setGraph(final Graph graph) {
this.graph = Objects.requireNonNull(graph);
invalidateMetricCache();
return this;
}
/**
* Resets the current configuration to the default settings:
*
* - {@code AStarModule}
* - {@code LandmarkMetric}
* - 20 landmarks
* - {@code RandomLandmarkProvider}
*
*
* @return This builder instance
*/
@SuppressWarnings({ "WeakerAccess", "UnusedReturnValue" })
public ShortestPathComputationBuilder resetDefault() {
modules.clear();
useAStarModule = true;
metric = null;
useLandmarkMetric = true;
landmarkProvider = new RandomLandmarkProvider<>(graph);
amountOfLandmarks = ShortestPathComputationBuilder.DEFAULT_AMOUNT_OF_LANDMARKS;
return this;
}
/**
* Resets the current configuration to a setting that uses the ordinary Dijkstra algorithm, without any modules.
*
* @return This builder instance
*/
public ShortestPathComputationBuilder resetOrdinaryDijkstra() {
modules.clear();
useAStarModule = false;
metric = null;
useLandmarkMetric = false;
landmarkProvider = new RandomLandmarkProvider<>(graph);
amountOfLandmarks = ShortestPathComputationBuilder.DEFAULT_AMOUNT_OF_LANDMARKS;
return this;
}
/**
* Adds the given module to be used by {@code Module-Dijkstra}.
*
* @param module The module to add, not null
*
* @return This builder instance
*/
public ShortestPathComputationBuilder addModule(final DijkstraModule module) {
modules.add(Objects.requireNonNull(module));
return this;
}
/**
* Adds a module to be used by {@code Module-Dijkstra} which aborts computation right before a node has been settled
* that matches the given predicate.
*
* @param predicate The predicate to test the node against, not null
*
* @return This builder instance
*/
public ShortestPathComputationBuilder addModuleAbortBeforeIf(
final Predicate> predicate) {
modules.add(AbortBeforeIfModule.of(Objects.requireNonNull(predicate)));
return this;
}
/**
* Adds a module to be used by {@code Module-Dijkstra} which aborts computation as soon as a node has been settled
* that matches the given predicate.
*
* @param predicate The predicate to test the node against, not null
*
* @return This builder instance
*/
public ShortestPathComputationBuilder addModuleAbortAfterIf(
final Predicate> predicate) {
modules.add(AbortAfterIfModule.of(Objects.requireNonNull(predicate)));
return this;
}
/**
* Adds a module to be used by {@code Module-Dijkstra} which ignores exploring edges if they match the given
* predicate.
*
* @param predicate The predicate to test the edge against, not null
*
* @return This builder instance
*/
public ShortestPathComputationBuilder addModuleIgnoreEdgeIf(final Predicate predicate) {
modules.add(IgnoreEdgeIfModule.of(Objects.requireNonNull(predicate)));
return this;
}
/**
* Adds a module to be used by {@code Module-Dijkstra} which only explores the graph up until the given range.
*
* @param range The range to explore to, not negative
*
* @return This builder instance
*/
public ShortestPathComputationBuilder addModuleAbortAfterRange(final double range) {
if (range < 0) {
throw new IllegalArgumentException("Range must not be negative");
}
modules.add(AbortAfterRangeModule.of(range));
return this;
}
/**
* Adds a module to be used by {@code Module-Dijkstra} which uses the A-Star algorithm with a given heuristic metric
* for optimization.
*
* Use {@link #setMetric(Metric)} or similar methods to set a metric.
*
* @return This builder instance
*/
public ShortestPathComputationBuilder addModuleAStar() {
useAStarModule = true;
return this;
}
/**
* Removes a previously added A-Star module. Has no effect if such a module was not added before.
*
* @return This builder instance
*/
public ShortestPathComputationBuilder removeModuleAStar() {
useAStarModule = false;
return this;
}
/**
* Clears all previously added modules to be used by {@code Module-Dijkstra}.
*
* @return This builder instance
*/
public ShortestPathComputationBuilder clearModules() {
modules.clear();
useAStarModule = false;
return this;
}
/**
* Sets the metric to be used by the A-Star module. Use {@link #addModuleAStar()} to add such a module.
*
* Has no effect if no such module has been added.
*
* @param metric The heuristic metric to use, not null
*
* @return This builder instance
*/
public ShortestPathComputationBuilder setMetric(final Metric metric) {
this.metric = Objects.requireNonNull(metric);
useLandmarkMetric = false;
return this;
}
/**
* Sets the metric to be used by the A-Star module to a landmark heuristic. Use {@link #addModuleAStar()} to add
* such a module.
*
* Has no effect if no such module has been added.
*
* The landmark heuristic needs a landmark provider, use {@link #setLandmarkProvider(LandmarkProvider)} and similar
* methods to set one. Additionally, it needs the amount of landmarks to use, use {@link #setAmountOfLandmarks(int)}
* to set the amount.
*
* @return This builder instance
*/
public ShortestPathComputationBuilder setMetricLandmark() {
useLandmarkMetric = true;
invalidateMetricCache();
return this;
}
/**
* Sets the landmark provider to be used by the landmark heuristic. Use {@link #setMetricLandmark()} to use such a
* metric.
*
* Has no effect if the chosen metric is not landmarks.
*
* @param landmarkProvider The landmark provider to use, not null
*
* @return This builder instance
*/
public ShortestPathComputationBuilder setLandmarkProvider(final LandmarkProvider landmarkProvider) {
this.landmarkProvider = Objects.requireNonNull(landmarkProvider);
invalidateMetricCache();
return this;
}
/**
* Sets the landmark provider to be used by the landmark heuristic to a greedy farthest strategy. Use {@link
* #setMetricLandmark()} to use such a metric.
*
* Has no effect if the chosen metric is not landmarks.
*
* This strategy is more expensive than {@link #setLandmarkProviderRandom()} and consumes more space, but yields
* better results.
*
* @return This builder instance
*/
public ShortestPathComputationBuilder setLandmarkProviderGreedyFarthest() {
landmarkProvider = new GreedyFarthestLandmarkProvider<>(graph);
invalidateMetricCache();
return this;
}
/**
* Sets the landmark provider to be used by the landmark heuristic to a random selection strategy. Use {@link
* #setMetricLandmark()} to use such a metric.
*
* Has no effect if the chosen metric is not landmarks.
*
* @return This builder instance
*/
public ShortestPathComputationBuilder setLandmarkProviderRandom() {
landmarkProvider = new RandomLandmarkProvider<>(graph);
invalidateMetricCache();
return this;
}
/**
* Sets the amount of landmarks to be used by the landmark heuristic. Use {@link #setMetricLandmark()} to use such a
* metric.
*
* Has no effect if the chosen metric is not landmarks.
*
* The more landmarks, the better the heuristic, but the longer the memory consumption and pre-computation takes.
*
* @param amountOfLandmarks The amount of landmarks to use, not negative.
*
* @return This builder instance
*/
public ShortestPathComputationBuilder setAmountOfLandmarks(final int amountOfLandmarks) {
if (amountOfLandmarks < 0) {
throw new IllegalArgumentException("Amount of landmarks must not be negative");
}
this.amountOfLandmarks = amountOfLandmarks;
return this;
}
/**
* Builds a shortest path computation algorithm using the set properties.
*
* Depending on the graph size and the current settings, the initial call to this method might take some time.
* However, results are cached and further calls try to utilize the cache as much as possible.
*
* @return An algorithm using the set properties
*
* @throws IllegalStateException If the builder has an invalid configuration, such as specifying A-Star module but
* no metric.
*/
public ShortestPathComputation build() {
if (modules.isEmpty()) {
return new Dijkstra<>(graph);
}
final ModuleDijkstra moduleDijkstra = new ModuleDijkstra<>(graph);
if (useAStarModule) {
if (useLandmarkMetric && metric == null) {
// Construction takes time, result is cached in field
metric = new LandmarkMetric<>(amountOfLandmarks, graph, landmarkProvider);
}
if (metric == null) {
throw new IllegalStateException("Invalid builder configuration, no metric is set");
}
moduleDijkstra.addModule(AStarModule.of(metric));
}
modules.forEach(moduleDijkstra::addModule);
return moduleDijkstra;
}
/**
* Invalidates previously cached metrics if the current settings would utilize the metric if {@link #build()} would
* be called now.
*/
private void invalidateMetricCache() {
if (!useLandmarkMetric || metric == null) {
return;
}
metric = null;
}
}