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/schedule for flow #666

Open
wants to merge 53 commits into
base: dev
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 29 commits
Commits
Show all changes
53 commits
Select commit Hold shift + click to select a range
c34d5e7
Add schedule for flows
Dec 9, 2019
c49a010
Adapted existing test files
Dec 9, 2019
0887645
Adapted existing tests for scheduled flows
Dec 9, 2019
b760396
added constraint and variables to docstring
Dec 10, 2019
ea2e2ff
Added flexible schedule
Dec 10, 2019
78fceff
Added new `whatsnew` version file
Dec 10, 2019
8bd7dda
Fixed if statement according to Style Guide
Dec 10, 2019
650f9ec
Fixed boolean logic mistake
Dec 10, 2019
e264e5a
Clearified docstring for penalty parameters
Dec 10, 2019
a9ca25a
Clarified doc for _pos _neg parameters/variables
Dec 10, 2019
46cc201
Improved code to pep8 standards
Dec 10, 2019
c7882a9
Improved code to pep8 standards
Dec 10, 2019
d5e5f6c
Added constraint test for scheduled flows
Dec 10, 2019
2d89775
Added flow_scheduled.lp file for testing
Dec 10, 2019
151576d
Improve test for flow schedule
Dec 10, 2019
97d94b0
Merge branch 'dev' into features/schedule_for_flow
c-koehl Dec 10, 2019
31397a1
Rename slack_neg and slack_pos to schedule_slack
Jan 13, 2020
bd43f3e
Deleted unnecessary print statement
Jan 13, 2020
a7709af
Deleted unnecessary else statement
Jan 13, 2020
cbabaa7
Replaced lp file due to new variable names
Jan 13, 2020
f71ae4a
Replaced lp file due to new variable names
Jan 13, 2020
54fa08e
Merge branch
Jan 13, 2020
4a7106f
Renamed added parameters
Feb 10, 2020
c65349c
add csv
Feb 12, 2020
fab3209
renamed csv file
Feb 12, 2020
810ea70
Merge upstream dev into schedule_for_flows
uvchik Apr 7, 2020
ecf0c67
Fix merge in blocks
uvchik Apr 7, 2020
0b07a2c
Merge branch 'dev' into features/schedule_for_flow
uvchik Apr 7, 2020
36d7ad0
Fix pep8 issues
uvchik Apr 7, 2020
2a66fc2
Fix codacy issue
uvchik Apr 7, 2020
abab4ea
Fix another codacy issue
uvchik Apr 7, 2020
5f63483
Remove default value dor schedule penalty costs
Apr 10, 2020
3048635
mind the dynamic property of sequence
Apr 10, 2020
5b5d328
fixed logical bug in schedule attribute validation
Apr 10, 2020
15d32d1
fixed logical bug in schedule attribute validation
Apr 10, 2020
0cd492b
Adapt tests to removed schedulecosts default value
Apr 10, 2020
564c7e7
added test for schedule attributes validation
Apr 10, 2020
9a2b0fd
Merge branch 'features/schedule_for_flow' of https://github.com/c-koe…
Apr 10, 2020
a31aed0
Merge branch 'dev' into features/schedule_for_flow
Jun 10, 2020
c51772b
Added whatsnew file
Jun 10, 2020
2c3f8f1
Delete old whatsnew file
Jun 10, 2020
0436ab1
Added missing line at end of file
Jun 10, 2020
134ec9a
Added schedule attributes to Flow class
Jun 10, 2020
4ba1c2c
Deleted files added by mistake
Jun 10, 2020
c948e6d
Renamed penalty_costs to schedule_costs
Jun 10, 2020
346e687
Added ValueError tests for schedule attributes
Jun 10, 2020
b6cefa2
Fixed merge conflict
Jun 10, 2020
5ec2d0d
Fixed merge conflict
Jun 10, 2020
40e0a9f
Merge pull request #4 from oemof/dev
Aug 24, 2020
f37c523
Added newfeatures to whatsnew file
Aug 31, 2020
309ba2e
Merge branch 'dev' into features/schedule_for_flow
Aug 31, 2020
5a7a4de
Merge branch 'dev' into features/schedule_for_flow
Sep 2, 2020
589d230
Deleted todo from older version
Sep 2, 2020
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
2 changes: 2 additions & 0 deletions docs/whatsnew/v0-3-3.rst
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,8 @@ New features
############

* :class:`~oemof.solph.GenericStorage` can now have "fixed_losses", that are independent from storage content.
* It is now possible to determine a schedule for a flow. Flow will be pushed
to the schedule, if possible.

New components
##############
Expand Down
70 changes: 67 additions & 3 deletions src/oemof/solph/blocks.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,16 @@ class Flow(SimpleBlock):
Difference of a flow in consecutive timesteps if flow is increased
indexed by NEGATIVE_GRADIENT_FLOWS, TIMESTEPS.

