#!/usr/bin/env python3
# Copyright 2010-2022 Google LLC
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
#     http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.

"""Maximize the minimum of pairwise distances between n robots in a square space."""

import math
from typing import Sequence
from absl import app
from absl import flags

from google.protobuf import text_format
from ortools.sat.python import cp_model

_NUM_ROBOTS = flags.DEFINE_integer("num_robots", 8, "Number of robots to place.")
_ROOM_SIZE = flags.DEFINE_integer(
    "room_size", 20, "Size of the square room where robots are."
)
_PARAMS = flags.DEFINE_string(
    "params",
    "num_search_workers:16, max_time_in_seconds:20",
    "Sat solver parameters.",
)


def spread_robots(num_robots: int, room_size: int, params: str):
    """Optimize robots placement."""
    model = cp_model.CpModel()

    # Create the list of coordinates (x, y) for each robot.
    x = [model.NewIntVar(1, room_size, f"x_{i}") for i in range(num_robots)]
    y = [model.NewIntVar(1, room_size, f"y_{i}") for i in range(num_robots)]

    # The specification of the problem is to maximize the minimum euclidian
    # distance between any two robots. Unfortunately, the euclidian distance
    # uses the square root operation which is not defined on integer variables.
    # To work around, we will create a min_square_distance variable, then we make
    # sure that its value is less than the square of the euclidian distance
    # between any two robots.
    #
    # This encoding has a low precision. To improve the precision, we will scale
    # the domain of the min_square_distance variable by a constant factor, then
    # multiply the square of the euclidian distance between two robots by the same
    # factor.
    #
    # we create a scaled_min_square_distance variable with a domain of
    # [0..scaling * max euclidian distance**2] such that
    #   forall i:
    #     scaled_min_square_distance <= scaling * (x_diff_sq[i] + y_diff_sq[i])
    scaling = 1000
    scaled_min_square_distance = model.NewIntVar(
        0, 2 * scaling * room_size**2, "scaled_min_square_distance"
    )

    # Build intermediate variables and get the list of squared distances on
    # each dimension.
    for i in range(num_robots - 1):
        for j in range(i + 1, num_robots):
            # Compute the distance on each dimension between robot i and robot j.
            x_diff = model.NewIntVar(-room_size, room_size, f"x_diff{i}")
            y_diff = model.NewIntVar(-room_size, room_size, f"y_diff{i}")
            model.Add(x_diff == x[i] - x[j])
            model.Add(y_diff == y[i] - y[j])

            # Compute the square of the previous differences.
            x_diff_sq = model.NewIntVar(0, room_size**2, f"x_diff_sq{i}")
            y_diff_sq = model.NewIntVar(0, room_size**2, f"y_diff_sq{i}")
            model.AddMultiplicationEquality(x_diff_sq, x_diff, x_diff)
            model.AddMultiplicationEquality(y_diff_sq, y_diff, y_diff)

            # We just need to be <= to the scaled square distance as we are
            # maximizing the min distance, which is equivalent as maximizing the min
            # square distance.
            model.Add(scaled_min_square_distance <= scaling * (x_diff_sq + y_diff_sq))

    # Naive symmetry breaking.
    for i in range(1, num_robots):
        model.Add(x[0] <= x[i])
        model.Add(y[0] <= y[i])

    # Objective
    model.Maximize(scaled_min_square_distance)

    # Creates a solver and solves the model.
    solver = cp_model.CpSolver()
    if params:
        text_format.Parse(params, solver.parameters)
    solver.parameters.log_search_progress = True
    status = solver.Solve(model)

    # Prints the solution.
    if status == cp_model.OPTIMAL or status == cp_model.FEASIBLE:
        print(
            f"Spread {num_robots} with a min pairwise distance of"
            f" {math.sqrt(solver.ObjectiveValue() / scaling)}"
        )
        for i in range(num_robots):
            print(f"robot {i}: x={solver.Value(x[i])} y={solver.Value(y[i])}")
    else:
        print("No solution found.")


def main(argv: Sequence[str]) -> None:
    if len(argv) > 1:
        raise app.UsageError("Too many command-line arguments.")

    spread_robots(_NUM_ROBOTS.value, _ROOM_SIZE.value, _PARAMS.value)


if __name__ == "__main__":
    app.run(main)
