concretizer: use clingo json output instead of text

Clingo actually has an option to output JSON -- use that instead of
parsing the raw otuput ourselves.

This also allows us to pick the best answer -- modify the parser to
*only* construct a spec for that one rather than building all of them
like we did before.
This commit is contained in:
Todd Gamblin 2019-10-13 21:05:35 -07:00
parent a332981f2f
commit 36ec66d997
2 changed files with 85 additions and 69 deletions

View File

@ -22,7 +22,7 @@
level = 'long' level = 'long'
#: output options #: output options
dump_options = ('asp', 'warnings', 'output', 'solutions') dump_options = ('asp', 'output', 'solutions')
def setup_parser(subparser): def setup_parser(subparser):
@ -87,6 +87,8 @@ def solve(parser, args):
# dump generated ASP program # dump generated ASP program
result = asp.solve(specs, dump=dump, models=models) result = asp.solve(specs, dump=dump, models=models)
if dump == ['asp']:
return
# die if no solution was found # die if no solution was found
# TODO: we need to be able to provide better error messages than this # TODO: we need to be able to provide better error messages than this
@ -95,13 +97,20 @@ def solve(parser, args):
# dump the solutions as concretized specs # dump the solutions as concretized specs
if 'solutions' in dump: if 'solutions' in dump:
for i, answer in enumerate(result.answers): best = min(result.answers)
tty.msg("Answer %d" % (i + 1)) assert best[1] == result.answers[-1][1]
for spec in specs:
answer_spec = answer[spec.name] opt, i, answer = best
if args.yaml: tty.msg(
out = answer_spec.to_yaml() "%d Answers. Best optimization %s:" % (i + 1, opt)
else: )
out = answer_spec.tree(
color=sys.stdout.isatty(), **kwargs) # iterate over roots from command line
sys.stdout.write(out) for spec in specs:
answer_spec = answer[spec.name]
if args.yaml:
out = answer_spec.to_yaml()
else:
out = answer_spec.tree(
color=sys.stdout.isatty(), **kwargs)
sys.stdout.write(out)

View File

@ -6,6 +6,7 @@
from __future__ import print_function from __future__ import print_function
import collections import collections
import json
import pkgutil import pkgutil
import re import re
import sys import sys
@ -503,64 +504,60 @@ def depends_on(self, pkg, dep):
self._specs[pkg]._add_dependency( self._specs[pkg]._add_dependency(
self._specs[dep], ('link', 'run')) self._specs[dep], ('link', 'run'))
def parse(self, stream, result): def call_actions_for_functions(self, function_strings):
for line in stream: function_re = re.compile(r'(\w+)\(([^)]*)\)')
match = re.match(r'SATISFIABLE', line)
if match: # parse functions out of ASP output
result.satisfiable = True functions = []
for string in function_strings:
m = function_re.match(string)
name, arg_string = m.groups()
args = re.split(r'\s*,\s*', arg_string)
args = [s.strip('"') if s.startswith('"') else int(s)
for s in args]
functions.append((name, args))
# Functions don't seem to be in particular order in output.
# Sort them here so that nodes are first, and so created
# before directives that need them (depends_on(), etc.)
functions.sort(key=lambda f: f[0] != "node")
self._specs = {}
for name, args in functions:
action = getattr(self, name, None)
if not action:
print("%s(%s)" % (name, ", ".join(str(a) for a in args)))
continue continue
assert action and callable(action)
action(*args)
match = re.match(r'OPTIMUM FOUND', line) def parse_json(self, data, result):
if match: if data["Result"] == "UNSATISFIABLE":
result.satisfiable = True result.satisfiable = False
continue return
match = re.match(r'UNSATISFIABLE', line) result.satisfiable = True
if match: if data["Result"] == "OPTIMUM FOUND":
result.satisfiable = False result.optimal = True
continue
match = re.match(r'Answer: (\d+)', line) nmodels = data["Models"]["Number"]
if not match: best_model_number = nmodels - 1
continue best_model = data["Call"][0]["Witnesses"][best_model_number]
opt = list(best_model["Costs"])
answer_number = int(match.group(1)) functions = best_model["Value"]
assert answer_number == len(result.answers) + 1
answer = next(stream)
tty.debug("Answer: %d" % answer_number, answer)
# parse functions out of ASP output
functions = []
for m in re.finditer(r'(\w+)\(([^)]*)\)', answer):
name, arg_string = m.groups()
args = re.split(r'\s*,\s*', arg_string)
args = [s.strip('"') if s.startswith('"') else int(s)
for s in args]
functions.append((name, args))
# Functions don't seem to be in particular order in output.
# Sort them here so that nodes are first, and so created
# before directives that need them (depends_on(), etc.)
functions.sort(key=lambda f: f[0] != "node")
self._specs = {}
for name, args in functions:
action = getattr(self, name, None)
if not action:
print("%s(%s)" % (name, ", ".join(str(a) for a in args)))
continue
assert action and callable(action)
action(*args)
result.answers.append(self._specs)
self.call_actions_for_functions(functions)
result.answers.append((opt, best_model_number, self._specs))
class Result(object): class Result(object):
def __init__(self, asp): def __init__(self, asp):
self.asp = asp self.asp = asp
self.satisfiable = None self.satisfiable = None
self.optimal = None
self.warnings = None self.warnings = None
# specs ordered by optimization level
self.answers = [] self.answers = []
@ -625,12 +622,18 @@ def colorize(string):
if 'asp' in dump: if 'asp' in dump:
if sys.stdout.isatty(): if sys.stdout.isatty():
tty.msg('ASP program:') tty.msg('ASP program:')
colorize(result.asp)
if dump == ['asp']:
print(result.asp)
return
else:
colorize(result.asp)
with tempfile.TemporaryFile("w+") as output: with tempfile.TemporaryFile("w+") as output:
with tempfile.TemporaryFile() as warnings: with tempfile.TemporaryFile() as warnings:
clingo( clingo(
'--models=%d' % models, '--models=0',
'--outf=2',
input=program, input=program,
output=output, output=output,
error=warnings, error=warnings,
@ -640,25 +643,29 @@ def colorize(string):
result.warnings = warnings.read().decode("utf-8") result.warnings = warnings.read().decode("utf-8")
# dump any warnings generated by the solver # dump any warnings generated by the solver
if 'warnings' in dump: if result.warnings:
if result.warnings: if sys.stdout.isatty():
if sys.stdout.isatty(): tty.msg('Clingo gave the following warnings:')
tty.msg('Clingo gave the following warnings:') colorize(result.warnings)
colorize(result.warnings) else:
else: if sys.stdout.isatty():
if sys.stdout.isatty(): tty.msg('No warnings.')
tty.msg('No warnings.')
output.seek(0) output.seek(0)
result.output = output.read() result.output = output.read()
data = json.loads(result.output)
# dump the raw output of the solver # dump the raw output of the solver
if 'output' in dump: if 'output' in dump:
if sys.stdout.isatty(): if sys.stdout.isatty():
tty.msg('Clingo output:') tty.msg('Clingo output:')
colorize(result.output)
print(result.output)
if 'solutions' not in dump:
return
output.seek(0) output.seek(0)
parser.parse(output, result) parser.parse_json(data, result)
return result return result