diff --git a/launay-iorgulescu-ex3/.settings/org.eclipse.jdt.core.prefs b/launay-iorgulescu-ex3/.settings/org.eclipse.jdt.core.prefs index 54e493c..bb35fa0 100644 --- a/launay-iorgulescu-ex3/.settings/org.eclipse.jdt.core.prefs +++ b/launay-iorgulescu-ex3/.settings/org.eclipse.jdt.core.prefs @@ -1,11 +1,11 @@ eclipse.preferences.version=1 org.eclipse.jdt.core.compiler.codegen.inlineJsrBytecode=enabled -org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.6 +org.eclipse.jdt.core.compiler.codegen.targetPlatform=1.8 org.eclipse.jdt.core.compiler.codegen.unusedLocal=preserve -org.eclipse.jdt.core.compiler.compliance=1.6 +org.eclipse.jdt.core.compiler.compliance=1.8 org.eclipse.jdt.core.compiler.debug.lineNumber=generate org.eclipse.jdt.core.compiler.debug.localVariable=generate org.eclipse.jdt.core.compiler.debug.sourceFile=generate org.eclipse.jdt.core.compiler.problem.assertIdentifier=error org.eclipse.jdt.core.compiler.problem.enumIdentifier=error -org.eclipse.jdt.core.compiler.source=1.6 +org.eclipse.jdt.core.compiler.source=1.8 diff --git a/launay-iorgulescu-ex3/config/agents.xml b/launay-iorgulescu-ex3/config/agents.xml index 21f9a1e..05ab146 100644 --- a/launay-iorgulescu-ex3/config/agents.xml +++ b/launay-iorgulescu-ex3/config/agents.xml @@ -4,13 +4,20 @@ - + + + + + + + + - + diff --git a/launay-iorgulescu-ex3/src/ch/epfl/iagents/AStarPlanner.java b/launay-iorgulescu-ex3/src/ch/epfl/iagents/AStarPlanner.java new file mode 100644 index 0000000..07a784f --- /dev/null +++ b/launay-iorgulescu-ex3/src/ch/epfl/iagents/AStarPlanner.java @@ -0,0 +1,49 @@ +package ch.epfl.iagents; + +import java.util.*; +import java.util.stream.Collectors; + +public class AStarPlanner extends Planner { + @Override + protected State getFinalState(State initialState) { + final int deliverableTaskCount = initialState.getDeliverableTaskCount(); + // Set of "border" nodes. The use of TreeSet automatically sort them on insertion + TreeSet q = new TreeSet<>(new StateComparator()); + q.add(initialState); //start with origin + + /* + * Set of node "seen". Because our heuristic is not consistent, we'll have to compare the cost of an already-seen node + * if we reach it again, hence the use of HashMap + */ + HashMap c = new HashMap<>(); + + int numLoop = 0; //feedback info + State s; //avoid re-instantiation and used as final node when the loop is over + do { + numLoop++; + + s = q.pollFirst(); + if (s.delivered == deliverableTaskCount) break; + //if not has not been reached yet, or we reached it with a better cost + if (!c.containsKey(s) || c.get(s) > s.f()) { + c.put(s, s.f()); + //s.getSuccessors().collect(Collectors.toCollection(() -> q)); + q.addAll(s.getSuccessors()); + } + // System.out.println(q.size()); + } while (!q.isEmpty()); + System.out.println(c.size() + " estimated, " + q.size() + " border, " + numLoop + " loops, final cost: " + + s.costToReach +" using ASTAR."); + return s; + } + + // StateComparator required for TreeMap + private class StateComparator implements Comparator { + public int compare(State o1, State o2) { + int r = Double.compare(o1.f(), o2.f()); + if (r != 0) return r; + // Hackish: if the states are equidistant, break the tie. + return o1.equals(o2) ? 0 : 1; + } + } +} diff --git a/launay-iorgulescu-ex3/src/ch/epfl/iagents/Algorithm.java b/launay-iorgulescu-ex3/src/ch/epfl/iagents/Algorithm.java new file mode 100644 index 0000000..c570626 --- /dev/null +++ b/launay-iorgulescu-ex3/src/ch/epfl/iagents/Algorithm.java @@ -0,0 +1,6 @@ +package ch.epfl.iagents; + +public enum Algorithm { + BFS, + ASTAR +} diff --git a/launay-iorgulescu-ex3/src/ch/epfl/iagents/BFSPlanner.java b/launay-iorgulescu-ex3/src/ch/epfl/iagents/BFSPlanner.java new file mode 100644 index 0000000..50b41cb --- /dev/null +++ b/launay-iorgulescu-ex3/src/ch/epfl/iagents/BFSPlanner.java @@ -0,0 +1,29 @@ +package ch.epfl.iagents; + +import java.util.LinkedList; +import java.util.stream.Collectors; + +public class BFSPlanner extends Planner { + + @Override + protected State getFinalState(State initialState) { + final int deliverableTaskCount = initialState.getDeliverableTaskCount(); + LinkedList q = new LinkedList<>(); + q.add(initialState); + + State bestState = null; + Double bestResult = Double.POSITIVE_INFINITY; + while (!q.isEmpty()) { + State s = q.poll(); + if (s.delivered == deliverableTaskCount && s.costToReach < bestResult) { + bestResult = s.costToReach; + bestState = s; + } else { + //s.getSuccessors().collect(Collectors.toCollection(() -> q)); + q.addAll(s.getSuccessors()); + } + } + + return bestState; + } +} diff --git a/launay-iorgulescu-ex3/src/ch/epfl/iagents/DeliberativeAgent.java b/launay-iorgulescu-ex3/src/ch/epfl/iagents/DeliberativeAgent.java new file mode 100644 index 0000000..5e7ba79 --- /dev/null +++ b/launay-iorgulescu-ex3/src/ch/epfl/iagents/DeliberativeAgent.java @@ -0,0 +1,94 @@ +package ch.epfl.iagents; + +/* import table */ + +import logist.agent.Agent; +import logist.behavior.DeliberativeBehavior; +import logist.plan.Plan; +import logist.simulation.Vehicle; +import logist.task.Task; +import logist.task.TaskDistribution; +import logist.task.TaskSet; +import logist.topology.Topology; + +import java.util.Arrays; + +import static ch.epfl.iagents.State.HOLDING; +import static ch.epfl.iagents.State.NOT_PICKED; +import static ch.epfl.iagents.State.UNAVAILABLE; + +/** + * An optimal planner for one vehicle. + */ +@SuppressWarnings("unused") +public class DeliberativeAgent implements DeliberativeBehavior { + + /* + * Maximum (few seconds responses): + * BFS => 5 tasks + * ASTAR:NONE => 10 tasks + * ASTAR:MAXTRIP => 11 tasks + */ + + /* Environment */ + boolean environmentInitialized = false; + TaskSet carriedTasks; + Task[] tasks; + + /* the properties of the agent */ + int capacity; + + Planner planner; + Heuristic heuristic; + + @Override + public void setup(Topology topology, TaskDistribution td, Agent agent) { + // initialize the planner + this.capacity = agent.vehicles().get(0).capacity(); + String algorithmName = agent.readProperty("algorithm", String.class, "ASTAR"); + // Throws IllegalArgumentException if algorithm is unknown + planner = Planner.getPlanner(Algorithm.valueOf(algorithmName.toUpperCase())); + + String heuristicName = agent.readProperty("heuristic", String.class, "MAXTRIP"); + // Throws IllegalArgumentException if algorithm is unknown + heuristic = Heuristics.getHeuristic(HeuristicKind.valueOf(heuristicName.toUpperCase())); + } + + private int[] getTaskStatuses(TaskSet tasks) { + int[] taskStatuses = new int[this.tasks.length]; + Arrays.fill(taskStatuses, UNAVAILABLE); + tasks.forEach(t -> taskStatuses[t.id] = NOT_PICKED); + carriedTasks.forEach(t -> taskStatuses[t.id] = HOLDING); + return taskStatuses; + } + + private void initialize(TaskSet tasks) { + // First call to plan(). + // Setup carriedTasks to have the same universe as the tasks. + carriedTasks = TaskSet.noneOf(tasks); + // Setup tasks like taskSet universe. + // Since some tasks may be done, allocate space for the largest task id found. + this.tasks = new Task[tasks.stream().map(t -> t.id).mapToInt(Integer::intValue).max().orElse(-1) + 1]; + tasks.forEach(t -> this.tasks[t.id] = t); + environmentInitialized = true; + } + + @Override + public Plan plan(Vehicle vehicle, TaskSet tasks) { + if (!environmentInitialized) initialize(tasks); + if (tasks.isEmpty() && carriedTasks.isEmpty()) return Plan.EMPTY; + return planner.getPlan(new State(vehicle.getCurrentCity(), this.tasks, getTaskStatuses(tasks), + tasks.size() + carriedTasks.size(), null, 0.0, + capacity - carriedTasks.weightSum(), 0, heuristic)); + } + + @Override + public void planCancelled(TaskSet carriedTasks) { + /* + * Register the carriedTasks. + * They will be taken into account when plan() is called + */ + this.carriedTasks = carriedTasks; + } + +} diff --git a/launay-iorgulescu-ex3/src/ch/epfl/iagents/Heuristic.java b/launay-iorgulescu-ex3/src/ch/epfl/iagents/Heuristic.java new file mode 100644 index 0000000..90cc326 --- /dev/null +++ b/launay-iorgulescu-ex3/src/ch/epfl/iagents/Heuristic.java @@ -0,0 +1,5 @@ +package ch.epfl.iagents; + +public interface Heuristic { + double getEstimation(State currentState); +} diff --git a/launay-iorgulescu-ex3/src/ch/epfl/iagents/HeuristicKind.java b/launay-iorgulescu-ex3/src/ch/epfl/iagents/HeuristicKind.java new file mode 100644 index 0000000..328d49e --- /dev/null +++ b/launay-iorgulescu-ex3/src/ch/epfl/iagents/HeuristicKind.java @@ -0,0 +1,6 @@ +package ch.epfl.iagents; + +public enum HeuristicKind { + NONE, + MAXTRIP +} diff --git a/launay-iorgulescu-ex3/src/ch/epfl/iagents/Heuristics.java b/launay-iorgulescu-ex3/src/ch/epfl/iagents/Heuristics.java new file mode 100644 index 0000000..dcb27d5 --- /dev/null +++ b/launay-iorgulescu-ex3/src/ch/epfl/iagents/Heuristics.java @@ -0,0 +1,51 @@ +package ch.epfl.iagents; + +import logist.task.Task; + +import java.util.stream.Stream; + +class Heuristics { + + static Heuristic getHeuristic(HeuristicKind heuristic) throws IllegalArgumentException { + switch(heuristic) { + case MAXTRIP: + return new MaxTripHeuristic(); + default: + throw new IllegalArgumentException("Undefined heuristic type: " + heuristic); + } + } + + private static class MaxTripHeuristic implements Heuristic { + @Override + public double getEstimation(State currentState) { + /* + double pickupMax = Stream.of(currentState.tasks) + .filter(t -> currentState.taskStatuses[t.id] == State.NOT_PICKED) + .map(t -> currentState.inCity.distanceTo(t.pickupCity) + t.pickupCity.distanceTo(t.deliveryCity)) + .mapToDouble(Double::doubleValue) + .max().orElse(0); + + double deliverMax = Stream.of(currentState.tasks) + .filter(t -> currentState.taskStatuses[t.id] == State.HOLDING) + .map(t -> currentState.inCity.distanceTo(t.deliveryCity)) + .mapToDouble(Double::doubleValue) + .max().orElse(0); + + return pickupMax > deliverMax ? pickupMax : deliverMax; + */ + double result = 0; + for (Task t : currentState.tasks) { + if (t == null) continue; + double value = 0; + if (currentState.taskStatuses[t.id] == State.NOT_PICKED) { + value = currentState.inCity.distanceTo(t.pickupCity) + t.pickupCity.distanceTo(t.deliveryCity); + } else if (currentState.taskStatuses[t.id] == State.HOLDING) { + value = currentState.inCity.distanceTo(t.deliveryCity); + } + if (value > result) result = value; + } + + return result; + } + } +} diff --git a/launay-iorgulescu-ex3/src/ch/epfl/iagents/Planner.java b/launay-iorgulescu-ex3/src/ch/epfl/iagents/Planner.java new file mode 100644 index 0000000..e933c40 --- /dev/null +++ b/launay-iorgulescu-ex3/src/ch/epfl/iagents/Planner.java @@ -0,0 +1,62 @@ +package ch.epfl.iagents; + +import logist.plan.Action; +import logist.plan.Plan; +import logist.task.Task; +import logist.topology.Topology; + +import java.util.ArrayList; +import java.util.Collections; +import java.util.stream.Collectors; + +public abstract class Planner { + abstract protected State getFinalState(State initialState); + + static Planner getPlanner(Algorithm algorithm) + throws IllegalArgumentException { + switch (algorithm) { + case BFS: + return new BFSPlanner(); + case ASTAR: + return new AStarPlanner(); + default: + throw new IllegalArgumentException("Undefined algorithm type: " + algorithm); + } + } + + Plan getPlan(State initialState) { + assert(initialState != null); + + long startTime = System.currentTimeMillis(); + State state = getFinalState(initialState); + if (state != null) { + System.out.println(state.tasks.length + " tasks, result: " + state.costToReach + " in " + + (System.currentTimeMillis() - startTime) + " ms"); + } + + ArrayList plan = new ArrayList<>(); + while (state.previousState != null) { + Task transitionTask = state.transitionTask; + + // Check if the task was delivered or picked up. + if (state.taskStatuses[transitionTask.id] == State.DELIVERED) + plan.add(new Action.Delivery(transitionTask)); + else plan.add(new Action.Pickup(transitionTask)); + + // Check if we moved to a different location. + // We construct the path from destination to source, since we need to reverse it at the end. + if (state.inCity != state.previousState.inCity) { + final Topology.City sourceCity = state.previousState.inCity; + plan.add(new Action.Move(state.inCity)); + plan.addAll(state.inCity.pathTo(state.previousState.inCity) + .stream().filter(c -> c != sourceCity) + .map(Action.Move::new).collect(Collectors.toList())); + } + + state = state.previousState; + } + + Collections.reverse(plan); + return new Plan(state.inCity, plan); + } +} diff --git a/launay-iorgulescu-ex3/src/ch/epfl/iagents/State.java b/launay-iorgulescu-ex3/src/ch/epfl/iagents/State.java new file mode 100644 index 0000000..0a12cd4 --- /dev/null +++ b/launay-iorgulescu-ex3/src/ch/epfl/iagents/State.java @@ -0,0 +1,135 @@ +package ch.epfl.iagents; + +import logist.task.Task; +import logist.topology.Topology; + +import java.util.ArrayList; +import java.util.Arrays; +import java.util.List; +import java.util.stream.Stream; + +public class State { + + //The variables that actually make the state: inCity and taskStatuses + final Topology.City inCity; + final Task[] tasks; + final int[] taskStatuses; + + //define constant for readability + static final int NOT_PICKED = 0, HOLDING = 1, DELIVERED = 2, UNAVAILABLE = 3; + + /* These two variables are derived from taskStatuses and kept to avoid recomputing them + * weightCarried is the sum of the weights of the tasks currently held + * delivered is the number of tasks delivered + * A state is final if delivered = #tasks + */ + private final int remainingCapacity; + private final int deliverableTaskCount; + int delivered = 0; + + /* + * Variables used during the research algorithm. + * previousState allows to reconstruct the path after completion + * costToReach = previousState.costToReach + previousState.inCity.distanceTo(this.inCity) + * heurist is the heuristic for this state, which computation is based on taskStatuses + */ + final State previousState; + /* + * The task that was picked up or delivered from the previous state to arrive at this state. + * For the root state it will be null. + */ + Task transitionTask; + + double costToReach; + private final double estimatedCost; + + private final Heuristic heuristic; + + State(Topology.City cCity, Task[] tasks, int[] tStatus, int deliverableTaskCount, State previousState, + double cost, int remainingCapacity, int delivered, Heuristic heuristic) { + this.inCity = cCity; + this.tasks = tasks; + this.taskStatuses = tStatus; + this.deliverableTaskCount = deliverableTaskCount; + this.previousState = previousState; + this.costToReach = cost; + this.remainingCapacity = remainingCapacity; + this.delivered = delivered; + this.heuristic = heuristic; + + this.estimatedCost = heuristic.getEstimation(this); + } + + private State(Topology.City cCity, Task[] tasks, int[] tStatus, int deliverableTaskCount, State previousState, + double cost, int remainingCapacity, int delivered, Heuristic heuristic, Task transitionTask) { + this(cCity, tasks, tStatus, deliverableTaskCount, previousState, cost, remainingCapacity, delivered, heuristic); + this.transitionTask = transitionTask; + + taskStatuses[transitionTask.id] ++; // NOT_PICKED -> HOLDING ; HOLDING -> DELIVERED. + if (taskStatuses[transitionTask.id] == DELIVERED) this.delivered++; + } + + private State applyTask(Task t) { + // Task is not yet delivered. + // NOT_PICKED -> we will pick up the task. + // HOLDING -> we will deliver the task. + Topology.City destination = taskStatuses[t.id] == HOLDING ? t.deliveryCity : t.pickupCity; + int newRemainingCapacity = remainingCapacity + (taskStatuses[t.id] == HOLDING ? t.weight : -t.weight); + return new State( + destination, + tasks, + Arrays.copyOf(taskStatuses, taskStatuses.length), + deliverableTaskCount, + this, + costToReach + inCity.distanceTo(destination), + newRemainingCapacity, + delivered, + heuristic, + t); + } + + /* + * Return a list containing all the possible successors of this state + * it is obtained by going through the task list and trying to make progress for each one if possible. + */ + List getSuccessors() { + ArrayList states = new ArrayList<>(); + for (Task t : tasks) { + if (t == null) continue; + if (taskStatuses[t.id] == HOLDING || + (taskStatuses[t.id] == NOT_PICKED && t.weight <= remainingCapacity)) + states.add(applyTask(t)); + } + return states; +// return tasks.stream() +// .filter(t -> taskStatuses[t.id] == HOLDING || +// (taskStatuses[t.id] == NOT_PICKED && t.weight <= remainingCapacity)) +// .map(this::applyTask); + } + + // The distance function: f=g+h=costToReach+heurist + double f() { + return this.costToReach + this.estimatedCost; + } + + int getDeliverableTaskCount() { + return deliverableTaskCount; + } + + //Override hashCode() and equals to be able to use State in HashMap and TreeSet + @Override + public int hashCode() { + return inCity.id + Arrays.hashCode(taskStatuses); + } + + @Override + public boolean equals(Object obj) { + if (obj == null) return false; + if (!(obj instanceof State)) return false; + if (this == obj) + return true; + State other = (State) obj; + return inCity == other.inCity && Arrays.equals(taskStatuses, other.taskStatuses); + } + +}