Skip to content

Add Left-Right Planarity Test Algorithm to NetworKit#1276

Merged
fabratu merged 45 commits intonetworkit:masterfrom
Schwarf:feature/left_right_planarity_test
Feb 4, 2025
Merged

Add Left-Right Planarity Test Algorithm to NetworKit#1276
fabratu merged 45 commits intonetworkit:masterfrom
Schwarf:feature/left_right_planarity_test

Conversation

@Schwarf
Copy link
Copy Markdown
Contributor

@Schwarf Schwarf commented Jan 8, 2025

Overview

This PR introduces an implementation of the Left-Right Planarity Test algorithm, which is used to determine whether a given graph is planar. The implementation was guided by the paper "The Left-Right Planarity Test" as well as the corresponding implementation in NetworkX.

Unit Tests

To ensure correctness and robustness, I have included 18 unit tests for the algorithm. However, given the complexity of the problem, I would be happy to add more test cases with known outcomes if provided or suggested.

Graph API Extension

I added a new method, sortNeighbors, to the Graph class. This method sorts the neighbors of a graph node based on a binary predicate, and I have included a corresponding unit test for this functionality. However, it’s possible that this could be achieved with existing APIs, and I would appreciate guidance if such an alternative already exists.

Notes

This PR represents my first feature contribution to NetworKit. There is likely room for optimization, and I welcome any feedback or suggestions for improvement.

I look forward to your review and guidance to help refine this contribution.

@fabratu fabratu self-assigned this Jan 8, 2025
…e default constructor of Edge class constepxr.
Copy link
Copy Markdown
Member

@angriman angriman left a comment

Choose a reason for hiding this comment

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

Thank you for your contribution! I left some comments, most of them are just minor style changes to be consistent with the rest of the codebase.

@Schwarf
Copy link
Copy Markdown
Contributor Author

Schwarf commented Jan 19, 2025

Hi @angriman,

Thank you for your feedback! I’d like to provide some context and arguments for the choice of graph sizes in the tests and the use of many test cases:

  1. Multiple and Larger Graphs
    During the initial implementation, I relied on smaller, single-graph tests, which passed successfully. However, when I extended the tests to include many graphs of the same class (e.g., multiple grid graphs instead of a single 3x3 grid graph), some cases emerged, revealing bugs in the algorithm. Additionally, larger and random graphs helped me uncover issues that smaller graphs missed. These are the main reasons I included these tests.

  2. Algorithm extension
    I plan to extend my contribution by modifying the algorithm to provide planar embeddings for planar graphs. This may require minor refactoring of the current implementation. Similarly, potential performance improvements might also lead to refactoring. Having a variety of test cases (including larger and multiple graphs) will make it easier for me and others to ensure correctness during these changes.

  3. Negligible Runtime Cost
    On my machine, the runtime difference between testing a single graph versus hundreds of graphs is of the order of a few milliseconds, which is negligible compared to tests that take up to 10 seconds. The same applies to the included large random graphs. While I understand the importance of keeping tests efficient ("Kleinvieh macht auch Mist"), I currently believe the benefits of including larger and multiple graphs, outweigh the costs.

Given these reasons, I’d appreciate it if we could retain the current test cases in the file LeftRightPlanarityCheckGTest.cpp. Of course, I’m happy to hear your thoughts.

What do you think?

@fabratu
Copy link
Copy Markdown
Member

fabratu commented Jan 20, 2025

Hi @angriman,

Thank you for your feedback! I’d like to provide some context and arguments for the choice of graph sizes in the tests and the use of many test cases:

1. Multiple and Larger Graphs
   During the initial implementation, I relied on smaller, single-graph tests, which passed successfully. However, when I extended the tests to include many graphs of the same class (e.g., multiple grid graphs instead of a single 3x3 grid graph), some cases emerged, revealing bugs in the algorithm. Additionally, larger and random graphs helped me uncover issues that smaller graphs missed. These are the main reasons I included these tests.

2. Algorithm extension
   I plan to extend my contribution by modifying the algorithm to provide planar embeddings for planar graphs. This may require minor refactoring of the current implementation. Similarly, potential performance improvements might also lead to refactoring. Having a variety of test cases (including larger and multiple graphs) will make it easier for me and others to ensure correctness during these changes.

3. Negligible Runtime Cost
   On my machine, the runtime difference between testing a single graph versus hundreds of graphs is of the order of a few milliseconds, which is negligible compared to tests that take up to 10 seconds. The same applies to the included large random graphs. While I understand the importance of keeping tests efficient ("Kleinvieh macht auch Mist"), I currently believe the benefits of including larger and multiple graphs, outweigh the costs.

