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

Add ev tutorial #1150

Draft
wants to merge 55 commits into
base: docs/make-style-modern
Choose a base branch
from
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
55 commits
Select commit Hold shift + click to select a range
d574666
Add kwarg fail_on_infeasable in model.solve
esske Nov 22, 2024
e329a14
Change keyword name to allow_nonoptimal
esske Nov 22, 2024
1439e9e
Save solver results before checking status
esske Nov 22, 2024
bdc7845
Add info to whatsnew
esske Nov 22, 2024
907bdef
add name to AUTHORS.rst and CITATION.cff
esske Nov 22, 2024
4d4283f
add tests
esske Dec 20, 2024
165b0ea
Ad stub for the EV charging tutorial.
p-snft Jan 7, 2025
78870c0
Merge branch 'dev' into infeasable_message
p-snft Jan 10, 2025
a9e0b3c
add unidirectional loading
AntonellaGia Jan 11, 2025
22e83e1
unidirectional loading
AntonellaGia Jan 11, 2025
45dacd2
add example unidirectional loading at home
AntonellaGia Jan 12, 2025
2c1bbb2
Merge branch 'docs/make-style-modern' into feature/EV_Tutorial
p-snft Jan 14, 2025
f6f7e91
Adhere to Black
p-snft Jan 14, 2025
17c1854
Move ev tutorial to tutorial directory
p-snft Jan 14, 2025
db6b401
Include tutorials in pyproject.toml
p-snft Jan 14, 2025
f7e5105
Clearify docstring of GenericStorage
p-snft Jan 14, 2025
ed35604
Start restructuring EV tutorial
p-snft Jan 14, 2025
69a8b7a
Fix typo in pyproject.toml
p-snft Jan 15, 2025
fd48a96
Merge branch 'docs/make-style-modern' into feature/EV_Tutorial
p-snft Jan 15, 2025
c49e3a0
Proceed with ev charging example
p-snft Jan 15, 2025
64bbd17
Continue EV charging tutorial
p-snft Jan 15, 2025
ead6dd7
Introduced balanced battery in EV tutorial
p-snft Jan 15, 2025
3613ec5
Fix inconsistency in evcharging tutorial
p-snft Jan 16, 2025
18c58e2
Fix typo
p-snft Jan 16, 2025
67804b1
Fix parameter processing for custom attributes.
uvchik Jan 16, 2025
006b2a2
Check for too high dimensions in sequences
uvchik Jan 16, 2025
a09d36e
Add string test
uvchik Jan 16, 2025
5497882
Fix isort issues
uvchik Jan 16, 2025
d8c8f7a
Use Number class instead of a list of numeric types
uvchik Jan 17, 2025
61fab34
Extend error message.
uvchik Jan 17, 2025
ef4944a
Rename custom_attributes to custom_properties to be in line with othe…
uvchik Jan 17, 2025
3241fed
Merge branch 'fix-bugs-in-parameter-as-dict' of http://github.com/oem…
uvchik Jan 17, 2025
e98d4a2
Add missing import
uvchik Jan 17, 2025
349a800
Fix name of attribute in test
uvchik Jan 17, 2025
b249b77
Add an additional hint to the error message
uvchik Jan 17, 2025
96d174c
Merge pull request #1152 from oemof/fix-bugs-in-parameter-as-dict
p-snft Jan 17, 2025
0eb2fbc
add dynamic prices
AntonellaGia Jan 18, 2025
7168172
change comments
AntonellaGia Jan 18, 2025
5836e4d
change docu
AntonellaGia Jan 18, 2025
92ec0ff
black
AntonellaGia Jan 18, 2025
135257e
fixing
AntonellaGia Jan 18, 2025
b7a28cc
fixing flake8
AntonellaGia Jan 18, 2025
66d3576
Refactor variable price example
p-snft Jan 20, 2025
7b4ff42
Adhere to Black
p-snft Jan 20, 2025
4e38bcb
Merge pull request #1153 from oemof/ev_tutorial_variable_costs
p-snft Jan 20, 2025
1cce93f
Apply suggestions from code review
p-snft Jan 20, 2025
c119923
Merge branch 'infeasable_message' of github.com:esske/oemof-solph int…
p-snft Jan 20, 2025
61151bb
Fix format of model tests
p-snft Jan 20, 2025
f402c43
Merge branch 'esske-infeasable_message' into dev
p-snft Jan 20, 2025
c9570f4
Use cbc in offset_converter_example
p-snft Jan 22, 2025
520f963
Adjust saturating_storage example for solph v0.6
p-snft Jan 22, 2025
e03fa90
Fix emission limit example
p-snft Jan 22, 2025
6e0a757
Fix rendering error in saturating storage example results
p-snft Jan 22, 2025
a624005
Let examples work in smoke test
p-snft Jan 22, 2025
9cbeaef
Merge branch 'dev' into feature/EV_Tutorial
p-snft Jan 22, 2025
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
1 change: 1 addition & 0 deletions AUTHORS.rst
Original file line number Diff line number Diff line change
Expand Up @@ -39,3 +39,4 @@ Authors
* Stephan Günther
* Uwe Krien
* Tobi Rohrer
* Eva Schischke
4 changes: 4 additions & 0 deletions CITATION.cff
Original file line number Diff line number Diff line change
Expand Up @@ -170,4 +170,8 @@ authors:
family-names: Gering
given-names: Marie-Claire
alias: "@MaGering"
-
family-names: Schischke
given-names: Eva
alias: "@esske"
...
4 changes: 4 additions & 0 deletions docs/whatsnew/v0-6-0.rst
Original file line number Diff line number Diff line change
Expand Up @@ -35,10 +35,13 @@ Documentation
Bug fixes
#########

