Skip to content
12 changes: 12 additions & 0 deletions docs/src/api/zero_loci.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 2 additions & 0 deletions src/ExternalProducts.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
30 changes: 16 additions & 14 deletions src/FilteredBundle.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
# ═══════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -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

# ═══════════════════════════════════════════════════════════════════════════════
Expand Down Expand Up @@ -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

"""
Expand Down
227 changes: 227 additions & 0 deletions src/Kunneth.jl
Original file line number Diff line number Diff line change
@@ -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
1 change: 1 addition & 0 deletions src/PartialFlagVarieties.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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")
Expand Down
11 changes: 6 additions & 5 deletions src/ZeroLoci.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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]))
Expand All @@ -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

"""
Expand Down
Loading
Loading