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:

Overview of the built-in urban-scale example model

Overview of the built-in urban-scale example model

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.

Simple node

The layout of a simple supply technology, in this case supply_gas, which has a resource input and a carrier output. A carrier conversion efficiency (\(energy_{eff}\)) can also be applied (although isn’t considered for our supply technologies in this problem).

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).

Simple conversion node

The layout of a simple node, in this case boiler, which has one carrier input, one carrier output, a carrier conversion efficiency (\(energy_{eff}\)), and a constraint on its maximum built \(energy_{cap}\) (which puts an upper limit on \(carrier_{prod}\)).

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.

More complex conversion technology, with multiple output carriers

The layout of a more complex node, in this case chp, which makes use of multiple 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

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.

Transmission node

A simple transmission node with an \(energy_{eff}\).

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:

Locations and their technologies in the example model

Locations and their technologies in the urban-scale example model

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 our techs.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, and supply_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 any resource option in the definition of these demands. Instead, this is done directly via a location-specific override. For this location, the files demand_heat.csv and demand_power.csv are loaded. As no column is specified (see national scale example model) Calliope will assume that the column name matches the location name X1. 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 and y, 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 all resource_area technologies to the e.g. roof space available at our location. In this case, we just have pv, 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.

Notebook

Calliope Urban Scale Example Model

In [1]:
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')
In [2]:
model = calliope.examples.urban_scale()
[2018-04-20 09:51:37] INFO: Model: initialising
[2018-04-20 09:51:39] INFO: Model: preprocessing stage 1 (model_run)
[2018-04-20 09:51:41] INFO: Model: preprocessing stage 2 (model_data)
[2018-04-20 09:51:43] INFO: Model: preprocessing complete. Time since start: 0:00:05.200584
In [3]:
# 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
Out[3]:
<xarray.Dataset>
Dimensions:                            (carrier_tiers: 3, carriers: 3, coordinates: 2, costs: 1, loc_carriers: 10, loc_tech_carriers_conversion_plus: 3, loc_techs: 26, loc_techs_area: 3, loc_techs_conversion: 2, loc_techs_conversion_plus: 1, loc_techs_export: 4, loc_techs_finite_resource: 9, loc_techs_investment_cost: 20, loc_techs_non_conversion: 23, loc_techs_om_cost: 7, loc_techs_supply_plus: 3, loc_techs_transmission: 10, locs: 4, techs: 9, timesteps: 48)
Coordinates:
  * loc_techs                          (loc_techs) object 'X1::power_lines:X2' ...
  * loc_techs_conversion               (loc_techs_conversion) <U10 'X3::boiler' ...
  * costs                              (costs) object 'monetary'
  * loc_techs_supply_plus              (loc_techs_supply_plus) object 'X1::pv' ...
  * loc_techs_area                     (loc_techs_area) object 'X1::pv' ...
  * loc_techs_finite_resource          (loc_techs_finite_resource) object 'X3::demand_electricity' ...
  * loc_techs_conversion_plus          (loc_techs_conversion_plus) object 'X1::chp' ...
  * loc_techs_transmission             (loc_techs_transmission) object 'X1::power_lines:X2' ...
  * loc_techs_non_conversion           (loc_techs_non_conversion) object 'X1::power_lines:X2' ...
  * techs                              (techs) object 'boiler' ...
  * loc_techs_investment_cost          (loc_techs_investment_cost) object 'X1::power_lines:X2' ...
  * timesteps                          (timesteps) datetime64[ns] 2005-07-01 ...
  * loc_techs_export                   (loc_techs_export) object 'X1::pv' ...
  * carrier_tiers                      (carrier_tiers) <U5 'out_2' 'in' 'out'
  * coordinates                        (coordinates) object 'x' 'y'
  * carriers                           (carriers) object 'electricity' ...
  * locs                               (locs) object 'X2' 'N1' 'X3' 'X1'
  * loc_carriers                       (loc_carriers) object 'X3::electricity' ...
  * loc_tech_carriers_conversion_plus  (loc_tech_carriers_conversion_plus) object 'X1::chp::heat' ...
  * loc_techs_om_cost                  (loc_techs_om_cost) object 'X1::supply_gas' ...
