# The Basics
In a nutshell, [Paref's algorithms](./moo-algorithms.md) let you target certain user-defined
[properties of Pareto points](./faqs.md) making the MOO as
efficient and fast as possible.
This makes Paref ideal for optimizing expensive blackbox functions.
## What is a property of a Pareto point?
Properties are basically a-priori Pareto point rankings of the user.
For example, the user is only interested in
1. the Pareto points minimizing some component
2. the 'real trade-off' closest to the theoretical global optimum (i.e. the utopia point)
Evaluating other Pareto points would just waste valuable resources and
an MOO algorithm targeting only those Pareto points
is desirable. With Paref you can do exactly that.
## Workflow
The workflow of Paref is simple:
1. Implement a blackbox function
2. Explore the target space (by calling the blackbox function's ``perform_lhc(n)`` method)
3. Apply a MOO algorithm reflecting your preference
4. Analyze the output
5. Repeat steps 2-4 until satisfied
Full Example
```python
import numpy as np
from paref.interfaces.moo_algorithms.blackbox_function import BlackboxFunction
from paref.blackbox_functions.design_space.bounds import Bounds
from paref.express.express_search import ExpressSearch
from paref.express.info import Info
# 1: Implement a blackbox function
class TestBlackboxFunction(BlackboxFunction): # Implement the blackbox function interface
def __call__(self, x) -> np.ndarray:
return np.array([x[0],
x[0] ** 2 + x[1] ** 2]) # The blackbox function f relation of design (x) and target (f(x))
@property
def dimension_design_space(self) -> int: # The dimension of the design space
return 2
@property
def dimension_target_space(self) -> int: # The dimension of the target space
return 2
@property
def design_space(
self) -> Bounds: # The bounds of the design space (lower bounds, upper bounds) as instance of the Bounds class
return Bounds(lower_bounds=[-1,-1], upper_bounds=[1,1])
bbf = TestBlackboxFunction() # Initialize the blackbox function
# 2: Explore the target space
bbf.perform_lhc(n=20) # exploration based on the Latin Hypercube Sampling
# 3: Apply a MOO algorithm reflecting your preference
moo = ExpressSearch(blackbox_function=bbf) # Create an instance of the Paref Express class
moo.minimal_search(max_evaluations=3) # perform the MOO algorithm
# 4: Analyze the output
print(f"Pareto front of bbf:\n {bbf.pareto_front}") # have a look at the Pareto front
info = Info(blackbox_function=bbf) # Create an instance of the Paref Info class
info.topology # have a look at the topology of the Pareto front
info.suggestion_pareto_points
# 3: Apply a MOO algorithm reflecting your preference
moo.priority_search(max_evaluations=5, priority=[80, 20]) # perform the MOO algorithm
# 4: Analyze the output
print(f"Pareto point matching your preference best: \n {moo.priority_point}")
bbf.save('./deleteme.npy') # save the evaluations
bbf.clear_evaluations()
print(f"Current evaluations: {bbf.evaluations}")
bbf.load('./deleteme.npy') # load the evaluations
print(f"Loaded evaluations: {bbf.evaluations}")
```
## The blackbox function
In Paref a blackbox function (bbf) is implemented in the [``BlackboxFunction``]() interface.
This requires to implement the following four methods/properties:
- evaluating a design
```python
def __call__(self, x: np.ndarray) -> np.ndarray:
return (value of the blackbox function at x)
```
[//]: # (> **❗️NOTE❗:️** In order to make the optimization process as efficient as possible, the target values should be scaled to be approximately in the same range)
[//]: # (> by multiplying (and subtracting) them by an appropriate **positive** constant. For example, if the)
[//]: # (> first component of the target values is in the range of -100 to -1000 and the second component in the range of 0.001 to 0.002,)
[//]: # (> this will mostly likely cause a bad optimization. Multiplying the first component by 0.001 and the second component by 1000)
[//]: # (> will stabilize the optimization.)
- the design space dimension
```python
@property
def dimension_design_space(self) -> int:
return (dimension of the design space)
```
- the target space dimension
```python
@property
def dimension_target_space(self) -> int:
return (dimension of the target space)
```
- the lower and upper bounds of the design space
```python
@property
def design_space(self) -> Bounds:
return Bounds(lower_bounds=[lower_bound_component_1,...], upper_bounds=[upper_bound_component_1,...])
```
Example
```python
import numpy as np
from paref.interfaces.moo_algorithms.blackbox_function import BlackboxFunction
from paref.blackbox_functions.design_space.bounds import Bounds
class TestBlackboxFunction(BlackboxFunction): # Implement the blackbox function interface
def __call__(self, x) -> np.ndarray:
return np.array([x[0]+x[1],
x[0]+x[2]]) # The blackbox function f relation of design (x) and target (f(x))
@property
def dimension_design_space(self) -> int: # The dimension of the design space
return 3
@property
def dimension_target_space(self) -> int: # The dimension of the target space
return 2
@property
def design_space(
self) -> Bounds: # The bounds of the design space (lower bounds, upper bounds) as instance of the Bounds class
return Bounds(lower_bounds=[-1,-1,-1], upper_bounds=[1,1,1])
bbf = TestBlackboxFunction() # Initialize the blackbox function
```
Everything else is handled by Paref:
- the exploration of the target space: ``bbf.perform_lhc(n_samples)`` (20 samples as a rule of thumb)
- storing the evaluations of the blackbox function (accessed by ``bbf.evaluations``, ``bbf.x`` and ``bbf.y``)
- saving and loading evaluations (``bbf.save(path)`` and ``bbf.load(path)``)
- calculating the pareto front (``bbf.pareto_front``)
## How to apply one of Paref Express' MOO algorithms?
Applying one of Paref's MOO algorithms is done in a single line of code:
Simply call the respective algorithm by determining the maximum number of evaluations of the blackbox function
Using the example above, the following code first explores the design space granting 20 evaluations,
then searches for the Pareto points on the edges of the Pareto front and lastly the 'real trade-off'
closest to the theoretical global optimum granting 5 evaluations in total:
```python
from paref.express.express_search import ExpressSearch
bbf.perform_lhc(n=20) # exploration based on the Latin Hypercube Sampling
moo = ExpressSearch(blackbox_function=bbf) # Create an instance of the Paref Express class
moo.minimal_search(max_evaluations=5) # perform the MOO algorithm
```
_After_ performing the minimal search, we can apply a quite useful algorithm:
An MOO reflecting the users preference for certain objectives.
For example, minimizing component one could be more important than minimizing component two, in numbers 80% and 20%.
Applying Paref Express' priority search will target the trade-off which reflects that priority:
```python
moo.priority_search(max_evaluations=5, priority=[80, 20]) # perform the MOO algorithm
```
Other algorithms of Paref Express are:
- a search for the minimum of a specific objective (``moo.search_for_minima``)
- a further search for a 'real trade-off' (``moo.search_for_best_real_trade_off``)
## How to analyze the output?
Paref Express' Info class provides a helpful tool to answer the following questions and guiding further MOO:
- are the target objectives conflicting?
- is there a specific trade-off which I should prefer (and look for)? If so, how and why?
- what is the dimension of the Pareto front (i.e. a point, a line, a plane etc.)?
- what is the "shape" of the Pareto front (i.e. convex, concave, linear)?
- what minimum value in each component can I expect?
```python
from paref.express.info import Info
info = Info(blackbox_function=bbf)
```
from here you can access the following information:
- topology: the shape of your Pareto front (``Info.topology``)
- a suggestion for Pareto points to evaluate, how and why (``Info.suggestion_pareto_points``)
- minima: the estimated minima of each component (``Info.minima``)
- model fitness: how well the model approximates the bbf, how to improve it and how certain its estimation is (``Info.model_fitness``)