Zulip Chat Archive
Stream: mathlib4
Topic: Universe-heterogeneous ordinal and cardinal relations
Violeta Hernández (Jan 23 2025 at 06:05):
Here's something I've brought up earlier, but which I think we didn't really end up discussing much. Would it be desirable to introduce universe-heterogeneous versions of =
, ≤
, and <
on ordinals and cardinals? e.g. x =l y
would be defined as lift x = lift y
.
Violeta Hernández (Jan 23 2025 at 06:06):
I think such a predicate would make working with stuff from different universes much easier. It gets rid of a particular pain point, which is that there's two possible ways to write a lift to a higher universe lift.{v, u}
and lift.{max u v, u}
, and simp
can't rewrite them because it can't rewrite universes.
Violeta Hernández (Jan 23 2025 at 06:09):
(I previously suggested this on the docs#HasCardinalLT PR, but we decided that this should be discussed separately and independently of it)
Violeta Hernández (Jan 23 2025 at 06:26):
Here's a really basic sketch of how this would look like
import Mathlib.SetTheory.Ordinal.Basic
universe u v
class HasLift (α : Type u) (β : Type v) [LinearOrder α] [LinearOrder β] where
Common : Type (max u v + 1)
linearOrder_Common : LinearOrder Common := by infer_instance
lift_α : α <i Common
lift_β : β <i Common
variable {α : Type u} {β : Type v} [LinearOrder α] [LinearOrder β] [HasLift α β]
def lift_eq (x : α) (y : β) : Prop :=
HasLift.lift_α x = HasLift.lift_β y
infix:50 " =l " => lift_eq
noncomputable instance : HasLift Ordinal.{u} Ordinal.{v} where
Common := Ordinal.{max u v + 1}
lift_α := Ordinal.liftPrincipalSeg
lift_β := Ordinal.liftPrincipalSeg
Violeta Hernández (Jan 23 2025 at 06:36):
Actually, this might be a simpler approach:
import Mathlib.Order.InitialSeg
universe u v
variable {α : Type u} {β : Type v} [LinearOrder α] [LinearOrder β]
inductive lift_eq (x : α) (y : β) : Prop where
| intro (γ : Type (max u v)) [LinearOrder γ] (f : α ≤i γ) (g : β ≤i γ) (h : f x = g y) : lift_eq x y
Violeta Hernández (Jan 23 2025 at 06:40):
Basically, two elements from two different well-orders can be considered "equivalent" when they're the same after being mapped by initial segments into a larger well-order containing both.
Violeta Hernández (Jan 23 2025 at 06:42):
The choice of initial segments and of the larger well-order should be mostly irrelevant. Recall that the type of initial segments between two well-orders is a subsingleton.
Kevin Buzzard (Jan 23 2025 at 07:41):
Is there an actual application for this?
Violeta Hernández (Jan 23 2025 at 07:42):
Lots! You can find instances of lift x = lift y
and the like all over any part of Mathlib depending on cardinals/ordinals.
Violeta Hernández (Jan 23 2025 at 07:43):
Most notably, lift #X = lift #Y
iff Nonempty (X ≃ Y)
.
Violeta Hernández (Jan 23 2025 at 08:50):
I've put a more refined version of my design up at #20980
Violeta Hernández (Jan 23 2025 at 09:42):
I've added what I think is all of the basic API for these predicates. The PR should be ready for review.
Violeta Hernández (Jan 23 2025 at 14:40):
Maybe @Eric Wieser would be interested in this, iirc we've discussed how to deal with lift
lemmas in the past.
Junyan Xu (Jan 23 2025 at 21:28):
I can attest that lift = lift
lemmas are very common in mathlib; I just wrote a few for transcendence degrees, and I recall @Jz Pan also wrote a lot about Module.rank and Cardinal.mk.
Aaron Liu (Jan 23 2025 at 21:31):
This would be amazing!
Junyan Xu (Jan 23 2025 at 21:32):
It's definitely good to be able to automate this, and if I understand correctly Violeta's PR does this without introducing new tactics.
Eric Wieser (Jan 23 2025 at 21:34):
For cardinals, is the definition above any different to using lift.{max u v}
always?
Violeta Hernández (Jan 23 2025 at 21:39):
I think I didn't properly explain what my final design was. Basically, I introduce a new predicate liftEQ x y
, which takes in arguments from two well-orders α
and β
(which might be in different universes!), and is (morally) defined as "for every well-order γ
and initial segment embeddings f : α ≤i γ
and g : β ≤i γ
, we have f x = g y
". This predicate is equivalent to lift (typein (· < ·) x) = lift (typein (· < ·) y)
, but since this file is meant to be used before ordinals are defined, we can't define it this way.
In the case where α
and β
are both Cardinal.{u}
or Ordinal.{u}
for different universes u
, this predicate is also equivalent to lift x = lift y
. This is something I'll prove in a follow-up PR.
The advantage of using this predicate is that we don't have to grapple with all possible ways of writing lift x = lift y
. For instance, consider the statement "if lift x = lift y
and lift y = lift z
, then lift x = lift z
". In order to prove this in current Mathlib, we'd first have to rewrite all lifts to a common universe max u v w
, then apply transitivity, then rewrite the lifts to our target universe. But with this design, this is just liftEQ_trans
.
Violeta Hernández (Jan 23 2025 at 21:42):
This new predicate also interfaces nicely with simp
. We have liftEQ (lift x) y ↔ liftEQ x y
and liftEQ x (lift y) ↔ liftEQ x y
. Likewise, if x
and y
come from the same type (i.e. the same universe), we have liftEQ x y ↔ x = y
.
Eric Wieser (Jan 23 2025 at 21:53):
What I'm asking is why not instead use
def LiftRel (r) (a : Cardinal.{u}) (b : Cardinal.{v}) := r (lift.{v} a) (lift.{u} b)
Eric Wieser (Jan 23 2025 at 21:54):
I understand the benefit of having a name to write API around, I don't see so much the benefit of building it using initial segments
Junyan Xu (Jan 23 2025 at 21:54):
Eric Wieser said:
For cardinals, is the definition above any different to using
lift.{max u v}
always?
I think lift.{v} cu = lift.{u} cv
is the better spelling and if there is any lift.{max u v}
left in the mathlib they should be replaced (unfortunately Cardinal.lift_umax doesn't work as a simp lemma according to the docstring). The PR probably simplifies (or has the potential to simplify) long rewrites like this.
In addition to Eq, LE, LT, it would also be nice to make the other operations like Add and Mul universe polymorphic too. I'm not sure what framework it fits in though. Maybe we need some elaborator to automatically insert lift
s in the statements and tactic to insert them as proofs proceed and remove(cancel) them when possible. Maybe just design these specifically for Cardinal since it will be the main use case (as Eric points out).
Junyan Xu (Jan 23 2025 at 22:00):
A possible challenge problem for such elaborator/tactic:
import Mathlib
universe u v w x
variable {c : Cardinal.{u}} {d : Cardinal.{v}} {e : Cardinal.{w}} {f : Cardinal.{x}}
open Cardinal in
example (h₁ : lift.{v} c ≤ lift.{u} d) (h₂ : lift.{x} e ≤ lift.{w} f) :
lift.{max w x} (lift.{v} c * lift.{u} d) ≤ lift.{max u v} (lift.{x} e * lift.{w} f) := sorry
It's a pain to even write down the statement.
Eric Wieser (Jan 23 2025 at 22:02):
This is a nice example of where LiftLE
wouldn't really help
Eric Wieser (Jan 23 2025 at 22:04):
Junyan Xu said:
it would also be nice to make the other operations like Add and Mul universe polymorphic too
Note that it's messy to write Monoid
in this framework, because it would need 3 universe parameters for associativity, but only two for mul_one
. Maybe the "multiple parameters" rule for typeclasses only applies to term parameters not universe parameters though.
Eric Wieser (Jan 23 2025 at 22:05):
Another possible way out; refine lift
as def lift.{v, u} [UnivLE.{u, v}] (x : Cardinal.{u}) : Cardinal.{v}
Eric Wieser (Jan 23 2025 at 22:05):
And then the convention becomes "when writing theorems, pick one universe and use it for every lift
"
Junyan Xu (Jan 23 2025 at 22:10):
Yeah I also had the same idea. UnivLE/Small isn't data carrying though so this lift
can't be computable. If you make them data carrying I think there are diamonds. Some of mathlib may rely on the defeq that lift
is Quotient.map
of ULift
.
Violeta Hernández (Jan 23 2025 at 22:10):
Junyan Xu said:
A possible challenge problem for such elaborator/tactic:
import Mathlib universe u v w x variable {c : Cardinal.{u}} {d : Cardinal.{v}} {e : Cardinal.{w}} {f : Cardinal.{x}} open Cardinal in example (h₁ : lift.{v} c ≤ lift.{u} d) (h₂ : lift.{x} e ≤ lift.{w} f) : lift.{max w x} (lift.{v} c * lift.{u} d) ≤ lift.{max u v} (lift.{x} e * lift.{w} f) := sorry
It's a pain to even write down the statement.
We do have simp
lemmas for e.g. lift (x * y) = lift x * lift y
, so this shouldn't be *nearly" as bad.
Violeta Hernández (Jan 23 2025 at 22:17):
Eric Wieser said:
Another possible way out; refine
lift
asdef lift.{v, u} [UnivLE.{u, v}] (x : Cardinal.{u}) : Cardinal.{v}
This doesn't seem very different from the approach of using lift.{max u v}
throughout, except that we now have an extra typeclass argument to worry about...
Violeta Hernández (Jan 23 2025 at 22:19):
I do think the convention of writing lift.{max u v, u}
is better than our current one of writing lift.{v, u}
, though it still leads to some annoyance in e.g. the transitivity example.
Aaron Liu (Jan 23 2025 at 22:22):
We have push_cast
and norm_cast
, would it be helpful if someone wrote a push_lift
and norm_lift
tactic?
Violeta Hernández (Jan 23 2025 at 22:22):
What exactly would these tactics do?
Aaron Liu (Jan 23 2025 at 22:28):
It would move lift
s around through expressions. For example, push_cast
turns Nat.cast (m + n)
into Nat.cast m + Nat.cast n
, so push_lift
could turn Ordinal.lift (m + n)
into Ordinal.lift m + Ordinal.lift n
.
Aaron Liu (Jan 23 2025 at 22:29):
Actually, is it possible to add lemmas to push_cast
and norm_cast
?
Eric Wieser (Jan 23 2025 at 22:30):
I don't think you can reuse push_cast here, because probably isn't doesn't care for universes
Eric Wieser (Jan 23 2025 at 22:30):
Also in the example above, I think you'd need push_lift at h1 h2 |-
, which would move all the equalities to the same universe
Eric Wieser (Jan 23 2025 at 22:31):
(perhaps more importantly, I don't think push_cast
works on inequalities)
Violeta Hernández (Jan 23 2025 at 22:33):
A tactic that changes all lifts
to a common universe, such as the maximum of all universes that appear in the hypothesis and statement, would definitely alleviate most of the lift
hell.
Violeta Hernández (Jan 23 2025 at 22:34):
But imo my liftEQ
implementation can already provide very similar functionality without having to deal with automation
Violeta Hernández (Jan 23 2025 at 22:36):
There's another thing to consider: unless we add the UnivLE
hypothesis to lift
(which comes with other issues as we've discussed), we'd have to manually enforce the proper lift
convention. Again, the liftEQ
predicate sidesteps this issue by providing a single canonical form.
Eric Wieser (Jan 23 2025 at 22:37):
Here's a solution to @Junyan Xu's challenge:
import Mathlib
universe u v w x
variable {c : Cardinal.{u}} {d : Cardinal.{v}} {e : Cardinal.{w}} {f : Cardinal.{x}}
open Cardinal
attribute [coe] Cardinal.lift
attribute [norm_cast] Cardinal.lift_mul Cardinal.lift_lift
example (h₁ : lift.{v} c ≤ lift.{u} d) (h₂ : lift.{x} e ≤ lift.{w} f) :
lift.{max v x} (lift.{w} c * lift.{u} e) ≤ lift.{max u w} (lift.{x} d * lift.{v} f) := by
rw [← lift_le.{max u v w x}] at *
push_cast at *
refine mul_le_mul' h₁ h₂
Eric Wieser (Jan 23 2025 at 22:39):
(I think the original statement was wrong, so I fixed it)
Eric Wieser (Jan 23 2025 at 22:49):
Having copies of norm_cast that use a different attribute than coe
would certainly help a lot here
Eric Wieser (Jan 23 2025 at 22:50):
Unfortunately norm_cast is in core, so it's going to be annoting to protype that generalization
Violeta Hernández (Jan 23 2025 at 22:52):
So what does this mean for us in the present? Should I close my PR in favor of waiting for a fully-fledged push_lift
tactic? Should we at least change the way we write lift
equalities?
Violeta Hernández (Jan 23 2025 at 22:57):
Ideally, we could even automate the initial step of "lift everything to a common universe". But I don't know if that's a reasonable expectation.
Violeta Hernández (Jan 23 2025 at 23:02):
Actually, the norm_lift
tactic might not even be needed. We could just make our own simp
set:
import Mathlib
universe u v w x
variable {c : Cardinal.{u}} {d : Cardinal.{v}} {e : Cardinal.{w}} {f : Cardinal.{x}}
open Cardinal
example (h₁ : lift.{v} c ≤ lift.{u} d) (h₂ : lift.{x} e ≤ lift.{w} f) :
lift.{max v x} (lift.{w} c * lift.{u} e) ≤ lift.{max u w} (lift.{x} d * lift.{v} f) := by
rw [← lift_le.{max u v w x}] at *
simpa only [lift_lift, lift_mul] using mul_le_mul' h₁ h₂
Eric Wieser (Jan 23 2025 at 23:03):
The benefit of a custom tactic would be that it cleans up the goal state to not be a big pile of max
s
Eric Wieser (Jan 23 2025 at 23:05):
I guess "put everything in the same level" is not that far off from "lift everything to the smallest of Nat / Int / NNRat / Rat / NNReal / Real"
Violeta Hernández (Jan 23 2025 at 23:07):
Eric Wieser said:
The benefit of a custom tactic would be that it cleans up the goal state to not be a big pile of
max
s
I'm not sure if that's avoidable. For instance in the example above, we couldn't do something like norm_lift mul_le_mul' h₁ h₂
because the expression wouldn't typecheck before lifting h₁
and h₂
to a common universe.
Eric Wieser (Jan 23 2025 at 23:07):
So I think perhaps what's needed here is a norm_via
tactic written from scratch to solve both problems at once
Eric Wieser (Jan 23 2025 at 23:07):
I'm proposing a tactic that replaces the rw
and push_cast
in my example above
Violeta Hernández (Jan 23 2025 at 23:10):
Idk about you but this seems like a big pile of max
s to me
image.png
Violeta Hernández (Jan 23 2025 at 23:10):
(that's the state after rw
and push_cast
in your code)
Eric Wieser (Jan 23 2025 at 23:11):
Yes, so the tactic would clean all that up too
Violeta Hernández (Jan 23 2025 at 23:11):
How?
Eric Wieser (Jan 23 2025 at 23:44):
Tactic incoming...
Eric Wieser (Jan 23 2025 at 23:57):
import Mathlib
section SanitizeLevels
deriving instance DecidableEq for Lean.LevelMVarId
deriving instance DecidableEq for Lean.Level
deriving instance Ord for Lean.Name
deriving instance Ord for Lean.LevelMVarId
deriving instance Ord for Lean.Level
/-- Associate and sort `max`es within a `Level`. -/
def Lean.Level.normalizeMax (l : Level) : Level :=
addMax (go l)
where
addMax (l : List Level) : Level :=
dbg_trace s!"{l}, {l.dedup.mergeSort (compare · · ≠ .lt)}"
match l.dedup.mergeSort (compare · · ≠ .lt) with
| [] => panic!"impossible"
| [l] => l
| l :: ls => ls.dedup.foldl (Function.swap .max) l
go : Level → List Level
| u@.zero | u@(.mvar _) | u@(.param _) => [u]
| .succ u => [.succ <| addMax (go u)]
| .max u v => go u ++ go v
| .imax u v => [.imax (addMax (go u)) (addMax (go v))]
open Lean Elab Meta Parser Tactic Syntax in
/-- Order every `max` term within level arguments. -/
elab "sanitize_levels" loc: optional(location) : tactic => do
withLocation (Lean.Elab.Tactic.expandOptLocation <| Lean.mkOptionalNode loc)
(atLocal := fun fvar => do
Tactic.liftMetaTactic1 fun goal => do
goal.replaceLocalDeclDefEq fvar <| (← fvar.getType).replaceLevel fun l => l.normalizeMax)
(atTarget := do
Tactic.liftMetaTactic1 fun goal => do
goal.change <| (← goal.getType).replaceLevel fun l => l.normalizeMax)
(failed := fun _ => do throwError "Failed everywhere")
end SanitizeLevels
universe u v w x
variable {c : Cardinal.{u}} {d : Cardinal.{v}} {e : Cardinal.{w}} {f : Cardinal.{x}}
open Cardinal
attribute [coe] Cardinal.lift
attribute [norm_cast] Cardinal.lift_mul Cardinal.lift_lift
example (h₁ : lift.{v} c ≤ lift.{u} d) (h₂ : lift.{x} e ≤ lift.{w} f) :
lift.{max v x} (lift.{w} c * lift.{u} e) ≤ lift.{max u w} (lift.{x} d * lift.{v} f) := by
rw [← lift_le.{max u v w x}] at *; push_cast at *
sanitize_levels at *
sorry
which makes the goal
c : Cardinal.{u}
d : Cardinal.{v}
e : Cardinal.{w}
f : Cardinal.{x}
h₁ : lift.{max u v w x, u} c ≤ lift.{max u v w x, v} d
h₂ : lift.{max u v w x, w} e ≤ lift.{max u v w x, x} f
⊢ lift.{max u v w x, u} c * lift.{max u v w x, w} e ≤ lift.{max u v w x, v} d * lift.{max u v w x, x} f
Eric Wieser (Jan 23 2025 at 23:58):
@Kim Morrison, are those deriving
s inappropriate for global instances?
Eric Wieser (Jan 24 2025 at 00:02):
Actually, I think simp
already does this automatically, but docs#Lean.Level.normalize associates max
the wrong way
Eric Wieser (Jan 24 2025 at 00:06):
Namely, this accumulator is backwards, and should have its arguments swapped
Eric Wieser (Jan 24 2025 at 00:06):
Either than, or the associativity of the max
notation is wrong
Eric Wieser (Jan 24 2025 at 00:17):
Jz Pan (Jan 24 2025 at 15:36):
An example is the cardinality/rank of an MvPolynomial
and its fraction ring. The statement uses various lifts and max's. Do you think a "rw mod lift" tactic is useful?
Eric Wieser (Jan 24 2025 at 15:37):
I think we should get to the place where lift
can be treated in the same way as a cast
. So maybe rw_mod_lift
is useful, but it would be a better use of time to write it simultaneously with rw_mod_cast
Eric Wieser (Jan 28 2025 at 16:17):
Eric Wieser said:
Note this was closed as a duplicate of lean4#5695; please upvote that one instead!
Violeta Hernández (Feb 10 2025 at 06:48):
Any progress of getting these tactics into Mathlib?
Jz Pan (Mar 18 2025 at 06:03):
Maybe it's better if we can have a tactic which automatically generalize the universe homogeneous version of a theorem to universe heterogeneous one, by inserting Cardinal.lift
, etc. automatically to suitable places.
Last updated: May 02 2025 at 03:31 UTC