From bbb70c48911cb86bd9a0556051918bba0e8e7c26 Mon Sep 17 00:00:00 2001 From: Chunfan YAO Date: Fri, 13 Mar 2026 21:31:07 +0800 Subject: [PATCH 1/4] Add triangular lattice topology and subset generation --- examples/plot_triangular_lattices.jl | 62 +++++++++ src/GadgetSearch.jl | 4 + src/graphio/udg.jl | 181 +++++++++++++++++++++++++++ test/graphio/udg.jl | 95 ++++++++++++++ 4 files changed, 342 insertions(+) create mode 100644 examples/plot_triangular_lattices.jl diff --git a/examples/plot_triangular_lattices.jl b/examples/plot_triangular_lattices.jl new file mode 100644 index 0000000..4d5affd --- /dev/null +++ b/examples/plot_triangular_lattices.jl @@ -0,0 +1,62 @@ +using GadgetSearch + +const EXAMPLES_DIR = pkgdir(GadgetSearch, "examples") + +triangular_positions(nx::Int, ny::Int) = + GadgetSearch.get_physical_positions(Triangular(), vec(Tuple{Int, Int}[(i, j) for i in 1:nx, j in 1:ny])) + +function plot_lattice_examples() + full_graph = triangular_lattice_graph(4, 4) + full_positions = triangular_positions(4, 4) + full_path = joinpath(EXAMPLES_DIR, "triangular_lattice_4x4.svg") + GadgetSearch.plot_graph(full_graph, full_path; pos=full_positions, plot_size=700, vertex_size=8, vertex_label_size=12) + + small_graph = triangular_lattice_graph(3, 3) + small_positions = triangular_positions(3, 3) + small_path = joinpath(EXAMPLES_DIR, "triangular_lattice_3x3.svg") + GadgetSearch.plot_graph(small_graph, small_path; pos=small_positions, plot_size=600, vertex_size=10, vertex_label_size=14) + + return (full_path, small_path) +end + +function ensure_subset_dataset() + dataset_path = joinpath(EXAMPLES_DIR, "triangular_subset_dataset.g6") + if !isfile(dataset_path) + generate_triangular_udg_subsets(3, 3; subset_sizes=2:3, deduplicate=true, path=dataset_path) + end + return dataset_path +end + +function plot_subset_examples(dataset_path::String; nplots::Int=3) + loader = GraphLoader(dataset_path) + plot_count = min(nplots, length(loader)) + saved_paths = String[] + + for idx in 1:plot_count + graph = loader[idx] + positions = loader.layout[idx] + positions === nothing && continue + + outpath = joinpath(EXAMPLES_DIR, "triangular_subset_$(idx).svg") + GadgetSearch.plot_graph(graph, outpath; pos=positions, plot_size=500, vertex_size=12, vertex_label_size=16) + push!(saved_paths, outpath) + end + + return saved_paths +end + +function main() + lattice_paths = plot_lattice_examples() + dataset_path = ensure_subset_dataset() + subset_paths = plot_subset_examples(dataset_path) + + println("Triangular lattice plots:") + foreach(println, lattice_paths) + println("Subset dataset:") + println(dataset_path) + println("Subset plots:") + foreach(println, subset_paths) +end + +main() + diff --git a/src/GadgetSearch.jl b/src/GadgetSearch.jl index ae29e34..5657189 100644 --- a/src/GadgetSearch.jl +++ b/src/GadgetSearch.jl @@ -37,8 +37,12 @@ export save_cache export save_graph export Square, Triangular +export triangular_adjacency +export triangular_lattice_graph +export dedup_inner_subsets export generate_full_grid_udg export generate_full_grid_graph +export generate_triangular_udg_subsets # Core types export Gadget diff --git a/src/graphio/udg.jl b/src/graphio/udg.jl index 03c81f3..950ea7a 100644 --- a/src/graphio/udg.jl +++ b/src/graphio/udg.jl @@ -26,9 +26,138 @@ abstract type LatticeType end struct Square <: LatticeType end struct Triangular <: LatticeType end +""" + triangular_adjacency(i1::Int, j1::Int, i2::Int, j2::Int) -> Bool + +Return whether two sites of the triangular lattice are nearest neighbors using +only integer lattice coordinates. + +# Arguments +- `i1::Int`: Column index of the first site. +- `j1::Int`: Row index of the first site. +- `i2::Int`: Column index of the second site. +- `j2::Int`: Row index of the second site. + +# Returns +- `Bool`: `true` if the two lattice sites share an edge in the triangular lattice. +""" +function triangular_adjacency(i1::Int, j1::Int, i2::Int, j2::Int) + j1 == j2 && return abs(i1 - i2) == 1 + abs(j1 - j2) == 1 || return false + + if j1 > j2 + return triangular_adjacency(i2, j2, i1, j1) + end + + return isodd(j1) ? (i2 == i1 || i2 == i1 + 1) : (i2 == i1 || i2 == i1 - 1) +end + +""" + triangular_lattice_graph(nx::Int, ny::Int) -> SimpleGraph{Int} + +Build the full nearest-neighbor triangular lattice graph on an `nx × ny` grid +of lattice sites. + +# Arguments +- `nx::Int`: Number of lattice columns. +- `ny::Int`: Number of lattice rows. + +# Returns +- `SimpleGraph{Int}`: The graph whose vertices are lattice sites and whose + edges connect nearest neighbors. +""" +function triangular_lattice_graph(nx::Int, ny::Int) + if nx <= 0 || ny <= 0 + return SimpleGraph(0) + end + + vertex_index(i, j) = i + (j - 1) * nx + g = SimpleGraph(nx * ny) + + for j = 1:ny, i = 1:nx + v = vertex_index(i, j) + + if i < nx + add_edge!(g, v, vertex_index(i + 1, j)) + end + + if j < ny + add_edge!(g, v, vertex_index(i, j + 1)) + + if isodd(j) && i < nx + add_edge!(g, v, vertex_index(i + 1, j + 1)) + elseif iseven(j) && i > 1 + add_edge!(g, v, vertex_index(i - 1, j + 1)) + end + end + end + + return g +end + +""" + get_radius(lattice::LatticeType) -> Float64 + +Return the unit-disk radius used for the chosen lattice geometry. + +# Arguments +- `lattice::LatticeType`: Lattice family whose physical spacing determines the + unit-disk threshold. + +# Returns +- `Float64`: The interaction radius used when constructing unit-disk graphs. +""" get_radius(::Square) = 1.5 get_radius(::Triangular) = 1.1 +_triangular_grid_coordinates(nx::Int, ny::Int) = vec(Tuple{Int, Int}[(i, j) for i in 1:nx, j in 1:ny]) + +""" + dedup_inner_subsets(inner_grid::SimpleGraph{Int}, k::Integer; use_shortg::Bool=true) -> Vector{Vector{Int}} + +Enumerate all `k`-vertex subsets of `inner_grid` and, when possible, retain +one representative from each isomorphism class using `shortg`. + +# Arguments +- `inner_grid::SimpleGraph{Int}`: The lattice graph whose vertex subsets are enumerated. +- `k::Integer`: Size of each subset to generate. +- `use_shortg::Bool=true`: Whether to use `shortg` for isomorphism-based deduplication + when the executable is available in `PATH`. + +# Returns +- `Vector{Vector{Int}}`: A collection of vertex subsets, each represented by the + original vertex indices from `inner_grid`. +""" +function dedup_inner_subsets(inner_grid::SimpleGraph{Int}, k::Integer; use_shortg::Bool=true) + 0 <= k <= nv(inner_grid) || throw(ArgumentError("subset size k must satisfy 0 <= k <= nv(inner_grid)")) + + subsets = [collect(subset) for subset in Combinatorics.combinations(vertices(inner_grid), k)] + if isempty(subsets) || !use_shortg || Sys.which("shortg") === nothing + return subsets + end + + subgraphs = SimpleGraph{Int}[] + for subset in subsets + subgraph, _ = Graphs.induced_subgraph(inner_grid, subset) + push!(subgraphs, subgraph) + end + + mapping_file = tempname() + temp_path = tempname() + + try + save_graph(subgraphs, temp_path) + ok = _call_shortg(temp_path, mapping_file) + ok || return subsets + + canonical_to_original, _ = _parse_shortg_mapping(mapping_file) + return [subsets[canonical_to_original[idx][1]] for idx in sort!(collect(keys(canonical_to_original)))] + finally + isfile(mapping_file) && rm(mapping_file) + isfile(temp_path) && rm(temp_path) + end +end + get_physical_positions(::Square, pos::Vector{Tuple{Int, Int}}) = Vector{Tuple{Float64, Float64}}(pos) function get_physical_positions(::Triangular, pos::Vector{Tuple{Int, Int}}) h = sqrt(3) / 2 @@ -143,6 +272,58 @@ function generate_full_grid_udg(lattice::LatticeType, nx::Int, ny::Int; path::St return _process_and_save_graphs(results, path) end +""" + generate_triangular_udg_subsets(nx::Int, ny::Int; + subset_sizes=0:(nx * ny), + deduplicate::Bool=true, + use_shortg::Bool=true, + path::String="triangular_udg_subsets.g6") -> String + +Generate all triangular-lattice unit-disk graphs obtained by selecting subsets +of the `nx × ny` inner lattice sites and save them to disk. + +# Arguments +- `nx::Int`: Number of lattice columns in the inner triangular grid. +- `ny::Int`: Number of lattice rows in the inner triangular grid. +- `subset_sizes=0:(nx * ny)`: Iterable of subset sizes to enumerate. +- `deduplicate::Bool=true`: Whether to collapse isomorphic inner subsets before saving. +- `use_shortg::Bool=true`: Whether `shortg` may be used for deduplication when available. +- `path::String="triangular_udg_subsets.g6"`: Output file path. + +# Returns +- `String`: The path where the generated dataset was written. +""" +function generate_triangular_udg_subsets( + nx::Int, + ny::Int; + subset_sizes=0:(nx * ny), + deduplicate::Bool=true, + use_shortg::Bool=true, + path::String="triangular_udg_subsets.g6", +) + inner_grid = triangular_lattice_graph(nx, ny) + grid_coords = _triangular_grid_coordinates(nx, ny) + physical_positions = get_physical_positions(Triangular(), grid_coords) + results = Tuple{SimpleGraph{Int}, Vector{Tuple{Float64, Float64}}}[] + + for k in subset_sizes + 0 <= k <= nv(inner_grid) || throw(ArgumentError("subset sizes must satisfy 0 <= k <= nx * ny")) + + subsets = deduplicate ? + dedup_inner_subsets(inner_grid, k; use_shortg=use_shortg) : + [collect(subset) for subset in Combinatorics.combinations(vertices(inner_grid), k)] + + for subset in subsets + subset_positions = physical_positions[subset] + subset_graph = unit_disk_graph(subset_positions, get_radius(Triangular())) + push!(results, (subset_graph, subset_positions)) + end + end + + save_graph(results, path) + return path +end + function _call_shortg(temp_path::String, mapping_file::String) if Sys.which("shortg") === nothing # Make shortg optional: log and signal caller to fallback diff --git a/test/graphio/udg.jl b/test/graphio/udg.jl index 8bf623b..7378db8 100644 --- a/test/graphio/udg.jl +++ b/test/graphio/udg.jl @@ -109,6 +109,101 @@ end end end +@testset "Triangular Lattice Topology" begin + @testset "triangular_adjacency Function" begin + adjacency = GadgetSearch.triangular_adjacency + + known_neighbors = [ + ((1, 1), (2, 1)), + ((2, 2), (1, 2)), + ((2, 2), (2, 3)), + ((2, 2), (1, 3)), + ((2, 3), (2, 2)), + ((2, 3), (3, 2)), + ] + non_neighbors = [ + ((1, 1), (1, 1)), + ((1, 1), (3, 1)), + ((2, 2), (3, 3)), + ((2, 2), (2, 4)), + ] + + for ((i1, j1), (i2, j2)) in known_neighbors + @test adjacency(i1, j1, i2, j2) + @test adjacency(i2, j2, i1, j1) + end + + for ((i1, j1), (i2, j2)) in non_neighbors + @test !adjacency(i1, j1, i2, j2) + @test !adjacency(i2, j2, i1, j1) + end + end + + @testset "triangular_lattice_graph Function" begin + g11 = GadgetSearch.triangular_lattice_graph(1, 1) + @test nv(g11) == 1 + @test ne(g11) == 0 + + g22 = GadgetSearch.triangular_lattice_graph(2, 2) + @test nv(g22) == 4 + @test ne(g22) == 5 + @test has_edge(g22, 1, 2) + @test has_edge(g22, 1, 3) + @test has_edge(g22, 1, 4) + @test has_edge(g22, 2, 4) + @test has_edge(g22, 3, 4) + @test !has_edge(g22, 2, 3) + + coords = vec(Tuple{Int, Int}[(x, y) for x in 1:3, y in 1:2]) + physical = GadgetSearch.get_physical_positions(Triangular(), coords) + udg = GadgetSearch.unit_disk_graph(physical, get_radius(Triangular())) + g32 = GadgetSearch.triangular_lattice_graph(3, 2) + @test nv(g32) == nv(udg) + @test ne(g32) == ne(udg) + @test all(has_edge(udg, src(edge), dst(edge)) for edge in edges(g32)) + @test all(has_edge(g32, src(edge), dst(edge)) for edge in edges(udg)) + + gempty = GadgetSearch.triangular_lattice_graph(0, 3) + @test nv(gempty) == 0 + @test ne(gempty) == 0 + end +end + +@testset "Triangular UDG Subsets" begin + @testset "dedup_inner_subsets Function" begin + inner_grid = GadgetSearch.triangular_lattice_graph(2, 2) + + subsets_without_shortg = GadgetSearch.dedup_inner_subsets(inner_grid, 2; use_shortg=false) + @test length(subsets_without_shortg) == 6 + @test all(length(subset) == 2 for subset in subsets_without_shortg) + + subsets_with_shortg = GadgetSearch.dedup_inner_subsets(inner_grid, 2; use_shortg=true) + if Sys.which("shortg") === nothing + @test subsets_with_shortg == subsets_without_shortg + else + @test length(subsets_with_shortg) == 2 + induced_edges = sort(unique(ne(Graphs.induced_subgraph(inner_grid, subset)[1]) for subset in subsets_with_shortg)) + @test induced_edges == [0, 1] + end + end + + @testset "generate_triangular_udg_subsets Integration" begin + temp_file = tempname() * ".g6" + result = generate_triangular_udg_subsets(2, 1; subset_sizes=1:2, deduplicate=false, path=temp_file) + + @test result == temp_file + dataset = GraphDataset(temp_file) + @test dataset.n == 3 + @test map(length, dataset.layouts) == [1, 1, 2] + + loader = GraphLoader(temp_file) + edge_counts = sort([ne(loader[idx]) for idx in 1:length(loader)]) + @test edge_counts == [0, 0, 1] + + rm(temp_file) + end +end + @testset "get_pin_positions Function" begin @testset "Basic Pin Positioning" begin square = Square() From 68845aba94569048d0e29b3fda2b10c028760bf0 Mon Sep 17 00:00:00 2001 From: Chunfan YAO Date: Wed, 18 Mar 2026 20:03:02 +0800 Subject: [PATCH 2/4] refactor: align triangular UDG pins with canonical crossing roles Generate pr2 lattice boundary pins in left-top-right-bottom order so triangular UDG datasets match the fixed boundary-role semantics used by the crossing search. --- src/GadgetSearch.jl | 1 + src/graphio/udg.jl | 19 +++++++++++++++++-- test/graphio/udg.jl | 21 +++++++++++++++++++++ 3 files changed, 39 insertions(+), 2 deletions(-) diff --git a/src/GadgetSearch.jl b/src/GadgetSearch.jl index 4b2113e..6915d42 100644 --- a/src/GadgetSearch.jl +++ b/src/GadgetSearch.jl @@ -40,6 +40,7 @@ export save_graph export Square, Triangular export triangular_adjacency export triangular_lattice_graph +export canonical_crossing_pins export dedup_inner_subsets export generate_full_grid_udg export generate_full_grid_graph diff --git a/src/graphio/udg.jl b/src/graphio/udg.jl index 950ea7a..be1cb97 100644 --- a/src/graphio/udg.jl +++ b/src/graphio/udg.jl @@ -177,6 +177,21 @@ function get_inner_points(lattice::LatticeType, nx::Int, ny::Int) return get_physical_positions(lattice, vec(original_points)) end +""" + canonical_crossing_pins(top, bottom, left, right) + +Return the four boundary pin positions in the canonical crossing order used by +the unweighted crossing search: + +- pin `1`: left +- pin `2`: top +- pin `3`: right +- pin `4`: bottom +""" +function canonical_crossing_pins(top, bottom, left, right) + return [left, top, right, bottom] +end + """ complete_graph(n::Int) -> SimpleGraph @@ -262,13 +277,13 @@ function generate_full_grid_udg(lattice::LatticeType, nx::Int, ny::Int; path::St for top in top_candidates, bottom in bottom_candidates, left in left_candidates, right in right_candidates - selected = vcat([top, right, bottom, left], inner_points) + selected = vcat(canonical_crossing_pins(top, bottom, left, right), inner_points) g = unit_disk_graph(selected, radius) push!(results, (g, selected)) end - @info "pinset in generated graphs: [1,2,3,4]" + @info "pinset in generated graphs follows canonical crossing roles: [left, top, right, bottom]" return _process_and_save_graphs(results, path) end diff --git a/test/graphio/udg.jl b/test/graphio/udg.jl index 7378db8..fbda932 100644 --- a/test/graphio/udg.jl +++ b/test/graphio/udg.jl @@ -248,6 +248,16 @@ end end end +@testset "canonical_crossing_pins Function" begin + top = (10.0, 1.0) + bottom = (10.0, 9.0) + left = (1.0, 5.0) + right = (19.0, 5.0) + + pins = GadgetSearch.canonical_crossing_pins(top, bottom, left, right) + @test pins == [left, top, right, bottom] +end + @testset "get_inner_points Function" begin @testset "Basic Inner Points" begin square = Square() @@ -307,6 +317,17 @@ end rm(temp_file) end end + + @testset "Canonical crossing pin order" begin + square = Square() + top, bottom, left, right = GadgetSearch.get_pin_positions(square, 1, 1) + pins = GadgetSearch.canonical_crossing_pins(top[1], bottom[1], left[1], right[1]) + + @test pins[1] == left[1] + @test pins[2] == top[1] + @test pins[3] == right[1] + @test pins[4] == bottom[1] + end @testset "Parameter Validation" begin square = Square() From d6fc5cdc960979a2cc34af08a86cfb8d209200eb Mon Sep 17 00:00:00 2001 From: Chunfan YAO Date: Wed, 18 Mar 2026 20:23:28 +0800 Subject: [PATCH 3/4] test: improve triangular lattice diff coverage Exercise the triangular plotting example and the new triangular UDG validation branches so the PR covers more of the lattice-topology diff on CI. --- examples/plot_triangular_lattices.jl | 29 ++++++++------ test/graphio/udg.jl | 56 ++++++++++++++++++++++++++++ 2 files changed, 73 insertions(+), 12 deletions(-) diff --git a/examples/plot_triangular_lattices.jl b/examples/plot_triangular_lattices.jl index 4d5affd..5383bdc 100644 --- a/examples/plot_triangular_lattices.jl +++ b/examples/plot_triangular_lattices.jl @@ -5,29 +5,32 @@ const EXAMPLES_DIR = pkgdir(GadgetSearch, "examples") triangular_positions(nx::Int, ny::Int) = GadgetSearch.get_physical_positions(Triangular(), vec(Tuple{Int, Int}[(i, j) for i in 1:nx, j in 1:ny])) -function plot_lattice_examples() +function plot_lattice_examples(outdir::AbstractString=EXAMPLES_DIR) + mkpath(outdir) full_graph = triangular_lattice_graph(4, 4) full_positions = triangular_positions(4, 4) - full_path = joinpath(EXAMPLES_DIR, "triangular_lattice_4x4.svg") + full_path = joinpath(outdir, "triangular_lattice_4x4.svg") GadgetSearch.plot_graph(full_graph, full_path; pos=full_positions, plot_size=700, vertex_size=8, vertex_label_size=12) small_graph = triangular_lattice_graph(3, 3) small_positions = triangular_positions(3, 3) - small_path = joinpath(EXAMPLES_DIR, "triangular_lattice_3x3.svg") + small_path = joinpath(outdir, "triangular_lattice_3x3.svg") GadgetSearch.plot_graph(small_graph, small_path; pos=small_positions, plot_size=600, vertex_size=10, vertex_label_size=14) return (full_path, small_path) end -function ensure_subset_dataset() - dataset_path = joinpath(EXAMPLES_DIR, "triangular_subset_dataset.g6") +function ensure_subset_dataset(outdir::AbstractString=EXAMPLES_DIR) + mkpath(outdir) + dataset_path = joinpath(outdir, "triangular_subset_dataset.g6") if !isfile(dataset_path) generate_triangular_udg_subsets(3, 3; subset_sizes=2:3, deduplicate=true, path=dataset_path) end return dataset_path end -function plot_subset_examples(dataset_path::String; nplots::Int=3) +function plot_subset_examples(dataset_path::String; outdir::AbstractString=EXAMPLES_DIR, nplots::Int=3) + mkpath(outdir) loader = GraphLoader(dataset_path) plot_count = min(nplots, length(loader)) saved_paths = String[] @@ -37,7 +40,7 @@ function plot_subset_examples(dataset_path::String; nplots::Int=3) positions = loader.layout[idx] positions === nothing && continue - outpath = joinpath(EXAMPLES_DIR, "triangular_subset_$(idx).svg") + outpath = joinpath(outdir, "triangular_subset_$(idx).svg") GadgetSearch.plot_graph(graph, outpath; pos=positions, plot_size=500, vertex_size=12, vertex_label_size=16) push!(saved_paths, outpath) end @@ -45,10 +48,10 @@ function plot_subset_examples(dataset_path::String; nplots::Int=3) return saved_paths end -function main() - lattice_paths = plot_lattice_examples() - dataset_path = ensure_subset_dataset() - subset_paths = plot_subset_examples(dataset_path) +function main(; outdir::AbstractString=EXAMPLES_DIR, nplots::Int=3) + lattice_paths = plot_lattice_examples(outdir) + dataset_path = ensure_subset_dataset(outdir) + subset_paths = plot_subset_examples(dataset_path; outdir=outdir, nplots=nplots) println("Triangular lattice plots:") foreach(println, lattice_paths) @@ -58,5 +61,7 @@ function main() foreach(println, subset_paths) end -main() +if abspath(PROGRAM_FILE) == @__FILE__ + main() +end diff --git a/test/graphio/udg.jl b/test/graphio/udg.jl index fbda932..d4ab322 100644 --- a/test/graphio/udg.jl +++ b/test/graphio/udg.jl @@ -2,6 +2,8 @@ using Test using GadgetSearch using Graphs +include(joinpath(pkgdir(GadgetSearch), "examples", "plot_triangular_lattices.jl")) + @testset "UDG Basic Types and Radius" begin @testset "Lattice Type Definitions" begin # Test that lattice types are concrete types @@ -187,6 +189,12 @@ end end end + @testset "dedup_inner_subsets validation" begin + inner_grid = GadgetSearch.triangular_lattice_graph(2, 2) + @test_throws ArgumentError GadgetSearch.dedup_inner_subsets(inner_grid, -1; use_shortg=false) + @test_throws ArgumentError GadgetSearch.dedup_inner_subsets(inner_grid, 5; use_shortg=false) + end + @testset "generate_triangular_udg_subsets Integration" begin temp_file = tempname() * ".g6" result = generate_triangular_udg_subsets(2, 1; subset_sizes=1:2, deduplicate=false, path=temp_file) @@ -202,6 +210,54 @@ end rm(temp_file) end + + @testset "generate_triangular_udg_subsets zero subset and validation" begin + temp_file = tempname() * ".g6" + try + result = generate_triangular_udg_subsets( + 2, + 1; + subset_sizes=0:1, + deduplicate=true, + use_shortg=false, + path=temp_file, + ) + + @test result == temp_file + dataset = GraphDataset(temp_file) + @test dataset.n == 3 + @test dataset.layouts[1] === nothing + @test map(length, dataset.layouts[2:3]) == [1, 1] + finally + isfile(temp_file) && rm(temp_file) + end + + @test_throws ArgumentError generate_triangular_udg_subsets( + 2, + 1; + subset_sizes=[-1], + deduplicate=false, + path=tempname() * ".g6", + ) + end +end + +@testset "Triangular lattice example smoke test" begin + mktempdir() do tmpdir + lattice_paths = plot_lattice_examples(tmpdir) + @test all(isfile, lattice_paths) + @test all(path -> filesize(path) > 0, lattice_paths) + + dataset_path = ensure_subset_dataset(tmpdir) + @test isfile(dataset_path) + @test filesize(dataset_path) > 0 + + subset_paths = plot_subset_examples(dataset_path; outdir=tmpdir, nplots=2) + @test length(subset_paths) == 2 + @test all(isfile, subset_paths) + + @test_nowarn main(; outdir=tmpdir, nplots=2) + end end @testset "get_pin_positions Function" begin From 9af2dd60bec36cf4352b78b112f5e7febed8570b Mon Sep 17 00:00:00 2001 From: Chunfan YAO Date: Mon, 23 Mar 2026 12:50:09 +0800 Subject: [PATCH 4/4] refactor: make subset dedup strategy explicit and testable Move dedup strategy decisions to the entry point, require shortg explicitly when requested, and add g6 fallback plus strict-dedup behavior tests to prevent silent degradation. Made-with: Cursor --- src/graphio/udg.jl | 71 ++++++++++++++++++++++++++---------------- test/graphio/udg.jl | 75 ++++++++++++++++++++++++++++++++++++++++++--- 2 files changed, 115 insertions(+), 31 deletions(-) diff --git a/src/graphio/udg.jl b/src/graphio/udg.jl index be1cb97..de8b987 100644 --- a/src/graphio/udg.jl +++ b/src/graphio/udg.jl @@ -112,29 +112,43 @@ get_radius(::Triangular) = 1.1 _triangular_grid_coordinates(nx::Int, ny::Int) = vec(Tuple{Int, Int}[(i, j) for i in 1:nx, j in 1:ny]) +function _all_inner_subsets(inner_grid::SimpleGraph{Int}, k::Integer) + return [collect(subset) for subset in Combinatorics.combinations(vertices(inner_grid), k)] +end + +function _dedup_subsets_by_g6(inner_grid::SimpleGraph{Int}, subsets::Vector{Vector{Int}}) + seen = Set{String}() + deduped = Vector{Vector{Int}}() + for subset in subsets + subgraph, _ = Graphs.induced_subgraph(inner_grid, subset) + g6 = GraphIO.Graph6._graphToG6String(subgraph)[11:end] + if !(g6 in seen) + push!(seen, g6) + push!(deduped, subset) + end + end + return deduped +end + """ dedup_inner_subsets(inner_grid::SimpleGraph{Int}, k::Integer; use_shortg::Bool=true) -> Vector{Vector{Int}} -Enumerate all `k`-vertex subsets of `inner_grid` and, when possible, retain -one representative from each isomorphism class using `shortg`. +Enumerate all `k`-vertex subsets of `inner_grid` and retain one representative +per class. -# Arguments -- `inner_grid::SimpleGraph{Int}`: The lattice graph whose vertex subsets are enumerated. -- `k::Integer`: Size of each subset to generate. -- `use_shortg::Bool=true`: Whether to use `shortg` for isomorphism-based deduplication - when the executable is available in `PATH`. - -# Returns -- `Vector{Vector{Int}}`: A collection of vertex subsets, each represented by the - original vertex indices from `inner_grid`. +When `use_shortg=true`, deduplication is by graph isomorphism via `shortg`. +When `use_shortg=false`, deduplication uses g6-string equality (same labeled +graph encoding). """ function dedup_inner_subsets(inner_grid::SimpleGraph{Int}, k::Integer; use_shortg::Bool=true) 0 <= k <= nv(inner_grid) || throw(ArgumentError("subset size k must satisfy 0 <= k <= nv(inner_grid)")) - subsets = [collect(subset) for subset in Combinatorics.combinations(vertices(inner_grid), k)] - if isempty(subsets) || !use_shortg || Sys.which("shortg") === nothing - return subsets + subsets = _all_inner_subsets(inner_grid, k) + isempty(subsets) && return subsets + if !use_shortg + return _dedup_subsets_by_g6(inner_grid, subsets) end + Sys.which("shortg") === nothing && throw(ArgumentError("use_shortg=true requires `shortg` in PATH")) subgraphs = SimpleGraph{Int}[] for subset in subsets @@ -147,8 +161,7 @@ function dedup_inner_subsets(inner_grid::SimpleGraph{Int}, k::Integer; use_short try save_graph(subgraphs, temp_path) - ok = _call_shortg(temp_path, mapping_file) - ok || return subsets + _call_shortg(temp_path, mapping_file) canonical_to_original, _ = _parse_shortg_mapping(mapping_file) return [subsets[canonical_to_original[idx][1]] for idx in sort!(collect(keys(canonical_to_original)))] @@ -292,6 +305,7 @@ end subset_sizes=0:(nx * ny), deduplicate::Bool=true, use_shortg::Bool=true, + strict_dedup::Bool=false, path::String="triangular_udg_subsets.g6") -> String Generate all triangular-lattice unit-disk graphs obtained by selecting subsets @@ -303,6 +317,7 @@ of the `nx × ny` inner lattice sites and save them to disk. - `subset_sizes=0:(nx * ny)`: Iterable of subset sizes to enumerate. - `deduplicate::Bool=true`: Whether to collapse isomorphic inner subsets before saving. - `use_shortg::Bool=true`: Whether `shortg` may be used for deduplication when available. +- `strict_dedup::Bool=false`: When `true`, require `shortg` and throw if unavailable. - `path::String="triangular_udg_subsets.g6"`: Output file path. # Returns @@ -314,19 +329,30 @@ function generate_triangular_udg_subsets( subset_sizes=0:(nx * ny), deduplicate::Bool=true, use_shortg::Bool=true, + strict_dedup::Bool=false, path::String="triangular_udg_subsets.g6", ) inner_grid = triangular_lattice_graph(nx, ny) grid_coords = _triangular_grid_coordinates(nx, ny) physical_positions = get_physical_positions(Triangular(), grid_coords) results = Tuple{SimpleGraph{Int}, Vector{Tuple{Float64, Float64}}}[] + shortg_available = Sys.which("shortg") !== nothing + do_shortg_dedup = deduplicate && use_shortg && shortg_available + + if strict_dedup && deduplicate && !do_shortg_dedup + throw(ArgumentError("strict_dedup=true requires use_shortg=true and `shortg` available in PATH")) + end + + if deduplicate && use_shortg && !shortg_available + @warn "`shortg` not found; falling back to g6-string deduplication." + end for k in subset_sizes 0 <= k <= nv(inner_grid) || throw(ArgumentError("subset sizes must satisfy 0 <= k <= nx * ny")) subsets = deduplicate ? - dedup_inner_subsets(inner_grid, k; use_shortg=use_shortg) : - [collect(subset) for subset in Combinatorics.combinations(vertices(inner_grid), k)] + dedup_inner_subsets(inner_grid, k; use_shortg=do_shortg_dedup) : + _all_inner_subsets(inner_grid, k) for subset in subsets subset_positions = physical_positions[subset] @@ -340,15 +366,8 @@ function generate_triangular_udg_subsets( end function _call_shortg(temp_path::String, mapping_file::String) - if Sys.which("shortg") === nothing - # Make shortg optional: log and signal caller to fallback - @warn "Optional tool `shortg` not found; skipping canonicalization/dedup." - return false - else - @info "shortg found in PATH; running canonicalization" - end + @info "shortg found in PATH; running canonicalization" run(pipeline(`shortg -v -u $(temp_path)`, stderr=mapping_file)) - return true end function _process_and_save_graphs(results::Vector{Tuple{SimpleGraph{T}, Vector{Tuple{Float64, Float64}}}}, path::String) where T diff --git a/test/graphio/udg.jl b/test/graphio/udg.jl index d4ab322..dc61cd4 100644 --- a/test/graphio/udg.jl +++ b/test/graphio/udg.jl @@ -176,13 +176,15 @@ end inner_grid = GadgetSearch.triangular_lattice_graph(2, 2) subsets_without_shortg = GadgetSearch.dedup_inner_subsets(inner_grid, 2; use_shortg=false) - @test length(subsets_without_shortg) == 6 + @test length(subsets_without_shortg) == 2 @test all(length(subset) == 2 for subset in subsets_without_shortg) + induced_edges_no_shortg = sort(unique(ne(Graphs.induced_subgraph(inner_grid, subset)[1]) for subset in subsets_without_shortg)) + @test induced_edges_no_shortg == [0, 1] - subsets_with_shortg = GadgetSearch.dedup_inner_subsets(inner_grid, 2; use_shortg=true) if Sys.which("shortg") === nothing - @test subsets_with_shortg == subsets_without_shortg + @test_throws ArgumentError GadgetSearch.dedup_inner_subsets(inner_grid, 2; use_shortg=true) else + subsets_with_shortg = GadgetSearch.dedup_inner_subsets(inner_grid, 2; use_shortg=true) @test length(subsets_with_shortg) == 2 induced_edges = sort(unique(ne(Graphs.induced_subgraph(inner_grid, subset)[1]) for subset in subsets_with_shortg)) @test induced_edges == [0, 1] @@ -193,6 +195,9 @@ end inner_grid = GadgetSearch.triangular_lattice_graph(2, 2) @test_throws ArgumentError GadgetSearch.dedup_inner_subsets(inner_grid, -1; use_shortg=false) @test_throws ArgumentError GadgetSearch.dedup_inner_subsets(inner_grid, 5; use_shortg=false) + if Sys.which("shortg") === nothing + @test_throws ArgumentError GadgetSearch.dedup_inner_subsets(inner_grid, 2; use_shortg=true) + end end @testset "generate_triangular_udg_subsets Integration" begin @@ -225,9 +230,9 @@ end @test result == temp_file dataset = GraphDataset(temp_file) - @test dataset.n == 3 + @test dataset.n == 2 @test dataset.layouts[1] === nothing - @test map(length, dataset.layouts[2:3]) == [1, 1] + @test map(length, dataset.layouts[2:2]) == [1] finally isfile(temp_file) && rm(temp_file) end @@ -239,6 +244,66 @@ end deduplicate=false, path=tempname() * ".g6", ) + + if Sys.which("shortg") === nothing + @test_throws ArgumentError generate_triangular_udg_subsets( + 2, + 1; + subset_sizes=1:2, + deduplicate=true, + use_shortg=true, + strict_dedup=true, + path=tempname() * ".g6", + ) + end + + @test_throws ArgumentError generate_triangular_udg_subsets( + 2, + 1; + subset_sizes=1:2, + deduplicate=true, + use_shortg=false, + strict_dedup=true, + path=tempname() * ".g6", + ) + end + + @testset "generate_triangular_udg_subsets dedup strategy behavior" begin + raw_file = tempname() * ".g6" + dedup_file = tempname() * ".g6" + try + generate_triangular_udg_subsets( + 2, + 2; + subset_sizes=[2], + deduplicate=false, + use_shortg=false, + path=raw_file, + ) + generate_triangular_udg_subsets( + 2, + 2; + subset_sizes=[2], + deduplicate=true, + use_shortg=false, + path=dedup_file, + ) + + raw_dataset = GraphDataset(raw_file) + dedup_dataset = GraphDataset(dedup_file) + + @test raw_dataset.n == 6 + @test dedup_dataset.n == 2 + @test dedup_dataset.n <= raw_dataset.n + @test all(layout -> layout !== nothing && length(layout) == 2, dedup_dataset.layouts) + + dedup_loader = GraphLoader(dedup_file) + dedup_edge_counts = sort([ne(dedup_loader[idx]) for idx in 1:length(dedup_loader)]) + @test dedup_edge_counts == [0, 1] + finally + isfile(raw_file) && rm(raw_file) + isfile(dedup_file) && rm(dedup_file) + end end end