Tutorial 2: urban scale¶
This example consists of two possible sources of electricity, one possible source of heat, and one possible source of simultaneous heat and electricity. There are three locations, each describing a building, with transmission links between them. The diagram below gives an overview:
Supply technologies¶
This example model defines three supply technologies.
The first two are supply_gas
and supply_grid_power
, referring to the supply of gas
(natural gas) and electricity
, respectively, from the national distribution system. These ‘inifinitely’ available national commodities can become energy carriers in the system, with the cost of their purchase being considered at supply, not conversion.
The definition of these technologies in the example model’s configuration looks as follows:
supply_grid_power:
essentials:
name: 'National grid import'
color: '#C5ABE3'
parent: supply
carrier: electricity
constraints:
resource: inf
energy_cap_max: 2000
lifetime: 25
costs:
monetary:
interest_rate: 0.10
energy_cap: 15
om_con: 0.1 # 10p/kWh electricity price #ppt
supply_gas:
essentials:
name: 'Natural gas import'
color: '#C98AAD'
parent: supply
carrier: gas
constraints:
resource: inf
energy_cap_max: 2000
lifetime: 25
costs:
monetary:
interest_rate: 0.10
energy_cap: 1
om_con: 0.025 # 2.5p/kWh gas price #ppt
The final supply technology is pv
(solar photovoltaic power), which serves as an inflexible supply technology. It has a time-dependant resource availablity, loaded from file, a maximum area over which it can capture its reosurce (resource_area_max
) and a requirement that all available resource must be used (force_resource: True
). This emulates the reality of solar technologies: once installed, their production matches the availability of solar energy.
The efficiency of the DC to AC inverter (which occurs after conversion from resource to energy carrier) is considered in parasitic_eff
and the resource_area_per_energy_cap
gives a link between the installed area of solar panels to the installed capacity of those panels (i.e. kWp).
In most cases, domestic PV panels are able to export excess energy to the national grid. We allow this here by specifying an export_carrier
. Revenue for export will be considered on a per-location basis.
The definition of this technology in the example model’s configuration looks as follows:
pv:
essentials:
name: 'Solar photovoltaic power'
color: '#F9D956'
parent: supply_power_plus
constraints:
export_carrier: electricity
resource: file=pv_resource.csv # Already accounts for panel efficiency - kWh/m2. Source: Renewables.ninja Solar PV Power - Version: 1.1 - License: https://creativecommons.org/licenses/by-nc/4.0/ - Reference: https://doi.org/10.1016/j.energy.2016.08.060
parasitic_eff: 0.85 # inverter losses
energy_cap_max: 250
resource_area_max: 1500
force_resource: true
resource_area_per_energy_cap: 7 # 7m2 of panels needed to fit 1kWp of panels
lifetime: 25
costs:
monetary:
interest_rate: 0.10
energy_cap: 1350
Finally, the parent of the PV technology is not supply_plus
, but rather supply_power_plus
. We use this to show the possibility of an intermediate technology group, which provides the information on the energy carrier (electricity
) and the ultimate abstract base technology (supply_plus
):
tech_groups:
supply_power_plus:
essentials:
parent: supply_plus
carrier: electricity
Intermediate technology groups allow us to avoid repetition of technology information, be it in essentials
, constraints
, or costs
, by linking multiple technologies to the same intermediate group.
Conversion technologies¶
The example model defines two conversion technologies.
The first is boiler
(natural gas boiler), which serves as an example of a simple conversion technology with one input carrier and one output carrier. Its only constraints are the cost of built capacity (costs.monetary.energy_cap
), a constraint on its maximum built capacity (constraints.energy_cap.max
), and an energy conversion efficiency (energy_eff
).
The definition of this technology in the example model’s configuration looks as follows:
boiler:
essentials:
name: 'Natural gas boiler'
color: '#8E2999'
parent: conversion
carrier_out: heat
carrier_in: gas
constraints:
energy_cap_max: 600
energy_eff: 0.85
lifetime: 25
costs:
monetary:
interest_rate: 0.10
There are a few things to note. First, boiler
defines a name, a color (given as an HTML color code), and a stack_weight. These are used by the built-in analysis tools when analyzing model results. Second, it specifies its parent, conversion
, its carrier_in gas
, and its carrier_out heat
, thus setting itself up as a gas to heat conversion technology. This is followed by the definition of constraints and costs (the only cost class used is monetary, but this is where other “costs”, such as emissions, could be defined).
The second technology is chp
(combined heat and power), and serves as an example of a possible conversion_plus technology making use of two output carriers.
This definition in the example model’s configuration is more verbose:
chp:
essentials:
name: 'Combined heat and power'
color: '#E4AB97'
parent: conversion_plus
primary_carrier: electricity
carrier_in: gas
carrier_out: electricity
carrier_out_2: heat
constraints:
export_carrier: electricity
energy_cap_max: 1500
energy_eff: 0.405
carrier_ratios.carrier_out_2.heat: 0.8
lifetime: 25
costs:
monetary:
interest_rate: 0.10
energy_cap: 750
om_prod: 0.004 # .4p/kWh for 4500 operating hours/year
export: file=export_power.csv
See also
Again, chp
has the definitions for name, color, parent, and carrier_in/out. It now has an additional carrier (carrier_out_2
) defined in its essential information, allowing a second carrier to be produced at the same time as the first carrier (carrier_out
). The carrier ratio constraint tells us the ratio of carrier_out_2 to carrier_out that we can achieve, in this case 0.8 units of heat are produced every time a unit of electricity is produced. to produce these units of energy, gas is consumed at a rate of carrier_prod(carrier_out) / energy_eff
, so gas consumption is only a function of power output.
As with the pv
, the chp
an export eletricity. The revenue gained from this export is given in the file export_power.csv
, in which negative values are given per time step.
Demand technologies¶
Electricity and heat demand are defined here:
demand_electricity:
essentials:
name: 'Electrical demand'
color: '#072486'
parent: demand
carrier: electricity
demand_heat:
essentials:
name: 'Heat demand'
color: '#660507'
parent: demand
carrier: heat
Electricity and heat demand are technologies like any other. We will associate an actual demand time series with each demand technology later.
Transmission technologies¶
In this district, electricity and heat can be distributed between locations. Gas is made available in each location without consideration of transmission.
power_lines:
essentials:
name: 'Electrical power distribution'
color: '#6783E3'
parent: transmission
carrier: electricity
constraints:
energy_cap_max: 2000
energy_eff: 0.98
lifetime: 25
costs:
monetary:
interest_rate: 0.10
energy_cap_per_distance: 0.01
heat_pipes:
essentials:
name: 'District heat distribution'
color: '#823739'
parent: transmission
carrier: heat
constraints:
energy_cap_max: 2000
energy_eff_per_distance: 0.975
lifetime: 25
costs:
monetary:
interest_rate: 0.10
energy_cap_per_distance: 0.3
power_lines
has an efficiency of 0.95, so a loss during transmission of 0.05. heat_pipes
has a loss rate per unit distance of 2.5%/unit distance (or energy_eff_per_distance
of 97.5%). Over the distance between the two locations of 0.5km (0.5 units of distance), this translates to \(2.5^{0.5}\) = 1.58% loss rate.
Locations¶
In order to translate the model requirements shown in this section’s introduction into a model definition, four locations are used: X1
, X2
, X3
, and N1
.
The technologies are set up in these locations as follows:
Let’s now look at the first location definition:
X1:
techs:
chp:
pv:
supply_grid_power:
costs.monetary.energy_cap: 100 # cost of transformers
supply_gas:
demand_electricity:
constraints.resource: file=demand_power.csv
demand_heat:
constraints.resource: file=demand_heat.csv
available_area: 500
coordinates: {x: 2, y: 7}
There are several things to note here:
- The location specifies a dictionary of technologies that it allows (
techs
), with each key of the dictionary referring to the name of technologies defined in ourtechs.yaml
file. Note that technologies listed here must have been defined elsewhere in the model configuration. - It also overrides some options for both
demand_electricity
,demand_heat
, andsupply_grid_power
. For the latter, it simply sets a location-specific cost. For demands, the options set here are related to reading the demand time series from a CSV file. CSV is a simple text-based format that stores tables by comma-separated rows. Note that we did not define anyresource
option in the definition of these demands. Instead, this is done directly via a location-specific override. For this location, the filesdemand_heat.csv
anddemand_power.csv
are loaded. As no column is specified (see national scale example model) Calliope will assume that the column name matches the location nameX1
. Note that in Calliope, a supply is positive and a demand is negative, so the stored CSV data will be negative. - Coordinates are defined by cartesian coordinates
x
andy
, which will be used to calculate distance of transmission lines (unless we specify otherwise later on) and for location-based visualisation. These coordinates are abstract, unlike latitude and longitude, and can be used when we don’t know (or care) about the geographical location of our problem. - An
available_area
is defined, which will limit the maximum area of allresource_area
technologies to the e.g. roof space available at our location. In this case, we just havepv
, but the case where solar thermal panels compete with photovoltaic panels for space, this would the sum of the two to the available area.
The remaining location definitions look like this:
X2:
techs:
boiler:
costs.monetary.energy_cap: 43.1 # different boiler costs
pv:
costs.monetary:
om_prod: -0.0203 # revenue for just producing electricity
export: -0.0491 # FIT return for PV export
supply_gas:
demand_electricity:
constraints.resource: file=demand_power.csv
demand_heat:
constraints.resource: file=demand_heat.csv
available_area: 1300
coordinates: {x: 8, y: 7}
X3:
techs:
boiler:
costs.monetary.energy_cap: 78 # different boiler costs
pv:
constraints:
energy_cap_max: 50 # changing tariff structure below 50kW
costs.monetary:
om_annual: -80.5 # reimbursement per kWp from FIT
supply_gas:
demand_electricity:
constraints.resource: file=demand_power.csv
demand_heat:
constraints.resource: file=demand_heat.csv
available_area: 900
coordinates: {x: 5, y: 3}
X2
and X3
are very similar to X1
, except that they do not connect to the national electricity grid, nor do they contain the chp
technology. Specific pv
cost structures are also given, emulating e.g. commercial vs. domestic feed-in tariffs.
N1
differs to the others by virtue of containing no technologies. It acts as a branching station for the heat network, allowing connections to one or both of X2
and X3
without double counting the pipeline from X1
to N1
. Its definition look like this:
N1: # location for branching heat transmission network
coordinates: {x: 5, y: 7}
For transmission technologies, the model also needs to know which locations can be linked, and this is set up in the model configuration as follows:
X1,X2:
techs:
power_lines:
distance: 10
X1,X3:
techs:
power_lines:
X1,N1:
techs:
heat_pipes:
N1,X2:
techs:
heat_pipes:
N1,X3:
techs:
heat_pipes:
The distance measure for the power line is larger than the straight line distance given by the coordinates of X1
and X2
, so we can provide more information on non-direct routes for our distribution system. These distances will override any automatic straight-line distances calculated by coordinates.
Revenue by export¶
Defined for both PV and CHP, there is the option to accrue revenue in the system by exporting electricity. This export is considered as a removal of the energy carrier electricity
from the system, in exchange for negative cost (i.e. revenue). To allow this, carrier_export: electricity
has been given under both technology definitions and an export
value given under costs.
The revenue from PV export varies depending on location, emulating the different feed-in tariff structures in the UK for commercial and domestic properties. In domestic properties, the revenue is generated by simply having the installation (per kW installed capacity), as export is not metered. Export is metered in commercial properties, thus revenue is generated directly from export (per kWh exported). The revenue generated by CHP depends on the electricity grid wholesale price per kWh, being 80% of that. These revenue possibilities are reflected in the technologies’ and locations’ definitions.
Running the model¶
We now take you through running the model in a Jupyter notebook, which is included fully below. To download and run the notebook yourself, you can find it here. You will need to have Calliope installed.
Calliope Urban Scale Example Model¶
import calliope
# cufflinks allows for easy plotly plots to be
# produced from a pandas DataFrame
import cufflinks
# We increase logging verbosity
calliope.set_log_level('INFO')
model = calliope.examples.urban_scale()
# Model inputs can be viewed at `model.inputs`.
# Variables are indexed over any combination of `techs`, `locs`, `carriers`, `costs` and `timesteps`,
# although `techs`, `locs`, and `carriers` are often concatenated.
# e.g. `chp`, `X1`, `heat` -> `X1::chp::heat`
model.inputs
# Individual data variables can be accessed easily, `to_pandas()` reformats the data to look nicer
model.inputs.resource.to_pandas()
# To reformat the array, deconcatenating loc_techs / loc_tech_carriers, you can use model.get_formatted_array()
# You can then apply loc/tech/carrier only operations, like summing information over locations:
model.get_formatted_array('resource').sum('locs').to_pandas()
# Solve the model. Results are loaded into `model.results`.
# By including logging (see package importing), we can see the timing of parts of the run, as well as the solver's log
model.run()
# Model results are held in the same structure as model inputs.
# The results consist of the optimal values for all decision variables, including capacities and carrier flow
# There are also results, like system capacity factor and levelised costs, which are calculated in postprocessing
# before being added to the results Dataset
model.results
# We can sum heat output over all locations and turn the result into a pandas DataFrame
df_heat = model.get_formatted_array('carrier_prod').loc[{'carriers':'heat'}].sum('locs').to_pandas().T
#The information about the dataframe tells us about the amount of data it holds in the index and in each column
df_heat.info()
# Using .head() to see the first few rows of heat generation and demand
# Note: carrier production in heat_pipes:N1 is heat received by the heat network at any location connected to `N1`
df_heat.head()
# We can plot this by using the timeseries plotting functionality.
# The top-left dropdown gives us the chance to scroll through other timeseries data too.
model.plot.timeseries()
# plot.capacities gives a graphical view of the non-timeseries variables, both input and output
model.plot.capacity()
# We can also examine total technology costs
# notice all the NaN values which appear when seperating loc::techs to locs and techs.
# Any NaN value means we never considered that combination of `loc` and `tech` for the variable
costs = model.get_formatted_array('cost').loc[{'costs': 'monetary'}].to_pandas()
costs
# We might like to plot this, which is outside the scope of internal calliope analysis functions
# But by using cufflinks, we can plot directly from our pandas DataFrame
# Note that the colours are not correct for our technologies here
costs.iplot(kind='bar')
# We can examine levelized costs for each location and technology
lcoes = model.results.systemwide_levelised_cost.loc[{'carriers': 'electricity', 'costs':'monetary'}].to_pandas()
lcoes
# We could plot this using lcoes.iplot(kind='bar', title='Systemwide levelised costs')
# Or we can just plot it directly using calliope analysis functionality
model.plot.capacity(array='systemwide_levelised_cost')
# Plotting a map of locations and transmission lines is easily possible
# In our example, the system is purely hypothetical and the resulting map
# gives us little useful information...
model.plot.transmission()
See the Calliope documentation for more details on setting up and running a Calliope model.
Calliope Urban Scale Example Model¶
Previous: Tutorial 1: national scale | Next: Tutorial 3: Mixed Integer Linear Programming