Traveling Salesman

https://img.shields.io/badge/AIMMS_4.90-ZIP:_Traveling_Salesman-blue https://img.shields.io/badge/AIMMS_4.90-Github:_Traveling_Salesman-blue https://img.shields.io/badge/AIMMS_Community-Forum-yellow ../../_images/project2.gif

Story

The Traveling Salesman Problem (TSP) is the problem of finding the best route given a set of locations and distances between those locations.

It is widely researched for several reasons including:

  1. They are notoriously hard to solve to proven optimality.

  2. There is a wide range of practical applications.

  3. There are various ways of solving TSP problems.

The purpose of this example application is to illustrate some ways of solving a TSP.

Mathematical Model

In this example you will find both heuristic and MIP model for comparison.

Heuristic

This example illustrates some of AIMMS control flow statements by means of the traveling salesman 2-opt heuristic. In the model tree, you will find some declarations to define the problem. In addition, you will find:

  • A procedure and some declarations to compute and visualize an initial tour constructed by starting at some city and successively selecting the next city as the closest city not yet part of the tour.

  • A procedure and some declarations to compute and visualize an improved tour constructed by repetitively swapping those two arcs in the tour by means of the 2-opt heuristic that give the largest overall distance improvement, until no further improvement is possible or the iteration limit is reached.

  • A procedure and some declarations to compute and visualize an improved tour constructed by repetitively swapping the next arc in the (modified) tour with that neighbor arc which gives the largest distance improvement, until the iteration limit is reached or a full cycle over the tour gives no further improvement.

MIP

This AIMMS project illustrates the use of a binary variables. In this example the (symmetric) Traveling Salesman Problem (TSP) is formulated using subtour elimination constraints. The amount of subtour elimination constraints is exponential, and therefore they are added using a lazy constraint callback. Lazy constraints are constraints that should be satisfied by any solution to the problem, but they are not generated upfront. The lazy constraint callback checks whether the incumbent solution found by the solver contains subtours. If yes, then subtour elimination constraints are added that forbid these subtours. If not, then the incumbent solution forms a true solution of the TSP problem, as it contains only one tour.

Traveling Salesman Problem

Sets and indices:

\(S\), \(i,j \in S\)

Cities

Parameters:

\(C_{i,j} \in \mathbb{R_{+}}\)

Distance between cities

Variables:

\(X_{i,j} \in \{0..1\} \forall i,j: i>j\)

Travel from i to j, or visa versa

Constraints:

1

\(\forall j: \sum_i X_{i,j} + X_{i,j} = 1\)

Minimize:

\(\sum_{i,j} C_{i,j} * X_{i,j}\)

Total distance traveled

Language

Rest API

In this example we make use of an external Rest API to retrieve the relevant location data. We use the AIMMS Data Exchange Library (DEX) for sending out the requests and receiving and parsing the responses.

API Key

PositionStack is used on this example and you can sign up for a free api key. So the first step required when using this project is adding your key on sp_def_apiKey.

API Request

You can use the ‘forward geocoding’ functionality from the PositionStack API to retrieve geo location data. Their documentation describes which parameters are required (and optional), allowing you to build the request using the dex::client::NewRequest method of DEX. You will send the API key for authentication and as the query you will request the geo location based on a city name. To make the query more specific, you can also send the country in which the city is located, and maximize the result to 1. To prevent unwanted errors replace any spaces in city names with the URL-friendly character ‘%20’, as seen below:

 1sp_loc_requestId := "fetch" + sp_in_city;
 2
 3sp_in_city := FindReplaceStrings(sp_in_city, " ", "%20");
 4
 5sp_loc_urlCall
 6:=   "http://api.positionstack.com/v1/forward?access_key=" + sp_def_apiKey
 7      + "&query=" + sp_in_city
 8      + "&limit=1"
 9      + "&country=" + sp_countryAcronym(ep_in_cityCountry);
10
11dex::client::NewRequest(
12      sp_loc_requestId,
13      sp_loc_urlCall,
14      'dex::client::EmptyCallback',
15      responsefile:"out/Output.json",
16      tracefile:"Trace.xml");
17
18dex::client::PerformRequest(sp_loc_requestId);
19dex::client::WaitForResponses(2000);

