Combination of investment and binary status variable¶
Different components in common energy system¶
General description¶
This example illustrates a possible combination of solph.Investment and solph.NonConvex. Note that both options are added to different components of the energy system.
There are the following components:
demand_heat: heat demand (high in winter, low in summer)
fireplace: wood firing, has a minimum heat and will burn for a minimum time if lit
boiler: gas firing, more flexible but with higher (flexible) cost than wood firing
thermal_collector: solar thermal collector, size is to be optimized in this example (high gain in summer, low in winter)
excess_heat: allow for some heat overproduction (solution would be trivial without, as the collector size would be given by the demand in summer)
Code¶
Download source code: house_without_nonconvex_investment.py
Click to display code
from oemof.tools import economics
from oemof import solph
try:
import matplotlib.pyplot as plt
except ImportError:
plt = None
def main():
##########################################################################
# Initialize the energy system and calculate necessary parameters
##########################################################################
periods = 365
time = pd.date_range("1/1/2018", periods=periods, freq="D")
es = solph.EnergySystem(timeindex=time)
b_heat = solph.buses.Bus(label="b_heat")
es.add(b_heat)
def heat_demand(d):
"""basic model for heat demand, solely based on the day of the year"""
return 0.6 + 0.4 * np.cos(2 * np.pi * d / 356)
def solar_thermal(d):
"""
basic model for solar thermal yield, solely based on the day of the
year
"""
return 0.5 - 0.5 * np.cos(2 * np.pi * d / 356)
demand_heat = solph.components.Sink(
label="demand_heat",
inputs={
b_heat: solph.flows.Flow(
fix=[heat_demand(day) for day in range(0, periods)],
nominal_capacity=10,
)
},
)
fireplace = solph.components.Source(
label="fireplace",
outputs={
b_heat: solph.flows.Flow(
nominal_capacity=10,
min=0.4,
max=1.0,
variable_costs=0.1,
nonconvex=solph.NonConvex(
minimum_uptime=2,
initial_status=1,
),
)
},
)
boiler = solph.components.Source(
label="boiler",
outputs={
b_heat: solph.flows.Flow(nominal_capacity=10, variable_costs=0.2)
},
)
# For one year, the equivalent periodical costs (epc) of an
# investment are equal to the annuity.
epc = economics.annuity(5000, 20, 0.05)
thermal_collector = solph.components.Source(
label="thermal_collector",
outputs={
b_heat: solph.flows.Flow(
fix=[solar_thermal(day) for day in range(0, periods)],
nominal_capacity=solph.Investment(
ep_costs=epc, minimum=1.0, maximum=5.0
),
)
},
)
excess_heat = solph.components.Sink(
label="excess_heat",
inputs={b_heat: solph.flows.Flow(nominal_capacity=10)},
)
es.add(demand_heat, fireplace, boiler, thermal_collector, excess_heat)
##########################################################################
# Optimise the energy system
##########################################################################
# create an optimization problem and solve it
om = solph.Model(es)
# solve model
om.solve(solver="cbc", solve_kwargs={"tee": True})
##########################################################################
# Check and plot the results
##########################################################################
results = solph.processing.results(om)
invest = solph.views.node(results, "b_heat")["scalars"][
(("thermal_collector", "b_heat"), "invest")
]
print("Invested in {} solar thermal power.".format(invest))
# plot data
if plt is not None:
# plot heat bus
data = solph.views.node(results, "b_heat")["sequences"]
exclude = ["excess_heat", "status"]
columns = [
c
for c in data.columns
if not any(s in c[0] or s in c[1] for s in exclude)
]
data = data[columns]
ax = data.plot(kind="line", drawstyle="steps-post", grid=True, rot=0)
ax.set_xlabel("Date")
ax.set_ylabel("Heat (arb. units)")
plt.show()
if __name__ == "__main__":
main()
Installation requirements¶
This example requires the version v0.5.x of oemof.solph. Install by:
pip install 'oemof.solph>=0.5,<0.6'
Same component but different Flows¶
General description¶
This example illustrates a possible combination of solph.Investment and solph.NonConvex. Note that both options are added to the same component of the energy system.
There are the following components:
demand_heat: heat demand (high in winter, low in summer)
fireplace: wood firing, has a minimum heat and will burn for a minimum time if lit.
boiler: gas firing, more flexible but still with minimal load and also with higher cost than wood firing
Code¶
Download source code: house_with_nonconvex_investment.py
Click to display code
__license__ = "MIT"
import numpy as np
import pandas as pd
from oemof.tools import economics
from oemof import solph
try:
import matplotlib.pyplot as plt
except ImportError:
plt = None
def main():
##########################################################################
# Initialize the energy system and calculate necessary parameters
##########################################################################
periods = 365
time = pd.date_range("1/1/2018", periods=periods, freq="D")
es = solph.EnergySystem(timeindex=time)
b_heat = solph.buses.Bus(label="b_heat")
es.add(b_heat)
def heat_demand(d):
"""basic model for heat demand, solely based on the day of the year"""
return 0.6 + 0.4 * np.cos(2 * np.pi * d / 356)
demand_heat = solph.components.Sink(
label="demand_heat",
inputs={
b_heat: solph.flows.Flow(
fix=[heat_demand(day) for day in range(0, periods)],
nominal_capacity=10,
)
},
)
# For one year, the equivalent periodical costs (epc) of an
# investment are equal to the annuity.
epc = economics.annuity(5000, 20, 0.05)
fireplace = solph.components.Source(
label="fireplace",
outputs={
b_heat: solph.flows.Flow(
max=1.0,
min=0.9,
variable_costs=0.1,
nonconvex=solph.NonConvex(
minimum_uptime=5,
initial_status=1,
),
nominal_capacity=solph.Investment(
ep_costs=epc,
minimum=1.0,
maximum=10.0,
),
)
},
)
boiler = solph.components.Source(
label="boiler",
outputs={
b_heat: solph.flows.Flow(
nominal_capacity=10, min=0.3, variable_costs=0.2
)
},
)
excess_heat = solph.components.Sink(
label="excess_heat",
inputs={b_heat: solph.flows.Flow(nominal_capacity=10)},
)
es.add(demand_heat, fireplace, boiler, excess_heat)
##########################################################################
# Optimise the energy system
##########################################################################
# create an optimization problem and solve it
om = solph.Model(es)
# solve model
om.solve(solver="cbc", solve_kwargs={"tee": True})
##########################################################################
# Check and plot the results
##########################################################################
results = solph.processing.results(om)
invest = solph.views.node(results, "b_heat")["scalars"][
(("fireplace", "b_heat"), "invest")
]
print("Invested in {} solar fireplace power.".format(invest))
# plot data
if plt is not None:
# plot heat bus
data = solph.views.node(results, "b_heat")["sequences"]
exclude = ["excess_heat", "status"]
columns = [
c
for c in data.columns
if not any(s in c[0] or s in c[1] for s in exclude)
]
data = data[columns]
ax = data.plot(kind="line", drawstyle="steps-post", grid=True, rot=0)
ax.set_xlabel("Date")
ax.set_ylabel("Heat (arb. units)")
plt.show()
if __name__ == "__main__":
main()
Installation requirements¶
This example requires the version v0.5.x of oemof.solph. Install by:
pip install 'oemof.solph>=0.5,<0.6'
Same Flow¶
General description¶
This example illustrates the combination of Investment and NonConvex options applied to a diesel generator in a hybrid mini-grid system.
There are the following components:
pv: solar potential to generate electricity
diesel_source: input diesel for the diesel genset
diesel_genset: generates ac electricity
rectifier: converts generated ac electricity from the diesel genset to dc electricity
inverter: converts generated dc electricity from the pv to ac electricity
battery: stores the generated dc electricity
demand_el: ac electricity demand (given as a separate csv file)
excess_el: allows for some electricity overproduction
Code¶
Download source code: diesel_genset_nonconvex_investment.py
Click to display code
__copyright__ = "oemof developer group"
__license__ = "MIT"
import os
import time
import warnings
from datetime import datetime
from datetime import timedelta
import numpy as np
import pandas as pd
from oemof import solph
try:
import matplotlib.pyplot as plt
except ImportError:
plt = None
def main():
##########################################################################
# Initialize the energy system and calculate necessary parameters
##########################################################################
# Start time for calculating the total elapsed time.
start_simulation_time = time.time()
start = "2022-01-01"
# The maximum number of days depends on the given *.csv file.
n_days = 10
n_days_in_year = 365
# Create date and time objects.
start_date_obj = datetime.strptime(start, "%Y-%m-%d")
start_date = start_date_obj.date()
start_datetime = datetime.combine(
start_date_obj.date(), start_date_obj.time()
)
end_datetime = start_datetime + timedelta(days=n_days)
# Import data.
filename = os.path.join(os.getcwd(), "solar_generation.csv")
data = pd.read_csv(filename)
# 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")
# Choose the range of the solar potential and demand
# based on the selected simulation period.
solar_potential = data.SolarGen.loc[start_datetime:end_datetime]
hourly_demand = data.Demand.loc[start_datetime:end_datetime]
peak_solar_potential = solar_potential.max()
peak_demand = hourly_demand.max()
# Create the energy system.
date_time_index = solph.create_time_index(
number=n_days * 24, start=start_date
)
energysystem = solph.EnergySystem(
timeindex=date_time_index, infer_last_interval=False
)
# -------------------- BUSES --------------------
# Create electricity and diesel buses.
b_el_ac = solph.buses.Bus(label="electricity_ac")
b_el_dc = solph.buses.Bus(label="electricity_dc")
b_diesel = solph.buses.Bus(label="diesel")
# -------------------- SOURCES --------------------
diesel_cost = 0.65 # currency/l
diesel_density = 0.846 # kg/l
diesel_lhv = 11.83 # kWh/kg
diesel_source = solph.components.Source(
label="diesel_source",
outputs={
b_diesel: solph.flows.Flow(
variable_costs=diesel_cost / diesel_density / diesel_lhv
)
},
)
# EPC stands for the equivalent periodical costs.
epc_pv = 152.62 # currency/kW/year
pv = solph.components.Source(
label="pv",
outputs={
b_el_dc: solph.flows.Flow(
fix=solar_potential / peak_solar_potential,
nominal_capacity=solph.Investment(
ep_costs=epc_pv * n_days / n_days_in_year
),
variable_costs=0,
)
},
)
# -------------------- CONVERTERS --------------------
# The diesel genset assumed to have a fixed efficiency of 33%.
# The output power of the diesel genset can only vary between
# the given minimum and maximum loads, which represent the fraction
# of the optimal capacity obtained from the optimization.
epc_diesel_genset = 84.80 # currency/kW/year
variable_cost_diesel_genset = 0.045 # currency/kWh
min_load = 0.2
max_load = 1.0
diesel_genset = solph.components.Converter(
label="diesel_genset",
inputs={b_diesel: solph.flows.Flow()},
outputs={
b_el_ac: solph.flows.Flow(
variable_costs=variable_cost_diesel_genset,
min=min_load,
max=max_load,
nominal_capacity=solph.Investment(
ep_costs=epc_diesel_genset * n_days / n_days_in_year,
maximum=2 * peak_demand,
),
nonconvex=solph.NonConvex(),
)
},
conversion_factors={b_el_ac: 0.33},
)
# The rectifier assumed to have a fixed efficiency of 98%.
epc_rectifier = 62.35 # currency/kW/year
rectifier = solph.components.Converter(
label="rectifier",
inputs={
b_el_ac: solph.flows.Flow(
nominal_capacity=solph.Investment(
ep_costs=epc_rectifier * n_days / n_days_in_year
),
variable_costs=0,
)
},
outputs={b_el_dc: solph.flows.Flow()},
conversion_factors={
b_el_dc: 0.98,
},
)
# The inverter assumed to have a fixed efficiency of 98%.
epc_inverter = 62.35 # currency/kW/year
inverter = solph.components.Converter(
label="inverter",
inputs={
b_el_dc: solph.flows.Flow(
nominal_capacity=solph.Investment(
ep_costs=epc_inverter * n_days / n_days_in_year
),
variable_costs=0,
)
},
outputs={b_el_ac: solph.flows.Flow()},
conversion_factors={
b_el_ac: 0.98,
},
)
# -------------------- STORAGE --------------------
epc_battery = 101.00 # currency/kWh/year
battery = solph.components.GenericStorage(
label="battery",
nominal_capacity=solph.Investment(
ep_costs=epc_battery * n_days / n_days_in_year
),
inputs={b_el_dc: solph.flows.Flow(variable_costs=0)},
outputs={
b_el_dc: solph.flows.Flow(
nominal_capacity=solph.Investment(ep_costs=0)
)
},
initial_storage_level=0.0,
min_storage_level=0.0,
max_storage_level=1.0,
balanced=True,
inflow_conversion_factor=0.9,
outflow_conversion_factor=0.9,
invest_relation_input_capacity=1,
invest_relation_output_capacity=0.5,
)
# -------------------- SINKS --------------------
demand_el = solph.components.Sink(
label="electricity_demand",
inputs={
b_el_ac: solph.flows.Flow(
fix=hourly_demand / peak_demand,
nominal_capacity=peak_demand,
)
},
)
excess_el = solph.components.Sink(
label="excess_el",
inputs={b_el_dc: solph.flows.Flow()},
)
# Add all objects to the energy system.
energysystem.add(
pv,
diesel_source,
b_el_dc,
b_el_ac,
b_diesel,
inverter,
rectifier,
diesel_genset,
battery,
demand_el,
excess_el,
)
##########################################################################
# Optimise the energy system
##########################################################################
# 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 = "cbc"
model = solph.Model(energysystem)
model.solve(
solver=solver,
solve_kwargs={"tee": True},
cmdline_options=solver_option[solver],
)
# End of the calculation time.
end_simulation_time = time.time()
##########################################################################
# Process the results
##########################################################################
results = solph.processing.results(model)
results_pv = solph.views.node(results=results, node="pv")
results_diesel_source = solph.views.node(
results=results, node="diesel_source"
)
results_diesel_genset = solph.views.node(
results=results, node="diesel_genset"
)
results_inverter = solph.views.node(results=results, node="inverter")
results_rectifier = solph.views.node(results=results, node="rectifier")
results_battery = solph.views.node(results=results, node="battery")
results_demand_el = solph.views.node(
results=results, node="electricity_demand"
)
results_excess_el = solph.views.node(results=results, node="excess_el")
# -------------------- SEQUENCES (DYNAMIC) --------------------
# Hourly demand profile.
sequences_demand = results_demand_el["sequences"][
(("electricity_ac", "electricity_demand"), "flow")
]
# Hourly profiles for solar potential and pv production.
sequences_pv = results_pv["sequences"][(("pv", "electricity_dc"), "flow")]
# Hourly profiles for diesel consumption and electricity production
# in the diesel genset.
# The 'flow' from oemof is in kWh and must be converted to
# kg by dividing it by the lower heating value and then to
# liter by dividing it by the diesel density.
sequences_diesel_consumption = (
results_diesel_source["sequences"][
(("diesel_source", "diesel"), "flow")
]
/ diesel_lhv
/ diesel_density
)
# Hourly profiles for electricity production in the diesel genset.
sequences_diesel_genset = results_diesel_genset["sequences"][
(("diesel_genset", "electricity_ac"), "flow")
]
# Hourly profiles for excess ac and dc electricity production.
sequences_excess = results_excess_el["sequences"][
(("electricity_dc", "excess_el"), "flow")
]
# -------------------- SCALARS (STATIC) --------------------
capacity_diesel_genset = results_diesel_genset["scalars"][
(("diesel_genset", "electricity_ac"), "invest")
]
# Define a tolerance to force 'too close' numbers to the `min_load`
# and to 0 to be the same as the `min_load` and 0.
tol = 1e-8
load_diesel_genset = sequences_diesel_genset / capacity_diesel_genset
sequences_diesel_genset[np.abs(load_diesel_genset) < tol] = 0
sequences_diesel_genset[np.abs(load_diesel_genset - min_load) < tol] = (
min_load * capacity_diesel_genset
)
sequences_diesel_genset[np.abs(load_diesel_genset - max_load) < tol] = (
max_load * capacity_diesel_genset
)
capacity_pv = results_pv["scalars"][(("pv", "electricity_dc"), "invest")]
capacity_inverter = results_inverter["scalars"][
(("electricity_dc", "inverter"), "invest")
]
capacity_rectifier = results_rectifier["scalars"][
(("electricity_ac", "rectifier"), "invest")
]
capacity_battery = results_battery["scalars"][
(("electricity_dc", "battery"), "invest")
]
total_cost_component = (
(
epc_diesel_genset * capacity_diesel_genset
+ epc_pv * capacity_pv
+ epc_rectifier * capacity_rectifier
+ epc_inverter * capacity_inverter
+ epc_battery * capacity_battery
)
* n_days
/ n_days_in_year
)
# The only componnet with the variable cost is the diesl genset
total_cost_variable = (
variable_cost_diesel_genset * sequences_diesel_genset.sum(axis=0)
)
total_cost_diesel = diesel_cost * sequences_diesel_consumption.sum(axis=0)
total_revenue = (
total_cost_component + total_cost_variable + total_cost_diesel
)
total_demand = sequences_demand.sum(axis=0)
# Levelized cost of electricity in the system in currency's Cent per kWh.
lcoe = 100 * total_revenue / total_demand
# The share of renewable energy source used to cover the demand.
res = (
100
* sequences_pv.sum(axis=0)
/ (sequences_diesel_genset.sum(axis=0) + sequences_pv.sum(axis=0))
)
# The amount of excess electricity (which must probably be dumped).
excess_rate = (
100
* sequences_excess.sum(axis=0)
/ (sequences_excess.sum(axis=0) + sequences_demand.sum(axis=0))
)
##########################################################################
# Print the results in the terminal
##########################################################################
print("\n" + 50 * "*")
print(
f"Simulation Time:\t {end_simulation_time-start_simulation_time:.2f} s"
)
print(50 * "*")
print(f"Peak Demand:\t {sequences_demand.max():.0f} kW")
print(f"LCOE:\t\t {lcoe:.2f} cent/kWh")
print(f"RES:\t\t {res:.0f}%")
print(f"Excess:\t\t {excess_rate:.1f}% of the total production")
print(50 * "*")
print("Optimal Capacities:")
print("-------------------")
print(f"Diesel Genset:\t {capacity_diesel_genset:.0f} kW")
print(f"PV:\t\t {capacity_pv:.0f} kW")
print(f"Battery:\t {capacity_battery:.0f} kW")
print(f"Inverter:\t {capacity_inverter:.0f} kW")
print(f"Rectifier:\t {capacity_rectifier:.0f} kW")
print(50 * "*")
##########################################################################
# Plot the duration curve for the diesel genset
##########################################################################
if plt is not None:
# Create the duration curve for the diesel genset.
fig, ax = plt.subplots(figsize=(10, 5))
# Sort the power generated by the diesel genset in descending order.
diesel_genset_duration_curve = np.sort(sequences_diesel_genset)[::-1]
percentage = (
100
* np.arange(1, len(diesel_genset_duration_curve) + 1)
/ len(diesel_genset_duration_curve)
)
ax.scatter(
percentage,
diesel_genset_duration_curve,
color="dodgerblue",
marker="+",
)
# Plot a horizontal line representing the minimum load
ax.axhline(
y=min_load * capacity_diesel_genset,
color="crimson",
linestyle="--",
)
min_load_annotation_text = (
f"minimum load: {min_load * capacity_diesel_genset:0.2f} kW"
)
ax.annotate(
min_load_annotation_text,
xy=(100, min_load * capacity_diesel_genset),
xytext=(0, 5),
textcoords="offset pixels",
horizontalalignment="right",
verticalalignment="bottom",
)
# Plot a horizontal line representing the maximum load
ax.axhline(
y=max_load * capacity_diesel_genset,
color="crimson",
linestyle="--",
)
max_load_annotation_text = (
f"maximum load: {max_load * capacity_diesel_genset:0.2f} kW"
)
ax.annotate(
max_load_annotation_text,
xy=(100, max_load * capacity_diesel_genset),
xytext=(0, -5),
textcoords="offset pixels",
horizontalalignment="right",
verticalalignment="top",
)
ax.set_title(
"Duration Curve for the Diesel Genset Electricity Production",
fontweight="bold",
)
ax.set_ylabel("diesel genset production [kW]")
ax.set_xlabel("percentage of annual operation [%]")
# Create the second axis on the right side of the diagram
# representing the operation load of the diesel genset.
second_ax = ax.secondary_yaxis(
"right",
functions=(
lambda x: x / capacity_diesel_genset * 100,
lambda x: x * capacity_diesel_genset / 100,
),
)
second_ax.set_ylabel("diesel genset operation load [%]")
plt.show()
if __name__ == "__main__":
main()
Data¶
Download data: solar_generation.csv
Installation requirements¶
This example requires the version v0.5.x of oemof.solph. Install by:
pip install 'oemof.solph>=0.5,<0.6'