pythonz3z3py

Confused on a simple SAT problem using Python z3


Problem Statement

Five trucks, including two fridge trucks, have to deliver the following 131 items to a grocery in Brisbane using no more than twenty pallets.

Any solution to the problem should satisfy all of the following constraints.


I am finding it difficult to find a satisfiable solver for the problem.

Confused about if I should map each item to the objects or if i should just focus on the pallets and trucks. Total beginner to z3 and satisfiability problems.

from z3 import *

total_quantity = 131
max_trucks = 5
max_pallets = 20

items = {
    "bananas": (21, 2),
    "tomatoes": (20, 1.8),
    "nuts": (25, 1.2),
    "candies": (30, 1),
    "milk": (35, 0.2)
}

p = Function('p', IntSort(), IntSort(), BoolSort()) #confirms if item is in pallet
fridge = Function('fridge', IntSort(), BoolSort()) #confirms if its a fridge truck

s = Solver()

# Set Fridge Trucks
constraint_data = [fridge(i) for i in range(max_trucks)]
s.add(Sum(constraint_data) == 2)

# Set Maximum number of pallets as 20
constraint_data = Or([p(item,pallet) for item in range(total_quantity) for pallet in range(max_pallets)])
s.add(Sum(constraint_data) <= max_pallets)

# Constraint1 - Set Pallet Capacity
for pallet in range(max_pallets):
    quantity_constraint_data = [p(item, pallet) for item in range(total_quantity)]
    weight_constraint_data = []