The DEX method dex::client::PerformRequest will send out the actual request (as defined in dex::client::NewRequest by requestId) and the dex::client::WaitForResponses forces the callback to be called synchronously. The fourth argument of dex::client::NewRequest saves he responsefile in the folder ‘out’ and the fifth one saves a tracefile in case something goes wrong and you want to investigate.

Mapping the Results

After a successful request the geo location data will be in the file ‘Output.json’ in the out folder. It looks like this:

{
"data": {
   "results": [
      {
         "latitude": 38.897675,
         "longitude": -77.036547,
         "label": "1600 Pennsylvania Avenue NW, Washington, DC, USA",
         "name": "1600 Pennsylvania Avenue NW",
         "type": "address",
         "number": "1600",
         "street": "Pennsylvania Avenue NW",
         "postal_code": "20500",
         "confidence": 1,
         "region": "District of Columbia",
         "region_code": "DC",
         "administrative_area": null,
         "neighbourhood": "White House Grounds",
         "country": "United States",
         "country_code": "US",
         "map_url": "http://map.positionstack.com/38.897675,-77.036547"
      }
   ]
}}

Now you can use a mapping file to instruct AIMMS how to map the data from the output file onto the data model. First dex::AddMapping should be used to create/add the mapping to AIMMS:

1dex::AddMapping(
2     mappingName :  "LatLongMapping",
3     mappingFile :  "Mappings/Generated/LatLongDataset.xml");

The mappingfile (based on the JSON output) looks as follows:

<?xml version="1.0"?>
<AimmsJSONMapping>
     <ObjectMapping>
             <ArrayMapping name="data">
                     <ObjectMapping>
                             <ValueMapping name="name" binds-to="i_int_city" />
                             <ValueMapping name="latitude" maps-to="p_int_latitude(i_int_city)" />
                             <ValueMapping name="longitude" maps-to="p_int_longitude(i_int_city)" />
                     </ObjectMapping>
             </ArrayMapping>
     </ObjectMapping>
     </AimmsJSONMapping>

You can see that from the JSON array ‘data’ the ‘name’, ‘latitude’ and ‘longitude’ values are being mapped onto known/existing AIMMS identifiers within the model.

Now that the mapping is defined, the dex::ReadFromFile method can be used to actually read in the data of the file:

1dex::ReadFromFile(
2      dataFile         :  "out/Output.json",
3      mappingName      :  "LatLongMapping",
4      emptyIdentifiers :  1,
5      emptySets        :  1,
6      resetCounters    :  1);

Done that, the node will appear on the Network page!

Case Management

Data Manager is a native feature in any WebUI application. On this example, you will find 4 ready to use scenarios.

  • 100_BR: 100 nodes on one country: Brazil.

  • 200_BR: 200 nodes on one country: Brazil.

  • 100_ALL: 100 nodes in the world.

  • 200_ALL: 200 nodes in the world.

Note that you can create your own case, or adapt an existing case.

Haversine

Input data for this project is the exact latitude and longitude of cities in the world. So, direct distance between the nodes will not provide a good approximate distance. So here, is used the Haversine formula.

Haversine formula is an equation important in navigation, giving great-circle distances between two points on a sphere from their longitudes and latitudes [1].

The parameter that holds its value is:

Parameter p_def_haversineDistance {
   IndexDomain: (i_node1,i_node2) | i_node1 <> i_node2;
   Text: "Distance from city i to city j";
   Definition: {
      ((6371.0 )
      *   arccos(
                  cos(radians(90 - P_latitude(i_node1)))
                  * cos(radians(90 - P_latitude(i_node2)))
                  +
                  sin(radians(90 - P_latitude(i_node1)))
                  * sin(radians(90 - P_latitude(i_node2)))
                  * cos(radians(p_longitude(i_node1) - p_longitude(i_node2)))))
   }
}

See also

In this article there is another way to use Haversine by calling an external procedure on Visual Code.

ScheduleAt

../../_images/heuristic_menu.png

