Zulip Chat Archive
Stream: graph theory
Topic: concrete simple graph
Alexandre Rademaker (Nov 12 2020 at 01:16):
I am trying to understand how can I use the simple_graph
to introduce some basic concepts of graph theory in my course on discrete math. In particular, I am trying to construct a concrete simple graph but I got a type error:
import combinatorics.simple_graph
open nat
def e1 : ℕ → ℕ → Prop
| 1 2 := true
| 2 1 := true
| 2 3 := true
| 3 2 := true
| _ _ := false
def g1 := simple_graph.mk e1
What am I doing wrong?
Bryan Gin-ge Chen (Nov 12 2020 at 01:27):
docs#simple_graph is a structure with 3 fields, the first is an edge relation, which you've provided with e1
. The second and third are proofs that the edge relation is symmetric and irreflexive, respectively. If you don't provide them, the . obviously
syntax in the definition means that Lean will try using docs#obviously (I think this just calls tactic#tidy) to provide proofs. The error message says that obviously
isn't able to prove those goals, so you'll most likely have to provide proofs yourself.
Chris B (Nov 12 2020 at 01:29):
^^ What he said.
def g1 : simple_graph nat :=
have h1 : symmetric e1, from sorry,
have h2 : irreflexive e1, from sorry,
simple_graph.mk e1 h1 h2
Alexandre Rademaker (Nov 12 2020 at 01:41):
Nice! Thank you, looks like I need a better definition for the edges, these proofs are not trivial for my definition of e1
.
Alexandre Rademaker (Nov 12 2020 at 02:22):
Ok, that is the complete boring step-by-step. I am probably missing some magic way to simplify the proof over finite data.
import combinatorics.simple_graph
inductive node : Type
| a : node
| b : node
| c : node
open node
def e1 : node → node → Prop
| a b := true
| b a := true
| a c := true
| c a := true
| _ _ := false
def g1 : simple_graph node :=
have h1 : symmetric e1, from
begin
intros n m,
apply node.cases_on n,
apply node.cases_on m,
rw e1, trivial,
rw e1, intro, trivial,
rw e1, intro, trivial,
apply node.cases_on m,
rw [e1, e1], intro, trivial,
rw e1, trivial,
rw [e1, e1], trivial,
apply node.cases_on m,
rw [e1,e1], intro, trivial,
rw e1, trivial,
rw e1, trivial,
end,
have h2 : irreflexive e1, from
begin
intro n,
apply node.cases_on n,
rw e1, trivial,
rw e1, trivial,
rw e1, trivial,
end,
simple_graph.mk e1 h1 h2
I could simplify to
def g1 : simple_graph node :=
have h1 : symmetric e1, from
begin
intros n m,
cases n,
repeat {
cases m,
repeat {repeat {rw e1}, try {intro}, trivial}
},
end,
have h2 : irreflexive e1, from
begin
intro n,
cases n,
repeat { rw e1, trivial },
end,
simple_graph.mk e1 h1 h2
Alex J. Best (Nov 12 2020 at 02:50):
you can use ;
to apply to all created goals instead of repeat.
def g1 : simple_graph node :=
have h1 : symmetric e1, from
begin
intros x y h,
cases x;
cases y; exact h,
end,
have h2 : irreflexive e1, from
begin
intro n,
cases n; rw e1; trivial,
end,
simple_graph.mk e1 h1 h2
Alexandre Rademaker (Nov 12 2020 at 02:53):
thank you.
Mario Carneiro (Nov 12 2020 at 02:54):
It's much easier to work with an inductive:
inductive e1 : node → node → Prop
| ab : e1 a b
| ba : e1 b a
| ac : e1 a c
| ca : e1 c a
theorem e1.symm {x y} (h : e1 x y) : e1 y x := by cases h; constructor
theorem e1.irrefl (x) : ¬ e1 x x := λ h, by cases h
def g1 : simple_graph node := simple_graph.mk e1 @e1.symm e1.irrefl
Mario Carneiro (Nov 12 2020 at 03:02):
you can also use a symmetric closure to cut down on the number of cases (although in this example it's not a very big deal, in a larger inductive it might be worth it)
import combinatorics.simple_graph
namespace relation
variables {α : Type*} (r : α → α → Prop)
def symm_gen (x y : α) : Prop := r x y ∨ r y x
variable {r}
theorem symm_gen.symm : symmetric (symm_gen r) := λ _ _, or.symm
theorem symm_gen.irrefl (H : irreflexive r) : irreflexive (symm_gen r) :=
by rintro _ (h|h); exact H _ h
end relation
inductive node : Type
| a : node
| b : node
| c : node
open node
inductive e1 : node → node → Prop
| ab : e1 a b
| ac : e1 a c
def g1 : simple_graph node :=
{ adj := relation.symm_gen e1,
sym := relation.symm_gen.symm,
loopless := relation.symm_gen.irrefl $ by rintro _ ⟨⟩ }
Alexandre Rademaker (Nov 12 2020 at 03:12):
Thank you @Mario Carneiro, I will try more tomorrow. It is a new world for me! I am trying to understand many things that I haven't played yet in Lean. It took me 30 min to understand how to obtain the neighbors of a node with #reduce (simple_graph.neighbor_set g2 c)
that reduces to e1 c
.
Mario Carneiro (Nov 12 2020 at 03:14):
I don't know that much about this graph library, but if you want to enumerate the values of x
such that e1 c x
you can do cases (h : e1 c x)
Kyle Miller (Nov 12 2020 at 04:40):
It's not particularly well developed, but you can create graphs from edge sets:
import combinatorics.simple_graph
def simple_graph.from_edges {α : Type*}
(edges : set (sym2 α)) (loopless : ∀ e ∈ edges, ¬sym2.is_diag e) :
simple_graph α :=
{ adj := λ v w, ⟦(v, w)⟧ ∈ edges,
sym := by { intros v w, simp [sym2.eq_swap] },
loopless := by { intros v h, have h' := loopless _ h, simpa using h' } }
open simple_graph
def my_edges : set (sym2 ℕ) :=
{ ⟦(1, 2)⟧,
⟦(2, 3)⟧ }
def my_graph : simple_graph ℕ :=
simple_graph.from_edges my_edges (by { dunfold my_edges, finish })
Kyle Miller (Nov 12 2020 at 04:44):
Part of it is that data.sym2
is missing the inverse to from_rel
, which would take a set of unordered pairs and produce a relation. It should also have a lemma that if the set of unordered pairs has no diagonal elements, then the relation is irreflexive.
Kyle Miller (Nov 12 2020 at 04:53):
If anyone wants to add from_edges
to the simple graph library, here's an additional lemma:
@[simp]
lemma edge_set_from_edges {α : Type*}
{edges : set (sym2 α)} {loopless : ∀ e ∈ edges, ¬sym2.is_diag e} :
(simple_graph.from_edges edges loopless).edge_set = edges :=
by tidy
(Maybe it would be better to be called from_edge_set
rather than from_edges
.)
Alexandre Rademaker (Nov 12 2020 at 12:56):
Hi @Kyle Miller , this is very interesting. The use of sym2
instead of a function simplified the definition of the edges and the proofs necessary to construct the graph. I also learned about dunfold
that I have never seen before.
But thinking broadly, maybe I need to change my mindset to use lean to present basic graph theory. I am trying to compute with graphs as data structures and maybe the library as not developed with that purpose.
Alexandre Rademaker (Nov 12 2020 at 17:17):
Mario Carneiro said:
I don't know that much about this graph library, but if you want to enumerate the values of
x
such thate1 c x
you can docases (h : e1 c x)
Hum, that would be an alternative to my tentative #reduce
command. But I would need to create a context to use this tactic..
Kyle Miller (Nov 12 2020 at 18:41):
@Alexandre Rademaker The graph library isn't designed to be used for data structures directly, and I think of it more as an abstract interface to graphs: you might create specialty data structures for computation along with a function to simple_graph
to prove things (though this idea hasn't been tested yet). The library tries to make things decidable, at least, if the underlying relation is, though you have to implement fintype
instances to get it to be able to #reduce
the degree of a vertex, for example. Instances like these are where you supply your own algorithms that depend on the specific data structure.
Alexandre Rademaker (Nov 17 2020 at 04:51):
Thank you @Kyle Miller, but I didn't get the point about the abstract interface to graphs. Now that I know how to construct a particular graph, I am back to see how Lean can help me in the introduction to graph theory.
The majority of the courses/books about introduction to graphs have isomorphism as one of its first topics. It seems that providing examples like the ones described in https://www.youtube.com/watch?v=z-GfKbzvtBA&feature=youtu.be (possible following also the ideas of https://math.stackexchange.com/questions/2486944/how-to-tell-whether-two-graphs-are-isomorphic would be nice.
Kyle Miller (Nov 17 2020 at 05:07):
In an experimental branch, we have graph isomorphisms https://github.com/leanprover-community/mathlib/blob/simple_graphs2/src/combinatorics/simple_graph/hom.lean#L137 (though no real examples except for the one on line 200). An isomorphism of simple graphs is a bijection on vertex sets that preserves the adjacency relation in both directions.
Alexandre Rademaker (Nov 17 2020 at 22:48):
I thought that isomorphism would be a proposition not a type ... I would like to be able to provide two simple graphs and construct a prove of their isomorphism. That is, given an f , show that it is bijective and preserves adjacent relations
Kyle Miller (Nov 17 2020 at 22:53):
Two graphs are isomorphic if there exists an isomorphism between them. The best way to show two graphs are isomorphic is to provide an explicit isomorphism, which gives the exact correspondence between vertices in each graph. If you want to turn it into a proposition, you could say nonempty (G ≃g G')
(using the notation from that branch), but you'd only want to do that if for some reason you can't construct the isomorphism.
Kyle Miller (Nov 17 2020 at 22:58):
But maybe you're asking for Lean to compute whether two graphs are isomorphic? You could probably write a tactic to do this, if the graphs are presented suitably. (Or maybe use a Mario-style trick where you do logic programming at the typeclass level?)
Kyle Miller (Nov 17 2020 at 23:15):
I backported graph isomorphism from the experimental branch to what's currently in mathlib and then took the pair of isomorphic graphs from the beginning of the YouTube video and showed they're isomorphic by providing an explicit isomorphism (with inefficient proofs):
import combinatorics.simple_graph
universes u v
namespace simple_graph
def from_edges {α : Type*}
(edges : set (sym2 α)) (loopless : ∀ e ∈ edges, ¬sym2.is_diag e) :
simple_graph α :=
{ adj := λ v w, ⟦(v, w)⟧ ∈ edges,
sym := by { intros v w, simp [sym2.eq_swap] },
loopless := by { intros v h, have h' := loopless _ h, simpa using h' } }
lemma from_edges.edge_set {α : Type*}
{edges : set (sym2 α)} {loopless : ∀ e ∈ edges, ¬sym2.is_diag e} :
(simple_graph.from_edges edges loopless).edge_set = edges :=
by tidy
abbreviation iso {V : Type u} {V' : Type v} (G : simple_graph V) (G' : simple_graph V') : Type (max u v) :=
rel_iso G.adj G'.adj
infix ` ≃g ` : 50 := iso
end simple_graph
open simple_graph
inductive V1 | v1 | v2 | v3 | v4 | v5 | v6
inductive V2 | va | vb | vc | vd | ve | vf
open V1 V2
def G : simple_graph V1 :=
simple_graph.from_edges
{⟦(v1, v4)⟧,
⟦(v1, v5)⟧,
⟦(v1, v6)⟧,
⟦(v2, v4)⟧,
⟦(v2, v5)⟧,
⟦(v2, v6)⟧,
⟦(v3, v4)⟧,
⟦(v3, v5)⟧,
⟦(v3, v6)⟧,
}
(by finish)
def H : simple_graph V2 :=
simple_graph.from_edges
{⟦(va, vb)⟧,
⟦(va, vd)⟧,
⟦(va, vf)⟧,
⟦(vb, vc)⟧,
⟦(vb, ve)⟧,
⟦(vc, vd)⟧,
⟦(vc, vf)⟧,
⟦(vd, ve)⟧,
⟦(ve, vf)⟧,
}
(by finish)
def GtoH_verts : V1 → V2
| v1 := vb
| v2 := vd
| v3 := vf
| v4 := vc
| v5 := ve
| v6 := va
def HtoG_verts : V2 → V1
| vb := v1
| vd := v2
| vf := v3
| vc := v4
| ve := v5
| va := v6
def GisoH : G ≃g H :=
{ to_fun := GtoH_verts,
inv_fun := HtoG_verts,
left_inv := begin intro x, cases x; simp [GtoH_verts, HtoG_verts], end,
right_inv := begin intro x, cases x; simp [GtoH_verts, HtoG_verts], end,
map_rel_iff' := begin
intros x y,
dsimp [G, H, from_edges],
simp only [set.mem_insert_iff, set.mem_singleton_iff, sym2.eq_iff],
cases x; cases y; dsimp [GtoH_verts]; simp,
end }
Kyle Miller (Nov 17 2020 at 23:18):
(There's probably some way to have HtoG_verts
be automatically constructed from GtoH_verts
since V1
and V2
are finite types, but I don't know how off the top of my head.)
Mario Carneiro (Nov 18 2020 at 00:39):
You can't construct HtoG_verts
directly from GtoH_verts
without leaving an important verification condition on the table: the goal is to show a bijection and if you just make a list of pairs you don't know it's bijective. Writing the functions in each direction ensures exhaustiveness, and checking that they are converses is easy (linear time)
Kyle Miller (Nov 18 2020 at 06:10):
@Mario Carneiro I was imagining something like this, but more automated (and also more computable since the types are decidable and finite).
import data.fintype.basic
open fintype
inductive V1 | v1 | v2 | v3 | v4 | v5 | v6
inductive V2 | va | vb | vc | vd | ve | vf
open V1 V2
instance : fintype V1 :=
begin use {v1, v2, v3, v4, v5, v6}, tidy, cases x; simp end
instance : fintype V2 :=
begin use {va, vb, vc, vd, ve, vf}, tidy, cases x; simp end
@[simp] lemma card_V1 : card V1 = 6 := by tidy
@[simp] lemma card_V2 : card V2 = 6 := by tidy
def f : V1 → V2
| v1 := vb
| v2 := vd
| v3 := vf
| v4 := vc
| v5 := ve
| v6 := va
lemma f_inj : function.injective f :=
by { intros x y, cases x; cases y; simp [f] }
lemma f_bij : function.bijective f :=
(bijective_iff_injective_and_card f).mpr ⟨f_inj, by simp⟩
noncomputable
def f_equiv : V1 ≃ V2 :=
equiv.of_bijective f f_bij
lemma f_equiv_to_fun : f_equiv.to_fun = f :=
by { ext x, simp [f_equiv] }
Mario Carneiro (Nov 18 2020 at 06:11):
this is way more expensive than the first version
Mario Carneiro (Nov 18 2020 at 06:12):
the injectivity check is O(n^2) while the back and forth check is O(n)
Mario Carneiro (Nov 18 2020 at 06:12):
it's also noncomputable
Kyle Miller (Nov 18 2020 at 06:16):
Technically it's all O(1) since there are only 6 elements, but in any case I'm only looking for mathematical convenience to define an explicit bijection and I don't care too much about running time of the resulting function.
How is this noncomputable? There are only 6^6 possible functions, and you can check all of them to see what would be the inverse in a computable way.
Kevin Buzzard (Nov 18 2020 at 07:23):
It's noncomputable in the sense that you wrote noncomputable
?
Mario Carneiro (Nov 18 2020 at 07:30):
If you are looking for mathematical convenience, the example is too minimal. Almost all of the boilerplate can be removed but it needs to fit in some larger pattern of graph isomorphisms. This looks way too much like a toy example to draw general conclusions
Alexandre Rademaker (Nov 22 2020 at 15:22):
I was thinking about the current definition of simple graph. What is the motivation for this particular way to define graphs? In particular why carrying out the proofs as components of the structure?
Aaron Anderson (Nov 22 2020 at 16:48):
In part, in analogy to the way groups and such are defined.
Aaron Anderson (Nov 22 2020 at 16:49):
If you want to have a structure that has some data that follow some properties, the easiest way to do it seems to be to bundle together the data and the proofs of the properties.
Aaron Anderson (Nov 22 2020 at 16:52):
Another possible way to do it could have been by defining a set of edges, but if that was defined as say, a set of the type sym2
, the symmetric square of the vertex type, then you’d still need to include a proof of the irreflexivity axiom.
Alexandre Rademaker (Nov 23 2020 at 15:14):
In what situations and how the proofs in the structure can or should be used?
Jalex Stark (Nov 24 2020 at 22:48):
for "how" to use the proofs in a structure, use dot notation?
Alexandre Rademaker (Nov 26 2020 at 13:58):
Thank you @Jalex Stark and sorry, I know that we can access fields in a structure with the dot notation. The how
is not a question about Lean syntax, it is more about the purpose of having this term in the structure and contexts that it would be useful. See
def e1 : node → node → Prop
| a b := true
| b a := true
| a c := true
| c a := true
| _ _ := false
def g2 : simple_graph node :=
have h1 : symmetric e1, from
begin
intros n m,
cases n,
repeat {
cases m,
repeat {repeat {rw e1}, try {intro}, trivial}
},
end,
have h2 : irreflexive e1, from
begin
intro n,
cases n,
repeat { rw e1, trivial },
end,
simple_graph.mk e1 h1 h2
#check g2.sym
This only reveals that g2.sym
is a very unreadable term g2.sym : auto_param (symmetric g2.adj) (name.mk_string "obviously" name.anonymous)
of type symmetric e1
I hope. But what is the benefits of carrying this proof with the structure?
Jalex Stark (Nov 26 2020 at 16:18):
hmm so my understanding is a bit shaky here, too. I think that the part matching auto_param (_) (_ "obviously" _)
means that Lean did or will try to prove symmetric e1
using the obviously
tactic, which is currently the same as trying to prove it with tidy
Jalex Stark (Nov 26 2020 at 16:20):
Before seeing your code I had the impression that g2.sym
would be of type symmetric e1
or at least of some type that is defeq to symmetric e1
.
Jalex Stark (Nov 26 2020 at 16:21):
Maybe @Kyle Miller or @Aaron Anderson or @Alena Gusakov can say something more helpful
Jalex Stark (Nov 26 2020 at 16:22):
Often in mathlib we manage to build up a lot of theory without being very good at producing concrete examples. Thank you for trying to do that !
Jalex Stark (Nov 26 2020 at 16:22):
When I first came to this server i was trying to prove any theorems about a certain finite group of cardinality and I found the experience pretty disheartening
Aaron Anderson (Nov 26 2020 at 16:36):
The goal of the definition
structure simple_graph (V : Type u) :=
(adj : V → V → Prop)
(sym : symmetric adj . obviously)
(loopless : irreflexive adj . obviously)
is to be functionally equivalent to the simpler definition
structure simple_graph (V : Type u) :=
(adj : V → V → Prop)
(sym : symmetric adj)
(loopless : irreflexive adj)
Aaron Anderson (Nov 26 2020 at 16:38):
To see what I mean by "functionally equivalent", check out the proofs that use G.sym
and G.loopless
.
lemma ne_of_adj {a b : V} (hab : G.adj a b) : a ≠ b :=
by { rintro rfl, exact G.loopless a hab }
uses G.loopless
exactly as if it was of type irreflexive G.adj
, and
lemma edge_symm (u v : V) : G.adj u v ↔ G.adj v u := ⟨λ x, G.sym x, λ x, G.sym x⟩
uses G.sym
exactly as if it was of type symmetric G.adj
.
Aaron Anderson (Nov 26 2020 at 16:39):
So when you are proving things using those properties, you can pretend it's just the simpler definition, and lean will make the necessary conversions for you.
Aaron Anderson (Nov 26 2020 at 16:43):
When you want to prove those properties, you have two choices:
Aaron Anderson (Nov 26 2020 at 16:43):
forget about the . obviously
, and prove the two conditions by hand (what you've done)
Aaron Anderson (Nov 26 2020 at 16:44):
or try an abbreviated form.
Aaron Anderson (Nov 26 2020 at 16:44):
What I'd do in general is try what we did for the complete_graph
:
def complete_graph (V : Type u) : simple_graph V :=
{ adj := ne }
except with adj :=
whatever your relation is.
Aaron Anderson (Nov 26 2020 at 16:45):
Then lean will complain at you and say if it's missing one or both of the proofs, which you then just have to do the old-fashioned way.
Aaron Anderson (Nov 26 2020 at 16:46):
As to the other question you could be asking is why have proofs as part of the structure at all?
Aaron Anderson (Nov 26 2020 at 16:46):
The answer I have to that is, if we didn't, objects of this type wouldn't be graphs!
Aaron Anderson (Nov 26 2020 at 16:49):
You could define it as
structure simple_graph (V : Type u) :=
(adj : V → V → Prop)
but then you couldn't prove basic theorems that are true of graphs, because you'll have counterexamples to symmetry and irreflexitivity.
Alexandre Rademaker (Nov 29 2020 at 21:49):
I see, thank you @Aaron Anderson for the detailed explanation. So the instantiation of a simple_graph forces you to carry the proofs that certificate that the instance is a simple graph. The lemmas you provided using the proofs in the structure field also makes thing clear.
Kyle Miller (Nov 30 2020 at 21:42):
@Alexandre Rademaker https://leanprover.github.io/reference/expressions.html#implicit-arguments explains all the kinds of arguments that Lean supports -- these can appear both in argument lists to functions and to fields for structures. The auto_param
is an internal detail for the case of using a tactic to synthesize the optional argument (I think the documentation should say optional and not implicit in that last case)
Kyle Miller (Dec 01 2020 at 04:07):
This is how I might write that proof (just changing things a little without changing the tactics really):
inductive node | a | b | c
open node
def e1 : node → node → Prop
| a b := true
| b a := true
| a c := true
| c a := true
| _ _ := false
def g2 : simple_graph node :=
{ adj := e1,
sym := begin
intros n m,
cases n; cases m; { dunfold e1, intro, trivial },
end,
loopless := begin
intro n,
cases n; { dunfold e1, trivial },
end }
Kyle Miller (Dec 01 2020 at 05:29):
The tidy
tactic is able to prove it's a simple graph with a little help:
def g2 : simple_graph node :=
{ adj := e1,
sym := λ n m, by cases n; cases m; tidy,
loopless := λ n, by cases n; tidy }
What ;
does is run the following tactics for each case generated by the first tactic. So, for example, cases n; cases m; tidy
ends up running tidy for each possibility for n
and m
.
Kyle Miller (Dec 01 2020 at 05:36):
In the end, both proofs just do cases
on each argument, so here's a way to compact it. This should work in general to prove that other adjacency relations defined in a similar way are graphs:
meta def cases' : tactic unit := `[intro h, cases h; { trivial <|> cases' }]
def g2 : simple_graph node :=
{ adj := e1,
sym := by cases',
loopless := by cases' }
Last updated: Dec 20 2023 at 11:08 UTC