Search code examples
answer-set-programmingclingogringo

Only compute/solve a specific rule in clingo


Warning: ASP newbie.

Let's assume we have this simple program:

% crime scene

% Facts
present(harry). % was present on the crime scene
present(sally).
present(mary).
motive(harry). % has a motive
motive(sally). 
guilty(harry). 

% encoding / rules
innocent(Suspect) :- motive(Suspect), not guilty(Suspect).
witness(Suspect) :- present(Suspect), not motive(Suspect), not guilty(Suspect).

The output, as expected, is:

present(harry) present(sally) present(mary)
motive(harry) motive(sally) guilty(harry)
innocent(sally) witness(mary)

Is there a way to only compute a specific rule out of all the defined ones? I'm not talking about hiding an atom with the #show statement but really telling the solver to only compute the witness(Suspect) rule for example.

I assume writing two different programs is an answer but is there anything else?

For context, I use the Clingo Python API to execute an ASP program that contains dozens of rules. Those (independent) rules correspond to questions the user might want an answer to. For now, all of them are computed when executing the program and I filter the output to only process the atoms of interest once an answer is found.


Solution

  • I found the answer in the paper "A Tutorial on Hybrid Answer Set Solving with clingo" by Kaminski et al. (2017) [Archive here].

    Short answer

    Use the #program directive to partition a program into subprograms (blocks of rules). Then use scripting to only execute the subprogram you want.

    Concretely:

    % crime scene
    
    % Facts
    #program base. % NEW
    present(harry). 
    present(sally).
    present(mary).
    motive(harry). 
    motive(sally). 
    guilty(harry). 
    
    % encoding / rules
    #program groundInnocent. % NEW
    innocent(Suspect) :- motive(Suspect), not guilty(Suspect).
    #program groundWitness. % NEW
    witness(Suspect) :- present(Suspect), not motive(Suspect), not guilty(Suspect).
    

    Then, you can either include a script in the ASP code:

    #script(python) 
    def main(prg):
        prg.ground([("base", []), ("groundWitness", [])])
        prg.solve() 
    #end
    

    Or use the Clingo Python API:

    from clingo.control import Control
    def on_model(model):
        print ("result = ", model)
    
    if __name__ == '__main__':
        ctl = Control()
        ctl.configuration.solve.models = 1
        ctl.load("path_to_asp_file")
        ctl.ground([("base", []), ("groundWitness", [])])
        ctl.solve(on_model=on_model)
    

    Important:

    1. Note that facts are put in the base subprogram. And the base program is explicitly called before any other subprogram to ensure facts used in witness are grounded.
    2. This implies that subprograms must be independent. Here, both of our rules are independent. If we had witness(Suspect) :- present(Suspect), not innocent(Suspect)., the rule would not be grounded since innocent is outside the scope of the groundWitness subprogram and is not part of base either.

    See long answer below to understand why.

    Long answer

    Direct citation from section 3.1 ("A gentle introduction") of the paper mentioned above:

    [...] a program can be partitioned into several subprograms by means of the directive #program; it comes with a name and an optional list of parameters. [...] As an example, two subprograms base and acid(k) can be specified as follows:

    a(1).
    #program acid(k). 
    b(k).
    c(X,k) :- a(X).
    #program base.
    a(2).
    

    Note that base is a dedicated subprogram (with an empty parameter list): in addition to the rules in its scope, it gathers all rules not preceded by any #program directive. Hence, in the above example, the base subprogram includes the facts a(1) and a(2), although, only the latter is in the actual scope of the directive [...].

    Without further control instructions (see below), clingo grounds and solves the base subprogram only [...]. The processing of other subprograms such as acid(k) is subject to scripting control.

    For customized control over grounding and solving, a main routine (taking a control object representing the state of clingo as argument) can be supplied.

    #script(python) 
    def main(prg):
    prg.ground([("base",[])])
    prg.solve() 
    #end.
    

    While the above control program matches the default behavior of clingo, the one below ignores all rules in the base program but rather contains a ground instruction for acid(k) [...], where the parameter k is to be instantiated with the term 42.

    #script(python) 
    def main(prg):
    prg.ground([("acid",[42])])
    prg.solve()
    #end.
    

    Accordingly, the schematic fact b(k) is turned into b(42), no ground rule is obtained from c(X,k) :- a(X) due to lacking instances of a(X), and the solve command [...] yields a stable model consisting of b(42) only.

    Note that ground instructions apply to the subprograms given as arguments, while solve triggers reasoning w.r.t. all accumulated ground rules.