**The following sets are created:** (-> see basic sets at :class:`.Model` )
schedule_slack_pos :
Difference of a flow to schedule in consecutive timesteps if flow
has deficit to schedule. Indexed by SCHEDULE_FLOWS, TIMESTEPS.

schedule_slack_neg :
Excees of flow compared to schedule. Indexed by SCHEDULE_FLOWS,
TIMESTEPS.

**The following sets are created:** (-> see basic sets at
:class:`.Model` )

SUMMED_MAX_FLOWS
A set of flows with the attribute :attr:`summed_max` being not None.
Expand All @@ -49,6 +58,9 @@ class Flow(SimpleBlock):
INTEGER_FLOWS
A set of flows where the attribute :attr:`integer` is True (forces flow
to only take integer values)
SCHEDULE_FLOWS
A set of flows with :attr:`schedule` not None (forces flow to follow
schedule)

**The following constraints are build:**

Expand Down Expand Up @@ -79,16 +91,28 @@ class Flow(SimpleBlock):
\forall (i, o) \in \textrm{POSITIVE\_GRADIENT\_FLOWS}, \\
\forall t \in \textrm{TIMESTEPS}.

Schedule constraint :attr:`om.Flow.positive_gradient_constr[i, o]`:
.. math:: flow(i, o, t) + schedule_slack_pos(i, o, t) - \
schedule_slack_neg(i, o, t) = schedule(i, o, t) \\
\forall (i, o) \in \textrm{SCHEDULE\_FLOWS}, \\
\forall t \in \textrm{TIMESTEPS}.

**The following parts of the objective function are created:**

If :attr:`variable_costs` are set by the user:
.. math::
.. math::
\sum_{(i,o)} \sum_t flow(i, o, t) \cdot variable\_costs(i, o, t)

The expression can be accessed by :attr:`om.Flow.variable_costs` and
their value after optimization by :meth:`om.Flow.variable_costs()` .

If :attr:`schedule`, :attr:`schedule_cost_pos` and
:attr:`schedule_cost_neg` are set by the user:
.. math:: \sum_{(i,o)} \sum_t schedule_cost_pos(i, o, t) \cdot \
schedule_slack_pos(i, o, t) + schedule_cost_neg(i, o, t) \cdot \
schedule_slack_neg(i, o, t)
"""

def __init__(self, *args, **kwargs):
super().__init__(*args, **kwargs)

Expand Down Expand Up @@ -128,6 +152,13 @@ def _create(self, group=None):
self.INTEGER_FLOWS = Set(
initialize=[(g[0], g[1]) for g in group
if g[2].integer])

self.SCHEDULE_FLOWS = Set(
initialize=[(g[0], g[1]) for g in group if (
len(g[2].schedule) != 0 or
(len(g[2].schedule) == 0 and
g[2].schedule[0] is not None))])

# ######################### Variables ################################

self.positive_gradient = Var(self.POSITIVE_GRADIENT_FLOWS,
Expand All @@ -138,6 +169,13 @@ def _create(self, group=None):

self.integer_flow = Var(self.INTEGER_FLOWS,
m.TIMESTEPS, within=NonNegativeIntegers)

self.schedule_slack_pos = Var(self.SCHEDULE_FLOWS,
m.TIMESTEPS, within=NonNegativeReals)

self.schedule_slack_neg = Var(self.SCHEDULE_FLOWS,
m.TIMESTEPS, within=NonNegativeReals)

# set upper bound of gradient variable
for i, o, f in group:
if m.flows[i, o].positive_gradient['ub'][0] is not None:
Expand Down Expand Up @@ -217,6 +255,21 @@ def _integer_flow_rule(block, ii, oi, ti):
self.integer_flow_constr = Constraint(self.INTEGER_FLOWS, m.TIMESTEPS,
rule=_integer_flow_rule)

def _schedule_rule(model):
for inp, out in self.SCHEDULE_FLOWS:
for ts in m.TIMESTEPS:
if m.flows[inp, out].schedule[ts] is not None:
lhs = (m.flow[inp, out, ts] +
self.schedule_slack_pos[inp, out, ts] -
self.schedule_slack_neg[inp, out, ts])
rhs = m.flows[inp, out].schedule[ts]
self.schedule_constr.add((inp, out, ts),
lhs == rhs)
self.schedule_constr = Constraint(
self.SCHEDULE_FLOWS, m.TIMESTEPS, noruleinit=True)
self.schedule_build = BuildAction(
rule=_schedule_rule)

def _objective_expression(self):
r""" Objective expression for all standard flows with fixed costs
and variable costs.
Expand All @@ -225,6 +278,7 @@ def _objective_expression(self):

variable_costs = 0
gradient_costs = 0
penalty_costs = 0

