"""
We execute an experiment with 2 algorithms on 1 problem and plot the ERTs.

The (empiricially estimated) Expected Running Time - or ERT for short - gives
us an impression how long an optimization algorithm will need to reach a
certain goal quality for a given optimization problem instance.

If we apply an algorithm once to a problem instance, it will randomly start
at a rather bad solution quality. It will then step by step improve. We can
log each improvement in a log file. If we conduct multiple runs of the
algorithm, we will collect multiple log files from them. For each objective
value `f` that was reached by any of the runs, we can compute the arithmetic
mean that it took them to do so. This works well as long as all runs can
reach `f`. However, especially for better (i.e., small) values of `f`, some
runs may fail to do so. These runs stop improving earlier. The arithmetic
mean of the time to read these `f` would go to infinity.

Luckily, we can do better than that. We can simply assume that once an
unsuccessful run terminates, we would directly start a new run afterwards.
If at least one run in our experiment was successful, this means that the
probability to reach `f` is larger than zero and if we imagine that we
keep starting new runs again and again, we will eventually succeed. This is
the idea behind the ERT. For any goal `f`, it is computed as

  `ERT[f] = Time(fbest >= f) / s`

where `s` is the number of successful runs, i.e., of runs that reached the
goal `f` and `Time(fbest >= f)` is the sum of the runtime of all runs that
was spent until the objective value reached `f` (or the run terminated).

An ERT plot shows the ERT for all objective values `f` encountered during
a set of runs. It is therefore a curve that grows for shrinking `f`.

In this example, we apply both a randomized local search and a random walk
to the minimization version of the 12-bit OneMax problem. We grant each of
them 100 FEs and perform 21 runs per setup. We then plot the ERT in terms
of the expected number of objective function evaluations (FEs) over the
objective values (`f`).
"""
from time import sleep
from webbrowser import open_new_tab

from pycommons.io.temp import temp_dir
from pycommons.processes.caller import is_build

from moptipy.algorithms.random_walk import RandomWalk
from moptipy.algorithms.so.rls import RLS
from moptipy.api.execution import Execution
from moptipy.api.experiment import run_experiment
from moptipy.evaluation.axis_ranger import AxisRanger
from moptipy.evaluation.ert import from_progresses
from moptipy.evaluation.plot_ert import plot_ert
from moptipy.evaluation.progress import from_logs
from moptipy.examples.bitstrings.onemax import OneMax
from moptipy.operators.bitstrings.op0_random import Op0Random
from moptipy.operators.bitstrings.op1_flip1 import Op1Flip1
from moptipy.spaces.bitstrings import BitStrings
from moptipy.utils.plot_utils import create_figure, save_figure

# We try to solve only the 16-bit OneMax problem
problems = [lambda: OneMax(16)]


def make_rls(problem) -> Execution:
    """
    Create an RLS Execution with single bit flip mutation.

    :param problem: the OneMax problem
    :returns: the execution
    """
    ex = Execution()
    ex.set_solution_space(BitStrings(problem.n))
    ex.set_objective(problem)
    ex.set_algorithm(RLS(  # create RLS that
        Op0Random(),  # starts with a random bit string and
        Op1Flip1()))  # flips exactly one bit
    ex.set_max_fes(100)  # the maximum FEs
    ex.set_log_improvements(True)  # log the progress!
    return ex


def make_random_walk(problem) -> Execution:
    """
    Create a Random Walk Execution.

    :param problem: the OneMax problem
    :returns: the execution
    """
    ex = Execution()
    ex.set_solution_space(BitStrings(problem.n))
    ex.set_objective(problem)
    ex.set_algorithm(
        RandomWalk(  # create a random walk that
            Op0Random(),  # starts with a random bit string and
            Op1Flip1()))  # flip exactly one bit
    ex.set_max_fes(100)  # the maximum FEs
    ex.set_log_improvements(True)  # log the progress!
    return ex


# We execute the whole experiment in a temp directory.
# For a real experiment, you would put an existing directory path into `td` by
# doing `from pycommons.io.path import Path; td = Path("mydir")` and not use
# the `with` block.
with temp_dir() as td:  # create temporary directory `td`
    run_experiment(base_dir=td,  # set the base directory for log files
                   instances=problems,  # define the problem instances
                   setups=[make_rls,  # provide RLS run creator
                           make_random_walk],  # provide random walk creator
                   n_runs=21)  # we will execute 71 runs per setup
    # Once we arrived here, the experiment with 2*1*31 = 62 runs has completed.

    data = []  # we will load the data into this list
    from_logs(path=td,  # the result directory
              consumer=data.append,  # put the data into data
              time_unit="FEs",  # time is in FEs (as opposed to "ms")
              f_name="plainF")  # use raw, unscaled objective values
    ert = []  # we will load the ERT into this list
    # The below function groups all runs of one algorithm and instance
    # together and then computes the ERT.
    from_progresses(data, ert.append)

    # Plot the ERT functions.
    # This function will automatically pick the labels of the axes and choose
    # that the horizontal axis (FEs) be log-scaled.
    fig = create_figure(width=4)  # create an empty, 4"-wide figure
    plot_ert(erts=ert, figure=fig)  # plot all the data into the figure
    # Notice that save_figure returns a list of files that has been generated.
    # You can specify multiple formats, e.g., ("svg", "pdf", "png") and get
    # multiple files.
    # Below, we generate a svg image, a pdf image, and a png image and
    # remember the list (containing the generated files) as `files`. We will
    # add all other files we generate to this list and, in the end, display
    # all of them in the web browser.
    files = save_figure(fig=fig,  # store fig to a file
                        file_name="log_ert_over_f",  # base name
                        dir_name=td,  # store graphic in temporary directory
                        formats=("svg", "png", "pdf"))  # file types
    del fig  # dispose figure

    # Plot the ECDF functions, but this time do not log-scale the x-axis.
    fig = create_figure(width=4)  # create an empty, 4"-wide figure
    plot_ert(erts=ert, figure=fig,  # plot all the data into the figure
             y_axis=AxisRanger.for_axis("FEs", log_scale=False))
    # This time, we save the image only as svg.
    files.extend(save_figure(fig=fig,  # store fig to a file
                             file_name="ert_over_fes",  # base name
                             dir_name=td,  # store graphic in temp directory
                             formats="svg"))  # file type: this time only svg
    del fig  # dispose figure

    # OK, we have now plotted a set of different ERT plots.
    # We will open them in the web browser if we are not in a make build.
    if not is_build():
        for file in files:  # for each file we generated
            open_new_tab(f"file://{file}")  # open a browser tab
        sleep(10)  # sleep 10 seconds (enough time for the browser to load)
# The temp directory is deleted as soon as we leave the `with` block.
# Hence, all the figures generated above as well as the experimental results
# now have disappeared.