* Remove unneeded use of gurobi from examples.

Other changes
#############

* `Model.solve()` will now fail, if solver status is nonoptimal.
Added new keyword `allow_nonoptimal` to deactivate this behaviour.

Known issues
############
Expand All @@ -48,4 +51,5 @@ Contributors
############

* Patrik Schönfeldt
* Eva Schischke
* Johannes Kochems
7 changes: 5 additions & 2 deletions examples/basic_example/basic_example.py
Original file line number Diff line number Diff line change
Expand Up @@ -81,8 +81,11 @@


def get_data_from_file_path(file_path: str) -> pd.DataFrame:
dir = os.path.dirname(os.path.abspath(__file__))
data = pd.read_csv(dir + "/" + file_path)
try:
data = pd.read_csv(file_path)
except FileNotFoundError:
dir = os.path.dirname(os.path.abspath(__file__))
data = pd.read_csv(dir + "/" + file_path)
return data


Expand Down
2 changes: 1 addition & 1 deletion examples/emission_constraint/emission_constraint.py
Original file line number Diff line number Diff line change
Expand Up @@ -109,7 +109,7 @@ def main():
constraints.emission_limit(model, limit=100)

# print out the emission constraint
model.integral_limit_emission_factor_constraint.pprint()
model.integral_limit_emission_factor_upper_limit.pprint()
model.integral_limit_emission_factor.pprint()

# solve the model
Expand Down
24 changes: 15 additions & 9 deletions examples/flexible_modelling/saturating_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -19,7 +19,7 @@

Installation requirements
-------------------------
This example requires oemof.solph (v0.5.x), install by:
This example requires oemof.solph (v0.6.x), install by:

.. code:: bash

Expand Down Expand Up @@ -86,21 +86,21 @@ def saturating_storage_example():
model = solph.Model(es)

def soc_limit_rule(m):
for p, ts in m.TIMEINDEX:
for ts in m.TIMESTEPS:
soc = (
m.GenericStorageBlock.storage_content[battery, ts + 1]
/ storage_capacity
)
expr = (1 - soc) / (1 - full_charging_limit) >= m.flow[
bel, battery, p, ts
bel, battery, ts
] / inflow_capacity
getattr(m, "soc_limit").add((p, ts), expr)
getattr(m, "soc_limit").add(ts, expr)

setattr(
model,
"soc_limit",
po.Constraint(
model.TIMEINDEX,
model.TIMESTEPS,
noruleinit=True,
),
)
Expand All @@ -116,17 +116,23 @@ def soc_limit_rule(m):
# create result object
results = solph.processing.results(model)

plt.plot(results[(battery, None)]["sequences"], "r--", label="content")
plt.plot(
results[(battery, None)]["sequences"]["storage_content"],
"r--",
label="content",
)
plt.step(
20 * results[(bel, battery)]["sequences"], "b-", label="20*inflow"
20 * results[(bel, battery)]["sequences"]["flow"],
"b-",
label="20*inflow",
)
plt.legend()
plt.grid()

plt.figure()
plt.plot(
results[(battery, None)]["sequences"][1:],
results[(bel, battery)]["sequences"][:-1],
results[(battery, None)]["sequences"]["storage_content"][1:],
results[(bel, battery)]["sequences"]["flow"][:-1],
"b-",
)
plt.grid()
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -60,6 +60,15 @@
plt = None


def get_data_from_file_path(file_path: str) -> pd.DataFrame:
try:
data = pd.read_csv(file_path)
except FileNotFoundError:
dir = os.path.dirname(os.path.abspath(__file__))
data = pd.read_csv(dir + "/" + file_path)
return data