Given these reasons, I’d appreciate it if we could retain the current test cases in the file LeftRightPlanarityCheckGTest.cpp. Of course, I’m happy to hear your thoughts.

What do you think?

There are positives and negatives about having random generated graphs for testing. In the past it helped us discover some quite subtle bugs. However, sometimes these can come from the generators itself, leading to the case that a test implicitly checks the functionalities of different code units. Also you can not predict when these errors turn up. Overall there are better systems to generate these tests, but these would need a complete pipeline overhaul.

We had more bugs in algorithms / core functionalities which only use a pre-defined set of graphs, since these are often too narrow to cover all corner-cases and are tailored around the original use-cases of the algorithm. Therefore I would not drop the strategy of testing random graphs.

It is true though that this one algorithm does incorporate a lot of generated graphs. While fast for now, running times can explode when new features are encoded or data structures change. I would therefore suggest to drop some redundancy / cases. As a rule of thumb, most algorithms don't test for more than 10 different graphs.

@Schwarf
Copy link
Copy Markdown
Contributor Author

Schwarf commented Jan 20, 2025

Hi @angriman,
Thank you for your feedback! I’d like to provide some context and arguments for the choice of graph sizes in the tests and the use of many test cases:

1. Multiple and Larger Graphs
   During the initial implementation, I relied on smaller, single-graph tests, which passed successfully. However, when I extended the tests to include many graphs of the same class (e.g., multiple grid graphs instead of a single 3x3 grid graph), some cases emerged, revealing bugs in the algorithm. Additionally, larger and random graphs helped me uncover issues that smaller graphs missed. These are the main reasons I included these tests.

2. Algorithm extension
   I plan to extend my contribution by modifying the algorithm to provide planar embeddings for planar graphs. This may require minor refactoring of the current implementation. Similarly, potential performance improvements might also lead to refactoring. Having a variety of test cases (including larger and multiple graphs) will make it easier for me and others to ensure correctness during these changes.

3. Negligible Runtime Cost
   On my machine, the runtime difference between testing a single graph versus hundreds of graphs is of the order of a few milliseconds, which is negligible compared to tests that take up to 10 seconds. The same applies to the included large random graphs. While I understand the importance of keeping tests efficient ("Kleinvieh macht auch Mist"), I currently believe the benefits of including larger and multiple graphs, outweigh the costs.

Given these reasons, I’d appreciate it if we could retain the current test cases in the file LeftRightPlanarityCheckGTest.cpp. Of course, I’m happy to hear your thoughts.
What do you think?

There are positives and negatives about having random generated graphs for testing. In the past it helped us discover some quite subtle bugs. However, sometimes these can come from the generators itself, leading to the case that a test implicitly checks the functionalities of different code units. Also you can not predict when these errors turn up. Overall there are better systems to generate these tests, but these would need a complete pipeline overhaul.

We had more bugs in algorithms / core functionalities which only use a pre-defined set of graphs, since these are often too narrow to cover all corner-cases and are tailored around the original use-cases of the algorithm. Therefore I would not drop the strategy of testing random graphs.

It is true though that this one algorithm does incorporate a lot of generated graphs. While fast for now, running times can explode when new features are encoded or data structures change. I would therefore suggest to drop some redundancy / cases. As a rule of thumb, most algorithms don't test for more than 10 different graphs.

Hi @farbratu,

Thank you for your detailed feedback! I have to admit that I might not have fully grasped all the concerns raised by @angriman. Thank you for providing additional context and suggestions.

  1. I agree with your suggestion to reduce redundancy and limit the number of graphs tested for each type. I’ll adjust the tests to include around 10 graphs per type and in some cases, perhaps up to 20.
  2. Regarding the use of random graphs: these are programmatically generated. I will reduce their size from 50–100 nodes to a more manageable 10–20 nodes to ensure the tests remain efficient while still being diverse.

Before I proceed, is the above proposal acceptable for you guys?

@angriman
Copy link
Copy Markdown
Member

Hi @angriman,
Thank you for your feedback! I’d like to provide some context and arguments for the choice of graph sizes in the tests and the use of many test cases:

1. Multiple and Larger Graphs
   During the initial implementation, I relied on smaller, single-graph tests, which passed successfully. However, when I extended the tests to include many graphs of the same class (e.g., multiple grid graphs instead of a single 3x3 grid graph), some cases emerged, revealing bugs in the algorithm. Additionally, larger and random graphs helped me uncover issues that smaller graphs missed. These are the main reasons I included these tests.

