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

Commit 73caeaa

Browse files
robertylewisdigama0
andcommitted
perf(tactic/linarith): implement redundancy test to reduce number of comparisons (#3079)
This implements a redundancy check in `linarith` which can drastically reduce the size of the sets of comparisons that the tactic needs to deal with. `linarith` iteratively transforms sets of inequalities to equisatisfiable sets by eliminating a single variable. If there are `n` comparisons in the set, we expect `O(n^2)` comparisons after one elimination step. Many of these comparisons are redundant, i.e. removing them from the set does not change its satisfiability. Deciding redundancy is expensive, but there are cheap approximations that are very useful in practice. This PR tests comparisons for non-minimal history (http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.51.493&rep=rep1&type=pdf p.13, theorem 7). Non-minimal history implies redundancy. Co-authored-by: Mario Carneiro <di.gama@gmail.com> Co-authored-by: Rob Lewis <rob.y.lewis@gmail.com>
1 parent 21bf873 commit 73caeaa

File tree

3 files changed

+168
-32
lines changed

3 files changed

+168
-32
lines changed

src/analysis/special_functions/trigonometric.lean

Lines changed: 7 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -766,23 +766,23 @@ lemma cos_le_cos_of_nonneg_of_le_pi {x y : ℝ} (hx₁ : 0 ≤ x) (hx₂ : x ≤
766766
(le_of_lt ∘ cos_lt_cos_of_nonneg_of_le_pi hx₁ hx₂ hy₁ hy₂)
767767
(λ h, h ▸ le_refl _)
768768

769-
lemma sin_lt_sin_of_le_of_le_pi_div_two {x y : ℝ} (hx₁ : -(π / 2) ≤ x) (hx₂ : x ≤ π / 2) (hy₁ : -(π / 2) ≤ y)
769+
lemma sin_lt_sin_of_le_of_le_pi_div_two {x y : ℝ} (hx₁ : -(π / 2) ≤ x)
770770
(hy₂ : y ≤ π / 2) (hxy : x < y) : sin x < sin y :=
771771
by rw [← cos_sub_pi_div_two, ← cos_sub_pi_div_two, ← cos_neg (x - _), ← cos_neg (y - _)];
772772
apply cos_lt_cos_of_nonneg_of_le_pi; linarith
773773

774-
lemma sin_le_sin_of_le_of_le_pi_div_two {x y : ℝ} (hx₁ : -(π / 2) ≤ x) (hx₂ : x ≤ π / 2) (hy₁ : -(π / 2) ≤ y)
774+
lemma sin_le_sin_of_le_of_le_pi_div_two {x y : ℝ} (hx₁ : -(π / 2) ≤ x)
775775
(hy₂ : y ≤ π / 2) (hxy : x ≤ y) : sin x ≤ sin y :=
776776
(lt_or_eq_of_le hxy).elim
777-
(le_of_lt ∘ sin_lt_sin_of_le_of_le_pi_div_two hx₁ hx₂ hy₁ hy₂)
777+
(le_of_lt ∘ sin_lt_sin_of_le_of_le_pi_div_two hx₁ hy₂)
778778
(λ h, h ▸ le_refl _)
779779

780780
lemma sin_inj_of_le_of_le_pi_div_two {x y : ℝ} (hx₁ : -(π / 2) ≤ x) (hx₂ : x ≤ π / 2) (hy₁ : -(π / 2) ≤ y)
781781
(hy₂ : y ≤ π / 2) (hxy : sin x = sin y) : x = y :=
782782
match lt_trichotomy x y with
783-
| or.inl h := absurd (sin_lt_sin_of_le_of_le_pi_div_two hx₁ hx₂ hy₁ hy₂ h) (by rw hxy; exact lt_irrefl _)
783+
| or.inl h := absurd (sin_lt_sin_of_le_of_le_pi_div_two hx₁ hy₂ h) (by rw hxy; exact lt_irrefl _)
784784
| or.inr (or.inl h) := h
785-
| or.inr (or.inr h) := absurd (sin_lt_sin_of_le_of_le_pi_div_two hy₁ hy₂ hx₁ hx₂ h) (by rw hxy; exact lt_irrefl _)
785+
| or.inr (or.inr h) := absurd (sin_lt_sin_of_le_of_le_pi_div_two hy₁ hx₂ h) (by rw hxy; exact lt_irrefl _)
786786
end
787787

788788
lemma cos_inj_of_nonneg_of_le_pi {x y : ℝ} (hx₁ : 0 ≤ x) (hx₂ : x ≤ π) (hy₁ : 0 ≤ y) (hy₂ : y ≤ π)
@@ -986,8 +986,7 @@ lemma arcsin_nonneg {x : ℝ} (hx : 0 ≤ x) : 0 ≤ arcsin x :=
986986
if hx₁ : x ≤ 1 then
987987
not_lt.1 (λ h, not_lt.2 hx begin
988988
have := sin_lt_sin_of_le_of_le_pi_div_two
989-
(neg_pi_div_two_le_arcsin _) (arcsin_le_pi_div_two _)
990-
(neg_nonpos.2 (le_of_lt pi_div_two_pos)) (le_of_lt pi_div_two_pos) h,
989+
(neg_pi_div_two_le_arcsin _) (le_of_lt pi_div_two_pos) h,
991990
rw [real.sin_arcsin, sin_zero] at this; linarith
992991
end)
993992
else by rw [arcsin, dif_neg]; simp [hx₁]
@@ -1091,8 +1090,7 @@ lemma tan_lt_tan_of_nonneg_of_lt_pi_div_two {x y : ℝ} (hx₁ : 0 ≤ x) (hx₂
10911090
begin
10921091
rw [tan_eq_sin_div_cos, tan_eq_sin_div_cos],
10931092
exact div_lt_div
1094-
(sin_lt_sin_of_le_of_le_pi_div_two (by linarith) (le_of_lt hx₂)
1095-
(by linarith) (le_of_lt hy₂) hxy)
1093+
(sin_lt_sin_of_le_of_le_pi_div_two (by linarith) (le_of_lt hy₂) hxy)
10961094
(cos_le_cos_of_nonneg_of_le_pi hx₁ (by linarith) hy₁ (by linarith) (le_of_lt hxy))
10971095
(sin_nonneg_of_nonneg_of_le_pi hy₁ (by linarith))
10981096
(cos_pos_of_neg_pi_div_two_lt_of_lt_pi_div_two (by linarith) hy₂)

src/meta/rb_map.lean

Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,22 @@ meta def of_list_core {key} (base : native.rb_set key) : list key → native.rb_
4545
| [] := base
4646
| (x::xs) := native.rb_set.insert (of_list_core xs) x
4747

48+
/--
49+
`of_list l` transforms a list `l : list key` into an `rb_set`,
50+
inferring an order on the type `key`.
51+
-/
52+
meta def of_list {key} [has_lt key] [decidable_rel ((<) : key → key → Prop)] :
53+
list key → rb_set key :=
54+
of_list_core mk_rb_set
55+
56+
/--
57+
`sdiff s1 s2` returns the set of elements that are in `s1` but not in `s2`.
58+
It does so by folding over `s2`. If `s1` is significantly smaller than `s2`,
59+
it may be worth it to reverse the fold.
60+
-/
61+
meta def sdiff {α} (s1 s2 : rb_set α) : rb_set α :=
62+
s2.fold s1 $ λ v s, s.erase v
63+
4864
end rb_set
4965

5066
namespace rb_map

src/tactic/linarith.lean

Lines changed: 145 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,9 @@ meta def nat.to_pexpr : ℕ → pexpr
2323
| 1 := ``(1)
2424
| n := if n % 2 = 0 then ``(bit0 %%(nat.to_pexpr (n/2))) else ``(bit1 %%(nat.to_pexpr (n/2)))
2525

26+
2627
open native
28+
2729
namespace linarith
2830

2931
section lemmas
@@ -177,6 +179,10 @@ def cmp : linexp → linexp → ordering
177179
else if z2 < z1 then ordering.gt
178180
else cmp t1 t2
179181

182+
/-- `l.vars` returns the list of variables that occur in `l`. -/
183+
def vars (l : linexp) : list ℕ :=
184+
l.map prod.fst
185+
180186
end linexp
181187

182188
section datatypes
@@ -216,11 +222,16 @@ The represented term is `coeffs.sum (λ ⟨k, v⟩, v * Var[k])`.
216222
str determines the direction of the comparison -- is it < 0, ≤ 0, or = 0?
217223
-/
218224
@[derive inhabited]
219-
meta structure comp : Type :=
225+
structure comp : Type :=
220226
(str : ineq)
221227
(coeffs : linexp)
222228

223-
meta inductive comp_source : Type
229+
/-- `c.vars` returns the list of variables that appear in the linear expression contained in `c`. -/
230+
def comp.vars : comp → list ℕ :=
231+
linexp.vars ∘ comp.coeffs
232+
233+
@[derive inhabited]
234+
inductive comp_source : Type
224235
| assump : ℕ → comp_source
225236
| add : comp_source → comp_source → comp_source
226237
| scale : ℕ → comp_source → comp_source
@@ -230,17 +241,76 @@ meta def comp_source.flatten : comp_source → rb_map ℕ ℕ
230241
| (comp_source.add c1 c2) := (comp_source.flatten c1).add (comp_source.flatten c2)
231242
| (comp_source.scale n c) := (comp_source.flatten c).map (λ v, v * n)
232243

233-
meta def comp_source.to_string : comp_source → string
244+
def comp_source.to_string : comp_source → string
234245
| (comp_source.assump e) := to_string e
235246
| (comp_source.add c1 c2) := comp_source.to_string c1 ++ " + " ++ comp_source.to_string c2
236247
| (comp_source.scale n c) := to_string n ++ " * " ++ comp_source.to_string c
237248

238249
meta instance comp_source.has_to_format : has_to_format comp_source :=
239250
⟨λ a, comp_source.to_string a⟩
240251

241-
meta structure pcomp :=
252+
/--
253+
A `pcomp` stores a linear comparison `Σ cᵢ*xᵢ R 0`,
254+
along with information about how this comparison was derived.
255+
256+
The original expressions fed into `linarith` are each assigned a unique natural number label.
257+
The *historical set* `pcomp.history` stores the labels of expressions
258+
that were used in deriving the current `pcomp`.
259+
260+
Variables are also indexed by natural numbers. The sets `pcomp.effective`, `pcomp.implicit`,
261+
and `pcomp.vars` contain variable indices.
262+
263+
* `pcomp.vars` contains the variables that appear in `pcomp.c`. We store them in `pcomp` to
264+
avoid recomputing the set, which requires folding over a list. (TODO: is this really needed?)
265+
* `pcomp.effective` contains the variables that have been effectively eliminated from `pcomp`.
266+
A variable `n` is said to be *effectively eliminated* in `pcomp` if the elimination of `n`
267+
produced at least one of the ancestors of `pcomp`.
268+
* `pcomp.implicit` contains the variables that have been implicitly eliminated from `pcomp`.
269+
A variable `n` is said to be *implicitly eliminated* in `pcomp` if it satisfies the following
270+
properties:
271+
- There is some `ancestor` of `pcomp` such that `n` appears in `ancestor.vars`.
272+
- `n` does not appear in `pcomp.vars`.
273+
- `n` was not effectively eliminated.
274+
275+
We track these sets in order to compute whether the history of a `pcomp` is *minimal*.
276+
Checking this directly is expensive, but effective approximations can be defined in terms of these
277+
sets. During the variable elimination process, a `pcomp` with non-minimal history can be discarded.
278+
-/
279+
meta structure pcomp : Type :=
242280
(c : comp)
243281
(src : comp_source)
282+
(history : rb_set ℕ)
283+
(effective : rb_set ℕ)
284+
(implicit : rb_set ℕ)
285+
(vars : rb_set ℕ)
286+
287+
/--
288+
Any comparison whose history is not minimal is redundant,
289+
and need not be included in the new set of comparisons.
290+
291+
`elimed_ge : ℕ` is a natural number such that all variables with index ≥ `elimed_ge` have been
292+
removed from the system.
293+
294+
This test is an overapproximation to minimality. It gives necessary but not sufficient conditions.
295+
If the history of `c` is minimal, then `c.maybe_minimal` is true,
296+
but `c.maybe_minimal` may also be true for some `c` with minimal history.
297+
Thus, if `c.maybe_minimal` is false, `c` is known not to be minimal and must be redundant.
298+
299+
See http://citeseerx.ist.psu.edu/viewdoc/download?doi=10.1.1.51.493&rep=rep1&type=pdf p.13
300+
(Theorem 7).
301+
302+
The condition described there considers only implicitly eliminated variables that have been
303+
officially eliminated from the system. This is not the case for every implicitly eliminated variable.
304+
Consider eliminating `z` from `{x + y + z < 0, x - y - z < 0}`. The result is the set
305+
`{2*x < 0}`; `y` is implicitly but not officially eliminated.
306+
307+
This implementation of Fourier-Motzkin elimination processes variables in decreasing order of
308+
indices. Immediately after a step that eliminates variable `k`, variable `k'` has been eliminated
309+
iff `k' ≥ k`. Thus we can compute the intersection of officially and implicitly eliminated variables
310+
by taking the set of implicitly eliminated variables with indices ≥ `elimed_ge`.
311+
-/
312+
meta def pcomp.maybe_minimal (c : pcomp) (elimed_ge : ℕ) : bool :=
313+
c.history.size ≤ 1 + ((c.implicit.filter (≥ elimed_ge)).union c.effective).size
244314

245315
/-- `comp` has a lex order. First the `ineq`s are compared, then the `coeff`s. -/
246316
meta def comp.cmp : comp → comp → ordering
@@ -268,10 +338,47 @@ meta def comp.add (c1 c2 : comp) : comp :=
268338
⟨c1.str.max c2.str, c1.coeffs.add c2.coeffs⟩
269339

270340
meta def pcomp.scale (c : pcomp) (n : ℕ) : pcomp :=
271-
⟨c.c.scale n, comp_source.scale n c.src
341+
{c with c := c.c.scale n, src := c.src.scale n}
272342

273-
meta def pcomp.add (c1 c2 : pcomp) : pcomp :=
274-
⟨c1.c.add c2.c, comp_source.add c1.src c2.src⟩
343+
/--
344+
`pcomp.add c1 c2 elim_var` creates the result of summing the linear comparisons `c1` and `c2`,
345+
during the process of eliminating the variable `elim_var`.
346+
The computation assumes, but does not enforce, that `elim_var` appears in both `c1` and `c2`
347+
and does not appear in the sum.
348+
349+
Computing the sum of the two comparisons is easy; the complicated details lie in tracking the
350+
additional fields of `pcomp`.
351+
352+
* The historical set `pcomp.history` of `c1 + c2` is the union of the two historical sets.
353+
* We recompute the variables that appear in `c1 + c2` from the newly created `linexp`,
354+
since some may have been implicitly eliminated.
355+
* The effectively eliminated variables of `c1 + c2` are the union of the two effective sets,
356+
with `elim_var` inserted.
357+
* The implicitly eliminated variables of `c1 + c2` are those that appear in at least one of
358+
`c1.vars` and `c2.vars` but not in `(c1 + c2).vars`, excluding `elim_var`.
359+
-/
360+
meta def pcomp.add (c1 c2 : pcomp) (elim_var : ℕ) : pcomp :=
361+
let c := c1.c.add c2.c,
362+
src := c1.src.add c2.src,
363+
history := c1.history.union c2.history,
364+
vars := native.rb_set.of_list c.vars,
365+
effective := (c1.effective.union c2.effective).insert elim_var,
366+
implicit := ((c1.vars.union c2.vars).sdiff vars).erase elim_var in
367+
⟨c, src, history, effective, implicit, vars⟩
368+
369+
/--
370+
`pcomp.assump c n` creates a `pcomp` whose comparison is `c` and whose source is
371+
`comp_source.assump n`, that is, `c` is derived from the `n`th hypothesis.
372+
The history is the singleton set `{n}`.
373+
No variables have been eliminated (effectively or implicitly).
374+
-/
375+
meta def pcomp.assump (c : comp) (n : ℕ) : pcomp :=
376+
{ c := c,
377+
src := comp_source.assump n,
378+
history := mk_rb_set.insert n,
379+
effective := mk_rb_set,
380+
implicit := mk_rb_set,
381+
vars := rb_set.of_list c.vars }
275382

276383
meta instance pcomp.to_format : has_to_format pcomp :=
277384
⟨λ p, to_fmt p.c.coeffs ++ to_string p.c.str ++ "0"
@@ -288,42 +395,36 @@ end datatypes
288395
section fm_elim
289396

290397
/-- If `c1` and `c2` both contain variable `a` with opposite coefficients,
291-
produces `v1`, `v2`, and `c` such that `a` has been cancelled in `c := v1*c1 + v2*c2`. -/
292-
meta def elim_var (c1 c2 : comp) (a : ℕ) : option (ℕ × ℕ × comp) :=
398+
produces `v1`, `v2` such that `a` has been cancelled in `v1*c1 + v2*c2`. -/
399+
meta def elim_var (c1 c2 : comp) (a : ℕ) : option (ℕ × ℕ) :=
293400
let v1 := c1.coeff_of a,
294401
v2 := c2.coeff_of a in
295402
if v1 * v2 < 0 then
296403
let vlcm := nat.lcm v1.nat_abs v2.nat_abs,
297404
v1' := vlcm / v1.nat_abs,
298405
v2' := vlcm / v2.nat_abs in
299-
some ⟨v1', v2', comp.add (c1.scale v1') (c2.scale v2')
406+
some ⟨v1', v2'⟩
300407
else none
301408

302409
meta def pelim_var (p1 p2 : pcomp) (a : ℕ) : option pcomp :=
303-
do (n1, n2, c) ← elim_var p1.c p2.c a,
304-
return ⟨c, comp_source.add (p1.src.scale n1) (p2.src.scale n2)
410+
do (n1, n2) ← elim_var p1.c p2.c a,
411+
return $ (p1.scale n1).add (p2.scale n2) a
305412

306413
meta def comp.is_contr (c : comp) : bool := c.coeffs.empty ∧ c.str = ineq.lt
307414

308415
meta def pcomp.is_contr (p : pcomp) : bool := p.c.is_contr
309416

310417
meta def elim_with_set (a : ℕ) (p : pcomp) (comps : rb_set pcomp) : rb_set pcomp :=
311-
if ¬ p.c.coeffs.contains a then mk_pcomp_set.insert p else
312418
comps.fold mk_pcomp_set $ λ pc s,
313419
match pelim_var p pc a with
314-
| some pc := s.insert pc
420+
| some pc := if pc.maybe_minimal a then s.insert pc else s
315421
| none := s
316422
end
317423

318424
/--
319425
The state for the elimination monad.
320426
* `vars`: the set of variables present in `comps`
321427
* `comps`: a set of comparisons
322-
* `inputs`: a set of pairs of exprs `(t, pf)`, where `t` is a term and `pf` is a proof that
323-
`t {<, ≤, =} 0`, indexed by `ℕ`.
324-
* `has_false`: stores a `pcomp` of `0 < 0` if one has been found
325-
326-
TODO: is it more efficient to store comps as a list, to avoid comparisons?
327428
-/
328429
meta structure linarith_structure :=
329430
(vars : rb_set ℕ)
@@ -351,13 +452,34 @@ end
351452
meta def update (vars : rb_set ℕ) (comps : rb_set pcomp) : linarith_monad unit :=
352453
state_t.put ⟨vars, comps⟩ >> validate
353454

455+
/--
456+
`split_set_by_var_sign a comps` partitions the set `comps` into three parts.
457+
* `pos` contains the elements of `comps` in which `a` has a positive coefficient.
458+
* `neg` contains the elements of `comps` in which `a` has a negative coefficient.
459+
* `not_present` contains the elements of `comps` in which `a` has coefficient 0.
460+
461+
Returns `(pos, neg, not_present)`.
462+
-/
463+
meta def split_set_by_var_sign (a : ℕ) (comps : rb_set pcomp) :
464+
rb_set pcomp × rb_set pcomp × rb_set pcomp :=
465+
comps.fold ⟨mk_pcomp_set, mk_pcomp_set, mk_pcomp_set⟩ $ λ pc ⟨pos, neg, not_present⟩,
466+
let n := pc.c.coeff_of a in
467+
if n > 0 then ⟨pos.insert pc, neg, not_present⟩
468+
else if n < 0 then ⟨pos, neg.insert pc, not_present⟩
469+
else ⟨pos, neg, not_present.insert pc⟩
470+
471+
/--
472+
`monad.elim_var a` performs one round of Fourier-Motzkin elimination, eliminating the variable `a`
473+
from the `linarith` state.
474+
-/
354475
meta def monad.elim_var (a : ℕ) : linarith_monad unit :=
355476
do vs ← get_vars,
356477
when (vs.contains a) $
357-
do comps ← get_comps,
358-
let cs' := comps.fold mk_pcomp_set (λ p s, s.union (elim_with_set a p comps)),
478+
do ⟨pos, neg, not_present⟩ ← split_set_by_var_sign a <$> get_comps,
479+
let cs' := pos.fold not_present (λ p s, s.union (elim_with_set a p neg)),
359480
update (vs.erase a) cs'
360481

482+
361483
meta def elim_all_vars : linarith_monad unit :=
362484
get_var_list >>= list.mmap' monad.elim_var
363485

@@ -516,8 +638,8 @@ do pftps ← l.mmap infer_type,
516638
let prmap := rb_map.of_list $ lz.map (λ ⟨n, x⟩, (n, x.1)),
517639
let vars : rb_set ℕ := rb_map.set_of_list $ list.range map.size,
518640
let pc : rb_set pcomp :=
519-
rb_set.of_list_core mk_pcomp_set $ lz.map (λ ⟨n, x⟩, x.2, comp_source.assump n⟩),
520-
return (vars, pc⟩, prmap)
641+
rb_set.of_list_core mk_pcomp_set $ lz.map (λ ⟨n, x⟩, pcomp.assump x.2 n),
642+
return ({vars := vars, comps := pc}, prmap)
521643

522644
meta def linarith_monad.run (red : transparency) {α} (tac : linarith_monad α) (l : list expr) :
523645
tactic ((pcomp ⊕ α) × rb_map ℕ (expr × expr)) :=

0 commit comments

Comments
 (0)