"""
We execute an experiment with 2 algorithms on 1 problem and plot the ECDFs.
An Empirical Cumulative Distribution Function (ECDF) plot illustrates the
fraction of runs (executions of an algorithm) that have solved their
corresponding problem instance on the vertical axis, over the runtime that has
been consumed on the horizontal axis.
Its highest value is 1, its lowest value is 0.
If you execute 10 runs of your algorithm on two problems each, the ECDF curves
can take on (2*10 + 1) = 21 different values from [0, 1].
If all of your runs succeed in reaching the goal objective value and the
slowest run does so after T2 time units, then the ECDF becomes 1 for time=T2.
The ECDF remains 0 until the time T1 when the fastest run has solved its
corresponding problem instance.
Between T1 and T2, the ECDF is monotonously rising.
If can happen that an ECDF never reaches 1.
For example, in our two-problem situation described above, it could be that
our algorithm can only solve the first problem (all 10 runs succeed), but
never the second problem (all 10 runs fail).
Then, the ECDF will reach 0.5 but no higher value.
Of course, an algorithm is the better, the faster its ECDF rises and the
higher it rises.
In other words, the bigger the area under the ECDF curve, the better.
There are four tools involved in creating such diagrams:
The module `moptipy.utils.plot_utils` provides the functions for allocating
and storing figures (named `create_figure` and `save_figure`).
The module `moptipy.evaluation.progress` includes the class `Progress`.
An instance `Progress` represents a two-dimensional progress curve of a
single run.
Its time dimensions can either be `FEs` or `ms`. In the former case, it
records the time in objective function evaluations (FEs) and in the latter
case, it records them in milliseconds.
Its objective dimension can either be `plainF`, `scaledF` or `normalizedF`,
but for ECDFs, we would usually keep them `plainF`.
The module `moptipy.evaluation.ecdf` includes the class `Ecdf`.
An instance `Ecdf` represents a two-dimensional ECDF curve.
It can be constructed from a selection of `Progress` objects.
Matter of fact, a whole sequence of ECDF curves can automatically be
generated from a sequence of `Progress` objects, which are automatically
grouped by algorithm.
Finally, the module `moptipy.evaluation.plot_ecdf_impl` provides the
function `plot_ecdf`. This function accepts a sequence contain `Ecdf` objects
and illustrates them automatically. It will automatically choose axis labels,
colors, and group and order the data in a way that it deems reasonable.
In this file, we will use all the above tools.
We will run a small experiment, parse the resulting log files, and then
illustrate different groupings and selections of the data.
We will create svg figures and open them in the web browser for viewing.
"""
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.ecdf import from_progresses
from moptipy.evaluation.plot_ecdf import plot_ecdf
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 8-bit OneMax problem
problems = [lambda: OneMax(8)]
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())) # flip exactly one bit
ex.set_max_fes(256) # permit 256 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(256) # permit 256 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=31) # we will execute 31 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
ecdfs = [] # we will load the ECDFs into this list
# The below function uses the goal objective values from the log files to
# compute the ECDF functions. It groups all runs of one algorithm together
# and then computes the algorithm's overall ECDF.
from_progresses(data, ecdfs.append)
# Plot the ECDF 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_ecdf(ecdfs=ecdfs, # plot all the data
figure=fig) # 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="ecdf_over_log_fes", # 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_ecdf(ecdfs=ecdfs, # plot all the data
figure=fig, # into the figure
x_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="ecdf_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 ECDF 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.