From 13a5c1f8dfe0df4a09139cbe3dadb293aed0de74 Mon Sep 17 00:00:00 2001 From: Jeffrey Ward Date: Sat, 11 Jul 2026 18:56:21 -0400 Subject: [PATCH] feat: add `show` methods for spaces, tensors and BilinearOperator Only LinearOperator and BettiSpectrum had one; everything else fell back to the default struct dump, which for a tensor splats the whole coefficient vector into the REPL. Formats mirror the upstream Python `__repr__`s. Un-skips the three repr assertions in the ported Python suite. Closes #15. --- src/operators/types/bilinear.jl | 5 ++++ src/operators/types/linear.jl | 4 +-- src/tensors/base_tensor/base_tensor.jl | 7 +++++ src/tensors/base_tensor/base_tensor_space.jl | 6 ++++ src/tensors/direct_sum/direct_sum.jl | 5 ++++ src/tensors/forms/forms.jl | 3 ++ test/test_pyclasses.jl | 29 +++++++++++++++----- test/test_tensor_algebra.jl | 14 ++++++++++ 8 files changed, 64 insertions(+), 9 deletions(-) diff --git a/src/operators/types/bilinear.jl b/src/operators/types/bilinear.jl index 28b1fb4..47a8c99 100644 --- a/src/operators/types/bilinear.jl +++ b/src/operators/types/bilinear.jl @@ -33,6 +33,11 @@ end component_shape(B::BilinearOperator) = (space_dim(B.codomain), space_dim(B.domain_a), space_dim(B.domain_b)) +function Base.show(io::IO, B::BilinearOperator) + print(io, "BilinearOperator(domain_a=", B.domain_a, ", domain_b=", B.domain_b, + ", codomain=", B.codomain, ", component_shape=", component_shape(B), ")") +end + """Weak-form component 3-tensor.""" function weak(B::BilinearOperator) B._weak !== nothing && return B._weak diff --git a/src/operators/types/linear.jl b/src/operators/types/linear.jl index 2705e57..add137e 100644 --- a/src/operators/types/linear.jl +++ b/src/operators/types/linear.jl @@ -48,8 +48,8 @@ function LinearOperator(domain::AbstractTensorSpace, codomain::AbstractTensorSpa end function Base.show(io::IO, L::LinearOperator) - print(io, "LinearOperator($(typeof(L.domain)) → $(typeof(L.codomain)), ", - "shape=($(space_dim(L.codomain)), $(space_dim(L.domain))))") + print(io, "LinearOperator(domain=", L.domain, ", codomain=", L.codomain, + ", shape=(", space_dim(L.codomain), ", ", space_dim(L.domain), "))") end """Operator matrix in weak (bilinear-form) representation.""" diff --git a/src/tensors/base_tensor/base_tensor.jl b/src/tensors/base_tensor/base_tensor.jl index fadb8fc..d4b2719 100644 --- a/src/tensors/base_tensor/base_tensor.jl +++ b/src/tensors/base_tensor/base_tensor.jl @@ -8,6 +8,13 @@ """Pointwise data of a tensor at the sample points, shape `(batch..., n·C)`.""" to_pointwise_basis(t::AbstractTensor) = _to_pointwise_basis(t.coeffs, t.space) +# ── Display ──────────────────────────────────────────────────────────────────── +# Coefficients are deliberately not printed: a tensor carries one per basis +# function per component, which swamps the REPL to no purpose. +Base.show(io::IO, t::AbstractTensor) = + print(io, nameof(typeof(t)), "(space=", t.space, ", shape=", size(t.coeffs), + ", batch_shape=", batch_shape(t), ")") + # ── Linear structure ─────────────────────────────────────────────────────────── Base.:-(t::AbstractTensor) = wrap(t.space, -t.coeffs) diff --git a/src/tensors/base_tensor/base_tensor_space.jl b/src/tensors/base_tensor/base_tensor_space.jl index c9d61c1..fd4ef9d 100644 --- a/src/tensors/base_tensor/base_tensor_space.jl +++ b/src/tensors/base_tensor/base_tensor_space.jl @@ -72,6 +72,12 @@ function Base.zeros(space::AbstractTensorSpace, batch::Tuple=()) return wrap(space, zeros(Float64, batch..., space_dim(space))) end +# ── Display ──────────────────────────────────────────────────────────────────── +# Concrete spaces carrying extra structure (a form degree, a list of summands) +# override this; everything else is just its name and its coefficient dimension. +Base.show(io::IO, space::AbstractTensorSpace) = + print(io, nameof(typeof(space)), "(dim=", space_dim(space), ")") + # ── Equality / hashing (used as cache keys) ──────────────────────────────────── function Base.:(==)(a::AbstractTensorSpace, b::AbstractTensorSpace) a === b && return true diff --git a/src/tensors/direct_sum/direct_sum.jl b/src/tensors/direct_sum/direct_sum.jl index 1c48177..670c1e1 100644 --- a/src/tensors/direct_sum/direct_sum.jl +++ b/src/tensors/direct_sum/direct_sum.jl @@ -32,6 +32,11 @@ end space_dim(ds::DirectSumSpace) = sum(space_dim(s) for s in ds.spaces) +function Base.show(io::IO, ds::DirectSumSpace) + parts = join((nameof(typeof(s)) for s in ds.spaces), ", ") + print(io, "DirectSumSpace(spaces=[", parts, "], dim=", space_dim(ds), ")") +end + # Offset ranges for splitting the coefficient vector into summands. function _coeff_ranges(ds::DirectSumSpace) ranges = UnitRange{Int}[] diff --git a/src/tensors/forms/forms.jl b/src/tensors/forms/forms.jl index b943daa..c0a7742 100644 --- a/src/tensors/forms/forms.jl +++ b/src/tensors/forms/forms.jl @@ -25,6 +25,9 @@ end space_degree(space::FormSpace) = space.degree degree(f::Form) = f.space.degree +Base.show(io::IO, space::FormSpace) = + print(io, "FormSpace(degree=", space.degree, ", dim=", space_dim(space), ")") + cdc_components(space::FormSpace) = gamma_coords_compound(space.dg.cache, space.degree)[2] function wrap(space::FormSpace, coeffs::AbstractArray) diff --git a/test/test_pyclasses.jl b/test/test_pyclasses.jl index c782b9e..3165b32 100644 --- a/test/test_pyclasses.jl +++ b/test/test_pyclasses.jl @@ -5,8 +5,7 @@ # error paths. It runs over the same d = 1..4 config sweep (see pysuite.jl). # # Purely Python-shaped tests are skipped with a note where they appear: numpy ufunc -# dispatch (`np.multiply(..., where=...)`), `repr` string contents (Julia defines -# `show` only for LinearOperator), the numpy RuntimeWarning check, and the +# dispatch (`np.multiply(..., where=...)`), the numpy RuntimeWarning check, and the # `MockDiffusionGeometry` subclass (Julia has no subclassing; the behaviours it # probes are exercised against a real geometry instead). @@ -521,8 +520,12 @@ end @test coeffs(components[2]) ≈ c2 end - # Upstream also asserts on `repr(element)`. Julia defines `show` only for - # LinearOperator, so there is no equivalent to check. + @testset "repr includes space" begin + rep = repr(zeros(s1 + s2)) + @test occursin("DirectSumElement(", rep) + @test occursin("space=DirectSumSpace(", rep) + @test occursin("spaces=[FunctionSpace, VectorFieldSpace]", rep) + end end end @@ -782,10 +785,16 @@ end @test coeffs(out) ≈ expected end end + + @testset "repr includes spaces" begin + rep = repr(identity_operator(sp)) + @test occursin("LinearOperator(", rep) + @test occursin("domain=FunctionSpace(dim=", rep) + @test occursin("codomain=FunctionSpace(dim=", rep) + end end - # Skipped: `repr(op)` contents (Julia's `show` for LinearOperator has a different - # format), and the numpy RuntimeWarning check, which has no Julia analogue. + # Skipped: the numpy RuntimeWarning check, which has no Julia analogue. end # ── test_operators_spectral.py ──────────────────────────────────────────────── @@ -1201,7 +1210,13 @@ end @test coeffs(B(x, y)) ≈ coeffs(Bt(y, x)) end - # Skipped: `repr(B)` contents — Julia defines no `show` for BilinearOperator. + @testset "repr includes domains" begin + rep = repr(lie_bracket(dg)) + @test occursin("BilinearOperator(", rep) + @test occursin("domain_a=VectorFieldSpace(dim=", rep) + @test occursin("domain_b=VectorFieldSpace(dim=", rep) + @test occursin("codomain=VectorFieldSpace(dim=", rep) + end end end diff --git a/test/test_tensor_algebra.jl b/test/test_tensor_algebra.jl index 6b50289..d27dffc 100644 --- a/test/test_tensor_algebra.jl +++ b/test/test_tensor_algebra.jl @@ -111,4 +111,18 @@ @test aeq(Xu.coeffs, X.coeffs) @test aeq(fu.coeffs, f.coeffs) end + + @testset "show" begin + fs, vfs = function_space(dg), vector_field_space(dg) + @test repr(fs) == "FunctionSpace(dim=$(space_dim(fs)))" + @test repr(form_space(dg, 2)) == "FormSpace(degree=2, dim=$(space_dim(form_space(dg, 2))))" + @test repr(vfs + fs) == + "DirectSumSpace(spaces=[VectorFieldSpace, FunctionSpace], dim=$(space_dim(vfs + fs)))" + + # A tensor shows its space and batch shape, never its coefficients. + @test repr(w2) == "Form(space=$(repr(w2.space)), shape=$(size(w2.coeffs)), batch_shape=())" + batched = wrap(fs, zeros(3, space_dim(fs))) + @test repr(batched) == + "ScalarFunction(space=$(repr(fs)), shape=(3, $(space_dim(fs))), batch_shape=(3,))" + end end