Skip to content
This repository was archived by the owner on Jul 24, 2024. It is now read-only.

Commit 4ccbb51

Browse files
committed
feat(linear_algebra): eigenspaces of linear maps (#3927)
Add eigenspaces and eigenvalues of linear maps. Add lemma that in a finite-dimensional vector space over an algebraically closed field, eigenvalues exist. Add lemma that eigenvectors belonging to distinct eigenvalues are linearly independent. This is a rework of #3864, following Cyril's suggestion. Generalized eigenspaces will come in a subsequent PR.
1 parent 1353b7e commit 4ccbb51

File tree

11 files changed

+388
-4
lines changed

11 files changed

+388
-4
lines changed

src/data/finsupp/basic.lean

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -241,6 +241,21 @@ rfl
241241
(on_finset s f hf).support ⊆ s :=
242242
filter_subset _
243243

244+
lemma mem_support_on_finset
245+
{s : finset α} {f : α → β} (hf : ∀ (a : α), f a ≠ 0 → a ∈ s) {a : α} :
246+
a ∈ (finsupp.on_finset s f hf).support ↔ f a ≠ 0 :=
247+
by simp [finsupp.mem_support_iff, finsupp.on_finset_apply]
248+
249+
lemma support_on_finset
250+
{s : finset α} {f : α → β} (hf : ∀ (a : α), f a ≠ 0 → a ∈ s) :
251+
(finsupp.on_finset s f hf).support = s.filter (λ a, f a ≠ 0) :=
252+
begin
253+
ext a,
254+
rw [mem_support_on_finset, finset.mem_filter],
255+
specialize hf a,
256+
finish
257+
end
258+
244259
end on_finset
245260

246261
/-! ### Declarations about `map_range` -/

src/data/multiset/basic.lean

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,23 @@ theorem subset_zero {s : multiset α} : s ⊆ 0 ↔ s = 0 :=
247247

248248
end subset
249249

250+
section to_list
251+
252+
/-- Produces a list of the elements in the multiset using choice. -/
253+
@[reducible] noncomputable def to_list {α : Type*} (s : multiset α) :=
254+
classical.some (quotient.exists_rep s)
255+
256+
@[simp] lemma to_list_zero {α : Type*} : (multiset.to_list 0 : list α) = [] :=
257+
(multiset.coe_eq_zero _).1 (classical.some_spec (quotient.exists_rep multiset.zero))
258+
259+
lemma coe_to_list {α : Type*} (s : multiset α) : (s.to_list : multiset α) = s :=
260+
classical.some_spec (quotient.exists_rep _)
261+
262+
lemma mem_to_list {α : Type*} (a : α) (s : multiset α) : a ∈ s.to_list ↔ a ∈ s :=
263+
by rw [←multiset.mem_coe, multiset.coe_to_list]
264+
265+
end to_list
266+
250267
/- multiset order -/
251268

252269
/-- `s ≤ t` means that `s` is a sublist of `t` (up to permutation).
@@ -797,6 +814,13 @@ theorem prod_ne_zero {R : Type*} [integral_domain R] {m : multiset R} :
797814
multiset.induction_on m (λ _, one_ne_zero) $ λ hd tl ih H,
798815
by { rw forall_mem_cons at H, rw prod_cons, exact mul_ne_zero H.1 (ih H.2) }
799816

817+
lemma prod_eq_zero {α : Type*} [comm_semiring α] {s : multiset α} (h : (0 : α) ∈ s) :
818+
multiset.prod s = 0 :=
819+
begin
820+
rcases multiset.exists_cons_of_mem h with ⟨s', hs'⟩,
821+
simp [hs', multiset.prod_cons]
822+
end
823+
800824
@[to_additive]
801825
lemma prod_hom [comm_monoid α] [comm_monoid β] (s : multiset α) (f : α → β) [is_monoid_hom f] :
802826
(s.map f).prod = f s.prod :=

src/field_theory/algebraic_closure.lean

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,10 @@ theorem of_exists_root (H : ∀ p : polynomial k, p.monic → irreducible p →
6262
let ⟨x, hx⟩ := H (q * C (leading_coeff q)⁻¹) (monic_mul_leading_coeff_inv hq.ne_zero) this in
6363
degree_mul_leading_coeff_inv q hq.ne_zero ▸ degree_eq_one_of_irreducible_of_root this hx⟩
6464

65+
lemma degree_eq_one_of_irreducible [is_alg_closed k] {p : polynomial k} (h_nz : p ≠ 0) (hp : irreducible p) :
66+
p.degree = 1 :=
67+
degree_eq_one_of_irreducible_of_splits h_nz hp (polynomial.splits' _)
68+
6569
end is_alg_closed
6670

6771
instance complex.is_alg_closed : is_alg_closed ℂ :=

src/field_theory/splitting_field.lean

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,6 +117,15 @@ begin
117117
rw [finset.prod_insert hat, splits_mul_iff i ht.1 (finset.prod_ne_zero_iff.2 ht.2), ih ht.2]
118118
end
119119

120+
lemma degree_eq_one_of_irreducible_of_splits {p : polynomial β}
121+
(h_nz : p ≠ 0) (hp : irreducible p) (hp_splits : splits (ring_hom.id β) p) :
122+
p.degree = 1 :=
123+
begin
124+
rcases hp_splits,
125+
{ contradiction },
126+
{ apply hp_splits hp, simp }
127+
end
128+
120129
lemma exists_root_of_splits {f : polynomial α} (hs : splits i f) (hf0 : degree f ≠ 0) :
121130
∃ x, eval₂ i x f = 0 :=
122131
if hf0 : f = 0 then37, by simp [hf0]⟩

src/group_theory/submonoid/basic.lean

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -339,6 +339,23 @@ lemma closure_Union {ι} (s : ι → set M) : closure (⋃ i, s i) = ⨆ i, clos
339339

340340
end submonoid
341341

342+
section is_unit
343+
344+
/-- The submonoid consisting of the units of a monoid -/
345+
def is_unit.submonoid (M : Type*) [monoid M] : submonoid M :=
346+
{ carrier := set_of is_unit,
347+
one_mem' := by simp only [is_unit_one, set.mem_set_of_eq],
348+
mul_mem' := by { intros a b ha hb, rw set.mem_set_of_eq at *, exact is_unit.mul ha hb } }
349+
350+
lemma is_unit.mem_submonoid_iff {M : Type*} [monoid M] (a : M) :
351+
a ∈ is_unit.submonoid M ↔ is_unit a :=
352+
begin
353+
change a ∈ set_of is_unit ↔ is_unit a,
354+
rw set.mem_set_of_eq
355+
end
356+
357+
end is_unit
358+
342359
namespace monoid_hom
343360

344361
variables {N : Type*} {P : Type*} [monoid N] [monoid P] (S : submonoid M)

src/linear_algebra/eigenspace.lean

Lines changed: 246 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,246 @@
1+
/-
2+
Copyright (c) 2020 Alexander Bentkamp. All rights reserved.
3+
Released under Apache 2.0 license as described in the file LICENSE.
4+
Author: Alexander Bentkamp.
5+
-/
6+
7+
import field_theory.algebraic_closure
8+
import linear_algebra.finsupp
9+
10+
/-!
11+
# Eigenvectors and eigenvalues
12+
13+
This file defines eigenspaces and eigenvalues.
14+
15+
An eigenspace of a linear map `f` for a scalar `μ` is the kernel of the map `(f - μ • id)`. The
16+
nonzero elements of an eigenspace are eigenvectors `x`. They have the property `f x = μ • x`. If
17+
there are eigenvectors for a scalar `μ`, the scalar `μ` is called an eigenvalue.
18+
19+
There is no consensus in the literature whether `0` is an eigenvector. Our definition of
20+
`eigenvector` permits only nonzero vectors. For an eigenvector `x` that may also be `0`, we write
21+
`x ∈ eigenspace f μ`.
22+
23+
## Notations
24+
25+
The expression `algebra_map K (End K V)` appears very often, which is why we use `am` as a local
26+
notation for it.
27+
28+
## References
29+
30+
* [Sheldon Axler, *Down with determinants!*,
31+
https://www.maa.org/sites/default/files/pdf/awards/Axler-Ford-1996.pdf][axler1996]
32+
* https://en.wikipedia.org/wiki/Eigenvalues_and_eigenvectors
33+
34+
## Tags
35+
36+
eigenspace, eigenvector, eigenvalue, eigen
37+
-/
38+
39+
universes u v w
40+
41+
namespace module
42+
namespace End
43+
44+
open vector_space principal_ideal_ring polynomial finite_dimensional
45+
46+
variables {K : Type v} {V : Type w} [add_comm_group V]
47+
48+
local notation `am` := algebra_map K (End K V)
49+
50+
/-- The submodule `eigenspace f μ` for a linear map `f` and a scalar `μ` consists of all vectors `x`
51+
such that `f x = μ • x`. -/
52+
def eigenspace [comm_ring K] [module K V] (f : End K V) (μ : K) : submodule K V :=
53+
(f - am μ).ker
54+
55+
/-- A nonzero element of an eigenspace is an eigenvector. -/
56+
def has_eigenvector [comm_ring K] [module K V] (f : End K V) (μ : K) (x : V) : Prop :=
57+
x ≠ 0 ∧ x ∈ eigenspace f μ
58+
59+
/-- A scalar `μ` is an eigenvalue for a linear map `f` if there are nonzero vectors `x`
60+
such that `f x = μ • x`. -/
61+
def has_eigenvalue [comm_ring K] [module K V] (f : End K V) (a : K) : Prop :=
62+
eigenspace f a ≠ ⊥
63+
64+
lemma mem_eigenspace_iff [comm_ring K] [module K V]
65+
{f : End K V} {μ : K} {x : V} : x ∈ eigenspace f μ ↔ f x = μ • x :=
66+
by rw [eigenspace, linear_map.mem_ker, linear_map.sub_apply, algebra_map_End_apply,
67+
sub_eq_zero]
68+
69+
lemma eigenspace_div [field K] [vector_space K V] (f : End K V) (a b : K) (hb : b ≠ 0) :
70+
eigenspace f (a / b) = (b • f - am a).ker :=
71+
calc
72+
eigenspace f (a / b) = eigenspace f (b⁻¹ * a) : by { dsimp [(/)], rw mul_comm }
73+
... = (f - (b⁻¹ * a) • linear_map.id).ker : rfl
74+
... = (f - b⁻¹ • a • linear_map.id).ker : by rw smul_smul
75+
... = (f - b⁻¹ • am a).ker : rfl
76+
... = (b • (f - b⁻¹ • am a)).ker : by rw linear_map.ker_smul _ b hb
77+
... = (b • f - am a).ker : by rw [smul_sub, smul_inv_smul' hb]
78+
79+
lemma eigenspace_eval₂_polynomial_degree_1 [field K] [vector_space K V]
80+
(f : End K V) (q : polynomial K) (hq : degree q = 1) :
81+
eigenspace f (- q.coeff 0 / q.leading_coeff) = (eval₂ am f q).ker :=
82+
calc
83+
eigenspace f (- q.coeff 0 / q.leading_coeff) = (q.leading_coeff • f - am (- q.coeff 0)).ker
84+
: by { rw eigenspace_div, intro h, rw leading_coeff_eq_zero_iff_deg_eq_bot.1 h at hq, cases hq }
85+
... = (eval₂ am f (C q.leading_coeff * X + C (q.coeff 0))).ker
86+
: by { rw C_mul', simpa [algebra_map, algebra.to_ring_hom] }
87+
... = (eval₂ am f q).ker
88+
: by { congr, apply (eq_X_add_C_of_degree_eq_one hq).symm }
89+
90+
lemma ker_eval₂_ring_hom_noncomm_unit_polynomial [field K] [vector_space K V]
91+
(f : End K V) (c : units (polynomial K)) :
92+
((eval₂_ring_hom_noncomm am (λ x y, (algebra.commutes x y).symm) f) ↑c).ker = ⊥ :=
93+
begin
94+
rw polynomial.eq_C_of_degree_eq_zero (degree_coe_units c),
95+
simp only [eval₂_ring_hom_noncomm, ring_hom.of, ring_hom.coe_mk, eval₂_C],
96+
apply ker_algebra_map_End,
97+
apply coeff_coe_units_zero_ne_zero c
98+
end
99+
100+
/-- Every linear operator on a vector space over an algebraically closed field has
101+
an eigenvalue. (Axler's Theorem 2.1.) -/
102+
lemma exists_eigenvalue
103+
[field K] [is_alg_closed K] [vector_space K V] [finite_dimensional K V] [nontrivial V]
104+
(f : End K V) :
105+
∃ (c : K), f.has_eigenvalue c :=
106+
begin
107+
classical,
108+
-- Choose a nonzero vector `v`.
109+
obtain ⟨v, hv⟩ : ∃ v : V, v ≠ 0 := exists_ne (0 : V),
110+
-- The infinitely many vectors v, f v, f (f v), ... cannot be linearly independent
111+
-- because the vector space is finite dimensional.
112+
have h_lin_dep : ¬ linear_independent K (λ n : ℕ, (f ^ n) v),
113+
{ apply not_linear_independent_of_infinite, },
114+
-- Therefore, there must be a nonzero polynomial `p` such that `p(f) v = 0`.
115+
obtain ⟨p, h_eval_p, h_p_ne_0⟩ : ∃ p, eval₂ am f p v = 0 ∧ p ≠ 0,
116+
{ simp only [not_imp.symm],
117+
exact not_forall.1 (λ h, h_lin_dep ((linear_independent_powers_iff_eval₂ f v).2 h)) },
118+
-- Then `p(f)` is not invertible.
119+
have h_eval_p_not_unit : eval₂_ring_hom_noncomm am _ f p ∉ is_unit.submonoid (End K V),
120+
{ rw [is_unit.mem_submonoid_iff, linear_map.is_unit_iff, linear_map.ker_eq_bot'],
121+
intro h,
122+
exact hv (h v h_eval_p) },
123+
-- Hence, there must be a factor `q` of `p` such that `q(f)` is not invertible.
124+
obtain ⟨q, hq_factor, hq_nonunit⟩ : ∃ q, q ∈ factors p ∧ ¬ is_unit (eval₂ am f q),
125+
{ simp only [←not_imp, (is_unit.mem_submonoid_iff _).symm],
126+
apply not_forall.1 (λ h, h_eval_p_not_unit (ring_hom_mem_submonoid_of_factors_subset_of_units_subset
127+
(eval₂_ring_hom_noncomm am (λ x y, (algebra.commutes x y).symm) f)
128+
(is_unit.submonoid (End K V)) p h_p_ne_0 h _)),
129+
simp only [is_unit.mem_submonoid_iff, linear_map.is_unit_iff],
130+
apply ker_eval₂_ring_hom_noncomm_unit_polynomial },
131+
-- Since the field is algebraically closed, `q` has degree 1.
132+
have h_deg_q : q.degree = 1 := is_alg_closed.degree_eq_one_of_irreducible _
133+
(ne_zero_of_mem_factors h_p_ne_0 hq_factor)
134+
((factors_spec p h_p_ne_0).1 q hq_factor),
135+
-- Then the kernel of `q(f)` is an eigenspace.
136+
have h_eigenspace: eigenspace f (-q.coeff 0 / q.leading_coeff) = (eval₂ am f q).ker,
137+
from eigenspace_eval₂_polynomial_degree_1 f q h_deg_q,
138+
-- Since `q(f)` is not invertible, the kernel is not `⊥`, and thus there exists an eigenvalue.
139+
show ∃ (c : K), f.has_eigenvalue c,
140+
{ use -q.coeff 0 / q.leading_coeff,
141+
rw [has_eigenvalue, h_eigenspace],
142+
intro h_eval_ker,
143+
exact hq_nonunit ((linear_map.is_unit_iff (eval₂ am f q)).2 h_eval_ker) }
144+
end
145+
146+
/-- Eigenvectors corresponding to distinct eigenvalues of a linear operator are linearly
147+
independent. (Axler's Proposition 2.2)
148+
149+
We use the eigenvalues as indexing set to ensure that there is only one eigenvector for each
150+
eigenvalue in the image of `xs`. -/
151+
lemma eigenvectors_linear_independent [field K] [vector_space K V]
152+
(f : End K V) (μs : set K) (xs : μs → V)
153+
(h_eigenvec : ∀ μ : μs, f.has_eigenvector μ (xs μ)) :
154+
linear_independent K xs :=
155+
begin
156+
classical,
157+
-- We need to show that if a linear combination `l` of the eigenvectors `xs` is `0`, then all
158+
-- its coefficients are zero.
159+
suffices : ∀ l, finsupp.total μs V K xs l = 0 → l = 0,
160+
{ rw linear_independent_iff,
161+
apply this },
162+
intros l hl,
163+
-- We apply induction on the finite set of eigenvalues whose eigenvectors have nonzero
164+
-- coefficients, i.e. on the support of `l`.
165+
induction h_l_support : l.support using finset.induction with μ₀ l_support' hμ₀ ih generalizing l,
166+
-- If the support is empty, all coefficients are zero and we are done.
167+
{ exact finsupp.support_eq_empty.1 h_l_support },
168+
-- Now assume that the support of `l` contains at least one eigenvalue `μ₀`. We define a new
169+
-- linear combination `l'` to apply the induction hypothesis on later. The linear combination `l'`
170+
-- is derived from `l` by multiplying the coefficient of the eigenvector with eigenvalue `μ`
171+
-- by `μ - μ₀`.
172+
-- To get started, we define `l'` as a function `l'_f : μs → K` with potentially infinite support.
173+
{ let l'_f : μs → K := (λ μ : μs, (↑μ - ↑μ₀) * l μ),
174+
-- The support of `l'_f` is the support of `l` without `μ₀`.
175+
have h_l_support' : ∀ (μ : μs), μ ∈ l_support' ↔ l'_f μ ≠ 0 ,
176+
{ intro μ,
177+
suffices : μ ∈ l_support' → μ ≠ μ₀,
178+
{ simp [l'_f, ← finsupp.not_mem_support_iff, h_l_support, sub_eq_zero, ←subtype.ext_iff],
179+
tauto },
180+
rintro hμ rfl,
181+
contradiction },
182+
-- Now we can define `l'_f` as an actual linear combination `l'` because we know that the
183+
-- support is finite.
184+
let l' : μs →₀ K :=
185+
{ to_fun := l'_f, support := l_support', mem_support_to_fun := h_l_support' },
186+
-- The linear combination `l'` over `xs` adds up to `0`.
187+
have total_l' : finsupp.total μs V K xs l' = 0,
188+
{ let g := f - am μ₀,
189+
have h_gμ₀: g (l μ₀ • xs μ₀) = 0,
190+
by rw [linear_map.map_smul, linear_map.sub_apply, mem_eigenspace_iff.1 (h_eigenvec _).2,
191+
algebra_map_End_apply, sub_self, smul_zero],
192+
have h_useless_filter : finset.filter (λ (a : μs), l'_f a ≠ 0) l_support' = l_support',
193+
{ rw finset.filter_congr _,
194+
{ apply finset.filter_true },
195+
{ apply_instance },
196+
exact λ μ hμ, (iff_true _).mpr ((h_l_support' μ).1 hμ) },
197+
have bodies_eq : ∀ (μ : μs), l'_f μ • xs μ = g (l μ • xs μ),
198+
{ intro μ,
199+
dsimp only [g, l'_f],
200+
rw [linear_map.map_smul, linear_map.sub_apply, mem_eigenspace_iff.1 (h_eigenvec _).2,
201+
algebra_map_End_apply, ←sub_smul, smul_smul, mul_comm] },
202+
rw [←linear_map.map_zero g, ←hl, finsupp.total_apply, finsupp.total_apply,
203+
finsupp.sum, finsupp.sum, linear_map.map_sum, h_l_support,
204+
finset.sum_insert hμ₀, h_gμ₀, zero_add],
205+
refine finset.sum_congr rfl (λ μ _, _),
206+
apply bodies_eq },
207+
-- Therefore, by the induction hypothesis, all coefficients in `l'` are zero.
208+
have l'_eq_0 : l' = 0 := ih l' total_l' rfl,
209+
-- By the defintion of `l'`, this means that `(μ - μ₀) * l μ = 0` for all `μ`.
210+
have h_mul_eq_0 : ∀ μ : μs, (↑μ - ↑μ₀) * l μ = 0,
211+
{ intro μ,
212+
calc (↑μ - ↑μ₀) * l μ = l' μ : rfl
213+
... = 0 : by { rw [l'_eq_0], refl } },
214+
-- Thus, the coefficients in `l` for all `μ ≠ μ₀` are `0`.
215+
have h_lμ_eq_0 : ∀ μ : μs, μ ≠ μ₀ → l μ = 0,
216+
{ intros μ hμ,
217+
apply or_iff_not_imp_left.1 (mul_eq_zero.1 (h_mul_eq_0 μ)),
218+
rwa [sub_eq_zero, ←subtype.ext_iff] },
219+
-- So if we sum over all these coefficients, we obtain `0`.
220+
have h_sum_l_support'_eq_0 : finset.sum l_support' (λ (μ : ↥μs), l μ • xs μ) = 0,
221+
{ rw ←finset.sum_const_zero,
222+
apply finset.sum_congr rfl,
223+
intros μ hμ,
224+
rw h_lμ_eq_0,
225+
apply zero_smul,
226+
intro h,
227+
rw h at hμ,
228+
contradiction },
229+
-- The only potentially nonzero coefficient in `l` is the one corresponding to `μ₀`. But since
230+
-- the overall sum is `0` by assumption, this coefficient must also be `0`.
231+
have : l μ₀ = 0,
232+
{ rw [finsupp.total_apply, finsupp.sum, h_l_support,
233+
finset.sum_insert hμ₀, h_sum_l_support'_eq_0, add_zero] at hl,
234+
by_contra h,
235+
exact (h_eigenvec μ₀).1 ((smul_eq_zero.1 hl).resolve_left h) },
236+
-- Thus, all coefficients in `l` are `0`.
237+
show l = 0,
238+
{ ext μ,
239+
by_cases h_cases : μ = μ₀,
240+
{ rw h_cases,
241+
assumption },
242+
exact h_lμ_eq_0 μ h_cases } }
243+
end
244+
245+
end End
246+
end module

src/linear_algebra/finite_dimensional.lean

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -255,6 +255,15 @@ begin
255255
apply cardinal.nat_lt_omega,
256256
end
257257

258+
lemma not_linear_independent_of_infinite {ι : Type w} [inf : infinite ι] [finite_dimensional K V]
259+
(v : ι → V) : ¬ linear_independent K v :=
260+
begin
261+
intro h_lin_indep,
262+
have : ¬ omega ≤ mk ι := not_le.mpr (lt_omega_of_linear_independent h_lin_indep),
263+
have : omega ≤ mk ι := infinite_iff.mp inf,
264+
contradiction
265+
end
266+
258267
/-- A finite dimensional space has positive `findim` iff it has a nonzero element. -/
259268
lemma findim_pos_iff_exists_ne_zero [finite_dimensional K V] : 0 < findim K V ↔ ∃ x : V, x ≠ 0 :=
260269
iff.trans (by { rw ← findim_eq_dim, norm_cast }) (@dim_pos_iff_exists_ne_zero K V _ _ _)

src/linear_algebra/finsupp.lean

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -395,6 +395,18 @@ lemma total_comap_domain
395395
(l.support.preimage f hf).sum (λ i, (l (f i)) • (v i)) :=
396396
by rw finsupp.total_apply; refl
397397

398+
lemma total_on_finset
399+
{s : finset α} {f : α → R} (g : α → M) (hf : ∀ a, f a ≠ 0 → a ∈ s):
400+
finsupp.total α M R g (finsupp.on_finset s f hf) =
401+
finset.sum s (λ (x : α), f x • g x) :=
402+
begin
403+
simp only [finsupp.total_apply, finsupp.sum, finsupp.on_finset_apply, finsupp.support_on_finset],
404+
rw finset.sum_filter_of_ne,
405+
intros x hx h,
406+
contrapose! h,
407+
simp [h],
408+
end
409+
398410
end total
399411

400412
/-- An equivalence of domains induces a linear equivalence of finitely supported functions. -/

0 commit comments

Comments
 (0)