buguroo / pyknow

PyKnow: Expert Systems for Python
GNU Lesser General Public License v3.0
470 stars 141 forks source link

Rules called on already retracted facts #41

Closed nilp0inter closed 5 years ago

nilp0inter commented 5 years ago

Rules are being called on already retracted facts.

P = [[None, 2, None, 6, None, 8, None, None, None],
     [5, 8, None, None, None, 9, 7, None, None],
     [None, None, None, None, 4, None, None, None, None],
     [3, 7, None, None, None, None, 5, None, None],
     [6, None, None, None, None, None, None, None, 4],
     [None, None, 8, None, None, None, None, 1, 3],
     [None, None, None, None, 2, None, None, None, None],
     [None, None, 9, 8, None, None, None, 3, 6],
     [None, None, None, 3, None, 6, None, 9, None]]

class Possible(Fact):
    pass

class Solver(KnowledgeEngine):
    @DefFacts()
    def init_puzzle(self):
        for x, row in enumerate(P):
            for y, cell in enumerate(row):
                block = ((y // 3) * 3) + (x // 3)
                if cell is None:
                    yield Fact(value=None, y=y, x=x, block=block)
                    for i in range(1, 10):
                        yield Possible(value=i, y=y, x=x, block=block)
                else:
                    yield Fact(value=cell, y=y, x=x, block=block)

    @Rule(Fact(value=~L(None) & MATCH.v, y=MATCH.y),
          AS.p << Possible(value=MATCH.v, y=MATCH.y))
    def discarded_by_column(self, p):
        self.retract(p)

    @Rule(Fact(value=~L(None) & MATCH.v, x=MATCH.x),
          AS.p << Possible(value=MATCH.v, x=MATCH.x))
    def discarded_by_row(self, p):
        self.retract(p)

    @Rule(Fact(value=~L(None) & MATCH.v, block=MATCH.b),
          AS.p << Possible(value=MATCH.v, block=MATCH.b))
    def discarded_by_block(self, p):
        self.retract(p)

    @Rule(AS.cell << Fact(value=None, x=MATCH.x, y=MATCH.y, block=MATCH.b),
          Possible(value=MATCH.v, x=MATCH.x, y=MATCH.y, block=MATCH.b),
          NOT(Possible(value=~MATCH.v, x=MATCH.x, y=MATCH.y, block=MATCH.b)))
    def only_one_possible(self, cell, v):
        self.retract(cell)
        self.declare(Fact(value=v, x=cell['x'], y=cell['y'], block=cell['block']))

    @Rule(AS.cell << Fact(value=None, x=MATCH.x, y=MATCH.y, block=MATCH.b),
          Possible(value=MATCH.v, x=MATCH.x, y=MATCH.y, block=MATCH.b),
          NOT(Possible(value=MATCH.v, x=~MATCH.x, y=~MATCH.y, block=MATCH.b)))
    def unique_candidate_block(self, cell, v):
        self.retract(cell)
        self.declare(Fact(value=v, x=cell['x'], y=cell['y'], block=cell['block']))

    @Rule(AS.cell << Fact(value=None, x=MATCH.x, y=MATCH.y, block=MATCH.b),
          Possible(value=MATCH.v, x=MATCH.x, y=MATCH.y, block=MATCH.b),
          NOT(Possible(value=MATCH.v, x=~MATCH.x, y=MATCH.y, block=~MATCH.b)))
    def unique_candidate_col(self, cell, v):
        self.retract(cell)
        self.declare(Fact(value=v, x=cell['x'], y=cell['y'], block=cell['block']))

    @Rule(AS.cell << Fact(value=None, x=MATCH.x, y=MATCH.y, block=MATCH.b),
          Possible(value=MATCH.v, x=MATCH.x, y=MATCH.y, block=MATCH.b),
          NOT(Possible(value=MATCH.v, x=MATCH.x, y=~MATCH.y, block=~MATCH.b)))
    def unique_candidate_row(self, cell, v):
        self.retract(cell)
        self.declare(Fact(value=v, x=cell['x'], y=cell['y'], block=cell['block']))

    @Rule(Fact(value=~L(None) & MATCH.v, x=MATCH.x, y=MATCH.y, block=MATCH.b),
          AS.p << Possible(value=~MATCH.v, x=MATCH.x, y=MATCH.y, block=MATCH.b))
    def remove_other_candidates(self, p):
        self.retract(p)

watch('RULES', 'FACTS')
s = Solver()
s.reset()
s.run()

This code raises the following exception:

INFO:pyknow.watchers.FACTS: <== <f-130>: Possible(value=2, y=1, x=2, block=0)
INFO:pyknow.watchers.RULES:FIRE 531 unique_candidate_col: <f-130>, <f-128>
Traceback (most recent call last):
  File "sudoku.py", line 95, in <module>
    s.run()
  File "/home/nil/.local/share/virtualenvs/sudoku-VtHvu9jk/lib/python3.7/site-packages/pyknow/engine.py", line 168, in run
    for k, v in activation.context.items()
  File "/home/nil/.local/share/virtualenvs/sudoku-VtHvu9jk/lib/python3.7/site-packages/pyknow/rule.py", line 87, in __call__
    return self._wrapped(*args, **kwargs)
  File "sudoku.py", line 76, in unique_candidate_col
    self.retract(cell)
  File "/home/nil/.local/share/virtualenvs/sudoku-VtHvu9jk/lib/python3.7/site-packages/pyknow/engine.py", line 124, in retract
    self.facts.retract(idx_or_declared_fact)
  File "/home/nil/.local/share/virtualenvs/sudoku-VtHvu9jk/lib/python3.7/site-packages/pyknow/factlist.py", line 111, in retract
    raise IndexError('Fact not found.')
IndexError: Fact not found.

A similar exception is raised if the method modify is used instead of retract+declare.

ricardobur commented 5 years ago

Hi @nilp0inter,

I am using pyknow library to build some logic based on logic and facts. I have faced the same behavior as you explain in this issue. In fact, I had the same error as it seems in one point (not exactly when) the activations in the agenda have a factid that has being retracted (so it does not exists any more).

I have created on a fork the behavior that I would need, but I suppose the problem should be solved by ensuring the activation facts has the proper factid that stays in the facts list of the engine, no?

Thanks!

nilp0inter commented 5 years ago

This is maybe a regression after the last optimizations made this year. A couple of regression tests and a git bisect could help here.

lucasmpaim commented 5 years ago

+1 any updates on this?

lucasmpaim commented 5 years ago

for my case this decorator solves my problem:

def safe_access_fact(attr, fact_type):
    def func_receiver(func):
        def inner(*args, **kwargs):
            database = args[0]
            for fact in database.facts.items():
                if isinstance(fact[1], fact_type):
                    return func(*args,
                                **kwargs,
                                **{attr: fact[1]})
            return func(*args, **kwargs)
        return inner
    return func_receiver

And the use:

    @Rule(
        EXISTS(SystemPreCheck()),
        Common(current_state=QuestionsBlock.SYSTEM_PRE_CHECK),
        ~SystemPreCheck(have_error_within_time_limit=W()))
    @safe_access_fact('system', SystemPreCheck)
    def have_error_within_time_limit(self, system):
        read = auto_read([True, False])
        print(f'Existe defeito atribuído dentro do prazo?: {"y" if read else "n"}')
        self.modify(system, have_error_within_time_limit=read)

if I use the AS.* << operator, I receive a retracted fact, causing the error

nilp0inter commented 5 years ago

Already fixed in Experta