oemof / oemof-solph

A model generator for energy system modelling and optimisation (LP/MILP).
https://oemof.org
MIT License
302 stars 126 forks source link

Non-intuitve access to optimized storage capacity value (investment mode) #550

Closed smartie2076 closed 4 years ago

smartie2076 commented 5 years ago

Hello community,

Recently I have been working with oemof optimizing storage capacities (investment mode) and noted the, from my perspective, non-intuitive way one has to get the optimized storage capacity.

When defining a storage capacity as:

storage = solph.components.GenericStorage(
    label='generic_storage',
    investment=solph.Investment(ep_costs=experiment['storage_cost_annuity']),
    inputs                          = {bus_electricity_mg: solph.Flow()},
    outputs                         = {bus_electricity_mg: solph.Flow(
        variable_costs=experiment['storage_cost_var'])},
    capacity_min                    = experiment['storage_capacity_min'],
    capacity_max                    = experiment['storage_capacity_max'],
    inflow_conversion_factor        = experiment['storage_inflow_efficiency'],
    outflow_conversion_factor       = experiment['storage_outflow_efficiency'],
    invest_relation_input_capacity  = experiment['storage_Crate'],  
    invest_relation_output_capacity = experiment['storage_Crate'] 
)

In my case, the battery can not be fully charged and discharged in one time step – it can only be charged and discharged at C-rate. This behaviour can be included:

invest_relation_input_capacity = 1/6
invest_relation_ouput_capacity = 1/5

After optimization, following .invest values related to the storage can be accessed:

electricity_bus['scalars'][(('generic_storage', 'bus_electricity_mg'), 'invest')]
    = 80.0
electricity_bus['scalars'][(('bus_electricity_mg', 'generic_storage'), 'invest')]
    = 66.7

A quick check displaying the maximal stored capacity can be performed (not equal to optimized capacity if experiment['storage_capacity_max'] != 1):

max(generic_storage['sequences'][(('generic_storage', 'None'), 'capacity')]) 
    = 400

Therefore, the invest values are neither equal to each other, not equal to the optimized storage capacity - which I was intuitively expecting as an output, as this is had been the case with the other invest values.

This happens due to above relations != 1. The invest value is equal to the portion of investment costs connected to the flow (respectively the input or output flow of the storage). It is necessary to process the invest value further:

optimized storage capacity = electricity_bus['scalars'][(('generic_storage', 'bus_electricity_mg'), 'invest')] / invest_relation_input_capacity
    = electricity_bus['scalars'][(('bus_electricity_mg', 'generic_storage'), 'invest')] / invest_relation_output_capacity 
    = 400

I thought of this as a bit confusing. I think it would be better, if...

Best, Martha

uvchik commented 5 years ago

The storage consists of three parts. The charging capacity, the storage capacity and the discharge capacity. In the non-investment mode you can just define these three capacities. (The English capacity might be confusing (charge/discharge in power units and storage capacity in energy units).

Now you can use the investment mode on all three parts separately. This can be useful if you have a PHES with prices for the pump, the turbine and the basin.

If you think of a battery you may want to couple the investment because a small battery has the same relation between capacity, input and output as a bigger battery (or as ten batteries). Therefore, you can use the invest_relation_input_capacityor invest_relation_output_capacity to couple the invest of the in-/output capacity with the storage capcity. This is what you did.

To get the results you can use:

# storage capacity
>>> results[(storage, None)]['scalars']['invest']
400

# input capacity
>>> results[(electricity_bus, storage)]['scalars']['invest']
66.66666666666667

# output capacity
>>> results[(storage, electricity_bus)]['scalars']['invest']
80

And this is not surprisingly exactly what you defined. Input is 1/6 of 400 and output 1/5 of 400.

invest_relation_input_capacity = 1/6
invest_relation_ouput_capacity = 1/5

Does that help?

If you have an idea how to describe that better in the documentation your contribution is highly appreciated :slightly_smiling_face:

smartie2076 commented 5 years ago

Thank you for the fast reply Uwe!

Now I got why it makes sense to have investments connected to the these three instances to a storage (kWh, in/out power) - I was so fixated on batteries, that I totally forgot about other storage solutions and the possibility to use the investment mode on them.

In a minimal example, I tried to access the total capacity in the way you suggested

print(results[(generic_storage, None)]['scalars']['invest'])

And it works just fine. But I noticed an issue when including it in my code. As soon as I use

micro_grid_system.dump(dpath=output_folder, filename=output_file+".oemof")
micro_grid_system = solph.EnergySystem()
micro_grid_system.restore(dpath=output_folder, filename=output_file+".oemof")

It is not possible to get this value from a restored .oemof file. I suppose this is because my oemof-objects generic_storage and electricity_bus_mg are not restored and thus can not be called? It´s a bit over my head. And probably why I went astray from the examples and ended up calling the optimized value from the flows. The error message is:

KeyError: (<oemof.solph.components.GenericStorage object at 0x7f1d90628958>, None)

I then tried to access the values by calling their labels – without success:

print(results[('generic_storage', None)]['scalars']['invest'])
KeyError: ('generic_storage', None)

Using outputlib I can successfully call the values by using object labels:

generic_storage = outputlib.views.node(results, 'generic_storage')
generic_storage['scalars'][(('generic_storage', 'None'), 'invest')])
generic_storage['scalars'][(('generic_storage', 'bus_electricity_mg'), 'invest')])
generic_storage['scalars'][(('bus_electricity_mg', 'generic_storage'), 'invest')])

