oemof-solph icon indicating copy to clipboard operation
oemof-solph copied to clipboard

Features/Multiobjective Optimization

Open lensum opened this issue 3 years ago • 9 comments

Hey there,

we have integrated a feature to solve an oemof-model using a multi-objective approach. For now only a weighted objective function approach is implemented, but other multi-objective algorithms could be integrated as well. The implementation is similar to that of the investment and nonconvex methods in using an additional attribute of Flow instances with an associated class in options.

Overview:

To solve a multi-objective optimisation in oemof, these new elements are introduced:

  • new class MultiObjectiveModel in module models
    • with new _add_objective()
    • with new solve()
  • new class MultiObjectiveFlow in module blocks
  • new class MultiObjective with a comfort class Objective in module options
  • new function _multiobjective_grouping and corresponding grouping in module groupings

The main idea is that flows can have different costs for different objectives by setting the multiobjective attribute of the Flow. This consists of key-value-pairs with the name for the partial objective function and an instance of the Objective nested class with the corresponding parameters.

Example: Adding a flow with two different costs for ecological and financial objectives:

import oemof.solph as solph
from oemof.solph.options import MultiObjective as mo

# create and add electrical source with two different prices
el_source = solph.Source(
        label='el_source',
        outputs={el_bus: solph.Flow(multiobjective=mo(
                ecological=mo.Objective(
                        variable_costs=15),
                financial=mo.Objective(
                        variable_costs=10)))})
energy_system.add(el_source_fix)

The implementation is therefore similar to the investment- and nonconvex-methods and extends the standard solph.GROUPINGS to aggregate all objectives with the same objective function handle.

The solve() function currently differentiates between single-objective and multi-objective optimization through the keyword argument optimization_type. Further attributes then depend on the chosen type, e.g. objective_weights.

Example: creating a mulitobjective Model and solving it with a weighted objective function:

# create a pyomo optimization problem
optimisation_model = solph.MultiObjectiveModel(energy_system)

# solve problem using glpk
solver_results = optimisation_model.solve(
        solver='glpk',
        optimization_type='weighted',
        objective_weights={'ecological': 0.4,
                            'financial': 0.5},)

As written above: Currently only weighted objective functions are implemented, but other multi-objective algorithms can be integrated in the future as well.

Insights:

Some further technical insights:

  • class MultiObjectiveModel
    • function _add_objective() only collects objective functions and does not build objective-attribute of model instance
      • this faciliates solving the same model instance with e.g. different weightings or even different objectives
      • adds default handle '_standard' for backwards compability
      • not previously collected objectives - e.g. from invest or normal variable_costs-attributes - are added to '_standard'-objective
    • the objective function is only build when calling solve() due to there being different attribute sets etc. for each type of optimization
    • the weighted sum approach currently uses the given weights directly without considerations for normalization or possible numeric problems
  • class MultiObjectiveFlow
    • adds no new constraints
    • uses same calculation for objectives as Flow split into different objective functions
  • nested class Objective in class MultiObjective
    • currently used only to faciliate setting parameters
    • could possibly be extended to include investment parameters for different objectives
  • the additional grouping works the same as that for nonconvex or investment flows by checking the multiobjective attribute

Questions:

We chose to create a new class MultiObjectiveModel for this feature, but also discussed integrating it into the Model class directly. Our reasoning was that our approach would not be API-breaking and therefore preferable. We also ensured, however, that the behaviour of the MultiObjectiveModel class defaults to that of the Model class. Which implementation would you prefer?

Best regards,

Contributors: west-a, esske, lensum, matvanbeek, matnpy

A little heads up: This contribution is still work in progress. The code performs as expected, but we do not fulfill your guidelines on Pull-Requests yet. E.g.:

  1. We have not yet checked if the tests run with tox (we worked on it with pytest)
  2. The documentation needs updating
  3. Changes must still be mentioned in CHANGELOG.rst
  4. We did not yet add our names to AUTHORS.rst

