sfp 0.2.1 → 0.3.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
data/lib/sfp/planner.rb DELETED
@@ -1,482 +0,0 @@
1
- module Sfp
2
- class Planner
3
- Heuristic = 'mixed' # lmcut, cg, cea, ff, mixed ([cg|cea|ff]=>lmcut)
4
- Debug = false
5
-
6
- class Config
7
- # The timeout for the solver in seconds (default 600s/5mins)
8
- @@timeout = 600
9
-
10
- def self.timeout; @@timeout; end
11
-
12
- def self.set_timeout(timeout); @@timeout = timeout; end
13
-
14
- # The maximum memory that can be consumed by the solver
15
- @@max_memory = 2048000 # (in K) -- default ~2GB
16
-
17
- def self.max_memory; @@max_memory; end
18
-
19
- def self.set_max_memory(memory); @@max_memory = memory; end
20
- end
21
-
22
-
23
- attr_accessor :debug
24
- attr_reader :parser
25
-
26
- def initialize(params={})
27
- @debug = Debug
28
- @parser = Sfp::Parser.new(params)
29
- end
30
-
31
- # @param :string : SFP task in string
32
- # @param :sfp : SFP task in Hash data structure
33
- # @param :file : SFP task in file with specified path
34
- # @param :sas_plan : if true then return a raw SAS plan
35
- # @param :parallel : if true then return a parallel (partial-order) plan,
36
- # if false or nil then return a sequential plan
37
- # @param :json : if true then return the plan in JSON
38
- # @param :pretty_json : if true then return in pretty JSON
39
- #
40
- def solve(params={})
41
- if params[:string].is_a?(String)
42
- @parser.parse(string)
43
- elsif params[:sfp].is_a?(Hash)
44
- @parser.root = params[:sfp]
45
- elsif params[:file].is_a?(String)
46
- raise Exception, "File not found: #{params[:file]}" if not File.exist?(params[:file])
47
- @parser.home_dir = File.expand_path(File.dirname(params[:file]))
48
- @parser.parse(File.read(params[:file]))
49
- end
50
-
51
- if not @parser.conformant
52
- return self.solve_classical_task(params)
53
- else
54
- return self.solve_conformant_task(params)
55
- end
56
- end
57
-
58
- # @param :parallel : if true then return a parallel (partial-order) plan,
59
- # if false or nil then return a sequential plan
60
- # @param :json : if true then return the plan in JSON
61
- # @param :pretty_json : if true then return in pretty JSON
62
- #
63
- def to_bsig(params={})
64
- raise Exception, "Conformant task is not supported yet" if @parser.conformant
65
-
66
- bsig = (params[:parallel] ? self.to_parallel_bsig : self.to_sequential_bsig)
67
- return (params[:json] ? JSON.generate(bsig) :
68
- (params[:pretty_json] ? JSON.pretty_generate(bsig) : bsig))
69
- end
70
-
71
- # @param :json : if true then return in JSON
72
- # @param :pretty_json : if true then return in pretty JSON
73
- #
74
- def final_state(params={})
75
- return nil if @plan.nil?
76
- state = @sas_task.final_state
77
- return (params[:json] ? JSON.generate(state) :
78
- (params[:pretty_json] ? JSON.pretty_generate(state) : state))
79
- end
80
-
81
- protected
82
- def solve_conformant_task(params={})
83
- # TODO
84
- # 1) generate all possible initial states
85
- # remove states that do not satisfy the global constraint
86
- def get_possible_partial_initial_states(init)
87
- def combinators(variables, var_values, index=0, result={}, bucket=[])
88
- if index >= variables.length
89
- # collect
90
- bucket << result.clone
91
- else
92
- var = variables[index]
93
- var_values[var].each do |value|
94
- result[var] = value
95
- combinators(variables, var_values, index+1, result, bucket)
96
- end
97
- end
98
- bucket
99
- end
100
- # collect variables with non-deterministic value
101
- collector = Sfp::Visitor::ConformantVariables.new
102
- init.accept(collector)
103
- vars = collector.var_values.keys
104
- combinators(vars, collector.var_values)
105
- end
106
-
107
- # 2) for each initial states, generate a plan (if possible)
108
- # for given goal state
109
- def get_possible_plans(partial_inits)
110
- # TODO
111
- solutions = []
112
- partial_inits.each do |partial_init|
113
- parser = Sfp::Parser.new
114
- parser.root = Sfp::Helper.deep_clone(@parser.root)
115
- init = parser.root['initial']
116
- partial_init.each do |path,value|
117
- parent, var = path.extract
118
- parent = init.at?(parent)
119
- parent[var] = value
120
- end
121
- plan, sas_task = self.solve_sas(parser)
122
- solution = {:partial_init => partial_init,
123
- :plan => plan,
124
- :sas_task => sas_task}
125
- solutions << solution
126
- end
127
- solutions
128
- end
129
-
130
- # 3) merge the plans into one
131
- def merge_plans(solutions)
132
- # TODO
133
- solutions.each { |sol| puts sol[:partial_init].inspect + " => " + sol[:plan].inspect }
134
- nil
135
- end
136
-
137
- partial_inits = get_possible_partial_initial_states(@parser.root['initial'])
138
- solutions = get_possible_plans(partial_inits)
139
- merged_plan = merge_plans(solutions)
140
- end
141
-
142
- def solve_classical_task(params={})
143
- @plan, @sas_task = self.solve_sas(@parser)
144
-
145
- return @plan if params[:sas_plan]
146
-
147
- plan = (params[:parallel] ? self.get_parallel_plan : self.get_sequential_plan)
148
- return (params[:json] ? JSON.generate(plan) :
149
- (params[:pretty_json] ? JSON.pretty_generate(plan) : plan))
150
- end
151
-
152
- def bsig_template
153
- return {'version' => 1, 'operators' => [], 'id' => Time.now.getutc.to_i, 'goal' => []}
154
- end
155
-
156
- def to_sequential_bsig
157
- bsig = self.bsig_template
158
- return bsig if @plan.length <= 0
159
- plan = self.get_sequential_plan
160
- bsig['operators'] = workflow = plan['workflow']
161
- (workflow.length-1).downto(1) do |i|
162
- op = workflow[i]
163
- prev_op = workflow[i-1]
164
- prev_op['effect'].each { |k,v| op['condition'][k] = v }
165
- end
166
- bsig['goal'], _ = self.bsig_goal_operator(workflow)
167
- return bsig
168
- end
169
-
170
- def to_parallel_bsig
171
- return nil if @plan.nil?
172
- bsig = self.bsig_template
173
- return bsig if @plan.length <= 0
174
- plan = self.get_parallel_plan
175
- # foreach operator's predecessors, add its effects to operator's conditions
176
- bsig['operators'] = workflow = plan['workflow']
177
- workflow.each do |op|
178
- op['predecessors'].each do |pred|
179
- pred_op = workflow[pred]
180
- pred_op['effect'].each { |k,v| op['condition'][k] = v }
181
- end
182
- end
183
- # remove unnecessary information
184
- workflow.each do |op|
185
- op.delete('id')
186
- op.delete('predecessors')
187
- op.delete('successors')
188
- end
189
- bsig['goal'], bsig['goal_operator'] = self.bsig_goal_operator(workflow)
190
- return bsig
191
- end
192
-
193
- def bsig_goal_operator(workflow)
194
- goal_op = {}
195
- goal = {}
196
- @sas_task.final_state.each do |g|
197
- variable, value = @parser.variable_name_and_value(g[:id], g[:value])
198
- # search a supporting operator
199
- (workflow.length-1).downto(0) do |i|
200
- if workflow[i]['effect'].has_key?(variable)
201
- if workflow[i]['effect'][variable] == value
202
- goal_op[variable] = workflow[i]['name']
203
- goal[variable] = value
204
- break
205
- else
206
- #Nuri::Util.debug "#{variable}=#{value} is not changing"
207
- #Nuri::Util.debug value.inspect + ' == ' + workflow[i]['effect'][variable].inspect
208
- end
209
- end
210
- end
211
- end
212
- return goal, goal_op
213
- end
214
-
215
- def get_sequential_plan
216
- json = { 'type'=>'sequential', 'workflow'=>nil, 'version'=>'1', 'total'=>0 }
217
- return json if @plan == nil
218
- json['workflow'] = []
219
- @plan.each do |line|
220
- op_name = line[1, line.length-2].split(' ')[0]
221
- operator = @parser.operators[op_name]
222
- raise Exception, 'Cannot find operator: ' + op_name if operator == nil
223
- json['workflow'] << operator.to_sfw
224
- end
225
- json['total'] = json['workflow'].length
226
- return json
227
- end
228
-
229
- def get_parallel_plan
230
- json = {'type'=>'parallel', 'workflow'=>nil, 'init'=>nil, 'version'=>'1', 'total'=>0}
231
- return json if @plan == nil
232
- json['workflow'], json['init'], json['total'] = @sas_task.get_partial_order_workflow(@parser)
233
- return json
234
- end
235
-
236
- def extract_sas_plan(sas_plan, parser)
237
- actions = Array.new
238
- sas_plan.split("\n").each do |sas_operator|
239
- op_name = sas_operator[1,sas_operator.length-2].split(' ')[0]
240
- actions << Action.new(parser.operators[op_name])
241
- end
242
- end
243
-
244
- def solve_sas(parser)
245
- return nil if parser.nil?
246
-
247
- tmp_dir = '/tmp/nuri_' + (rand * 100000).to_i.abs.to_s
248
- begin
249
- while File.exist?(tmp_dir)
250
- tmp_dir = '/tmp/nuri_' + (rand * 100000).to_i.abs.to_s
251
- end
252
- Dir.mkdir(tmp_dir)
253
- sas_file = tmp_dir + '/problem.sas'
254
- plan_file = tmp_dir + '/out.plan'
255
- File.open(sas_file, 'w') do |f|
256
- f.write(parser.to_sas)
257
- f.flush
258
- end
259
-
260
- if Heuristic == 'mixed'
261
- mixed = MixedHeuristic.new(tmp_dir, sas_file, plan_file)
262
- mixed.solve
263
- else
264
- command = Sfp::Planner.getcommand(tmp_dir, sas_file, plan_file, Heuristic)
265
- Kernel.system(command)
266
- end
267
- plan = (File.exist?(plan_file) ? File.read(plan_file) : nil)
268
-
269
- if plan != nil
270
- plan = extract_sas_plan(plan, parser)
271
- sas_task = Nuri::Sas::Task.new(sas_file)
272
- sas_task.sas_plan = plan
273
-
274
- tmp = []
275
- goal_op = nil
276
- plan.each do |op|
277
- _, name, _ = op.split('-', 3)
278
- goal_op = op if name == 'goal'
279
- next if name == 'goal' or name == 'globalop' or name == 'sometime'
280
- tmp.push(op)
281
- end
282
- sas_task.goal_operator_name = goal_op
283
- plan = tmp
284
- end
285
-
286
- return plan, sas_task
287
- rescue Exception => exp
288
- raise exp
289
- ensure
290
- File.delete('plan_numbers_and_cost') if File.exist?('plan_numbers_and_cost')
291
- Kernel.system('rm -rf ' + tmp_dir) if not @debug
292
- end
293
-
294
- return nil, nil
295
- end
296
-
297
- def self.path
298
- os = `uname -s`.downcase.strip
299
- machine = `uname -m`.downcase.strip
300
- planner = nil
301
-
302
- if os == 'linux' and machine[0,3] == 'x86'
303
- planner = File.expand_path(File.dirname(__FILE__) + '/../../bin/solver/linux-x86')
304
- elsif os == 'linux' and machine[0,3] == 'arm'
305
- planner = File.expand_path(File.dirname(__FILE__) + '/../../bin/solver/linux-arm')
306
- #Sfp::Planner::Config.set_max_memory(512)
307
- elsif os == 'macos' or os == 'darwin'
308
- planner = File.expand_path(File.dirname(__FILE__) + '/../../bin/solver/macos')
309
- end
310
-
311
- raise UnsupportedPlatformException, "#{os} is not supported" if planner.nil?
312
- planner
313
- end
314
-
315
- # Return the solver parameters based on given heuristic mode.
316
- # Default value: FF
317
- def self.parameters(heuristic='ff')
318
- return case heuristic
319
- when 'lmcut' then '--search "astar(lmcut())"'
320
- when 'blind' then '--search "astar(blind())"'
321
- when 'cg' then '--search "lazy_greedy(cg(cost_type=2))"'
322
- when 'cea' then '--search "lazy_greedy(cea(cost_type=2))"'
323
- when 'mad' then '--search "lazy_greedy(mad())"'
324
- when 'lama2011' then ' \
325
- --heuristic "hlm1,hff1=lm_ff_syn(lm_rhw(
326
- reasonable_orders=true,lm_cost_type=1,cost_type=1))" \
327
- --heuristic "hlm2,hff2=lm_ff_syn(lm_rhw(
328
- reasonable_orders=true,lm_cost_type=2,cost_type=2))" \
329
- --search "iterated([
330
- lazy_greedy([hff1,hlm1],preferred=[hff1,hlm1],
331
- cost_type=1,reopen_closed=false),
332
- lazy_greedy([hff2,hlm2],preferred=[hff2,hlm2],
333
- reopen_closed=false),
334
- lazy_wastar([hff2,hlm2],preferred=[hff2,hlm2],w=5),
335
- lazy_wastar([hff2,hlm2],preferred=[hff2,hlm2],w=3),
336
- lazy_wastar([hff2,hlm2],preferred=[hff2,hlm2],w=2),
337
- lazy_wastar([hff2,hlm2],preferred=[hff2,hlm2],w=1)],
338
- repeat_last=true,continue_on_fail=true)"'
339
- else '--search "lazy_greedy(ff(cost_type=0))"'
340
- end
341
- end
342
-
343
- # Return a command to run the planner:
344
- # - within given working directory "dir"
345
- # - problem in SAS+ format, available in"sas_file"
346
- # - solution will be saved in "plan_file"
347
- def self.getcommand(dir, sas_file, plan_file, heuristic='ff', debug=false)
348
- planner = Sfp::Planner.path
349
- params = Sfp::Planner.parameters(heuristic)
350
- timeout = Sfp::Planner::Config.timeout
351
-
352
- os = `uname -s`.downcase.strip
353
- command = case os
354
- when 'linux'
355
- then "cd #{dir}; " +
356
- "ulimit -Sv #{Sfp::Planner::Config.max_memory}; " +
357
- "#{planner}/preprocess < #{sas_file} 2>/dev/null 1>/dev/null; " +
358
- "if [ -f 'output' ]; then " +
359
- "timeout #{timeout} nice #{planner}/downward #{params} " +
360
- "--plan-file #{plan_file} < output; fi"
361
- when 'macos', 'darwin'
362
- then "cd #{dir}; " +
363
- "ulimit -Sv #{Sfp::Planner::Config.max_memory}; " +
364
- "#{planner}/preprocess < #{sas_file} 1> /dev/null; " +
365
- "#{planner}/downward #{params} " +
366
- "--plan-file #{plan_file} < output 1> /dev/null;"
367
- else nil
368
- end
369
-
370
- if not command.nil? and (os == 'linux' or os == 'macos' or os == 'darwin')
371
- command = "#{command} 1> /dev/null 2>/dev/null"
372
- end
373
-
374
- command
375
- end
376
-
377
- # Combination between two heuristic to obtain a suboptimal plan.
378
- # 1) solve the problem with CG/CEA/FF, that will produce (usually) a non-optimal plan
379
- # 2) remove actions which are not selected by previous step
380
- # 3) solve the problem with LMCUT using A*-search to obtain a sub-optimal plan
381
- class MixedHeuristic
382
- def initialize(dir, sas_file, plan_file)
383
- @dir = dir
384
- @sas_file = sas_file
385
- @plan_file = plan_file
386
- end
387
-
388
- def solve
389
- # 1) solve with FF
390
- planner1 = Sfp::Planner.getcommand(@dir, @sas_file, @plan_file, 'ff')
391
- Kernel.system(planner1)
392
- # 1b) if not found, try CEA
393
- if not File.exist?(@plan_file)
394
- planner2 = Sfp::Planner.getcommand(@dir, @sas_file, @plan_file, 'cea')
395
- Kernel.system(planner2)
396
- end
397
- # 1c) if not found, try CG
398
- if not File.exists?(@plan_file)
399
- planner3 = Sfp::Planner.getcommand(@dir, @sas_file, @plan_file, 'cg')
400
- Kernel.system(planner3)
401
- return false if not File.exist?(@plan_file)
402
- end
403
-
404
- # 2) remove unselected operators
405
- new_sas = @sas_file + '.2'
406
- new_plan = @plan_file + '.2'
407
- self.filter_operators(@sas_file, @plan_file, new_sas)
408
-
409
- # 3) generate the final plan with LMCUT
410
- lmcut = Sfp::Planner.getcommand(@dir, new_sas, new_plan, 'lmcut')
411
- Kernel.system(lmcut)
412
-
413
- # LMCUT cannot find the sub-optimized plan
414
- File.delete(@plan_file)
415
- File.rename(new_plan, @plan_file) if File.exist?(new_plan)
416
-
417
- true
418
- end
419
-
420
- def filter_operators(sas, plan, new_sas)
421
- # generate the selected actions
422
- selected = []
423
- File.read(plan).each_line do |line|
424
- line.strip!
425
- line = line[1, line.length-2]
426
- selected << line.split(' ', 2)[0]
427
- end
428
-
429
- # remove unselected operators
430
- output = ""
431
- operator = nil
432
- id = nil
433
- total_op = false
434
- counter = 0
435
- File.read(sas).each_line do |line|
436
- if line =~ /^end_goal/
437
- total_op = true
438
- elsif total_op
439
- output += "__TOTAL_OPERATOR__\n"
440
- total_op = false
441
- next
442
- end
443
-
444
- if line =~ /^begin_operator/
445
- operator = ""
446
- id = nil
447
- elsif line =~ /^end_operator/
448
- if not selected.index(id).nil?
449
- output += "begin_operator\n#{operator}end_operator\n"
450
- counter += 1
451
- end
452
- operator = nil
453
- id = nil
454
- elsif operator.nil?
455
- output += line
456
- else
457
- id = line.split(' ', 2)[0] if id.nil?
458
- operator += line
459
- end
460
- end
461
-
462
- # replace total operator
463
- output.sub!(/__TOTAL_OPERATOR__/, counter.to_s)
464
-
465
- # save filtered problem
466
- File.open(new_sas, 'w') { |f| f.write(output) }
467
- end
468
- end
469
-
470
- class Action
471
- attr_accessor :operator, :predecessor
472
-
473
- def initialize(operator)
474
- @operator = operator
475
- @predecessor = Array.new
476
- end
477
- end
478
-
479
- class UnsupportedPlatformException < Exception
480
- end
481
- end
482
- end