I think it would be better if it were possible to call the optimized storage capacity right from the restored results as well. Anyway, I agree that my first issue and this second one can be solved by a more detailed documentation (I also didn't know that outputlib.views.node(results, 'generic_storage') would still have the 'invest' value and not only the sequences) . Can't guarantee though that I will come up with an idea how to phrase it differently...

Martha

uvchik commented 5 years ago

If you dump and restore the object is internally not the same any more. This has to do with Python and not with oemof. But actually it is no problem because in the long run you may want to split the scripts and produce results with one script and analyse them later on a another script.

in that case you can use energystem.add(solph.components.GenericStorage(...) directly because you do not need the variable.

You have different ways to find you results, which depend on the size of your problem:

Big models:

list_of_storages = [x[0] for x in results if
                    (isinstance(x[0], solph.components.GenericStorage)) &
                    (x[1] is None)]
for s in list_of_storages:
    print(results[s, None]['scalars'])

I use such filter to sort my over 250 components in my model. You can use them in small models as well and if you know that there will be only one storage, you don not have to loop but can use list_of_storages[0]. The advantage is, that you can use it always. You can also use labels to find a component explained here.

storages = [x[0] for x in results if (x[0].label.category == 'storage') & (x[1] is None)]
for s in storages:
    print(results[s, None]['scalars'])

# with one storage
print(results[storages[0], None]['scalars'])

But if you know the name you could also use the groups method as described in the basic_example.

storage = energysystem.groups["my_battery"]
print(results[storage, None]['scalars'])

Or you can use the convert_keys_to_strings() function to convert the object-keys to string keys. You have to be careful that None will also be string. If you pass a string to the outputlib.views.node() function the convert_keys_to_strings() function is internally used. So you can use the strings afterwards. If you pass an object to outputlib.views.node() this will NOT happen and you cannot use strings afterwards. I do not like strings as keys but it might be reasonable in small models.

By the way, the reason why we dump and restore in one script is to show people what will happen. Because normally it is absolutely useless to do that within one script.

There might be some typos but i am too lazy to check after writing all this... :smile:

uvchik commented 5 years ago

We may want to use some of my examples to improve the documentation...

https://oemof.readthedocs.io/en/stable/oemof_outputlib.html

smartie2076 commented 5 years ago

Pew, I still have a long way to go to be fluent in python and oemof. :D Thank you for the long answer - I will do my best to understand and then incorporate it into my scripts...

uvchik commented 5 years ago

I leave it open as a reminder, that we want to improve the documentation.

@mloenneberga wll be in charge.

p-snft commented 5 years ago

I postpone this to the v0.3.1 release -- and suggest to do it better sooner than later.

uvchik commented 5 years ago

I think this is related to #500.

joroeder commented 5 years ago

Same as in #500, we could close this issue for now, if nobody wants to have some further improvement at the moment.

jakob-wo commented 4 years ago

I think it is time to close this Issue.