def offset_converter_example():
##########################################################################
# Initialize the energy system and calculate necessary parameters
Expand All @@ -84,9 +93,7 @@ def offset_converter_example():
end_datetime = start_datetime + timedelta(days=n_days)

# Import data.
current_directory = os.path.dirname(os.path.abspath(__file__))
filename = os.path.join(current_directory, "diesel_genset_data.csv")
data = pd.read_csv(filepath_or_buffer=filename)
data = get_data_from_file_path("diesel_genset_data.csv")

# Change the index of data to be able to select data based on the time range.
data.index = pd.date_range(start="2022-01-01", periods=len(data), freq="h")
Expand Down Expand Up @@ -281,7 +288,7 @@ def offset_converter_example():
# The higher the MipGap or ratioGap, the faster the solver would converge,
# but the less accurate the results would be.
solver_option = {"gurobi": {"MipGap": "0.02"}, "cbc": {"ratioGap": "0.02"}}
solver = "gurobi"
solver = "cbc"

model = solph.Model(energy_system)
model.solve(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ def main():
]["flow"]
results_df["storage_relative"] = results[(storage_relative, None)][
"sequences"
]
]["storage_content"]
results_df["storage_relative_inflow"] = results[(bus, storage_relative)][
"sequences"
]["flow"]
Expand Down
33 changes: 20 additions & 13 deletions src/oemof/solph/_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -373,7 +373,9 @@ def results(self):
"""
return processing.results(self)

def solve(self, solver="cbc", solver_io="lp", **kwargs):
def solve(
self, solver="cbc", solver_io="lp", allow_nonoptimal=False, **kwargs
):
r"""Takes care of communication with solver to solve the model.

Parameters
Expand All @@ -399,32 +401,37 @@ def solve(self, solver="cbc", solver_io="lp", **kwargs):
"""
solve_kwargs = kwargs.get("solve_kwargs", {})
solver_cmdline_options = kwargs.get("cmdline_options", {})

opt = SolverFactory(solver, solver_io=solver_io)

# set command line options
options = opt.options
for k in solver_cmdline_options:
options[k] = solver_cmdline_options[k]

solver_results = opt.solve(self, **solve_kwargs)

status = solver_results["Solver"][0]["Status"]
termination_condition = solver_results["Solver"][0][
"Termination condition"
]
status = solver_results.Solver.Status
termination_condition = solver_results.Solver.Termination_condition

self.es.results = solver_results
self.solver_results = solver_results

if status == "ok" and termination_condition == "optimal":
logging.info("Optimization successful...")
else:
msg = (
"Optimization ended with status {0} and termination "
"condition {1}"
f"The solver did not return an optimal solution. "
f"Instead the optimization ended with\n "
f" - status: {status}\n"
f" - termination condition: {termination_condition}"
)
warnings.warn(
msg.format(status, termination_condition), UserWarning
)
self.es.results = solver_results
self.solver_results = solver_results

if allow_nonoptimal:
warnings.warn(
msg.format(status, termination_condition), UserWarning
)
else:
raise RuntimeError(msg)

return solver_results

Expand Down
8 changes: 8 additions & 0 deletions src/oemof/solph/_plumbing.py
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,14 @@ def sequence(iterable_or_scalar):
10
"""
if len(np.shape(iterable_or_scalar)) > 1:
d = len(np.shape(iterable_or_scalar))
raise ValueError(
f"Dimension too high ({d} > 1) for {iterable_or_scalar}\n"
"The dimension of a number is 0, of a list 1, of a table 2 and so "
"on.\nPlease notice that a table with one column is still a table "
"with 2 dimensions and not a Series."
)
if isinstance(iterable_or_scalar, str):
return iterable_or_scalar
elif isinstance(iterable_or_scalar, abc.Iterable):
Expand Down
3 changes: 2 additions & 1 deletion src/oemof/solph/components/_generic_storage.py
Original file line number Diff line number Diff line change
Expand Up @@ -106,7 +106,8 @@ class GenericStorage(Node):
see: min_storage_level
storage_costs : numeric (iterable or scalar), :math:`c_{storage}(t)`
Cost (per energy) for having energy in the storage, starting from
time point :math:`t_{1}`.
time point :math:`t_{1}`. (:math:`t_{0}` is left out to avoid counting
it twice if balanced=True.)
lifetime_inflow : int, :math:`n_{in}`
Determine the lifetime of an inflow; only applicable for multi-period
models which can invest in storage capacity and have an
Expand Down
18 changes: 14 additions & 4 deletions src/oemof/solph/components/_source.py
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,9 @@ class Source(Node):
outputs: dict
A dictionary mapping input nodes to corresponding outflows
(i.e. output values).
custom_properties: dict
Additional keyword arguments for use in user-defined equations or for
information purposes.

