org.jgrapht.nio.tsplib.TSPLIBImporter Maven / Gradle / Ivy
/*
* (C) Copyright 2020-2023, by Hannes Wellmann and Contributors.
*
* JGraphT : a free Java graph-theory library
*
* See the CONTRIBUTORS.md file distributed with this work for additional
* information regarding copyright ownership.
*
* This program and the accompanying materials are made available under the
* terms of the Eclipse Public License 2.0 which is available at
* http://www.eclipse.org/legal/epl-2.0, or the
* GNU Lesser General Public License v2.1 or later
* which is available at
* http://www.gnu.org/licenses/old-licenses/lgpl-2.1-standalone.html.
*
* SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
*/
package org.jgrapht.nio.tsplib;
import org.jgrapht.*;
import org.jgrapht.generate.*;
import org.jgrapht.nio.*;
import org.jgrapht.util.*;
import java.io.*;
import java.util.*;
import java.util.ArrayList;
import java.util.function.*;
import java.util.regex.*;
import java.util.stream.*;
import static java.util.Arrays.*;
/**
* Importer for files in the
* TSPLIB95 format.
*
*
* This importer reads the nodes of a Symmetric travelling salesman problem instance from a
* file and creates a {@link GraphTests#isComplete(Graph) complete graph} and provides further data
* from the file and about the imported graph.
*
*
* This implementation does not cover the full TSPLIB95 standard and only implements the subset of
* capabilities required at the time of creation. All keywords of The specification part in
* chapter 1.1 of the TSPLIB95 standard are considered. Their values can be obtained from
* the corresponding getters of the {@link Specification}. But only the following
*
EDGE_WEIGHT_TYPE values are supported for a NODE_DATA_SECTION:
*
* - EUC_2D
* - EUC_3D
* - MAX_2D
* - MAX_3D
* - MAN_2D
* - MAN_3D
* - CEIL2D
* - GEO
* - ATT
*
*
*
* The following data sections of The data part in chapter 1.2 of the TSPLIB95
* standard are supported:
*
* - NODE_COORD_SECTION
* - TOUR_SECTION
*
*
*
* It was attempted to make the structure of this implementation generic so further keywords from
* the specification part or other data sections can be considered if required by broaden this
* class. Currently this implementation only reads Symmetric travelling salesman problems
* with a NODE_COORD_SECTION and on of the supported EDGE_WEIGHT_TYPE.
*
*
* The website of the TSPLIB standard already contains a large library of different TSP instances
* provided as files in TSPLIB format. The
* TSPLIB library of the University of
* Waterlo provides more problem instances, among others a World TSP and instances based on
* cities of different countries.
*
*
* @author Hannes Wellmann
* @param the graph vertex type
* @param the graph edge type
*
*/
public class TSPLIBImporter
implements GraphImporter
{
private static final String NAME = "NAME";
private static final String TYPE = "TYPE";
private static final String COMMENT = "COMMENT";
private static final String DIMENSION = "DIMENSION";
private static final String CAPACITY = "CAPACITY";
private static final String EDGE_WEIGHT_TYPE = "EDGE_WEIGHT_TYPE";
private static final String EDGE_WEIGHT_FORMAT = "EDGE_WEIGHT_FORMAT";
private static final String EDGE_DATA_FORMAT = "EDGE_DATA_FORMAT";
private static final String NODE_COORD_TYPE = "NODE_COORD_TYPE";
private static final String DISPLAY_DATA_TYPE = "DISPLAY_DATA_TYPE";
private static final String NODE_COORD_SECTION = "NODE_COORD_SECTION";
private static final String TOUR_SECTION = "TOUR_SECTION";
private static final List VALID_TYPES =
asList("TSP", "ATSP", "SOP", "HCP", "CVRP", "TOUR");
private static final List VALID_EDGE_WEIGHT_TYPES = asList(
"EXPLICIT", "EUC_2D", "EUC_3D", "MAX_2D", "MAX_3D", "MAN_2D", "MAN_3D", "CEIL_2D", "GEO",
"ATT", "XRAY1", "XRAY2", "SPECIAL");
private static final List VALID_EDGE_WEIGHT_FORMATS = asList(
"FUNCTION", "FULL_MATRIX", "UPPER_ROW", "LOWER_ROW", "UPPER_DIAG_ROW", "LOWER_DIAG_ROW",
"UPPER_COL", "LOWER_COL", "UPPER_DIAG_COL", "LOWER_DIAG_COL");
private static final List VALID_EDGE_DATA_FORMATS = asList("EDGE_LIST", "ADJ_LIST");
private static final List VALID_NODE_COORD_TYPES =
asList("TWOD_COORDS", "THREED_COORDS", "NO_COORDS");
private static final List VALID_DISPLAY_DATA_TYPE =
asList("COORD_DISPLAY", "TWOD_DISPLAY", "NO_DISPLAY");
/**
* Container for the entry values read from the specification part of a file in
* TSPLIB95 format.
*
* @author Hannes Wellmann
*/
public static class Specification
{
private String name;
private String type;
private final List comment = new ArrayList<>();
private Integer dimension;
private Integer capacity;
private String edgeWeightType;
private String edgeWeightFormat;
private String edgeDataFormat;
private String nodeCoordType;
private String displayDataType;
Specification()
{
}
/**
* Returns the value of the NAME keyword in the imported file.
*
* @return the value of the NAME keyword
*/
public String getName()
{
return name;
}
/**
* Returns the value of the TYPE keyword in the imported file.
*
* @return the value of the TYPE keyword
*/
public String getType()
{
return type;
}
/**
* Returns the {@link List} of values for the COMMENT keyword in the imported file.
*
* @return the value of the COMMENT keyword
*/
public List getComments()
{
return Collections.unmodifiableList(comment);
}
/**
* Returns the value of the DIMENSION keyword in the imported file.
*
* @return the value of the DIMENSION keyword
*/
public Integer getDimension()
{
return dimension;
}
/**
* Returns the value of the CAPACITY keyword in the imported file.
*
* @return the value of the CAPACITY keyword
*/
public Integer getCapacity()
{
return capacity;
}
/**
* Returns the value of the EDGE_WEIGHT_TYPE keyword in the imported file.
*
* @return the value of the EDGE_WEIGHT_TYPE keyword
*/
public String getEdgeWeightType()
{
return edgeWeightType;
}
/**
* Returns the value of the EDGE_WEIGHT_FORMAT keyword in the imported file.
*
* @return the value of the EDGE_WEIGHT_FORMAT keyword
*/
public String getEdgeWeightFormat()
{
return edgeWeightFormat;
}
/**
* Returns the value of the EDGE_DATA_FORMAT keyword in the imported file.
*
* @return the value of the EDGE_DATA_FORMAT keyword
*/
public String getEdgeDataFormat()
{
return edgeDataFormat;
}
/**
* Returns the value of the NODE_COORD_TYPE keyword in the imported file.
*
* @return the value of the NODE_COORD_TYPE keyword
*/
public String getNodeCoordType()
{
return nodeCoordType;
}
/**
* Returns the value of the DISPLAY_DATA_TYPE keyword in the imported file.
*
* @return the value of the DISPLAY_DATA_TYPE keyword
*/
public String getDisplayDataType()
{
return displayDataType;
}
}
/**
* Container for the meta data of an imported TSPLIB95 file.
*
* @author Hannes Wellmann
* @param the graph vertex type
* @param the graph edge type
*/
public static class Metadata
{
private final Specification spec = new Specification();
private Map vertex2node;
private Graph graph;
private List tour;
private Boolean hasDistinctLocations;
private Boolean hasDistinctNeighborDistances;
private Metadata()
{
}
/**
* Returns the {@link Specification} instance containing all values from the specification
* part of a TSPLIB95 file.
*
* @return the {@code Specification} of an imported TSPLIB95 file
*/
public Specification getSpecification()
{
return spec;
}
/**
* Returns the mapping of vertex to corresponding node imported from the
* NODE_COORD_SECTION of a TSPLIB95 file.
*
* @return the mapping of vertex to corresponding node
*/
public Map getVertexToNodeMapping()
{
return vertex2node;
}
/**
* Returns the {@link List} of vertices in the order of the tour defined in an imported
* TSPLIB95 file or null if no tour was imported.
*
* Note that a tour can be imported by {@link TSPLIBImporter#importGraph(Graph, Reader)} or
* {@link TSPLIBImporter#importTour(Metadata, Reader)} .
*
*
* @return the vertex tour from the file or null
*/
public List getTour()
{
return tour;
}
/**
* Returns true if for the imported graph all vertices have distinct coordinates and non of
* them have {@link Arrays#equals(Object) equal} {@link Node#getCoordinates() coordinate
* values} , else false.
*
* @return true if no equally located nodes were imported from the file, else false
* @throws IllegalStateException if no graph was imported
*/
public boolean hasDistinctNodeLocations()
{
if (graph == null) {
throw new IllegalStateException("No graph imported");
}
if (hasDistinctLocations == null) {
hasDistinctLocations = Boolean.TRUE;
Set> distinctCoordinates =
CollectionUtil.newHashSetWithExpectedSize(vertex2node.size());
for (Node node : vertex2node.values()) {
double[] coordinates = node.getCoordinates();
// Arrays.equals checks identity. Conversion to a List and use of a
// HashSet has linear runtime. Unlike with a TreeSet using a comparator.
Double[] coordinateObj = stream(coordinates).boxed().toArray(Double[]::new);
if (!distinctCoordinates.add(Arrays.asList(coordinateObj))) {
hasDistinctLocations = Boolean.FALSE;
return hasDistinctLocations;
}
}
}
return hasDistinctLocations;
}
/**
* Returns true if for the imported graph each vertex all touching edges have different
* weights.
*
* If this method returns true this means for the TSP that for each location each other
* location has a different distance, so there are no two other locations that have the same
* distance from that location.
*
*
* @return true if all touching edges of each vertex have different weight, else false
* @throws IllegalStateException if no graph was imported
*/
public boolean hasDistinctNeighborDistances()
{
if (graph == null) {
throw new IllegalStateException("No graph imported");
}
if (hasDistinctNeighborDistances == null) {
hasDistinctNeighborDistances = Boolean.TRUE;
Set vertices = graph.vertexSet(); // each vertex has vertices.size()-1 edges
Set weights =
CollectionUtil.newHashSetWithExpectedSize(vertices.size() - 1);
for (V v : vertices) {
weights.clear();
for (E edge : graph.edgesOf(v)) {
if (!weights.add(graph.getEdgeWeight(edge))) {
hasDistinctNeighborDistances = Boolean.FALSE;
return hasDistinctNeighborDistances;
}
}
}
}
return hasDistinctNeighborDistances;
}
}
/**
* A node imported from the NODE_COORD_SECTION of a TSPLIB95-file.
*
* @author Hannes Wellmann
*/
public static class Node
{
/** The one based number of this node. */
private final int number;
/** The coordinates of this node. */
private final double[] coordinates;
Node(int number, double[] coordinates)
{
this.number = number;
this.coordinates = coordinates;
}
/**
* Returns the number of this node as specified in the source TSPLIB95-file.
*
* @return the number of this node
*/
public int getNumber()
{
return number;
}
/**
* Returns the number of elements the coordinates of this node have (either two or three).
*
* @return the number of coordinate elements of this node
*/
public int getCoordinatesLength()
{
return coordinates.length;
}
/**
* Returns the value of the coordinate element with zero-based index i of this
* node.
*
* @param i the index of the coordinate element
* @return the value of the i-th coordinate element
*/
public double getCoordinateValue(int i)
{
return coordinates[i];
}
/**
* Returns a copy of the coordinates of this node.
*
* @return the coordinates of this node
*/
public double[] getCoordinates()
{
return Arrays.copyOf(coordinates, coordinates.length);
}
@Override
public String toString()
{
return number + " " + Arrays
.stream(coordinates).mapToObj(Double::toString).collect(Collectors.joining(" "));
}
}
private int vectorLength = -1;
private Metadata metadata;
/** Constructs a new importer. */
public TSPLIBImporter()
{ // NoOp
}
/**
* Returns the {@link Metadata} of the latest imported file or null, if no import completed yet
* or the latest import failed.
*
* @return {@code TSPLIBFileData} of the latest import
*/
public Metadata getMetadata()
{
return metadata;
}
// read of node data section
/**
* {@inheritDoc}
*
* The given {@link Graph} must be weighted. Also the graph should be empty, otherwise the
* behavior is unspecified which could lead to exceptions.
*
*
* The source of the given Reader should contain a NODE_COORD_SECTION (if not the graph
* is not changed) and can contain a TOUR_SECTION. If a TOUR_SECTION is
* present a corresponding NODE_COORD_SECTION is mandatory and the read vertex numbers
* are referred to the NODE_COORD_SECTION in the same source.
*
*
* {@link Metadata} of the import can be obtained with {@link #getMetadata()} after this method
* returns. If the readers source contains a TOUR_SECTION the imported tour can be
* obtained from {@link Metadata#getTour()}.
*
*
* This implementation is not thread-safe and must be synchronized externally if called by
* concurrent threads.
*
*
* @param graph the graph into which this importer writes, must weighted.
* @throws IllegalArgumentException if the specified {@code graph} is not weighted
*/
@Override
public void importGraph(Graph graph, Reader in)
{
metadata = null;
try {
Iterator lines = getLineIterator(in);
metadata = readContentForGraph(lines, graph);
} catch (Exception e) {
throw getImportException(e, "graph");
}
}
private Metadata readContentForGraph(Iterator lines, Graph graph)
{
if (!graph.getType().isWeighted()) {
throw new IllegalArgumentException("Graph must be weighted");
}
vectorLength = -1;
Metadata data = new Metadata<>();
List tour = null;
while (lines.hasNext()) {
String[] keyValue = lines.next().split(":");
String key = getKey(keyValue);
if (readSpecificationSection(key, data.spec, keyValue)) {
// some specification element was read. Continue with next line.
} else if (NODE_COORD_SECTION.equals(key)) {
requireNotSet(data.graph, NODE_COORD_SECTION);
data.graph = graph;
data.vertex2node = readNodeCoordinateSection(lines, data);
} else if (TOUR_SECTION.equals(key)) {
requireNotSet(tour, TOUR_SECTION);
tour = readTourSection(lines, data.spec.dimension);
}
}
if (tour != null) {
data.tour = getVertexTour(tour, data.vertex2node);
}
return data;
}
/**
* Reads all nodes of the NODE_COORD_SECTION and fills the graph of the data accordingly.
*
* @return a mapping from created graph {@link V vertex} to corresponding imported {@link Node}
*/
private Map readNodeCoordinateSection(Iterator lines, Metadata data)
{
requireSet(data.spec.edgeWeightType, NODE_COORD_SECTION);
requireSet(data.spec.dimension, DIMENSION); // DIMENSION specifies the number of nodes
ToIntBiFunction edgeWeightFunction =
getEdgeWeightFunction(data.spec.edgeWeightType);
List nodes = readNodes(lines, data.spec.dimension);
// create vertices for all imported nodes
Map vertex2node = CollectionUtil.newHashMapWithExpectedSize(nodes.size());
Graph graph = data.graph;
for (Node node : nodes) {
V v = graph.addVertex();
vertex2node.put(v, node);
}
// create edges for each possible pair of vertices and compute their weights
new CompleteGraphGenerator().generateGraph(graph, null);
graph.edgeSet().forEach(e -> {
Node s = vertex2node.get(graph.getEdgeSource(e));
Node t = vertex2node.get(graph.getEdgeTarget(e));
double weight = edgeWeightFunction.applyAsInt(s, t);
graph.setEdgeWeight(e, weight);
});
return Collections.unmodifiableMap(vertex2node);
}
private ToIntBiFunction getEdgeWeightFunction(String edgeWeightType)
{
switch (edgeWeightType) {
case "EUC_2D":
vectorLength = 2;
return this::computeEuclideanDistance;
case "EUC_3D":
vectorLength = 3;
return this::computeEuclideanDistance;
case "MAX_2D":
vectorLength = 2;
return this::computeMaximumDistance;
case "MAX_3D":
vectorLength = 3;
return this::computeMaximumDistance;
case "MAN_2D":
vectorLength = 2;
return this::computeManhattanDistance;
case "MAN_3D":
vectorLength = 3;
return this::computeManhattanDistance;
case "CEIL_2D":
vectorLength = 2;
return this::compute2DCeilingEuclideanDistance;
case "GEO":
vectorLength = 2;
return this::compute2DGeographicalDistance;
case "ATT":
vectorLength = 2;
return this::compute2DPseudoEuclideanDistance;
default:
throw new IllegalStateException(
"Unsupported EDGE_WEIGHT_TYPE <" + edgeWeightType + ">");
}
}
private List readNodes(Iterator lines, int dimension)
{
List nodes = new ArrayList<>(dimension);
for (int i = 0; i < dimension && lines.hasNext(); i++) {
String line = lines.next();
Node node = parseNode(line);
nodes.add(node);
}
return nodes;
}
private static final Pattern WHITE_SPACE = Pattern.compile("[ \t]+");
private Node parseNode(String line)
{
String[] elements = WHITE_SPACE.split(line);
if (elements.length != vectorLength + 1) {
throw new IllegalArgumentException(
"Unexpected number of elements <" + elements.length + "> in line: " + line);
}
int number = Integer.parseInt(elements[0]);
double[] coordinates =
Arrays.stream(elements, 1, elements.length).mapToDouble(Double::parseDouble).toArray();
return new Node(number, coordinates);
}
// read of tour data section
/**
* Imports a tour described by a {@link List} of {@link V vertices} using the given Reader.
*
* It is the callers responsibility to ensure the {@code Reader} is closed after this method
* returned.
*
*
* The source of the given Reader should contain a TOUR_SECTION (if not null is
* returned). The vertices specified by their number in the TOUR_SECTION are referred
* to the nodes respectively vertices in the given {@code metadata}.
*
*
* The {@link Metadata} of the import can be obtained with {@link #getMetadata()} after this
* method returns. The {@code Metadata#getVertexToNodeMapping() vertexToNodeMapping} in the
* metadata of this import is the same as in the given {@code metadata}.
*
*
* This implementation is not thread-safe and must be synchronized externally if called by
* concurrent threads.
*
*
* @param referenceMetadata the {@code Metadata} defining the available vertices and their
* {@code Nodes}.
* @param in the input reader
* @return the imported tour or null, if no tour was imported
*/
public List importTour(Metadata referenceMetadata, Reader in)
{
metadata = null;
try {
Iterator lines = getLineIterator(in);
metadata = readContentForTour(lines, referenceMetadata.vertex2node);
return metadata.tour;
} catch (Exception e) {
throw getImportException(e, "tour");
}
}
private Metadata readContentForTour(Iterator lines, Map vertex2node)
{
Metadata data = new Metadata<>();
while (lines.hasNext()) {
String[] keyValue = lines.next().split(":");
String key = getKey(keyValue);
if (readSpecificationSection(key, data.spec, keyValue)) {
// some specification element was read. Continue with next line.
} else if (TOUR_SECTION.equals(key)) {
requireNotSet(data.tour, TOUR_SECTION);
List tour = readTourSection(lines, data.spec.dimension);
data.tour = getVertexTour(tour, vertex2node);
}
}
data.vertex2node = vertex2node;
return data;
}
/**
* Reads a tour of the TOUR_SECTION and returns the List of ordered vertex numbers describing
* the tour.
*
* @return the list of vertex number describing the tour
*/
private List readTourSection(Iterator lines, Integer dimension)
{
List tour = dimension != null ? new ArrayList<>(dimension) : new ArrayList<>();
while (lines.hasNext()) {
String lineContent = lines.next();
if ("-1".equals(lineContent)) {
break;
}
tour.add(Integer.valueOf(lineContent));
}
return tour;
}
private List getVertexTour(List tour, Map vertex2node)
{
requireSet(vertex2node, TOUR_SECTION);
List orderedVertices = getOrderedVertices(vertex2node);
List vertexTour = new ArrayList<>(orderedVertices.size());
for (Integer vertexNumber : tour) { // number may be zero or one based (its more a id)
V v = vertexNumber < orderedVertices.size() ? orderedVertices.get(vertexNumber) : null;
if (v == null) {
throw new IllegalStateException("Missing vertex with number " + vertexNumber);
}
vertexTour.add(v);
}
return vertexTour;
}
private List getOrderedVertices(Map vertex2node)
{
int maxNumber = vertex2node.values().stream().mapToInt(Node::getNumber).max().getAsInt();
@SuppressWarnings("unchecked") V[] orderedVertices = (V[]) new Object[maxNumber + 1];
vertex2node.forEach((v, n) -> orderedVertices[n.number] = v);
return asList(orderedVertices);
}
// read of specification
private boolean readSpecificationSection(String key, Specification spec, String[] lineElements)
{
// only read value if it is sure that there should be a value
switch (key) {
case NAME:
requireNotSet(spec.name, NAME);
spec.name = getValue(lineElements);
return true;
case TYPE:
requireNotSet(spec.type, TYPE);
String type = getValue(lineElements);
spec.type = requireValidValue(type, VALID_TYPES, TYPE);
return true;
case COMMENT:
String comment = getValue(lineElements);
spec.comment.add(comment);
return true;
case DIMENSION:
requireNotSet(spec.dimension, DIMENSION);
String dimension = getValue(lineElements);
spec.dimension = parseInteger(dimension, DIMENSION);
return true;
case CAPACITY:
requireNotSet(spec.capacity, CAPACITY);
String capacity = getValue(lineElements);
spec.capacity = parseInteger(capacity, CAPACITY);
return true;
case EDGE_WEIGHT_TYPE:
requireNotSet(spec.edgeWeightType, EDGE_WEIGHT_TYPE);
String edgeWeightType = getValue(lineElements);
spec.edgeWeightType =
requireValidValue(edgeWeightType, VALID_EDGE_WEIGHT_TYPES, EDGE_WEIGHT_TYPE);
return true;
case EDGE_WEIGHT_FORMAT:
requireNotSet(spec.edgeWeightFormat, EDGE_WEIGHT_FORMAT);
String edgeWeightFormat = getValue(lineElements);
spec.edgeWeightFormat =
requireValidValue(edgeWeightFormat, VALID_EDGE_WEIGHT_FORMATS, EDGE_WEIGHT_FORMAT);
return true;
case EDGE_DATA_FORMAT:
requireNotSet(spec.edgeDataFormat, EDGE_DATA_FORMAT);
String edgeDataFormat = getValue(lineElements);
spec.edgeDataFormat =
requireValidValue(edgeDataFormat, VALID_EDGE_DATA_FORMATS, EDGE_DATA_FORMAT);
return true;
case NODE_COORD_TYPE:
requireNotSet(spec.nodeCoordType, NODE_COORD_TYPE);
String nodeCoordType = getValue(lineElements);
spec.nodeCoordType =
requireValidValue(nodeCoordType, VALID_NODE_COORD_TYPES, NODE_COORD_TYPE);
return true;
case DISPLAY_DATA_TYPE:
requireNotSet(spec.displayDataType, DISPLAY_DATA_TYPE);
String displayDataType = getValue(lineElements);
spec.displayDataType =
requireValidValue(displayDataType, VALID_DISPLAY_DATA_TYPE, DISPLAY_DATA_TYPE);
return true;
default:
return false;
}
}
private String requireValidValue(String value, List validValues, String valueType)
{
value = extractValueBeforeWhitespace(value);
for (String validValue : validValues) {
if (validValue.equalsIgnoreCase(value)) {
return validValue; // always use the upper case version
}
}
throw new IllegalArgumentException("Invalid " + valueType + " value <" + value + ">");
}
private Integer parseInteger(String valueStr, String valueType)
{
valueStr = extractValueBeforeWhitespace(valueStr);
try {
return Integer.valueOf(valueStr);
} catch (NumberFormatException e) {
throw new IllegalArgumentException(
"Invalid " + valueType + " integer value <" + valueStr + ">", e);
}
}
public String extractValueBeforeWhitespace(String value)
{
return WHITE_SPACE.split(value.strip(), 2)[0]; // discard everything after first white-space
}
// read utilities
private static Iterator getLineIterator(Reader in)
{
BufferedReader reader = new BufferedReader(in);
return Stream.iterate(readLine(reader), Objects::nonNull, l -> readLine(reader)).iterator();
}
private static String readLine(BufferedReader reader)
{
try {
String line = reader.readLine();
if (line != null) {
line = line.strip();
return "EOF".equals(line) ? null : line;
}
return null;
} catch (IOException e) {
throw new IllegalStateException("I/O exception while reading line of TSPLIB file", e);
}
}
private static String getKey(String[] keyValue)
{
return keyValue[0].strip().toUpperCase();
}
private String getValue(String[] keyValue)
{
if (keyValue.length < 2) {
throw new IllegalStateException("Missing value for key " + getKey(keyValue));
}
return keyValue[1].strip();
}
private void requireNotSet(Object target, String keyName)
{
if (target != null) {
throw new IllegalStateException("Multiple values for key " + keyName);
}
}
private void requireSet(Object requirement, String target)
{
if (requirement == null) {
throw new IllegalStateException("Missing data to read <" + target + ">");
}
}
private static ImportException getImportException(Exception e, String target)
{
return new ImportException(
"Failed to import " + target + " from TSPLIB-file: " + e.getMessage(), e);
}
// distance computations
// all of the following methods are implemented in accordance to
// section "2. The distance functions" of TSPLIB95
/**
* Computes the distance of the two nodes n1 and n2 according to the {@code EUC_2D} or
* {@code EUC_3D} metric depending on their dimension. The used metric is also known as L2-norm.
*
* @param n1 a {@code Node} with two or three dimensional coordinates
* @param n2 a {@code Node} with two or three dimensional coordinates
* @return the {@code EUC_2D} or {@code EUC_3D} edge weight for nodes n1 and n2
*/
int computeEuclideanDistance(Node n1, Node n2)
{ // according to TSPLIB95 distances are rounded to next integer value
return (int) Math.round(getL2Distance(n1, n2));
}
/**
* Computes the distance of the two nodes n1 and n2 according to the {@code MAX_2D} or
* {@code MAX_3D} metric depending on their dimension. The used metric is also known as
* L∞-norm.
*
* @param n1 a {@code Node} with two or three dimensional coordinates
* @param n2 a {@code Node} with two or three dimensional coordinates
* @return the {@code MAX_2D} or {@code MAX_3D} edge weight for nodes n1 and n2
*/
int computeMaximumDistance(Node n1, Node n2)
{ // according to TSPLIB95 distances are rounded to next integer value
return (int) Math.round(getLInfDistance(n1, n2));
}
/**
* Computes the distance of the two nodes n1 and n2 according to the {@code MAN_2D} or
* {@code MAN_3D} metric depending on their dimension. The used metric is also known as L1-norm.
*
* @param n1 a {@code Node} with two or three dimensional coordinates
* @param n2 a {@code Node} with two or three dimensional coordinates
* @return the {@code MAN_2D} or {@code MAN_3D} edge weight for nodes n1 and n2
*/
int computeManhattanDistance(Node n1, Node n2)
{ // according to TSPLIB95 distances are rounded to next integer value
return (int) Math.round(getL1Distance(n1, n2));
}
/**
* Computes the distance of the two nodes n1 and n2 according to the {@code CEIL_2D} metric, the
* round up version of {@code EUC_2D}. The points must have dimension two.
*
* @param n1 a {@code Node} with two or three dimensional coordinates
* @param n2 a {@code Node} with two or three dimensional coordinates
* @return the {@code CEIL_2D} edge weight for nodes n1 and n2
* @see #computeEuclideanDistance(RealVector, RealVector)
*/
int compute2DCeilingEuclideanDistance(Node n1, Node n2)
{
return (int) Math.ceil(getL2Distance(n1, n2));
}
/**
* Computes the distance of the two nodes n1 and n2 according to the {@code GEO} metric. The
* used metric computes the distance between two points on a earth-like sphere, while the point
* coordinates describe their geographical latitude and longitude. The points must have
* dimension two.
*
* @param n1 a {@code Node} with two or three dimensional coordinates
* @param n2 a {@code Node} with two or three dimensional coordinates
* @return the {@code GEO} edge weight for nodes n1 and n2
*/
int compute2DGeographicalDistance(Node n1, Node n2)
{
double latitude1 = computeRadiansAngle(n1.getCoordinateValue(0));
double longitude1 = computeRadiansAngle(n1.getCoordinateValue(1));
double latitude2 = computeRadiansAngle(n2.getCoordinateValue(0));
double longitude2 = computeRadiansAngle(n2.getCoordinateValue(1));
double q1 = Math.cos(longitude1 - longitude2);
double q2 = Math.cos(latitude1 - latitude2);
double q3 = Math.cos(latitude1 + latitude2);
return (int) (RRR * Math.acos(0.5 * ((1.0 + q1) * q2 - (1.0 - q1) * q3)) + 1.0);
}
static final double PI = 3.141592; // constants according to TSPLIB95
static final double RRR = 6378.388; // constants according to TSPLIB95
private static double computeRadiansAngle(double x)
{ // computation according to TSPLIB95 chapter 2.4 - Geographical distance
// First computes decimal angle from degrees and minutes, then converts it into radian
double deg = Math.round(x);
double min = x - deg;
return PI * (deg + 5.0 * min / 3.0) / 180.0;
}
/**
* Computes the distance of two the two nodes n1 and n2 according to the {@code ATT} metric. The
* nodes must have two dimensional coordinates.
*
* @param n1 a {@code Node} with two dimensional coordinates
* @param n2 a {@code Node} with two dimensional coordinates
* @return the {@code ATT} edge weight for nodes n1 and n2
*/
int compute2DPseudoEuclideanDistance(Node n1, Node n2)
{
double xd = n1.getCoordinateValue(0) - n2.getCoordinateValue(0);
double yd = n1.getCoordinateValue(1) - n2.getCoordinateValue(1);
double rij = Math.sqrt((xd * xd + yd * yd) / 10.0);
double tij = Math.round(rij);
if (tij < rij) {
return (int) (tij + 1);
} else {
return (int) tij;
}
}
private double getL1Distance(Node n1, Node n2)
{
double elementSum = 0;
for (int i = 0; i < vectorLength; i++) {
double delta = n1.getCoordinateValue(i) - n2.getCoordinateValue(i);
elementSum += Math.abs(delta);
}
return elementSum;
}
private double getL2Distance(Node n1, Node n2)
{
double elementSum = 0;
for (int i = 0; i < vectorLength; i++) {
double delta = n1.getCoordinateValue(i) - n2.getCoordinateValue(i);
elementSum += delta * delta;
}
return Math.sqrt(elementSum);
}
private double getLInfDistance(Node n1, Node n2)
{
double maxElement = 0;
for (int i = 0; i < vectorLength; i++) {
double delta = n1.getCoordinateValue(i) - n2.getCoordinateValue(i);
maxElement = Math.max(maxElement, Math.abs(delta));
}
return maxElement;
}
}