Data variables:
    energy_con                         (loc_techs) float64 1.0 nan 1.0 nan ...
    resource_eff                       (loc_techs_finite_resource) float64 nan ...
    energy_cap_max                     (loc_techs) float64 2e+03 2e+03 2e+03 ...
    force_resource                     (loc_techs_finite_resource) bool True ...
    resource_area_max                  (loc_techs_area) int32 1500 1500 1500
    resource                           (loc_techs_finite_resource, timesteps) float64 -18.76 ...
    resource_area_per_energy_cap       (loc_techs_area) int32 7 7 7
    parasitic_eff                      (loc_techs_supply_plus) float64 0.85 ...
    resource_unit                      (loc_techs_finite_resource) <U5 'power' ...
    export_carrier                     (loc_techs_export) <U11 'electricity' ...
    energy_prod                        (loc_techs) float64 1.0 1.0 1.0 1.0 ...
    energy_eff                         (loc_techs) float64 0.98 nan 0.98 nan ...
    lifetime                           (loc_techs) float64 25.0 25.0 25.0 ...
    reserve_margin                     (carriers) float64 nan nan nan
    cost_om_annual                     (costs, loc_techs_om_cost) float64 nan ...
    cost_export                        (costs, loc_techs_om_cost, timesteps) float64 nan ...
    cost_energy_cap                    (costs, loc_techs_investment_cost) float64 0.1 ...
    cost_depreciation_rate             (costs, loc_techs_investment_cost) float64 0.1102 ...
    cost_om_con                        (costs, loc_techs_om_cost) float64 0.025 ...
    cost_om_prod                       (costs, loc_techs_om_cost) float64 nan ...
    distance                           (loc_techs_transmission) float64 10.0 ...
    lookup_remotes                     (loc_techs_transmission) <U18 'X2::power_lines:X1' ...
    available_area                     (locs) float64 1.3e+03 nan 900.0 500.0
    loc_coordinates                    (coordinates, locs) int32 8 5 5 2 7 7 3 7
    colors                             (techs) <U7 '#8E2999' '#C5ABE3' ...
    inheritance                        (techs) <U29 'conversion' 'supply' ...
    names                              (techs) <U29 'Natural gas boiler' ...
    carrier_ratios                     (carrier_tiers, loc_tech_carriers_conversion_plus) float64 0.8 ...
    carrier_ratios_min                 (carrier_tiers, loc_techs_conversion_plus) float64 0.8 ...
    lookup_loc_carriers                (loc_carriers) <U175 'X3::demand_electricity::electricity,X3::power_lines:X1::electricity,X3::pv::electricity' ...
    lookup_loc_techs                   (loc_techs_non_conversion) <U35 'X1::power_lines:X2::electricity' ...
    lookup_loc_techs_conversion        (carrier_tiers, loc_techs_conversion) object None ...
    lookup_loc_techs_conversion_plus   (carrier_tiers, loc_techs_conversion_plus) object 'X1::chp::heat' ...
    lookup_primary_loc_tech_carriers   (loc_techs_conversion_plus) <U20 'X1::chp::electricity' ...
    lookup_loc_techs_export            (loc_techs_export) <U20 'X1::pv::electricity' ...
    lookup_loc_techs_area              (locs) <U6 'X2::pv' '' 'X3::pv' 'X1::pv'
    timestep_resolution                (timesteps) float64 1.0 1.0 1.0 1.0 ...
    timestep_weights                   (timesteps) float64 1.0 1.0 1.0 1.0 ...
    max_demand_timesteps               (carriers) datetime64[ns] 2005-07-01T08:00:00 ...
Attributes:
    model.calliope_version:       0.6.0
    model.name:                   Urban-scale example model
    model.subset_time:            ['2005-07-01', '2005-07-02']
    model.timeseries_dateformat:  %Y-%m-%d %H:%M:%S
    run.backend:                  pyomo
    run.bigM:                     1000000.0
    run.ensure_feasibility:       True
    run.mode:                     plan
    run.objective:                cost_minimization
    run.solver:                   glpk
    run.zero_threshold:           1e-10
    calliope_version:             0.6.0-dev
    defaults:                     available_area: null\ncarrier_ratios: {}\nc...
    allow_operate_mode:           1
