331 lines
10 KiB
MiniZinc
331 lines
10 KiB
MiniZinc
% Multi-agent Collective Construction (MACC)
|
|
%
|
|
% The multi-agent collective construction problem tasks agents to construct any
|
|
% given three-dimensional structure on a grid by repositioning blocks. Agents
|
|
% are required to also use the blocks to build ramps in order to access the
|
|
% higher levels necessary to construct the building, and then remove the ramps
|
|
% upon completion of the building.
|
|
%
|
|
% Further details on the problem can be found in:
|
|
% Lam, E., Stuckey, P., Koenig, S., & Kumar, T. K. S. Exact Approaches
|
|
% to the Multi-Agent Collective Construction Problem. CP2020.
|
|
% https://ed-lam.com/papers/macc2020.pdf
|
|
%
|
|
% Edward Lam <edward.lam@monash.edu>
|
|
|
|
% --------
|
|
% Instance
|
|
% --------
|
|
|
|
int: A; % Number of agents
|
|
int: T; % Time horizon
|
|
int: X; % Width
|
|
int: Y; % Depth
|
|
int: Z; % Height
|
|
array[YY,XX] of ZZ: building; % Structure to construct
|
|
|
|
% -----------
|
|
% Environment
|
|
% -----------
|
|
|
|
set of int: TT = 0..T-1;
|
|
set of int: TTT = 0..T-2;
|
|
set of int: GRID = 0..X*Y-1;
|
|
set of int: XX = 0..X-1;
|
|
set of int: YY = 0..Y-1;
|
|
set of int: ZZ = 0..Z-1;
|
|
|
|
array[XX,YY] of GRID: id = array2d(XX, YY, [x*Y + y | y in YY, x in XX]);
|
|
set of GRID: BORDER = {id[x,0] | x in XX} union
|
|
{id[x,Y-1] | x in XX} union
|
|
{id[0,y] | y in YY} union
|
|
{id[X-1,y] | y in YY};
|
|
set of GRID: INTERIOR = GRID diff BORDER;
|
|
|
|
set of int: OFFGRID = -2..-1;
|
|
set of int: WORLD = GRID union OFFGRID;
|
|
array[GRID] of set of OFFGRID: off_grid_neighbour = array1d(GRID,
|
|
[(if x == 0 then {-1,-2} else {} endif) union
|
|
(if x == X-1 then {-1,-2} else {} endif) union
|
|
(if y == 0 /\ 0 < x /\ x < X-1 then {-1,-2} else {} endif) union
|
|
(if y == Y-1 /\ 0 < x /\ x < X-1 then {-1,-2} else {} endif)
|
|
| y in YY, x in XX]
|
|
);
|
|
|
|
array[GRID] of set of GRID: neighbours = array1d(GRID,
|
|
[(if x > 0 then {id[x-1,y]} else {} endif) union
|
|
(if x < X-1 then {id[x+1,y]} else {} endif) union
|
|
(if y > 0 then {id[x,y-1]} else {} endif) union
|
|
(if y < Y-1 then {id[x,y+1]} else {} endif)
|
|
| y in YY, x in XX]
|
|
);
|
|
array[GRID] of set of GRID: neighbours_and_self = array1d(GRID, [neighbours[i] union {i} | i in GRID]);
|
|
array[GRID] of set of WORLD: world_neighbours_and_self = array1d(GRID, [neighbours_and_self[i] union off_grid_neighbour[i] | i in GRID]);
|
|
|
|
array[WORLD] of min(WORLD)..max(XX): x_of_pos = array1d(WORLD, [i | i in OFFGRID] ++ [x | y in YY, x in XX]);
|
|
array[WORLD] of min(WORLD)..max(YY): y_of_pos = array1d(WORLD, [i | i in OFFGRID] ++ [y | y in YY, x in XX]);
|
|
|
|
enum ACTION = {
|
|
UNUSED,
|
|
MOVE,
|
|
BLOCK
|
|
};
|
|
|
|
% -----------------------
|
|
% Environment constraints
|
|
% -----------------------
|
|
|
|
% Height of the positions at each time step
|
|
array[TT,WORLD] of var ZZ: pos_height;
|
|
|
|
% Height is 0 outside the map
|
|
constraint forall(t in TT, i in OFFGRID) (
|
|
pos_height[t,i] == 0
|
|
);
|
|
|
|
% Height is 0 at the border
|
|
constraint forall(t in TT, i in BORDER) (
|
|
pos_height[t,i] == 0
|
|
);
|
|
|
|
% Height is 0 at the first two time steps
|
|
constraint forall(t in min(TT)..min(TT)+1, i in GRID) (
|
|
pos_height[t,i] == 0
|
|
);
|
|
|
|
% Height is equal to the building at the last two time steps
|
|
constraint forall(t in max(TT)-1..max(TT), i in GRID) (
|
|
pos_height[t,i] == building[y_of_pos[i],x_of_pos[i]]
|
|
);
|
|
|
|
% Change in height
|
|
constraint forall(t in TTT, i in GRID) (
|
|
pos_height[t,i] - 1 <= pos_height[t+1,i]
|
|
);
|
|
constraint forall(t in TTT, i in GRID) (
|
|
pos_height[t+1,i] <= pos_height[t,i] + 1
|
|
);
|
|
|
|
% -----------------
|
|
% Agent constraints
|
|
% -----------------
|
|
|
|
% Action of the agents at each position and time step
|
|
array[TT,WORLD] of var ACTION: agent_action;
|
|
array[TT,WORLD] of var WORLD: agent_next_position;
|
|
array[TT,GRID] of var GRID: agent_block_position;
|
|
array[TT,WORLD] of var bool: agent_carrying;
|
|
|
|
array[TTT,GRID] of var bool: agent_pickup;
|
|
array[TTT,GRID] of var bool: agent_delivery;
|
|
|
|
% Fix actions at dummy positions off the map.
|
|
constraint forall(t in TT, i in OFFGRID) (
|
|
agent_action[t,i] == MOVE
|
|
);
|
|
|
|
% Fix next positions at dummy positions off the map.
|
|
constraint forall(t in TT, i in OFFGRID) (
|
|
agent_next_position[t,i] == i
|
|
);
|
|
|
|
% Fix carrying state at dummy positions off the map.
|
|
constraint forall(t in TT) (let {int: i = -1} in
|
|
agent_carrying[t,i] == true
|
|
);
|
|
constraint forall(t in TT) (let {int: i = -2} in
|
|
agent_carrying[t,i] == false
|
|
);
|
|
|
|
% All agents must be off the grid at the start
|
|
constraint forall(i in GRID) (let {int: t = min(TT)} in
|
|
agent_action[t,i] == UNUSED
|
|
);
|
|
|
|
% All agents must be off the grid at the end
|
|
constraint forall(i in GRID) (let {int: t = max(TT)} in
|
|
agent_action[t,i] == UNUSED
|
|
);
|
|
|
|
% Agents must move to a neighbouring position or the same position
|
|
constraint forall(t in TT, i in GRID) (
|
|
agent_next_position[t,i] in world_neighbours_and_self[i]
|
|
);
|
|
|
|
% Agents stay at the same position when doing a pickup or delivery.
|
|
constraint forall(t in TT, i in GRID) (
|
|
agent_action[t,i] == BLOCK
|
|
->
|
|
agent_next_position[t,i] == i
|
|
);
|
|
|
|
% Agents cannot pickup or deliver at the same position
|
|
constraint forall(t in TT, i in GRID) (
|
|
agent_block_position[t,i] in neighbours[i]
|
|
);
|
|
|
|
% Carrying status
|
|
constraint forall(t in TTT, i in GRID) (
|
|
agent_action[t,i] == MOVE
|
|
->
|
|
agent_carrying[t+1,agent_next_position[t,i]] == agent_carrying[t,i]
|
|
);
|
|
constraint forall(t in TTT, i in GRID) (
|
|
agent_action[t,i] == BLOCK
|
|
->
|
|
agent_carrying[t+1,i] == not agent_carrying[t,i]
|
|
);
|
|
|
|
% Carrying status - pickup
|
|
constraint forall(t in TTT, i in GRID) (
|
|
agent_pickup[t,i]
|
|
<->
|
|
agent_action[t,i] == BLOCK /\ agent_carrying[t+1,i] /\ not agent_carrying[t,i]
|
|
);
|
|
|
|
% Carrying status - delivery
|
|
constraint forall(t in TTT, i in GRID) (
|
|
agent_delivery[t,i]
|
|
<->
|
|
agent_action[t,i] == BLOCK /\ not agent_carrying[t+1,i] /\ agent_carrying[t,i]
|
|
);
|
|
|
|
% Flow out
|
|
constraint forall(t in TTT, i in GRID) (
|
|
(agent_action[t,i] == UNUSED)
|
|
\/
|
|
(agent_action[t+1,agent_next_position[t,i]] != UNUSED)
|
|
);
|
|
|
|
% Flow in
|
|
constraint forall(t in TTT, i in INTERIOR) (
|
|
agent_action[t+1,i] != UNUSED
|
|
->
|
|
exists (j in neighbours_and_self[i]) (agent_action[t,j] != UNUSED /\ agent_next_position[t,j] == i)
|
|
);
|
|
|
|
% Vertex collision - limit flows into (t+1,i)
|
|
constraint forall(t in min(TT)+1..max(TT)-1, i in GRID) (
|
|
sum(j in neighbours_and_self[i]) (bool2int(agent_action[t,j] == MOVE /\ agent_next_position[t,j] == i)) +
|
|
bool2int(agent_action[t,i] == BLOCK) +
|
|
sum(j in neighbours[i]) (bool2int(agent_action[t+1,j] == BLOCK /\ agent_block_position[t+1,j] == i))
|
|
<= 1
|
|
);
|
|
|
|
% Edge collision
|
|
constraint forall(t in min(TT)+1..max(TT)-1, i in GRID) (
|
|
agent_action[t,i] == MOVE /\ agent_next_position[t,i] != i /\ agent_action[t,agent_next_position[t,i]] == MOVE
|
|
->
|
|
agent_next_position[t,agent_next_position[t,i]] != i
|
|
);
|
|
|
|
% Maximum number of agents
|
|
constraint forall(t in min(TT)+1..max(TT)) (
|
|
sum(i in GRID) (bool2int(agent_action[t,i] != UNUSED))
|
|
+
|
|
sum(i in BORDER) (bool2int(agent_action[t-1,i] == MOVE /\ agent_next_position[t-1,i] < 0))
|
|
<= A
|
|
);
|
|
|
|
% ---------------------------
|
|
% Interdependence constraints
|
|
% ---------------------------
|
|
|
|
% Height of move
|
|
constraint forall(t in TTT, i in GRID) (let {var int: next_pos = agent_next_position[t,i]} in
|
|
agent_action[t,i] == MOVE
|
|
->
|
|
pos_height[t,i] - 1 <= pos_height[t+1,next_pos]
|
|
);
|
|
constraint forall(t in TTT, i in GRID) (let {var int: next_pos = agent_next_position[t,i]} in
|
|
agent_action[t,i] == MOVE
|
|
->
|
|
pos_height[t+1,next_pos] <= pos_height[t,i] + 1
|
|
);
|
|
|
|
% Height of wait
|
|
constraint forall(t in TTT, i in GRID) (let {var int: next_pos = agent_next_position[t,i]} in
|
|
agent_action[t,i] == MOVE /\ next_pos == i
|
|
->
|
|
pos_height[t+1,i] == pos_height[t,i]
|
|
);
|
|
|
|
% Height of pickup
|
|
constraint forall(t in TTT, i in GRID) (let {var int: block_pos = agent_block_position[t,i]} in
|
|
agent_pickup[t,i]
|
|
->
|
|
pos_height[t,block_pos] == pos_height[t,i] + 1
|
|
);
|
|
constraint forall(t in TTT, i in GRID) (let {var int: block_pos = agent_block_position[t,i]} in
|
|
agent_pickup[t,i]
|
|
->
|
|
pos_height[t+1,block_pos] == pos_height[t,block_pos] - 1
|
|
);
|
|
|
|
% Height of delivery
|
|
constraint forall(t in TTT, i in GRID) (let {var int: block_pos = agent_block_position[t,i]} in
|
|
agent_delivery[t,i]
|
|
->
|
|
pos_height[t,block_pos] == pos_height[t,i]
|
|
);
|
|
constraint forall(t in TTT, i in GRID) (let {var int: block_pos = agent_block_position[t,i]} in
|
|
agent_delivery[t,i]
|
|
->
|
|
pos_height[t+1,block_pos] == pos_height[t,block_pos] + 1
|
|
);
|
|
|
|
% Height change
|
|
constraint forall(t in TTT, i in GRID) (
|
|
pos_height[t+1,i] == pos_height[t,i]
|
|
- sum(j in neighbours[i]) (bool2int(agent_pickup[t,j] /\ agent_block_position[t,j] == i))
|
|
+ sum(j in neighbours[i]) (bool2int(agent_delivery[t,j] /\ agent_block_position[t,j] == i))
|
|
);
|
|
|
|
% -------------------------------------------
|
|
% Symmetry-breaking and redundant constraints
|
|
% -------------------------------------------
|
|
|
|
% Start at the first time step
|
|
constraint symmetry_breaking_constraint(exists(i in BORDER) (
|
|
agent_action[min(TT)+1,i] != UNUSED
|
|
));
|
|
|
|
% Height decrease - pickup
|
|
constraint forall(t in TTT, i in GRID) (redundant_constraint(
|
|
pos_height[t+1,i] == pos_height[t,i] - 1
|
|
->
|
|
exists(j in neighbours[i]) (agent_pickup[t,j] /\ agent_block_position[t,j] == i)
|
|
));
|
|
|
|
% Height increase - delivery
|
|
constraint forall(t in TTT, i in GRID) (redundant_constraint(
|
|
pos_height[t+1,i] == pos_height[t,i] + 1
|
|
->
|
|
exists(j in neighbours[i]) (agent_delivery[t,j] /\ agent_block_position[t,j] == i)
|
|
));
|
|
|
|
% ------------------
|
|
% Objective function
|
|
% ------------------
|
|
|
|
var int: objective = sum(t in TT, i in GRID) (bool2int(agent_action[t,i] != UNUSED));
|
|
|
|
solve :: seq_search([
|
|
int_search(agent_action, first_fail, indomain_min, complete),
|
|
int_search(agent_next_position, first_fail, indomain_min, complete),
|
|
int_search(agent_block_position, first_fail, indomain_min, complete),
|
|
int_search(agent_carrying, first_fail, indomain_min, complete),
|
|
int_search(pos_height, first_fail, indomain_min, complete),
|
|
]) minimize objective;
|
|
|
|
output [
|
|
"objective = \(objective);\n",
|
|
"pos_height = array2d(\(TT), \(WORLD), \(pos_height));\n",
|
|
"agent_action = array2d(\(TT), \(WORLD), \(agent_action));\n",
|
|
"agent_next_position = array2d(\(TT), \(WORLD), \(agent_next_position));\n",
|
|
"agent_block_position = array2d(\(TT), \(GRID), \(agent_block_position));\n",
|
|
"agent_carrying = array2d(\(TT), \(WORLD), \(agent_carrying));\n",
|
|
"agent_pickup = array2d(\(TTT), \(GRID), \(agent_pickup));\n",
|
|
"agent_delivery = array2d(\(TTT), \(GRID), \(agent_delivery));\n",
|
|
];
|