On Heuristic page, there are a few ways to run the different heuristics. You can find then on the Page Actions:

  • Clear Solutions: it will clear all heuristic solutions.

  • Initial Solutions: it will run the initial tour heuristic.

  • Improved Simultaneous: this will run the improved simultaneous tour with iterations.

  • Improved Cyclic: this will run the improved cyclic tour with iterations.

  • Run All: this will run all 3 heuristics without iterations. This run will be important when comparing execution time.

Both Improved Simultaneous and Improved Cyclic buttons will run iteratively. This means that every iteration of the heuristic will be shown on the map. It can take a while, so, if the nodes are orange, the heuristic is still running. Pink means that the run is complete.

This is possible by using ScheduleAt native AIMMS procedure. This is precise up to 1 second. Below, there is the procedure used to schedule each iteration.

Procedure pr_scheduleOver(p_in_noSecs, ep_in_payLoad)
 1sp_loc_refDate := "2023-01-01 00:00:00" ;
 2
 3p_loc_tmpSec := CurrentToMoment([s], sp_loc_refDate) ;
 4p_loc_tmpSec += p_in_noSecs ;
 5
 6if p_loc_scheduleAtUsesUTC then
 7   sp_loc_launchDate := MomentToString("%c%y-%m-%d %H:%M:%S%TZ('UTC')", [s], sp_loc_refDate, p_loc_tmpSec);
 8else
 9   sp_loc_launchDate := MomentToString("%c%y-%m-%d %H:%M:%S", [s], sp_loc_refDate, p_loc_tmpSec);
10endif ;
11
12! Nb ScheduleAt is precise up to a second.
13if not ScheduleAt(sp_loc_launchDate, ep_in_payLoad) then
14   raise error "Error scheduling procedure \'"
15               + ep_in_payLoad
16               + "\': "
17               + CurrentErrorMessage
18         code 'Schedule-at-procedure' ;
19endif;

See also

On this article you will find how to create an iterative graph using ScheduleAt.

Stopwatch Library

To compare the execution time for each solve, Stopwatch Library was used, for more documentation click here.

WebUI Features

This project you will find many ‘hidden’ and interesting features, for example, by right clicking on any node, you will be able to delete it specifically. The status bar here is used to let the user know when the iteration run is in progress. The “Help” side panels document some of those features.

The following WebUI features are used:

UI Styling

Below there are the css files you will find with comments on what they change.

1.annotation-node-done{
2   fill: var(--secondary);
3}
4.annotation-node-running{
5   fill: var(--secondary2);
6}
 1/*Add logo on the background*/
 2.scroll-wrapper--pagev2 .page-container {
 3   content: " ";
 4   background: url(img/RightBackground.png) rgb(249, 249, 249) no-repeat left/contain;
 5}
 6
 7/*Changing tittle to be uppercase*/
 8.title-addon {
 9   text-transform: uppercase;
10   text-shadow: 2px 2px 0px var(--primaryDark);
11   color: whitesmoke;
12}
13
14/*Changing button font*/
15.ui-widget, .ui-widget button, .ui-widget input, .ui-widget select, .ui-widget textarea {
16   font-family: var(--font_headers),Montserrat,Roboto,Arial,Helvetica,sans-serif;
17}
18
19/*Changing button size*/
20.aimms-widget[data-widget\.uri="btn_addThisCity"]  .ui-button,
21.aimms-widget[data-widget\.uri="btn_addByCountry"]  .ui-button,
22.aimms-widget[data-widget\.uri="btn_addByLimity"]  .ui-button  {
23   width: 40px;
24}
 1:root {
 2   --primaryLight: #00A0C8;
 3   --primary: #0082AA;
 4   --primaryDark: #0A5078;
 5   --secondaryDarker: #A00028;
 6   --secondary: #C80A50;
 7   --secondary2: #DC9600;
 8
 9   --bg_app-logo: 15px 50% / 50px 50px no-repeat url(/app-resources/resources/images/traveling.png);
10   --spacing_app-logo_width: 65px;
11
12   --color_border-divider_themed: var(--primary);
13   --color_text_edit-select-link: var(--primaryDark);
14   --color_text_edit-select-link_hover: var(--primaryLight);
15   --color_bg_edit-select-link_inverted: var(--secondary);
16
17   --color_bg_button_primary: var(--primaryLight);
18   --color_text_button_primary: white;
19   --border_button_primary: 1px solid var(--primaryLight);
20
21   --color_bg_button_primary_hover: var(--primaryLight);
22   --color_text_button_primary_hover: var(--primaryDark);
23   --border_button_primary_hover: 1px solid var(--primaryDark);
24
25   --color_text_button_secondary: var(--secondary);
26   --border_button_secondary: 1px solid var(--secondary);
27   --color_text_button_secondary_hover: var(--primaryDark);
28   --border_button_secondary_hover: 1px solid var(--primaryDark);
29
30   --color_bg_widget-header: var(--primaryDark);
31   --border_widget-header: 3px solid var(--primary);
32}
 1/*Changing label color*/
 2.tag-label>.label {
 3   background: var(--primary);
 4}
 5
 6.sidepanel-container .sidepanel-tab.active {
 7   background-color: var(--primaryDark);
 8}
 9
