python-3.xor-toolsconstraint-programmingcp-satconvex-polygon

OR TOOLS SAT Ray tracing algorithm and toggle a boolean


I guys, I am trying to implement a ray tracing algorithm to check if a point lies inside a polygon. I got this python code and I am trying to implement the following part in CP-SAT

int pnpoly(int nvert, double *vertx, double *verty, double testx, double testy)
{
  int i, j, c = 0;
  for (i = 0, j = nvert-1; i < nvert; j = i++) {
    if ( ((verty[i]>testy) != (verty[j]>testy)) &&
     (testx < (vertx[j]-vertx[i]) * (testy-verty[i]) / (verty[j]-verty[i]) + vertx[i]) )
       c = !c;
 }

here you can see that there are a lot of conditions to check inside if().

What I have tried I tried to implement the same code for 2 points and 1 test point using CP_SAT and it looks something like this:

from ortools.sat.python import cp_model
model = cp_model.CpModel()

# 2 points
vert_x1 = model.NewIntVar(1, 1, 'x1')
vert_y1 = model.NewIntVar(4, 4, 'y1')

vert_x2 = model.NewIntVar(6, 6, 'x2')
vert_y2 = model.NewIntVar(0, 0, 'y2')

# 1 test point
testx = model.NewIntVar(2, 2, 'xc')
testy = model.NewIntVar(2, 2, 'yc')

result = model.NewBoolVar("result")

# booleans for y-axis conditions
b1 = model.NewBoolVar('b1')
b2 = model.NewBoolVar('b2')
b3 = model.NewBoolVar('b3')

#booleans for x-axis conditions
c1 = model.NewBoolVar('c1')

# booleans for final decision
d1 = model.NewBoolVar('d1')
d2 = model.NewBoolVar('d2')

#temp_variable for x_conditions
diff_1 = model.NewIntVar(-100, 100, 'diff_1')
diff_2 = model.NewIntVar(-100, 100, 'diff_2')
diff_3 = model.NewIntVarFromDomain(cp_model.Domain.FromIntervals([[-100, -1]]), 'diff_3')
add_1 = model.NewIntVar(-100, 100, 'add_1')
div_1 = model.NewIntVar(-100, 100, 'div_1')
prod_1 = model.NewIntVar(-1000, 1000, 'prod_1')

"""
(verty[i] > testy) && (verty[j] > testy)
"""

#check conditions for y_axis
model.Add(vert_y1 > testy).OnlyEnforceIf(b1)
model.Add(vert_y1 <= testy).OnlyEnforceIf(b1.Not())

model.Add(vert_y2 > testy).OnlyEnforceIf(b2)
model.Add(vert_y2 <= testy).OnlyEnforceIf(b2.Not())

model.Add(b1 != b2).OnlyEnforceIf(b3)
model.Add(b1 == b2).OnlyEnforceIf(b3.Not())

"""
(testx < (vertx[j] - vertx[i]) * (testy - vert[i]) / (verty[j] - verty[i])  + vertx[i] )
"""
#check conditions for x_axis
model.Add(diff_1 == vert_x2 - vert_x1)
model.Add(diff_2 == testy - vert_y1)
model.Add(diff_3 == vert_y2 - vert_y1)
model.AddMultiplicationEquality(prod_1, diff_1, diff_2)
model.AddDivisionEquality(div_1, prod_1, diff_3 )
model.Add(add_1 == div_1 + vert_x1)

model.Add(testx < add_1).OnlyEnforceIf(c1)
model.Add(testx >= add_1).OnlyEnforceIf(c1.Not())

"""
check if both conditions are True)
"""
model.Add(c1 == 1).OnlyEnforceIf(d1)
model.Add(b3 == 1).OnlyEnforceIf(d2)

"""toggle result
(I dont know how to implement toggle)
"""
model.Add(d1 != d2).OnlyEnforceIf(result.Not())

solver = cp_model.CpSolver()
solver.parameters.log_search_progress = True
status = solver.Solve(model)

if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
    print("b1 = ", solver.Value(b1))
    print("b2 = ", solver.Value(b2))
    print("b3 = ", solver.Value(b3))
    print("c1 = ", solver.Value(c1))
    print("d1 = ", solver.Value(d1))
    print("d2 = ", solver.Value(d2))
    print("result : ", solver.Value(result))
    print("")
    print("diff_1", solver.Value(diff_1))
    print("diff_2", solver.Value(diff_2))
    print("diff_3", solver.Value(diff_3))
    print("prod_1 = ", solver.Value(prod_1))
    print("div_1", solver.Value(div_1))
    print("addd_1", solver.Value(add_1))

this gives results for 2 points but I want to use for multiple 2d points.

**Issue: **

You can see that there is a division in the calculation having division as diff_3 and when I write the domain of diff_3 as:

diff_3 = model.NewIntVarFromDomain(cp_model.Domain.FromIntervals([[-100, -1], [1, 100]]), 'diff_3')

then it says the The Divisior cannot span accross zero in constraint

But how is it possible as the domain does not have a "0" in it.

**Also, **

How one can toggle a boolean variable in CP - SAT?

Any help will be appreciated :)