2. Algorithm extension
   I plan to extend my contribution by modifying the algorithm to provide planar embeddings for planar graphs. This may require minor refactoring of the current implementation. Similarly, potential performance improvements might also lead to refactoring. Having a variety of test cases (including larger and multiple graphs) will make it easier for me and others to ensure correctness during these changes.

3. Negligible Runtime Cost
   On my machine, the runtime difference between testing a single graph versus hundreds of graphs is of the order of a few milliseconds, which is negligible compared to tests that take up to 10 seconds. The same applies to the included large random graphs. While I understand the importance of keeping tests efficient ("Kleinvieh macht auch Mist"), I currently believe the benefits of including larger and multiple graphs, outweigh the costs.

Given these reasons, I’d appreciate it if we could retain the current test cases in the file LeftRightPlanarityCheckGTest.cpp. Of course, I’m happy to hear your thoughts.
What do you think?

There are positives and negatives about having random generated graphs for testing. In the past it helped us discover some quite subtle bugs. However, sometimes these can come from the generators itself, leading to the case that a test implicitly checks the functionalities of different code units. Also you can not predict when these errors turn up. Overall there are better systems to generate these tests, but these would need a complete pipeline overhaul.
We had more bugs in algorithms / core functionalities which only use a pre-defined set of graphs, since these are often too narrow to cover all corner-cases and are tailored around the original use-cases of the algorithm. Therefore I would not drop the strategy of testing random graphs.
It is true though that this one algorithm does incorporate a lot of generated graphs. While fast for now, running times can explode when new features are encoded or data structures change. I would therefore suggest to drop some redundancy / cases. As a rule of thumb, most algorithms don't test for more than 10 different graphs.

Hi @farbratu,

Thank you for your detailed feedback! I have to admit that I might not have fully grasped all the concerns raised by @angriman. Thank you for providing additional context and suggestions.

  1. I agree with your suggestion to reduce redundancy and limit the number of graphs tested for each type. I’ll adjust the tests to include around 10 graphs per type and in some cases, perhaps up to 20.
  2. Regarding the use of random graphs: these are programmatically generated. I will reduce their size from 50–100 nodes to a more manageable 10–20 nodes to ensure the tests remain efficient while still being diverse.

Before I proceed, is the above proposal acceptable for you guys?

I agree that testing on random graphs is very helpful for finding bugs in the algorithm you are developing (I've used them a lot in the past). However, I think that, when possible, they should not be part of unit tests because of the following reasons (more about testing best practices can be found in [1]):

  • Reproducibility: random tests can be flaky when tested on different architectures. It is important to have reproducible tests when possible.
  • If they fail, they don't help you understand what in the algorithm is not behaving as intended.

My recommendation is to test on a fixed and representative set of instances to evaluate that the algorithm behaves as intended. I understand that sometimes this is easier said than done because the algorithm may be complex. If that's the case, we can keep tests on random graphs.

Regarding large graphs: it's ok to have individual tests on larger instances (you can also use those in the input/ directory in NetworKit). What I'm against is writing a unit test with 50+ lines used for graph edges, which makes it impossible for a future reader to understand what's going on. Why do you need do define such a large graph? What kind of behavior are you trying to test that cannot be tested with a smaller instance?

If you just need to test your algorithm on a larger graph, then my recommendation is to replace the custom graphs with 50+ nodes (testRandomGeneratedPlanarGraph50/51/100Nodes) with graphs in the input/ directory.

Let me know if you have further questions!

[1] https://abseil.io/resources/swe-book/html/ch12.html

@Schwarf
Copy link
Copy Markdown
Contributor Author

Schwarf commented Jan 21, 2025

Hi @angriman,
Thank you for your feedback! I’d like to provide some context and arguments for the choice of graph sizes in the tests and the use of many test cases:

1. Multiple and Larger Graphs
   During the initial implementation, I relied on smaller, single-graph tests, which passed successfully. However, when I extended the tests to include many graphs of the same class (e.g., multiple grid graphs instead of a single 3x3 grid graph), some cases emerged, revealing bugs in the algorithm. Additionally, larger and random graphs helped me uncover issues that smaller graphs missed. These are the main reasons I included these tests.

2. Algorithm extension
   I plan to extend my contribution by modifying the algorithm to provide planar embeddings for planar graphs. This may require minor refactoring of the current implementation. Similarly, potential performance improvements might also lead to refactoring. Having a variety of test cases (including larger and multiple graphs) will make it easier for me and others to ensure correctness during these changes.

3. Negligible Runtime Cost
   On my machine, the runtime difference between testing a single graph versus hundreds of graphs is of the order of a few milliseconds, which is negligible compared to tests that take up to 10 seconds. The same applies to the included large random graphs. While I understand the importance of keeping tests efficient ("Kleinvieh macht auch Mist"), I currently believe the benefits of including larger and multiple graphs, outweigh the costs.

Given these reasons, I’d appreciate it if we could retain the current test cases in the file LeftRightPlanarityCheckGTest.cpp. Of course, I’m happy to hear your thoughts.
What do you think?

There are positives and negatives about having random generated graphs for testing. In the past it helped us discover some quite subtle bugs. However, sometimes these can come from the generators itself, leading to the case that a test implicitly checks the functionalities of different code units. Also you can not predict when these errors turn up. Overall there are better systems to generate these tests, but these would need a complete pipeline overhaul.
We had more bugs in algorithms / core functionalities which only use a pre-defined set of graphs, since these are often too narrow to cover all corner-cases and are tailored around the original use-cases of the algorithm. Therefore I would not drop the strategy of testing random graphs.
It is true though that this one algorithm does incorporate a lot of generated graphs. While fast for now, running times can explode when new features are encoded or data structures change. I would therefore suggest to drop some redundancy / cases. As a rule of thumb, most algorithms don't test for more than 10 different graphs.

Hi @farbratu,
Thank you for your detailed feedback! I have to admit that I might not have fully grasped all the concerns raised by @angriman. Thank you for providing additional context and suggestions.

  1. I agree with your suggestion to reduce redundancy and limit the number of graphs tested for each type. I’ll adjust the tests to include around 10 graphs per type and in some cases, perhaps up to 20.
  2. Regarding the use of random graphs: these are programmatically generated. I will reduce their size from 50–100 nodes to a more manageable 10–20 nodes to ensure the tests remain efficient while still being diverse.

Before I proceed, is the above proposal acceptable for you guys?

I agree that testing on random graphs is very helpful for finding bugs in the algorithm you are developing (I've used them a lot in the past). However, I think that, when possible, they should not be part of unit tests because of the following reasons (more about testing best practices can be found in [1]):

* Reproducibility: random tests can be flaky when tested on different architectures. It is important to have reproducible tests when possible.

* If they fail, they don't help you understand what in the algorithm is not behaving as intended.

My recommendation is to test on a fixed and representative set of instances to evaluate that the algorithm behaves as intended. I understand that sometimes this is easier said than done because the algorithm may be complex. If that's the case, we can keep tests on random graphs.

Regarding large graphs: it's ok to have individual tests on larger instances (you can also use those in the input/ directory in NetworKit). What I'm against is writing a unit test with 50+ lines used for graph edges, which makes it impossible for a future reader to understand what's going on. Why do you need do define such a large graph? What kind of behavior are you trying to test that cannot be tested with a smaller instance?

If you just need to test your algorithm on a larger graph, then my recommendation is to replace the custom graphs with 50+ nodes (testRandomGeneratedPlanarGraph50/51/100Nodes) with graphs in the input/ directory.

Let me know if you have further questions!

[1] https://abseil.io/resources/swe-book/html/ch12.html

Thank you for the detailed explanation of your concerns. I considered the large graphs of the input folder before but shied away from the effort to independently determine if they are planar or not. I will go ahead and try while removing the "random graphs" from the unit tests. For the "typed graphs" I will reduce the number of generated graphs as proposed above and reorganize the tests to focus on specific behaviors. Thank your for the link to the article. It was a great read and a helpful reminder of best practices.

@Schwarf Schwarf requested review from angriman and fabratu January 26, 2025 13:05
angriman
angriman previously approved these changes Jan 27, 2025
Copy link
Copy Markdown
Member

@angriman angriman left a comment

Choose a reason for hiding this comment

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

Thank you for addressing the comments, I left a few minor ones, the rest LGTM.

@fabratu
Copy link
Copy Markdown
Member

fabratu commented Jan 28, 2025

A side-comment: The error in the pipeline for the aarch64 wheel build is fixed in PR #1283 and can be ignored concerning the finish state of this PR.

@angriman
Copy link
Copy Markdown
Member

There are still unresolved comments (e.g., std::ranges::is_sorted vs std::is_sorted), did you forget to push some commits?

@Schwarf
Copy link
Copy Markdown
Contributor Author

Schwarf commented Jan 28, 2025

There are still unresolved comments (e.g., std::ranges::is_sorted vs std::is_sorted), did you forget to push some commits?

Sorry. They should all be marked as Outdated. I will go through and resolve them.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants