A first model
After you have successfully installed IESopt, you can start to build your first model. In this tutorial, we will show you how to create a simple model, solve it, and extract some basic results.
Let’s start by creating a Python file, e.g. main.py
, to hold the necessary code, and add the following lines:
import iesopt
# Load and solve a model.
model = iesopt.run("my_first_model.iesopt.yaml")
print("Objective value:", model.objective_value)
Next we’ll describe a simple model that we would like to solve and set various parameters. Create an empty file my_first_model.iesopt.yaml
(later on multiple files can be combined to describe more complex models), that we can now use to actually describe the model.
Model configuration
The first part of each *.iesopt.yaml
file describes general configuration parameters. Add the following lines:
config:
optimization:
problem_type: LP
snapshots:
count: 24
This tells IESopt to start building a model, while expecting all formulations to be representable as LP (so, adding binary
variables will cause an error). Further we specify how many time steps (called Snapshot
) we’d like to use, here telling IESopt that we are looking to optimize a full day (by the default a Snapshot
’s duration is one hour).
Energy carriers
Since IESopt is a general purpose energy system model, it does not restrict you to a set of predefined types of energy, but rather expects you to first define those. For our first model, we’ll only care about electricity and gas, so we add the following lines:
carriers:
electricity: {}
gas: {}
The {}
represents an empty dictionary. Additional parameters related to the carrier could later be specified there.
Model components
Now that all general settings of the model are in place, we can actually start describing the model’s structure:
A
photovoltaic
(aProfile
) system is feeding in electricity (based on some external availability factor) into a local electricity grid (aNode
calledelec_grid
)A simple
storage
is connected (via aConnection
) to this grid, able to shift energy between time stepsAn endogenous
demand
(electricity) must be met at every time stepAny uncovered demand (by PV or storage) can be satisfied using a
gasturbine
(aUnit
), that draws gas from agas_grid
, that needs to buy all used gas from agas_market
We now describe these seven components, starting with the electricity grid Node
. Add the following lines to
my_first_model.iesopt.yaml
- everything after a #
is considered a comment by IESopt:
components:
elec_grid: # the unique name of this component
type: Node # the type of this component
carrier: electricity # this (Node-specific) parameter fixes the carrier to be electricity
Head over to this section of the docs to read up on the different component types that are available.
We add the other two Node
s, making sure, that the storage
is stateful (models a “state of charge”), and that the
gas_grid
has the proper carrier. Make sure that you do get the correct indent, since all of the following lines still
belong to the overall components:
definition:
gas_grid: # the unique name of this component
type: Node # the type of this component
carrier: gas # this (Node-specific) parameter fixes the carrier to be electricity
storage:
type: Node
carrier: electricity
has_state: true # this allows this Node to have an "internal state"
state_lb: 0 # the state can not drop below 0
state_ub: 50 # a maximum of 50 electricity can be stored
Two important things can be seen with the storage
:
Values in IESopt do not carry an explicit unit (at the moment, this will be possible in the future). This means, that if we consider electricity to be in kW/kWh in this model, we need to make sure that all settings are adjusted to match that.
We are implicitly using a default setting of a parameter that we did not specify:
state_cyclic
(check it out in the docs!) is set toeq
per default, forcing the model to always end the optimization with as much “charge” in the storage as it started with in the first time step (however, how much that is, is left to the optimizer to decide).
Now that we have all Node
s in place, we can insert the only Connection
by adding:
conn:
type: Connection
node_from: elec_grid # energy flows from HERE
node_to: storage # to THERE
capacity: 15 # with a maximum capacity of +-15 units of electricity
Notice that while we did specify the Node
s that are connected by this Connection
, no energy
carrier was explicitly set. This is due to the fact that Connection
s infer the energy carrier and will automatically fail if they
connect two Node
s with a different type of energy. The specified capacity
therefore refers to 15 units of electricity
and constructs symmetric bounds on the flow (again, read up in the docs for other options and asymmetric bounds).
Next, it’s time to add all three Profile
s to the model. A Profile
allows for the “creation” or “destruction” of energy:
Normally, all energy needs to move through the model (possibly being transformed), but can not enter/leave the model. This
is where Profile
s help, representing for example the cost of buying gas (gas_market
) or a fixed demand that needs to
be covered (demand
).
We now add:
demand:
type: Profile # the type is now "Profile"
carrier: electricity
value: 5 # this models a fixed demand of 5 units of electricity during every Snapshot
node_from: elec_grid # this tells MFC that this Profile draws energy from "elec_grid"
# We can also set the "value" of a Profile to a time series, as can be seen:
photovoltaic:
type: Profile
carrier: electricity
value: [0,0,0,0,0,1,2,3,4,5,8,12,12,12,8,5,4,3,2,1,0,0,0,0]
node_to: elec_grid # now feeding INTO "elec_grid"
gas_market:
type: Profile
carrier: gas
mode: create # this changes the mode from the default ("fixed") to "create"
cost: 100 # this specifies the "cost of gas"
node_to: gas_grid
Note on setting
Profile
values: While the value of aProfile
can be set directly in the*.iesopt.yaml
file, most of the time this will just result in a convoluted file. It’s therefore possible to load external data files (in CSV format) and directly link to them using a simplecolumn@filename
syntax, that can be seen in other examples.
While the first two Profile
s should be mostly self-explanatory, the gas_market
introduces a new concept: While
standard Profile
s always consider a fixed value (a time series), some time series may not be exogenous, e.g. how
much gas is bought from the gas market. That’s where the mode: create
setting helps by defining a Profile
that can
freely choose (as long as the value is >= 0) how much gas is being bought, but associates every unit of gas with a cost
that has to be “paid” (this is therefore automatically inserted into the objective function).
The only thing missing from the model description is the Unit
(gas_turbine
). It takes gas as its only input and
transforms that into electricity. For this we first add the following component:
gasturbine:
type: Unit
inputs: {gas: gas_grid}
outputs: {electricity: elec_grid}
conversion: 1 gas -> 0.40 electricity
capacity: 100 out:electricity
Let’s look at the Unit
-specific settings in detail:
inputs: {gas: gas_grid}
: This tells IESopt that an input acceptinggas
(the carrier) is connected togas_grid
(= it is consuming gas from there).Similarly,
outputs: {electricity: elec_grid}
tells IESopt where the only output (with carrierelectricity
) feeds energy to.The most important part is kept in the so-called “conversion expression”
conversion: 1 gas -> 0.40 electricity
: This tells IESopt that theUnit
will use 1 unit of gas (e.g. kWh, MJ, …) and convert it into 0.4 units of electricity (e.g. kWh) at a fixed rate.Finally,
capacity: 100 out:electricity
specifies the “capacity limitations” of thisUnit
: 100 units of electricity can be produced. This implicitly limits the maximum amount of gas that can be used during a time step to 250 units of gas.
Unit
s come with a lot of additional (and very specific) parameters (e.g. marginal_cost
, availability
, …) that are
explained in detail in the specific section of the docs.
Final config file
Following the above steps you should now have your my_first_model.iesopt.yaml
config file setup like this:
config:
optimization:
problem_type: LP
snapshots:
count: 24
carriers:
electricity: {}
gas: {}
components:
elec_grid: # the unique name of this component
type: Node # the type of this component
carrier: electricity # this (Node-specific) parameter fixes the carrier to be electricity
gas_grid: # the unique name of this component
type: Node # the type of this component
carrier: gas # this (Node-specific) parameter fixes the carrier to be electricity
storage:
type: Node
carrier: electricity
has_state: true # this allows this Node to have an "internal state"
state_lb: 0 # the state can not drop below 0
state_ub: 50 # a maximum of 50 electricity can be stored
conn:
type: Connection
node_from: elec_grid # energy flows from HERE
node_to: storage # to THERE
capacity: 15 # with a maximum capacity of +-15 units of electricity
demand:
type: Profile # the type is now "Profile"
carrier: electricity
value: 5 # this models a fixed demand of 5 units of electricity during every Snapshot
node_from: elec_grid # this tells MFC that this Profile draws energy from "elec_grid"
# We can also set the "value" of a Profile to a time series, as can be seen:
photovoltaic:
type: Profile
carrier: electricity
value: [0,0,0,0,0,1,2,3,4,5,8,12,12,12,8,5,4,3,2,1,0,0,0,0]
node_to: elec_grid # now feeding INTO "elec_grid"
gas_market:
type: Profile
carrier: gas
mode: create # this changes the mode from the default ("fixed") to "create"
cost: 100 # this specifies the "cost of gas"
node_to: gas_grid
gasturbine:
type: Unit
inputs: {gas: gas_grid}
outputs: {electricity: elec_grid}
conversion: 1 gas -> 0.40 electricity
capacity: 100 out:electricity
Running the optimization
Assuming that both my_first_model.iesopt.yaml
and main.py
are located in the same folder, you can now execute the following
command there (make sure you are in the correct Python environment):
python ./main.py
The output should show a total objective value of 9500
, resulting from 38
units of electricity missing after
accounting for PV production, amounting to a total need of 95
units of gas, at a price of 100
.
Note on startup time: If you are running this for the first time, you might notice a considerable delay before the output is shown. This is due to the fact that IESopt is automatically connecting to the internal “core” (which is written in Julia) and updating it. This can be avoided by running the
import iesopt
command once and then just iterating on the generate/optimize part, in a “REPL-style” approach. Remember: Interactively executing a line or block of code in VSCode is usually bound toShift + Enter
.
Extracting model results
Now, head over to the extracting results tutorial, to get started with extracting actually results from your model.