Extracting results: Part II

This tutorial showcases more ways to handle and analyse results. Make sure that you’ve read the first part!

We make use of the same example, and first just load and run it.

import iesopt

config_file = iesopt.make_example(
    "48_custom_results", dst_dir="ex_custom_results", dst_name="config"
)
model = iesopt.run(config_file, verbosity=False)

Remember that the most versatile way (for most tasks) is one that you already know: df = model.results.to_pandas(). This will give you all results as a single pandas.DataFrame that you can then filter, resample, analyse, etc. in any way you wish, with all functions that you are used to from pandas.

What should I look at?

If you are unsure which results are even available, you can make use of query_available_results(...) to find out:

model.results.query_available_results("storage.storage", mode="primal")
[('var', 'state'), ('exp', 'injection')]

This shows you that two results exist for the component storage.storage: var.state (the level of the state of this Node) and exp.injection (the expression holding the injection into the Node).

More results are available when using mode="dual", or mode="both" - try it out!

If you’d be interested in seeing results for the first, then you could (check part I of this tutorial for different ways to access this) do:

model.results.get("component", "storage.storage", "var", "state", mode="primal")
array([-0.        ,  4.42105263,  0.94736842,  0.94736842, -0.        ,
       -0.        , -0.        , -0.        ,  3.19552632,  6.80552632,
        6.90052632, 10.13052632,  6.65684211,  4.02526316,  3.70947368,
        3.28842105,  3.28842105,  3.28842105,  2.44631579,  4.72631579,
        8.52631579,  4.94736842,  1.47368421,  0.52631579])

Finally, one hint: query_available_results(...) treats its first parameter as regular expression, so you can use any regex you want to look up more than one component at the same time! regex.101 is a good place to test your regular expressions that you wrote using any LLM (they are quite okay at that!).

Looking at (a) specific component(s)

If you are now interested in seeing all results for a single component, the DataFrame returned by to_pandas(...) can get overwhelming quickly. That’s what overview(...) can be used for.

Temporal results

Observe the following:

model.results.overview("storage.*ing", temporal=True, mode="both").head()
storage.charging storage.discharging
con var con var
flow_lb flow flow_lb flow
dual dual primal dual dual primal
t1 -0.000000 0.0 4.65374 -1.026316 0.0 -0.000000
t2 -1.080332 0.0 -0.00000 -0.000000 0.0 3.473684
t3 0.000000 0.0 -0.00000 -1.026316 0.0 -0.000000
t4 -1.080332 0.0 -0.00000 -0.000000 0.0 0.947368
t5 0.000000 0.0 -0.00000 -1.026316 0.0 -0.000000

As you can see:

  • The results are automatically given in wide format.

  • The regular expression "storage.*ing" matched all components that (1) start with “storage”, but also (2) end with “ing”. Try changing that to "storage." and see what other components get matched too.

  • Since we passed mode="both", it returns both primal and dual results. Try passing "primal" or "dual" instead.

Since we set temporal=True, we got results that are available for every Snapshot.

Non-temporal results

Let’s see what happens if we instead do:

model.results.overview(".*", temporal=False, mode="both")
storage.storage  con  last_state_lb  dual        0.000000
                      last_state_ub  dual       -0.000000
generator        obj  marginal_cost  primal    981.174515
dtype: float64

Since all results are now non-temporal, we get back a pandas.Series instead.

Can you explain why this now contains the dual results of constraints constructed by storage.storage? The documentation of the core component Node may help… But - in any case, feel free to ask stuff like this (we are very happy to answer this).

But, lets look at an example that contains more interesting results for something like this. First we pull a different example and solve it

other_config = iesopt.make_example(
    "08_basic_investment", dst_dir="ex_custom_results", dst_name="config"
)
other_model = iesopt.run(other_config, verbosity=False)
INFO:iesopt:Data folder for examples already exists; NOT copying ANY contents
INFO:iesopt:Creating example ('08_basic_investment') at: 'ex_custom_results/config.iesopt.yaml'
INFO:iesopt:Set write permissions for example ('ex_custom_results/config.iesopt.yaml'), and data folder ('ex_custom_results/files')

