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 singlepandas.DataFrame
that you can then filter, resample, analyse, etc. in any way you wish, with all functions that you are used to frompandas
.
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"
, ormode="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 whyh2_south
andh2_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
.