Skip to content

Commit

Permalink
minor bug fixes, improvements, and added tests
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasWeise committed Jul 26, 2022
1 parent 3a4150c commit af22559
Show file tree
Hide file tree
Showing 13 changed files with 157 additions and 117 deletions.
21 changes: 18 additions & 3 deletions examples/log_file_jssp.py
Original file line number Diff line number Diff line change
@@ -1,6 +1,16 @@
"""This file shows how a single log file is generated in an experiment."""
"""
This file shows how a single log file is generated in an experiment.
As basis for the experiment, we use the Job Shop Scheduling Problem (JSSP)
and apply a simple Randomized Local Search (RLS) to it. We only perform one
single run on one single instance, the trivial `demo` instance. Afterwards,
we print the contents of the log file to the console. We also load the Gantt
chart that was the result of the experiment from the log file and print it,
too - just for fun.
"""
from moptipy.algorithms.rls import RLS # the algorithm we use
from moptipy.examples.jssp.experiment import run_experiment # the JSSP runner
from moptipy.examples.jssp.gantt import Gantt # Gantt chart data structure
from moptipy.operators.permutations.op0_shuffle import Op0Shuffle # 0-ary op
from moptipy.operators.permutations.op1_swap2 import Op1Swap2 # 1-ary op
from moptipy.utils.temp import TempDir # temp directory tool
Expand All @@ -21,7 +31,12 @@
n_runs=1, # perform exactly one run
n_threads=1) # use exactly one thread
# The random seed is automatically generated based on the instance name.
print(td.resolve_inside( # so we know algorithm, instance, and seed
file = td.resolve_inside( # so we know algorithm, instance, and seed
"rls_swap2/demo/rls_swap2_demo_0x5a9363100a272f12.txt")
.read_all_str()) # read file into string (which then gets printed)
print(file.read_all_str()) # read file into string and print contents

# One more example: Load the resulting Gantt chart from the log file
gantt = Gantt.from_log(file)
print(gantt)

# When leaving "while", the temp dir will be deleted
94 changes: 88 additions & 6 deletions moptipy/algorithms/ea.py
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,6 @@

from numpy.random import Generator

from moptipy.algorithms.utils import Record, _int_0, _float_0
from moptipy.api.algorithm import Algorithm2
from moptipy.api.operators import Op0, Op1, Op2
from moptipy.api.process import Process
Expand All @@ -39,6 +38,89 @@
from moptipy.utils.types import type_error


# start record
class _Record:
"""
A point in the search space, its quality, and creation time.
A record stores a point in the search space :attr:`x` together with
the corresponding objective value :attr:`f`. It also stores a
"iteration index" :attr:`it`, i.e., the time when the point was
created or modified.
This allows for representing and storing solutions in a population.
If the population is sorted, then records with better objective
value will be moved to the beginning of the list. Ties are broken
such that younger individuals (with higher :attr:`it` value) are
preferred.
"""

def __init__(self, x, f: Union[int, float]):
"""
Create the record.
:param x: the data structure for a point in the search space
:param f: the corresponding objective value
"""
#: the point in the search space
self.x: Final = x
#: the objective value corresponding to x
self.f: Union[int, float] = f
#: the iteration index when the record was created
self.it: int = 0

def __lt__(self, other) -> bool:
"""
Precedence if 1) better or b) equally good but younger.
:param other: the other record
:returns: `True` if this record has a better objective value
(:attr:`f`) or if it has the same objective value but is newer,
i.e., has a larger :attr:`it` value
>>> r1 = _Record(None, 10)
>>> r2 = _Record(None, 9)
>>> r1 < r2
False
>>> r2 < r1
True
>>> r1.it = 22
>>> r2.f = r1.f
>>> r2.it = r1.it
>>> r1 < r2
False
>>> r2 < r1
False
>>> r2.it = r1.it + 1
>>> r1 < r2
False
>>> r2 < r1
True
"""
f1: Final[Union[int, float]] = self.f
f2: Final[Union[int, float]] = other.f
return (f1 < f2) or ((f1 == f2) and (self.it > other.it))
# end record


def _int_0(_: int) -> int:
"""
Return an integer with value `0`.
:retval 0: always
"""
return 0


def _float_0() -> float:
"""
Return a float with value `0.0`.
:retval 0.0: always
"""
return 0.0


# start nobinary
class EA(Algorithm2):
"""
Expand Down Expand Up @@ -67,7 +149,7 @@ def solve(self, process: Process) -> None:
op0: Final[Callable] = self.op0.op0 # the nullary operator
op1: Final[Callable] = self.op1.op1 # the unary operator
op2: Final[Callable] = self.op2.op2 # the binary operator
br: Final[float] = self.__br # the application are of binary op
br: Final[float] = self.__br # the rate at which to use op2
should_terminate: Final[Callable] = process.should_terminate
r0i: Final[Callable[[int], int]] = cast( # only if m > 1, we
Callable[[int], int], random.integers # need random
Expand All @@ -86,7 +168,7 @@ def solve(self, process: Process) -> None:
if should_terminate(): # should we quit?
return # computational budget exhausted -> quit
f = evaluate(x) # continue? ok, evaluate new solution
lst[i] = Record(x, f) # create and store record
lst[i] = _Record(x, f) # create and store record

iteration: int = 1 # The first "real" iteration has index 1
while True: # lst: keep 0..mu-1, overwrite mu..mu+lambda-1
Expand All @@ -95,12 +177,12 @@ def solve(self, process: Process) -> None:
if should_terminate(): # only continue if we still...
return # have sufficient budget ... otherwise quit

offspring: Record = lst[oi] # pick offspring
offspring: _Record = lst[oi] # pick offspring
x = offspring.x # the point in search space we work on
offspring.it = iteration # mark as member of new iter.

pi: int = r0i(end) # random record in 0..end-1=unused
rec: Record = lst[pi] # pick the unused record
rec: _Record = lst[pi] # pick the unused record
if end > 1: # swap rec to the end to not use again
end = end - 1 # reduce number of unused records
lst[end], lst[pi] = rec, lst[end] # rec <-> end
Expand All @@ -110,7 +192,7 @@ def solve(self, process: Process) -> None:
# start binary
if r01() < br: # apply binary operator at rate br
pi = r0i(end) # random record in 0..end-1
rec2: Record = lst[pi] # pick second unused record
rec2: _Record = lst[pi] # get second unused record
if end > 1: # swap rec2 to end to not use again
end = end - 1 # reduce num. of unused records
lst[end], lst[pi] = rec2, lst[end]
Expand Down
11 changes: 0 additions & 11 deletions moptipy/algorithms/mo/nsga2.py
Original file line number Diff line number Diff line change
Expand Up @@ -251,11 +251,6 @@ def solve_mo(self, process: MOProcess) -> None:
pop.append(rec)
_non_dominated_sorting(pop, pop_size, domination)

for i, p in enumerate(pop):
for j in range(i + 1, len(pop)):
if p is pop[j]:
raise ValueError("!")

# create offspring records (initially empty)
for _ in range(pop_size):
pop.append(_NSGA2Record(create_x(), create_f()))
Expand Down Expand Up @@ -302,12 +297,6 @@ def solve_mo(self, process: MOProcess) -> None:
# "start" marks the begin of the last front that was created.
# "end" marks the total number of elements sorted.
# It holds that "start < pop_size <= end".

for i, p in enumerate(pop):
for j in range(i + 1, len(pop)):
if p is pop[j]:
raise ValueError("!")

end = _non_dominated_sorting(pop, pop_size, domination)
# We only perform the crowding distance computation on the first
# "end" elements in the population.
Expand Down
67 changes: 0 additions & 67 deletions moptipy/algorithms/utils.py

This file was deleted.

2 changes: 1 addition & 1 deletion moptipy/api/_mo_process_no_ss.py
Original file line number Diff line number Diff line change
Expand Up @@ -232,7 +232,7 @@ def _log_and_check_archive_entry(self, index: int, rec: MORecord,
if not isinstance(f, (int, float)):
raise type_error(f, "scalarized objective value", (int, float))
if not isfinite(f):
raise ValueError(f"scalaized objective value {f} is not finite")
raise ValueError(f"scalarized objective value {f} is not finite")

with logger.text(f"{PREFIX_SECTION_ARCHIVE}{index}"
f"{SUFFIX_SECTION_ARCHIVE_Y}") as lg:
Expand Down
2 changes: 1 addition & 1 deletion moptipy/api/_mo_process_ss.py
Original file line number Diff line number Diff line change
Expand Up @@ -174,7 +174,7 @@ def _log_and_check_archive_entry(self, index: int, rec: MORecord,
if not isinstance(f, (int, float)):
raise type_error(f, "scalarized objective value", (int, float))
if not isfinite(f):
raise ValueError(f"scalaized objective value {f} is not finite")
raise ValueError(f"scalarized objective value {f} is not finite")

with logger.text(f"{PREFIX_SECTION_ARCHIVE}{index}"
f"{SUFFIX_SECTION_ARCHIVE_X}") as lg:
Expand Down
3 changes: 3 additions & 0 deletions moptipy/api/component.py
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,9 @@ def __str__(self):
Get the default to-string implementation returns the class name.
:returns: the class name of this component
>>> print(Component())
Component
"""
s: Final[str] = type_name_of(self)
i: Final[int] = s.rfind(".")
Expand Down
17 changes: 1 addition & 16 deletions moptipy/api/mo_algorithm.py
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
"""The base classes for multi-objective optimization algorithms."""
from typing import cast

from moptipy.api.algorithm import Algorithm, check_algorithm
from moptipy.api.algorithm import Algorithm
from moptipy.api.mo_process import MOProcess
from moptipy.api.process import Process
from moptipy.utils.types import type_error
Expand Down Expand Up @@ -31,18 +31,3 @@ def solve_mo(self, process: MOProcess) -> None:
best-so-far solution, and takes care of creating log files (if
this is wanted).
"""


def check_mo_algorithm(algorithm: MOAlgorithm) -> MOAlgorithm:
"""
Check whether an object is a valid instance of :class:`MOAlgorithm`.
:param algorithm: the algorithm object
:return: the object
:raises TypeError: if `algorithm` is not an instance of
:class:`MOAlgorithm`
"""
check_algorithm(algorithm)
if not isinstance(algorithm, MOAlgorithm):
raise type_error(algorithm, "algorithm", MOAlgorithm)
return algorithm
25 changes: 24 additions & 1 deletion moptipy/api/mo_archive.py
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,24 @@ def __lt__(self, other) -> bool:
Compare for sorting.
:param other: the other record
>>> import numpy as np
>>> r1 = MORecord("a", np.array([1, 1, 1]))
>>> r2 = MORecord("b", np.array([1, 1, 1]))
>>> r1 < r2
False
>>> r2 < r1
False
>>> r2 = MORecord("b", np.array([1, 1, 2]))
>>> r1 < r2
True
>>> r2 < r1
False
>>> r1 = MORecord("a", np.array([2, 1, 1]))
>>> r1 < r2
False
>>> r2 < r1
True
"""
return lexicographic(self.fs, other.fs) < 0

Expand All @@ -46,8 +64,13 @@ def __str__(self):
Get the string representation of this record.
:returns: the string representation of this record
>>> import numpy as np
>>> r = MORecord(4, np.array([1, 2, 3]))
>>> print(r)
fs=1;2;3, x=4
"""
return f"fs={self.fs}, x={self.x}"
return f"fs={';'.join([str(a) for a in self.fs])}, x={self.x}"


class MOArchivePruner(Component):
Expand Down
7 changes: 0 additions & 7 deletions moptipy/evaluation/axis_ranger.py
Original file line number Diff line number Diff line change
Expand Up @@ -102,13 +102,6 @@ def __init__(self,
#: Did we detect a maximum?
self.__has_detected_max = False

def reset(self) -> None:
"""Reset the detected data, making the object ready for reuse."""
self.__has_detected_min = False
self.__has_detected_max = False
self.__detected_min = inf
self.__detected_max = _MIN_LOG_FLOAT if self.log_scale else -inf

def register_array(self, data: np.ndarray) -> None:
"""
Register a data array.
Expand Down
17 changes: 17 additions & 0 deletions moptipy/utils/text_format.py
Original file line number Diff line number Diff line change
Expand Up @@ -52,6 +52,23 @@ def add_format(s: str, bold: bool = False, italic: bool = False,
:param bold: should the format be bold face?
:param italic: should the format be italic face?
:param code: should the format be code face?
>>> from typing import cast
>>> st = "abc"
>>> type(st)
<class 'str'>
>>> fs = cast(FormattedStr, FormattedStr.add_format(st, bold=True))
>>> type(fs)
<class 'moptipy.utils.text_format.FormattedStr'>
>>> fs.bold
True
>>> fs.italic
False
>>> fs = cast(FormattedStr, FormattedStr.add_format(fs, italic=True))
>>> fs.bold
True
>>> fs.italic
True
"""
if isinstance(s, FormattedStr):
bold = bold or s.bold
Expand Down
Loading

0 comments on commit af22559

Please sign in to comment.