PyCSP3

Declaring Variables

Below, we show how to declare stand-alone variables, and multi-dimensional arrays of variables.

Remark. In PyCSP$^3$, which is currently targeted to XCSP$^3$-core, we can only define integer and symbolic variables with finite domains, i.e., variables with a finite set of integers or symbols (strings).

To see how it works, we need first to import the library PyCSP$^3$:

from pycsp3 import *


Stand-alone Variables

Stand-alone variables can be declared by means of the PyCSP$^3$ function Var(). To define the domain of a variable, we can simply list values, or use range(). For example, we can define three integer variables w, x and y, as well as a symbolic variable z as follows:

w = Var(range(15))
x = Var(0, 1)
y = Var(0, 2, 4, 6, 8)
z = Var("a", "b", "c")


The domain of a variable is given by a field called dom. So, we can control the domain of the declared variables:

print("Domain of w: ", w.dom)
print("Domain of x: ", x.dom)
print("Domain of y: ", y.dom)
print("Domain of z: ", z.dom)

Domain of w:  0..14
Domain of x:  0 1
Domain of y:  0 2 4 6 8
Domain of z:  a b c


Note that values can be directly listed as above, or given in a set as follows:

clear()  # to discard previously posted variables
w = Var(set(range(15)))
x = Var({0, 1})
y = Var({0, 2, 4, 6, 8})
z = Var({"a", "b", "c"})


We can check that we obtain similar domains:

print("Domain of w: ", w.dom)
print("Domain of x: ", x.dom)
print("Domain of y: ", y.dom)
print("Domain of z: ", z.dom)

Domain of w:  0..14
Domain of x:  0 1
Domain of y:  0 2 4 6 8
Domain of z:  a b c


It is also possible to name the parameter dom when defining the domain:

clear()  # to discard previously posted variables
w = Var(dom=range(15))   # or equivalently, w = Var(dom=set(range(15)))
x = Var(dom={0, 1})
y = Var(dom={0, 2, 4, 6, 8})
z = Var(dom={"a", "b", "c"})


We can check that we obtain similar domains:

print("Domain of w: ", w.dom)
print("Domain of x: ", x.dom)
print("Domain of y: ", y.dom)
print("Domain of z: ", z.dom)

Domain of w:  0..14
Domain of x:  0 1
Domain of y:  0 2 4 6 8
Domain of z:  a b c


Finally, it is of course possible to use generators and comprehension sets. For example, for y, we can write:

clear()
y = Var(i for i in range (10) if i % 2 == 0)
print("Domain of y: ", y.dom)
clear()
y = Var({i for i in range (10) if i % 2 == 0})
print("Domain of y: ", y.dom)
clear()
y = Var(dom={i for i in range(10) if i % 2 == 0})
print("Domain of y: ", y.dom)

Domain of y:  0 2 4 6 8
Domain of y:  0 2 4 6 8
Domain of y:  0 2 4 6 8


Arrays of Variables

The PyCSP$^3$ function for declaring an array of variables is VarArray() that requires two named parameters size and dom. For declaring a one-dimensional array of variables, the value of size must be an integer (or a list containing only one integer), for declaring a two-dimensional array of variables, the value of size must be a list containing exactly two integers, and so on. The named parameter dom indicates the domain of each variable in the array.

For example, to declare:

• x, a one-dimensional array of 10 variables with domain {0,1}
• y, a two-dimensional array of $5 \times 5$ variables with domain {0,1,…, 9}
• z, a three-dimensional array of $4 \times 3 \times 4$ variables with domain {1, 5, 10, 20}

we write:

clear()  # to discard previously posted variables
x = VarArray(size=10, dom={0, 1})
y = VarArray(size=[5, 5], dom=range(10))
z = VarArray(size=[4, 3, 4], dom={1, 5, 10, 20})


We can display the (structure of the) arrays, as well as the domains of the variables.

print("Array x: ", x)
print("Array y: ", y)
print("Array z: ", z)
print("Domain of variables of x: ", x[0].dom)
print("Domain of variables of y: ", y[0][0].dom)
print("Domain of variables of z: ", z[0][0][0].dom)

Array x:  [x[0], x[1], x[2], x[3], x[4], x[5], x[6], x[7], x[8], x[9]]
Array y:  [
[y[0][0], y[0][1], y[0][2], y[0][3], y[0][4]]
[y[1][0], y[1][1], y[1][2], y[1][3], y[1][4]]
[y[2][0], y[2][1], y[2][2], y[2][3], y[2][4]]
[y[3][0], y[3][1], y[3][2], y[3][3], y[3][4]]
[y[4][0], y[4][1], y[4][2], y[4][3], y[4][4]]
]
Array z:  [
[
[z[0][0][0], z[0][0][1], z[0][0][2], z[0][0][3]]
[z[0][1][0], z[0][1][1], z[0][1][2], z[0][1][3]]
[z[0][2][0], z[0][2][1], z[0][2][2], z[0][2][3]]
],
[
[z[1][0][0], z[1][0][1], z[1][0][2], z[1][0][3]]
[z[1][1][0], z[1][1][1], z[1][1][2], z[1][1][3]]
[z[1][2][0], z[1][2][1], z[1][2][2], z[1][2][3]]
],
[
[z[2][0][0], z[2][0][1], z[2][0][2], z[2][0][3]]
[z[2][1][0], z[2][1][1], z[2][1][2], z[2][1][3]]
[z[2][2][0], z[2][2][1], z[2][2][2], z[2][2][3]]
],
[
[z[3][0][0], z[3][0][1], z[3][0][2], z[3][0][3]]
[z[3][1][0], z[3][1][1], z[3][1][2], z[3][1][3]]
[z[3][2][0], z[3][2][1], z[3][2][2], z[3][2][3]]
],
]
Domain of variables of x:  0 1
Domain of variables of y:  0..9
Domain of variables of z:  1 5 10 20


