In practice, most EMAworkbench work is iterative. When optimizing, picking a useful value of function evaluations (nfe) is especially uncertain. Users are probably most likely to first pick a number of evaluations that fits their computational or temporal resources. If this is insufficient, they might then pick a larger number to run, or run the computations on a bigger computer (or cluster).
In these scenarios, it would be useful to take the policies on the Pareto front of the previous runs and give them to the optimizer as initial values/parents.
I see that robust_optimize() will eventually call _optimize() (as does optimize()), which then runs optimizer.run(). optimizer itself is declared in line 817 of em_framework.optimization as the chosen Platypus algorithm.
Attempted solution
Every algorithm in platypus is a child of platypus.algorithms.AbstractGeneticAlgorithm, which has the method initialize.
def initialize(self):
self.population = [self.generator.generate(self.problem) for _ in range(self.population_size)]
self.evaluate_all(self.population)
I replaced self.population with my own set, which was defined as a list that is syntactically compatible with platypus. Unfortunately, this actually took some time to figure out exactly how platypus defines its solutions. In pseudo-code, this is what we did:
Slice results data frame for just the policies and their lever choices
Define a little function to convert the levers into a format for platypus (binary)
Import ema_workbench.em_framework.optimization.to_robust_problem and platypus.EpsNSGAII (platypus.NSGAII also works; we didn't test other algorithms).
Generate as many solutions as we have Pareto policies (this step is mostly for proper syntax)
init_policies = [optimizer.generator.generate(optimizer.problem) for n in range(num_paretos)]
Replace the variables object in those initial solutions with our own
Print this list, and then copy and paste it into platypus's algorithms.py
Note: Regarding 2, our lever types were integers, but platypus stores them as some form of binary. For example, the lever 4 was saved in self.population.solution.variables (Solution() being the relevant object type here) as [[False], [True], [False], [False]] because the levers could range from 0-9 (9 being 1001 in binary). The size of this list seems to be set to that required by the largest value. When the lever was only 0 or 1, the platypus Solution variables were only either [True] or [False].
However, this approach didn't quite work. I don't have the debug codes anymore, but from what I recall, Platypus couldn't read our initial variables array even though it, to my eye, looked identical to theirs. Obviously, I missed something!
Further solution ideas
It seems we might be able to pass to NSGAII an archive argument.
We might be able to take advantage of the fact that, in the method platypus.NSGAII, we find on lines 179-188:
def step(self):
if self.nfe == 0:
self.initialize()
else:
self.iterate()
if self.archive is not None:
self.result = self.archive
else:
self.result = self.population
which is housed in Platypus's highest parent class, Algorithm. Here, in a while loop, it calls the child method step(), which is listed above.
Therefore, I think we could most succinctly achieve this goal by overriding ... something in Platypus by setting .nfe==1, which reflects that we are actually building off of an initial set of policies, and .archive or .population to a set of policy solutions that we have, which need to be a list of platypus.Solution() objects. I haven't gone back to try and understand how Platypus uses those objects though.
Additional notes:
Ideally, evaluators.optimize() could have a parameter initial_policies=None that the user can define directly from the results of the previous run.
If initial_policies are added and defined, then the parameter search_over should also be forced to uncertainties (check to make sure user hasn't erroneously defined it otherwise)
I don't know why I've been sitting on this so long, but I've now been on holiday/doing other work too long to naturally slide back into this very quickly. I'm happy to look at it again in September though!
Problem
In practice, most EMAworkbench work is iterative. When optimizing, picking a useful value of function evaluations (nfe) is especially uncertain. Users are probably most likely to first pick a number of evaluations that fits their computational or temporal resources. If this is insufficient, they might then pick a larger number to run, or run the computations on a bigger computer (or cluster).
In these scenarios, it would be useful to take the policies on the Pareto front of the previous runs and give them to the optimizer as initial values/parents.
I see that
robust_optimize()
will eventually call_optimize()
(as doesoptimize()
), which then runsoptimizer.run()
.optimizer
itself is declared in line 817 ofem_framework.optimization
as the chosen Platypus algorithm.Attempted solution
Every algorithm in platypus is a child of
platypus.algorithms.AbstractGeneticAlgorithm
, which has the methodinitialize
.I replaced
self.population
with my own set, which was defined as a list that is syntactically compatible with platypus. Unfortunately, this actually took some time to figure out exactly how platypus defines its solutions. In pseudo-code, this is what we did:ema_workbench.em_framework.optimization.to_robust_problem
andplatypus.EpsNSGAII
(platypus.NSGAII
also works; we didn't test other algorithms).init_policies = [optimizer.generator.generate(optimizer.problem) for n in range(num_paretos)]
variables
object in those initial solutions with our ownNote: Regarding 2, our lever types were integers, but platypus stores them as some form of binary. For example, the lever
4
was saved inself.population.solution.variables
(Solution()
being the relevant object type here) as[[False], [True], [False], [False]]
because the levers could range from 0-9 (9 being1001
in binary). The size of this list seems to be set to that required by the largest value. When the lever was only 0 or 1, the platypus Solution variables were only either[True]
or[False]
.However, this approach didn't quite work. I don't have the debug codes anymore, but from what I recall, Platypus couldn't read our initial variables array even though it, to my eye, looked identical to theirs. Obviously, I missed something!
Further solution ideas
It seems we might be able to pass to NSGAII an archive argument.
We might be able to take advantage of the fact that, in the method
platypus.NSGAII
, we find on lines 179-188:which is housed in Platypus's highest parent class,
Algorithm
. Here, in a while loop, it calls the child methodstep()
, which is listed above.Therefore, I think we could most succinctly achieve this goal by overriding ... something in Platypus by setting
.nfe==1
, which reflects that we are actually building off of an initial set of policies, and.archive
or.population
to a set of policy solutions that we have, which need to be a list ofplatypus.Solution()
objects. I haven't gone back to try and understand how Platypus uses those objects though.Additional notes:
evaluators.optimize()
could have a parameterinitial_policies=None
that the user can define directly from the results of the previous run.initial_policies
are added and defined, then the parametersearch_over
should also be forced touncertainties
(check to make sure user hasn't erroneously defined it otherwise)