From 3b01541cb8ed98447c9d99d88f97ce2761d2ca7a Mon Sep 17 00:00:00 2001 From: nhz2 Date: Wed, 4 Nov 2026 02:49:29 -0500 Subject: [PATCH] Pretty print containers Assisted-by: Claude Code --- .gitignore | 4 +- src/SimplexCellLists.jl | 2 + src/show.jl | 169 +++++++++++++++++++++++++++++++++++ test/test-force-energy.jl | 84 +++++++++++++++++ test/test-linesegcelllist.jl | 17 ++++ test/test-neighbor-lists.jl | 57 ++++++++++++ test/test-pointcelllist.jl | 18 ++++ 7 files changed, 349 insertions(+), 2 deletions(-) create mode 100644 src/show.jl diff --git a/.gitignore b/.gitignore index 825cc39..66b22b5 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,5 @@ *.jl.*.cov *.jl.cov *.jl.mem -/Manifest.toml -/test/Manifest.toml \ No newline at end of file +/Manifest*.toml +/test/Manifest*.toml \ No newline at end of file diff --git a/src/SimplexCellLists.jl b/src/SimplexCellLists.jl index c9bf70a..056db7f 100644 --- a/src/SimplexCellLists.jl +++ b/src/SimplexCellLists.jl @@ -173,4 +173,6 @@ let NL = NeighborLists{DefaultCollidePolicy, DefaultPairParams} end end +include("show.jl") + end diff --git a/src/show.jl b/src/show.jl new file mode 100644 index 0000000..7d91687 --- /dev/null +++ b/src/show.jl @@ -0,0 +1,169 @@ +# `text/plain` `show` methods for the container types, printing a summary +# instead of the default dump of every field. + +# Print `n` followed by the singular or plural form of a noun. +function _print_count(io::IO, n::Integer, one::AbstractString, many::AbstractString) + print(io, n, " ", isone(n) ? one : many) +end + +function Base.show(io::IO, ::MIME"text/plain", pcl::PointCellList) + lens = Iterators.map(c -> c.len, pcl.cells) + summary(io, pcl) + print(io, " with ") + _print_count(io, sum(lens; init=0), "point", "points") + print(io, ":\n size: ", join(pcl.size, "×"), " cells") + print(io, "\n grid spacing: ") + show(IOContext(io, :compact => true), 2*inv(pcl.inv_half_grid_spacing)) + print(io, "\n most points in one cell: ", maximum(lens; init=0)) +end + +function Base.show(io::IO, ::MIME"text/plain", scl::LineSegCellList) + lens = Iterators.map(c -> c.len, scl.cells) + # Each line segment has exactly one entry with the `is_end` flag. + nsegs = sum(scl.cells; init=0) do c + count(cell_line_seg_is_end, view(c.list, 1:c.len)) + end + summary(io, scl) + print(io, " with ") + _print_count(io, nsegs, "line segment", "line segments") + print(io, ":\n size: ", join(scl.size, "×"), " cells") + print(io, "\n grid spacing: ") + show(IOContext(io, :compact => true), 2*inv(scl.inv_half_grid_spacing)) + print(io, "\n cell entries: ", sum(lens; init=0)) + print(io, "\n most entries in one cell: ", maximum(lens; init=0)) +end + +# Print the forces of all beads as type `F`, one bead per line, for the +# `text/plain` `show` of accumulators that store forces. Like printing a +# `Vector`, a limited `io` shows only the first and last beads so the output +# fits the display, including the `header_lines` already printed. +function _show_bead_forces(io::IO, fe::ForceEnergy, ::Type{F}, header_lines::Int) where F + n = get_nbeads(fe) + iszero(n) && return + print(io, "\n forces:") + max_lines = if get(io, :limit, false) + # Leave 5 lines: the "forces:" line, the "⋮" line, and 3 free lines + # for the prompt, the same as a long `Vector` leaves free. + max(displaysize(io)[1] - header_lines - 5, 2) + else + n + end + beads = if n ≤ max_lines + (1:n,) + else + (1:cld(max_lines, 2), n-fld(max_lines, 2)+1:n) + end + width = ndigits(n) + # The `:typeinfo` stops a `Rational{BigInt}[...]` style prefix. + compact_io = IOContext(io, :compact => true, :typeinfo => SVector{3, F}) + for (k, range) in enumerate(beads) + isone(k) || print(io, "\n ", lpad("⋮", width)) + for i in range + print(io, "\n ", lpad(i, width), ": ") + show(compact_io, get_bead_force(fe, i, F)) + end + end +end + +function Base.show(io::IO, ::MIME"text/plain", fe::ForceEnergyT{T}) where T + summary(io, fe) + print(io, " with ") + _print_count(io, get_nbeads(fe), "bead", "beads") + print(io, ":\n energy: ") + show(IOContext(io, :compact => true), fe.energy) + _show_bead_forces(io, fe, T, 2) +end + +function Base.show(io::IO, ::MIME"text/plain", fe::ForceEnergyFixedPoint) + summary(io, fe) + print(io, " with ") + _print_count(io, get_nbeads(fe), "bead", "beads") + print(io, ":\n energy: ") + show(IOContext(io, :compact => true), get_energy(fe)) + _show_bead_forces(io, fe, Float64, 2) +end + +function Base.show(io::IO, ::MIME"text/plain", fe::ForceNoEnergyFixedPoint) + summary(io, fe) + print(io, " with ") + _print_count(io, get_nbeads(fe), "bead", "beads") + print(io, " and no energy:") + _show_bead_forces(io, fe, Float64, 1) +end + +function Base.show(io::IO, ::MIME"text/plain", fe::DebugForceEnergy{T}) where T + summary(io, fe) + print(io, " with ") + _print_count(io, get_nbeads(fe), "bead", "beads") + print(io, ":\n ") + _print_count(io, length(fe.added_energy), "energy contribution", "energy contributions") + print(io, ", ") + _print_count(io, length(fe.added_bead_force), "force contribution", "force contributions") + print(io, "\n energy: ") + show(IOContext(io, :compact => true), get_energy(fe, T)) + _show_bead_forces(io, fe, T, 3) +end + +function Base.show(io::IO, ::MIME"text/plain", fe::NullForceEnergy) + summary(io, fe) + print(io, " with ") + _print_count(io, get_nbeads(fe), "bead", "beads") + print(io, ", discarding all forces and energy") +end + +function Base.show(io::IO, ::MIME"text/plain", inputs::NeighborListInputs) + compact_io = IOContext(io, :compact => true) + summary(io, inputs) + print(io, ":\n policy: ") + show(compact_io, inputs.policy) + # Name and lengths of the object, radius, and params vectors of each + # object type, which must match for the inputs to be valid. + objects = ( + ("points", "p", length(inputs.points), length(inputs.p_radius), length(inputs.p_params)), + ("clines", "c", length(inputs.clines), length(inputs.c_radius), length(inputs.c_params)), + ("lines", "l", length(inputs.lines), length(inputs.l_radius), length(inputs.l_params)), + ("line_points", "lp", length(inputs.line_points), length(inputs.lp_radius), length(inputs.lp_params)), + ("triangles", "t", length(inputs.triangles), length(inputs.t_radius), length(inputs.t_params)), + ("triangle_lines", "tl", length(inputs.triangle_lines), length(inputs.tl_radius), length(inputs.tl_params)), + ("triangle_points", "tp", length(inputs.triangle_points), length(inputs.tp_radius), length(inputs.tp_params)), + ) + name_width = maximum(o -> length(o[1]), objects) + 1 + count_width = maximum(o -> ndigits(o[3]), objects) + for (name, prefix, n, n_radius, n_params) in objects + print(io, "\n ", rpad(name * ":", name_width), " ", lpad(n, count_width)) + if n_radius != n || n_params != n + print(io, ", but ", n_radius, " ", prefix, "_radius and ", n_params, " ", prefix, "_params") + end + end + print(io, "\n no_collide_pairs: ", sum(length, inputs.no_collide_pairs)) + for pair_type in instances(CollidePairs) + n = length(inputs.no_collide_pairs[Int(pair_type)]) + iszero(n) || print(io, "\n ", pair_type, ": ", n) + end + print(io, "\n skin: ") + show(compact_io, inputs.skin) + print(io, "\n nthreads: ", inputs.nthreads) +end + +function Base.show(io::IO, ::MIME"text/plain", s::NeighborLists) + lists = ( + ("PPNL", "point–point", length(s.PPNL)), + ("PCNL", "point–cline", length(s.PCNL)), + ("PLNL", "point–line", length(s.PLNL)), + ("PTNL", "point–triangle", length(s.PTNL)), + ("CCNL", "cline–cline", length(s.CCNL)), + ("CLNL", "cline–line", length(s.CLNL)), + ("LLNL", "line–line", length(s.LLNL)), + ) + summary(io, s) + print(io, " with ") + _print_count(io, sum(l -> l[3], lists), "edge", "edges") + print(io, ":\n policy: ") + show(IOContext(io, :compact => true), s.policy) + desc_width = maximum(l -> textwidth(l[2]), lists) + 1 + count_width = maximum(l -> ndigits(l[3]), lists) + for (name, desc, n) in lists + # `rpad` pads by `textwidth`, so the en dashes align. + print(io, "\n ", name, " ", rpad(desc * ":", desc_width), " ", lpad(n, count_width)) + end +end diff --git a/test/test-force-energy.jl b/test/test-force-energy.jl index d761792..580abeb 100644 --- a/test/test-force-energy.jl +++ b/test/test-force-energy.jl @@ -335,5 +335,89 @@ using StaticArrays @test_throws MethodError get_force!(fe, zeros(SVector{3, Float64}, nbeads)) @test_throws MethodError get_force(fe) end + + @testset "show" begin + plain(x; kwargs...) = repr(MIME"text/plain"(), x; kwargs...) + fe = ForceEnergyFloat64(3) + add_bead_force!(fe, 2, SA[1.0, -2/3, 0.0]) + add_energy!(fe, 0.5) + @test plain(fe) == """ + ForceEnergyFloat64 with 3 beads: + energy: 0.5 + forces: + 1: [0.0, 0.0, 0.0] + 2: [1.0, -0.666667, 0.0] + 3: [0.0, 0.0, 0.0]""" + @test plain(ForceEnergyFloat32(0)) == """ + ForceEnergyFloat32 with 0 beads: + energy: 0.0""" + + # A limited display shows only the first and last beads. + limited = IOContext(devnull, :limit => true, :displaysize => (12, 80)) + @test plain(ForceEnergyFloat64(1000); context=limited) == """ + ForceEnergyFloat64 with 1000 beads: + energy: 0.0 + forces: + 1: [0.0, 0.0, 0.0] + 2: [0.0, 0.0, 0.0] + 3: [0.0, 0.0, 0.0] + ⋮ + 999: [0.0, 0.0, 0.0] + 1000: [0.0, 0.0, 0.0]""" + + # Fixed point shows converted values, not the raw integers. + fx = ForceEnergyFixedPoint{30, 30}(2) + add_bead_force!(fx, 1, SA[0.5, 0.25, -1.0]) + add_energy!(fx, 2.5) + @test plain(fx) == """ + ForceEnergyFixedPoint{30, 30} with 2 beads: + energy: 2.5 + forces: + 1: [0.5, 0.25, -1.0] + 2: [0.0, 0.0, 0.0]""" + + nfx = ForceNoEnergyFixedPoint{30}(1) + add_bead_force!(nfx, 1, SA[0.5, 0.25, -1.0]) + @test plain(nfx) == """ + ForceNoEnergyFixedPoint{30} with 1 bead and no energy: + forces: + 1: [0.5, 0.25, -1.0]""" + # With one header line, one more bead fits than above. + @test plain(ForceNoEnergyFixedPoint{30}(1000); context=limited) == """ + ForceNoEnergyFixedPoint{30} with 1000 beads and no energy: + forces: + 1: [0.0, 0.0, 0.0] + 2: [0.0, 0.0, 0.0] + 3: [0.0, 0.0, 0.0] + ⋮ + 998: [0.0, 0.0, 0.0] + 999: [0.0, 0.0, 0.0] + 1000: [0.0, 0.0, 0.0]""" + + dfe = DebugForceEnergy{Rational{Int}}(2) + add_bead_force!(dfe, 1, SA[1//3, 0, 0]) + add_bead_force!(dfe, 1, SA[1//3, 0, 0]) + add_energy!(dfe, 1//7) + @test plain(dfe) == """ + DebugForceEnergy{Rational{Int64}} with 2 beads: + 1 energy contribution, 2 force contributions + energy: 1//7 + forces: + 1: [2//3, 0, 0] + 2: [0, 0, 0]""" + # With three header lines, one less bead fits than above. + @test plain(DebugForceEnergy{Float64}(1000); context=limited) == """ + DebugForceEnergy{Float64} with 1000 beads: + 0 energy contributions, 0 force contributions + energy: 0.0 + forces: + 1: [0.0, 0.0, 0.0] + 2: [0.0, 0.0, 0.0] + ⋮ + 999: [0.0, 0.0, 0.0] + 1000: [0.0, 0.0, 0.0]""" + + @test plain(NullForceEnergy(1)) == "NullForceEnergy with 1 bead, discarding all forces and energy" + end end nothing diff --git a/test/test-linesegcelllist.jl b/test/test-linesegcelllist.jl index ff34bce..24fb844 100644 --- a/test/test-linesegcelllist.jl +++ b/test/test-linesegcelllist.jl @@ -369,5 +369,22 @@ end end end end + + @testset "show" begin + scl = LineSegCellList{Int,Float32}((4,4,4), 1f0) + # Crosses x = -1, 0, and 1, so has entries in 4 cells. + cell_line_seg_add!(scl, SA[-1.5f0, 0.5f0, 0.5f0], SA[1.5f0, 0.5f0, 0.5f0], 1) + # Inside one of those cells. + cell_line_seg_add!(scl, SA[0.2f0, 0.5f0, 0.5f0], SA[0.3f0, 0.5f0, 0.5f0], 2) + @test repr(MIME"text/plain"(), scl) == """ + LineSegCellList{Int64, Float32} with 2 line segments: + size: 4×4×4 cells + grid spacing: 1.0 + cell entries: 5 + most entries in one cell: 2""" + cell_line_segs_clear!(scl) + cell_line_seg_add!(scl, SA[0.2f0, 0.5f0, 0.5f0], SA[0.3f0, 0.5f0, 0.5f0], 2) + @test startswith(repr(MIME"text/plain"(), scl), "LineSegCellList{Int64, Float32} with 1 line segment:\n") + end end nothing diff --git a/test/test-neighbor-lists.jl b/test/test-neighbor-lists.jl index cb93b2b..c608abe 100644 --- a/test/test-neighbor-lists.jl +++ b/test/test-neighbor-lists.jl @@ -320,5 +320,62 @@ using Random end end end + + @testset "show" begin + plain(x) = repr(MIME"text/plain"(), x) + params = DefaultObjectParams(1.0f0, UInt32(1), UInt32(0)) + inputs = NeighborListInputs(policy; + points = PointIdxPart.(UInt32.(1:3)), + p_radius = fill(0.3f0, 3), + p_params = fill(params, 3), + skin = 0.25f0, + ) + push!(inputs.no_collide_pairs[Int(CollidePoint_Point)], UInt32(1) => UInt32(3)) + @test plain(inputs) == """ + NeighborListInputs{DefaultCollidePolicy, DefaultObjectParams}: + policy: DefaultCollidePolicy(8.0) + points: 3 + clines: 0 + lines: 0 + line_points: 0 + triangles: 0 + triangle_lines: 0 + triangle_points: 0 + no_collide_pairs: 1 + CollidePoint_Point: 1 + skin: 0.25 + nthreads: 1""" + + # Only the first two points are close enough to collide. + pos = [SA[0.0, 0.0, 0.0], SA[0.5, 0.0, 0.0], SA[10.0, 0.0, 0.0]] + nl = NeighborLists(policy) + setup_neighbors_naive!(nl, pos, inputs) + @test plain(nl) == """ + NeighborLists{DefaultCollidePolicy, DefaultPairParams} with 1 edge: + policy: DefaultCollidePolicy(8.0) + PPNL point–point: 1 + PCNL point–cline: 0 + PLNL point–line: 0 + PTNL point–triangle: 0 + CCNL cline–cline: 0 + CLNL cline–line: 0 + LLNL line–line: 0""" + + # Mismatched lengths, which setup rejects, are pointed out. + push!(inputs.c_radius, 1.0f0) + @test contains(plain(inputs), "\n clines: 0, but 1 c_radius and 0 c_params\n") + + # The count column widens to fit the largest count. + inputs = NeighborListInputs(policy; + lines = fill(LineIdxPart(UInt32(1), UInt32(2)), 12345), + l_radius = fill(0.3f0, 12345), + l_params = fill(params, 12345), + ) + @test contains(plain(inputs), """ + points: 0 + clines: 0 + lines: 12345 + """) + end end nothing diff --git a/test/test-pointcelllist.jl b/test/test-pointcelllist.jl index 0d34ed4..a93e811 100644 --- a/test/test-pointcelllist.jl +++ b/test/test-pointcelllist.jl @@ -322,5 +322,23 @@ total_entries(pcl::PointCellList) = sum(pcl.cells[i].len for i in 1:prod(pcl.siz end end end + + @testset "show" begin + pcl = PointCellList{Int,Float32}((4,3,2), 0.5f0) + cell_point_add!(pcl, SA[0.1f0, 0.1f0, 0.1f0], 1) + cell_point_add!(pcl, SA[0.2f0, 0.1f0, 0.1f0], 2) + cell_point_add!(pcl, SA[-0.6f0, 0.1f0, 0.1f0], 3) + @test repr(MIME"text/plain"(), pcl) == """ + PointCellList{Int64, Float32} with 3 points: + size: 4×3×2 cells + grid spacing: 0.5 + most points in one cell: 2""" + cell_points_clear!(pcl) + @test repr(MIME"text/plain"(), pcl) == """ + PointCellList{Int64, Float32} with 0 points: + size: 4×3×2 cells + grid spacing: 0.5 + most points in one cell: 0""" + end end nothing