Skip to content

Compute Hom / Ext over exterior algebra via pushforward - #4477

Open
joel-dodge wants to merge 68 commits into
Macaulay2:developmentfrom
joel-dodge:hom-main
Open

joel-dodge wants to merge 68 commits into
Macaulay2:developmentfrom
joel-dodge:hom-main

Conversation

@joel-dodge

@joel-dodge joel-dodge commented Jul 18, 2026 •

Copy link
Copy Markdown
Contributor

This PR adds a few loosely related changes all centered around the use of the PushForward package to compute Hom and Ext of modules over exterior algebras:

If R is non-commutative, Hom_R(M, N) is not in general an R-module. If f: S -> R is a RingMap, this introduces:

  • Hom(RingMap, *, *) methods to support computing Hom_R(M, N) as an appropriate S-submodule of Hom_S(pushFwd(f, M), pushFwd(f, N)).
    • supports functoriality as well as homomorphism / homomorphism' functionality for this construction
  • Similarly support Ext(RingMap, Module, Module) and auxiliary methods like yonedaExtension', yonedaMap'..

Here is a short list of auxiliary changes that this PR includes:

  • add flattenDegreeMap method that helps improve behavior of pushforward in cases where degreeGroup source f =!= degreeGroup target f.
  • standardize interface for pushFwd
    • introduce pushFwdGens helper method
    • fix up all uses of the previous interface
  • introduce optimization to reduce computation of pushFwd of a free module to the rank one untwisted case
    • exploit direct sum structure when computing pushFwd modules
  • update option from NoPrune -> MinimalGenerators

2026-11-01 edit:

  • added a guard in the core Hom.m2 against using Hom(M, N) for modules over a skew-commutative ring
    • a priori the construction ought to be nonsense for non-commutative rings since it produces an R-module and Hom_R(M, N) does not in general enjoy an R-module structure
    • using the pushforward Hom construction to compare with I have satisfied my curiousity that the Hom construction used for the commutative case does not work
    • it typically produces matrices that do NOT give left R-module homomorphisms
    • in many cases Hom(M, M) does not contain the identity map
    • comparing Hom(f, M, N) with pushFwd(f, Hom(M, N)) shows that the latter can be either too big or too small (in terms of dim over kk).

@joel-dodge
joel-dodge changed the base branch from stable to development July 18, 2026 23:37
@d-torrance d-torrance added Core Issues involving the Core scripts. update to existing package(s) labels Jul 19, 2026

@mahrud mahrud left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is still a draft, but noticed the changes to Hom.m2 and wanted to share some early thoughts in case that's helpful at all.

Comment thread M2/Macaulay2/m2/Hom.m2 Outdated
})
DegreeLimit => null,
MinimalGenerators => true,
Strategy => null

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Don't think the indentation needs to change, and the trailing comma is harmless, and in this case intentional in order to minimize git line changes when adding a new option.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Makes sense. I added the comma back and moved the closing }} to be at the same indentaiton level as the options as it was before. The only other change here is turn some tabs into spaces which I think is a virtuous change to make.

Comment thread M2/Macaulay2/m2/Hom.m2 Outdated
H.cache.homomorphism = f -> map(N, M, adjoint'(f, M, N), Degree => first degrees source f + degree f);
H.cache.formation = FunctionApplication { Hom, (M, N, DegreeLimit => e) };
H)
Y#(Hom, M, N, e) ?? (

@mahrud mahrud Oct 26, 2026 •

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm a fan of f(x) := x -> x.cache.blah ??= (...) for readability, but it's good to be aware that this code is not equivalent to the previous version: your new version attempts to reach Y#(Hom, M, N, e), but an internal error is returned, so then the remainder is computed, whereas previously an internal error wasn't generated. Addition of the extra indentation makes me slightly prefer rolling back this change.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Interesting. I think that were I to rewrite this now I would replace L41 with

H = if opts.MinimalGenerators then trim H else H

and make this L36 into an??=

If I understand your explanation then I think this would make the proposed code equivalent to the original. The diversion was unintentional but I can't recall what I was thinking when this was written.

In light of this, you still prefer to keep it as it was and lose the extra indentation?

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Huh I think there's actually been a git-merge bug with trim and trim' here and the intention was something like this:

    trim' := if opts.MinimalGenerators then trim else identity;
    if Y#?(Hom, M, N, e) then return trim' Y#(Hom, M, N, e);
    ...
    H = Y#(Hom, M, N, e) = ...;
    H.cache.homomorphism = ...;
    H.cache.formation = ...;
    trim' H)

