Introduction
I am currently working on an answer set programming project. For the project, I would like to assert facts over time into an ASP model and incrementally solve for an answer. For a simple use case example, consider a Mastermind game:
Example of problem
The game consists of a player and a score-keeper. In the game, a solution is generated by the score-keeper and the player makes guesses as to what the solution is. For each guess, the player is given feedback on his guess, where he uses it to sharpen his vision of the answer and makes another guess.
Presently, we have this game played between two computers in the following form:
- Computer A generates a solution and Computer B makes a random guess.
- Computer A computes the proper feedback of the first guess
- Computer B receives the feedback and solves with clingo with the Python API
- Computer B creates a new instance of the
clingo.Control
class
- Computer B loads the ASP model for mastermind via
clingo.Control.load(filename: str) -> None
- Computer B asserts the previous guess and its feedback as ASP facts via
clingo.Control.add(program_name: str, program_arguments: tuple, asp_code_snippet: str) -> None
- Computer B grounds via
clingo.Control.ground(programs: list) -> None
- Computer B solves via
clingo.Control.solve(on_model=lambda model: None) -> None
- Computer B inspects the answer set via the
on_model
callback
- Computer B sends another guess based on what clingo predicts the solution is
- Computer A computes feedback
- Computer B destroys the previous
clingo.Control
instance, creates a new one, and continues with all of the steps in item 3.
However, looking at item 5, we thought that destroying a clingo instance just to recreate it is inefficient. I am trying to figure out how to maintain one instance of clingo.
Steps taken to resolve problem
So, to replace item 5, I configured Computer B to add the next guess and its feedback via clingo.Control.add(program_name: str, program_arguments: tuple, asp_code_snippet: str) -> None
and to ground everything again so that the assertions could be realized by clingo. However when I do this, I get ambiguous RuntimeError
errors with the message redefinition of atom <'_', 20>
. I have narrowed its cause to a generative clause like the one below.
After encountering this, it occurred to me that I was grounding too much and cut back to grounding everything only once. However, when doing this, the clingo model becomes stupid in response to the second guess and its feedback. It begins to think that every possible permutation is a valid guess and ignores the constraints put in place, which leads to believe I am now not grounding enough.
However, I cannot find a happy medium. I figure I am not understanding something about the grounding fully. Do any of you know what's up with this?
Steps to reproduce errors
Errors of the form: RuntimeError: redefinition of atom <'XXX', YYY>
Given the clingo test file:
#program program_a.
foo(1..3).
#program program_b.
bar(1..3).
#program program_c.
1 { foobar(Foo, Bar) : bar(Bar) } 1 :- foo(Foo).
#program program_d.
:- foobar(1, 1).
#program program_e.
barbar(A, B) :- foobar(A, B).
Run the following Python 3 script:
#!/usr/bin/python3
"""
Tests which ASP programs cause problems when grounding twice.
This program grounds all ASP programs once, then tests every
combination of ASP programs with further grounding.
Note: this script uses Python 3.
``clingo --version``::
clingo version 5.2.0
Address model: 64-bit
libgringo version 5.2.0
Configuration: with Python 3.5.2, with Lua 5.3.1
libclasp version 3.3.0 (libpotassco version 1.0.0)
Configuration: WITH_THREADS=1
Copyright (C) Benjamin Kaufmann
License: The MIT License <https://opensource.org/licenses/MIT>
"""
import itertools
import logging
import os
from collections import namedtuple
import clingo
ASPProgram = namedtuple('ASPProgram', 'name, arguments')
logging.basicConfig(level=logging.INFO, filename='clingo.log')
def log_answer_set(clingo_model):
# type: (clingo.Model) -> None
logging.info('New model')
answer_set = model.symbols(shown=True)
logging.debug('Answer set: %s', answer_set)
for answer in answer_set:
logging.debug(answer)
if __name__ == '__main__':
logging.info('=====Start of test=====')
asp_programs = (
ASPProgram(name='program_a', arguments=tuple()),
ASPProgram(name='program_b', arguments=tuple()),
ASPProgram(name='program_c', arguments=tuple()),
ASPProgram(name='program_d', arguments=tuple()),
ASPProgram(name='program_e', arguments=tuple()),
)
program_combinations_that_yield_no_errors = list()
for combination_length in reversed(range(1, len(asp_programs) + 1)):
print('Trying combinations of length %d' % combination_length)
for programs in itertools.combinations(asp_programs, combination_length):
try:
logging.info('Grounding once')
clingo_control = clingo.Control(['-t %d' % os.cpu_count()])
clingo_control.load('./path/to/test_clingo_file.lp')
clingo_control.ground(asp_programs) # Ground all programs first time
logging.info('Grounded once without crashing!')
clingo_control.solve(on_model=log_answer_set)
try:
logging.info('Grounding twice')
clingo_control.ground(programs)
logging.info('Grounded twice without crashing!')
clingo_control.solve(on_model=log_answer_set)
program_combinations_that_yield_no_errors.append(programs)
except RuntimeError as error:
logging.info('Failed on second grounding with %s', programs)
logging.exception(error)
except RuntimeError as error:
logging.error('Failed on first grounding with %s!' % asp_programs)
raise error
if len(program_combinations_that_yield_no_errors) > 0:
print('Found combination of ASP programs that did not yield errors!')
break
best_program_combination = tuple()
print()
for combination_number, combination in enumerate(program_combinations_that_yield_no_errors):
logging.info('Working combination %d: %s', combination_number + 1, combination)
print('Combination %d:' % (combination_number + 1))
for asp_program in combination:
print(asp_program.name)
print()
if len(combination) > len(best_program_combination):
best_program_combination = combination
logging.info('Longest combination: %s', best_program_combination)
logging.info('=====End of test=====')
The above script can ground program_a
, program_b
, and program_d
a second time without errors. If grounding all of them twice, the following error is raised:
RuntimeError: redefinition of atom <'barbar(1,2)',12>