In [4]:
# Individual data variables can be accessed easily, `to_pandas()` reformats the data to look nicer
model.inputs.resource.to_pandas()
Out[4]:
timesteps 2005-07-01 00:00:00 2005-07-01 01:00:00 2005-07-01 02:00:00 2005-07-01 03:00:00 2005-07-01 04:00:00 2005-07-01 05:00:00 2005-07-01 06:00:00 2005-07-01 07:00:00 2005-07-01 08:00:00 2005-07-01 09:00:00 ... 2005-07-02 14:00:00 2005-07-02 15:00:00 2005-07-02 16:00:00 2005-07-02 17:00:00 2005-07-02 18:00:00 2005-07-02 19:00:00 2005-07-02 20:00:00 2005-07-02 21:00:00 2005-07-02 22:00:00 2005-07-02 23:00:00
loc_techs_finite_resource
X3::demand_electricity -18.762912 -18.762912 -18.762912 -18.762912 -18.762912 -30.212425 -35.233307 -61.395269 -63.642962 -62.679665 ... -18.762912 -18.954418 -19.145924 -18.762912 -18.762912 -18.762912 -18.762912 -18.762912 -18.762912 -18.762912
X1::demand_heat -0.215376 -0.200838 -0.207306 -0.318949 -0.650734 -1.039384 -1.181567 -1.285403 -1.209117 -1.219912 ... -0.491097 -0.540068 -0.641510 -0.741098 -0.761822 -0.707075 -0.634092 -0.523707 -0.400787 -0.272040
X2::pv 0.000000 0.000000 0.000000 0.000000 0.000000 0.007857 0.029143 0.055571 0.078429 0.096000 ... 0.057857 0.041143 0.027000 0.014714 0.006143 0.000857 0.000000 0.000000 0.000000 0.000000
X1::pv 0.000000 0.000000 0.000000 0.000000 0.000000 0.007857 0.029143 0.055571 0.078429 0.096000 ... 0.057857 0.041143 0.027000 0.014714 0.006143 0.000857 0.000000 0.000000 0.000000 0.000000
X2::demand_heat -64.731991 -70.453439 -77.192976 -104.556436 -123.228444 -167.668819 -264.887092 -365.137675 -258.172589 -190.585578 ... -105.261025 -84.614417 -104.549875 -122.646451 -166.442507 -161.099889 -166.931078 -240.034833 -143.576460 -86.082014
X2::demand_electricity -94.545801 -76.960619 -77.475750 -77.475750 -82.731496 -148.533479 -189.570817 -238.734711 -244.284493 -231.440181 ... -199.895515 -221.324570 -188.344877 -249.962248 -248.894106 -269.344347 -245.412357 -196.280957 -135.289242 -103.741556
X3::pv 0.000000 0.000000 0.000000 0.000000 0.000000 0.007857 0.029143 0.055571 0.078429 0.096000 ... 0.057857 0.041143 0.027000 0.014714 0.006143 0.000857 0.000000 0.000000 0.000000 0.000000
X3::demand_heat -0.015600 -0.860322 -0.015600 -0.015600 -0.860407 -7.263327 -9.398229 -5.792842 -3.322585 -1.927264 ... -0.015600 -0.015600 -0.860335 -0.015600 -0.015600 -0.860291 -0.015600 -0.860300 -0.015600 -0.860290
X1::demand_electricity -0.455564 -0.405798 -0.393291 -0.393992 -0.440085 -0.567821 -0.732535 -0.713803 -0.689992 -0.707650 ... -0.683060 -0.780370 -0.940634 -0.978388 -1.022063 -1.169519 -1.307938 -1.099334 -0.826212 -0.559499

9 rows × 48 columns

In [5]:
# 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()
Out[5]:
timesteps 2005-07-01 00:00:00 2005-07-01 01:00:00 2005-07-01 02:00:00 2005-07-01 03:00:00 2005-07-01 04:00:00 2005-07-01 05:00:00 2005-07-01 06:00:00 2005-07-01 07:00:00 2005-07-01 08:00:00 2005-07-01 09:00:00 ... 2005-07-02 14:00:00 2005-07-02 15:00:00 2005-07-02 16:00:00 2005-07-02 17:00:00 2005-07-02 18:00:00 2005-07-02 19:00:00 2005-07-02 20:00:00 2005-07-02 21:00:00 2005-07-02 22:00:00 2005-07-02 23:00:00
techs
demand_electricity -113.764277 -96.129329 -96.631954 -96.632654 -101.934493 -179.313724 -225.536658 -300.843784 -308.617446 -294.827496 ... -219.341487 -241.059359 -208.431436 -269.703548 -268.679081 -289.276777 -265.483207 -216.143204 -154.878366 -123.063967
demand_heat -64.962967 -71.514599 -77.415882 -104.890985 -124.739585 -175.971530 -275.466888 -372.215920 -262.704292 -193.732754 ... -105.767723 -85.170084 -106.051720 -123.403148 -167.219928 -162.667255 -167.580770 -241.418840 -143.992847 -87.214344
pv 0.000000 0.000000 0.000000 0.000000 0.000000 0.023571 0.087429 0.166714 0.235286 0.288000 ... 0.173571 0.123429 0.081000 0.044143 0.018429 0.002571 0.000000 0.000000 0.000000 0.000000

