Skip to content

Rewrite det(inv(X)) → 1/det(X)#2102

Open
alessandrogentili001 wants to merge 2 commits intopymc-devs:mainfrom
alessandrogentili001:rewrite-determinant-of-inverse-matrix
Open

Rewrite det(inv(X)) → 1/det(X)#2102
alessandrogentili001 wants to merge 2 commits intopymc-devs:mainfrom
alessandrogentili001:rewrite-determinant-of-inverse-matrix

Conversation

@alessandrogentili001
Copy link
Copy Markdown
Contributor

Description

Related Issue

Checklist

Type of change

  • New feature / enhancement
  • Bug fix
  • Documentation
  • Maintenance
  • Other (please specify):

@register_stabilize
@register_specialize
@node_rewriter([Elemwise])
def local_reciprocal_linalg_special_cases(fgraph, node):
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.

These aren't related to linalg, the name is wrong

Each of these should be a separate rewrite, and we should be using the new scalar Op properties. We have monotonic_increasing and monotonic_decreasing, which imply sign(f(x)) -> sign(x) and sign(f(x)) -> sign(-x) if the op is also zero_preserving.

We just have to be careful about strict montonicity vs non-strict. I can't remember if ceil(x) is marked as monotonic for example. We might need a separate flag for the strict variety and check it in this rewrite.

def det_of_inv(fgraph, node):
"""Replace det(matrix_inverse(X)) with reciprocal(det(X)).

Since det(inv(X)) = 1/det(X), we avoid computing the inverse.
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.

Suggested change
Since det(inv(X)) = 1/det(X), we avoid computing the inverse.

@alessandrogentili001
Copy link
Copy Markdown
Contributor Author

Hi jesse, thanks for the feedback! I've refactored the implementation to address your points regarding modularity and property-based rewriting.

  1. Decoupling Linalg from Math: I've moved the non-linalg rewrites (like log(reciprocal(x))) out of pytensor/tensor/rewriting/linalg/summary.py and into pytensor/tensor/rewriting/math.py.
  2. Property-Based sign(f(x)) Rewrite: I implemented a generic local_sign_of_monotonic rewriter in math.py. Instead of hardcoding special cases, it now queries ScalarOp properties.
  3. Strict Monotonicity Flags: To handle the edge cases you mentioned (like ceil), I've added strictly_monotonic_increasing and strictly_monotonic_decreasing flags to the UnaryScalarOp base class and applied them to all relevant strictly monotonic Ops across basic.py and math.py (including Log, Exp, Tanh, Sigmoid, Erf, etc.). The sign rewrite now specifically checks for strict monotonicity to ensure correctness.
  4. Atomic Rewrites: I've separated the logic into atomic rewriters (local_log_reciprocal, local_sign_reciprocal, and local_sign_of_monotonic) and registered them across canonicalize, stabilize, and specialize to ensure they trigger reliably.
  5. Docstring Cleanup: Simplified the det_of_inv docstring as suggested.
  6. Expanded Testing: Added comprehensive tests in tests/tensor/rewriting/test_math.py to verify these generic rewrites, including a negative test case for ceil to confirm it is not incorrectly optimized.

Let me know if you have any further suggestions!

Comment thread pytensor/scalar/math.py
class Erf(UnaryScalarOp):
preserves_zero = True
monotonic_increasing = True
strictly_monotonic_increasing = True
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 love this, what cases disagree right now between the two?

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.

ceil and floor, for example

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 mark those instead?

Copy link
Copy Markdown
Member

@ricardoV94 ricardoV94 May 2, 2026

Choose a reason for hiding this comment

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

Also I don't get it, any discrete input version to these ops is also not strictly monotonic.

nvm

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 mark those instead?

I'm not against that, but then we should use the strictly_ language everywhere (drop the shorter one) to be clear what is going on

Copy link
Copy Markdown
Member

@ricardoV94 ricardoV94 May 2, 2026

Choose a reason for hiding this comment

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

Circling back to the sign thing, if that's the motivation, I don't you can apply it based on monotonicity, strict or not. sign(exp(x)) is obviously not sign(x).

We are adding these properties for specific uses, not for mathematical idealism, so they need not be verbose nor geberalized besides the problems we want to solve with them. sctrict vs non strict is more a question of invertible 1-1 map not the direction. wouldn't a combination ot those 2 poperties + zero preserving be a better way to achieve the goal?

Copy link
Copy Markdown
Member

@jessegrabowski jessegrabowski May 2, 2026

Choose a reason for hiding this comment

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

exp isn't zero preserving, so the rule doesn't apply. Both things are important. It has to be strictly monotonic increasing and zero preserving. I think taking strict monotonicity as our canonical form is nice, because who cares about ceil/floor anyway. But I also think it's important to be clear in language, otherwise someone can come along in a few years and say "Well technicaly BitwiseInverse is monotonic_increasing, why isn't it marked" and the answer is "because we define monotonicity as strict monotonicity but it isn't written anywhere". We lose nothing by just writing it.

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.

Yeah I know sign thing doesn't apply to zero, did I say so?

Otherwise okay, we can go with verbose, don't love it but it's strictly more precise.

Can we stop there and not at strictly_monotonic_increasing_over_defined_domain?

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 was reacting to this: sign(exp(x)) is obviously not sign(x).

Maybe i misunderstood your point.

I'm not being dogmatic about the name change, if we want to just define monotonic to mean "strictly monotonic" and put it in the docs somewhere, I have no objection. I just want it to be written down, and I like self-documenting code. Agreed that there is a limit.

@register_specialize
@node_rewriter([log])
def local_log_reciprocal(fgraph, node):
"""Rewrite log(reciprocal(x)) -> -log(x)."""
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.

we should do the more general as well (reciprocal is fine): log(a/b), where a or b is a non-negative constant -> log(a) - log(b) (the constant constant-folded already).

@register_specialize
@node_rewriter([sign])
def local_sign_reciprocal(fgraph, node):
"""Rewrite sign(reciprocal(x)) -> sign(x)."""
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.

same here sign a/b, where one is a positive constant -> sign of the other term. If the constant is negative, 1-sign of the other. If it's mixed, can't do anything

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

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Rewrite det(inv(X)) → 1/det(X)

3 participants