Running the MILP example model¶
This notebook will show you how to load, build, solve, and examine the results of the MILP example model.
import calliope
# We increase logging verbosity
calliope.set_log_verbosity("INFO", include_solver_output=False)
Load model and examine inputs¶
model = calliope.examples.milp()
[2024-12-16 11:51:40] INFO Model: initialising
[2024-12-16 11:51:40] INFO (scenarios, milp ) | Applying the following overrides: ['milp'].
[2024-12-16 11:51:40] INFO Model: preprocessing stage 1 (model_run)
[2024-12-16 11:51:43] INFO Model: preprocessing stage 2 (model_data)
[2024-12-16 11:51:43] INFO Model: preprocessing complete
Model inputs can be viewed at model.inputs
.
Variables are indexed over any combination of techs
, nodes
, carriers
, costs
and timesteps
.
model.inputs
<xarray.Dataset> Size: 89kB Dimensions: (costs: 1, techs: 12, carriers: 3, nodes: 4, timesteps: 48) Coordinates: * costs (costs) object 8B 'monetary' * techs (techs) object 96B 'N1_to_X2' ... 'supply_gri... * carriers (carriers) object 24B 'electricity' 'gas' 'heat' * nodes (nodes) object 32B 'N1' 'X1' 'X2' 'X3' * timesteps (timesteps) datetime64[ns] 384B 2005-07-01 ..... Data variables: (12/42) cost_interest_rate (costs) float64 8B 0.1 bigM float64 8B 1e+06 objective_cost_weights (costs) float64 8B 1.0 base_tech (techs) object 96B 'transmission' ... 'supply' cap_method (techs) object 96B nan nan nan ... nan nan nan carrier_export (techs, carriers) float64 288B nan nan ... nan ... ... longitude (nodes) float64 32B -0.1247 -0.1613 ... -0.1311 source_use_equals (techs, timesteps) float64 5kB nan nan ... nan sink_use_equals (timesteps, techs, nodes) float64 18kB nan ..... definition_matrix (nodes, techs, carriers) bool 144B False ... ... timestep_resolution (timesteps) float64 384B 1.0 1.0 1.0 ... 1.0 1.0 timestep_weights (timesteps) float64 384B 1.0 1.0 1.0 ... 1.0 1.0 Attributes: calliope_version_defined: 0.7.0 calliope_version_initialised: 0.7.0.dev5 applied_overrides: milp scenario: milp defaults: {'bigM': 1000000000.0, 'objective_cost_wei... allow_operate_mode: 1 config: {'init': {'name': 'Urban-scale example mod... name: Urban-scale example model with MILP timestamp_model_creation: 1734349900.765952
Individual data variables can be accessed easily, to_series().dropna()
allows us to view the data in a nice tabular format.
model.inputs.flow_cap_max.to_series().dropna()
techs carriers nodes N1_to_X2 heat N1 2000.0 X2 2000.0 N1_to_X3 heat N1 2000.0 X3 2000.0 X1_to_N1 heat N1 2000.0 X1 2000.0 X1_to_X2 electricity X1 2000.0 X2 2000.0 X1_to_X3 electricity X1 2000.0 X3 2000.0 boiler heat X2 600.0 X3 600.0 pv electricity X1 250.0 X2 250.0 X3 50.0 supply_gas gas X1 2000.0 X2 2000.0 X3 2000.0 supply_grid_power electricity X1 2000.0 Name: flow_cap_max, dtype: float64
You can apply node/tech/carrier/timesteps only operations, like summing information over timesteps
model.inputs.sink_use_equals.sum(
"timesteps", min_count=1, skipna=True
).to_series().dropna()
techs nodes demand_electricity X1 35.271156 X2 8796.878622 X3 1244.604116 demand_heat X1 33.999992 X2 7147.808356 X3 50.567751 Name: sink_use_equals, dtype: float64
Build and solve the optimisation problem.¶
Results are loaded into model.results
.
By setting the log verbosity at the start of this tutorial to "INFO", we can see the timing of parts of the run, as well as the solver's log.
model.build()
model.solve()
[2024-12-16 11:51:43] INFO Model: backend build starting
[2024-12-16 11:51:43] INFO Math preprocessing | added file 'plan'.
[2024-12-16 11:51:43] INFO Math preprocessing | added file 'additional_math.yaml'.
[2024-12-16 11:51:43] INFO Math preprocessing | validated math against schema.
[2024-12-16 11:51:44] INFO Optimisation Model | parameters | Generated.
[2024-12-16 11:51:47] INFO Optimisation Model | Validated math strings.
[2024-12-16 11:51:47] INFO Optimisation Model | variables | Generated.
[2024-12-16 11:51:48] INFO Optimisation Model | global_expressions | Generated.
[2024-12-16 11:51:50] INFO Optimisation Model | constraints | Generated.
[2024-12-16 11:51:50] INFO Optimisation Model | piecewise_constraints | Generated.
[2024-12-16 11:51:50] INFO Optimisation Model | objectives | Generated.
[2024-12-16 11:51:50] INFO Model: backend build complete
[2024-12-16 11:51:50] INFO Optimisation model | starting model in plan mode.
[2024-12-16 11:51:59] INFO Backend: solver finished running. Time since start of solving optimisation problem: 0:00:08.870915
[2024-12-16 11:51:59] INFO Postprocessing: zero threshold of 1e-10 not required
[2024-12-16 11:51:59] INFO Postprocessing: ended. Time since start of solving optimisation problem: 0:00:09.002684
[2024-12-16 11:51:59] INFO Backend: model solve completed. Time since start of solving optimisation problem: 0:00:09.005506
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
Examine results¶
model.results
<xarray.Dataset> Size: 417kB Dimensions: (nodes: 4, techs: 12, carriers: 3, timesteps: 48, costs: 1) Coordinates: * techs (techs) object 96B 'N1_to_X2' ... 'supply_gri... * carriers (carriers) <U11 132B 'electricity' 'gas' 'heat' * nodes (nodes) object 32B 'N1' 'X1' 'X2' 'X3' * timesteps (timesteps) datetime64[ns] 384B 2005-07-01 ..... * costs (costs) object 8B 'monetary' Data variables: (12/25) flow_cap (nodes, techs, carriers) float64 1kB nan ... nan link_flow_cap (techs) float64 96B 214.8 10.4 238.7 ... nan nan flow_out (nodes, techs, carriers, timesteps) float64 55kB ... flow_in (nodes, techs, carriers, timesteps) float64 55kB ... flow_export (nodes, techs, carriers, timesteps) float64 55kB ... area_use (nodes, techs) float64 384B nan nan ... nan nan ... ... cost_operation_fixed (nodes, techs, costs) float64 384B nan ... nan cost (nodes, techs, costs) float64 384B 0.05836 ..... capacity_factor (nodes, techs, carriers, timesteps) float64 55kB ... systemwide_capacity_factor (techs, carriers) float64 288B 0.0 0.0 ... 0.0 systemwide_levelised_cost (carriers, techs, costs) float64 288B nan ...... total_levelised_cost (carriers, costs) float64 24B 0.08171 ... 0.1099 Attributes: (12/14) termination_condition: optimal calliope_version_defined: 0.7.0 calliope_version_initialised: 0.7.0.dev5 applied_overrides: milp scenario: milp defaults: {'bigM': 1000000000.0, 'objective_cost_wei... ... ... name: Urban-scale example model with MILP timestamp_model_creation: 1734349900.765952 timestamp_build_start: 1734349903.664858 timestamp_build_complete: 1734349910.250961 timestamp_solve_start: 1734349910.252127 timestamp_solve_complete: 1734349919.257633
Integer variables purchased_units
and operating_units
are available in the results
model.results.purchased_units.to_series().dropna()
nodes techs X1 chp 1.0 X2 boiler 1.0 X3 boiler 0.0 Name: purchased_units, dtype: float64
model.results.operating_units.to_series().dropna().unstack("techs").head()
techs | chp | |
---|---|---|
nodes | timesteps | |
X1 | 2005-07-01 00:00:00 | 1.0 |
2005-07-01 01:00:00 | 1.0 | |
2005-07-01 02:00:00 | 1.0 | |
2005-07-01 03:00:00 | 1.0 | |
2005-07-01 04:00:00 | 1.0 |
We can sum heat output over all locations and turn the result into a pandas DataFrame.
Note: heat output of transmission technologies (e.g., N1_to_X2
) is the import of heat at nodes.
df_heat = (
model.results.flow_out.sel(carriers="heat")
.sum("nodes", min_count=1, skipna=True)
.to_series()
.dropna()
.unstack("techs")
)
df_heat.head()
techs | N1_to_X2 | N1_to_X3 | X1_to_N1 | boiler | chp |
---|---|---|---|---|---|
timesteps | |||||
2005-07-01 00:00:00 | 64.731991 | 0.015600 | 69.857405 | 0.000000 | 75.585392 |
2005-07-01 01:00:00 | 66.352993 | 0.860322 | 72.541074 | 4.100446 | 78.466297 |
2005-07-01 02:00:00 | 67.566474 | 0.015600 | 72.915564 | 9.626502 | 78.876806 |
2005-07-01 03:00:00 | 67.471047 | 0.015600 | 72.812606 | 37.085389 | 78.877367 |
2005-07-01 04:00:00 | 70.036981 | 0.860407 | 76.515868 | 53.191464 | 83.204646 |
You can apply node/tech/carrier only operations, like summing information over nodes
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 5 columns): # Column Non-Null Count Dtype --- ------ -------------- ----- 0 N1_to_X2 48 non-null float64 1 N1_to_X3 48 non-null float64 2 X1_to_N1 48 non-null float64 3 boiler 48 non-null float64 4 chp 48 non-null float64 dtypes: float64(5) memory usage: 2.2 KB
We can also examine total technology costs.
costs = model.results.cost.to_series().dropna()
costs
nodes techs costs N1 N1_to_X2 monetary 0.058362 N1_to_X3 monetary 0.003767 X1_to_N1 monetary 0.064846 X1 X1_to_N1 monetary 0.064846 X1_to_X2 monetary 0.008273 X1_to_X3 monetary 0.000691 chp monetary 183.482186 pv monetary 0.000000 supply_gas monetary 528.971602 supply_grid_power monetary 64.468219 X2 N1_to_X2 monetary 0.058362 X1_to_X2 monetary 0.008273 boiler monetary 11.853703 pv monetary 52.821906 supply_gas monetary 39.663214 X3 N1_to_X3 monetary 0.003767 X1_to_X3 monetary 0.000691 boiler monetary 0.000000 pv monetary 18.692301 supply_gas monetary 0.000000 Name: cost, dtype: float64
We can also examine levelized costs for each location and technology, which is calculated in a post-processing step.
lcoes = (
model.results.systemwide_levelised_cost.sel(carriers="electricity")
.to_series()
.dropna()
)
lcoes
techs costs X1_to_X2 monetary 0.000002 X1_to_X3 monetary 0.000002 chp monetary 0.021430 pv monetary 0.038439 supply_grid_power monetary 0.108373 Name: systemwide_levelised_cost, dtype: float64
See the Calliope documentation for more details on setting up and running a Calliope model.