EDIT:

Thanks to to Laurent's answer. I hereby have implemented his code for multiple points. The instance of the code is written below:

denum_pos = [model.NewIntVar(1, 100, 'denum_pos_{i}') for i in range(num_points)]
denum_neg = [model.NewIntVar(-100, -1, 'denum_neg_{i}') for i in range(num_points)]
denum_is_positive = [model.NewBoolVar('denum_is_positive_{i}') for i in range(num_points)]
target_pos = [model.NewIntVar(1, 100, 'target_pos_{i}') for i in range(num_points)]
target_neg = [model.NewIntVar(-100, -1, 'target_neg_{i}') for i in range(num_points)]


for i in range(0, num_points):
    j = i - 1 if i != 0 else num_points - 1
    model.Add(diff_1_arr[i] == xp[j] - xp[i])
    model.Add(diff_2_arr[i] == yc - yp[i])
    model.Add(diff_3_arr[i] == yp[j] - yp[i])
    model.AddMultiplicationEquality(prod_1_arr[i], diff_1_arr[i], diff_2_arr[i])

    #perform division
    model.Add(diff_3_arr[i] > 0).OnlyEnforceIf(denum_is_positive[i])
    model.Add(diff_3_arr[i] < 0).OnlyEnforceIf(denum_is_positive[i].Not())
    
    model.Add(denum_pos[i] == diff_3_arr[i]).OnlyEnforceIf(denum_is_positive[i])
    model.Add(denum_pos[i] == 1).OnlyEnforceIf(denum_is_positive[i].Not())
    model.Add(denum_neg[i] == 1).OnlyEnforceIf(denum_is_positive[i])
    model.Add(denum_neg[i] == diff_3_arr[i]).OnlyEnforceIf(denum_is_positive[i].Not())
    
    model.AddDivisionEquality(target_pos[i], prod_1_arr[i], denum_pos[i])
    model.AddDivisionEquality(target_neg[i], prod_1_arr[i], denum_neg[i])
    
    model.Add(div_1_arr[i] == target_pos[i]).OnlyEnforceIf(denum_is_positive[i])
    model.Add(div_1_arr[i] == target_neg[i]).OnlyEnforceIf(denum_is_positive[i].Not())
    model.Add(add_1_arr[i] == div_1_arr[i] + xp[i])

And the domain of denum is as such:

diff_3_arr = [model.NewIntVar(-1000, 1000, 'diff_3_arr_{i}') for i in range(num_points)]

When I comment out this whole part I get some output hence there should be no problem in the other constriants.


Solution

  • For the division with domains spanning across 0, you need to do a little coding

    model.AddDivisionEquality(target, numerator, denum)
    

    can be encoded as follows:

    create 2 variables denum_neg and denum_pos with each half domain (positive and negative).

    create a Boolean variable denum_is_positive.

    link denum_is_positive, denum, and denum_neg, denum_pos

    model.Add(denum > 0).OnlyEnforceIf(denum_is_positive)
    model.Add(denum < 0).OnlyEnforceIf(denum_is_positive.Not())
    
    model.Add(denum_pos == denum).OnlyEnforceIf(denum_is_positive)
    model.Add(denum_pos == 1).OnlyEnforceIf(denum_is_positive.Not())
    
    model.Add(denum_neg == -1).OnlyEnforceIf(denum_is_positive)
    model.Add(denum_neg == denum).OnlyEnforceIf(denum_is_positive.Not())
    

    now duplicate the division

    model.AddDivisionEquality(target_pos, numerator, denum_pos)
    model.AddDivisionEquality(target_neg, numerator, denum_neg)
    

    channel back with `denum_is_positive)

    model.Add(target == target_pos).OnlyEnforceIf(denum_is_positive)
    model.Add(target == target_neg).OnlyEnforceIf(denum_is_positive.Not())