lensum avatar Mar 26 '21 12:03 lensum

Hello @lensum! Thanks for updating this PR. We checked the lines you've touched for PEP 8 issues, and found:

Line 604:9: E303 too many blank lines (2) Line 648:10: E225 missing whitespace around operator Line 679:55: E251 unexpected spaces around keyword / parameter equals Line 679:57: E251 unexpected spaces around keyword / parameter equals Line 684:49: E225 missing whitespace around operator

Comment last updated at 2021-07-08 14:07:12 UTC

pep8speaks avatar Mar 26 '21 12:03 pep8speaks

@lensum

esske avatar Mar 26 '21 12:03 esske

Hey there, I think this PR is almost ready, so I removed the "WIP" in the title. A maintainer would need to approve the running workflows. Codacy does not like the import statement for the new MultiObjectiveFlow, I think this also requires action from a maintainer. Due to bad style from our site, we cannot change the CHANGELOG.rst or the AUTHORS.rst without creating a merge conflict. Is there anything we need to do additionally?

lensum avatar Jun 10 '21 07:06 lensum

Hi @lensum,

I'm sorry, I'm not a maintainer myself. But what you can / should do beforehand is merge the current version of the oemof/dev branch into your project and resolve the merge conflicts. If you are a PyCharm user, this is pretty straightforward and you can just "click" together a combined version in the merge view. For the network/flow.py file, it seems that you are just lacking the latest additions.

jokochems avatar Jun 11 '21 06:06 jokochems

Thanks @jokochems, just did that. Seems to me, the only thing left is the approval of a maintainer :+1:

lensum avatar Jun 14 '21 14:06 lensum

Thanks for your effort.

I do understand you approach, but to represent the current feature level, other choices would be easier to maintain. In the end, it comes down to one sentence in your introduction:

other multi-objective algorithms could be integrated as well.

If you are going to implement something like this, it makes sense to explicitly add different types of cost. If this is not the case, I would opt for a solution that just adds the weighted cost:

import oemof.solph as solph

weights = {
    "ecological" 0.4,
    "financial": 0.6,
}

# create and add electrical source with two different prices
el_source = solph.Source(
        label='el_source',
        outputs={el_bus: solph.Flow(
                variable_costs=(weights["ecological"] *  15
                                + weights["financial"] * 10))})
energy_system.add(el_source_fix)

p-snft avatar Jun 16 '21 08:06 p-snft

Thanks for your effort.

I do understand you approach, but to represent the current feature level, other choices would be easier to maintain. In the end, it comes down to one sentence in your introduction:

other multi-objective algorithms could be integrated as well.

If you are going to implement something like this, it makes sense to explicitly add different types of cost. If this is not the case, I would opt for a solution that just adds the weighted cost:

import oemof.solph as solph

weights = {
    "ecological" 0.4,
    "financial": 0.6,
}

# create and add electrical source with two different prices
el_source = solph.Source(
        label='el_source',
        outputs={el_bus: solph.Flow(
                variable_costs=(weights["ecological"] *  15
                                + weights["financial"] * 10))})
energy_system.add(el_source_fix)

Thank you for the reply, @p-snft. True, the above method would also work. Then I guess the sensible thing to do is to put this MR on hold until we've added another method?

lensum avatar Jun 22 '21 06:06 lensum

There hasn't been any activity on this pull request in quite a while. Nonetheless, I think, it would be a good improvement to include after the release of v0.5.0 resp. v0.5.1.

jokochems avatar Nov 10 '22 13:11 jokochems

Thank you @jokochems for the reminder! I also think it would be best to wait for the release of v.0.5.0 and maybe include it in the release of v.0.5.1. However, so far the development has been a group effort (and I would like to keep it that way), so it might be difficult to keep up with the schedule for the release of v.0.5.1. We'll try!

lensum avatar Nov 15 '22 06:11 lensum