"""
Provide the functionality to access search and solution spaces.
A :class:`Space` is the abstraction of the data structures for solutions and
points in the search space that need to be generated, copied, and stored
during the optimization process. This allows us to develop black-box
algorithms while still being able to properly remember the best solutions,
storing them as text strings in log files, and to validate whether they are
correct.
All search or solution spaces in `moptipy` inherit from :class:`Space`. If
you implement a new space, you should test it with the pre-defined unit test
routine :func:`~moptipy.tests.space.validate_space`.
The following pre-defined spaces are currently available:
- :class:`~moptipy.spaces.bitstrings.BitStrings`, the space of `n`-dimensional
bit strings
- :class:`~moptipy.spaces.intspace.IntSpace`, a space of `n`-dimensional
integer strings, where each element is between predefined inclusive bounds
`min_value...max_value`.
- :class:`~moptipy.spaces.permutations.Permutations` is a special version of
the :class:`~moptipy.spaces.intspace.IntSpace` where all elements are
permutations of a base string
:attr:`~moptipy.spaces.permutations.Permutations.blueprint`. This means that
it can represent permutations both with and without repetitions. Depending
on the base string, each element may occur an element-specific number of
times. For the base string `(-1, -1, 2, 7, 7, 7)`, for example, `-1` may
occur twice, `2` can occur once, and `7` three times.
- :class:`~moptipy.spaces.vectorspace.VectorSpace` is the space of
`n`-dimensional floating point number vectors.
"""
from typing import Any
from pycommons.types import type_error
from moptipy.api.component import Component
# start book
[docs]
class Space(Component):
"""
A class to represent both search and solution spaces.
The space basically defines a container data structure and basic
operations that we can apply to them. For example, a solution
space contains all the possible solutions to an optimization
problem. All of them are instances of one data structure. An
optimization as well as a black-box process needs to be able to
create and copy such objects. In order to store the solutions we
found in a text file, we must further be able to translate them to
strings. We should also be able to parse such strings. It is also
important to detect whether two objects are the same and whether
the contents of an object are valid. All of this functionality is
offered by the `Space` class.
"""
[docs]
def create(self) -> Any:
# end book
"""
Generate an instance of the data structure managed by the space.
The state/contents of this data structure are undefined. It may
not pass the :meth:`validate` method.
:return: the new instance
"""
[docs]
def copy(self, dest, source) -> None: # +book
"""
Copy one instance of the data structure to another one.
Notice that the first parameter of this method is the destination,
which will be overwritten by copying the contents of the second
parameter over it.
:param dest: the destination data structure,
whose contents will be overwritten with those from `source`
:param source: the source data structure, which remains
unchanged and whose contents will be copied to `dest`
"""
[docs]
def to_str(self, x) -> str: # +book
"""
Obtain a textual representation of an instance of the data structure.
This method should convert an element of the space to a string
representation that is parseable by :meth:from_str: and should ideally
not be too verbose. For example, when converting a list or array `x`
of integers to a string, one could simply do
`";".join([str(xx) for xx in x])`, which would convert it to a
semicolon-separated list without any wasted space.
Notice that this method is used by the
:class:`~moptipy.utils.logger.Logger` when storing the final
optimization results in the log files in form of a
:class:`~moptipy.utils.logger.TextLogSection` created via
:meth:`~moptipy.utils.logger.Logger.text`. By implementing
:meth:from_str: and :meth:to_str: as exact inverse of each other, you
can thus ensure that you can always automatically load the results of
your optimization runs from the log files created via
:meth:`~moptipy.api.execution.Execution.set_log_file` of the
:class:`~moptipy.api.execution.Execution` class.
:param x: the instance
:return: the string representation of x
"""
[docs]
def from_str(self, text: str) -> Any: # +book
"""
Transform a string `text` to one element of the space.
This method should be implemented as inverse to :meth:to_str:.
It should check the validity of the result before returning it.
It may not always be possible to implement this method, but you
should try.
:param text: the input string
:return: the element in the space corresponding to `text`
"""
[docs]
def is_equal(self, x1, x2) -> bool: # +book
"""
Check if the contents of two instances of the data structure are equal.
:param x1: the first instance
:param x2: the second instance
:return: `True` if the contents are equal, `False` otherwise
"""
[docs]
def validate(self, x) -> None: # +book
"""
Check whether a given point in the space is valid.
This function should be implemented such that it very carefully checks
whether the argument `x` is a valid element of this space. It should
check the Python data type of `x` and the type of its components and
raise a `TypeError` if it does not match the exact requirements. It
should also check the value of each element of `x` whether it is
permitted. Once this function returns without throwing an exception,
the user can rely on that the data structure `x` is correct.
For example, if we have a space of
:mod:`~moptipy.spaces.permutations` of the values from `1` to `n`,
where the elements are represented as :class:`numpy.ndarray` objects,
then this function should first check whether `x` is indeed an
instance of :class:`numpy.ndarray`. If not, it should raise a
`TypeError`. Then it could check whether the length of `x` is indeed
`n` and raise a `ValueError` otherwise. It could then check whether
each element of `x` is from `1..n` *and* occurs exactly one time (and
otherwise raise a `ValueError`). Moreover, it should also check
whether the numpy `dtype` of `x` is an appropriate integer data type
and raise a `ValueError` otherwise. Hence, if this function checks an
element `x` and does not raise any error, the user can rely on that
this element `x` is, indeed, a fully valid permutation.
In our system, we use this method for example at the end of
optimization processes. Every solution that is written to a log file
must pass through this method. In other words, we ensure that only
valid solutions are stored. If the optimization algorithm or a search
operator has a bug and sometimes may produce invalid data structures,
this a) helps us in finding the bug and b) prevents us from storing
invalid solutions. It is also strongly encouraged that the
:meth:`from_str` method of any :class:`Space` implementation should
run its results through :meth:`validate`. Since :meth:`from_str` can
be used to, e.g., parse the data from the result sections of log
files, this ensures that no corrupted or otherwise invalid data is
parsed into an application.
See https://thomasweise.github.io/moptipy/#data-formats for more
information on log files.
:param x: the point
:raises TypeError: if the point `x` (or one of its elements, if
applicable) has the wrong data type
:raises ValueError: if the point `x` is invalid and/or simply is not
an element of this space
"""
[docs]
def n_points(self) -> int:
"""
Get the approximate number of different elements in the space.
This operation can help us when doing tests of the space API
implementations. If we know how many points exist in the space,
then we can judge whether a method that randomly generates
points is sufficiently random, for instance.
By default, this method simply returns `2`. If you have a better
approximation of the size of the space, then you should override it.
:return: the approximate scale of the space
"""
return 2
[docs]
def check_space(space: Any, none_is_ok: bool = False) -> Space | None:
"""
Check whether an object is a valid instance of :class:`Space`.
:param space: the object
:param none_is_ok: is it ok if `None` is passed in?
:return: the object
:raises TypeError: if `space` is not an instance of
:class:`~moptipy.api.space.Space`
>>> check_space(Space())
Space
>>> check_space(Space(), True)
Space
>>> check_space(Space(), False)
Space
>>> try:
... check_space('A')
... except TypeError as te:
... print(te)
space should be an instance of moptipy.api.space.\
Space but is str, namely 'A'.
>>> try:
... check_space('A', True)
... except TypeError as te:
... print(te)
space should be an instance of moptipy.api.space.\
Space but is str, namely 'A'.
>>> try:
... check_space('A', False)
... except TypeError as te:
... print(te)
space should be an instance of moptipy.api.space.\
Space but is str, namely 'A'.
>>>
>>> try:
... check_space(None)
... except TypeError as te:
... print(te)
space should be an instance of moptipy.api.space.\
Space but is None.
>>> print(check_space(None, True))
None
>>> try:
... check_space(None, False)
... except TypeError as te:
... print(te)
space should be an instance of moptipy.api.space.\
Space but is None.
"""
if isinstance(space, Space):
return space
if none_is_ok and (space is None):
return None
raise type_error(space, "space", Space)