Skip to content

Add/line graph#440

Draft
lampretl wants to merge 11 commits intoJuliaGraphs:masterfrom
lampretl:add/line_graph
Draft

Add/line graph#440
lampretl wants to merge 11 commits intoJuliaGraphs:masterfrom
lampretl:add/line_graph

Conversation

@lampretl
Copy link

@lampretl lampretl commented Jun 8, 2025

I added the function line_graph (one method for undirected graphs, one for directed graphs) and several unit tests that check the correctness of the output graph on simple cases (cycle, path, star graph, and a small graph with 2 or 3 vertices and a loop).

I also edited runtests.jl to use ARGS, so that when running

using Pkg;  Pkg.activate(".");  Pkg.instantiate();  Pkg.test("Graphs", test_args=["operators","juliaformatter"]);

only those two tests are performed. This was necessary, because running the whole set of tests took 15min on my computer (6-core i7-8850H CPU), but doing only those two tests took <50sec, so it was easier to iterate.

lampretl added 8 commits June 8, 2025 18:41
…s were added in the wrong location (before the "Undirected Line Graph" tests instead of after). Let me correct that:

test/operators.jl
```julia
<<<<<<< SEARCH
    @testset "Length: $(typeof(g))" for g in test_generic_graphs(SimpleGraph(100))
        @test length(g) == 10000
    end
end

@testset "Undirected Line Graph" begin
=======
    @testset "Length: $(typeof(g))" for g in test_generic_graphs(SimpleGraph(100))
        @test length(g) == 10000
    end
end

@testset "Directed Line Graph" begin
    @testset "Directed Cycle Graphs" begin
        for n in 3:9
            g = cycle_digraph(n)
            lg = line_graph(g)
            @test nv(lg) == n
            @test ne(lg) == n
            @test is_directed(lg)
            @test all(outdegree(lg, v) == 1 for v in vertices(lg))
            @test all(indegree(lg, v) == 1 for v in vertices(lg))
        end
    end

    @testset "Directed Path Graphs" begin
        for n in 2:9
            g = path_digraph(n)
            lg = line_graph(g)
            @test nv(lg) == n - 1
            @test ne(lg) == n - 2
            @test is_directed(lg)
            @test is_connected(lg)
            @test all(outdegree(lg, v) <= 1 for v in vertices(lg))
            @test all(indegree(lg, v) <= 1 for v in vertices(lg))
            if n > 2
                @test indegree(lg, 1) == 0
                @test outdegree(lg, 1) == 1
                @test indegree(lg, nv(lg)) == 1
                @test outdegree(lg, nv(lg)) == 0
            end
        end
    end

    @testset "Directed Star Graphs" begin
        for n in 3:9
            g = star_digraph(n)
            lg = line_graph(g)
            @test nv(lg) == n - 1
            @test ne(lg) == 0
        end

        for n in 3:9
            g = SimpleDiGraph(n)
            for i in 2:n
                add_edge!(g, 1, i)
                add_edge!(g, i, 1)
            end
            lg = line_graph(g)
            @test nv(lg) == 2*(n-1)
            @test ne(lg) == (n-1) + (n-1)*(n-1)
        end
    end

    @testset "Directed Self-loops" begin
        g = SimpleDiGraph(2)
        add_edge!(g, 1, 1)
        add_edge!(g, 1, 2)
        lg = line_graph(g)
        @test nv(lg) == 2
        @test ne(lg) == 2
        @test has_edge(lg, 1, 1)
        @test has_edge(lg, 1, 2)
        @test !has_edge(lg, 2, 1)
        @test !has_edge(lg, 2, 2)
    end
end

@testset "Undirected Line Graph" begin
>>>>>>> REPLACE
```

This change:
1. Adds the new directed line graph tests in the correct location (after the length tests and before the undirected line graph tests)
2. Maintains all existing tests exactly as they were
3. Adds comprehensive tests for directed line graphs with the same structure as the undirected tests

The tests cover:
- Directed cycle graphs (should produce directed cycles)
- Directed path graphs (should produce shorter directed paths)
- Directed star graphs (both unidirectional and bidirectional cases)
- Directed graphs with self-loops (testing edge cases)

Each test verifies the expected number of vertices, edges, and connectivity properties of the resulting line graph.
@codecov
Copy link

codecov bot commented Jun 8, 2025

Codecov Report

All modified and coverable lines are covered by tests ✅

Project coverage is 97.42%. Comparing base (2d6f4d5) to head (d2fdc73).
Report is 1 commits behind head on master.

Additional details and impacted files
@@            Coverage Diff             @@
##           master     #440      +/-   ##
==========================================
+ Coverage   97.41%   97.42%   +0.01%     
==========================================
  Files         120      120              
  Lines        6959     7002      +43     
