"""
Another example for multi-objective optimization, this time using NSGA-II.

Like in `mo_example.py`, we apply a multi-objective algorithm to a
multi-objective version of the JSSP. This time, however, we use the famous
NSGA-II algorithm instead of the multi-objective version of the randomized
local search. If you compare the results, you will find that NSGA-II indeed
performs much better.
"""

from pycommons.io.temp import temp_file

from moptipy.algorithms.mo.nsga2 import NSGA2
from moptipy.api.mo_archive import MORecord
from moptipy.api.mo_execution import MOExecution
from moptipy.examples.jssp.gantt_space import GanttSpace
from moptipy.examples.jssp.instance import Instance
from moptipy.examples.jssp.makespan import Makespan
from moptipy.examples.jssp.ob_encoding import OperationBasedEncoding
from moptipy.examples.jssp.worktime import Worktime
from moptipy.mo.problem.weighted_sum import Prioritize
from moptipy.operators.permutations.op0_shuffle import Op0Shuffle
from moptipy.operators.permutations.op1_swapn import Op1SwapN
from moptipy.operators.permutations.op2_gap import (
    Op2GeneralizedAlternatingPosition,
)
from moptipy.spaces.permutations import Permutations
from moptipy.utils.nputils import array_to_str

instance = Instance.from_resource("swv02")     # We load the instance "swv02".
solution_space = GanttSpace(instance)          # Solutions are Gantt charts.
search_space = Permutations.with_repetitions(  # We will encode solutions as
    instance.jobs, instance.machines)          # permutations w. repetitions.
encoding = OperationBasedEncoding(instance)    # Decode permutations to Gantt.

# Each multi-objective optimization problem is defined by several objective
# functions *and* a way to scalarize the vector of objective values.
# The scalarization is only used by the system to decide for one single best
# solution in the end *and* if we actually apply a single-objective algorithm
# to the problem instead of a multi-objective one. (Here we will apply a
# multi-objective algorithm.)
f1 = Makespan(instance)  # The first objective be the makespan.
f2 = Worktime(instance)  # The second objective be the total work time.
# Here, we decide for a priorization scalarization: The single best end result
# will be the one with the shortest makespan.
problem = Prioritize([f1, f2])

# NSGA-II is the most well-known multi-objective optimization algorithm.
# It works directly on the multiple objectives. It does not require the
# scalarization above at all. The scalarization is _only_ used internally in
# the `Process` objects to ensure compatibility with single-objective
# optimization and for being able to remember a single "best" solution.
algorithm = NSGA2(              # Create the NSGA-II algorithm.
    Op0Shuffle(search_space),   # start with a random permutation and
    Op1SwapN(),                 # swap a random number of elements per step.
    Op2GeneralizedAlternatingPosition(search_space),  # use this crossover
    16, 0.05)  # population size = 16, crossover rate = 0.05

# We work with a temporary log file which is automatically deleted after this
# experiment.
# For a real experiment, you would put the path to a file into `tf` by doing
# `from pycommons.io.path import Path; tf = Path("myfile.txt")` and not use
# the `with` block.
with temp_file() as tf:  # create temporary file `tf`
    ex = MOExecution()  # begin configuring execution
    ex.set_solution_space(solution_space)
    ex.set_search_space(search_space)
    ex.set_encoding(encoding)
    ex.set_objective(problem)      # set the multi-objective problem
    ex.set_algorithm(algorithm)
    ex.set_rand_seed(199)          # set random seed to 199
    ex.set_log_improvements(True)  # log all improving moves
    ex.set_log_file(tf)            # set log file = temp file `tf`
    ex.set_max_fes(2800)           # allow at most 2800 function evaluations
    with ex.execute() as process:  # now run the algorithm*problem combination
        arch: list[MORecord] = process.get_archive()
        print(f"We found the {len(arch)} non-dominated trade-off solutions:")
        print("makespan;worktime")
        arch.sort()
        for ae in arch:
            print(array_to_str(ae.fs))

    print("\nNow reading and printing all the logged data:")
    print(tf.read_all_str())  # instead, we load and print the log file
# The temp file is deleted as soon as we leave the `with` block.