3 rows × 48 columns

In [6]:
# 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()
[2018-04-20 09:51:44] INFO: Backend: starting model run
[2018-04-20 09:51:47] INFO: Backend: model generated. Time since start: 0:00:09.747274
[2018-04-20 09:51:47] INFO: Backend: sending model to solver
[2018-04-20 09:51:51] INFO: Backend: solver finished running. Time since start: 0:00:13.606790
[2018-04-20 09:51:51] INFO: Backend: loaded results
[2018-04-20 09:51:51] INFO: Backend: generated solution array. Time since start: 0:00:13.747430
[2018-04-20 09:51:51] INFO: Postprocessing: started
[2018-04-20 09:51:52] INFO: Postprocessing: All values < 1e-10 set to 0 in carrier_prod, carrier_con, capacity_factor
[2018-04-20 09:51:52] INFO: Postprocessing: Model was feasible, deleting unmet_demand variable
[2018-04-20 09:51:52] INFO: Postprocessing: ended. Time since start: 0:00:14.731917
In [7]:
# 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
Out[7]:
<xarray.Dataset>
Dimensions:                     (carriers: 3, costs: 1, loc_tech_carriers_con: 19, loc_tech_carriers_export: 4, loc_tech_carriers_prod: 21, loc_techs: 26, loc_techs_area: 3, loc_techs_cost: 20, loc_techs_investment_cost: 20, loc_techs_om_cost: 7, loc_techs_supply_plus: 3, techs: 9, timesteps: 48)
Coordinates:
  * loc_techs                   (loc_techs) object 'X1::power_lines:X2' ...
  * costs                       (costs) object 'monetary'
  * loc_techs_supply_plus       (loc_techs_supply_plus) object 'X1::pv' ...
  * loc_techs_area              (loc_techs_area) object 'X1::pv' 'X3::pv' ...
  * loc_tech_carriers_prod      (loc_tech_carriers_prod) object 'N1::heat_pipes:X1::heat' ...
  * loc_techs_cost              (loc_techs_cost) object 'X1::power_lines:X2' ...
  * loc_tech_carriers_con       (loc_tech_carriers_con) object 'X1::heat_pipes:N1::heat' ...
  * techs                       (techs) object 'boiler' 'supply_grid_power' ...
  * loc_techs_investment_cost   (loc_techs_investment_cost) object 'X1::power_lines:X2' ...
  * loc_tech_carriers_export    (loc_tech_carriers_export) object 'X1::pv::electricity' ...
  * timesteps                   (timesteps) datetime64[ns] 2005-07-01 ...
  * carriers                    (carriers) object 'electricity' 'heat' 'gas'
  * loc_techs_om_cost           (loc_techs_om_cost) object 'X1::supply_gas' ...
Data variables:
    energy_cap                  (loc_techs) float64 274.5 644.3 45.78 0.0 ...
    carrier_prod                (loc_tech_carriers_prod, timesteps) float64 85.87 ...
    carrier_con                 (loc_tech_carriers_con, timesteps) float64 -92.65 ...
    cost                        (costs, loc_techs_cost) float64 0.008286 ...
    resource_area               (loc_techs_area) float64 0.0 350.0 443.2
    resource_con                (loc_techs_supply_plus, timesteps) float64 0.0 ...
    resource_cap                (loc_techs_supply_plus) float64 0.0 38.95 49.32
    carrier_export              (loc_tech_carriers_export, timesteps) float64 0.0 ...
    cost_var                    (costs, loc_techs_om_cost, timesteps) float64 7.165 ...
    cost_investment             (costs, loc_techs_investment_cost) float64 0.008286 ...
    capacity_factor             (loc_tech_carriers_prod, timesteps) float64 0.351 ...
    systemwide_capacity_factor  (techs, carriers) float64 0.0 0.1538 0.0 ...
    systemwide_levelised_cost   (carriers, techs, costs) float64 inf 0.116 ...
    total_levelised_cost        (carriers, costs) float64 0.0425 0.03106 0.03421