10.sidepanel-container .sidepanel-tab .sidepanel-icon,
11.sidepanel-container .sidepanel-tab:hover {
12   color: var(--primaryDark);
13}
14
15.sidepanel-container .sidepanel-tab.active{
16   color: var(--color_bg_widget-canvas,#fff);
17}
18
19.sidepanel-container .sidepanel-tab {
20   height: 150px;
21}
 1/*Link color*/
 2.ql-snow a {
 3   color: var(--primaryDark);
 4}
 5
 6/*Change checkbox color*/
 7input.boolean-cell-editor-contents {
 8   accent-color: var(--primaryDark);
 9}
10
11.aimms-widget .ui-button {
12   text-transform: uppercase;
13}
14
15/*Changing tittle to be uppercase*/
16.title-addon,
17.tag-label>.label,
18.ui-dialog .ui-dialog-title,
19.page-container__dialog-header .title,
20.sidepanel-header .side-panel__header-text{
21   text-transform: uppercase;
22   text-shadow: 1px 1px 0px var(--primaryDark);
23   color: whitesmoke;
24}
25
26.tag-table .grid-viewport .cell:not(.flag-readOnly), html:not(.using-touch) .tag-table .grid-viewport .cell:not(.flag-readOnly) {
27   color: var(--primaryDark);
28}
29
30.widget-menu-container .widget-menu-items-wrapper .widget-menu-item .title {
31   color: var(--primaryDark);
32}
1.widgetdiv .awf-dock-button .chrome-button.active, .widgetdiv .awf-dock-button .chrome-button.open {
2   background-color: var(--primaryDark);
3   color: #fff;
4}
5
6.widget-menu-container .widget-menu-items-wrapper .widget-menu-item:hover {
7   background: var(--primaryDark);
8}
 1/*Change color of the active step*/
 2.workflow-panel .step-item.current,
 3.workflow-panel.collapse .step-item.current {
 4   box-shadow: inset 0.3125rem 0 0 var(--primary);
 5}
 6
 7/*Change color of the titles*/
 8.workflow-panel .step-item.active.complete .title,
 9.workflow-panel .step-item.active.incomplete .title {
10   color: var(--primaryDark);
11}
12
13/*Change color of the icons*/
14.workflow-panel .step-item.active.complete .icon,
15.workflow-panel .step-item.active.incomplete .icon {
16   color: var(--primaryDark);
17   border: 1px solid var(--primaryDark);
18}

Minimal Requirements

AIMMS Community license is sufficient to run the Heuristics, call the Rest API and check the available scenarios. However, to run the MIP problem, you will need to buy a Developer License (see LP and MIP Solver Features).

A SQLite is used, to integrate that, you will need “SQLite3 ODBC Driver”. You will also need an API key from PositionStack api. To receive an free API key to test, please sign up to the free plan.

References

  1. Haversine Theory

  2. Generalization of TSP to Vehicle Routing Problem

  3. Solve with Lazy Constraints - Marcel Hunting.

  4. Applegate, D.L., R. E. Bixby, V. Chvátal, and W. J. Cook, The Traveling Salesman Problem: A Computational Study, Princeton University Press, Princeton, 2007

See also

Here you will find several euclidean TSP instances from TSPLIB at: http://comopt.ifi.uni-heidelberg.de/software/TSPLIB95/