You’re writing a Python optimization problem using the Z3 solver, and everything is set to go smoothly. But, suddenly, a strange error pops up when you try to pass a Z3 array to a Python function within your Z3 objective. You stare at something like this: “TypeError: unhashable type: ‘ArrayRef’“. Why did this happen, and how do you fix it?
If you’ve spent time coding optimization problems involving symbolic variables with Z3, you’ve probably faced confusion with handling Z3-specific data types. Especially annoying is bridging the gap between Python lists, something you’re comfortable with, and Z3 arrays—which behave quite differently.
Understanding Z3 Arrays vs Python Lists
Z3 arrays aren’t exactly like Python lists. Python lists keep real data you can immediately read and manipulate, while Z3 arrays hold symbolic elements—expressions whose values aren’t fixed until the solver finds a solution. Imagine Z3 arrays as mysterious containers that only decide what’s inside once you’ve run your solver’s magic.
On the other hand, Python lists are straightforward. You use brackets “[]” to access elements like my_list[2]
instantly, without symbolic calculations. This fundamental difference makes it tricky to pass a Z3 expression directly to Python functions, especially those calling built-in methods expecting immediate access to real values.
The reason you’re encountering “TypeError: unhashable type: ‘ArrayRef’” is because you’re attempting to use a Z3 array directly in a context expecting Python-native types—like hashing or indexing directly through Python functions.
Why Call a Python Function Within a Z3 Objective?
Sometimes, your objective function isn’t just a simple combination of numeric values. Often it’s computed by your custom Python logic—for instance, calculating the longest paths between selected inspection states, analyzing certain patterns, or estimating real-world costs dynamically.
Your setup likely involves defining decision variables within Z3 representing whether certain inspection states are selected (e.g., using boolean decision variables). You keep track of these states using symbolic arrays. Then you need to evaluate your objectives through custom Python functions considering these symbolic decisions.
Consider this scenario: you have inspection states that you’re allowing your solver to pick or reject. The solver’s job is to minimize selection costs and, at the same time, minimize the difficulty of navigating between selected states. To measure complexity naturally, you’ve written a Python function longest_paths()
that finds the hardest routes between selected states. But trouble arises when you try to pass the Z3 array directly into this function.
Setting Up Your Z3 Environment and Variables
Let’s first clearly outline your environment and variables:
- First, import and set up Z3’s optimization solver.
- Next, define boolean decision variables, typically something like
x[i] = Bool(f"x_{i}")
, each representing “Is state i selected?” - Define a Z3 array,
valid_inspections
, that symbolically stores whether an inspection is chosen or not.
Here’s roughly how your initial code looks:
from z3 import *
optimizer = Optimize()
num_inspections = 5
# Decision variables
x = [Bool(f"x_{i}") for i in range(num_inspections)]
# Z3 array definition
valid_inspections = Array("valid_inspections", IntSort(), BoolSort())
for i in range(num_inspections):
optimizer.add(valid_inspections[i] == x[i])
You decide to pass this valid_inspections
array to your custom Python function. But here’s the issue: your Python function expects a regular list of straight-up Python booleans, not symbolic Z3 expressions.
What Went Wrong? Investigating the Traceback
When Z3 tries to evaluate your Python function, it fails because it isn’t designed to resolve symbolic values directly. Inside your objective, you probably have something like:
path_length = longest_paths(valid_inspections)
Z3 returns something like:
TypeError: unhashable type: 'ArrayRef'
This error occurs because you’re mixing symbolic Z3 expressions directly in a Python function expecting concrete data structures. Python is naturally confused—Z3 arrays aren’t just lists, they’re symbolic references that Python cannot directly hash or manipulate straightforwardly.
How to Fix This Error
To correct this, you’ll need to bridge the gap by properly extracting symbolic values and passing them as Python-native lists in a context that Z3 can interpret.
The usual workaround involves converting the Z3 array values into Python lists after solving, or better yet, restructuring your objective directly as Z3 expressions.
A Better Way—Evaluate After Solver Runs
Instead of invoking your Python functions inside the Z3 objective itself (which will inevitably lead to issues with symbolic references), a recommended practice is:
- Define Z3-only objective functions clearly, without calling Python-native functions directly.
- Solve your Z3 optimization problem first.
- Extract concrete solutions from Z3’s output and THEN pass these concrete solutions into your custom Python functions.
Here’s how you would restructure your code properly:
Step 1: Clearly Define Z3-Only Objective and Constraints
# Assume cost is a predefined numeric list
selection_costs = [10, 15, 20, 5, 8]
# Objective: minimize cost of selected states
total_cost = Sum([If(x[i], selection_costs[i], 0) for i in range(num_inspections)])
optimizer.minimize(total_cost)
# Constraints (may add more if needed)
# optimizer.add(... constraints here ...)
Step 2: Run Solver and Get Concrete Results
if optimizer.check() == sat:
solution = optimizer.model()
selected_states = [solution.evaluate(x[i]) for i in range(num_inspections)]
# Convert to Python boolean list
selected_states_bool = [bool(str(v) == "True") for v in selected_states]
else:
print("No solution found!")
Step 3: Now Call Python Function With Safe, Concrete Values
# Now safe to call your Python function
path_length = longest_paths(selected_states_bool)
print("Longest Path:", path_length)
Now your Python function receives the concrete list it expects, no symbolic headaches.
Running Solver and Getting Results Clearly
Now that your optimization is correctly set up, you can confidently display results:
- Extract selected inspections after solver finishes.
- Compute total costs based on selections.
- Display results clearly.
Here’s what the final extraction looks like neatly:
selected_indices = [i for i, val in enumerate(selected_states_bool) if val]
final_selection_cost = sum(selection_costs[i] for i in selected_indices)
print(f"Selected inspection states: {selected_indices}")
print(f"Total Selection Cost: {final_selection_cost}")
print(f"Longest Path Value: {path_length}")
This structured approach ensures your Z3 optimization smoothly integrates with your Python logic.
Final Thoughts on Bridging Z3 Arrays and Python Lists
Handling symbolic solver types like Z3 arrays alongside Python-native data structures can feel tricky—but understanding their fundamental differences greatly simplifies troubleshooting.
Always clearly separate your solver’s symbolic computation stage from your Python-native logic. This ensures cleaner code and reduces confusing type errors. For more tips on solving optimization problems clearly in Python, check out my related articles on the Python tutorials category.
Have you encountered any challenges integrating symbolic solvers with Python functions before? Share your experiences in the comments below!
0 Comments