Attributes:
    model.calliope_version:       0.6.0
    model.name:                   Urban-scale example model
    model.subset_time:            ['2005-07-01', '2005-07-02']
    model.timeseries_dateformat:  %Y-%m-%d %H:%M:%S
    run.backend:                  pyomo
    run.bigM:                     1000000.0
    run.ensure_feasibility:       True
    run.mode:                     plan
    run.objective:                cost_minimization
    run.solver:                   glpk
    run.zero_threshold:           1e-10
    calliope_version:             0.6.0-dev
    defaults:                     available_area: null\ncarrier_ratios: {}\nc...
    allow_operate_mode:           1
    termination_condition:        optimal
    solution_time:                7.19469
    time_finished:                2018-04-20 09:51:51
In [8]:
# 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()
<class 'pandas.core.frame.DataFrame'>
DatetimeIndex: 48 entries, 2005-07-01 00:00:00 to 2005-07-02 23:00:00
Data columns (total 12 columns):
boiler               48 non-null float64
chp                  48 non-null float64
heat_pipes:N1        48 non-null float64
heat_pipes:X1        48 non-null float64
heat_pipes:X2        48 non-null float64
heat_pipes:X3        48 non-null float64
power_lines:X1       48 non-null float64
power_lines:X2       48 non-null float64
power_lines:X3       48 non-null float64
pv                   48 non-null float64
supply_gas           48 non-null float64
supply_grid_power    48 non-null float64
dtypes: float64(12)
memory usage: 4.9 KB
In [9]:
# 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()
Out[9]:
techs boiler chp heat_pipes:N1 heat_pipes:X1 heat_pipes:X2 heat_pipes:X3 power_lines:X1 power_lines:X2 power_lines:X3 pv supply_gas supply_grid_power
timesteps
2005-07-01 00:00:00 0.000000 92.861360 143.669589 85.869798 0.0 71.320854 0.0 0.0 0.0 0.0 0.0 0.0
2005-07-01 01:00:00 4.100446 78.466297 67.213315 72.541074 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0
2005-07-01 02:00:00 9.626502 78.876806 67.582074 72.915564 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0
2005-07-01 03:00:00 37.085389 78.877367 67.486647 72.812606 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0
2005-07-01 04:00:00 53.191464 83.204646 70.897388 76.515868 0.0 0.000000 0.0 0.0 0.0 0.0 0.0 0.0
In [10]:
# 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()
In [11]:
# plot.capacities gives a graphical view of the non-timeseries variables, both input and output
model.plot.capacity()
In [12]:
# 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
Out[12]:
techs boiler chp heat_pipes:N1 heat_pipes:X1 heat_pipes:X2 heat_pipes:X3 power_lines:X1 power_lines:X2 power_lines:X3 pv supply_gas supply_grid_power
locs
N1 NaN NaN NaN 0.066459 0.055975 0.105511 NaN NaN NaN NaN NaN NaN
X1 NaN 154.998801 0.066459 NaN NaN NaN NaN 0.008286 0.000691 0.000000 569.159885 14.73589
X2 5.017436 NaN 0.055975 NaN NaN NaN 0.008286 NaN NaN 30.598098 41.998857 NaN
X3 0.000000 NaN 0.105511 NaN NaN NaN 0.000691 NaN NaN 18.692301 0.000000 NaN
In [13]:
# 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')
Out[13]:
In [14]:
# We can examine levelized costs for each location and technology

lcoes = model.results.systemwide_levelised_cost.loc[{'carriers': 'electricity', 'costs':'monetary'}].to_pandas()
lcoes
Out[14]:
techs
boiler                     inf
supply_grid_power     0.115972
chp                   0.016822
demand_heat                NaN
heat_pipes                 NaN
pv                    0.044896
demand_electricity         NaN
supply_gas                 inf
power_lines                NaN
dtype: float64
In [15]:
# 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')
In [16]:
# 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.

t src="https://cdnjs.cloudflare.com/ajax/libs/require.js/2.1.10/require.min.js">

Calliope Urban Scale Example Model