Solution

  • This is my Z3py model for the problem. The model ran in 519 seconds on my machine.

    from z3 import (
        And, If, IntSort, BoolSort, Function, Solver, Sum, Not, Implies, sat
    )
    import time
    
    started = time.time()
    
    def get_packing(typ, qty):
        packing = {
            "bananas": "bunch",
            "tomatoes": "bunch",
            "nuts": "bag",
            "candies": "bag",
            "milk": "pack"
        }
        packing_text = (packing[typ] + "s" if packing[typ] != "bunch" else packing[typ] + "es") if qty > 1 else packing[typ]
        return f"{qty} {packing_text}"
    
    items = {
        "bananas": (21, 2),
        "tomatoes": (20, 1.8),
        "nuts": (25, 1.2),
        "candies": (30, 1),
        "milk": (35, 0.2)
    }
    
    total_quantity = 131
    max_trucks = 5
    max_pallets = 20
    
    # Mapping items to specific ranges
    '''
        Dictionary to create global mappig for items
        {
            'bananas': (0, 21),
            'tomatoes': (21, 41),
            'nuts': (41, 66),
            'candies': (66, 96),
            'milk': (96, 131)
        }
    '''
    item_ranges = {}
    current_index = 0
    for t, (count, weight) in items.items():
        item_ranges[t] = (current_index, current_index + count)
        current_index += count
    
    # Mapping item to corresponding type and weight
    '''
        Each item is mapped its weight and type for easy reference.
    '''
    item_type = {}
    item_weight = {}
    for t, (count, weight) in items.items():
        start, end = item_ranges[t]
        for i in range(start, end):
            item_type[i] = t
            item_weight[i] = weight
    
    p = Function('p', IntSort(), IntSort(), BoolSort()) # confirms if item is in pallet
    t = Function('t', IntSort(), IntSort(), BoolSort()) # confirms if pallet is in truck
    fridge = Function('fridge', IntSort(), BoolSort())  # confirms if it's a fridge truck
    
    s = Solver()
    
    # Each item must be on exactly one pallet.
    for i in range(total_quantity):
        s.add(Sum([If(p(i, j), 1, 0) for j in range(max_pallets)]) == 1)
    
    # Each pallet used assigned to exactly truck.
    for j in range(max_pallets):
        s.add(Sum([If(t(j, m), 1, 0) for m in range(max_trucks)]) <= 1) # max 1 truck
        s.add(Implies(Sum([If(p(i, j), 1, 0) for i in range(total_quantity)]) > 0,
                      Sum([If(t(j, m), 1, 0) for m in range(max_trucks)]) == 1)) 
    
    # Set Fridge Trucks
    s.add(Sum([If(fridge(m), 1, 0) for m in range(max_trucks)]) == 2)
    
    # Set Pallet Capacity
    '''
        Total number of items should be less than 8.
        Total weight of items on pallet, derived from the wieght mapping, 
        should be less than 10.
    '''
    for j in range(max_pallets):
        s.add(Sum([If(p(i, j), 1, 0) for i in range(total_quantity)]) <= 8)
        s.add(Sum([If(p(i, j), item_weight[i], 0) for i in range(total_quantity)]) <= 10)
    
    # Candies and nuts cannot be on the same pallet.
    '''
        Iterated through the candy and nuts indices to check if both of them
        are not in the same pallet
    '''
    candies_range = range(item_ranges["candies"][0], item_ranges["candies"][1])
    nuts_range = range(item_ranges["nuts"][0], item_ranges["nuts"][1])
    for j in range(max_pallets):
        for ci in candies_range:
            for ni in nuts_range:
                s.add(Not(And(p(ci, j), p(ni, j))))
    
    # Set Truck Capacity
    '''
        The total quantity and weight of the objects in each pallet that is 
        assigned to the respective truck is checked to satisfy the constraint.
    '''
    for m in range(max_trucks):
        s.add(Sum([If(t(j, m), Sum([If(p(i, j), 1, 0) for i in range(total_quantity)]), 0)
                   for j in range(max_pallets)]) <= 30)
        s.add(Sum([If(t(j, m), Sum([If(p(i, j), item_weight[i], 0) for i in range(total_quantity)]), 0)
                   for j in range(max_pallets)]) <= 40)
    
    # Milk must be on a fridge truck.
    '''
        Directly assigning Milk to Fridge trucks was causing my solver to be heavy.
        So I decided on going on with the idea that if milk is present in a pallet,
        then the pallet must be assigned to the fridge truck.
    '''
    milk_range = range(item_ranges["milk"][0], item_ranges["milk"][1])
    for j in range(max_pallets):
        milk_on_pallet = Sum([If(p(i, j), 1, 0) for i in milk_range])
        for m in range(max_trucks):
            s.add(Implies(And(milk_on_pallet > 0, t(j, m)), fridge(m)))
    
    # Each truck can carry no more than 5 bunches of bananas.
    '''
        Sum of all the bananas in all the pallets assigned to the truck is
        constrained to fall below 5.
    '''
    banana_range = range(item_ranges["bananas"][0], item_ranges["bananas"][1])
    for m in range(max_trucks):
        s.add(Sum([If(t(j, m), Sum([If(p(i, j), 1, 0) for i in banana_range]), 0)
                   for j in range(max_pallets)]) <= 5)
    
    if s.check() == sat:
        model = s.model()
        print("Model present")
    
        total_output = {item: 0 for item in items.keys()}
        with open("model.txt", "w") as f:
            for m in range(max_trucks):
                truck_type = "Fridge" if model.evaluate(fridge(m)) else "Normal"
                print(f"\n{truck_type} Truck {m+1}:", file=f)
                pallet_num = 0
                for j in range(max_pallets):
                    if model.evaluate(t(j, m)):
                        pallet_num += 1
                        pallet_items = {item: 0 for item in items.keys()}
                        for i in range(total_quantity):
                            if model.evaluate(p(i, j)):
                                typ = item_type[i]
                                pallet_items[typ] += 1
                                total_output[typ] += 1
                        print(f"  Pallet {pallet_num}:", file=f)
                        for typ, count in pallet_items.items():
                            if count > 0:
                                print(f"    {typ}: {get_packing(typ, count)}", file=f)
            print("\n\nTotal Output:", total_output, file=f)
    else:
        print("Solver Unsatisfiable")
    
    
    elapsed = time.time() - started
    print(f"Execution time: {int(elapsed)} seconds")  
    

    This is the model produced by the solver.

    ----------------------------  MODEL PRODUCED  ----------------------------
    Normal Truck 1:
      Pallet 1:
        bananas: 2 bunches
        tomatoes: 1 bunch
        nuts: 3 bags
      Pallet 2:
        bananas: 1 bunch
        tomatoes: 1 bunch
        candies: 6 bags
      Pallet 3:
        bananas: 2 bunches
        tomatoes: 1 bunch
        candies: 4 bags
      Pallet 4:
        tomatoes: 1 bunch
        nuts: 6 bags
    
    Fridge Truck 2:
      Pallet 1:
        milk: 8 packs
      Pallet 2:
        nuts: 6 bags
        milk: 2 packs
      Pallet 3:
        milk: 8 packs
      Pallet 4:
        bananas: 1 bunch
        tomatoes: 2 bunches
        nuts: 1 bag
        milk: 2 packs
    
    Fridge Truck 3:
      Pallet 1:
        milk: 8 packs
      Pallet 2:
        candies: 7 bags
        milk: 1 pack
      Pallet 3:
        bananas: 4 bunches
        tomatoes: 1 bunch
        milk: 1 pack
      Pallet 4:
        bananas: 1 bunch
        tomatoes: 1 bunch
        nuts: 1 bag
        milk: 5 packs
    
    Normal Truck 4:
      Pallet 1:
        bananas: 2 bunches
        tomatoes: 1 bunch
        candies: 1 bag
      Pallet 2:
        bananas: 1 bunch
        tomatoes: 3 bunches
        nuts: 2 bags
      Pallet 3:
        bananas: 2 bunches
        tomatoes: 1 bunch
        nuts: 3 bags
    
    Normal Truck 5:
      Pallet 1:
        tomatoes: 2 bunches
        candies: 6 bags
      Pallet 2:
        bananas: 4 bunches
        tomatoes: 1 bunch
      Pallet 3:
        bananas: 1 bunch
        tomatoes: 2 bunches
        nuts: 3 bags
      Pallet 4:
        tomatoes: 2 bunches
        candies: 6 bags
    
    Total Output: {'bananas': 21, 'tomatoes': 20, 'nuts': 25, 'candies': 30, 'milk': 35}
    ------------------------------------------------------------------------------------