Important: Indexing starts at 0. For example, x[2] is the third variable of x, and y[1] is the second row of y. Technically, variable arrays are objects that are instances of ListVar, a subclass of list; additional functionalities of such objects are useful, for example, when posting constraints Element.

Arrays with Variables of Different Domains

In some situations, you may want to declare variables in an array with different domains. For a one-dimensional array, you can give the name of a function that accepts an integer i and returns the domain to be associated with the variable at index i in the array. For a two-dimensional array, you can give the name of a function that accepts a pair of integers (i,j) and returns the domain to be associated with the variable at indexes i, j in the array. And so on.

For example, suppose that the domain of all variables of the first column of y is range(5) instead of range(10). We can write:

clear() # to discard previously posted variables

def domain_y(i,j):
return range(5) if j == 0 else range(10)

y = VarArray(size=[5, 5], dom=domain_y)


We can observe that domains of variables in y are not all the same:

for i in range(5):
print(f"Domains on row {i}: {[y[i][j].dom for j in range(5)]}")

Domains on row 0: [0..4, 0..9, 0..9, 0..9, 0..9]
Domains on row 1: [0..4, 0..9, 0..9, 0..9, 0..9]
Domains on row 2: [0..4, 0..9, 0..9, 0..9, 0..9]
Domains on row 3: [0..4, 0..9, 0..9, 0..9, 0..9]
Domains on row 4: [0..4, 0..9, 0..9, 0..9, 0..9]


We can also use a lambda function:

clear() # to discard previously posted variables

y = VarArray(size=[5, 5], dom=lambda i,j: range(5) if j == 0 else range(10))


We can check:

for i in range(5):
print(f"Domains on row {i}: {[y[i][j].dom for j in range(5)]}")

Domains on row 0: [0..4, 0..9, 0..9, 0..9, 0..9]
Domains on row 1: [0..4, 0..9, 0..9, 0..9, 0..9]
Domains on row 2: [0..4, 0..9, 0..9, 0..9, 0..9]
Domains on row 3: [0..4, 0..9, 0..9, 0..9, 0..9]
Domains on row 4: [0..4, 0..9, 0..9, 0..9, 0..9]


Sometimes, not all variables in an array are relevant. For example, you may only want to use the variables in the lower part of a two-dimensional array (matrix). In that case, the value None must be used.

For example, if one wanted to introduce an auxiliary array y for the problem Golomb Ruler to compute distances between any two pairs of the main array x, we could write:

clear() # to discard previously posted variables

# y[i][j] is the distance between x[i] and x[j] for i strictly less than j
y = VarArray(size=[5, 5], dom=lambda i, j: range(1, 5 * 5) if i < j else None)


One can see that only half of the array is really used (i.e., really contains variables): the lower part (below the main downward diagonal) only contains None. For example, y[1][0] is equal to None.

print("Array y: ", y)

Array y:  [
[None, y[0][1], y[0][2], y[0][3], y[0][4]]
[None, None, y[1][2], y[1][3], y[1][4]]
[None, None, None, y[2][3], y[2][4]]
[None, None, None, None, y[3][4]]
[None, None, None, None, None]
]


Naming Variables and Arrays of Variables

Since Version 2.1, when declaring a stand-alone variable, one can set the name (id) with the parameter id. Then, to designate the variable, you just has to call the function var() with the specified name. Here is an example of model:

clear()

x = Var(range(10))
y = Var(dom=range(5), id="yy_12")
Var(dom=range(10), id="z")

d = dict()
a = 1
d[0] = Var(0, 1, id="d_0")
d[a] = Var(0, 1, id="d_1")

satisfy(
x >= 3,
var("x") <= 6,
y > 2,
var("yy_12") < 4,
var("z") != 4,
d[0] + d[1] != 0,
var("d_0") + var("d_1") != 2
)

intension(function:ge(x,3))
intension(function:le(x,6))
intension(function:gt(yy_12,2))
intension(function:lt(yy_12,4))
intension(function:ne(z,4))


You can observe the correpondances (aliases) from the output above.

Similarly, one can use the parameter id when declaring arrays of variables, and call the same function var() to get access to arrays. Here is an example of model:

clear()

x = VarArray(size=3, dom={0, 1})
y = VarArray(size=3, dom={0, 1}, id="yy")
VarArray(size=3, dom={0, 1}, id="zz")

d = dict()
a = 1
d[0] = VarArray(size=3, dom={0, 1}, id="d0")
d[a] = VarArray(size=3, dom={0, 1}, id="d_a")

satisfy(
Sum(x) == 1,
Sum(y) > 0,
Sum(var("yy")) < 2,
Sum(var("zz")) < 2,
Sum(d[0] + d[a]) > 0,
Sum(var("d0") + var("d_a")) < 2,
var("d_a")[1] == 1
)

sum(list:[x[0], x[1], x[2]], condition:(eq,1))
sum(list:[yy[0], yy[1], yy[2]], condition:(gt,0))
sum(list:[yy[0], yy[1], yy[2]], condition:(lt,2))
sum(list:[zz[0], zz[1], zz[2]], condition:(lt,2))
sum(list:[d0[0], d0[1], d0[2], d_a[0], d_a[1], d_a[2]], condition:(gt,0))
sum(list:[d0[0], d0[1], d0[2], d_a[0], d_a[1], d_a[2]], condition:(lt,2))
intension(function:eq(d_a[1],1))