/** * Licensed to the Apache Software Foundation (ASF) under one * or more contributor license agreements. See the NOTICE file * distributed with this work for additional information regarding * copyright ownership. The ASF licenses this file to you 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. */ package siebog.agents.xjaf.aco.tsp; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Random; import javax.ejb.Remote; import javax.ejb.Stateful; import siebog.agents.AID; import siebog.agents.Agent; import siebog.agents.AgentClass; import siebog.agents.AgentInitArgs; import siebog.agents.XjafAgent; import siebog.interaction.ACLMessage; import siebog.interaction.Performative; import siebog.utils.LoggerUtil; /** * Implementation of an ant. * * @author <a href="mailto:tntvteod@neobee.net">Teodor Najdan Trifunov</a> * @author <a href="mailto:milan.laketic@yahoo.com">Milan Laketic</a> */ @Stateful @Remote(Agent.class) public class Ant extends XjafAgent { private static final long serialVersionUID = 8886978416763257091L; // AID of the agent maintaining the world graph. private AID mapAID; // Number of nodes on the map (provided by map agent). private int mapSize; // Index of the current (x,y) pair in the map nodes list. private int currentMapPosIndex; // Represents all nodes this ant has visited so far (in order of visit). private List<Integer> tourSoFar; // I-th element represents the weight between i-th and (i+1)-th node in the tourSoFar list. private List<Float> tourSoFarWeights; // Represents total weight of tourSoFar. private float totalWeightSoFar = 0f; // Local pheromone influence control parameter. private final float alpha = 1f; // Edge weight influence control parameter. private final float beta = 5f; // Pheromone evaporation rate (0 <= ro < 1). private final float ro = 0.1f; // Pheromone local evaporation rate (0 <= ksi < 1). private final float ksi = 0.1f; // @formatter:off /** * 1 - initial tour creation by probabilistic movement (send request) * 2 - while waiting for a reply to the 'PheromoneLevels?' message (process paths) * 3 - while waiting to obtain the weight of the last edge in the tour. * 4 - preparing for backtrack: remove last node on the tourSoFar list (it is the same as the first) * and update the best tour (in the Map agent) if necessary. * 5 - backtrack and pheromone adjustment */ // @formatter:on private int phase; // Pheromone local evaporation rate (0 <= ksi < 1). private float delta; // Space-delimited potential node indices. private String potentialNodeIndices; private final Random rnd = new Random(); @Override protected void onInit(AgentInitArgs args) { mapAID = agm().getAIDByRuntimeName("Map"); ACLMessage message = new ACLMessage(); message.performative = Performative.REQUEST; message.content = "MapSize?"; message.sender = myAid; message.receivers.add(mapAID); message.replyWith = "MapSize"; msm().post(message); } @Override protected void onMessage(ACLMessage message) { if ("MapSize".equals(message.inReplyTo)) { if (message.content.equals("DONE")) { return; } mapSize = Integer.parseInt(message.content); // choose starting map node randomly currentMapPosIndex = new Random().nextInt(mapSize); tourSoFar = new ArrayList<>(); tourSoFar.add(currentMapPosIndex); tourSoFarWeights = new ArrayList<>(); phase = 1; ACLMessage start = new ACLMessage(Performative.REQUEST); start.receivers.add(myAid); msm().post(start); return; } switch (phase) { case 1: { ACLMessage request = new ACLMessage(Performative.REQUEST); potentialNodeIndices = getPotentialNodeIndices().trim(); currentMapPosIndex = getCurrentMapPosIndex(); request.content = "PheromoneLevels? " + currentMapPosIndex + " " + potentialNodeIndices; request.receivers.add(mapAID); request.sender = myAid; msm().post(request); phase = 2; break; } case 2: { int newNodeIndex = 0; // response contains a header and alternating numbers designating pheromone level and // edge weight for every applicable edge String[] parts = message.content.split(" "); // set pheromone and weight hashmaps java.util.Map<Integer, Float> pheromones = new HashMap<>(); java.util.Map<Integer, Float> weights = new HashMap<>(); float total = 0f; for (int i = 1; i < parts.length; i += 2) { float weight = Float.parseFloat(parts[i + 1]); float pheromoneLevel = Float.parseFloat(parts[i]); float val = (float) (Math.pow(pheromoneLevel, alpha) * Math.pow(1 / weight, beta)); total += val; pheromones.put((i + 1) / 2, val); weights.put((i + 1) / 2, weight); } // set probability distribution java.util.Map<Integer, Float> probabilities = new HashMap<>(); for (int i = 1; i <= pheromones.size(); ++i) probabilities.put(i, pheromones.get(i) / total); // choose next pheromone index using probability distribution double random = rnd.nextDouble(); int i = 1; float val = probabilities.get(i); while (i < probabilities.size()) { if (random < val) break; else val += probabilities.get(++i); } // update next node index (using chosen pheromone index i, and potentialNodeIndices // string) String[] segs = potentialNodeIndices.split(" "); newNodeIndex = Integer.parseInt(segs[i - 1]); setCurrentMapPosIndex(newNodeIndex); // add new node to tourSoFar addNodeToTour(newNodeIndex); // add new weight to tourSoFarWeights addWeightToTour(weights.get(i)); // initiate local pheromone update ACLMessage localUpdate = new ACLMessage(Performative.INFORM); localUpdate.receivers.add(mapAID); localUpdate.content = "UpdateLocalPheromone " + currentMapPosIndex + " " + newNodeIndex + " " + ksi; // advance the phase as required (if tour complete, continue with phase 3, otherwise, // repeat phase 1) if (getTourSoFarSize() == getMapSize()) { int firstMapPosIndex = getFirstMapPosIndex(); addNodeToTour(firstMapPosIndex); ACLMessage edgeWeightReq = new ACLMessage(Performative.REQUEST); edgeWeightReq.receivers.add(mapAID); edgeWeightReq.content = "EdgeWeight? " + currentMapPosIndex + " " + firstMapPosIndex; edgeWeightReq.sender = myAid; msm().post(edgeWeightReq); currentMapPosIndex = firstMapPosIndex; phase = 3; } else { phase = 1; msm().post(message); } break; } case 3: { addWeightToTour(Float.parseFloat(message.content)); phase = 4; msm().post(message); break; } case 4: { ACLMessage updateBest = new ACLMessage(Performative.INFORM); updateBest.receivers.add(mapAID); StringBuilder tourSoFar = new StringBuilder(); for (int i = 0; i < getTourSoFarSize(); ++i) tourSoFar.append(" ").append(getTourNode(i)); float tourWeight = getTotalWeightSoFar(); updateBest.content = "UpdateBestTour " + tourWeight + tourSoFar.toString(); updateBest.sender = myAid; msm().post(updateBest); delta = 1 / tourWeight; removeLastNode(); // which is the same as the first phase = 5; msm().post(message); break; } default: // phase == 5 { int nextNodeIndex = removeLastNode(); if (nextNodeIndex == -1) { phase = 6; // when this ant is done, create another one String name = "Ant" + myAid.hashCode() + System.currentTimeMillis(); AgentClass agClass = new AgentClass(Agent.SIEBOG_MODULE, "Ant"); agm().startServerAgent(agClass, name, null); agm().stopAgent(myAid); return; } currentMapPosIndex = getCurrentMapPosIndex(); ACLMessage updatePheromone = new ACLMessage(Performative.INFORM); updatePheromone.receivers.add(mapAID); // float val = (1 - ro) * oldValue + ro * delta; // final formula is constructed in Map // agent (for simplicity of oldValue retrieval) updatePheromone.content = "UpdatePheromone " + currentMapPosIndex + " " + nextNodeIndex + " " + (1 - ro) + " " + ro * delta; updatePheromone.sender = myAid; msm().post(updatePheromone); setCurrentMapPosIndex(nextNodeIndex); msm().post(message); } } } public int getTourSoFarSize() { return tourSoFar.size(); } public int getMapSize() { return mapSize; } public void addNodeToTour(int nodeIndex) { tourSoFar.add(nodeIndex); } public void addWeightToTour(float weight) { tourSoFarWeights.add(weight); totalWeightSoFar += weight; } /** * @return space-delimited indices of unvisited nodes. */ public String getPotentialNodeIndices() { StringBuilder result = new StringBuilder(); for (int i = 0; i < mapSize; ++i) if (!tourSoFar.contains(i)) result.append(" ").append(i); return result.toString(); } /** * @return index of the map node (in map's 'nodes' list) this ant is currently at. */ public int getCurrentMapPosIndex() { return currentMapPosIndex; } /** * Set value as the current map position index. */ public void setCurrentMapPosIndex(int value) { currentMapPosIndex = value; } /** * @return index of the first node that was visited on the current tour. */ public int getFirstMapPosIndex() { return tourSoFar.get(0); } public float getTotalWeightSoFar() { return totalWeightSoFar; } /** * @return Last node of the tourSoFar list, which is subsequently removed, or -1, if the list is * already empty. */ public int removeLastNode() { if (tourSoFar.size() != 0) return tourSoFar.remove(tourSoFar.size() - 1); else return -1; } /** * @return Last edge weight of the tourSoFarWeights list, which is subsequently removed, or -1, * if the list is already empty; */ public float removeLastWeight() { if (tourSoFarWeights.size() != 0) return tourSoFarWeights.remove(tourSoFarWeights.size() - 1); else return -1f; } /** * @return tourSoFar element with index 'index'. */ public int getTourNode(int index) { return tourSoFar.get(index); } /** * Agent clean-up. */ @Override public void onTerminate() { LoggerUtil.log("Ant terminated.", true); } }