nit: Added link to `CONTRIBUTING.md` from the README
[nit.git] / contrib / simplan / simplan.nit
1 # This file is part of NIT ( http://www.nitlanguage.org ).
2 #
3 # Licensed under the Apache License, Version 2.0 (the "License");
4 # you may not use this file except in compliance with the License.
5 # You may obtain a copy of the License at
6 #
7 # http://www.apache.org/licenses/LICENSE-2.0
8 #
9 # Unless required by applicable law or agreed to in writing, software
10 # distributed under the License is distributed on an "AS IS" BASIS,
11 # WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12 # See the License for the specific language governing permissions and
13 # limitations under the License.
14
15 # A simple planning problem solver using A* for a robot that delivers parcels.
16 #
17 # A map is made of `Location`s (with cartesian coordinates) connected with `Road`s.
18 # Some parcels and a robot are placed on the map.
19 #
20 # The goal of the robot is to plan the delivery of the parcels to their destination in the most
21 # efficient way knowing that:
22 #
23 # * the robot has a given speed to drive
24 # * the loading and unloading of parcels take time
25 # * the capacity of the robot is limited
26 module simplan
27
28 import ai::search
29
30 private import simplan_lexer
31 private import simplan_parser
32
33 # The description of the planing problem to solve
34 class PlanProblem
35 super SearchProblem[State, Action]
36
37 # Some parameters of the problem
38
39 # The speed on the road.
40 # Used for the action `Drive`
41 var speed = 1.0
42
43 # Cost of loading a parcel in a robot
44 # Used for the action `Load`
45 var duration_loading = 30.0
46
47 # Cost of unloading a parcel in a robot
48 # Used for the action `Unload`
49 var duration_unloading = 30.0
50
51 # How many parcels can a robot transport at the same time?
52 var capacity = 2
53
54 # The name of the robot
55 var robot_name = "r0"
56
57 # Locations on the map, by their names
58 var locations = new HashMap[String, Location]
59
60 # All the parcels, by their `id`
61 var parcels = new Array[Parcel]
62
63 # All the parcels, by their names
64 var parcel_by_name = new HashMap[String, Parcel]
65
66 # +infinite, used to initialize some floats with the maximum value.
67 private var infinite: Float = 1.0 / 0.0
68
69 # The starting state
70 redef var initial_state: State is noinit
71
72 # Parse and initialize a problem from description `text`
73 fun parse(text: String)
74 do
75 var l = new Lexer_simplan(text)
76 var p = new Parser_simplan
77 p.tokens.add_all l.lex
78 var n = p.parse
79 if n isa NError then
80 print n.message
81 exit 1
82 end
83 n = n.children.first.children.first.as(not null)
84 if n isa Nplan then
85 print "Error: expected a problem, got a plan."
86 exit 1
87 end
88 assert n isa Nproblem
89
90 # Load all locations
91 for n2 in n.n_locations.n_list.children do
92 var e = new Location(locations.length, n2.n_name.text, n2.n_x.text.to_f, n2.n_y.text.to_f)
93 assert not locations.has_key(e.name)
94 locations[e.name] = e
95 end
96 print "# {locations.length} locations"
97
98 # Load all roads
99 var nbr = 0
100 for n2 in n.n_roads.n_list.children do
101 var o = locations.get_or_null(n2.n_orig.text)
102 var d = locations.get_or_null(n2.n_dest.text)
103 assert o != null and d != null
104 var r = new Road(o,d)
105 o.roads.add(r)
106 r = new Road(d,o)
107 d.roads.add(r)
108 nbr += 2
109 end
110 print "# {nbr} roads"
111
112 # Compute all road durations
113 for e in locations.values do
114 e.durations = new Array[Float].filled_with(infinite, locations.length)
115 e.durations[e.id] = 0.0
116 for r in e.roads do
117 e.durations[r.dest.id] = r.distance / speed
118 end
119 end
120
121 # Close the duration relation between each pair of locations
122 for k in locations.values do
123 for i in locations.values do
124 for j in locations.values do
125 var d = i.durations[k.id] + k.durations[j.id]
126 if i.durations[j.id] > d then
127 i.durations[j.id] = d
128 end
129 end
130 end
131 end
132
133 # Load the robot
134 var robot = null
135 for n2 in n.n_robots.n_list.children do
136 var name = n2.n_name.text
137 robot = locations.get_or_null(n2.n_emplacement.text)
138 assert name == robot_name and robot != null
139 end
140 assert robot != null
141 print "# 1 robot"
142
143 # Load the parcels
144 var parcel_locations = new Array[nullable Location]
145 for n2 in n.n_parcels.n_list.children do
146 var name = n2.n_name.text
147 var e = locations.get_or_null(n2.n_emplacement.text)
148 assert e != null
149 var parcel = new Parcel(parcels.length, name, e, e)
150 parcels.add parcel
151 parcel_locations.add e
152 parcel_by_name[name] = parcel
153
154 end
155 print "# {parcels.length} parcels"
156
157 # Load the goal of parcels
158 for n2 in n.n_goal.n_list.children do
159 var parcel = parcel_by_name.get_or_null(n2.n_name.text)
160 var e = locations.get_or_null(n2.n_emplacement.text)
161 assert parcel != null and e != null
162 parcel.goal = e
163 end
164 print "# 1 goal"
165
166 print "# size of the problem: {locations.length.to_f.pow(parcels.length.to_f+1.0).to_precision(0)}"
167
168 initial_state = new State(self, robot, parcel_locations, 0)
169 end
170
171 # Parse a plan for a given problem
172 fun parse_plan(text: String): Plan
173 do
174 var l = new Lexer_simplan(text)
175 var p = new Parser_simplan
176 p.tokens.add_all l.lex
177 var n = p.parse
178 if n isa NError then
179 print n.message
180 exit 1
181 end
182 n = n.children.first.children.first.as(not null)
183 if n isa Nproblem then
184 print "Error: expected a plan, got a problem."
185 exit 1
186 end
187 assert n isa Nplan
188
189 var res = new Plan(self)
190 var e = initial_state
191 var cost = 0.0
192 for n2 in n.n_actions.children do
193 if n2 isa Naction_load then
194 var parcel = parcel_by_name.get_or_null(n2.n_parcel.text)
195 assert parcel != null
196 var a = new Load(self, parcel)
197 res.actions.add(a)
198 e = a.trans(e)
199 cost += a.cost
200 else if n2 isa Naction_unload then
201 var parcel = parcel_by_name.get_or_null(n2.n_parcel.text)
202 assert parcel != null
203 var a = new Unload(self, parcel)
204 res.actions.add(a)
205 e = a.trans(e)
206 cost += a.cost
207 else if n2 isa Naction_drive then
208 var o = locations.get_or_null(n2.n_orig.text)
209 var d = locations.get_or_null(n2.n_dest.text)
210 assert o != null and d != null
211 var road: nullable Road = null
212 for r in o.roads do if r.dest == d then
213 road = r
214 break
215 end
216 assert road != null
217 var a = new Drive(self, road)
218 res.actions.add(a)
219 e = a.trans(e)
220 cost += a.cost
221 else abort
222 end
223 print "# loaded plan in {res.actions.length} steps; cost = {cost}"
224 return res
225 end
226
227 redef fun actions(state)
228 do
229 var res = new Array[Action]
230
231 # Driving?
232 for road in state.robot.roads do
233 res.add(new Drive(self, road))
234 end
235
236 # Loading?
237 for i in [0..state.parcels.length[ do
238 if state.parcels[i] == state.robot and state.nb_parcels < self.capacity and self.parcels[i].goal != state.robot then
239 res.add(new Load(self, parcels[i]))
240 end
241 end
242
243 # Unloading?
244 for i in [0..state.parcels.length[ do
245 # Unloading is always a valid action,
246 # even if dropping a parcel outside of its goal is unlikely (but possible)
247 # to be a part of an optimal plan.
248 if state.parcels[i] == null then #and self.parcels[i].goal == state.robot then
249 res.add(new Unload(self, parcels[i]))
250 end
251 end
252
253 return res
254 end
255
256 redef fun apply_action(state, action) do return action.trans(state)
257
258 redef fun cost(state1, action, state2) do return action.cost
259
260 redef fun is_goal(state)
261 do
262 for i in [0..parcels.length[ do
263 if state.parcels[i] != parcels[i].goal then return false
264 end
265 return true
266 end
267
268 redef fun heuristic(state)
269 do
270 # Combination (maximum) of two heuristic:
271 # Heuristic 1: maximum driving time to take and deliver a single parcel
272 var max_for_one = 0.0
273 # Heuristic 2: driving time to take the nearest parcel, then deliver all parcels
274 var total_deliver_drive = 0.0
275 var min_take_one = infinite
276 # Total loading/unloading time (incompressible cost, added to both heuristics)
277 var total_load = 0.0
278
279 for i in [0..parcels.length[ do
280 # The parcel location
281 var c = state.parcels[i]
282
283 # Its goal location
284 var b = parcels[i].goal
285
286 # Parcel is fine, nothing to do.
287 if c == b then continue
288
289 # Driving time to take and deliver this parcel
290 var t = 0.0
291
292 # Current position of the parcel (somewhere or in the robot)
293 var current: Location
294
295 if c == null then
296 # The parcel is in the robot
297 current = state.robot
298 min_take_one = 0.0
299 else
300 # The parcel is somewhere
301 current = c
302 # So go get it.
303 var tt = state.robot.duration(c)
304 total_load += duration_loading
305
306 t += tt
307 if tt < min_take_one then min_take_one = tt
308 end
309
310 # Drive and unload the parcel
311 var td = current.duration(b)
312 total_load += duration_unloading
313
314 t += td
315 if t > max_for_one then max_for_one = t
316 total_deliver_drive += td
317 end
318
319 # Since one robot can transport several parcels at the same time,
320 # the best optimistic scenario is to deliver at full capacity
321 if min_take_one == infinite then min_take_one = 0.0
322 var res = min_take_one + total_deliver_drive / capacity.to_f
323
324 # Get best of both heuristics
325 if res < max_for_one then res = max_for_one
326
327 # Add incompressible (un)loading costs
328 res += total_load
329
330 return res
331 end
332
333 redef fun make_memory
334 do
335 var res = super
336 res.add new RBTreeMap[State, SearchNode[State, Action]]
337 res.add new BinTreeMap[State, SearchNode[State, Action]]
338 return res
339 end
340 end
341
342 # A node on the map
343 class Location
344 # Indexed identifier, starting at 0
345 # Used to access locations in arrays.
346 var id: Int
347
348 # The name of the location, from the problem description
349 var name: String
350
351 # The x-coordinate of the location
352 var x: Float
353
354 # The y-coordinate of the location
355 var y: Float
356
357 # All roads from this location
358 var roads = new Array[Road]
359
360 # Drive duration to any other location on the map
361 fun duration(dest: Location): Float
362 do
363 return durations[dest.id]
364 end
365
366 private var durations = new Array[Float]
367
368 redef fun to_s do return "{id}({x}, {y})"
369 end
370
371 # A directed road segment between two locations
372 class Road
373 # The origin
374 var orig: Location
375
376 # The destination
377 var dest: Location
378
379 # The distance according to the coordinates of the locations.
380 var distance: Float is noautoinit
381
382 init
383 do
384 var dx = orig.x-dest.x
385 var dy = orig.y-dest.y
386 distance = (dx*dx + dy*dy).sqrt
387 end
388 end
389
390 # A parcel to move
391 class Parcel
392 # Indexed identifier, starting by 0
393 # Used to access parcels in arrays.
394 var id: Int
395 # The name of the parcel according to the problem description
396 # Used for output
397 var name: String
398 # The starting location
399 var start: Location
400 # The goal location
401 var goal: Location
402 end
403
404
405 # A plan on a problem
406 class Plan
407 # The original problem
408 var problem: PlanProblem
409
410 # The sequence of actions in the plan
411 var actions = new Array[Action]
412
413 # Check and write the plan
414 fun dump(verbose: Bool)
415 do
416 var e = problem.initial_state
417 print "SimplePlan \{"
418 var cost = 0.0
419 if verbose then print "#{e} {cost}"
420 for a in actions do
421 print "{a}"
422 e = a.trans(e)
423 cost += a.cost
424 if verbose then
425 print "#{e} {cost}"
426 if a isa Drive then
427 print "#{a.road.orig} -- {a.road.dest}: {a.road.distance}"
428 end
429 end
430
431 end
432 print "\}"
433 if not problem.is_goal(e) then
434 print "# /!\\ Goal Failed"
435 end
436 print "# plan in {actions.length} steps; cost={cost}\n"
437 end
438 end
439
440 # A primitive movement
441 abstract class Action
442 # The unitary cost of the action
443 fun cost: Float is abstract
444
445 # The state resulting to the application of the action
446 fun trans(e: State): State is abstract
447
448 # The original problem
449 var problem: PlanProblem
450 end
451
452 # A loading of a parcel
453 class Load
454 super Action
455
456 # The loaded parcel
457 var parcel: Parcel
458
459 redef fun cost do return problem.duration_loading
460
461 redef fun to_s do return "Load({problem.robot_name},{parcel.name});"
462
463 redef fun trans(e)
464 do
465 var res = e.clone
466 assert res.parcels[parcel.id] == res.robot
467 res.parcels = res.parcels.clone
468 res.parcels[parcel.id] = null
469 assert res.nb_parcels < problem.capacity
470 res.nb_parcels += 1
471 return res
472 end
473 end
474
475 # A unloading of a parcel
476 class Unload
477 super Action
478
479 # The unloaded parcel
480 var parcel: Parcel
481
482 redef fun cost do return problem.duration_unloading
483
484 redef fun to_s do return "Unload({problem.robot_name},{parcel.name});"
485
486 redef fun trans(e)
487 do
488 var res = e.clone
489 assert res.parcels[parcel.id] == null
490 res.parcels = res.parcels.clone
491 res.parcels[parcel.id] = res.robot
492 assert res.nb_parcels > 0
493 res.nb_parcels -= 1
494 return res
495 end
496 end
497
498 # A road driving
499 class Drive
500 super Action
501
502 # The road drove on
503 var road: Road
504
505 redef fun cost do return road.distance / problem.speed
506
507 redef fun to_s do return "Drive({problem.robot_name},{road.orig.name},{road.dest.name});"
508
509 redef fun trans(e)
510 do
511 var res = e.clone
512 assert res.robot == road.orig
513 res.robot = road.dest
514 return res
515 end
516 end
517
518 # Where each robot and parcel are?
519 class State
520 super Comparable
521 redef type OTHER: State
522
523 # The original problem
524 var problem: PlanProblem
525
526 # Where is the robot
527 var robot: Location
528
529 # Where each parcel is
530 # `null` if the parcel is loaded
531 var parcels: Array[nullable Location]
532
533 # Number of loaded parcels
534 # Must be consistent with `parcels`
535 var nb_parcels: Int
536
537 # Clone of the state
538 # Used by `Action::trans`
539 #
540 # Warning: the `parcels` array is not cloned for efficiency.
541 # You need to clone it if you mutate it
542 private fun clone: State
543 do
544 var res = new State(problem, robot, parcels, nb_parcels)
545 return res
546 end
547
548 redef fun hash
549 do
550 var res = hash_cache
551 if res != 0 then return res
552 res = robot.hash * 23 + parcels.hash * 7
553 hash_cache = res
554 return res
555 end
556
557 private var hash_cache = 0
558
559 redef fun ==(o)
560 do
561 if not o isa State then return false
562 return robot == o.robot and parcels == o.parcels
563 end
564
565 redef fun <(o) do return (self <=> o) < 0
566
567 redef fun <=>(o) do
568 var res = robot.id <=> o.robot.id
569 if res != 0 then return res
570 for i in [0..parcels.length[ do
571 var c = parcels[i]
572 var oc = o.parcels[i]
573 if c == oc then continue
574 if c == null then return -1
575 if oc == null then return 1
576 res = c.id <=> oc.id
577 if res != 0 then return res
578 end
579 return 0
580 end
581
582 redef fun to_s
583 do
584 var res = "{robot.name}"
585 for c in parcels do
586 res += ","
587 if c != null then
588 res += c.name
589 end
590 end
591 return res
592 end
593 end
594
595 # --configs
596 var configs = false
597 if not args.is_empty and args.first == "--configs" then
598 configs = true
599 args.shift
600 end
601
602 # Usage
603 if args.is_empty then
604 print "Usage: plan [--configs] problem.txt [plan.txt]\nSearch, or apply, a plan to a problem."
605 exit 0
606 end
607
608 var problem = new PlanProblem
609
610 # Load
611 var problem_text = args.first.to_path.read_all
612 problem.parse(problem_text)
613
614 # Apply a plan
615 if args.length == 2 then
616 var plan_text = args[1].to_path.read_all
617 var plan = problem.parse_plan(plan_text)
618 plan.dump(false)
619 exit 0
620 end
621
622 # run --configs?
623 if configs then
624 problem.run_configs(100000)
625 exit 0
626 end
627
628 # Solve the plan
629 var solver = problem.astar
630 solver.memorize = true
631 var res = solver.run
632 if res == null then
633 print "No plan found :("
634 return
635 end
636 var plan = new Plan(problem)
637 plan.actions.add_all res.plan
638 plan.dump(false)
639 print "# solver infos: {solver}"