Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

fix to avoid negative objective value #961

Merged
merged 17 commits into from
Jul 12, 2024

Conversation

GbotemiB
Copy link
Contributor

@GbotemiB GbotemiB commented Jan 18, 2024

Closes #956

Changes proposed in this Pull Request

The changes in the PR is as suggested in #956 which is to adjust the lines in base_network.py:_set_lines_s_nom_from_linetypes from s_nom to s_nom_min to prevent negative objective values after solving network.

Checklist

  • I consent to the release of this PR's code under the AGPLv3 license and non-code contributions under CC0-1.0 and CC-BY-4.0.
  • I tested my contribution locally and it seems to work fine.
  • Code and workflow changes are sufficiently documented.
  • Newly introduced dependencies are added to envs/environment.yaml and doc/requirements.txt.
  • Changes in configuration options are added in all of config.default.yaml and config.tutorial.yaml.
  • Add a test config or line additions to test/ (note tests are changing the config.tutorial.yaml)
  • Changes in configuration options are also documented in doc/configtables/*.csv and line references are adjusted in doc/configuration.rst and doc/tutorial.rst.
  • A note for the release notes doc/release_notes.rst is amended in the format of previous release notes, including reference to the requested PR.

Copy link
Member

@davide-f davide-f left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great @GbotemiB :)

So, the change should not be placed here, but rather in prepare_network.
By looking at the code in prepare_network, we actually have a function "set_transmission_limits" that should do that, but it seems it is not working.

def set_transmission_limit(n, ll_type, factor, costs, Nyears=1):

It may be good to investigate why, may you be interested in that?

@GbotemiB
Copy link
Contributor Author

Thanks @davide-f,

However, when I changed s_nom to s_nom_min in base_network.py, it worked to prevent my networks from having negative objective values.

I will run the prepare_network with a debug. Hopefully, I will be able to understand why it didn't work in the first instance.

@davide-f
Copy link
Member

Thanks @davide-f,

However, when I changed s_nom to s_nom_min in base_network.py, it worked to prevent my networks from having negative objective values.

I will run the prepare_network with a debug. Hopefully, I will be able to understand why it didn't work in the first instance.

It works, but if that option is changed there, there can be problems if the user desire to not do capacity expansion on the network as p_nom doesnt get initialized properly.
The set_transmission limit was meant to do that but it seems something is not working well

@davide-f
Copy link
Member

davide-f commented Feb 5, 2024

@GbotemiB not sure if you are also working here :) In case of problems, needs feel free to write :)

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Feb 5, 2024

@GbotemiB not sure if you are also working here :) In case of problems, needs feel free to write :)

Thanks @davide-f, I looked into it last week, I couldn't fix it yet. But I will report progress on this later today.

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Feb 8, 2024

@davide-f I have been able to finally debug the prepare_network.py

def set_transmission_limit(n, ll_type, factor, costs, Nyears=1):

It turns out the section of the highlighted code below doesn't get executed.

if factor == "opt" or float(factor) > 1.0:
n.lines["s_nom_min"] = lines_s_nom
n.lines["s_nom_extendable"] = True
n.links.loc[links_dc_b, "p_nom_min"] = n.links.loc[links_dc_b, "p_nom"]
n.links.loc[links_dc_b, "p_nom_extendable"] = True

The following image should give more context into why the conditional is not working.
image

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Feb 8, 2024

lines_s_nom = n.lines.s_nom.where(n.lines.type == "", _lines_s_nom)

@davide-f, If you don't mind, can you clarify the purpose of this section

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Feb 8, 2024

_lines_s_nom = (
np.sqrt(3)
* n.lines.type.map(n.line_types.i_nom)
* n.lines.num_parallel
* n.lines.bus0.map(n.buses.v_nom)
)

I also noticed that num_parallel here is a little lower than the same that was used in base_network.py

@davide-f
Copy link
Member

@davide-f I have been able to finally debug the prepare_network.py

def set_transmission_limit(n, ll_type, factor, costs, Nyears=1):

It turns out the section of the highlighted code below doesn't get executed.

if factor == "opt" or float(factor) > 1.0:
n.lines["s_nom_min"] = lines_s_nom
n.lines["s_nom_extendable"] = True
n.links.loc[links_dc_b, "p_nom_min"] = n.links.loc[links_dc_b, "p_nom"]
n.links.loc[links_dc_b, "p_nom_extendable"] = True

The following image should give more context into why the conditional is not working. image

Nice catch!
I believe it makes sense to revise the default parameters of tutorial and default to make sure that works.
Probably, the v0.3 should be revised in to opt (maybe better) or v1.5 (like in pypsa-eur) or something like that.

Great :D

@davide-f
Copy link
Member

_lines_s_nom = (
np.sqrt(3)
* n.lines.type.map(n.line_types.i_nom)
* n.lines.num_parallel
* n.lines.bus0.map(n.buses.v_nom)
)

I also noticed that num_parallel here is a little lower than the same that was used in base_network.py

This was imported from pypsa-eur.
The idea is that where the lines do not have a specific line type (which corresponds to a s_nom), the s_nom is derived from current and voltage of that line.
I guess the idea behind it is to fill only those values that are not filled yet.
Did you find issues with this?

@davide-f
Copy link
Member

davide-f commented Feb 11, 2024

_lines_s_nom = (
np.sqrt(3)
* n.lines.type.map(n.line_types.i_nom)
* n.lines.num_parallel
* n.lines.bus0.map(n.buses.v_nom)
)

I also noticed that num_parallel here is a little lower than the same that was used in base_network.py

This is instead expected as in the simplify_network phase, the whole network is simplified to a base voltage (e.g. 380kV) equivalent and we play with num_parallel to handle the equivalence.
See

def simplify_network_to_base_voltage(n, linetype, base_voltage):

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Feb 14, 2024

I went through simplify_network.py. Why is there a need to recalculate num_parallel

n.lines["num_parallel"] = n.lines.eval("s_nom / (sqrt(3) * v_nom * i_nom)")

@GbotemiB
Copy link
Contributor Author

Also, I noticed that in base_network.py, the line types is as in the image

image

while in prepare_network.py, the line types is
image

I think it might have to do with the rebasing of voltages

@GbotemiB
Copy link
Contributor Author

I also found out that n.lines.s_nom and _lines_s_nom have different values in prepare_network.py

image

when I used replace lines_s_nom by n.lines.s_nom here, my solved network didn't have any negative objectives

if factor == "opt" or float(factor) > 1.0:
n.lines["s_nom_min"] = lines_s_nom
n.lines["s_nom_extendable"] = True
n.links.loc[links_dc_b, "p_nom_min"] = n.links.loc[links_dc_b, "p_nom"]
n.links.loc[links_dc_b, "p_nom_extendable"] = True

@GbotemiB
Copy link
Contributor Author

looking at v_nom, the values are 300kV, which I don't think we should be doing the multiplication with line_types of 380kV
image

@GbotemiB
Copy link
Contributor Author

Hi @davide-f, It looks like the reason why n.lines.s_nom is different from _lines_s_nom is because the parameters used to calculate _lines_s_nom has been altered in simplify_network:simplify_network_to_base_voltage

logger.info(f"Mapping all network lines onto a single {int(base_voltage)}kV layer")
n.lines["type"] = linetype
n.lines["v_nom"] = base_voltage
n.lines["i_nom"] = n.line_types.i_nom[linetype]
# Note: s_nom is set in base_network
n.lines["num_parallel"] = n.lines.eval("s_nom / (sqrt(3) * v_nom * i_nom)")

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Feb 20, 2024

Regarding p_nom here

n.links.loc[links_dc_b, "p_nom_min"] = n.links.loc[links_dc_b, "p_nom"]
n.links.loc[links_dc_b, "p_nom_extendable"] = True

During debugging, n.links.p_nom and n.links.p_nom_min results to zero.
image
But p_nom and p_nom_min in generators are the same which is not equal to zero.
image

@ekatef
Copy link
Collaborator

ekatef commented Feb 20, 2024

Hello!

@GbotemiB it looks like you have done a quite intense investigation and found very nice bugs (in particular, the one which relates to loading the options from the file name looks great!!), However, the whole picture still looks a bit too complicated. I'd suggest to take a step back and clarify the architecture of the lines transformations workflow, starting from osm-extracted data up to the pre-solved network. I'll doing something similar at the moment and happy to support you with that.

The resulted diagram would facilitate discussion of high-level features with @davide-f

Once the intended path would have been clear, it will simplify testing and introducing fixes, as it'll be clear what should we expect at each stage.

What do you think @GbotemiB?

@GbotemiB
Copy link
Contributor Author

Thanks @ekatef.

You are definitely right. I will start with clarifying the workflow.

@GbotemiB
Copy link
Contributor Author

Hello @davide-f @ekatef @pz-max

Following last week's discussion on a possible check for the negative objective issue.

I did a check to verify the results from the solved networks (Monte-Carlo Runs) p_nom_opt - p_nom to see if p_nom_opt > p_nom

image image image image image

@GbotemiB
Copy link
Contributor Author

I have also reverted the current change in the PR.

@davide-f
Copy link
Member

Ok, the problem is in lines and can you confirm that the following code is executed?

if factor == "opt" or float(factor) > 1.0:
n.lines["s_nom_min"] = lines_s_nom
n.lines["s_nom_extendable"] = True
n.links.loc[links_dc_b, "p_nom_min"] = n.links.loc[links_dc_b, "p_nom"]
n.links.loc[links_dc_b, "p_nom_extendable"] = True

@davide-f
Copy link
Member

davide-f commented May 29, 2024

Moreover:

  • please check in pypsa-eur if in base_network the s_nom_min is also specified
  • please check in pypsa-eur the same chunk of code in the equivalent script to check how that works there and see if there are major differences

Many thanks!

@GbotemiB
Copy link
Contributor Author

@davide-f

Checking pypsa-eur prepare_network.py

https://github.com/PyPSA/pypsa-eur/blob/4fbb3c81c4ddb589614f67dc01c885c9b3806bbe/scripts/prepare_network.py#L78-L97

This is the part that has to do with lines that is present in PyPSA-Eur but not in PyPSA-Earth

@GbotemiB
Copy link
Contributor Author

Ok, the problem is in lines and can you confirm that the following code is executed?

if factor == "opt" or float(factor) > 1.0:
n.lines["s_nom_min"] = lines_s_nom
n.lines["s_nom_extendable"] = True
n.links.loc[links_dc_b, "p_nom_min"] = n.links.loc[links_dc_b, "p_nom"]
n.links.loc[links_dc_b, "p_nom_extendable"] = True

I also already confirm that this part is executed in the workflow.

please check in pypsa-eur if in base_network the s_nom_min is also specified

Also in pypsa-eur, s_nom_min is not specified in base_network.

@davide-f
Copy link
Member

davide-f commented Jun 5, 2024

Ok, so @GbotemiB I understand that the problem is back about why the s_nom_min calculated here differs from the p_nom value available in the network right?
It seems that one of the causes seems due to the missing name in the line type; did you get why that happens?
My feeling is that it is related to the clustering procedure

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Jun 6, 2024

Hi @davide-f, I haven't figured out the cause of the missing names in the line type. I will investigate the clustering procedure to see if something has been missed there.

@GbotemiB
Copy link
Contributor Author

Hi @davide-f, I have just uploaded a simple notebook here. I made some checks in the notebook. Feel free to look through it.

@davide-f
Copy link
Member

davide-f commented Jun 11, 2024

Interesting @GbotemiB ! We are getting closer to the problem!
I see you compare: p_nom.sum(), p_nom_min.sum()
Could you add to the list also the following computation:
n.lines.eval("sqrt(3) * i_nom * v_nom") ?
Note that i_nom may not be available for the line and there may be the need to be retrieved similarly to the code you have checked some comments previously.
I think we should identify when differences occur and solve the issue.
Note, the sum may not be the best approach but for now it does ok

@GbotemiB
Copy link
Contributor Author

Interesting @GbotemiB ! We are getting closer to the problem! I see you compare: p_nom.sum(), p_nom_min.sum() Could you add to the list also the following computation: n.lines.eval("sqrt(3) * i_nom * v_nom") ? Note that i_nom may not be available for the line and there may be the need to be retrieved similarly to the code you have checked some comments previously. I think we should identify when differences occur and solve the issue. Note, the sum may not be the best approach but for now it does ok

Just a little clarification. The difference section was between p_nom_opt and p_nom, while the other was between s_nom and s_nom_min.

I wanted to observe the changes in the workflow for s_nom and s_nom_min.

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Jun 14, 2024

I have updated the notebook here. I also rearranged the process for ease of understanding.

I also noticed that num_parallel was not included in the calculation formula. Was it intentional?

@davide-f
Copy link
Member

davide-f commented Jul 1, 2024

I have updated the notebook here. I also rearranged the process for ease of understanding.

I also noticed that num_parallel was not included in the calculation formula. Was it intentional?

Cool Emmanuel, apologies missed communication.
Could you update the last cells of the notebook to account for the number of parallel lines?
With:

(np.sqrt(3)
	* base.lines["type"].map(base.line_types.i_nom) 
	* base.lines.v_nom
	* base.lines.num_parallel).sum()

We are moving forward! 🚀 :D

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Jul 2, 2024

Thanks @davide-f,

I have updated the cells in the notebook.
Feel free to check again.

@davide-f
Copy link
Member

davide-f commented Jul 3, 2024

Amazing progress @GbotemiB ! :D :D
We identified the script where problem occurs: simplify_network
Indeed for that network, the calculation using the product i_nomv_nomnum_parallel * sqrt(3) does not match the s_nom value.

Somewhere there some calculations are performed.
The huntch is that during the various steps of the simplification and probably the aggregation strategies, something does not go as expected.

To debug that, I'd create a simple auxiliary function that computes the i_nomv_nomnum_parallel product, reads the s_nom value and check if the values match and print the results.

I'd test that for the various modified functions across the script to identify where the occurrence arrives.

What do you think?

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Jul 4, 2024

@davide-f, I'm glad we identified the simplify_network script as the source of the issue.
I'll go ahead and implement this function and test it at different points in the script. Hopefully, this will help us pinpoint exactly where things are going wrong.

@davide-f
Copy link
Member

davide-f commented Jul 4, 2024

Great :D Thank you!!!
As heads up, my guess is that during some aggregation strategies, line types are lost and/or the num_parallel are not summed up.

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Jul 8, 2024

Hello @davide-f,

I looked over the simplify_network.py script, looks like there is an additional line present in pypsa-eur that is not in pypsa-earth. You can check here

I added the line to my local files in the script:simplify_network_to_base_voltage.py, this seems to resolved the negative objective issue.

What are your thoughts?

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Jul 8, 2024

I have also updated the notebook here with the recent changes I made.

@davide-f
Copy link
Member

davide-f commented Jul 10, 2024

Hello @davide-f,

I looked over the simplify_network.py script, looks like there is an additional line present in pypsa-eur that is not in pypsa-earth. You can check here

I added the line to my local files in the script:simplify_network_to_base_voltage.py, this seems to resolved the negative objective issue.

What are your thoughts?

Really?

Yeah, definitely that should be added, however, instead of having "380" fixed, it should be base_voltage.
Does that really fix the issue? Does the notebook consider this?

It seems a too easy fix... XD

Update: I've better checked the script but unfortunately that does not solve the issue.
In the notebook, as a comment it is difficult to compare the values: we need to scroll a lot to compare the s_nom value and the corresponding s_nom computed using the formula.

By comparing the values, for example, for siml,
the sum of s_noms is: 327228
while the corresponding value computed using sqrt(3) * i_nom * v_nom * num_parallel is: 281636
The two values should match

@GbotemiB
Copy link
Contributor Author

GbotemiB commented Jul 11, 2024

@davide-f, I will check the notebook again. But I tested the implementation against the monte-carlo notebook. The negative objective values seem to not be present.
https://github.com/GbotemiB/documentation/blob/monte-carlo/notebooks/monte_carlo_tutorial.ipynb

@davide-f
Copy link
Member

davide-f commented Jul 11, 2024

@davide-f, I will check the notebook again. But I tested the implementation against the monte-carlo notebook. The negative objective values seem to not be present. https://github.com/GbotemiB/documentation/blob/monte-carlo/notebooks/monte_carlo_tutorial.ipynb

I understand, but the objective can still be misleading.
The s_nom_min should match s_nom.
Now we have reduced the difference but the problem is not fixed definitely unfortunately, if you had still resources. If not no problem

@GbotemiB
Copy link
Contributor Author

@davide-f, I will check the notebook again. But I tested the implementation against the monte-carlo notebook. The negative objective values seem to not be present. https://github.com/GbotemiB/documentation/blob/monte-carlo/notebooks/monte_carlo_tutorial.ipynb

I understand, but the objective can still be misleading. The s_nom_min should match s_nom. Now we have reduced the difference but the problem is not fixed definitely unfortunately, if you had still resources. If not no problem

@davide-f, Looks like I forgot to update the path in the notebook. I think we can smile now 😄
I updated the runs already, feel free to check.
https://github.com/GbotemiB/csp-design/blob/notebook/negative_obj.ipynb

Copy link
Member

@davide-f davide-f left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Great @GbotemiB :D
Would you like to add a release note? :)
This PR is ready to fly! :)

@davide-f
Copy link
Member

Many thanks @GbotemiB ! Merging :)

@davide-f davide-f merged commit af64048 into pypsa-meets-earth:main Jul 12, 2024
5 checks passed
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

add lines as s_nom_min not s_nom
3 participants