… and then we take a look at the primal, non-temporal results:

other_model.results.overview(".*", temporal=False, mode="primal")
build_pipeline  var  value          primal      0.750000
                obj  value          primal    750.000000
build_gas       var  value          primal      0.607143
                obj  value          primal    303.571429
plant_gas       obj  marginal_cost  primal    917.000000
build_storage   var  value          primal      0.450714
                obj  value          primal     45.071429
dtype: float64

This shows us the resulting values of all investment decisions in the model (e.g., (build_pipeline, var, value)), there associated costs (e.g., (build_pipeline, obj, value)), as well as the objective contribution of the marginal costs induced by operating plant_gas.

To only see investment decisions, you could either take this series and further filter it, for example by doing

sr = other_model.results.overview(".*", temporal=False, mode="primal")

sr[sr.index.get_level_values(1) == "var"]

which (un-)fortunately also hides the obj entries of the investment decisions.

Or you could stick to an “intelligent” naming convention of your components (for example like we did in the example, naming all investment decisions build_***) and make use of the regular expression support of overview(...) by instead doing:

other_model.results.overview("^build_.*$", temporal=False, mode="primal")
build_pipeline  var  value  primal      0.750000
                obj  value  primal    750.000000
build_gas       var  value  primal      0.607143
                obj  value  primal    303.571429
build_storage   var  value  primal      0.450714
                obj  value  primal     45.071429
dtype: float64

Check out regex101.com/r/GzgzG2/1, and read through the “Explanation” section, to understand what ^build_.*$ actually achieves. Note: Using the intuitive way, other_model.results.overview("build_", ...), would have worked (here) as well. The difference is minimal and subtle, but …

Filtering components

If you are not familiar with regular expressions, don’t worry. The most commonly used “filters” work as expected. Let’s switch back to temporal results and stick with the “new” example that we have just used.

Selecting a specific component

other_model.results.overview("pipeline", temporal=True, mode="primal").head()
pipeline
var
flow
primal
t1 -0.0
t2 -0.0
t3 -0.0
t4 -0.0
t5 -0.0

Selecting all components containing “plant”

other_model.results.overview("plant", temporal=True, mode="primal").head()
plant_gas plant_solar
exp var exp var
out_electricity conversion out_electricity conversion
primal primal primal primal
t1 0.000000 0.000000 0.00 -0.00
t2 0.000000 0.000000 0.00 -0.00
t3 0.034286 0.034286 0.02 0.02
t4 0.000000 0.000000 0.07 0.07
t5 0.000000 0.000000 0.16 0.16

Selecting specific components

Using |, you can select multiple specific components.

Note: You can use that multiple times. Try out passing "h2_south|h2_north|demand"! Can you explain why h2_south and h2_north return different types of results? If not - go ahead, ask us!

other_model.results.overview("h2_south|elec", temporal=True, mode="primal").head()
elec electrolysis h2_south
exp exp var exp
injection in_electricity out_h2 conversion injection
primal primal primal primal primal
t1 0.0 0.000000 0.000000 -0.000000 0.0
t2 0.0 0.000000 0.000000 -0.000000 0.0
t3 0.0 0.054286 0.027143 0.027143 0.0
t4 0.0 0.070000 0.035000 0.035000 0.0
t5 0.0 0.160000 0.080000 0.080000 0.0

But wait … we did not want to get results for electrolysis. That’s the disadvantage of being able to search for components containing plant, as shown before: Since electrolysis contains elec it matches this too.

Let’s fix this. Remember that we used ^build_.*$ before, without it being clear what this achieves? Let’s see …

other_model.results.overview("h2_south|^elec$", temporal=True, mode="primal").head()
elec h2_south
exp exp
injection injection
primal primal
t1 0.0 0.0
t2 0.0 0.0
t3 0.0 0.0
t4 0.0 0.0
t5 0.0 0.0

It works!

But why … ?

As regex101.com/r/GzgzG2/1 explains:

  • ^ asserts position at start of a line

  • $ asserts position at the end of a line

That means, instead of looking for any component that contains elec, we are looking for one that does not contain ANY characters before elec and also NONE after elec. In other words: It matches exactly elec.