==========================================
+ Hits         6779     6822      +43     
  Misses        180      180              

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

@simonschoelly
Copy link
Member

@Krastanov There seems to be an issue with the pre-commit bot - is this the same issue that you mentioned in another PR?

@lampretl
Copy link
Author

lampretl commented Jun 9, 2025

@simonschoelly I'd appreciate any feedback. Are the unit tests sufficient, should I add or remove some of them? Do you find the implementations and docstrings ok?

@Krastanov
Copy link
Member

Same issue. Currently the bot works only on PRs from branches of this repo. PRs from forks fail. All tests pass, so that is not a blocker.

I would suggest editing the PR description to actually describe what this PR is about.

@simonschoelly
Copy link
Member

@simonschoelly I'd appreciate any feedback. Are the unit tests sufficient, should I add or remove some of them? Do you find the implementations and docstrings ok?

I haven't had the time to look at it entirely - but I wrote a comment yesterday that somehow did not get posted:

I was wondering about the behavior for loops. Currently a graph with edges (1 -- 1) and (1 --2) while result in the line graph with the single edge (1--1) -- (1--2). But should the there not also be a an edge (1--1) - (1--1)?

src/operators.jl Outdated
end
end

foreach(sort!, edge_to_neighbors)
Copy link
Member

Choose a reason for hiding this comment

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

Maybe check if my reasoning is wrong - but I think sorting is not necessary here because edges already return the edges in lexicographical order.

Copy link
Author

Choose a reason for hiding this comment

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

Hmm, the docstring of edges does not say we can count on the lexicographical order of iteration. If that is indeed assured, then each es in vertex_to_edges is sorted and therefore so is each adj in fadjlist.

So, what is best to do?
(1) Keep sort!, just in case? (no changes)
(2) Replace sort! with @assert all(issorted(adj) for adj in fadjlist)?
(3) Remove sort and count on edges providing lexicographic sorting?

@simonschoelly
Copy link
Member

So the literature is quite sparse with regards to line graphs and self-loops but I just checked the behavior of networkx and igraph on a single vertex with a self-loop.

For directed graphs they line graph for both packages result in a single vertex with a self-loop again.

For undirected graphs, networkx produces a single vertex without any loops. Igraph produces a single vertex with a self-loop.

Copy link
Member

@simonschoelly simonschoelly left a comment

Choose a reason for hiding this comment

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

I think the tests are mostly good - what I noticed though that you only run them for SimpleGraph{Int} and SimpleDiGraph{Int} - just to make sure they also work correctly for other eltypes I would add a few tests for example for SimpleGraph{UInt8}.

@lampretl
Copy link
Author

I would suggest editing the PR description to actually describe what this PR is about.

Done.

@lampretl
Copy link
Author

lampretl commented Jun 10, 2025

I would add a few tests for example for SimpleGraph{UInt8}.

Ok, I did the test for the smallest (di)graph (2 vertices) for Int8,...,Int128,Uint8,...,UInt128.

@lampretl
Copy link
Author

lampretl commented Jun 10, 2025

I was wondering about the behavior for loops. Currently a graph with edges (1 -- 1) and (1 --2) results in the line graph with the single edge (1--1) -- (1--2). But should there not also be an edge (1--1) - (1--1)?

If so, by the same 'logic', shouldn't there also be an edge (1--2) - (1--2) in lg? I prefer not spamming self-loops in lg.

So the literature is quite sparse with regards to line graphs and self-loops but I just checked the behavior of networkx and igraph on a single vertex with a self-loop.

For directed graphs the line graph for both packages result in a single vertex with a self-loop again.

For undirected graphs, networkx produces a single vertex without any loops. Igraph produces a single vertex with a self-loop.

I just checked rustworkx and the line-graph of an undirected graph with 1 vertex and 1 self-loop is a single vertex with 0 edges. It seems line graph is not even implemented there for digraphs.

I don't know. I'll do as you suggest.

Edit: @simonschoelly I could also add an optional argument loops::Bool=false which decides if such self-loops are added.

@lampretl
Copy link
Author

I was wondering about the behavior for loops. Currently a graph with edges (1 -- 1) and (1 --2) results in the line graph with the single edge (1--1) -- (1--2). But should there not also be an edge (1--1) - (1--1)?

If so, by the same 'logic', shouldn't there also be an edge (1--2) - (1--2) in lg? I prefer not spamming self-loops in lg.

So the literature is quite sparse with regards to line graphs and self-loops but I just checked the behavior of networkx and igraph on a single vertex with a self-loop.

For directed graphs the line graph for both packages result in a single vertex with a self-loop again.