for i, o in m.FLOWS:
if m.flows[i, o].variable_costs[0] is not None:
Expand All @@ -245,7 +299,16 @@ def _objective_expression(self):
m.flows[i, o].negative_gradient[
'costs'])

return variable_costs + gradient_costs
schedule = m.flows[i, o].schedule
if (len(schedule) > 1 or
(len(schedule) == 0 and
schedule[0] is not None)):
for t in m.TIMESTEPS:
penalty_costs += (self.schedule_slack_pos[i, o, t] *
m.flows[i, o].schedule_cost_pos[t])
penalty_costs += (self.schedule_slack_neg[i, o, t] *
m.flows[i, o].schedule_cost_neg[t])
return variable_costs + gradient_costs + penalty_costs


class InvestmentFlow(SimpleBlock):
Expand Down Expand Up @@ -496,6 +559,7 @@ def _investvar_bound_rule(block, i, o):
# create status variable for a non-convex investment flow
self.invest_status = Var(self.NON_CONVEX_INVESTFLOWS, within=Binary)
# ######################### CONSTRAINTS ###############################
# TODO: Add gradient constraints

def _min_invest_rule(block, i, o):
"""Rule definition for applying a minimum investment
Expand Down
35 changes: 33 additions & 2 deletions src/oemof/solph/network.py
Original file line number Diff line number Diff line change
Expand Up @@ -120,6 +120,24 @@ class Flow(on.Edge):
:class:`Flow <oemof.solph.blocks.Flow>`.
Note: at the moment this does not work if the investment attribute is
set .
schedule : numeric (sequence or scalar)
Schedule for the flow. Flow has to follow the schedule, otherwise a
penalty term will be activated. If array-like, values can be None
for flexible, non-fixed flow during the certain timestep. Used in
combination with :attr:`schedule_cost_pos` and
:attr:`schedule_cost_neg`.
schedule_cost_pos : numeric (sequence or scalar)
A penalty parameter of the penalty term which describes the costs
associated with one unit of deficit of the flow compared to the
schedule. If this is set the costs will be added to the objective
expression of the optimization problem. Used in combination with the
:attr:`schedule` and :attr:`schedule_cost_neg`
schedule_cost_neg: numeric (sequence or scalar)
A penalty parameter of the penalty term which describes the costs
associated with one unit of the exceeded flow when flow exceeds the
schedule. If this is set the costs will be added to the objective
expression of the optimization problem. Used in combination with
the :attr:`schedule` and :attr:`schedule_cost_pos`

Notes
-----
Expand All @@ -146,6 +164,7 @@ class Flow(on.Edge):
>>> f1 = Flow(min=[0.2, 0.3], max=0.99, nominal_value=100)
>>> f1.max[1]
0.99

"""

def __init__(self, **kwargs):
Expand All @@ -159,11 +178,14 @@ def __init__(self, **kwargs):

scalars = ['nominal_value', 'summed_max', 'summed_min',
'investment', 'nonconvex', 'integer', 'fixed']
sequences = ['actual_value', 'variable_costs', 'min', 'max']
sequences = ['actual_value', 'variable_costs', 'min', 'max',
'schedule', 'schedule_cost_neg', 'schedule_cost_pos']
dictionaries = ['positive_gradient', 'negative_gradient']
defaults = {'fixed': False, 'min': 0, 'max': 1, 'variable_costs': 0,
'positive_gradient': {'ub': None, 'costs': 0},
'negative_gradient': {'ub': None, 'costs': 0}}
'negative_gradient': {'ub': None, 'costs': 0},
'schedule_cost_neg': 0, 'schedule_cost_pos': 0,
}
keys = [k for k in kwargs if k != 'label']

for attribute in set(scalars + sequences + dictionaries + keys):
Expand All @@ -189,6 +211,15 @@ def __init__(self, **kwargs):
if self.investment and self.nonconvex:
raise ValueError("Investment flows cannot be combined with " +
"nonconvex flows!")
if (
len(self.schedule) != 0 and
((len(self.schedule_cost_pos) == 0 and
not self.schedule_cost_pos[0]) or
(len(self.schedule_cost_neg) == 0 and
not self.schedule_cost_neg[0]))):
raise ValueError("The penalty and schedule attribute need "
Copy link
Member

Choose a reason for hiding this comment

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

This error is not tested. Please add a test.

"to be used in combination. \n Please set "
"the schedule attribute of the flow.")


class Bus(on.Bus):
Expand Down
17 changes: 17 additions & 0 deletions tests/constraint_tests.py
Original file line number Diff line number Diff line change
Expand Up @@ -722,6 +722,23 @@ def test_dsm_module_interval(self):
)
self.compare_lp_files('dsm_module_interval.lp')