Examples
--------
Expand All @@ -47,16 +50,23 @@ class Source(Node):

>>> str(pv_plant.outputs[bel].output)
'electricity'
>>> bgas = solph.buses.Bus(label='gas')
>>> gas_source = solph.components.Source(
... label='gas_import',
... outputs={bgas: solph.flows.Flow()},
... custom_properties={"emission": 201}) # g/kWh
>>> gas_source.custom_properties["emission"]
201
"""

def __init__(self, label=None, *, outputs, custom_attributes=None):
def __init__(self, label=None, *, outputs, custom_properties=None):
if outputs is None:
outputs = {}
if custom_attributes is None:
custom_attributes = {}
if custom_properties is None:
custom_properties = {}

super().__init__(
label=label, outputs=outputs, custom_properties=custom_attributes
label=label, outputs=outputs, custom_properties=custom_properties
)

def constraint_group(self):
Expand Down
3 changes: 2 additions & 1 deletion src/oemof/solph/processing.py
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@

"""

import numbers
import sys
from collections import abc
from itertools import groupby
Expand Down Expand Up @@ -589,7 +590,7 @@ def detect_scalars_and_sequences(com):

def move_undetected_scalars(com):
for ckey, value in list(com["sequences"].items()):
if isinstance(value, str):
if isinstance(value, (str, numbers.Number)):
com["scalars"][ckey] = value
del com["sequences"][ckey]
elif isinstance(value, _FakeSequence):
Expand Down
33 changes: 30 additions & 3 deletions tests/test_models.py
Original file line number Diff line number Diff line change
Expand Up @@ -34,9 +34,36 @@ def test_infeasible_model():
)
)
m = solph.Model(es)
with warnings.catch_warnings(record=True) as w:
m.solve(solver="cbc")
assert "Optimization ended with status" in str(w[0].message)
with pytest.warns(
UserWarning, match="The solver did not return an optimal solution"
):
m.solve(solver="cbc", allow_nonoptimal=True)

with pytest.raises(
RuntimeError, match="The solver did not return an optimal solution"
):
m.solve(solver="cbc", allow_nonoptimal=False)


def test_unbounded_model():
es = solph.EnergySystem(timeincrement=[1])
bel = solph.buses.Bus(label="bus")
es.add(bel)
# Add a Sink with a higher demand
es.add(solph.components.Sink(inputs={bel: solph.flows.Flow()}))

# Add a Source with a very high supply
es.add(
solph.components.Source(
outputs={bel: solph.flows.Flow(variable_costs=-5)}
)
)
m = solph.Model(es)

with pytest.raises(
RuntimeError, match="The solver did not return an optimal solution"
):
m.solve(solver="cbc", allow_nonoptimal=False)


@pytest.mark.filterwarnings(
Expand Down
53 changes: 53 additions & 0 deletions tests/test_outputlib/test_processing.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,53 @@
import pandas as pd

from oemof.solph import EnergySystem
from oemof.solph import Model
from oemof.solph import create_time_index
from oemof.solph import processing
from oemof.solph.buses import Bus
from oemof.solph.components import Sink
from oemof.solph.components import Source
from oemof.solph.flows import Flow


def test_custom_attribut_with_numeric_value():
date_time_index = create_time_index(2012, number=6)
energysystem = EnergySystem(timeindex=date_time_index)
bs = Bus(label="bus")
energysystem.add(bs)
src_custom_int = Source(
label="source_with_custom_attribute_int",
outputs={bs: Flow(nominal_value=5, fix=[3] * 7)},
custom_properties={"integer": 9},
)
s1 = pd.Series([1.4, 2.3], index=["a", "b"])
snk_custom_float = Sink(
label="source_with_custom_attribute_float",
inputs={bs: Flow()},
custom_properties={"numpy-float": s1["a"]},
)
src_custom_str = Source(
label="source_with_custom_attribute_string",
outputs={bs: Flow(nominal_value=5, fix=[3] * 7)},
custom_properties={"string": "name"},
)
energysystem.add(snk_custom_float, src_custom_int, src_custom_str)

# create optimization model based on energy_system
optimization_model = Model(energysystem=energysystem)

parameter = processing.parameter_as_dict(optimization_model)
assert (
parameter[snk_custom_float, None]["scalars"][
"custom_properties_numpy-float"
]
== 1.4
)
assert (
parameter[src_custom_int, None]["scalars"]["custom_properties_integer"]
== 9
)
assert (
parameter[src_custom_str, None]["scalars"]["custom_properties_string"]
== "name"
)
Loading
Loading