sfp 0.1.0
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/.gitignore +1 -0
- data/LICENSE +30 -0
- data/README.md +200 -0
- data/bin/sfp +24 -0
- data/bin/solver/linux/downward +0 -0
- data/bin/solver/linux/preprocess +0 -0
- data/bin/solver/macos/downward +0 -0
- data/bin/solver/macos/preprocess +0 -0
- data/lib/sfp/SfpLangLexer.rb +3127 -0
- data/lib/sfp/SfpLangParser.rb +9770 -0
- data/lib/sfp/Sfplib.rb +357 -0
- data/lib/sfp/parser.rb +128 -0
- data/lib/sfp/planner.rb +460 -0
- data/lib/sfp/sas.rb +966 -0
- data/lib/sfp/sas_translator.rb +1836 -0
- data/lib/sfp/sfw2graph.rb +168 -0
- data/lib/sfp/visitors.rb +132 -0
- data/lib/sfp.rb +17 -0
- data/sfp.gemspec +26 -0
- data/src/SfpLang.g +1005 -0
- data/src/build.sh +7 -0
- data/test/cloud-classes.sfp +77 -0
- data/test/cloud1.sfp +33 -0
- data/test/cloud2.sfp +41 -0
- data/test/cloud3.sfp +42 -0
- data/test/service-classes.sfp +151 -0
- data/test/service1.sfp +24 -0
- data/test/service3.sfp +27 -0
- data/test/task.sfp +22 -0
- data/test/test.inc +9 -0
- data/test/test.sfp +13 -0
- data/test/test1.sfp +19 -0
- data/test/test2.inc +40 -0
- data/test/test2.sfp +17 -0
- data/test/types.sfp +28 -0
- data/test/v1.1.sfp +22 -0
- metadata +120 -0
data/lib/sfp/planner.rb
ADDED
@@ -0,0 +1,460 @@
|
|
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
|
+
# @param :parallel : 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
|
+
# @param :parallel : 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
|
+
planner = case os
|
300
|
+
when 'linux' then File.expand_path(File.dirname(__FILE__) + '/../../bin/solver/linux')
|
301
|
+
when 'macos', 'darwin' then File.expand_path(File.dirname(__FILE__) + '/../../bin/solver/macos')
|
302
|
+
else nil
|
303
|
+
end
|
304
|
+
raise UnsupportedPlatformException, os + ' is not supported' if planner == nil
|
305
|
+
planner
|
306
|
+
end
|
307
|
+
|
308
|
+
# Return the solver parameters based on given heuristic mode.
|
309
|
+
# Default value: FF
|
310
|
+
def self.parameters(heuristic='ff')
|
311
|
+
return case heuristic
|
312
|
+
when 'lmcut' then '--search "astar(lmcut())"'
|
313
|
+
when 'blind' then '--search "astar(blind())"'
|
314
|
+
when 'cg' then '--search "lazy_greedy(cg(cost_type=2))"'
|
315
|
+
when 'cea' then '--search "lazy_greedy(cea(cost_type=2))"'
|
316
|
+
when 'mad' then '--search "lazy_greedy(mad())"'
|
317
|
+
else '--search "lazy_greedy(ff(cost_type=0))"'
|
318
|
+
end
|
319
|
+
end
|
320
|
+
|
321
|
+
# Return a command to run the planner:
|
322
|
+
# - within given working directory "dir"
|
323
|
+
# - problem in SAS+ format, available in"sas_file"
|
324
|
+
# - solution will be saved in "plan_file"
|
325
|
+
def self.getcommand(dir, sas_file, plan_file, heuristic='ff', debug=false)
|
326
|
+
planner = Sfp::Planner.path
|
327
|
+
params = Sfp::Planner.parameters(heuristic)
|
328
|
+
timeout = Sfp::Planner::Config.timeout
|
329
|
+
|
330
|
+
os = `uname -s`.downcase.strip
|
331
|
+
command = case os
|
332
|
+
when 'linux'
|
333
|
+
then "cd #{dir}; " +
|
334
|
+
"ulimit -Sv #{Sfp::Planner::Config.max_memory}; " +
|
335
|
+
"#{planner}/preprocess < #{sas_file} 2>/dev/null 1>/dev/null; " +
|
336
|
+
"if [ -f 'output' ]; then " +
|
337
|
+
"timeout #{timeout} nice #{planner}/downward #{params} " +
|
338
|
+
"--plan-file #{plan_file} < output; fi"
|
339
|
+
when 'macos', 'darwin'
|
340
|
+
then "cd #{dir}; " +
|
341
|
+
"ulimit -Sv #{Sfp::Planner::Config.max_memory}; " +
|
342
|
+
"#{planner}/preprocess < #{sas_file} 1> /dev/null; " +
|
343
|
+
"#{planner}/downward #{params} " +
|
344
|
+
"--plan-file #{plan_file} < output 1> /dev/null;"
|
345
|
+
else nil
|
346
|
+
end
|
347
|
+
|
348
|
+
if not command.nil? and (os == 'linux' or os == 'macos' or os == 'darwin')
|
349
|
+
command = "#{command} 1> /dev/null 2>/dev/null"
|
350
|
+
end
|
351
|
+
|
352
|
+
command
|
353
|
+
end
|
354
|
+
|
355
|
+
# Combination between two heuristic to obtain a suboptimal plan.
|
356
|
+
# 1) solve the problem with CG/CEA/FF, that will produce (usually) a non-optimal plan
|
357
|
+
# 2) remove actions which are not selected by previous step
|
358
|
+
# 3) solve the problem with LMCUT using A*-search to obtain a sub-optimal plan
|
359
|
+
class MixedHeuristic
|
360
|
+
def initialize(dir, sas_file, plan_file)
|
361
|
+
@dir = dir
|
362
|
+
@sas_file = sas_file
|
363
|
+
@plan_file = plan_file
|
364
|
+
end
|
365
|
+
|
366
|
+
def solve
|
367
|
+
# 1) solve with FF
|
368
|
+
planner1 = Sfp::Planner.getcommand(@dir, @sas_file, @plan_file, 'ff')
|
369
|
+
Kernel.system(planner1)
|
370
|
+
# 1b) if not found, try CEA
|
371
|
+
if not File.exist?(@plan_file)
|
372
|
+
planner2 = Sfp::Planner.getcommand(@dir, @sas_file, @plan_file, 'cea')
|
373
|
+
Kernel.system(planner2)
|
374
|
+
end
|
375
|
+
# 1c) if not found, try CG
|
376
|
+
if not File.exists?(@plan_file)
|
377
|
+
planner3 = Sfp::Planner.getcommand(@dir, @sas_file, @plan_file, 'cg')
|
378
|
+
Kernel.system(planner3)
|
379
|
+
return false if not File.exist?(@plan_file)
|
380
|
+
end
|
381
|
+
|
382
|
+
# 2) remove unselected operators
|
383
|
+
new_sas = @sas_file + '.2'
|
384
|
+
new_plan = @plan_file + '.2'
|
385
|
+
self.filter_operators(@sas_file, @plan_file, new_sas)
|
386
|
+
|
387
|
+
# 3) generate the final plan with LMCUT
|
388
|
+
lmcut = Sfp::Planner.getcommand(@dir, new_sas, new_plan, 'lmcut')
|
389
|
+
Kernel.system(lmcut)
|
390
|
+
|
391
|
+
# LMCUT cannot find the sub-optimized plan
|
392
|
+
File.delete(@plan_file)
|
393
|
+
File.rename(new_plan, @plan_file) if File.exist?(new_plan)
|
394
|
+
|
395
|
+
true
|
396
|
+
end
|
397
|
+
|
398
|
+
def filter_operators(sas, plan, new_sas)
|
399
|
+
# generate the selected actions
|
400
|
+
selected = []
|
401
|
+
File.read(plan).each_line do |line|
|
402
|
+
line.strip!
|
403
|
+
line = line[1, line.length-2]
|
404
|
+
selected << line.split(' ', 2)[0]
|
405
|
+
end
|
406
|
+
|
407
|
+
# remove unselected operators
|
408
|
+
output = ""
|
409
|
+
operator = nil
|
410
|
+
id = nil
|
411
|
+
total_op = false
|
412
|
+
counter = 0
|
413
|
+
File.read(sas).each_line do |line|
|
414
|
+
if line =~ /^end_goal/
|
415
|
+
total_op = true
|
416
|
+
elsif total_op
|
417
|
+
output += "__TOTAL_OPERATOR__\n"
|
418
|
+
total_op = false
|
419
|
+
next
|
420
|
+
end
|
421
|
+
|
422
|
+
if line =~ /^begin_operator/
|
423
|
+
operator = ""
|
424
|
+
id = nil
|
425
|
+
elsif line =~ /^end_operator/
|
426
|
+
if not selected.index(id).nil?
|
427
|
+
output += "begin_operator\n#{operator}end_operator\n"
|
428
|
+
counter += 1
|
429
|
+
end
|
430
|
+
operator = nil
|
431
|
+
id = nil
|
432
|
+
elsif operator.nil?
|
433
|
+
output += line
|
434
|
+
else
|
435
|
+
id = line.split(' ', 2)[0] if id.nil?
|
436
|
+
operator += line
|
437
|
+
end
|
438
|
+
end
|
439
|
+
|
440
|
+
# replace total operator
|
441
|
+
output.sub!(/__TOTAL_OPERATOR__/, counter.to_s)
|
442
|
+
|
443
|
+
# save filtered problem
|
444
|
+
File.open(new_sas, 'w') { |f| f.write(output) }
|
445
|
+
end
|
446
|
+
end
|
447
|
+
|
448
|
+
class Action
|
449
|
+
attr_accessor :operator, :predecessor
|
450
|
+
|
451
|
+
def initialize(operator)
|
452
|
+
@operator = operator
|
453
|
+
@predecessor = Array.new
|
454
|
+
end
|
455
|
+
end
|
456
|
+
|
457
|
+
class UnsupportedPlatformException < Exception
|
458
|
+
end
|
459
|
+
end
|
460
|
+
end
|