Skip to content

Commit

Permalink
added new temperature schedule
Browse files Browse the repository at this point in the history
  • Loading branch information
thomasWeise committed Nov 1, 2024
1 parent ff4b076 commit e970718
Show file tree
Hide file tree
Showing 7 changed files with 230 additions and 8 deletions.
2 changes: 1 addition & 1 deletion make.sh
Original file line number Diff line number Diff line change
Expand Up @@ -53,7 +53,7 @@ while ! (timeout --kill-after=60m 58m "$PYTHON_INTERPRETER" -m pip install --no-
cycle=$((cycle+1))
if (("$cycle" > 100)) ; then
echo "$(date +'%0Y-%0m-%0d %0R:%0S'): Something odd is happening: We have performed $cycle cycles of pip install and all failed. That's too many. Let's quit."
exit 2
exit 2 # A non-zero exit code indicates failure.
fi
echo "$(date +'%0Y-%0m-%0d %0R:%0S'): pip install failed, we will try again."
done
Expand Down
2 changes: 1 addition & 1 deletion make_venv.sh
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@ while ! (timeout --kill-after=60m 58m "$PYTHON_INTERPRETER" -m pip install --no-
cycle=$((cycle+1))
if (("$cycle" > 100)) ; then
echo "$(date +'%0Y-%0m-%0d %0R:%0S'): Something odd is happening: We have performed $cycle cycles of pip install and all failed. That's too many. Let's quit."
exit 2
exit 2 # A non-zero exit code indicates failure.
fi
echo "$(date +'%0Y-%0m-%0d %0R:%0S'): pip install failed, we will try again."
done
Expand Down
226 changes: 224 additions & 2 deletions moptipy/algorithms/modules/temperature_schedule.py
Original file line number Diff line number Diff line change
Expand Up @@ -18,12 +18,14 @@
evaluation is at index `0`.
"""

from math import e, isfinite, log
from math import e, inf, isfinite, log, nextafter
from typing import Final

from pycommons.types import type_error
from pycommons.strings.enforce import enforce_non_empty_str_without_ws
from pycommons.types import check_int_range, type_error

from moptipy.api.component import Component
from moptipy.api.objective import Objective, check_objective
from moptipy.utils.logger import KeyValueLogSection
from moptipy.utils.strings import num_to_str_for_name

Expand Down Expand Up @@ -273,3 +275,223 @@ def __str__(self) -> str:
"""
return (f"ln{num_to_str_for_name(self.t0)}_"
f"{num_to_str_for_name(self.epsilon)}")


#: the default maximum range
_DEFAULT_MAX_RANGE: Final[float] = inf

#: the default minimum range
_DEFAULT_MIN_RANGE: Final[float] = nextafter(0.0, _DEFAULT_MAX_RANGE)


class ExponentialScheduleBasedOnRange(ExponentialSchedule):
"""
An exponential schedule configured based on the objective's range.
This exponential schedule takes an objective function as parameter.
It uses the lower and the upper bound of this function, `LB` and `UB`,
to select a start and end temperature based on the provided fractions.
Here, we set `R = UB - LB`.
Roughly, the start temperature will be `R * start_range_frac` and
the end temperature, to be reached after `n_steps` FEs, will be
`R * end_range_frac`.
If one of `UB` or `LB` is not provided, we use `R = max(1, abs(other))`.
If neither is provided, we set `R = 1`.
Since sometimes the upper and lower bound may be excessivly large, we
can provide limits for `R` in form of `min_range` and `max_range`.
This will then override any other computation.
Notice that it is expected that `tau == 0` when the temperature function
is first called. It is expected that `tau == n_range - 1` when it is
called for the last time.
>>> from moptipy.examples.bitstrings.onemax import OneMax
>>> es = ExponentialScheduleBasedOnRange(OneMax(10), 0.01, 0.0001, 10**8)
>>> es.temperature(0)
0.1
>>> es.temperature(1)
0.09999999539482989
>>> es.temperature(10**8 - 1)
0.0010000000029841878
>>> es.temperature(10**8)
0.0009999999569324865
>>> es = ExponentialScheduleBasedOnRange(
... OneMax(10), 0.01, 0.0001, 10**8, max_range=5)
>>> es.temperature(0)
0.05
>>> es.temperature(1)
0.04999999769741494
>>> es.temperature(10**8 - 1)
0.0005000000014920939
>>> es.temperature(10**8)
0.0004999999784662432
>>> try:
... ExponentialScheduleBasedOnRange(1, 0.01, 0.0001, 10**8)
... except TypeError as te:
... print(te)
objective function should be an instance of moptipy.api.objective.\
Objective but is int, namely 1.
>>> try:
... ExponentialScheduleBasedOnRange(OneMax(10), 12.0, 0.0001, 10**8)
... except ValueError as ve:
... print(ve)
Invalid fraction range 12.0, 0.0001.
>>> try:
... ExponentialScheduleBasedOnRange(OneMax(10), 0.9, 0.0001, 1)
... except ValueError as ve:
... print(ve)
n_steps=1 is invalid, must be in 2..1000000000000000.
"""

def __init__(self, f: Objective, start_range_frac: float,
end_range_frac: float, n_steps: int,
min_range: int | float = _DEFAULT_MIN_RANGE,
max_range: int | float = _DEFAULT_MAX_RANGE) -> None:
"""
Initialize the range-based exponential schedule.
:param f: the objective function whose range we will use
:param start_range_frac: the starting fraction of the range to use for
the temperature
:param end_range_frac: the end fraction of the range to use for the
temperature
:param n_steps: the number of steps until the end range should be
reached
"""
f = check_objective(f)
if not isinstance(start_range_frac, float):
raise type_error(start_range_frac, "start_range_frac", float)
if not isinstance(end_range_frac, float):
raise type_error(end_range_frac, "end_range_frac", float)
if not (isfinite(start_range_frac) and isfinite(end_range_frac) and (
1 >= start_range_frac > end_range_frac >= 0)):
raise ValueError("Invalid fraction range "
f"{start_range_frac}, {end_range_frac}.")
if not isinstance(max_range, int | float):
raise type_error(max_range, "max_range", (int, float))
if not isinstance(min_range, int | float):
raise type_error(min_range, "min_range", (int, float))
if not (0 < min_range < max_range):
raise ValueError(
f"Invalid range delimiters {min_range}, {max_range}.")
#: the start objective range fraction
self.start_range_frac: Final[float] = start_range_frac
#: the end objective range fraction
self.end_range_frac: Final[float] = end_range_frac
#: the minimum objective range
self.min_range: Final[int | float] = min_range
#: the maximum objective range
self.max_range: Final[int | float] = max_range
#: the number of steps that we will perform until reaching the end
#: range fraction temperature
self.n_steps: Final[int] = check_int_range(
n_steps, "n_steps", 2, 1_000_000_000_000_000)

#: the name of the objective function used
self.used_objective: Final[str] = enforce_non_empty_str_without_ws(
str(f))

flb: Final[float | int] = f.lower_bound()
fub: Final[float | int] = f.upper_bound()
f_range: float | int = 1

if isfinite(flb):
if isfinite(fub):
if flb >= fub:
raise ValueError(
"objective function lower bound >= upper bound: "
f"{flb}, {fub}?")
f_range = fub - flb
if not isfinite(f_range) or (f_range <= 0):
raise ValueError(
f"Invalid bound range: {fub} - {flb} = {f_range}")
else:
f_range = max(abs(flb), 1)
elif isfinite(fub):
f_range = max(abs(fub), 1)
f_range = min(max_range, max(min_range, f_range))

#: the upper bound used for the objective range computation
self.f_upper_bound: Final[int | float] = fub
#: the lower bound used for the objective range computation
self.f_lower_bound: Final[int | float] = flb
#: The range of the objective function as used for the temperature
#: computation.
self.f_range: Final[int | float] = f_range

#: the start temperature
t0: Final[float] = start_range_frac * f_range
te: Final[float] = end_range_frac * f_range
if not (isfinite(t0) and isfinite(te) and (t0 > te)):
raise ValueError(
f"Invalid range {start_range_frac}, {end_range_frac}, "
f"{f_range} leading to temperatures {t0}, {te}.")
#: the end temperature
self.te: Final[float] = te

epsilon: Final[float] = 1 - (te / t0) ** (1 / (n_steps - 1))
if not (isfinite(epsilon) and (0 < epsilon < 1) and (
0 < (1 - epsilon) < 1)):
raise ValueError(
f"Invalid computed epsilon {epsilon} resulting from range "
f"{start_range_frac}, {end_range_frac}, {f_range} leading "
f"to temperatures {t0}, {te}.")
super().__init__(t0, epsilon)

def log_parameters_to(self, logger: KeyValueLogSection) -> None:
"""
Log all parameters of the configured exponential temperature schedule.
:param logger: the logger for the parameters
>>> from moptipy.utils.logger import InMemoryLogger
>>> from moptipy.examples.bitstrings.onemax import OneMax
>>> with InMemoryLogger() as l:
... with l.key_values("C") as kv:
... ExponentialScheduleBasedOnRange(
... OneMax(10), 0.1, 0.01, 10**8).log_parameters_to(kv)
... text = l.get_log()
>>> text[1]
'name: expR0d1_0d01'
>>> text[3]
'T0: 1'
>>> text[4]
'e: 2.3025850892643973e-8'
>>> len(text)
21
"""
super().log_parameters_to(logger)
logger.key_value("startRangeFrac", self.start_range_frac)
logger.key_value("endRangeFrac", self.end_range_frac)
logger.key_value("maxRange", self.max_range)
logger.key_value("minRange", self.min_range)
logger.key_value("usedObjective", self.used_objective)
logger.key_value("fLb", self.f_lower_bound)
logger.key_value("fUb", self.f_upper_bound)
logger.key_value("nSteps", self.n_steps)
logger.key_value("fRange", self.f_range)
logger.key_value("te", self.te)

def __str__(self) -> str:
"""
Get the string representation of the configured exponential schedule.
:returns: the name of this schedule
>>> from moptipy.examples.bitstrings.onemax import OneMax
>>> ExponentialScheduleBasedOnRange(OneMax(10), 0.01, 0.0001, 10**8)
expR0d01_0d0001
"""
base: Final[str] = (
f"expR{num_to_str_for_name(self.start_range_frac)}_"
f"{num_to_str_for_name(self.end_range_frac)}")
if (self.min_range != _DEFAULT_MIN_RANGE) or (
self.max_range != _DEFAULT_MAX_RANGE):
if self.min_range == _DEFAULT_MIN_RANGE:
return f"{base}_{num_to_str_for_name(self.max_range)}"
return (f"{base}_{num_to_str_for_name(self.min_range)}_"
f"{num_to_str_for_name(self.max_range)}")
return base
2 changes: 1 addition & 1 deletion moptipy/version.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,4 +2,4 @@
from typing import Final

#: the version string of `moptipy`
__version__: Final[str] = "0.9.131"
__version__: Final[str] = "0.9.132"
2 changes: 1 addition & 1 deletion requirements-dev.txt
Original file line number Diff line number Diff line change
Expand Up @@ -33,4 +33,4 @@
#

# pycommons provides lots of utilities
pycommons[dev] == 0.8.56
pycommons[dev] == 0.8.57
2 changes: 1 addition & 1 deletion requirements.txt
Original file line number Diff line number Diff line change
Expand Up @@ -39,7 +39,7 @@ psutil == 6.0.0

# pycommons offers many of the tools and utilities used in moptipy that are
# not related to optimization.
pycommons == 0.8.56
pycommons == 0.8.57

# scikit-learn is used to obtain some clusters of JSSP instances for our
# experiments.
Expand Down
2 changes: 1 addition & 1 deletion setup.cfg
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ install_requires =
matplotlib >= 3.9.2
pdfo >= 2.2.0
psutil >= 6.0.0
pycommons >= 0.8.56
pycommons >= 0.8.57
scikit-learn >= 1.5.2
scipy >= 1.14.1
packages = find:
Expand Down

0 comments on commit e970718

Please sign in to comment.