com.arcadedb.query.sql.function.graph.SQLFunctionAstar Maven / Gradle / Ivy
/*
* Copyright © 2021-present Arcade Data Ltd ([email protected])
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*
* SPDX-FileCopyrightText: 2021-present Arcade Data Ltd ([email protected])
* SPDX-License-Identifier: Apache-2.0
*/
package com.arcadedb.query.sql.function.graph;
import com.arcadedb.database.Database;
import com.arcadedb.database.Document;
import com.arcadedb.database.Identifiable;
import com.arcadedb.database.Record;
import com.arcadedb.graph.Edge;
import com.arcadedb.graph.Vertex;
import com.arcadedb.query.sql.executor.CommandContext;
import com.arcadedb.query.sql.executor.MultiValue;
import com.arcadedb.query.sql.executor.Result;
import com.arcadedb.utility.FileUtils;
import java.util.*;
/**
* A*'s algorithm describes how to find the cheapest path from one node to another node in a directed weighted graph with husrestic
* function.
*
* The first parameter is source record. The second parameter is destination record. The third parameter is a name of property that
* represents 'weight' and fourth parameter represents the map of options.
*
* If property is not defined in edge or is null, distance between vertexes are 0 .
*
* @author Saeed Tabrizi (saeed a_t nowcando.com)
*/
public class SQLFunctionAstar extends SQLFunctionHeuristicPathFinderAbstract {
public static final String NAME = "astar";
private String paramWeightFieldName = "weight";
private long currentDepth = 0;
protected final Set closedSet = new HashSet();
protected final Map cameFrom = new HashMap();
protected final Map gScore = new HashMap();
protected final Map fScore = new HashMap();
protected final PriorityQueue open = new PriorityQueue(1, (nodeA, nodeB) -> Double.compare(fScore.get(nodeA), fScore.get(nodeB)));
public SQLFunctionAstar() {
super(NAME);
}
public LinkedList execute(final Object iThis, final Identifiable iCurrentRecord, final Object iCurrentResult, final Object[] iParams,
final CommandContext iContext) {
context = iContext;
final SQLFunctionAstar context = this;
final Document record = iCurrentRecord != null ? (Document) iCurrentRecord.getRecord() : null;
Object source = iParams[0];
if (MultiValue.isMultiValue(source)) {
if (MultiValue.getSize(source) > 1)
throw new IllegalArgumentException("Only one sourceVertex is allowed");
source = MultiValue.getFirstValue(source);
if (source instanceof Result && ((Result) source).isElement()) {
source = ((Result) source).getElement().get();
}
}
if (record != null)
source = record.get((String) source);
if (source instanceof Identifiable) {
final Document elem = (Document) ((Identifiable) source).getRecord();
if (!(elem instanceof Vertex))
throw new IllegalArgumentException("The sourceVertex must be a vertex record");
paramSourceVertex = (Vertex) elem;
} else {
throw new IllegalArgumentException("The sourceVertex must be a vertex record");
}
Object dest = iParams[1];
if (MultiValue.isMultiValue(dest)) {
if (MultiValue.getSize(dest) > 1)
throw new IllegalArgumentException("Only one destinationVertex is allowed");
dest = MultiValue.getFirstValue(dest);
if (dest instanceof Result && ((Result) dest).isElement()) {
dest = ((Result) dest).getElement().get();
}
}
if (record != null)
dest = record.get((String) dest);
if (dest instanceof Identifiable) {
final Document elem = (Document) ((Identifiable) dest).getRecord();
if (!(elem instanceof Vertex))
throw new IllegalArgumentException("The destinationVertex must be a vertex record");
paramDestinationVertex = (Vertex) elem;
} else {
throw new IllegalArgumentException("The destinationVertex must be a vertex record");
}
paramWeightFieldName = FileUtils.getStringContent(iParams[2]);
if (iParams.length > 3) {
bindAdditionalParams(iParams[3], context);
}
iContext.setVariable("getNeighbors", 0);
if (paramSourceVertex == null || paramDestinationVertex == null) {
return new LinkedList<>();
}
return internalExecute(iContext, iContext.getDatabase());
}
private LinkedList internalExecute(final CommandContext iContext, final Database graph) {
final Vertex start = paramSourceVertex;
final Vertex goal = paramDestinationVertex;
open.add(start);
// The cost of going from start to start is zero.
gScore.put(start, 0.0);
// For the first node, that value is completely heuristic.
fScore.put(start, getHeuristicCost(start, null, goal, iContext));
while (!open.isEmpty()) {
Vertex current = open.poll();
// we discussed about this feature in https://github.com/orientechnologies/orientdb/pull/6002#issuecomment-212492687
if (paramEmptyIfMaxDepth && currentDepth >= paramMaxDepth) {
route.clear(); // to ensure our result is empty
return getPath();
}
// if start and goal vertex is equal so return current path from cameFrom hash map
if (current.getIdentity().equals(goal.getIdentity()) || currentDepth >= paramMaxDepth) {
while (current != null) {
route.add(0, current);
current = cameFrom.get(current);
}
return getPath();
}
closedSet.add(current);
for (final Edge neighborEdge : getNeighborEdges(current)) {
final Vertex neighbor = getNeighbor(current, neighborEdge, graph);
// Ignore the neighbor which is already evaluated.
if (closedSet.contains(neighbor)) {
continue;
}
// The distance from start to a neighbor
final double tentative_gScore = gScore.get(current) + getDistance(neighborEdge);
final boolean contains = open.contains(neighbor);
if (!contains || tentative_gScore < gScore.get(neighbor)) {
gScore.put(neighbor, tentative_gScore);
fScore.put(neighbor, tentative_gScore + getHeuristicCost(neighbor, current, goal, iContext));
if (contains) {
open.remove(neighbor);
}
open.offer(neighbor);
cameFrom.put(neighbor, current);
}
}
// Increment Depth Level
currentDepth++;
}
return getPath();
}
private Vertex getNeighbor(final Vertex current, final Edge neighborEdge, final Database graph) {
if (neighborEdge.getOut().equals(current.getIdentity())) {
return toVertex(neighborEdge.getIn());
}
return toVertex(neighborEdge.getOut());
}
private Vertex toVertex(Identifiable outVertex) {
if (outVertex == null)
return null;
if (!(outVertex instanceof Record))
outVertex = outVertex.getRecord();
return (Vertex) outVertex;
}
protected Set getNeighborEdges(final Vertex node) {
context.incrementVariable("getNeighbors");
final Set neighbors = new HashSet();
if (node != null) {
for (final Edge v : node.getEdges(paramDirection, paramEdgeTypeNames)) {
final Edge ov = v;
if (ov != null)
neighbors.add(ov);
}
}
return neighbors;
}
private void bindAdditionalParams(final Object additionalParams, final SQLFunctionAstar context) {
if (additionalParams == null) {
return;
}
Map mapParams = null;
if (additionalParams instanceof Map) {
mapParams = (Map) additionalParams;
} else if (additionalParams instanceof Identifiable) {
mapParams = ((Document) ((Identifiable) additionalParams).getRecord()).toMap();
}
if (mapParams != null) {
context.paramEdgeTypeNames = stringArray(mapParams.get(SQLFunctionAstar.PARAM_EDGE_TYPE_NAMES));
context.paramVertexAxisNames = stringArray(mapParams.get(SQLFunctionAstar.PARAM_VERTEX_AXIS_NAMES));
if (mapParams.get(SQLFunctionAstar.PARAM_DIRECTION) != null) {
if (mapParams.get(SQLFunctionAstar.PARAM_DIRECTION) instanceof String) {
context.paramDirection = Vertex.DIRECTION.valueOf(stringOrDefault(mapParams.get(SQLFunctionAstar.PARAM_DIRECTION), "OUT").toUpperCase(Locale.ENGLISH));
} else {
context.paramDirection = (Vertex.DIRECTION) mapParams.get(SQLFunctionAstar.PARAM_DIRECTION);
}
}
context.paramParallel = booleanOrDefault(mapParams.get(SQLFunctionAstar.PARAM_PARALLEL), false);
context.paramMaxDepth = longOrDefault(mapParams.get(SQLFunctionAstar.PARAM_MAX_DEPTH), context.paramMaxDepth);
context.paramEmptyIfMaxDepth = booleanOrDefault(mapParams.get(SQLFunctionAstar.PARAM_EMPTY_IF_MAX_DEPTH), context.paramEmptyIfMaxDepth);
context.paramTieBreaker = booleanOrDefault(mapParams.get(SQLFunctionAstar.PARAM_TIE_BREAKER), context.paramTieBreaker);
context.paramDFactor = doubleOrDefault(mapParams.get(SQLFunctionAstar.PARAM_D_FACTOR), context.paramDFactor);
if (mapParams.get(SQLFunctionAstar.PARAM_HEURISTIC_FORMULA) != null) {
if (mapParams.get(SQLFunctionAstar.PARAM_HEURISTIC_FORMULA) instanceof String) {
context.paramHeuristicFormula = SQLHeuristicFormula.valueOf(
stringOrDefault(mapParams.get(SQLFunctionAstar.PARAM_HEURISTIC_FORMULA), "MANHATTAN").toUpperCase(Locale.ENGLISH));
} else {
context.paramHeuristicFormula = (SQLHeuristicFormula) mapParams.get(SQLFunctionAstar.PARAM_HEURISTIC_FORMULA);
}
}
context.paramCustomHeuristicFormula = stringOrDefault(mapParams.get(SQLFunctionAstar.PARAM_CUSTOM_HEURISTIC_FORMULA), "");
}
}
public String getSyntax() {
return "astar(, , , []) \n // options : {direction:\"OUT\",edgeTypeNames:[] , vertexAxisNames:[] , parallel : false , tieBreaker:true,maxDepth:99999,dFactor:1.0,customHeuristicFormula:'custom_Function_Name_here' }";
}
@Override
public Object getResult() {
return getPath();
}
@Override
protected double getDistance(final Vertex node, final Vertex parent, final Vertex target) {
final Iterator edges = node.getEdges(paramDirection).iterator();
Edge e = null;
while (edges.hasNext()) {
final Edge next = edges.next();
if (next.getOut().equals(target.getIdentity()) || next.getIn().equals(target.getIdentity())) {
e = next;
break;
}
}
if (e != null) {
final Object fieldValue = e.get(paramWeightFieldName);
if (fieldValue != null)
if (fieldValue instanceof Float)
return (Float) fieldValue;
else if (fieldValue instanceof Number)
return ((Number) fieldValue).doubleValue();
}
return MIN;
}
protected double getDistance(final Edge edge) {
if (edge != null) {
final Object fieldValue = edge.get(paramWeightFieldName);
if (fieldValue != null)
if (fieldValue instanceof Float)
return (Float) fieldValue;
else if (fieldValue instanceof Number)
return ((Number) fieldValue).doubleValue();
}
return MIN;
}
@Override
public boolean aggregateResults() {
return false;
}
@Override
protected double getHeuristicCost(final Vertex node, Vertex parent, final Vertex target, final CommandContext iContext) {
double hresult = 0.0;
if (paramVertexAxisNames.length == 0) {
return hresult;
} else if (paramVertexAxisNames.length == 1) {
final double n = doubleOrDefault(node.get(paramVertexAxisNames[0]), 0.0);
final double g = doubleOrDefault(target.get(paramVertexAxisNames[0]), 0.0);
hresult = getSimpleHeuristicCost(n, g, paramDFactor);
} else if (paramVertexAxisNames.length == 2) {
if (parent == null)
parent = node;
final double sx = doubleOrDefault(paramSourceVertex.get(paramVertexAxisNames[0]), 0);
final double sy = doubleOrDefault(paramSourceVertex.get(paramVertexAxisNames[1]), 0);
final double nx = doubleOrDefault(node.get(paramVertexAxisNames[0]), 0);
final double ny = doubleOrDefault(node.get(paramVertexAxisNames[1]), 0);
final double px = doubleOrDefault(parent.get(paramVertexAxisNames[0]), 0);
final double py = doubleOrDefault(parent.get(paramVertexAxisNames[1]), 0);
final double gx = doubleOrDefault(target.get(paramVertexAxisNames[0]), 0);
final double gy = doubleOrDefault(target.get(paramVertexAxisNames[1]), 0);
switch (paramHeuristicFormula) {
case MANHATTAN:
hresult = getManhattanHeuristicCost(nx, ny, gx, gy, paramDFactor);
break;
case MAXAXIS:
hresult = getMaxAxisHeuristicCost(nx, ny, gx, gy, paramDFactor);
break;
case DIAGONAL:
hresult = getDiagonalHeuristicCost(nx, ny, gx, gy, paramDFactor);
break;
case EUCLIDEAN:
hresult = getEuclideanHeuristicCost(nx, ny, gx, gy, paramDFactor);
break;
case EUCLIDEANNOSQR:
hresult = getEuclideanNoSQRHeuristicCost(nx, ny, gx, gy, paramDFactor);
break;
}
if (paramTieBreaker) {
hresult = getTieBreakingHeuristicCost(px, py, sx, sy, gx, gy, hresult);
}
} else {
final Map sList = new HashMap();
final Map cList = new HashMap();
final Map pList = new HashMap();
final Map gList = new HashMap();
parent = parent == null ? node : parent;
for (int i = 0; i < paramVertexAxisNames.length; i++) {
final Double s = doubleOrDefault(paramSourceVertex.get(paramVertexAxisNames[i]), 0);
final Double c = doubleOrDefault(node.get(paramVertexAxisNames[i]), 0);
final Double g = doubleOrDefault(target.get(paramVertexAxisNames[i]), 0);
final Double p = doubleOrDefault(parent.get(paramVertexAxisNames[i]), 0);
if (s != null)
sList.put(paramVertexAxisNames[i], s);
if (c != null)
cList.put(paramVertexAxisNames[i], s);
if (g != null)
gList.put(paramVertexAxisNames[i], g);
if (p != null)
pList.put(paramVertexAxisNames[i], p);
}
switch (paramHeuristicFormula) {
case MANHATTAN:
hresult = getManhattanHeuristicCost(paramVertexAxisNames, sList, cList, pList, gList, currentDepth, paramDFactor);
break;
case MAXAXIS:
hresult = getMaxAxisHeuristicCost(paramVertexAxisNames, sList, cList, pList, gList, currentDepth, paramDFactor);
break;
case DIAGONAL:
hresult = getDiagonalHeuristicCost(paramVertexAxisNames, sList, cList, pList, gList, currentDepth, paramDFactor);
break;
case EUCLIDEAN:
hresult = getEuclideanHeuristicCost(paramVertexAxisNames, sList, cList, pList, gList, currentDepth, paramDFactor);
break;
case EUCLIDEANNOSQR:
hresult = getEuclideanNoSQRHeuristicCost(paramVertexAxisNames, sList, cList, pList, gList, currentDepth, paramDFactor);
break;
}
if (paramTieBreaker) {
hresult = getTieBreakingHeuristicCost(paramVertexAxisNames, sList, cList, pList, gList, currentDepth, hresult);
}
}
return hresult;
}
@Override
protected boolean isVariableEdgeWeight() {
return true;
}
}