def test_flow_schedule(self):
"""Constraint test of scheduled flows."""
b_gas = solph.Bus(label='bus_gas')
b_th = solph.Bus(label='bus_th_penalty')

schedule = [None, 300, 50]
solph.Transformer(
label="boiler_penalty",
inputs={b_gas: solph.Flow()},
outputs={b_th: solph.Flow(nominal_value=200, variable_costs=0,
schedule_cost_pos=[0, 800, 900],
schedule_cost_neg=999,
schedule=schedule)},
conversion_factors={b_th: 1}
)
self.compare_lp_files('flow_schedule.lp')

def test_nonconvex_investment_storage_without_offset(self):
"""All invest variables are coupled. The invest variables of the Flows
will be created during the initialisation of the storage e.g. battery
Expand Down
79 changes: 79 additions & 0 deletions tests/lp_files/flow_schedule.lp
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
\* Source Pyomo model name=Model *\

min
objective:
+999 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_0)
+999 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_1)
+999 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_2)
+800 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_1)
+900 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_2)

s.t.

c_e_Bus_balance(bus_gas_0)_:
+1 flow(bus_gas_boiler_penalty_0)
= 0

c_e_Bus_balance(bus_gas_1)_:
+1 flow(bus_gas_boiler_penalty_1)
= 0

c_e_Bus_balance(bus_gas_2)_:
+1 flow(bus_gas_boiler_penalty_2)
= 0

c_e_Bus_balance(bus_th_penalty_0)_:
+1 flow(boiler_penalty_bus_th_penalty_0)
= 0

c_e_Bus_balance(bus_th_penalty_1)_:
+1 flow(boiler_penalty_bus_th_penalty_1)
= 0

c_e_Bus_balance(bus_th_penalty_2)_:
+1 flow(boiler_penalty_bus_th_penalty_2)
= 0

c_e_Transformer_relation(boiler_penalty_bus_gas_bus_th_penalty_0)_:
-1 flow(boiler_penalty_bus_th_penalty_0)
+1 flow(bus_gas_boiler_penalty_0)
= 0

c_e_Transformer_relation(boiler_penalty_bus_gas_bus_th_penalty_1)_:
-1 flow(boiler_penalty_bus_th_penalty_1)
+1 flow(bus_gas_boiler_penalty_1)
= 0

c_e_Transformer_relation(boiler_penalty_bus_gas_bus_th_penalty_2)_:
-1 flow(boiler_penalty_bus_th_penalty_2)
+1 flow(bus_gas_boiler_penalty_2)
= 0

c_e_Flow_schedule_constr(boiler_penalty_bus_th_penalty_1)_:
-1 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_1)
+1 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_1)
+1 flow(boiler_penalty_bus_th_penalty_1)
= 300

c_e_Flow_schedule_constr(boiler_penalty_bus_th_penalty_2)_:
-1 Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_2)
+1 Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_2)
+1 flow(boiler_penalty_bus_th_penalty_2)
= 50

c_e_ONE_VAR_CONSTANT:
ONE_VAR_CONSTANT = 1.0

bounds
0 <= flow(boiler_penalty_bus_th_penalty_0) <= 200
0 <= flow(boiler_penalty_bus_th_penalty_1) <= 200
0 <= flow(boiler_penalty_bus_th_penalty_2) <= 200
0 <= flow(bus_gas_boiler_penalty_0) <= +inf
0 <= flow(bus_gas_boiler_penalty_1) <= +inf
0 <= flow(bus_gas_boiler_penalty_2) <= +inf
0 <= Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_1) <= +inf
0 <= Flow_schedule_slack_pos(boiler_penalty_bus_th_penalty_2) <= +inf
0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_0) <= +inf
0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_1) <= +inf
0 <= Flow_schedule_slack_neg(boiler_penalty_bus_th_penalty_2) <= +inf
end
7 changes: 6 additions & 1 deletion tests/test_processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,8 @@ def test_flows_with_none_exclusion(self):
'positive_gradient_costs': 0,
'variable_costs': 0,
'label': str(b_el2.outputs[demand].label),
'schedule_cost_pos': 0,
'schedule_cost_neg': 0,
}
).sort_index()
)
Expand Down Expand Up @@ -140,6 +142,9 @@ def test_flows_without_none_exclusion(self):
'flow': None,
'values': None,
'label': str(b_el2.outputs[demand].label),
'schedule_cost_pos': 0,
'schedule_cost_neg': 0,
'schedule': None,
}
assert_series_equal(
param_results[(b_el2, demand)]['scalars'].sort_index(),
Expand All @@ -149,7 +154,7 @@ def test_flows_without_none_exclusion(self):
'actual_value': self.demand_values,
}
default_sequences = [
'actual_value'
'actual_value',
]
for attr in default_sequences:
if attr not in sequences_attributes:
Expand Down