(note that trim' is not used at all currently, but should be used twice).

To be honest, in a perfect world there would be no MinimalGenerators option here, we would not trim Hom (because it can be quite costly), but the user can later run trim Hom(M,N) and it would preserve the homomorphism information, which currently is not kept.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Oh interestig. It looks like even in the current development branch that trim' is defined but not used at all. Maybe there was a merge bug at some point in the part?

Indeed I see that you are suggesting that the intention was to always store the untrimmed module in the cache (the cache key does not depend on this so that makes sense) and for trimming to be applied always just at the point that the module is returned. I think that your code would not work since trimming at the edge like that makes the homomorphism method in the cache unfindable by methods that need it (e.g. homomorphism(Matrix)). This could be addressed by making trim more aware of preserving cached structure in modules that it is trimming - a similar issue appears for all constructions like Hom, DirectSum, pushFwd, etc. but that seems like a big change that includes some m2 level philosophical concerns to me.

It feels to me like actually the Hom computation ought to be cached based on all of the options not just the degree limit and that if that change were made that the definition of trim' ought to just be removed here.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just pushed a couple commits to clean these things up. lmk what you think.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ya in this commit you can see that the use of trim' was dropped in favor of just in-lining the logic: e0ffe77 it was the PR containing this commit itself that introduced trim' though so hard to tell what was a merge error and what was just temporary draft work.

map(Module,RingMap) := Matrix => o -> (M,p) -> map(M,,p,map(M,cover M,1),o)

flattenDegreeMap = method()
flattenDegreeMap(RingMap) := (f) -> (

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to think about this more, but can we not always cache the degree map in the ring map given by flattenRing? Seems simpler to me.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I am not sure if I understand this comment.

My prototypical use case for this method is

i1 : R = kk[a..d, SkewCommutative => true];
i2 : f = flattenDegreeMap map(R, kk)
o2 = map (R, kk[], {})
o2 : RingMap R <-- kk[]
i3 : degreeGroup source f
       1
o3 = ZZ
o3 : ZZ-module, free

and I don't think I see how to apply your remarks to this situation.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think I'm confused about the purpose of this function based on its name. This is what I thought you meant:

S := kk[...];
R := S[...]/...;
(R', f) := flattenRing R;
f.cache.DegreeMap -- I thought flattenDegreeMap gives this

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I think that the comment on L602-606 describes what this construction is doing pretty clearly.

I was not especially happy with the name flattenDegreeMap so would be very open to changing it. I will think about this again and see what else I can come up with. Happy to entertain suggestions as well if the below description helps and something jumps out to you.

Y#(Ext,i,M,N) ??= (
R := ring M;
if not isCommutative R then error "'Ext' not implemented yet for noncommutative rings.";
-- if not isCommutative R then error "'Ext' not implemented yet for noncommutative rings.";

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I know this is still a draft, but eventually this should be removed and new checks should be added (e.g. for WeylAlgebras and potentially other types of rings which may support complexes)

AuxiliaryFiles => true
AuxiliaryFiles => true,
PackageImports => {
"Complexes" -- compute resolution for Ext

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Would it make more sense to add a new package ExteriorAlgebras which depends on both PushForward and Complexes, and adds new pushforward hooks for exterior algebras? (and perhaps move other exterior algebra related functionality there?)

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That is a great suggestion. Let me chew on it a bit but I will probably do exactly that.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unpacking this suggestion, I think that you are suggesting to hookify the pushFwd methods and refactor this package to enable capturing the exterior algebra specific logic in hooks that are controlled by a new ExteriorAlgebra package. It is not clear to me what the advantage is over the current shape of things though. It doesn't seem to me like specializing to the commutative case or specializing to the skew commutative case would provide any obvious performance advantages. Do you have intuition that suggests otherwise?

@joel-dodge
joel-dodge marked this pull request as ready for review October 29, 2026 17:54
@joel-dodge joel-dodge changed the title [Draft] Compute Hom / Ext via pushforward Compute Hom / Ext over exterior algebra via pushforward Oct 29, 2026
@joel-dodge

joel-dodge commented Nov 2, 2026 •

Copy link
Copy Markdown
Contributor Author

I just added the following edit to the PR description

2026-11-01 edit:

  • added a guard in the core Hom.m2 against using Hom(M, N) for modules over a skew-commutative ring
    • a priori the construction ought to be nonsense for non-commutative rings since it produces an R-module and Hom_R(M, N) does not in general enjoy an R-module structure
    • using the pushforward Hom construction to compare with I have satisfied my curiousity that the Hom construction used for the commutative case does not work
    • it typically produces matrices that do NOT give left R-module homomorphisms
    • in many cases Hom(M, M) does not contain the identity map
    • comparing Hom(f, M, N) with pushFwd(f, Hom(M, N)) shows that the latter can be either too big or too small (in terms of dim over kk).

@d-torrance do you have any suggestion for what the right way to persist the output of an investigation like this in a way which would be useful for anyone in the future who wants to understand these different hom constructions?

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

Core Issues involving the Core scripts. update to existing package(s)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants