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

New classes DirectedHamiltonianCycle and CyclicTransitiveReduction #1008

Open
wants to merge 33 commits into
base: master
Choose a base branch
from

Conversation

kriegaex
Copy link

@kriegaex kriegaex commented Nov 29, 2020

Currently this is a code-only contribution. I have tests locally, but not JUnit tests. I would have to transform them first and make them fit for the project. I am opening this PR with full knowledge that I am not meeting all of your PR requirements, but I or somebody else can refine the PR. I think the code is valuable enough as it is, there is enough Javadoc and it solves two open issues #667 and #1001. It would be nice to get feedback from maintainers as well as users. I just want to return something to this great project, so please don't bash me for making an imperfect or incomplete contribution. Nothing is more demotivating and I do hope that an imperfect contribution is better than none.


@kriegaex kriegaex changed the title New classes DirectedHamiltonianCycle (closes #1001) and CyclicTransitiveReduction (closes #667) New classes DirectedHamiltonianCycle and CyclicTransitiveReduction Nov 29, 2020
@jsichi
Copy link
Member

jsichi commented Nov 30, 2020

Thanks for the contribution. For now, we can keep it open in PR form waiting for someone to add the unit tests and complete a review.

In particular, it will be nice to get closer to moving all classes out of the .alg package (which we've completed for everything else), probably to a new .alg.transitive package.

I expect @d-michail and @jkinable will have some comments about the literature/reference side (e.g. links to youtube videos tend to go stale).

Copy link
Member

@jsichi jsichi left a comment

Choose a reason for hiding this comment

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

A few initial comments.

@kriegaex
Copy link
Author

@jsichi, I also would like to get your opinion about something I do not like much in the Hamiltonian cycle class hierarchy: You instantiate the algorithm, then you pass in the graph into getTour(). Would it not be better to create one HC instance per graph, passing the latter into the constructor? That way several properties could be made final and initialised right from the constructor. But that would be a breaking change, so I did not do it but just implemented my DirectedHamiltonianCycle according to the existing scheme. It felt awkward though. Maybe in the future when you refactor the HC classes, you want to change that. Not being able to pass different graphs into the same HC instance would also help making concurrency easier to manage.

@kriegaex
Copy link
Author

kriegaex commented Nov 30, 2020

What I also wanted to discuss: @d-michail mentioned in #667 (comment) that he did not like the singleton pattern in class TransitiveReduction, which is why I implemented it differently in CyclicTransitiveReduction. This is somewhat inconsistent. Should this be unified? But it would result in a breaking change in the legacy class. Actually, improving existing classes was not my point here, so I have refrained from refactoring it so far. I only added the cyclicity check and also additional unit tests because I wanted to make this class aware of its own limits and also tell the user about it.

@jkinable jkinable self-assigned this Nov 30, 2020
@jkinable jkinable self-requested a review November 30, 2020 21:57
@kriegaex

This comment has been minimized.

@jsichi
Copy link
Member

jsichi commented Dec 1, 2020

@kriegaex regarding the HC invocation pattern, I understand your point. Most of our other algorithm interfaces follow the pattern you prefer (taking the graph in the constructor, not as a parameter to the execution method).

Scanning the codebase, the reason HC is the way it is appears to be its usage in TwoOptHeuristicTSP (and maybe other places). TwoOptHeuristicTSP takes an instance of the HamiltonianCycleAlgorithm interface as one of its parameters. This allows the application to decide which HC algorithm implementation TwoOptHeuristicTSP should rely on. If the HC algorithm took the graph as its constructor parameter, then a factory would need to be supplied instead.

If memory serves, we've discussed addressing this uniformly by introducing corresponding factory interfaces for all algorithms, but haven't yet got around to it, which is why the inconsistency you noticed remains.

@jsichi
Copy link
Member

jsichi commented Dec 1, 2020

Regarding the singleton pattern: you've done it correctly (don't break the old code, but start following the desired pattern in new code). However, the new class should also live in a new package alg.transitive since we're trying to move out the remaining classes which currently live directly under .alg.

For dealing with the old code, we follow a deprecation policy:

https://github.com/jgrapht/jgrapht/wiki/Dev-guide%3A-Deprecation-policy

So (as a separate PR) we would copy the old code to a new class in the .alg.transitive package, eliminating the singleton there. Then we would deprecate and eventually delete the old code entirely.

@kriegaex
Copy link
Author

kriegaex commented Dec 1, 2020

Okay, here are the unit tests + some slower tests with bigger graphs, covering the two new algorithm classes. I hope they are sufficient and adequate, please review.

Edit: One of the test cases for cyclic transitive reduction is identical to the sketch I posted in this comment.

@kriegaex
Copy link
Author

kriegaex commented Dec 2, 2020

I expect @d-michail and @jkinable will have some comments about the literature/reference side (e.g. links to youtube videos tend to go stale).

Yeah well, I thought I would better add links to my source even if it not a scientific paper but "just" a video. Basically, the algorithm for Hamiltonian cycle in a directed graph is a simple backtracking solution which probably is too trivial to have been described in any scientific paper.

I found the source code for Martello's algorithm somewhere but I do not speak Fortran and would need a lot of time to try to understand and port it. Wolfram Mathworld mentions that the Wolfram Language (which I have zero experience with) offers several options called "Backtrack" (probably similar to mine), "Heuristic", "AngluinValiant", "Martello", and "MultiPath", but I do not know which one would perform better than exhaustive search backtracking and where to obtain sample source code or pseudo code algorithm descriptions which I could easily translate into a Java algorithm.

I am not a scientist and cannot easily access academic sources. I am also not well versed in reading mathematical notation, but a pseudo code algorithm description is something I could work with. As I understand it, the worst case for all those methods would still be the same order of magnitude as backtracking even though in many (just not all) cases heuristics or other variants could shorten the runtime considerably. Feel free to correct me if I am talking complete nonsense.

Copy link
Member

@jsichi jsichi left a comment

Choose a reason for hiding this comment

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

I've done another pass; mostly minor comments. That should be it from me, but I see that @jkinable intends to do a review also, so we'll see what he adds.

*
* SPDX-License-Identifier: EPL-2.0 OR LGPL-2.1-or-later
*/
package org.jgrapht.alg;
Copy link
Member

Choose a reason for hiding this comment

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

Per my previous comment, this class should be placed in a new package org.jgrapht.alg.transitive. (Creating a new package involves adding a package-info.java in the new subdirectory, as well as adding a new export to jgrapht-core/src/main/java/module-info.java.)

Copy link
Author

Choose a reason for hiding this comment

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

I leave this internal stuff to the maintainers. I have no desire to learn all your internal rules.

Copy link
Member

Choose a reason for hiding this comment

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

Just curious: when you visit someone's house, if they ask you to hang up your coat in a particular closet, do you instead just throw it on the couch?

Copy link
Author

Choose a reason for hiding this comment

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

No, but this is not the same situation. I am not visiting your house because you won't even let me enter the room. I would be much more inclined to hang my coat where you specify if I felt more welcome. Even then, as a host you could take my coat and hang it where and how you please.

Why are we even discussing this? This kind of refactoring can be done by a maintainer in any IDE in 10 seconds. I put my algorithms in the packages where similar ones already exist. If you want to refactor, do it after accepting the contribution. I know I am repeating myself, but you are micro-managing me, trying to pull my strings like I was a puppet. The debate takes much longer than you doing what you prefer after accepting the contribution.

I have reacted open-mindedly to many of your suggestions and implemented them. But this kind of stuff really is irrelevant to the functionality, it is purely a structural attribute. Maintainers should deal with that. Contributing to a project should be easy and not a big hurdle scaring off potential contributors because they surely won't try a second time. Don't make this feel like rocket science.

Copy link
Member

Choose a reason for hiding this comment

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

Who are these maintainers of which you speak?

Copy link
Author

@kriegaex kriegaex Dec 4, 2020

Choose a reason for hiding this comment

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

Sorry for not explaining what I thought was an obvious term:

By maintainer I mean someone who has direct commit rights to this repository and/or by her code review is part of the decision if a contribution is to be accepted into the product or not. Someone with commit rights might also squash commits or otherwise modify PRs while/after accepting them. Maintainers may have other responsibilities, such as build and publish releases. By contrast, a contributor is someone like me, outside of the core team without any special rights or privileges, who contributes to the project by means of PRs, raising and commenting on issues, updating wiki pages etc.

Both maintainers and contributors can be rather inactive or regularly active, my differentiation is unrelated to the level of activity or number or frequency of contributions.

Copy link
Member

Choose a reason for hiding this comment

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

We don't have any maintainers. We have committers. Committers review code and help contributors (via comments) to get it into suitable shape for being merged. That's our process, it's well documented, and it has worked quite effectively for us--take a look back over the history of all the other contributors whose work got merged. Since that process doesn't work for you, there's no more to be said.

But if you do find a maintainer with the necessary rights, please feel free to dictate to her whatever you think best.

Copy link
Author

@kriegaex kriegaex Dec 5, 2020

Choose a reason for hiding this comment

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

We don't have any maintainers. We have committers.

You call them committers, I call them maintainers because they do more than just commit. They effectively maintain the project.

if you do find a maintainer with the necessary rights, please feel free to dictate to her whatever you think best

Who is dictating? You put me in a position to either do exactly as you dictate or refuse my contribution due to minor structural issues we disagree upon and which you could easily fix by yourself after accepting the code. That is what a maintainer does where I come from. I am sorry to learn that this kind of thing is beneath you. If you want to lead, don't tell other people to run up a hill because you say so. Take the flag and walk first if you want to teach me. Don't whip me where you think is uphill.

"And we lead not by the example of our power, but by the power of our example."
Joe Biden

* @throws IllegalArgumentException if graph allows multiple edges between two vertices
*/
public CyclicTransitiveReduction(Graph<V, E> directedGraph) {
if (directedGraph == null)
Copy link
Member

Choose a reason for hiding this comment

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

We use Objects.requireNonNull for this.

Copy link
Author

Choose a reason for hiding this comment

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

But that throws a NPE and I want an IllegalArgumentException, as is clearly documented. So now what?

Copy link
Member

Choose a reason for hiding this comment

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

Change your documentation to match the rest of our library?

Copy link
Author

Choose a reason for hiding this comment

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

Firstly, like I said several times before, I do not know the rest of the library and have no desire to do so. I am a first-time user and also a first-time contributor, trying to do the project a favour.

Secondly, if I regard a null input graph an illegal argument, I will throw an IllegalArgumentException like I saw similar ones being thrown by other graph tests like e.g. GraphTests.requireDirected. I also think I am treating users kindly by documenting unchecked exceptions in this case.

This is not a documentation issue, it is about which exception to throw in a given situation.

Copy link
Member

Choose a reason for hiding this comment

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

Yes, and accessing a null pointer should result in a NullPointerException, which is why Objects.requireNonNull is defined that way as part of the standard library.

jgrapht-core/src/test/java/org/jgrapht/TestUtil.java Outdated Show resolved Hide resolved
jgrapht-core/src/test/java/org/jgrapht/TestUtil.java Outdated Show resolved Hide resolved
graph.addEdge("B", "C");
graph.addEdge("C", "A");
TransitiveReduction.INSTANCE.reduce(graph, false);
// CAVEAT: See how dangerous it is to call reduce(Graph, boolean) without the default cyclicity check?
Copy link
Member

Choose a reason for hiding this comment

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

The warning is worthwhile, but a test verifying incorrect behavior shouldn't be included. Maybe just move the warning to testAcyclicGraphNoCheck and invite the reader to investigate what happens if A->C is reversed.

Copy link
Author

Choose a reason for hiding this comment

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

I disagree for two reasons:

  1. Tests for me do not just verify behaviour but are also a readable specification. When not forcing myself to use JUnit but in my own projects using the BDD tool Spock, test classes even inherit from a class called Specification.
  2. Verifying faulty behaviour in this case is helpful because if one day someone fixes the class under test or unifies the simple and directed transitive reduction classes into one (which I actually considered before deciding that I would leave that up to the maintainers), this test would fail and could be removed along with the comment.

Copy link
Member

Choose a reason for hiding this comment

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

  1. That's great, but it's meaningless to specify faulty behavior, unless you're working on some bug-for-bug-compatible emulator for a piece of legacy software.

  2. Why wait when you can just remove it now?

Copy link
Author

@kriegaex kriegaex Dec 3, 2020

Choose a reason for hiding this comment

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

No, it is not meaningless because I am also documenting faulty behaviour that way. The test shows a use case in which the user can unintentionally mess up, using the parameter in the wrong situation. Arguably, someone could also write that into a wiki or user manual, but an executable test is more than just words, verifying facts which, if in the future they become obsolete, would make the test fail, triggering a developer possibly other than you or me to look at it and recognise that not only the test should be adapted or removed but also the documentation updated. Automate, automate, automate! You don't need to have active knowledge if the test shoves it in your face, maybe in 3 weeks, maybe in 3 years.

Copy link
Member

Choose a reason for hiding this comment

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

Have fun with that.

Copy link
Author

Choose a reason for hiding this comment

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

This has stopped being fun quite a while ago. Anyway, thanks, I take your remark as an OK and will click "resolve" on this one.

Copy link
Member

@jsichi jsichi left a comment

Choose a reason for hiding this comment

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

Per request from the contributor, I'm marking this as in need of changes so that it doesn't get accidentally merged as is.

@kriegaex
Copy link
Author

kriegaex commented Dec 4, 2020

Per request from the contributor, I'm marking this as in need of changes so that it doesn't get accidentally merged as is.

I requested no such thing. You see a need for changes, @jsichi. I do not. In contrast to your statement, I requested multiple times that the PR be accepted as is and if someone feels a need to reformat, rename packages, change access modifiers and do other stuff of paramount importance, they can do so afterwards when the code is under their direct control and it is beyond my power to stop them.

@jgrapht jgrapht locked as too heated and limited conversation to collaborators Dec 5, 2020
@jsichi
Copy link
Member

jsichi commented Dec 5, 2020

It's clear we've reached the end of productive dialogue, so I'm locking this PR for now. If someone else comes along and wants to continue the work to bring it to completion, please create a new issue linking here, and we'll unlock the conversation at that time.

Also make sure that the simple TransitiveReduction class knows about its own
limitation and throws an exception for cyclic digraphs.
If any given digraph is itself strongly connected or contains strongly
connected components, the simple algorithm fails as documented. This was not
tested before.
Warning was: invalid usage of tag <
…sses

This is used in CyclicTransitiveReduction in order to avoid double checks in
two distinct situations where it is already clear the the checked graph is
acyclic.
…new ones

This was a code review finding, thanks @jsichi!
Immutable data object containing basic information about an edge in a graph:
the edge itself, the source vertex and the target vertex.
- <V, E> void addVertices(Graph<V, E> graph, V... vertices)
- <V, E> void addHamiltonianCycle(Graph<V, E> graph, V... vertices)
- <V, E> void randomiseGraph(Graph<V, E> graph)
- long MathUtil.naturalNumberSumGauss(int n)
- <V, E> void addEdges(Graph<V, E> graph, V... vertices)
Use 'addEdges(..)' to bulk-add edges instead of multiple 'graph.addEdge(..)'
statements.
An SCC has either a single vertex and no edges or at least 3 vertices and 3
edges for a directed graph. Before the bugfix the algorithm was faulty insofar
as it always assumed that an SCC has 3 edges and even asserted for it. When
testing with another application, I noticed, then fixed the bug and added a test
case for it.
Be more precise about synthetic edge mode. This was a code review finding.
This is more verbose when used in tests, but also makes them more readable.
Furthermore, it does no longer require the check for an even number of varargs
at the beginning of the method.
The way SCCs are now created makes it more difficult to find a Hamiltonian
cycle. I did this in order to showcase the dramatic performance gain when using
synthetic edges in cyclic transitive reduction. Typical runtimes on my
workstation are:

randomisedGraphsWithSCCsDifferentSizesNoSynthetic() -> 17,000 ms
randomisedGraphsWithSCCsDifferentSizesSynthetic()   ->    250 ms
- document new helper methods in TestUtils
- rename 'addHamiltonianCycle' to 'addCycle' as suggested in the code review
- similar to 'addCycle', 'addEdges' now automatically adds missing vertices, so
  in most cases tests do not have to call 'addVertices' first anymore
…tter

The speed increase results from reducing SCC_MAX from 20 to 18, so the typical
randomizedGraphsWithSCCsDifferentSizesNoSynthetic() now runs in about 2.5 s
instead of 17-20 s before.

Before removing intra-method timing log output for the net sum of reduce() calls
as suggested during the code review, FWIW I also captured some typical measured
runtimes in a data table inside a code comment.
Because of a rebase on the main branch after a 2-year hiatus, the tests
previously implemented for JUnit 4 had to be migrated to JUnit Jupiter.
This mostly affects assertion imports, expected exceptions and marking
slow tests.
For a directed, unweighted graph with 3+ vertices, it is necessary to be
strongly connected. But that does not mean, that every strongly
connected such graph must have a HC.

Fixes #1.
- Rename one local constant variable in a test
- Exclude IntelliJ IDEA *.xml files from scanning
Sign up for free to subscribe to this conversation on GitHub. Already have an account? Sign in.
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

None yet

3 participants