--- /dev/null
+# This file is part of NIT ( http://www.nitlanguage.org ).
+#
+# 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.
+
+# A simple planning problem solver using A* for a robot that delivers parcels.
+#
+# A map is made of `Location`s (with cartesian coordinates) connected with `Road`s.
+# Some parcels and a robot are placed on the map.
+#
+# The goal of the robot is to plan the delivery of the parcels to their destination in the most
+# efficient way knowing that:
+#
+# * the robot has a given speed to drive
+# * the loading and unloading of parcels take time
+# * the capacity of the robot is limited
+module simplan
+
+import ai::search
+
+private import simplan_lexer
+private import simplan_parser
+
+# The description of the planing problem to solve
+class PlanProblem
+ super SearchProblem[State, Action]
+
+ # Some parameters of the problem
+
+ # The speed on the road.
+ # Used for the action `Drive`
+ var speed = 1.0
+
+ # Cost of loading a parcel in a robot
+ # Used for the action `Load`
+ var duration_loading = 30.0
+
+ # Cost of unloading a parcel in a robot
+ # Used for the action `Unload`
+ var duration_unloading = 30.0
+
+ # How many parcels can a robot transport at the same time?
+ var capacity = 2
+
+ # The name of the robot
+ var robot_name = "r0"
+
+ # Locations on the map, by their names
+ var locations = new HashMap[String, Location]
+
+ # All the parcels, by their `id`
+ var parcels = new Array[Parcel]
+
+ # All the parcels, by their names
+ var parcel_by_name = new HashMap[String, Parcel]
+
+ # +infinite, used to initialize some floats with the maximum value.
+ private var infinite: Float = 1.0 / 0.0
+
+ # The starting state
+ redef var initial_state: State is noinit
+
+ # Parse and initialize a problem from description `text`
+ fun parse(text: String)
+ do
+ var l = new Lexer_simplan(text)
+ var p = new Parser_simplan
+ p.tokens.add_all l.lex
+ var n = p.parse
+ if n isa NError then
+ print n.message
+ exit 1
+ end
+ n = n.children.first.children.first.as(not null)
+ if n isa Nplan then
+ print "Error: expected a problem, got a plan."
+ exit 1
+ end
+ assert n isa Nproblem
+
+ # Load all locations
+ for n2 in n.n_locations.n_list.children do
+ var e = new Location(locations.length, n2.n_name.text, n2.n_x.text.to_f, n2.n_y.text.to_f)
+ assert not locations.has_key(e.name)
+ locations[e.name] = e
+ end
+ print "# {locations.length} locations"
+
+ # Load all roads
+ var nbr = 0
+ for n2 in n.n_roads.n_list.children do
+ var o = locations.get_or_null(n2.n_orig.text)
+ var d = locations.get_or_null(n2.n_dest.text)
+ assert o != null and d != null
+ var r = new Road(o,d)
+ o.roads.add(r)
+ r = new Road(d,o)
+ d.roads.add(r)
+ nbr += 2
+ end
+ print "# {nbr} roads"
+
+ # Compute all road durations
+ for e in locations.values do
+ e.durations = new Array[Float].filled_with(infinite, locations.length)
+ e.durations[e.id] = 0.0
+ for r in e.roads do
+ e.durations[r.dest.id] = r.distance / speed
+ end
+ end
+
+ # Close the duration relation between each pair of locations
+ for k in locations.values do
+ for i in locations.values do
+ for j in locations.values do
+ var d = i.durations[k.id] + k.durations[j.id]
+ if i.durations[j.id] > d then
+ i.durations[j.id] = d
+ end
+ end
+ end
+ end
+
+ # Load the robot
+ var robot = null
+ for n2 in n.n_robots.n_list.children do
+ var name = n2.n_name.text
+ robot = locations.get_or_null(n2.n_emplacement.text)
+ assert name == robot_name and robot != null
+ end
+ assert robot != null
+ print "# 1 robot"
+
+ # Load the parcels
+ var parcel_locations = new Array[nullable Location]
+ for n2 in n.n_parcels.n_list.children do
+ var name = n2.n_name.text
+ var e = locations.get_or_null(n2.n_emplacement.text)
+ assert e != null
+ var parcel = new Parcel(parcels.length, name, e, e)
+ parcels.add parcel
+ parcel_locations.add e
+ parcel_by_name[name] = parcel
+
+ end
+ print "# {parcels.length} parcels"
+
+ # Load the goal of parcels
+ for n2 in n.n_goal.n_list.children do
+ var parcel = parcel_by_name.get_or_null(n2.n_name.text)
+ var e = locations.get_or_null(n2.n_emplacement.text)
+ assert parcel != null and e != null
+ parcel.goal = e
+ end
+ print "# 1 goal"
+
+ print "# size of the problem: {locations.length.to_f.pow(parcels.length.to_f+1.0).to_precision(0)}"
+
+ initial_state = new State(self, robot, parcel_locations, 0)
+ end
+
+ # Parse a plan for a given problem
+ fun parse_plan(text: String): Plan
+ do
+ var l = new Lexer_simplan(text)
+ var p = new Parser_simplan
+ p.tokens.add_all l.lex
+ var n = p.parse
+ if n isa NError then
+ print n.message
+ exit 1
+ end
+ n = n.children.first.children.first.as(not null)
+ if n isa Nproblem then
+ print "Error: expected a plan, got a problem."
+ exit 1
+ end
+ assert n isa Nplan
+
+ var res = new Plan(self)
+ var e = initial_state
+ var cost = 0.0
+ for n2 in n.n_actions.children do
+ if n2 isa Naction_load then
+ var parcel = parcel_by_name.get_or_null(n2.n_parcel.text)
+ assert parcel != null
+ var a = new Load(self, parcel)
+ res.actions.add(a)
+ e = a.trans(e)
+ cost += a.cost
+ else if n2 isa Naction_unload then
+ var parcel = parcel_by_name.get_or_null(n2.n_parcel.text)
+ assert parcel != null
+ var a = new Unload(self, parcel)
+ res.actions.add(a)
+ e = a.trans(e)
+ cost += a.cost
+ else if n2 isa Naction_drive then
+ var o = locations.get_or_null(n2.n_orig.text)
+ var d = locations.get_or_null(n2.n_dest.text)
+ assert o != null and d != null
+ var road: nullable Road = null
+ for r in o.roads do if r.dest == d then
+ road = r
+ break
+ end
+ assert road != null
+ var a = new Drive(self, road)
+ res.actions.add(a)
+ e = a.trans(e)
+ cost += a.cost
+ else abort
+ end
+ print "# loaded plan in {res.actions.length} steps; cost = {cost}"
+ return res
+ end
+
+ redef fun actions(state)
+ do
+ var res = new Array[Action]
+
+ # Driving?
+ for road in state.robot.roads do
+ res.add(new Drive(self, road))
+ end
+
+ # Loading?
+ for i in [0..state.parcels.length[ do
+ if state.parcels[i] == state.robot and state.nb_parcels < self.capacity and self.parcels[i].goal != state.robot then
+ res.add(new Load(self, parcels[i]))
+ end
+ end
+
+ # Unloading?
+ for i in [0..state.parcels.length[ do
+ # Unloading is always a valid action,
+ # even if dropping a parcel outside of its goal is unlikely (but possible)
+ # to be a part of an optimal plan.
+ if state.parcels[i] == null then #and self.parcels[i].goal == state.robot then
+ res.add(new Unload(self, parcels[i]))
+ end
+ end
+
+ return res
+ end
+
+ redef fun apply_action(state, action) do return action.trans(state)
+
+ redef fun cost(state1, action, state2) do return action.cost
+
+ redef fun is_goal(state)
+ do
+ for i in [0..parcels.length[ do
+ if state.parcels[i] != parcels[i].goal then return false
+ end
+ return true
+ end
+
+ redef fun heuristic(state)
+ do
+ # Combination (maximum) of two heuristic:
+ # Heuristic 1: maximum driving time to take and deliver a single parcel
+ var max_for_one = 0.0
+ # Heuristic 2: driving time to take the nearest parcel, then deliver all parcels
+ var total_deliver_drive = 0.0
+ var min_take_one = infinite
+ # Total loading/unloading time (incompressible cost, added to both heuristics)
+ var total_load = 0.0
+
+ for i in [0..parcels.length[ do
+ # The parcel location
+ var c = state.parcels[i]
+
+ # Its goal location
+ var b = parcels[i].goal
+
+ # Parcel is fine, nothing to do.
+ if c == b then continue
+
+ # Driving time to take and deliver this parcel
+ var t = 0.0
+
+ # Current position of the parcel (somewhere or in the robot)
+ var current: Location
+
+ if c == null then
+ # The parcel is in the robot
+ current = state.robot
+ min_take_one = 0.0
+ else
+ # The parcel is somewhere
+ current = c
+ # So go get it.
+ var tt = state.robot.duration(c)
+ total_load += duration_loading
+
+ t += tt
+ if tt < min_take_one then min_take_one = tt
+ end
+
+ # Drive and unload the parcel
+ var td = current.duration(b)
+ total_load += duration_unloading
+
+ t += td
+ if t > max_for_one then max_for_one = t
+ total_deliver_drive += td
+ end
+
+ # Since one robot can transport several parcels at the same time,
+ # the best optimistic scenario is to deliver at full capacity
+ if min_take_one == infinite then min_take_one = 0.0
+ var res = min_take_one + total_deliver_drive / capacity.to_f
+
+ # Get best of both heuristics
+ if res < max_for_one then res = max_for_one
+
+ # Add incompressible (un)loading costs
+ res += total_load
+
+ return res
+ end
+
+ redef fun make_memory
+ do
+ var res = super
+ res.add new RBTreeMap[State, SearchNode[State, Action]]
+ res.add new BinTreeMap[State, SearchNode[State, Action]]
+ return res
+ end
+end
+
+# A node on the map
+class Location
+ # Indexed identifier, starting at 0
+ # Used to access locations in arrays.
+ var id: Int
+
+ # The name of the location, from the problem description
+ var name: String
+
+ # The x-coordinate of the location
+ var x: Float
+
+ # The y-coordinate of the location
+ var y: Float
+
+ # All roads from this location
+ var roads = new Array[Road]
+
+ # Drive duration to any other location on the map
+ fun duration(dest: Location): Float
+ do
+ return durations[dest.id]
+ end
+
+ private var durations = new Array[Float]
+
+ redef fun to_s do return "{id}({x}, {y})"
+end
+
+# A directed road segment between two locations
+class Road
+ # The origin
+ var orig: Location
+
+ # The destination
+ var dest: Location
+
+ # The distance according to the coordinates of the locations.
+ var distance: Float is noautoinit
+
+ init
+ do
+ var dx = orig.x-dest.x
+ var dy = orig.y-dest.y
+ distance = (dx*dx + dy*dy).sqrt
+ end
+end
+
+# A parcel to move
+class Parcel
+ # Indexed identifier, starting by 0
+ # Used to access parcels in arrays.
+ var id: Int
+ # The name of the parcel according to the problem description
+ # Used for output
+ var name: String
+ # The starting location
+ var start: Location
+ # The goal location
+ var goal: Location
+end
+
+
+# A plan on a problem
+class Plan
+ # The original problem
+ var problem: PlanProblem
+
+ # The sequence of actions in the plan
+ var actions = new Array[Action]
+
+ # Check and write the plan
+ fun dump(verbose: Bool)
+ do
+ var e = problem.initial_state
+ print "SimplePlan \{"
+ var cost = 0.0
+ if verbose then print "#{e} {cost}"
+ for a in actions do
+ print "{a}"
+ e = a.trans(e)
+ cost += a.cost
+ if verbose then
+ print "#{e} {cost}"
+ if a isa Drive then
+ print "#{a.road.orig} -- {a.road.dest}: {a.road.distance}"
+ end
+ end
+
+ end
+ print "\}"
+ if not problem.is_goal(e) then
+ print "# /!\\ Goal Failed"
+ end
+ print "# plan in {actions.length} steps; cost={cost}\n"
+ end
+end
+
+# A primitive movement
+abstract class Action
+ # The unitary cost of the action
+ fun cost: Float is abstract
+
+ # The state resulting to the application of the action
+ fun trans(e: State): State is abstract
+
+ # The original problem
+ var problem: PlanProblem
+end
+
+# A loading of a parcel
+class Load
+ super Action
+
+ # The loaded parcel
+ var parcel: Parcel
+
+ redef fun cost do return problem.duration_loading
+
+ redef fun to_s do return "Load({problem.robot_name},{parcel.name});"
+
+ redef fun trans(e)
+ do
+ var res = e.clone
+ assert res.parcels[parcel.id] == res.robot
+ res.parcels = res.parcels.clone
+ res.parcels[parcel.id] = null
+ assert res.nb_parcels < problem.capacity
+ res.nb_parcels += 1
+ return res
+ end
+end
+
+# A unloading of a parcel
+class Unload
+ super Action
+
+ # The unloaded parcel
+ var parcel: Parcel
+
+ redef fun cost do return problem.duration_unloading
+
+ redef fun to_s do return "Unload({problem.robot_name},{parcel.name});"
+
+ redef fun trans(e)
+ do
+ var res = e.clone
+ assert res.parcels[parcel.id] == null
+ res.parcels = res.parcels.clone
+ res.parcels[parcel.id] = res.robot
+ assert res.nb_parcels > 0
+ res.nb_parcels -= 1
+ return res
+ end
+end
+
+# A road driving
+class Drive
+ super Action
+
+ # The road drove on
+ var road: Road
+
+ redef fun cost do return road.distance / problem.speed
+
+ redef fun to_s do return "Drive({problem.robot_name},{road.orig.name},{road.dest.name});"
+
+ redef fun trans(e)
+ do
+ var res = e.clone
+ assert res.robot == road.orig
+ res.robot = road.dest
+ return res
+ end
+end
+
+# Where each robot and parcel are?
+class State
+ super Comparable
+ redef type OTHER: State
+
+ # The original problem
+ var problem: PlanProblem
+
+ # Where is the robot
+ var robot: Location
+
+ # Where each parcel is
+ # `null` if the parcel is loaded
+ var parcels: Array[nullable Location]
+
+ # Number of loaded parcels
+ # Must be consistent with `parcels`
+ var nb_parcels: Int
+
+ # Clone of the state
+ # Used by `Action::trans`
+ #
+ # Warning: the `parcels` array is not cloned for efficiency.
+ # You need to clone it if you mutate it
+ private fun clone: State
+ do
+ var res = new State(problem, robot, parcels, nb_parcels)
+ return res
+ end
+
+ redef fun hash
+ do
+ var res = hash_cache
+ if res != 0 then return res
+ res = robot.hash * 23 + parcels.hash * 7
+ hash_cache = res
+ return res
+ end
+
+ private var hash_cache = 0
+
+ redef fun ==(o)
+ do
+ if not o isa State then return false
+ return robot == o.robot and parcels == o.parcels
+ end
+
+ redef fun <(o) do return (self <=> o) < 0
+
+ redef fun <=>(o) do
+ var res = robot.id <=> o.robot.id
+ if res != 0 then return res
+ for i in [0..parcels.length[ do
+ var c = parcels[i]
+ var oc = o.parcels[i]
+ if c == oc then continue
+ if c == null then return -1
+ if oc == null then return 1
+ res = c.id <=> oc.id
+ if res != 0 then return res
+ end
+ return 0
+ end
+
+ redef fun to_s
+ do
+ var res = "{robot.name}"
+ for c in parcels do
+ res += ","
+ if c != null then
+ res += c.name
+ end
+ end
+ return res
+ end
+end
+
+# --configs
+var configs = false
+if not args.is_empty and args.first == "--configs" then
+ configs = true
+ args.shift
+end
+
+# Usage
+if args.is_empty then
+ print "Usage: plan [--configs] problem.txt [plan.txt]\nSearch, or apply, a plan to a problem."
+ exit 0
+end
+
+var problem = new PlanProblem
+
+# Load
+var problem_text = args.first.to_path.read_all
+problem.parse(problem_text)
+
+# Apply a plan
+if args.length == 2 then
+ var plan_text = args[1].to_path.read_all
+ var plan = problem.parse_plan(plan_text)
+ plan.dump(false)
+ exit 0
+end
+
+# run --configs?
+if configs then
+ problem.run_configs(100000)
+ exit 0
+end
+
+# Solve the plan
+var solver = problem.astar
+solver.memorize = true
+var res = solver.run
+if res == null then
+ print "No plan found :("
+ return
+end
+var plan = new Plan(problem)
+plan.actions.add_all res.plan
+plan.dump(false)
+print "# solver infos: {solver}"