Search code examples
pythonz3z3py

Python using z3 library


I need to find all possible paths from the initial position to the destination across a frozen lake. The frozen lake is represented by a matrix, where each cell consists of either a 1 (safe cell) or a 0 (unsafe cell). I want to write a code using the Z3 theorem prover to find a valid path from the initial position to the destination without using any unsafe cells.

Here is an e# Example usages

In this matrix, cell D1 is the starting position, and C3 is the destination position. Cells A1, A2, A3, B1, B3, C1, C3, and D1 are safe cells (marked in orange), whereas cells A4, B2, B4, C2, C4, D2, D3, and D4 are unsafe cells (marked in red). There is only one valid path from the initial position to the destination position, given by D1 -> C1 -> B1 -> A1 -> A2 -> A3 -> B3 -> C3.

To solve this problem, I am using the Z3 theorem prover in Python. Here is the code:

from z3 import *

def find_path(matrix, start, end):
    # Define the dimensions of the matrix
    rows = len(matrix)
    cols = len(matrix[0])

    # Create Z3 variables for each cell in the matrix
    cells = [[Bool(f'cell_{i}_{j}') for j in range(cols)] for i in range(rows)]

    # Create a Z3 solver
    s = Solver()

    # Add constraints for the starting and destination positions
    s.add(cells[start[0]][start[1]])  # Starting position
    s.add(cells[end[0]][end[1]])  # Destination position

    # Add constraints for safe and unsafe cells
    for i in range(rows):
        for j in range(cols):
            if matrix[i][j] == 0:
                s.add(Not(cells[i][j]))  # Unsafe cell
            else:
                s.add(cells[i][j])  # Safe cell

    # Add constraints for adjacent cells
    for i in range(rows):
        for j in range(cols):
            adjacent_cells = []
            if i > 0:
                adjacent_cells.append(cells[i-1][j])  # Cell above
            if i < rows - 1:
                adjacent_cells.append(cells[i+1][j])  # Cell below
            if j > 0:
                adjacent_cells.append(cells[i][j-1])  # Cell to the left
            if j < cols - 1:
                adjacent_cells.append(cells[i][j+1])  # Cell to the right
            s.add(Implies(cells[i][j], Or(adjacent_cells)))  # Only connect to adjacent safe cells
    
    print(s)
    # Check if there is a valid path from the starting position to the destination
    if s.check() == sat:
        model = s.model()
        path = []
        for i in range(rows):
            for j in range(cols):
                if model[cells[i][j]]:
                    path.append((i, j))
        s.reset()
        return path
    else:
        return None

# Example usage
matrix = [[1, 1, 1, 0],
          [1, 0, 1, 0],
          [1, 0, 1, 0],
          [1, 0, 0, 0]]
start = (3, 0)
end = (2, 2)

path = find_path(matrix, start, end)
if path:
    print("Valid path found:")
    for cell in path:
        print(f"({chr(ord('A') + cell[0])}{cell[1] + 1})")
else:
    print("No valid path found.")

In this code, I first define the dimensions of the matrix and create Z3 variables for each cell. Then, I add constraints for the starting and destination positions, as well as for safe and unsafe cells. Finally, I add constraints for adjacent cells to ensure that the path only connects to adjacent safe cells. I use the Z3 solver to check if there is a valid path from the starting position to the destination, and if so, I retrieve the path.

However, when I run this code, I am not getting the expected output. It seems that there is an issue with the constraints or the way I am modeling the problem.

What actually is wrong, and how do I fix it?


Solution

  • You don't need to create symbolic values for the matrix itself: It's a constant array. Instead, create variables corresponding to the path, and tell z3 that this should be a valid walk through this matrix. Then z3 will find an assignment to the path variables that'll solve the maze for you. Something like:

    from z3 import *
    
    def find_path(matrix, start, end):
    
        # Define the dimensions of the matrix
        rows = len(matrix)
        cols = len(matrix[0])
    
        # symbolic look-up into the matrix:
        def lookup(x, y):
            val = 0
            for r in range(rows):
                for c in range(cols):
                    val = If(And(x == r, y == c), matrix[r][c], val)
            return val
    
        # Create a path, there are at most rows*cols elements
        path = []
        for r in range(rows):
            for c in range(cols):
                path.append([FreshInt(), FreshInt()])
    
        s = Solver()
    
        # assert that the very first element of the path is the start position:
        s.add(path[0][0] == start[0])
        s.add(path[0][1] == start[1])
    
        # for each remaining path-element, make sure either we reached the end, or it's a valid move
        prev = path[0]
        done = False
        for p in path[1:]:
            valid1 = And(p[0] >= 0, p[0] < rows, p[1] >= 0, p[1] < cols)  # Valid coords
    
            valid2 = Or( And(p[0] == prev[0]-1, p[1] == prev[1])     #    Go up
                       , And(p[0] == prev[0]+1, p[1] == prev[1])     # or Go down
                       , And(p[0] == prev[0],   p[1] == prev[1]+1)   # or Go right
                       , And(p[0] == prev[0],   p[1] == prev[1]-1))  # or Go left
    
            valid3 = lookup(p[0], p[1]) == 1 # The cell is safe
    
            # Either we're done, or all valid conditions must hold
            s.add(Or(done, And(valid1, valid2, valid3)))
    
            prev = p
    
            # We're done if p is the end position:
            done = Or(done, And(p[0] == end[0], p[1] == end[1]))
    
        # Make sure the path is unique:
        for i in range(len(path)):
            for j in range(len(path)):
                if j <= i:
                    continue
                s.add(Or(path[i][0] != path[j][0], path[i][1] != path[j][1]))
    
        # Compute the path:
        if s.check() == sat:
            model = s.model()
            walk = []
            for p in path:
                cur = [model[p[0]].as_long(), model[p[1]].as_long()]
                walk.append(cur)
                if (cur[0] == end[0] and cur[1] == end[1]):
                    break
            return walk
        else:
            return None
    
    # Example usage
    matrix = [[1, 1, 1, 0],
              [1, 0, 1, 0],
              [1, 0, 1, 0],
              [1, 0, 0, 0]]
    start = (3, 0)
    end = (2, 2)
    
    path = find_path(matrix, start, end)
    if path:
        print("Valid path found:")
        for cell in path:
            print(f"({chr(ord('A') + cell[0])}{cell[1] + 1})")
    else:
        print("No valid path found.")
    

    When run, this prints:

    Valid path found:
    (D1)
    (C1)
    (B1)
    (A1)
    (A2)
    (A3)
    (B3)
    (C3)
    

    which is the solution you were looking for.