NAUTILUS Navigator example
This example goes through the basic functionalities of the NAUTILUS Navigator method.
We will consider a simple 2D Pareto front which we will define next alongside the method itself. Both objectives are to be minimized.
Because of the nature of navigation based interactive optimization methods, the idea of NAUTILUS Navigator is best demonstrated using some graphical user interface. One such interface can be found online.
[1]:
import numpy as np
from desdeo_mcdm.interactive.NautilusNavigator import NautilusNavigator
# half of a parabola to act as a Pareto front
f1 = np.linspace(1, 100, 50)
f2 = f1[::-1] ** 2
front = np.stack((f1, f2)).T
ideal = np.min(front, axis=0)
nadir = np.max(front, axis=0)
method = NautilusNavigator((front), ideal, nadir)
To start, we can invoke the start
method.
[2]:
req_first = method.start()
print(req_first)
print(req_first.content.keys())
<desdeo_mcdm.interactive.NautilusNavigator.NautilusNavigatorRequest object at 0x7fca70bdfac0>
dict_keys(['message', 'ideal', 'nadir', 'reachable_lb', 'reachable_ub', 'user_bounds', 'reachable_idx', 'step_number', 'steps_remaining', 'distance', 'allowed_speeds', 'current_speed', 'navigation_point'])
The returned object is a NautilusNavigatorRequest. The keys should give an idea of what the contents of the request are. We will explain most of them in this example.
At the moment, the nadir
, reachable_lb
and reachable_ub
are most interesting to us. Navigation starts from the nadir and will proceed towards the Pareto optimal front enclosed between the limits defined in reachable_lb
and reachable_ub
.
To interact with the method, we must fill out the response
member of req
. Let’s see the contents of the message in req
next.
[3]:
print(req_first.content["message"])
Please supply aspirations levels for each objective between the upper and lower bounds as `reference_point`. Specify a speed between 1-5 as `speed`. If going to a previous step is desired, please set `go_to_previous` to True, otherwise it should be False. Bounds for one or more objectives may also be specified as 'user_bounds'; when navigating,the value of the objectives present in the navigation points will not exceed the valuesspecified in 'user_bounds'.Lastly, if stopping is desired, `stop` should be True, otherwise it should be set to False.
We should define the required values and set them as keys of a dictionary. Before that, it is useful to see the bounds to know the currently feasible objective values.
[4]:
print(req_first.content["reachable_lb"])
print(req_first.content["reachable_ub"])
[1. 1.]
[ 100. 10000.]
[7]:
reference_point = np.array([50, 6000])
go_to_previous = False
stop = False
speed = 1
response = dict(reference_point=reference_point, go_to_previous=False, stop=False, speed=1, user_bounds=[None, None])
go_to_previous
should be set to False
unless we desire going to a previous point. stop
should be True
if we wish to stop, otherwise it should be False
. speed
is the speed of the navigation. It is not used internally in the method. To continue, we call iterate
with suppliying the req
object with a defined response
attribute. We should get a new request as a return value.
[8]:
req_first.response = response
req_snd = method.iterate(req_first)
print(req_snd.content["reachable_lb"])
print(req_snd.content["reachable_ub"])
[3.02040816 9.12286547]
[ 100. 10000.]
We see that the bounds have narrowed down as they should.
In reality, iterate
should be called multiple times in succession with the same response
contents. We can do this in a loop until the 30th step is computed, for example. NB: Steps are internally zero-index based.
[9]:
previous_requests = [req_first, req_snd]
req = req_snd
while method._step_number < 30:
req.response = response
req = method.iterate(req)
previous_requests.append(req)
print(req.content["reachable_lb"])
print(req.content["reachable_ub"])
print(req.content["step_number"])
[ 11.10204082 449.61307788]
[ 81.81632653 8081.64306539]
30
The region of reachable Pareto optimal solutions has narrowed down. Suppose now we wish to return to a previous step and change our preferences. Let’s say, step 14.
[10]:
# fetch the 14th step saved previously
req_14 = previous_requests[13]
print(req_14.content["reachable_lb"])
print(req_14.content["reachable_ub"])
print(req_14.content["step_number"])
req_14.response["go_to_previous"] = True
req_14.response["reference_point"] = np.array([50, 5000])
new_response = req_14.response
[ 5.04081633 123.25531029]
[ 91.91836735 9208.16493128]
14
When going to a previous point, the method assumes that the state the method was in during that point is fully defined in the request object given to it when calling iterate
with go_to_previous
being True
. This is why we saved the request previously in a list.
[11]:
req_14_new = method.iterate(req_14)
req = req_14_new
# remember to unser go_to_previous!
new_response["go_to_previous"] = False
# continue iterating for 16 steps
while method._step_number < 30:
req.response = new_response
req = method.iterate(req)
print("Old 30th step")
print(previous_requests[29].content["reachable_lb"])
print(previous_requests[29].content["reachable_ub"])
print(previous_requests[29].content["step_number"])
print("New 30th step")
print(req.content["reachable_lb"])
print(req.content["reachable_ub"])
print(req.content["step_number"])
Old 30th step
[ 11.10204082 449.61307788]
[ 81.81632653 8081.64306539]
30
New 30th step
[ 11.10204082 368.01332778]
[ 81.81632653 8081.64306539]
30
We can see a difference in the limits when we changed the preference point.
To find the final solution, we can iterate till the end.
[12]:
while method._step_number < 100:
req.response = new_response
req = method.iterate(req)
print(req.content["reachable_idx"])
19
When finished navigating, the method will return the index of the reached solution based on the supplied Pareto front. It is assumed that if decision variables also exist for the problem, they are stored elsewhere. The final index returned can then be used to find the corresponding decision variables to the found solution in objective space.