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

Introduce parameter "expected" for Flows #814

Draft
wants to merge 7 commits into
base: dev
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
103 changes: 103 additions & 0 deletions examples/expected_flow_value/expected_flow_value.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,103 @@
# -*- coding: utf-8 -*-

"""
General description
-------------------

A minimal example to show speedup of giving expected value to solver.

Installation requirements
-------------------------

This example requires oemof.solph, install by:

pip install oemof.solph
"""
import numpy as np
import pandas as pd

from oemof import solph

solver = "cbc" # 'glpk', 'gurobi',...
solver_verbose = True # show/hide solver output
time_steps = 24*31 # 8760

date_time_index = pd.date_range("1/1/2000", periods=time_steps, freq="H")

rng = np.random.default_rng(seed=1337)
random_costs = rng.exponential(size=time_steps)
random_demands = rng.uniform(size=time_steps)
random_losses = np.minimum(
np.ones(time_steps),
rng.exponential(scale=1e-1, size=time_steps)
)


def run_energy_system(index, costs, demands, losses, expected=None):
energy_system = solph.EnergySystem(timeindex=index)

if expected is None:
expected = {
(("bus", "storage"), "flow"): None,
(("source", "bus"), "flow"): None,
(("storage", "bus"), "flow"): None,
}

bus = solph.buses.Bus(label="bus")
source = solph.components.Source(
label="source",
outputs={bus: solph.flows.NonConvexFlow(
variable_costs=costs,
min=0.2,
nominal_value=100,
activity_costs=0.01,
)},
)
storage = solph.components.GenericStorage(
label="storage",
inputs={bus: solph.flows.Flow(
expected=expected[(("bus", "storage"), "flow")])},
outputs={bus: solph.flows.Flow(
expected=expected[(("storage", "bus"), "flow")])},
nominal_storage_capacity=1e4,
loss_rate=losses,
)
sink = solph.components.Sink(
label="sink",
inputs={
bus: solph.flows.Flow(nominal_value=1,
fix=demands)
},
)

energy_system.add(bus, source, sink, storage)
model = solph.Model(energy_system)
model.solve(solver=solver, solve_kwargs={
"tee": solver_verbose,
"warmstart": True,
})

_results = solph.processing.results(model)
_meta_results = solph.processing.meta_results(model)
return _results, _meta_results


meta_results = {}

results, meta_results["no hints 1"] = run_energy_system(
date_time_index, random_costs, random_demands, random_losses)
bus_data = solph.views.node(results, "bus")["sequences"]
_, meta_results["no hints 2"] = run_energy_system(
date_time_index, random_costs, random_demands, random_losses)
_, meta_results["with hints"] = run_energy_system(
date_time_index, random_costs, random_demands, random_losses,
expected=bus_data,
)
_, meta_results["no hints 3"] = run_energy_system(
date_time_index, random_costs, random_demands, random_losses)

for meta_result in meta_results:
print("Time to solve run {run}: {time:.2f} s".format(
run=meta_result,
time=meta_results[meta_result]['solver']['Wallclock time'])
)
11 changes: 9 additions & 2 deletions src/oemof/solph/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -364,10 +364,10 @@ def _add_parent_block_variables(self):

for (o, i) in self.FLOWS:
if self.flows[o, i].nominal_value is not None:
if self.flows[o, i].fix[self.TIMESTEPS.at(1)] is not None:
if self.flows[o, i].fixed is True:
for t in self.TIMESTEPS:
self.flow[o, i, t].value = (
self.flows[o, i].fix[t]
self.flows[o, i].value[t]
* self.flows[o, i].nominal_value
)
self.flow[o, i, t].fix()
Expand All @@ -387,6 +387,13 @@ def _add_parent_block_variables(self):
elif (o, i) in self.UNIDIRECTIONAL_FLOWS:
for t in self.TIMESTEPS:
self.flow[o, i, t].setlb(0)

