aig-upf / tarski

Tarski - An AI Planning Modeling Framework
Apache License 2.0
59 stars 18 forks source link

Setting the cost function in the initial state appears to be broken in the current version #138

Closed ctpelok77 closed 2 years ago

ctpelok77 commented 2 years ago

The following miconic example works in version 0.6.0 but throws an exception in 0.8.2. The issue seems to be with init.set(cost, 0).

#! /usr/bin/env python

import tarski
from tarski.theories import Theory
import tarski.fstrips as fs
from tarski.io import fstrips as iofs
from tarski.io import FstripsWriter
from tarski.syntax import land

def constant_cost(problem, cost):
    return iofs.AdditiveActionCost(problem.language.constant(cost, problem.language.get_sort('Integer')))

def get_domain_problem():

    lang = iofs.language("miconic", theories=[Theory.EQUALITY, Theory.ARITHMETIC])
    cost = lang.function("total-cost", lang.Real)

    # Types
    passenger = lang.sort("passenger")
    floor = lang.sort("floor")
    elevator = lang.sort("elevator")

    # Predicates
    _origin = lang.predicate("ORIGIN", passenger, floor)
    _destination = lang.predicate("DESTINATION", passenger, floor)
    _above = lang.predicate("ABOVE", floor, floor)
    _boarded = lang.predicate("boarded", passenger, elevator)
    _served = lang.predicate("served", passenger)
    _at = lang.predicate("at", elevator, floor)

    # Problem instance
    problem = iofs.create_fstrips_problem(
                domain_name="miconic-domain",
                problem_name="miconic-problem",
                language=lang,
            )

    problem.metric(cost(), fs.OptimizationType.MINIMIZE)

    p = lang.variable("p", passenger)
    e = lang.variable("e", elevator)
    f = lang.variable("f1", floor)
    f2 = lang.variable("f2", floor)

    up = problem.action("up", [e, f, f2], 
                        precondition=_at(e, f) & _above(f, f2),
                        effects=[fs.DelEffect(_at(e, f)), fs.AddEffect(_at(e, f2))]
                        , cost= constant_cost(problem, 1) 
                        )

    down = problem.action("down", [e, f, f2], 
                        precondition=_at(e, f) & _above(f2, f),
                        effects=[fs.DelEffect(_at(e, f)), fs.AddEffect(_at(e, f2))]
                        , cost= constant_cost(problem, 1) 
                        )

    board = problem.action("board", [f, p, e], 
                        precondition=_at(e, f) & _origin(p, f),
                        effects=[fs.AddEffect(_boarded(p, e))]
                        , cost= constant_cost(problem, 0) 
                        )

    depart = problem.action("depart", [f, p, e], 
                        precondition=_at(e, f) & _destination(p, f) & _boarded(p, e),
                        effects=[fs.DelEffect(_boarded(p, e)), fs.AddEffect(_served(p))]
                        , cost= constant_cost(problem, 0) 
                        )

    ## --------------------------- ##
    # Instance related part 

    # Objects

    passengers = [lang.constant(f'passenger{k}', passenger) for k in range(1,7)]
    e1, e2 = [lang.constant(f'elevator{k}', elevator) for k in range(1,3)]
    floors = [lang.constant(f'floor{k}', floor) for k in range(1,5)]

    # Initial state and goal

    init = tarski.model.create(lang)
    init.set(cost, 0)

    for first, second in zip(floors, floors[1:]):    
        init.add(_above(first,second))

    init.add(_origin(passengers[0],floors[0]))
    init.add(_origin(passengers[1],floors[3]))
    init.add(_origin(passengers[2],floors[2]))
    init.add(_origin(passengers[3],floors[0]))
    init.add(_origin(passengers[4],floors[0]))
    init.add(_origin(passengers[5],floors[2]))

    init.add(_destination(passengers[0],floors[3]))
    init.add(_destination(passengers[1],floors[1]))
    init.add(_destination(passengers[2],floors[3]))
    init.add(_destination(passengers[3],floors[2]))
    init.add(_destination(passengers[4],floors[1]))
    init.add(_destination(passengers[5],floors[0]))

    init.add(_at(e1,floors[0]))
    init.add(_at(e2,floors[3]))

    problem.init = init

    # problem.goal = [_served(p) for p in passengers]
    problem.goal = land(*[_served(p) for p in passengers], flat=True)

    writer = FstripsWriter(problem)
    writer.write("domain-miconic.pddl", "problem-miconic.pddl")

    return writer.print_domain(), writer.print_instance()

domain, problem = get_domain_problem()
print(domain)
print(problem)
ctpelok77 commented 2 years ago

Ok, just figured out that if I change cost to cost(), it works. So, I guess it actually fixes the previous faulty syntax? Please feel free to close the issue.

miquelramirez commented 2 years ago

Hi @ctpelok77 ,

let me explain what is the metaphor here, it should help going forward.

Model objects map onto the notion of interpretation or semantic structure in logic. That is, a definition of an assignment (a function) that maps well-formed formulas to objects (truth values, numbers, etc.). So when one writes

init.set(cost, 0)

it is attempted to define the constant cost as 0. This is not allowed, because constants have fixed meaning (or "denotation" in logic-speak). On the other hand, it is allowed to associate a 0-ary function cost() to a value, e.g.

init.set(cost(), 0)

You may ask why we established this distinction... and it is mostly a matter of internal consistency, that helps us use Python to distinguish between objects (that conform the domain of functions) and terms (like x + y and cost()). @gfrances and me had a bit of a back and forth over this, as too much purity gets often in the way of getting things done, but with the perspective of the years since we came up with this solution, I think it has been vindicated by how straightforward results formulating constraints for SMT or CP from the structural elements of planning instances.

I hope this helps.

PS: "predicates" (which in my view are essentially functions mapping onto Booleans a.k.a. the Z_2 semi-ring) work a bit differently, as you have to explicitly define what the tuples/vectors/points in the domain of the predicate are true.

Miquel.