Zulip Chat Archive

Stream: mathlib4

Topic: Extending distance of graphs


Rida Hamadani (Jun 18 2024 at 07:32):

It turns out that it is useful to have an extended version of docs#SimpleGraph.dist, so that it returns when the graph is disconnected (see this to see why). However, I'm not sure what's the best way to define it, does this seem reasonable?

import Mathlib.Combinatorics.SimpleGraph.Metric
import Mathlib.Data.ENat.Lattice
variable {V : Type*} (G : SimpleGraph V)

noncomputable def edist [Decidable G.Connected] (u v : V) :  :=
  if G.Connected then G.dist u v else 

I'm worried about this definition requiring [Decidable G.Connected]. I wonder if there is a way to do this without needing it.

Yaël Dillies (Jun 18 2024 at 07:40):

The general pattern between finite and extended stuff is that the extended stuff is defined first, and then the finite stuff is defined in terms of it. So here it would mean defining

noncomputable def edist (u v : V) :  :=  w : G.Walk u v, w.length
noncomputable def dist (u v : V) :  := (edist u v).toNat

Rida Hamadani (Jun 18 2024 at 07:56):

Great!
In order to avoid a huge refactor (I'm not sure how many theorems use the old dist), is it possible to do something like:

theorem dist_def {u v : V} :
    G.dist u v = sInf (Set.range (Walk.length : G.Walk u v  )) := by -- this is the current (to be changed) definition of `dist`
  sorry

And then have lean understand this as a "second way to define dist"? Is there an annotation to be applied to the theorem that allows this?

Yaël Dillies (Jun 18 2024 at 07:58):

No, you would have to rewrite using that lemma in every theorem that (currently) unfolds the definition. I am pretty sure the new definition will provide a nicer API, though. You should try!

Rida Hamadani (Jun 18 2024 at 08:04):

I see, I will try this at evening
Do you have any APIs in mind that I can take inspiration from?

Yaël Dillies (Jun 18 2024 at 08:08):

The corresponding one for metric spaces. See file#Topology/MetricSpace/PseudoMetric

Peter Nelson (Jun 18 2024 at 11:30):

The infimum definition is definitely the right thing.

Rida Hamadani (Jun 18 2024 at 11:59):

But we are using infrimum in both cases, right?

Yaël Dillies (Jun 18 2024 at 16:03):

Yes, but no. One of them is not a proper infimum but a conditional infimum

Kyle Miller (Jun 18 2024 at 16:26):

In order to avoid a huge refactor

Yaël's suggestion seems right to me. Don't worry about refactoring if you need to.

Please include the dist_def theorem (perhaps as dist_eq_sInf?) using the old definition. Ideally you wouldn't use it in your refactor, instead it's just nice to have around.

Rida Hamadani (Jun 23 2024 at 21:03):

I've finally managed to prove dist_eq_sInf when G.dist u v = 0, but something feels wrong, I wanna check whether the proof is supposed to be this annoying, or am I doing something wrong.
Note that I've proved the helper ENat lemmas in mathlib4#14043, and I feel annoyed by the proofs in that PR too. They feel overcomplicated for such simple statements!

import Mathlib.Combinatorics.SimpleGraph.Connectivity
import Mathlib.Data.ENat.Lattice

namespace ENat

variable {m n : } {s : Set }

lemma lt_one_iff_eq_zero : n < 1  n = 0 := by
  rw [ not_le, one_le_iff_ne_zero.not, not_not]

lemma sInf_eq_zero : sInf s = 0  0  s := by
  refine fun h  ?_, inf_eq_bot_of_bot_mem
  rw [ bot_eq_zero, sInf_eq_bot] at h
  obtain n , hn₁, hn₂  := h 1 (by decide)
  exact lt_one_iff_eq_zero.mp hn₂  hn₁

end ENat

namespace SimpleGraph

variable {V : Type*} (G : SimpleGraph V)

noncomputable def edist (u v : V) :  :=
   w : G.Walk u v, w.length

noncomputable def dist (u v : V) :  :=
  (G.edist u v).toNat
#align simple_graph.dist SimpleGraph.dist

variable {G} {u v : V}

lemma dist_eq_sInf_of_dist_eq_zero (h : G.dist u v = 0) :
    G.dist u v = sInf (Set.range (Walk.length : G.Walk u v  )) := by
  rw [h]
  rw [dist, ENat.toNat_eq_zero, edist] at h
  symm
  rw [Nat.sInf_eq_zero]
  cases' h with h h
  · rw [iInf, ENat.sInf_eq_zero] at h
    apply Or.inl
    simp_all
  · rw [iInf, sInf_eq_top] at h
    apply Or.inr
    by_contra x
    rw [Set.range_eq_empty_iff, not_isEmpty_iff] at x
    obtain x, hx := x
    · simp_all [forall_const]
    · rename_i q p
      have : G.Walk u v := .cons q p
      aesop

Yaël Dillies (Jun 23 2024 at 21:04):

Surely you should case on whether G.edist u v = ∞ (or any equivalent statement)?

Yaël Dillies (Jun 23 2024 at 21:06):

If it's , then the sInf is zero. If it's finite, then the ENat-valued and Nat-valued sInfs agree.

Yaël Dillies (Jun 23 2024 at 21:06):

Also isn't dist_eq_sInf_of_dist_eq_zero a weird statement?

Rida Hamadani (Jun 23 2024 at 21:06):

I will try doing that, thanks!

Rida Hamadani (Jun 23 2024 at 21:07):

Yaël Dillies said:

Also isn't dist_eq_sInf_of_dist_eq_zero a weird statement?

I do not plan to include it in the API, it is just that I found dist_eq_sInftoo hard so I decided to start with something simpler.

Yaël Dillies (Jun 23 2024 at 21:07):

Yaël Dillies said:

Surely you should case on whether G.edist u v = ∞ (or any equivalent statement)?

I believe this should be your general strategy when trying to reduce the extended statement to the corresponding finite one

Yaël Dillies (Jun 23 2024 at 21:10):

The annoying part in that strategy will always be relating the proper no-junk extended operations to the junkful finite ones. Here, that will be incarnated by goals of the form ENat.toNat (⨅ i, f i) = ⨅ i, ENat.toNat (f i)

Rida Hamadani (Jun 23 2024 at 21:54):

Yes! In the above lemma, this boils down to proving that G.dist u v = ⨅ w : G.Walk u v, w.length, which is the iInf version of the lemma.

Rida Hamadani (Jun 30 2024 at 12:56):

I would like to prove the following statement about edist:

import Mathlib.Combinatorics.SimpleGraph.Connectivity
import Mathlib.Data.ENat.Lattice

namespace SimpleGraph

variable {V : Type*} (G : SimpleGraph V)

noncomputable def edist (u v : V) :  :=
   w : G.Walk u v, w.length

variable {G} {u v : V}

lemma edist_eq_top_of_not_reachable (h : ¬G.Reachable u v) : G.edist u v =  := by
  sorry

Here is the idea of the prove:
Since u and v are not reachable, there is no walks between them, thus w : G.Walk u v, w.length is , which means that its infrimum is top by docs#sInf_empty

Rida Hamadani (Jun 30 2024 at 12:58):

I'm having trouble translating this argument to lean, I don't know how to say "there is no walk between u and v since they are not reachable", and I don't know how to turn this into "there is no length of walks between them either".
Can you help me please?

Kyle Miller (Jun 30 2024 at 17:28):

Here's an unrefined "stream-of-conciousness" proof, where I had a feeling if I did simp after unfolding edist it would be amenable to simp:

lemma edist_eq_top_of_not_reachable (h : ¬G.Reachable u v) : G.edist u v =  := by
  unfold edist
  simp only [iInf_eq_top, ENat.coe_ne_top] -- found via `simp?`
  contrapose! h
  obtain p, _⟩ := h
  use p

Kyle Miller (Jun 30 2024 at 17:31):

This one is closer to your idea:

lemma edist_eq_top_of_not_reachable (h : ¬G.Reachable u v) : G.edist u v =  := by
  rw [reachable_iff_nonempty_univ,  Set.nonempty_iff_univ_nonempty, not_nonempty_iff] at h
  simp [edist]

A trick here is that simp [edist] can pick up on the resulting IsEmpty (G.Walk u v) hypothesis since it's an instance.

Kyle Miller (Jun 30 2024 at 17:32):

Maybe it's worth having a helper lemma for ¬G.Reachable u v ↔ IsEmpty (G.Walk u v), to replace that rw?

Rida Hamadani (Jul 01 2024 at 06:00):

Thank you very much!
Kyle Miller said:

Maybe it's worth having a helper lemma for ¬G.Reachable u v ↔ IsEmpty (G.Walk u v), to replace that rw?

I think this is nice to have in the connectivity file, I'll add it.

Rida Hamadani (Jul 01 2024 at 06:07):

#14315

Rida Hamadani (Jul 01 2024 at 08:31):

Kyle Miller said:

A trick here is that simp [edist] can pick up on the resulting IsEmpty (G.Walk u v) hypothesis since it's an instance.

One more question, how did Lean understand that this is an instance, as opposed to just a normal Prop?

Kyle Miller (Jul 01 2024 at 08:32):

It's because docs#IsEmpty is defined as a class

Rida Hamadani (Jul 10 2024 at 14:23):

Done! #14582 :cat:

Rida Hamadani (Jul 11 2024 at 14:19):

I'm trying to prove the instance PseudoMetricSpace V, but when I do this:

instance : PseudoMetricSpace V where
  dist := G.dist

I get the error:

type mismatch
  G.dist
has type
  V  V   : Type u_1
but is expected to have type
  V  V   : Type u_1

Is coercing dist to an acceptable way to solve this issue? And if so, how to do so? Because (G.dist : V → V → ℝ) is not coercing.

Ruben Van de Velde (Jul 11 2024 at 14:52):

Probably dist x y := (G.dist x y : Real)

Adam Topaz (Jul 11 2024 at 15:08):

You probably don’t even need to write : Real

Adam Topaz (Jul 11 2024 at 15:09):

Coercions work on elements, not codomains of functions.

Kyle Miller (Jul 11 2024 at 17:24):

This instance won't work, since V does not determine G

Kyle Miller (Jul 11 2024 at 17:25):

You could make a type abbreviation like

def Verts (G : SimpleGraph V) := V

and then define

instance : PseudoMetricSpace (Verts G) where
  ...

But then you have to be very careful to always write (u v : Verts G) rather than (u v : V).

Rida Hamadani (Jul 11 2024 at 19:59):

I see, do you think it is worth including that or is it useless? Because I rarely see Verts G being used.

Rida Hamadani (Jul 13 2024 at 14:02):

This is giving weird errors, I'm not sure what's the problem here.

import Mathlib.Combinatorics.SimpleGraph.Metric
import Mathlib.Topology.MetricSpace.Pseudo.Defs

variable (V : Type*) (G : SimpleGraph V)

def Verts (G : SimpleGraph V) := V

instance : PseudoMetricSpace (Verts G) where
  dist x y := (G.dist x y : )
  dist_self := sorry
  dist_comm := sorry
  dist_triangle := sorry
  edist_dist := sorry
  toUniformSpace := sorry
  uniformity_dist := sorry
  toBornology := sorry
  cobounded_sets := sorry

Edward van de Meent (Jul 13 2024 at 15:56):

the problem is that Verts in your definition has 2 explicit variables.
the fix is to use this:

variable {V} in
def Verts (_ : SimpleGraph V):= V

alternatively, you can do this:

import Mathlib.Combinatorics.SimpleGraph.Metric
import Mathlib.Topology.MetricSpace.Pseudo.Defs

def Verts {V : Type*} (_ : SimpleGraph V) := V
variable (V : Type*) (G : SimpleGraph V)

...

Rida Hamadani (Jul 13 2024 at 17:40):

Both suggestions are still giving the same error:

compiler IR check failed at 'instPseudoMetricSpaceVerts._rarg._lambda_1', error: unknown declaration 'SimpleGraph.dist'

At instance : PseudoMetricSpace (Verts G) where.

Edward van de Meent (Jul 13 2024 at 18:08):

i think the problem there is that the compiler doesn't know if G.dist is noncomputable or not, in general. I believe this depends on wether the field G.Adj is decidable (and possibly equality in V too), meaning that in general this will be noncomputable. The only fix i know for this would be to add noncomputable before the instance, although there might be another solution i don't know here....

Kyle Miller (Jul 13 2024 at 18:34):

Yeah, add noncomputable before instance since the distance function can't be compiled.

Kyle Miller (Jul 13 2024 at 18:37):

do you think it is worth including that or is it useless?

I would proceed with caution and treat this as a good exercise for yourself.

Another option is to make a definition that creates a Quiver from a SimpleGraph, since I think over there you get some of these metric spaces instances. You could then locally do let inst := G.toQuiver to locally use the quiver instance.

Rida Hamadani (Jul 13 2024 at 18:41):

One of the TODOs in the Metric.lean file is "Provide an additional computable version of SimpleGraph.dist for when G is connected."

Perhaps doing this is a third option that kills two birds with one stone?

Rida Hamadani (Jul 13 2024 at 18:42):

But I wonder how we can have computable distance, and if that's possible at all.
Can I somehow provide lean with an algorithm that computes the distance and this will make it computable?

Kyle Miller (Jul 13 2024 at 18:58):

I think the TODO is saying "implement breadth-first search". You need some additional assumptions on G for that to work though. I think it can either be (1) G is connected and locally finite or (2) G is finite.

Kyle Miller (Jul 13 2024 at 18:59):

Breadth-first search is useful in algorithms, but I'm not sure how useful it is to complete this TODO item (without a motivating formalization project, it's hard to know what you'd want out of such an algorithm).

Kyle Miller (Jul 13 2024 at 19:00):

Rida Hamadani said:

Perhaps doing this is a third option that kills two birds with one stone?

If you're thinking about how it removes the need for noncomputable, that's not something we should do. It is perfectly fine for the instance to be noncomputable.

Kyle Miller (Jul 13 2024 at 19:02):

One of the problems with making mathematics definitions computable is that, for generic data structures like a SimpleGraph, what can often happen is that the result is a merely computable definition. It's inefficient in practice since it isn't able to make use of the specific features of more efficient data structures.

Kyle Miller (Jul 13 2024 at 19:03):

Oh, I forgot I basically already implemented breadth-first search for finite graphs as part of the implementation of the docs#SimpleGraph.instDecidableConnected instance. It's a really badly implemented algorithm, but it works for small enough graphs. It's also not classic breadth-first search. It enumerates all walks up to a particular length, which is usually exponential time.

Rida Hamadani (Jul 13 2024 at 19:05):

I'm referencing this TODO item, it is not clear to me how it is referencing breadth-first search. My first thought was using Dijkistra's algorithm instead.

Edward van de Meent (Jul 13 2024 at 19:07):

Kyle Miller said:

One of the problems with making mathematics definitions computable is that, for generic data structures like a SimpleGraph, what can often happen is that the result is a merely computable definition. It's inefficient in practice since it isn't able to make use of the specific features of more efficient data structures.

so then... best practice would be to not change the definition, but provide a separate definition/implementation which gives provably the same result provided some conditions hold? (and then possibly make a tactic to do this efficient calculation?)

Rida Hamadani (Jul 13 2024 at 19:09):

Kyle Miller said:

If you're thinking about how it removes the need for noncomputable, that's not something we should do. It is perfectly fine for the instance to be noncomputable.

I see! If that's the case, I'll prove it as a noncomputable instance for now, I have no project in mind to develop this, I am doing this for the sake of completion (it feels like something is missing when we have a distance but no metric space).

Rida Hamadani (Jul 13 2024 at 19:11):

Edward van de Meent said:

so then... best practice would be to not change the definition, but provide a separate definition/implementation which gives provably the same result provided some conditions hold? (and then possibly make a tactic to do this efficient calculation?)

This is very nice! Do you have any examples in mind where something like this was done so I can learn from them?

Rida Hamadani (Jul 13 2024 at 20:18):

The MetricSpace instance is added in #14715, it depends on #14582 is in order to use the new definition and API of dist.
The linter fails because apparently instances cannot have named hypotheses (I didn't know this). What is the proper way to deal with this? I thought that due to docs#SimpleGraph.instDecidableConnected which Kyle mentioned above, lean should be able to infer connectivity. Did I misunderstand something?

Kyle Miller (Jul 13 2024 at 20:20):

Rida Hamadani said:

I'm referencing this TODO item, it is not clear to me how it is referencing breadth-first search. My first thought was using Dijkistra's algorithm instead.

Yeah, I understood which todo item it was :-). It comes from this PR, and although it's been a couple years, I'm pretty sure I was thinking about breadth-first search.

Dijkstra's algorithm is a generalization of breadth-first search when there is a weighted graph, but the dist measure under consideration is for unweighted graphs.

Kyle Miller (Jul 13 2024 at 20:25):

I left a comment on #14715, but I don't think mathlib should have this Verts G definition or the metric space structure on Verts G. At best, we might consider it if there's a project that proves it's a good idea, but there are issues with having both Verts G and V to refer to the same type without developing a bunch of boilerplate.

Rida Hamadani (Jul 13 2024 at 22:27):

Surprisingly, lean accepted the instance without defining Verts, I pushed the commit to #14715.
The CI still doesn't pass but it is due to something else (it is the issue I was talking about above regarding instances that take a hypothesis).

Kyle Miller (Jul 13 2024 at 23:04):

The lint output explains the issue:

/- The `impossibleInstance` linter reports:
SOME INSTANCES HAVE ARGUMENTS THAT ARE IMPOSSIBLE TO INFER
These are arguments that are not instance-implicit and do not appear in
another instance-implicit argument or the return type. -/

From V itself it can't determine what G should be. It's not about named hypotheses, but about having hypotheses at all.

Even if this instance passed the linter, it's not a good one since it's trying to put a metric space structure on V from some graph G, and instances are supposed to be canonical. Each G gives a different metric space structure, so it fails canonicity.

This is a reason why it's not a good idea having such an instance.

Kyle Miller (Jul 13 2024 at 23:07):

The Verts G idea solves the linter problem, since it puts G into the term. You can go further and have Verts G hG where hG : Connected G, if you want to experiment (not for the PR though)

Rida Hamadani (Jul 13 2024 at 23:09):

Ahh, that makes sense. Thank you for explaining.
It turns out that this will take a lot of effort for something that probably won't be useful, so I will close #14715, but I'll keep the idea of building a bridge to Quiver in mind for the future.
I learnt a lot from this nonetheless! :big_smile:

Kyle Miller (Jul 13 2024 at 23:11):

Even if it's not an instance, it's possible to have a def

Kyle Miller (Jul 13 2024 at 23:12):

There's also a question of whether there ought to be a class that says a function is a metric, rather than requiring a type to be equipped with a metric space structure

Rida Hamadani (Jul 13 2024 at 23:29):

I've found examples where def was used to prove metric spaces, such as docs#instL2OpMetricSpace
Do you think it is okay if we did this in our case?

Yakov Pechersky (Jul 14 2024 at 02:37):

You might also be interested in docs#MetricSpace.ofDistTopology

Jeremy Tan (Aug 31 2024 at 08:44):

@Kyle Miller how does this look like for breadth-first search?

import Mathlib.Data.Fintype.Basic

variable {α : Type*} [Fintype α] [DecidableEq α] (r : α  α  Prop) [DecidableRel r]

def elemsAtDepthAux (p : Finset α × Finset α) : Finset α × Finset α :=
  let k := p.2.filter (fun b   a  p.1, r a b)
  (k, p.2 \ k)

/-- Elements at distance `d` from the given set of roots. -/
def elemsAtDepth (roots : Finset α) (d : ) : Finset α :=
  ((elemsAtDepthAux r)^[d] (roots, roots)).1

Jeremy Tan (Aug 31 2024 at 10:36):

Or how about this? Is it usable and how can it be neatened up?

import Mathlib.Data.ENat.Basic
import Mathlib.Data.Fintype.Basic

variable {α : Type*} (r : α  α  Prop) [DecidableRel r]

def bfsRun : List (α × ) × List (α × ) × List α  List (α × ) × List (α × ) × List α
  | (res, [], cant_reach) => ((cant_reach.map (·, ) ++ res).reverse, [], [])
  | (res, v :: queue, not_yet) =>
    let ll := not_yet.partition (r v.1)
    bfsRun (v :: res, queue ++ ll.1.map (·, v.2 + 1), ll.2)
termination_by x => x.2.1.length + x.2.2.length
decreasing_by
  simp_wf
  change queue.length + (List.filter (fun b  decide (r v.1 b)) not_yet).length +
    (List.filter (fun b  !decide (r v.1 b)) not_yet).length < _
  rw [add_assoc,  List.length_eq_length_filter_add]; omega

def bfs (roots searchSpace : List α) : List (α × ) :=
  (bfsRun r ([], roots.map (·, 0), searchSpace)).1

def rr (a b : Fin 10) : Prop := (b + 10 - a) % 10 = 7

instance : DecidableRel rr := inferInstanceAs <| DecidableRel fun a b  (b + 10 - a) % 10 = 7

#eval bfs rr [0] (List.range 10).tail
/-
[(0, some 0),
 (7, some 1),
 (4, some 2),
 (1, some 3),
 (8, some 4),
 (5, some 5),
 (2, some 6),
 (9, some 7),
 (6, some 8),
 (3, some 9)]
-/

Jeremy Tan (Aug 31 2024 at 11:26):

hey, this works

variable {α : Type*} [Fintype α] [DecidableEq α] (r : α  α  Prop) [DecidableRel r]

def elemsAtDepthAux (p : Finset α × Finset α) : Finset α × Finset α :=
  let k := p.2.filter (fun b   a  p.1, r a b)
  (k, p.2 \ k)

/-- Elements at depth `d` relative to the given set of roots. -/
def elemsAtDepth (roots : Finset α) (d : ) : Finset α :=
  ((elemsAtDepthAux r)^[d] (roots, roots)).1

def collatz (a b : Fin 100) : Prop := 3 * b.1 + 1 = a.1  2 * a.1 = b.1

instance : DecidableRel collatz :=
  inferInstanceAs <| DecidableRel fun (a b : Fin 100)  3 * b.1 + 1 = a.1  2 * a.1 = b.1

#eval (elemsAtDepth collatz {1} 0).sort (·  ·) -- [1]
#eval (elemsAtDepth collatz {1} 1).sort (·  ·) -- [0, 2]
#eval (elemsAtDepth collatz {1} 2).sort (·  ·) -- [4]
#eval (elemsAtDepth collatz {1} 3).sort (·  ·) -- [8]
#eval (elemsAtDepth collatz {1} 4).sort (·  ·) -- [16]
#eval (elemsAtDepth collatz {1} 5).sort (·  ·) -- [5, 32]
#eval (elemsAtDepth collatz {1} 6).sort (·  ·) -- [10, 64]
#eval (elemsAtDepth collatz {1} 7).sort (·  ·) -- [3, 20, 21]
#eval (elemsAtDepth collatz {1} 8).sort (·  ·) -- [6, 40, 42]
#eval (elemsAtDepth collatz {1} 9).sort (·  ·) -- [12, 13, 80, 84]
#eval (elemsAtDepth collatz {1} 10).sort (·  ·) -- [24, 26]
#eval (elemsAtDepth collatz {1} 11).sort (·  ·) -- [48, 52]
#eval (elemsAtDepth collatz {1} 12).sort (·  ·) -- [17, 96]
#eval (elemsAtDepth collatz {1} 13).sort (·  ·) -- [34]
#eval (elemsAtDepth collatz {1} 14).sort (·  ·) -- [11, 68]
#eval (elemsAtDepth collatz {1} 15).sort (·  ·) -- [22]
#eval (elemsAtDepth collatz {1} 16).sort (·  ·) -- [7, 44]

end

Last updated: May 02 2025 at 03:31 UTC