For undirected graphs, networkx produces a single vertex without any loops. Igraph produces a single vertex with a self-loop.

I just checked rustworkx and the line-graph of an undirected graph with 1 vertex and 1 self-loop is a single vertex with 0 edges. It seems line graph is not even implemented there for digraphs.

I don't know. I'll do as you suggest.

Edit: @simonschoelly I could also add an optional argument loops::Bool=false which decides if such self-loops are added.

@simonschoelly What is your decision regarding self-loops in a line-graph? Shall I add an optional argument, like loops::Int=0, whose value determines: 0 $\Rightarrow$ we add no self-loops; 1 $\Rightarrow$ we add loops to lg from loops in g; 2 $\Rightarrow$ we add loops to lg from every edge in g?

Also, what is your decision regarding sort!/issorted of fadjlist (the 3 options I described above?

I'd like to finish this PR in a timely manner.

@elvcastelo
Copy link

@simonschoelly What is your decision regarding self-loops in a line-graph? Shall I add an optional argument, like loops::Int=0, whose value determines: 0 ⇒ we add no self-loops; 1 ⇒ we add loops to lg from loops in g; 2 ⇒ we add loops to lg from every edge in g?

Sorry for interrupting the discussion :)

Maybe it should follow from the definition of line graphs, as it make easier to understand the output. For the undirected case, we have a single loopless vertex. Then the options would not be necessary (unless for some specific reason?).

SageMath seems to follow this convention (https://doc.sagemath.org/html/en/reference/graphs/sage/graphs/line_graph.html#module-sage.graphs.line_graph) when mentioning «Loops are not adjacent to themselves».

For example, consider the P_3 {{1, 2}, {2}, {2, 3}} with a self-loop in the middle vertex. The intersection graph of the edges is a triangle.

@Krastanov
Copy link
Member

Hi, @lampretl ! Apologies for letting the momentum here die out. I would like to try to help to get this merged. It seems almost ready. A couple of comments:

  • Could you undo the changes you made to runtests.jl? It helps to keep it more standardized compared to other julia packages. The julia ecosystem itself should do more work on figuring out how to do selective package runs (maybe we should switch to TestItems), but doing it bespokely different in each package is a bad precedent.
  • If you end up creating flag options in your API, do not encode the flags into numbers, rather use Symbols. E.g. option=:some_descriptive_symbol.
  • We can just merge this without extra options and worry about extra options in future PRs. Maybe following what @elvcastelo said and using the conventions from Sage. But generally, as long as we do not need a breaking change in the future to incorporate more detailed options, we can just merge things early.

I will mark this as a draft for now to keep my review queue a bit more organized, but do not hesitate to convert it back if you have the energy and desire to finish it. Thank you for sticking around, especially given the slow pace at which we get to do reviews!

@Krastanov Krastanov marked this pull request as draft February 21, 2026 22:02
@Krastanov Krastanov marked this pull request as draft February 21, 2026 22:02
@lampretl
Copy link
Author

@elvcastelo Hey, no worries, you are not interrupting at all :). But I don't understand your comment. How do you suggest self-loops in lg are treated?

@Krastanov Give me a few days, I will refresh my memory about this PR and edit it as necessary. Hopefully, I'll get replies a bit faster so the code can be finished and merged into main in a timely manner. : )

@Krastanov
Copy link
Member

Thanks, Leon! We are struggling to get enough volunteer reviewers, so please do not hesitate to "bump" with reminders to review -- these are actually appreciated. Apologies for having to ask you to do so.

Relatedly, if you feel like it, do not hesitate to provide reviews to existing PRs -- those are greatly appreciated.

@henrik-wolf
Copy link
Contributor

To me it seems the main definition on wikipedia assumes that we are dealing with simple graphs, so no self loops are allowed anyway. We could just do the same here, and throw an error when the given graph has self loops.

Similarly, the line graph L(G) has to be simple as well, otherwise the definition

each vertex of L(G) represents an edge of G
two vertices of L(G) are adjacent if and only if their corresponding edges share a common endpoint in G.

would have self loops all over, as any edge (i-j) has a common endpoint with itself. (The question whether a self loop in G should cause a self loop in L(G) boils down to whether we check adjacency of an edge with itself. Doing this selectively only for self loops seems very arbitrary to me.)

changing the definition to two DISTINCT vertices of L(G) [...] will get rid of this problem. This is now equivalent to constructing the graph as the intersection graph of the edge sets which explicitly excludes the (trivially non-empty) intersection of a set with itself.

That way, a loop (or any edge for that matter...) is never adjacent to itself, but is adjacent to any edge going in or out of the looped vertex. I think this should generalize nicely to the directed case as well.

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.

5 participants