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

Features/Multiobjective Optimization #756

Draft
wants to merge 24 commits into
base: dev
Choose a base branch
from
Draft

Features/Multiobjective Optimization #756

wants to merge 24 commits into from

Conversation

lensum
Copy link
Contributor

@lensum lensum commented Mar 26, 2021

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

@pep8speaks
Copy link

pep8speaks commented Mar 26, 2021

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

@esske
Copy link

esske commented Mar 26, 2021

@lensum

Comment on lines 412 to 488
def _add_parent_block_sets(self):
""""""
# set with all nodes
self.NODES = po.Set(initialize=[n for n in self.es.nodes])

# pyomo set for timesteps of optimization problem
self.TIMESTEPS = po.Set(
initialize=range(len(self.es.timeindex)), ordered=True
)

# previous timesteps
previous_timesteps = [x - 1 for x in self.TIMESTEPS]
previous_timesteps[0] = self.TIMESTEPS.last()

self.previous_timesteps = dict(zip(self.TIMESTEPS, previous_timesteps))

# pyomo set for all flows in the energy system graph
self.FLOWS = po.Set(
initialize=self.flows.keys(), ordered=True, dimen=2
)

self.BIDIRECTIONAL_FLOWS = po.Set(
initialize=[
k
for (k, v) in self.flows.items()
if hasattr(v, "bidirectional")
],
ordered=True,
dimen=2,
within=self.FLOWS,
)

self.UNIDIRECTIONAL_FLOWS = po.Set(
initialize=[
k
for (k, v) in self.flows.items()
if not hasattr(v, "bidirectional")
],
ordered=True,
dimen=2,
within=self.FLOWS,
)

def _add_parent_block_variables(self):
""""""

self.flow = po.Var(self.FLOWS, self.TIMESTEPS, within=po.Reals)

for (o, i) in self.FLOWS:
if self.flows[o, i].nominal_value is not None:
if self.flows[o, i].fix[self.TIMESTEPS[1]] is not None:
for t in self.TIMESTEPS:
self.flow[o, i, t].value = (
self.flows[o, i].fix[t]
* self.flows[o, i].nominal_value
)
self.flow[o, i, t].fix()
else:
for t in self.TIMESTEPS:
self.flow[o, i, t].setub(
self.flows[o, i].max[t]
* self.flows[o, i].nominal_value
)

if not self.flows[o, i].nonconvex:
for t in self.TIMESTEPS:
self.flow[o, i, t].setlb(
self.flows[o, i].min[t]
* self.flows[o, i].nominal_value
)
elif (o, i) in self.UNIDIRECTIONAL_FLOWS:
for t in self.TIMESTEPS:
self.flow[o, i, t].setlb(0)
else:
if (o, i) in self.UNIDIRECTIONAL_FLOWS:
for t in self.TIMESTEPS:
self.flow[o, i, t].setlb(0)
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Did you actually change something compared to the BaseModel class? If not, we would not need this code, because the methods are provided by the BaseClass, aren't they? (long time doing sub-classing coding in Python)

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

It is an addition compared to the BaseModel class, yes, but not compared to the Model class. So I think the MultiObjectiveModel class should inherit from the Model class instead of the BaseModel class to reduce code duplication.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hi, quite a nice feature and a bit related to the pending multiperiod feature #750.
It seems, the only difference here is amending the CONSTRAINT_GROUPS list by block.MultiObjectiveFlow. So letting the class inherit from the Model class seems like a good idea and would save you redefining the methods already given there.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The dependency is now fixed, together with a few linting errors. Codacy throws an error because of an import statement which it should ignore. I don't really know what to do there, suggestions are highly appreciated.
Other than that, all pipelines are passed.

Altering the AUTHORS.rst and CHANGELOG.rst created a merge conflict, so I reverted that commit. I will try to alter those files one by one, to see whether only one of them creates a merge conflict and then go on from there.

lensum and others added 12 commits April 26, 2021 12:46
* Adapted test_multiobjective/test_multiobjective.py to adhere to pep8 regarding linebreaks around operators
* removed unnecessary check for solver status
Removed unneeded imports and variables
* Corrected import order in tests
* Added missing documentation in Flow class
* Changed module docstrings for tests
@lensum
Copy link
Contributor Author

lensum commented Jun 10, 2021

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 lensum changed the title WIP: Features/Multiobjective Optimization Features/Multiobjective Optimization Jun 10, 2021
@jokochems
Copy link
Member

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.

…estions. Merge conflict resulted from merging upstream repository into fork.
@lensum
Copy link
Contributor Author

lensum commented Jun 14, 2021

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

@p-snft
Copy link
Member

p-snft commented Jun 16, 2021

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)

@lensum
Copy link
Contributor Author

lensum commented Jun 22, 2021

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?

Corrected wrong parenthesis in MultiObjective.Objective class
Added pareto()-function in MultiObjectiveModel to automatically
calculate pareto frontiert based on weighted sums of objectives.

Calculated weight combinations can be used to find optimal
multi-objective solution.
@p-snft p-snft marked this pull request as draft December 1, 2021 13:15
@jokochems
Copy link
Member

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.

@lensum
Copy link
Contributor Author

lensum commented Nov 15, 2022

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

7 participants