if self.flows[o, i].value[self.TIMESTEPS.at(1)] is not None:
for t in self.TIMESTEPS:
self.flow[o, i, t].value = (
self.flows[o, i].value[t]
* self.flows[o, i].nominal_value
)
Comment on lines +393 to +396
Copy link
Member Author

Choose a reason for hiding this comment

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

This statement does not seem to have any effect. Also, I did not see any hint in the documentation that assignment is possible. (However, it seems to work for the fixed entries.)

Copy link
Member Author

Choose a reason for hiding this comment

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

I found examples that show the value being set that way in the documentation (https://pyomo.readthedocs.io/en/stable/working_models.html#changing-the-model-or-data-and-re-solving). Pyomo values are fixed, so thzis method seems to work. However, in the LP files, there is no starting value. Also, there is no run time difference.

As the documentation for Pyomo variables says that initialisation "is particularly important for non-linear models", I feel that the starting value might be simply ignored for linear variables.

else:
if (o, i) in self.UNIDIRECTIONAL_FLOWS:
for t in self.TIMESTEPS:
Expand Down
35 changes: 25 additions & 10 deletions src/oemof/solph/flows/_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -49,11 +49,13 @@ class Flow(on.Edge):
calculated by multiplying :attr:`nominal_value` with :attr:`max`
min : numeric (iterable or scalar), :math:`f_{min}`
Normed minimum value of the flow (see :attr:`max`).
fix : numeric (iterable or scalar), :math:`f_{actual}`
fix : numeric (iterable or scalar), :math:`f_{fix}`
Normed fixed value for the flow variable. Will be multiplied with the
:attr:`nominal_value` to get the absolute value. If :attr:`fixed` is
set to :obj:`True` the flow variable will be fixed to `fix
set, the flow variable will be fixed to `fix
* nominal_value`, i.e. this value is set exogenous.
expected: numeric (iterable or scalar),
Normed expected value for the flow variable (cf. fix).
positive_gradient : :obj:`dict`, default: `{'ub': None, 'costs': 0}`
A dictionary containing the following two keys:

Expand Down Expand Up @@ -87,10 +89,6 @@ class Flow(on.Edge):
The costs associated with one unit of the flow. If this is set the
costs will be added to the objective expression of the optimization
problem.
fixed : boolean
Boolean value indicating if a flow is fixed during the optimization
problem to its ex-ante set value. Used in combination with the
:attr:`fix`.
investment : :class:`Investment <oemof.solph.options.Investment>`
Object indicating if a nominal_value of the flow is determined by
the optimization problem. Note: This will refer all attributes to an
Expand Down Expand Up @@ -165,14 +163,18 @@ def __init__(self, **kwargs):
"nonconvex",
"integer",
]
sequences = ["fix", "variable_costs", "min", "max"]
batch_sequences = ["variable_costs", "min", "max"]
manual_sequences = ["expected", "fix"]
dictionaries = ["positive_gradient", "negative_gradient"]
defaults = {
"variable_costs": 0,
"positive_gradient": {"ub": None},
"negative_gradient": {"ub": None},
}
keys = [k for k in kwargs if k != "label"]

handled_keys = manual_sequences + ["label"]

keys = [k for k in kwargs if k not in handled_keys]

if "fixed_costs" in keys:
raise AttributeError(
Expand Down Expand Up @@ -211,6 +213,19 @@ def __init__(self, **kwargs):
if kwargs.get("max") is None:
defaults["max"] = 1

self.fixed = False
if kwargs.get("expected") is not None:
self.value = sequence(kwargs.pop("expected"))
if kwargs.get("fix") is not None:
raise ValueError(
"It is not possible to give both, expected and fix values."
)
elif kwargs.get("fix") is not None:
self.value = sequence(kwargs.pop("fix"))
self.fixed = True
else:
self.value = sequence(None)

# Check gradient dictionaries for non-valid keys
for gradient_dict in ["negative_gradient", "positive_gradient"]:
if gradient_dict in kwargs:
Expand All @@ -222,7 +237,7 @@ def __init__(self, **kwargs):
)
raise AttributeError(msg.format(gradient_dict))

for attribute in set(scalars + sequences + dictionaries + keys):
for attribute in set(scalars + batch_sequences + dictionaries + keys):
value = kwargs.get(attribute, defaults.get(attribute))
if attribute in dictionaries:
setattr(
Expand All @@ -235,7 +250,7 @@ def __init__(self, **kwargs):
setattr(
self,
attribute,
sequence(value) if attribute in sequences else value,
sequence(value) if attribute in batch_sequences else value,
)

# Checking for impossible attribute combinations
Expand Down
8 changes: 4 additions & 4 deletions src/oemof/solph/flows/_investment_flow.py
Original file line number Diff line number Diff line change
Expand Up @@ -198,7 +198,7 @@ class InvestmentFlowBlock(ScalarBlock):
", "Variable investment costs"
":math:`c_{invest,fix}`", ":py:obj:`flows[i, o].investment.offset`", "
Fix investment costs"
":math:`f_{actual}`", ":py:obj:`flows[i, o].fix[t]`", "Normed
":math:`f_{fix}`", ":py:obj:`flows[i, o].value[t]`", "Normed
fixed value for the flow variable"
":math:`f_{max}`", ":py:obj:`flows[i, o].max[t]`", "Normed maximum
value of the flow"
Expand Down Expand Up @@ -265,11 +265,11 @@ def _create(self, group=None):
)

self.FIXED_INVESTFLOWS = Set(
initialize=[(g[0], g[1]) for g in group if g[2].fix[0] is not None]
initialize=[(g[0], g[1]) for g in group if g[2].fixed is True]
)

self.NON_FIXED_INVESTFLOWS = Set(
initialize=[(g[0], g[1]) for g in group if g[2].fix[0] is None]
initialize=[(g[0], g[1]) for g in group if g[2].fixed is False]
)

self.FULL_LOAD_TIME_MAX_INVESTFLOWS = Set(
Expand Down Expand Up @@ -347,7 +347,7 @@ def _investflow_fixed_rule(block, i, o, t):
"""
expr = m.flow[i, o, t] == (
(m.flows[i, o].investment.existing + self.invest[i, o])
* m.flows[i, o].fix[t]
* m.flows[i, o].value[t]
)

return expr
Expand Down
11 changes: 9 additions & 2 deletions tests/test_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -96,6 +96,7 @@ def test_flows_with_none_exclusion(self):
param_results[(b_el2, demand)]["scalars"].sort_index(),
pandas.Series(
{
"fixed": True,
"nominal_value": 1,
"max": 1,
"min": 0,
Expand All @@ -106,7 +107,7 @@ def test_flows_with_none_exclusion(self):
)
assert_frame_equal(
param_results[(b_el2, demand)]["sequences"],
pandas.DataFrame({"fix": self.demand_values}),
pandas.DataFrame({"value": self.demand_values}),
check_like=True,
)

Expand All @@ -117,6 +118,7 @@ def test_flows_without_none_exclusion(self):
self.es, exclude_none=False
)
scalar_attributes = {
'fixed': True,
"integer": None,
"investment": None,
"nominal_value": 1,
Expand All @@ -137,9 +139,14 @@ def test_flows_without_none_exclusion(self):
pandas.Series(scalar_attributes).sort_index(),
)
sequences_attributes = {
"fix": self.demand_values,
"value": self.demand_values,
}

default_sequences = ["value"]
for attr in default_sequences:
if attr not in sequences_attributes:
sequences_attributes[attr] = [None]

assert_frame_equal(
param_results[(b_el2, demand)]["sequences"],
pandas.DataFrame(sequences_attributes),
Expand Down