diff --git a/docs/src/api/zero_loci.md b/docs/src/api/zero_loci.md index 2de51f3..af689bc 100644 --- a/docs/src/api/zero_loci.md +++ b/docs/src/api/zero_loci.md @@ -120,6 +120,18 @@ Exterior and symmetric powers use the derived graded-power formula, so they also work for arbitrary composite presentations rather than only for the tangent and cotangent sequences. +Before invoking the generic long-exact-sequence solver, `cohomology` checks +whether the zero locus splits and whether the current presentation is a +direct sum of external tensor products. If so, and the factor cohomology is +determined, it applies the Künneth formula. This is structural recognition: +the optimization also applies when the locus and bundle merely have the +factorable form, regardless of whether `product` or an `external_*` constructor +created them. Concretely, the recognizer groups terms sharing a factor and +checks that each group forms the rectangular grid of an external tensor +product: its presentation degrees must split as sums of factor degrees and its +multiplicities as products of factor multiplicities. Ambiguous or nonfactorable +presentations safely use the generic solver. + !!! note "Generic bundle cohomology versus the Hodge engine" `cohomology(exterior_power(cotangent_bundle(Z), p))` evaluates that one bundle from its presentation. `hodge_numbers(Z)` uses the same conormal diff --git a/src/ExternalProducts.jl b/src/ExternalProducts.jl index 3ec8304..f3e875a 100644 --- a/src/ExternalProducts.jl +++ b/src/ExternalProducts.jl @@ -19,6 +19,8 @@ function _lift_bundle_to_product( ) end +# Lift two ambient bundles to the common product ambient, placing the right +# bundle after the left Dynkin block. function _lift_external_factors( product_ambient::PartialFlagVariety, E::_AmbientBundle, diff --git a/src/FilteredBundle.jl b/src/FilteredBundle.jl index 13c38f0..3acb84c 100644 --- a/src/FilteredBundle.jl +++ b/src/FilteredBundle.jl @@ -143,6 +143,20 @@ equals the empty filtered bundle on the same variety. """ Base.iszero(F::FilteredBundle) = all(iszero, graded_pieces(F)) +# Assemble a filtered bundle from irreducible components indexed by filtration +# degree, omitting absent degrees while preserving their order. +function _filtered_bundle_from_graded_components( + X::PartialFlagVariety, terms::AbstractDict{Int,<:AbstractVector{IrrepLevi}} +) + FilteredBundle( + X, + [ + CompletelyReducibleBundle(X, terms[degree]) for + degree in sort!(collect(keys(terms))) + ], + ) +end + # ═══════════════════════════════════════════════════════════════════════════════ # Tensor products involving FilteredBundle # ═══════════════════════════════════════════════════════════════════════════════ @@ -183,13 +197,7 @@ function tensor_product(F::FilteredBundle, G::FilteredBundle) end end - FilteredBundle( - variety(F), - CompletelyReducibleBundle[ - CompletelyReducibleBundle(variety(F), terms[degree]) for - degree in sort!(collect(keys(terms))) - ], - ) + _filtered_bundle_from_graded_components(variety(F), terms) end # ═══════════════════════════════════════════════════════════════════════════════ @@ -365,13 +373,7 @@ function _graded_power(power, F::FilteredBundle, k::Integer) append!(get!(weight_terms, filtration_degree, IrrepLevi[]), components(term)) end - FilteredBundle( - F.variety, - CompletelyReducibleBundle[ - CompletelyReducibleBundle(F.variety, weight_terms[filtration_degree]) for - filtration_degree in sort!(collect(keys(weight_terms))) - ], - ) + _filtered_bundle_from_graded_components(F.variety, weight_terms) end """ diff --git a/src/Kunneth.jl b/src/Kunneth.jl new file mode 100644 index 0000000..06b3b22 --- /dev/null +++ b/src/Kunneth.jl @@ -0,0 +1,227 @@ +# ═══════════════════════════════════════════════════════════════════════════════ +# Structural Künneth recognition +# +# No product provenance is stored. Recognition therefore runs backwards from +# product-group representations and ambient presentation terms, accepting only +# unambiguous external tensor-product decompositions. +# ═══════════════════════════════════════════════════════════════════════════════ + +# Split a product-group highest weight at the boundary between the two ambient +# Dynkin types. Callers have already validated the product ambient. +function _split_product_irrep( + irrep::IrrepLevi, + left_ambient::PartialFlagVariety, + right_ambient::PartialFlagVariety, +) + coefficients = collect(Int, Semisimple.coefficients(p_dominant_weight(irrep))) + left_rank = rank(dynkin_type(left_ambient)) + ( + IrrepLevi(marked_dynkin_type(left_ambient), coefficients[1:left_rank]), + IrrepLevi(marked_dynkin_type(right_ambient), coefficients[(left_rank + 1):end]), + ) +end + +# Add `(left, right) => (degree, multiplicity)` to a product grid. Repeated +# factor pairs must have the same degree. +function _add_product_term!(terms, pair, degree::Int, multiplicity::Int=1) + old_degree, old_multiplicity = get(terms, pair, (degree, 0)) + old_degree == degree || return false + terms[pair] = (degree, old_multiplicity + multiplicity) + true +end + +# Group product-grid keys by shared left or right factor. Presentations are +# small, so a direct component walk is clearer than a separate graph structure. +function _product_term_components(terms) + remaining = Set(keys(terms)) + components = Vector{Vector{keytype(terms)}}() + while !isempty(remaining) + component = keytype(terms)[pop!(remaining)] + cursor = 1 + while cursor <= length(component) + pair = component[cursor] + neighbors = [ + candidate for candidate in remaining if + first(candidate) == first(pair) || last(candidate) == last(pair) + ] + append!(component, neighbors) + setdiff!(remaining, neighbors) + cursor += 1 + end + push!(components, component) + end + components +end + +# Recover factors of one product grid. External products have rectangular +# support, additive degrees, and multiplicative multiplicities. +function _factor_product_terms(terms, component=keys(terms)) + isempty(component) && return nothing + lefts = unique(first(pair) for pair in component) + rights = unique(last(pair) for pair in component) + length(component) == length(lefts) * length(rights) || return nothing + + degrees = [first(terms[(left, right)]) for left in lefts, right in rights] + multiplicities = [last(terms[(left, right)]) for left in lefts, right in rights] + degrees == degrees[:, 1] .+ degrees[1, :]' .- degrees[1, 1] || return nothing + multiplicities .* multiplicities[1, 1] == + multiplicities[:, 1] * multiplicities[1, :]' || return nothing + + # Normalize the opposite degree shift at the first left factor and the common + # multiplicity scale by the gcd of its row. The rank-one identity makes the + # division of the first column below exact. + row_gcd = reduce(gcd, @view multiplicities[1, :]) + right_multiplicities = multiplicities[1, :] .÷ row_gcd + left_multiplicities = multiplicities[:, 1] .÷ first(right_multiplicities) + left_data = Dict( + zip(lefts, zip(degrees[:, 1] .- degrees[1, 1], left_multiplicities)) + ) + right_data = Dict(zip(rights, zip(degrees[1, :], right_multiplicities))) + left_data, right_data +end + +# Expand factor data into terms grouped by degree. This is shared by filtered +# bundles and zero-locus-bundle presentations. +function _graded_factor_terms(data::AbstractDict{T}, shift::Int=0) where {T} + terms = Dict{Int,Vector{T}}() + for (term, (degree, multiplicity)) in data + append!(get!(terms, degree + shift, T[]), fill(term, multiplicity)) + end + terms +end + +# Split every irreducible summand of a completely reducible ambient bundle. +function _factor_ambient_bundle_on_product( + bundle::CompletelyReducibleBundle, + left_ambient::PartialFlagVariety, + right_ambient::PartialFlagVariety, +) + map(components(bundle)) do irrep + left, right = _split_product_irrep(irrep, left_ambient, right_ambient) + ( + CompletelyReducibleBundle(left_ambient, [left]), + CompletelyReducibleBundle(right_ambient, [right]), + ) + end +end + +# Normalize a one-step filtration to its total bundle. For a genuine filtration, +# factor its irreducible grid and verify the reconstructed external product. +function _factor_ambient_bundle_on_product( + bundle::FilteredBundle, + left_ambient::PartialFlagVariety, + right_ambient::PartialFlagVariety, +) + n_filtration_steps(bundle) == 1 && return _factor_ambient_bundle_on_product( + total_bundle(bundle), left_ambient, right_ambient + ) + + terms = Dict{Tuple{IrrepLevi,IrrepLevi},Tuple{Int,Int}}() + for (degree, piece) in enumerate(graded_pieces(bundle)), irrep in components(piece) + split = _split_product_irrep(irrep, left_ambient, right_ambient) + _add_product_term!(terms, split, degree) || return nothing + end + factor_data = _factor_product_terms(terms) + factor_data === nothing && return nothing + + factor_pair = map((left_ambient, right_ambient), factor_data) do ambient, data + factor = _filtered_bundle_from_graded_components( + ambient, _graded_factor_terms(data) + ) + n_filtration_steps(factor) == 1 ? total_bundle(factor) : factor + end + external_tensor_product(factor_pair...) == bundle ? [factor_pair] : nothing +end + +# Factor one connected presentation component and choose the unique shift for +# which both factor presentations have positive rank in degree zero. +function _factor_presentation_component( + terms, component, left_locus::ZeroLocus, right_locus::ZeroLocus +) + factor_data = _factor_product_terms(terms, component) + factor_data === nothing && return nothing + left_data, right_data = factor_data + + shifts = intersect( + Set(-degree for (_, (degree, _)) in left_data), + Set(degree for (_, (degree, _)) in right_data), + ) + factor_rank(data, shift) = sum( + (isodd(degree + shift) ? -1 : 1) * multiplicity * rank(bundle) for + (bundle, (degree, multiplicity)) in data; + init=0, + ) + filter!( + shift -> factor_rank(left_data, shift) > 0 && factor_rank(right_data, -shift) > 0, + shifts, + ) + length(shifts) == 1 || return nothing + + shift = only(shifts) + map((left_locus, right_locus), factor_data, (shift, -shift)) do locus, data, offset + ZeroLocusBundle( + locus, _AmbientBundlePresentation(_graded_factor_terms(data, offset)) + ) + end +end + +# Recognize a presentation as a direct sum of external products for one fixed +# bipartition of its zero locus. +function _kunneth_decomposition( + F::ZeroLocusBundle, left_locus::ZeroLocus, right_locus::ZeroLocus +) + left_ambient = ambient_variety(left_locus) + right_ambient = ambient_variety(right_locus) + terms = Dict{Tuple{_AmbientBundle,_AmbientBundle},Tuple{Int,Int}}() + + for (degree, summands) in F.presentation.terms, summand in summands + factorizations = _factor_ambient_bundle_on_product(summand, left_ambient, right_ambient) + factorizations === nothing && return nothing + for (left, right) in factorizations + _add_product_term!(terms, (left, right), degree) || return nothing + end + end + isempty(terms) && return nothing + + decomposition = Tuple{ZeroLocusBundle,ZeroLocusBundle}[] + for component in _product_term_components(terms) + factor_pair = _factor_presentation_component(terms, component, left_locus, right_locus) + factor_pair === nothing && return nothing + push!(decomposition, factor_pair) + end + decomposition +end + +# Try each contiguous bipartition of the recognized zero-locus factors. +function _kunneth_decomposition(F::ZeroLocusBundle) + Z = variety(F) + locus_factors = factors(Z) + for split in 1:(length(locus_factors) - 1) + left = reduce(product, locus_factors[1:split]) + right = reduce(product, locus_factors[(split + 1):end]) + product(left, right) == Z || continue + decomposition = _kunneth_decomposition(F, left, right) + decomposition === nothing || return decomposition + end + nothing +end + +# Convolve determined factor cohomologies; otherwise leave the generic +# presentation solver to retain its symbolic answer. +function _kunneth_cohomology(F::ZeroLocusBundle) + decomposition = _kunneth_decomposition(F) + decomposition === nothing && return nothing + + d = dimension(variety(F)) + entries = zeros(BigInt, d + 1) + for (left, right) in decomposition + left_cohomology = cohomology(left) + right_cohomology = cohomology(right) + is_determined(left_cohomology) && is_determined(right_cohomology) || return nothing + for p in 0:left_cohomology.max_degree, q in 0:right_cohomology.max_degree + entries[p + q + 1] += + left_cohomology[p].constant * right_cohomology[q].constant + end + end + Cohomology{AffineExpr}(AffineExpr.(entries), d) +end diff --git a/src/PartialFlagVarieties.jl b/src/PartialFlagVarieties.jl index 217bb49..a45b338 100644 --- a/src/PartialFlagVarieties.jl +++ b/src/PartialFlagVarieties.jl @@ -58,6 +58,7 @@ include("Constructions.jl") include("Koszul.jl") include("ZeroLoci.jl") include("ZeroLocusBundles.jl") +include("Kunneth.jl") include("Labels.jl") include("ExceptionalCollections.jl") include("Hodge.jl") diff --git a/src/ZeroLoci.jl b/src/ZeroLoci.jl index 74d1aee..bcadc0b 100644 --- a/src/ZeroLoci.jl +++ b/src/ZeroLoci.jl @@ -189,7 +189,9 @@ latter disconnects `Z` into `m` copies); a single reduced point is a Künneth identity and is dropped. `n_factors` counts the kept factors. `hodge_numbers`, `hochschild_cohomology`, and cohomology of the tangent bundle -recombine the factors by the Künneth formula, which determines +recombine the factors by the Künneth formula. Cohomology of any bundle whose +ambient presentation is structurally recognized as a direct sum of external +tensor products does the same. This determines diamonds/parallelograms the monolithic long-exact-sequence solver leaves symbolic. (The remaining invariants — `euler_characteristic`, `hilbert_polynomial`, the anticanonical degree — are already exact for a @@ -237,9 +239,8 @@ function factors(Z::ZeroLocus) filter(part -> dimension(part) >= 1 || euler_characteristic(part) >= 2, parts) end -# Partition the ambient factors `1:n` into connected blocks, joining two factors -# whenever some bundle summand is supported on both (union–find with path -# compression). Untouched factors form singleton blocks. +# Partition the ambient factors into connected blocks, joining the support of +# each defining-bundle summand. Untouched factors remain singleton blocks. function _connected_ambient_factors(supports, n) parent = collect(1:n) root(i) = parent[i] == i ? i : (parent[i] = root(parent[i])) @@ -250,7 +251,7 @@ function _connected_ambient_factors(supports, n) for factor in 1:n push!(blocks[root(factor)], factor) end - return filter(!isempty, blocks) + filter(!isempty, blocks) end """ diff --git a/src/ZeroLocusBundles.jl b/src/ZeroLocusBundles.jl index c933d41..b58a642 100644 --- a/src/ZeroLocusBundles.jl +++ b/src/ZeroLocusBundles.jl @@ -15,7 +15,9 @@ export ZeroLocusBundle, restrict struct _AmbientBundlePresentation terms::Dict{Int,Vector{_AmbientBundle}} - function _AmbientBundlePresentation(terms::Dict{Int,Vector{_AmbientBundle}}) + function _AmbientBundlePresentation( + terms::AbstractDict{Int,<:AbstractVector{<:_AmbientBundle}} + ) cleaned = Dict{Int,Vector{_AmbientBundle}}() for (degree, summands) in terms kept = _AmbientBundle[summand for summand in summands if rank(summand) != 0] @@ -25,11 +27,14 @@ struct _AmbientBundlePresentation end end +# Wrap one ambient bundle as a one-term presentation in the requested degree. _AmbientBundlePresentation(degree::Int, bundle::_AmbientBundle) = _AmbientBundlePresentation( Dict{Int,Vector{_AmbientBundle}}(degree => _AmbientBundle[bundle]) ) +# Compare summand multisets; their order within one presentation degree carries +# no mathematical information. function _same_summands(left::Vector{_AmbientBundle}, right::Vector{_AmbientBundle}) length(left) == length(right) || return false unmatched = trues(length(right)) @@ -99,6 +104,7 @@ variety(F::ZeroLocusBundle) = F.locus """Return the ambient partial flag variety of the base of `F`.""" ambient_variety(F::ZeroLocusBundle) = ambient_variety(variety(F)) +# Guard binary bundle operations against mixing different zero loci. function _check_same_locus(F::ZeroLocusBundle, G::ZeroLocusBundle, operation::String) variety(F) == variety(G) || throw( ArgumentError("$operation requires bundles on the same zero locus.") @@ -187,6 +193,8 @@ function _lift_bundle_to_product( ZeroLocusBundle(product_locus, _AmbientBundlePresentation(terms)) end +# Lift two presentations to their common product zero locus, placing the right +# presentation after the left ambient Dynkin block. function _lift_external_factors(F::ZeroLocusBundle, G::ZeroLocusBundle) product_locus = product(variety(F), variety(G)) right_offset = rank(dynkin_type(ambient_variety(F))) @@ -471,8 +479,11 @@ presentation is the normal sequence ``0 \\to \\mathrm{T}_Z \\to \\mathrm{T}_X|_Z \\to \\mathcal{E}|_Z \\to 0``. """ function tangent_bundle(Z::ZeroLocus) + ambient_tangent = filtered_tangent_bundle(ambient_variety(Z)) + n_filtration_steps(ambient_tangent) == 1 && + (ambient_tangent = total_bundle(ambient_tangent)) terms = Dict{Int,Vector{_AmbientBundle}}( - 0 => _AmbientBundle[filtered_tangent_bundle(ambient_variety(Z))], + 0 => _AmbientBundle[ambient_tangent], 1 => _AmbientBundle[defining_bundle(Z)], ) ZeroLocusBundle(Z, _AmbientBundlePresentation(terms)) @@ -532,6 +543,8 @@ end # Additive invariants and cohomology # ═══════════════════════════════════════════════════════════════════════════════ +# Evaluate the Euler characteristic of an ambient bundle after restriction; +# a filtration contributes the sum of its graded pieces. _euler_characteristic_on_restriction(Z::ZeroLocus, F::CompletelyReducibleBundle) = _euler_characteristic_from_counts(Z, _to_counts(F)) @@ -560,6 +573,7 @@ end chi(F::ZeroLocusBundle) = euler_characteristic(F) +# Dispatch an ambient summand to the appropriate restriction LES backend. function _restriction_cohomology( Z::ZeroLocus, F::CompletelyReducibleBundle, var_counter::Ref{Int} ) @@ -572,6 +586,8 @@ function _restriction_cohomology( _restrict_to_zero_locus_les(Z, F, var_counter) end +# Add the restriction cohomologies of all ambient summands in one presentation +# degree; the presentation overload supplies an empty degree when necessary. function _presentation_term_cohomology( Z::ZeroLocus, summands::Vector{_AmbientBundle}, var_counter::Ref{Int} ) @@ -642,19 +658,19 @@ Compute dimension-valued sheaf cohomology from the ambient presentation. Entries are exact integers where exactness and the available geometric constraints determine them, and symbolic affine expressions otherwise. +When the zero locus splits as a product and the presentation is +structurally a direct sum of external tensor products, this method first tries +the Künneth formula. Recognition uses the current locus and presentation, not +the constructors that produced them. If no unambiguous factorization is found, +or factor cohomology remains symbolic, computation falls back to the generic +long-exact-sequence backend. + Character-valued cohomology is not defined: a section cutting out a zero locus is generally not invariant under the ambient group. """ function cohomology(F::ZeroLocusBundle) Z = variety(F) d = dimension(Z) - is_tangent = F == tangent_bundle(Z) - - if is_tangent && n_factors(Z) >= 2 - tangent_row = AffineExpr[hochschild_cohomology(Z)[1, q] for q in 0:d] - all(is_determined, tangent_row) && return Cohomology{AffineExpr}(tangent_row, d) - end - var_counter = Ref(0) # A degree-zero presentation is just a direct sum of restricted ambient @@ -665,6 +681,15 @@ function cohomology(F::ZeroLocusBundle) return Cohomology{AffineExpr}(entries, d) end + is_tangent = F == tangent_bundle(Z) + if is_tangent && n_factors(Z) >= 2 + tangent_row = AffineExpr[hochschild_cohomology(Z)[1, q] for q in 0:d] + all(is_determined, tangent_row) && return Cohomology{AffineExpr}(tangent_row, d) + end + + kunneth_cohomology = _kunneth_cohomology(F) + kunneth_cohomology === nothing || return kunneth_cohomology + entries, inequalities = _cohomology_from_presentation(F, var_counter) entry_count = length(entries) system = vcat(entries, inequalities) diff --git a/test/runtests.jl b/test/runtests.jl index 26316ca..961093b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -656,6 +656,28 @@ mdt(::Type{DT}, marked) where {DT<:DynkinType} = MarkedDynkinType(DT, marked) @test total_bundle(mixed_left) == external_tensor_product(total_bundle(F), M) @test total_bundle(mixed_right) == external_tensor_product(L, total_bundle(G)) @test iszero(external_tensor_product(F, zero_bundle(Y))) + + mixed_factors = only( + PartialFlagVarieties._factor_ambient_bundle_on_product( + mixed_left, variety(F), variety(M) + ), + ) + @test external_tensor_product(mixed_factors...) == mixed_left + + H = filtered_tangent_bundle(full_flag_variety(TypeB{2})) + FH = external_tensor_product(F, H) + filtered_factors = only( + PartialFlagVarieties._factor_ambient_bundle_on_product(FH, variety(F), variety(H)) + ) + @test external_tensor_product(filtered_factors...) == FH + + one_step_product = external_tensor_product(G, G) + one_step_factors = only( + PartialFlagVarieties._factor_ambient_bundle_on_product( + one_step_product, variety(G), variety(G) + ), + ) + @test external_tensor_product(one_step_factors...) == total_bundle(one_step_product) end @testset "Twist" begin @@ -2265,6 +2287,45 @@ mdt(::Type{DT}, marked) where {DT<:DynkinType} = MarkedDynkinType(DT, marked) end @testset "Bundles on zero loci: external operations" begin + # An external tensor product has rectangular support, additive degrees, + # and rank-one multiplicities in its left/right factor grid. + factor_terms = PartialFlagVarieties._factor_product_terms + term_components = PartialFlagVarieties._product_term_components + add_term! = PartialFlagVarieties._add_product_term! + product_terms = Dict( + (:a, :x) => (1, 6), + (:a, :y) => (4, 8), + (:b, :x) => (2, 15), + (:b, :y) => (5, 20), + ) + left_data, right_data = factor_terms(product_terms) + for ((left, right), (degree, multiplicity)) in product_terms + left_degree, left_multiplicity = left_data[left] + right_degree, right_multiplicity = right_data[right] + @test degree == left_degree + right_degree + @test multiplicity == left_multiplicity * right_multiplicity + end + + disconnected_terms = merge(product_terms, Dict((:c, :z) => (0, 1))) + @test sort!(length.(term_components(disconnected_terms))) == [1, 4] + incomplete_terms = copy(product_terms) + delete!(incomplete_terms, (:b, :y)) + @test all( + isnothing ∘ factor_terms, + ( + empty(product_terms), + incomplete_terms, + merge(product_terms, Dict((:b, :y) => (6, 20))), + merge(product_terms, Dict((:b, :y) => (5, 21))), + ), + ) + + repeated_term = Dict{Tuple{Symbol,Symbol},Tuple{Int,Int}}() + @test add_term!(repeated_term, (:a, :x), 2, 3) + @test add_term!(repeated_term, (:a, :x), 2, 4) + @test repeated_term[(:a, :x)] == (2, 7) + @test !add_term!(repeated_term, (:a, :x), 3) + Z = zero_locus(line_bundle(projective_space(2), 1)) W = zero_locus(line_bundle(projective_space(2), 2)) ZW = product(Z, W) @@ -2298,6 +2359,45 @@ mdt(::Type{DT}, marked) where {DT<:DynkinType} = MarkedDynkinType(DT, marked) @test euler_characteristic(tangent_sum) == euler_characteristic(TZ) * euler_characteristic(structure_sheaf(W)) + euler_characteristic(structure_sheaf(Z)) * euler_characteristic(TW) + + quartic_ambient = projective_space(3) + quartic = zero_locus(line_bundle(quartic_ambient, 4)) + tangent_quartic = tangent_bundle(quartic) + tangent_box_square = external_tensor_product(tangent_quartic, tangent_quartic) + tangent_box_sum = external_direct_sum(tangent_quartic, tangent_quartic) + @test cohomology(tangent_box_square).entries == AffineExpr.([0, 0, 400, 0, 0]) + @test cohomology(tangent_box_sum).entries == AffineExpr.([0, 40, 0, 40, 0]) + + product_ambient = product(quartic_ambient, quartic_ambient) + manual_locus = zero_locus( + direct_sum( + line_bundle(product_ambient, [4, 0]), + line_bundle(product_ambient, [0, 4]), + ), + ) + nonfactorable_filtration = FilteredBundle( + product_ambient, + CompletelyReducibleBundle[ + line_bundle(product_ambient, [1, 0]), + line_bundle(product_ambient, [0, 1]), + ], + ) + @test isnothing( + PartialFlagVarieties._kunneth_decomposition( + restrict(manual_locus, nonfactorable_filtration) + ), + ) + + manual_ambient_bundle = direct_sum( + direct_sum( + structure_sheaf(product_ambient), + line_bundle(product_ambient, [1, 0]), + ), + line_bundle(product_ambient, [0, 1]), + ) + manual_bundle = restrict(manual_locus, manual_ambient_bundle) + @test manual_locus == product(quartic, quartic) + @test cohomology(manual_bundle).entries == AffineExpr.([9, 0, 10, 0, 1]) end # The zero locus of a section of O(1) on the Cayley plane OP² = E6/P1