Zulip Chat Archive

Stream: mathlib4

Topic: tactic for polynomial equality


Arend Mellendijk (Feb 25 2025 at 17:10):

It's currently quite annoying to prove equality of polynomials using ring, because ring isn't aware that Polynomial.C is a homomorphism:

example (a : ) : (.C a + Polynomial.X)^2 = .X ^ 2 + .C (2 * a) * .X + .C (a^2) := by
  ring
  /-
  a : ℚ
  ⊢ Polynomial.C a * Polynomial.X * 2 + Polynomial.C a ^ 2 + Polynomial.X ^ 2 =
    Polynomial.X * Polynomial.C (a * 2) + Polynomial.X ^ 2 + Polynomial.C (a ^ 2)
  -/
  sorry

I have a prototype tactic for proving equations of this form, granted that the exponents are explicit natural numbers.

example (a : ) : (.C a + Polynomial.X)^2 = .X ^ 2 + .C (2 * a) * .X + .C (a^2) := by
  poly
  /-
  a : ℚ
  ⊢ a + a = 2 * a
  a : ℚ
  ⊢ a * a = a ^ 2
  -/

It works quite like ring in that it puts both polynomials into a normal form, except this tactic can then equate the coefficients in the base ring and produce side goals for the user.

Is there interest for a tactic like this in mathlib? This is my first serious metaprogramming project. It's quite close to being usable, but cleaning up the code would take some time.

Kevin Buzzard (Feb 25 2025 at 18:41):

Why not teach the tactic that C is a ring homomorphism and have it work for all commutative rings?

Johan Commelin (Feb 26 2025 at 04:59):

It is quite ironic that ring uses (its own version of) polynomials under the hood, but is then actually not so good at proving equalities between Polynomials :rofl:

Arend Mellendijk (Feb 26 2025 at 10:39):

Kevin Buzzard said:

Why not teach the tactic that C is a ring homomorphism and have it work for all commutative rings?

The reason I chose polynomials is that there is a very clear normal form - group the coefficients by .X^n and move all of them into a single .C. In a more general setting it's not obvious to me (a) what to group by and (b) how to normalise the coefficients.

Arend Mellendijk (Feb 26 2025 at 10:42):

Johan Commelin said:

It is quite ironic that ring uses (its own version of) polynomials under the hood, but is then actually not so good at proving equalities between Polynomials :rofl:

I'll point out ring uses rational polynomials with explicit constants under the hood, so dealing with coefficients in an arbitrary ring is not so obvious!

Yaël Dillies (Feb 26 2025 at 10:51):

If we were using a • X ^ n (like quite literally every other algebra) instead of C a * X ^ n, we could simply use module and module_nf

Yaël Dillies (Feb 26 2025 at 10:53):

I guess you could create a tactic that does the following:

  • Turn C a * into a •
  • Run module or module_nf accordingly
  • In the normal form version, turn back a • into C a *

Yaël Dillies (Feb 26 2025 at 10:54):

But I would much rather we drop the Polynomial/MvPolynomial particularism of using C a instead of and algebraMap

Arend Mellendijk (Feb 26 2025 at 11:19):

I'm not familiar with module, but it doesn't seem to play very nice with polynomials:

example (a b: ) : (b  1 + a  X) * (b  1 + a  X)
  = ((b^2)  1) + ((2*a*b)  X) + ((a^2)  (X : Polynomial )^2) := by
  match_scalars
  /-
  a b : ℚ
  ⊢ 1 = 0
  a b : ℚ
  ⊢ 0 = b ^ 2 * 1
  a b : ℚ
  ⊢ 0 = 2 * a * b * 1
  a b : ℚ
  ⊢ 0 = a ^ 2 * 1
  -/
  repeat sorry

Riccardo Brasca (Feb 26 2025 at 11:22):

It doesn't play very nice with multiplication I think. If you replace all the * by (by hand), it should work.

Arend Mellendijk (Feb 26 2025 at 11:24):

Not quite...

import Mathlib
open Polynomial

example (a b: ) : (b  (1 : Polynomial ) + a  X)  (b  (1 : Polynomial ) + a  X)
  = ((b^2)  1) + ((2*a*b)  X) + ((a^2)  (X : Polynomial )^2) := by
  match_scalars
  /-
  a b : ℚ
  ⊢ (b • 1 + a • X) * (↑b * 1) = ↑b ^ 2 * 1
  a b : ℚ
  ⊢ (b • 1 + a • X) * (↑a * 1) = ↑2 * ↑a * ↑b * 1
  a b : ℚ
  ⊢ 0 = ↑a ^ 2 * 1
  -/
  repeat sorry

