Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

V18 bounds #34

Merged
merged 10 commits into from
Feb 10, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
39 changes: 31 additions & 8 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,17 +6,40 @@
Optilab is a lightweight and flexible python framework for testing black-box optimization.

## Features
- Intuitive interface to quickly prototype and run optimizers and metamodels.
- High quality documentation.
- Objective functions, optimizers, plotting and data handling.
- CLI functionality to easily summarize results of previous experiments.
- Multiprocessing for faster computation.
- Intuitive interface to quickly prototype and run optimizers and metamodels.
- 📚 High quality documentation.
- 📈 Objective functions, optimizers, plotting and data handling.
- CLI functionality to easily summarize results of previous experiments.
- 🚀 Multiprocessing for faster computation.

## How to run
Optilab has been tested to work on the latest python versions. To install it, just run `make install`.
## How to install
Optilab has been tested to work on python versions 3.11 and above. To install it from PyPI, run:
```
pip install optilab
```
You can also install from source by cloning this repo and running:
```
make install
```

## Try the demos
If you're not sure how to start using optilab, see some examples in `demo` directory.
Learn how to use optilab by using our demo notebook. See `demo/tutorial.ipynb`.

## CLI tool
Optilab comes with a powerful CLI tool to easily summarize your experiments. It allows for plotting the results and performing statistical testing to check for statistical significance in optimization results.
```
Optilab CLI utility.
usage: python -m optilab [-h] [--hide_plots] [--test_y] [--test_evals] pickle_path

positional arguments:
pickle_path Path to pickle file or directory with optimization runs.

options:
-h, --help show this help message and exit
--hide_plots Hide plots when running the script.
--test_y Perform Mann-Whitney U test on y values.
--test_evals Perform Mann-Whitney U test on eval values.
```

## Docker
This project comes with a docker container. You can pull it from dockerhub:
Expand Down
488 changes: 488 additions & 0 deletions demo/tutorial.ipynb

Large diffs are not rendered by default.

2 changes: 1 addition & 1 deletion docs/conf.py
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
project = 'optilab'
copyright = '2025, Marcin Łojek'
author = 'Marcin Łojek'
release = '17'
release = '18'

# -- General configuration ---------------------------------------------------
# https://www.sphinx-doc.org/en/master/usage/configuration.html#general-configuration
Expand Down
9 changes: 2 additions & 7 deletions docs/index.rst
Original file line number Diff line number Diff line change
@@ -1,15 +1,10 @@
.. optilab documentation master file, created by
sphinx-quickstart on Sat Dec 7 23:13:57 2024.
You can adapt this file completely to your liking, but it should at least
contain the root `toctree` directive.

optilab documentation
=====================
Optilab is a python framework for black-box optimization.
Optilab is a python framework for black-box optimization. To learn more visit the official repository of this project at `github <https://github.com/mlojek/optilab>`_.

.. toctree::
:maxdepth: 2
:caption: Contents:

modules
optilab

5 changes: 3 additions & 2 deletions pyproject.toml
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
[project]
name = "optilab"
version = "17"
version = "18"
authors = [
{ name="mlojek", email="marcin.lojek@pw.edu.pl" },
]
Expand Down Expand Up @@ -31,7 +31,8 @@ dependencies = [
"cma==4.0.0",
"pandas==2.2.3",
"scikit-learn==1.5.2",
"shapely==2.0.6"
"shapely==2.0.6",
"jupyter"
]

[project.urls]
Expand Down
97 changes: 97 additions & 0 deletions src/optilab/data_classes/bounds.py
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
Class representing bounds of the search space.
"""

from copy import deepcopy
from dataclasses import dataclass
from typing import List

Expand Down Expand Up @@ -83,3 +84,99 @@ def random_point_list(self, num_points: int, dim: int) -> PointList:
Point: List of randomly sampled points from the search space.
"""
return PointList([self.random_point(dim) for _ in range(num_points)])

# search space bounds handling methods
def reflect(self, point: Point) -> Point:
"""
Handle bounds by reflecting the point back into the
search area.

Args:
point (Point): The point to handle.

Returns:
Point: Reflected point.
"""
new_x = []

for val in point.x:
if val < self.lower or val > self.upper:
val -= self.lower
remainder = val % (self.upper - self.lower)
relative_distance = val // (self.upper - self.lower)

if relative_distance % 2 == 0:
new_x.append(self.lower + remainder)
else:
new_x.append(self.upper - remainder)
else:
new_x.append(val)

point.x = new_x
return point

def wrap(self, point: Point) -> Point:
"""
Handle bounds by wrapping the point around the
search area.

Args:
point (Point): The point to handle.

Returns:
Point: Wrapped point.
"""
new_x = []

for val in point.x:
if val < self.lower or val > self.upper:
val -= self.lower
val %= self.upper - self.lower
val += self.lower
new_x.append(val)
else:
new_x.append(val)

point.x = new_x
return point

def project(self, point: Point) -> Point:
"""
Handle bounds by projecting the point onto the bounds
of the search area.

Args:
point (Point): The point to handle.

Returns:
Point: Projected point.
"""
new_x = []

for val in point.x:
if val < self.lower:
new_x.append(deepcopy(self.lower))
elif val > self.upper:
new_x.append(deepcopy(self.upper))
else:
new_x.append(val)

point.x = new_x
return point

def handle_bounds(self, point: Point, mode: str) -> Point:
"""
Function to choose the bound handling method by name of the method.

Args:
point (Point): The point to handle.
mode (str): Bound handling mode to use, choose from reflect, wrap or project.

Returns:
Point: Handled point.
"""
methods = {"reflect": self.reflect, "wrap": self.wrap, "project": self.project}
try:
return methods[mode](point)
except KeyError as err:
raise ValueError(f"Invalid mode {mode} in Bounds.handle_bounds!") from err
113 changes: 113 additions & 0 deletions tests/data_classes/bounds_handle/conftest.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,113 @@
"""
Pytest fixtures used in Bounds handling unit tests.
"""

import pytest

from optilab.data_classes import Bounds, Point


@pytest.fixture()
def example_bounds() -> Bounds:
"""
Example bounds used in bounds handling unit tests.
"""
return Bounds(10, 20)


@pytest.fixture()
def point_in_bounds() -> Point:
"""
Point that lies within the example_bounds fixture.
Expected values for bounds handlers is the same as the point.
"""
return Point([14])


@pytest.fixture()
def point_equal_lower_bound() -> Point:
"""
Point that lies on the lower bound of example_bounds fixture.
Expected values for all bounds handlers are equal to the point.
"""
return Point([10])


@pytest.fixture()
def point_equal_upper_bound() -> Point:
"""
Point that lies on the upper bound of example_bounds fixture.
Expected values for all bounds handlers are equal to the point.
"""
return Point([20])


@pytest.fixture()
def point_below_bounds() -> Point:
"""
Point that lies below the example_bounds fixture.
Expected values for bounds handlers are:
- reflect: 18
- wrap: 12
- project: 10
"""
return Point([2])


@pytest.fixture()
def point_above_bounds() -> Point:
"""
Point that lies above the example_bounds fixture.
Expected values for bounds handlers are:
- reflect: 17
- wrap: 13
- project: 20
"""
return Point([23])


@pytest.fixture
def point_twice_below_bounds() -> Point:
"""
Point that lies below the lower bound of the example_bounds, and the difference
in distance from the lower bound is bigger than the length of the bounds.
Expected values for bounds handlers are:
- reflect: 16
- wrap: 16
- project: 10
"""
return Point([-4])


@pytest.fixture
def point_twice_above_bounds() -> Point:
"""
Point that lies below the lower bound of the example_bounds, and the difference
in distance from the lower bound is bigger than the length of the bounds.
Expected values for bounds handlers are:
- reflect: 12
- wrap: 12
- project: 20
"""
return Point([32])


@pytest.fixture
def point_multidimensional() -> Point:
"""
A multidimensional point that has all the values of the previous fixtures.
Expected values for bounds handlers are:
- reflect: [14, 10, 20, 18, 17, 16, 12]
- wrap: [14, 10, 20, 12, 13, 16, 12]
- project: [14, 10, 20, 10, 20, 10, 20]
"""
return Point([14, 10, 20, 2, 23, -4, 32])


@pytest.fixture
def evaluated_point() -> Point:
"""
An evaluated point, with y value and is_evaluated set to True.
Used to check if the handled point has the same y and is_evaluated values.
"""
return Point(x=[14, 10, 20, -4], y=10.1, is_evaluated=True)
73 changes: 73 additions & 0 deletions tests/data_classes/bounds_handle/test_bounds_project.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,73 @@
"""
Unit tests for Bounds.project method.
"""


class TestBoundsProject:
"""
Unit tests for Bounds.project method.
"""

def test_point_in_bounds(self, example_bounds, point_in_bounds):
"""
Test if projection works as expected when the point lies within bounds.
"""
handled_point = example_bounds.project(point_in_bounds)
assert handled_point.x == [14]

def test_point_equal_lower_bound(self, example_bounds, point_equal_lower_bound):
"""
Test if projection works as expected when the point lies on the lower bound.
"""
handled_point = example_bounds.project(point_equal_lower_bound)
assert handled_point.x == [10]

def test_point_equal_upper_bound(self, example_bounds, point_equal_upper_bound):
"""
Test if projection works as expected when the point lies on the upper bound.
"""
handled_point = example_bounds.project(point_equal_upper_bound)
assert handled_point.x == [20]

def test_point_below_bounds(self, example_bounds, point_below_bounds):
"""
Test if projection works as expected when the point lies below the lower bound.
"""
handled_point = example_bounds.project(point_below_bounds)
assert handled_point.x == [10]

def test_point_above_bounds(self, example_bounds, point_above_bounds):
"""
Test if projection works as expected when the point lies above the upper bound.
"""
handled_point = example_bounds.project(point_above_bounds)
assert handled_point.x == [20]

def test_point_twice_below_bounds(self, example_bounds, point_twice_below_bounds):
"""
Test if projection works as expected when the point lies far below the lower bound.
"""
handled_point = example_bounds.project(point_twice_below_bounds)
assert handled_point.x == [10]

def test_point_twice_above_bounds(self, example_bounds, point_twice_above_bounds):
"""
Test if projection works as expected when the point lies far above the upper bound.
"""
handled_point = example_bounds.project(point_twice_above_bounds)
assert handled_point.x == [20]

def test_multidimensional(self, example_bounds, point_multidimensional):
"""
Test if projection works as expected for a multidimensional point.
"""
handled_point = example_bounds.project(point_multidimensional)
assert handled_point.x == [14, 10, 20, 10, 20, 10, 20]

def test_evaluated_point(self, example_bounds, evaluated_point):
"""
Test if projecting a point leaves y and is_evaluated members unchanged.
"""
handled_point = example_bounds.project(evaluated_point)
assert handled_point.y == 10.1
assert handled_point.is_evaluated
Loading