Riccardo Brasca (Feb 26 2025 at 11:30):

Well, you mix various module structure, the first is external multiplication by R on R[X], but the third one is just multiplication in R[X]

Arend Mellendijk (Feb 26 2025 at 11:32):

Right, but if we want a tactic that can solve polynomial equations we surely want it to handle multiplication inR[X]!

Riccardo Brasca (Feb 26 2025 at 11:35):

Yes of course. I am just saying that the tactic should do something before calling module.

Damiano Testa (Feb 26 2025 at 11:35):

I think that you should probably first normalize to get as close as possible to each individual summand being of the form constant * X ^ n and then replace the *.

Riccardo Brasca (Feb 26 2025 at 11:36):

As in

example (a b: ) : (b  (1 : Polynomial ) + a  X)  (b  (1 : Polynomial ) + a  X)
  = ((b^2)  1) + ((2*a*b)  X) + ((a^2)  (X : Polynomial )^2) := by
  rw [smul_eq_mul, add_mul, mul_add, mul_add, smul_mul_smul_comm, smul_mul_smul_comm,
    smul_mul_smul_comm, smul_mul_smul_comm]
  simp only [ pow_two, one_mul, mul_one, one_pow]
  module

Arend Mellendijk (Feb 26 2025 at 11:38):

Damiano Testa said:

I think that you should probably first normalize to get as close as possible to each individual summand being of the form constant * X ^ n and then replace the *.

To be clear, this is essentially what my prototype tactic does. Perhaps I'm duplicating work by matching the coefficients manually rather than calling module or match_scalars

Damiano Testa (Feb 26 2025 at 11:40):

If you can piggy-back on the existing module tactic, then your add-on only needs to take care of "expanding and merging powers", which means simpler and more robust implementation.

Yaël Dillies (Feb 26 2025 at 11:45):

It sounds like you want to write an algebra tactic that replaces algebraMap a * b and b * algebraMap a with a • b, algebraMap a with a • 1, then run module

Arend Mellendijk (Feb 26 2025 at 11:50):

I disagree - the example above is written entirely in terms of and module fails to solve it as it doesn't handle multiplication in the algebra itself.

Yaël Dillies (Feb 26 2025 at 11:52):

Isn't that quite precisely what I said?

Yaël Dillies (Feb 26 2025 at 11:52):

My point is that your tactic shouldn't be Polynomial-specific

Damiano Testa (Feb 26 2025 at 11:56):

The tactic should also be aware of distributing multiplications over additions.

Arend Mellendijk (Feb 26 2025 at 11:57):

and normalizing terms on the RHS of an smul: match_scalars views X*X and X^2 as different things

Damiano Testa (Feb 26 2025 at 16:10):

If you end up outsourcing coefficient matching to match_scalars, it would be nice if you could also get match_scalars to label each goal with the name of the coefficient being matched.

Damiano Testa (Feb 26 2025 at 16:11):

So, the goal's list when applying it to a polynomial would tell you from which degree each side goals comes.

Arend Mellendijk (Feb 26 2025 at 17:48):

Thinking about ringhoms in terms of smul has carified things for me:

The reason module breaks for an R-algebra A is that it is completely oblivious to multiplication: . a • (x*y) won't match a • (y*x). To make it work we'd have to (a) distribute out all of the multiplications in A and (b) for every "product of atoms" we reorder the product into a canonical ordering (e.g. x*y*x => x^2*y).

Now I could write a tactic that first uses simp to distribute out all the products and exponents, then goes through the resulting expression, normalising all of the subterms of type A that are products of atoms. I worry that this approach is brittle. It also comes with an inherent performance penalty: (a+b)^8 gets expanded into 256 terms by simp

Another option is to essentially extend module/ring and parse the entire expression into a normal form. That would be a sum of r_i • a_i where each a_i is a normalised product of atoms. At that point there isn't much reason to call out to module - we already have the coefficients in a neat list. If we keep the r_i in ring normal form, this would also solve the exponential blowup problem mentioned before, but that would be a fair bit of work.

I'm not sure yet which way is best, but the second option seems more fruitful.


Last updated: May 02 2025 at 03:31 UTC