/-
Copyright (c) 2019 Jeremy Avigad. All rights reserved.
Released under Apache 2.0 license as described in the file LICENSE.
Authors: Jeremy Avigad, Yury Kudryashov
-/
import analysis.normed_space.basic
import topology.local_homeomorph

/-!
# Asymptotics

We introduce these relations:

* `is_O_with c f g l` : "f is big O of g along l with constant c";
* `is_O f g l` : "f is big O of g along l";
* `is_o f g l` : "f is little o of g along l".

Here `l` is any filter on the domain of `f` and `g`, which are assumed to be the same. The codomains
of `f` and `g` do not need to be the same; all that is needed that there is a norm associated with
these types, and it is the norm that is compared asymptotically.

The relation `is_O_with c` is introduced to factor out common algebraic arguments in the proofs of
similar properties of `is_O` and `is_o`. Usually proofs outside of this file should use `is_O`
instead.

Often the ranges of `f` and `g` will be the real numbers, in which case the norm is the absolute
value. In general, we have

  `is_O f g l ↔ is_O (λ x, ∥f x∥) (λ x, ∥g x∥) l`,

and similarly for `is_o`. But our setup allows us to use the notions e.g. with functions
to the integers, rationals, complex numbers, or any normed vector space without mentioning the
norm explicitly.

If `f` and `g` are functions to a normed field like the reals or complex numbers and `g` is always
nonzero, we have

  `is_o f g l ↔ tendsto (λ x, f x / (g x)) l (𝓝 0)`.

In fact, the right-to-left direction holds without the hypothesis on `g`, and in the other direction
it suffices to assume that `f` is zero wherever `g` is. (This generalization is useful in defining
the Fréchet derivative.)
-/

open filter set
open_locale topological_space big_operators classical filter nnreal

namespace asymptotics

variables {α : Type*} {β : Type*} {E : Type*} {F : Type*} {G : Type*}
  {E' : Type*} {F' : Type*} {G' : Type*} {R : Type*} {R' : Type*} {𝕜 : Type*} {𝕜' : Type*}

variables [has_norm E] [has_norm F] [has_norm G] [normed_group E'] [normed_group F']
  [normed_group G'] [normed_ring R] [normed_ring R'] [normed_field 𝕜] [normed_field 𝕜']
  {c c' : ℝ} {f : α → E} {g : α → F} {k : α → G} {f' : α → E'} {g' : α → F'} {k' : α → G'}
  {l l' : filter α}

section defs

/-! ### Definitions -/

/-- This version of the Landau notation `is_O_with C f g l` where `f` and `g` are two functions on
a type `α` and `l` is a filter on `α`, means that eventually for `l`, `∥f∥` is bounded by `C * ∥g∥`.
In other words, `∥f∥ / ∥g∥` is eventually bounded by `C`, modulo division by zero issues that are
avoided by this definition. Probably you want to use `is_O` instead of this relation. -/
@[irreducible]
def is_O_with (c : ℝ) (f : α → E) (g : α → F) (l : filter α) : Prop :=
∀ᶠ x in l, ∥ f x ∥ ≤ c * ∥ g x ∥

/-- Definition of `is_O_with`. We record it in a lemma as we will set `is_O_with` to be irreducible
at the end of this file. -/
lemma is_O_with_iff {c : ℝ} {f : α → E} {g : α → F} {l : filter α} :
  is_O_with c f g l ↔ ∀ᶠ x in l, ∥ f x ∥ ≤ c * ∥ g x ∥ := by rw is_O_with

alias is_O_with_iff ↔ asymptotics.is_O_with.bound asymptotics.is_O_with.of_bound

/-- The Landau notation `is_O f g l` where `f` and `g` are two functions on a type `α` and `l` is
a filter on `α`, means that eventually for `l`, `∥f∥` is bounded by a constant multiple of `∥g∥`.
In other words, `∥f∥ / ∥g∥` is eventually bounded, modulo division by zero issues that are avoided
by this definition. -/
@[irreducible]
def is_O (f : α → E) (g : α → F) (l : filter α) : Prop := ∃ c : ℝ, is_O_with c f g l

/-- Definition of `is_O` in terms of `is_O_with`. We record it in a lemma as we will set
`is_O` to be irreducible at the end of this file. -/
lemma is_O_iff_is_O_with {f : α → E} {g : α → F} {l : filter α} :
  is_O f g l ↔ ∃ c : ℝ, is_O_with c f g l := by rw is_O

/-- Definition of `is_O` in terms of filters. We record it in a lemma as we will set
`is_O` to be irreducible at the end of this file. -/
lemma is_O_iff {f : α → E} {g : α → F} {l : filter α} :
  is_O f g l ↔ ∃ c : ℝ, ∀ᶠ x in l, ∥ f x ∥ ≤ c * ∥ g x ∥ := by simp [is_O, is_O_with]

lemma is_O.of_bound (c : ℝ) {f : α → E} {g : α → F} {l : filter α}
  (h : ∀ᶠ x in l, ∥ f x ∥ ≤ c * ∥ g x ∥) : is_O f g l := is_O_iff.2 ⟨c, h⟩

lemma is_O.bound {f : α → E} {g : α → F} {l : filter α} :
  is_O f g l → ∃ c : ℝ, ∀ᶠ x in l, ∥ f x ∥ ≤ c * ∥ g x ∥ := is_O_iff.1

/-- The Landau notation `is_o f g l` where `f` and `g` are two functions on a type `α` and `l` is
a filter on `α`, means that eventually for `l`, `∥f∥` is bounded by an arbitrarily small constant
multiple of `∥g∥`. In other words, `∥f∥ / ∥g∥` tends to `0` along `l`, modulo division by zero
issues that are avoided by this definition. -/
@[irreducible]
def is_o (f : α → E) (g : α → F) (l : filter α) : Prop := ∀ ⦃c : ℝ⦄, 0 < c → is_O_with c f g l

/-- Definition of `is_o` in terms of `is_O_with`. We record it in a lemma as we will set
`is_o` to be irreducible at the end of this file. -/
lemma is_o_iff_forall_is_O_with {f : α → E} {g : α → F} {l : filter α} :
  is_o f g l ↔ ∀ ⦃c : ℝ⦄, 0 < c → is_O_with c f g l := by rw is_o

alias is_o_iff_forall_is_O_with ↔ asymptotics.is_o.forall_is_O_with asymptotics.is_o.of_is_O_with

/-- Definition of `is_o` in terms of filters. We record it in a lemma as we will set
`is_o` to be irreducible at the end of this file. -/
lemma is_o_iff {f : α → E} {g : α → F} {l : filter α} :
  is_o f g l ↔ ∀ ⦃c : ℝ⦄, 0 < c → ∀ᶠ x in l, ∥ f x ∥ ≤ c * ∥ g x ∥ :=
by simp only [is_o, is_O_with]

alias is_o_iff ↔ asymptotics.is_o.bound asymptotics.is_o.of_bound

lemma is_o.def {f : α → E} {g : α → F} {l : filter α} (h : is_o f g l) {c : ℝ} (hc : 0 < c) :
  ∀ᶠ x in l, ∥ f x ∥ ≤ c * ∥ g x ∥ :=
is_o_iff.1 h hc

lemma is_o.def' {f : α → E} {g : α → F} {l : filter α} (h : is_o f g l) {c : ℝ} (hc : 0 < c) :
  is_O_with c f g l :=
is_O_with_iff.2 $ is_o_iff.1 h hc

end defs

/-! ### Conversions -/

theorem is_O_with.is_O (h : is_O_with c f g l) : is_O f g l := by rw is_O; exact ⟨c, h⟩

theorem is_o.is_O_with (hgf : is_o f g l) : is_O_with 1 f g l := hgf.def' zero_lt_one

theorem is_o.is_O (hgf : is_o f g l) : is_O f g l := hgf.is_O_with.is_O

lemma is_O.is_O_with {f : α → E} {g : α → F} {l : filter α} :
  is_O f g l → ∃ c : ℝ, is_O_with c f g l := is_O_iff_is_O_with.1

theorem is_O_with.weaken (h : is_O_with c f g' l) (hc : c ≤ c') : is_O_with c' f g' l :=
is_O_with.of_bound $ mem_sets_of_superset h.bound $ λ x hx,
calc ∥f x∥ ≤ c * ∥g' x∥ : hx
... ≤ _ : mul_le_mul_of_nonneg_right hc (norm_nonneg _)

theorem is_O_with.exists_pos (h : is_O_with c f g' l) :
  ∃ c' (H : 0 < c'), is_O_with c' f g' l :=
⟨max c 1, lt_of_lt_of_le zero_lt_one (le_max_right c 1), h.weaken $ le_max_left c 1⟩

theorem is_O.exists_pos (h : is_O f g' l) : ∃ c (H : 0 < c), is_O_with c f g' l :=
let ⟨c, hc⟩ := h.is_O_with in hc.exists_pos

theorem is_O_with.exists_nonneg (h : is_O_with c f g' l) :
  ∃ c' (H : 0 ≤ c'), is_O_with c' f g' l :=
let ⟨c, cpos, hc⟩ := h.exists_pos in ⟨c, le_of_lt cpos, hc⟩

theorem is_O.exists_nonneg (h : is_O f g' l) :
  ∃ c (H : 0 ≤ c), is_O_with c f g' l :=
let ⟨c, hc⟩ := h.is_O_with in hc.exists_nonneg

/-- `f = O(g)` if and only if `is_O_with c f g` for all sufficiently large `c`. -/
lemma is_O_iff_eventually_is_O_with : is_O f g' l ↔ ∀ᶠ c in at_top, is_O_with c f g' l :=
is_O_iff_is_O_with.trans
  ⟨λ ⟨c, hc⟩, mem_at_top_sets.2 ⟨c, λ c' hc', hc.weaken hc'⟩, λ h, h.exists⟩

/-- `f = O(g)` if and only if `∀ᶠ x in l, ∥f x∥ ≤ c * ∥g x∥` for all sufficiently large `c`. -/
lemma is_O_iff_eventually : is_O f g' l ↔ ∀ᶠ c in at_top, ∀ᶠ x in l, ∥f x∥ ≤ c * ∥g' x∥ :=
is_O_iff_eventually_is_O_with.trans $ by simp only [is_O_with]

/-! ### Subsingleton -/

@[nontriviality] lemma is_o_of_subsingleton [subsingleton E'] : is_o f' g' l :=
is_o.of_bound $ λ c hc, by simp [subsingleton.elim (f' _) 0, mul_nonneg hc.le]

@[nontriviality] lemma is_O_of_subsingleton [subsingleton E'] : is_O f' g' l :=
is_o_of_subsingleton.is_O

/-! ### Congruence -/

theorem is_O_with_congr {c₁ c₂} {f₁ f₂ : α → E} {g₁ g₂ : α → F} {l : filter α}
  (hc : c₁ = c₂) (hf : f₁ =ᶠ[l] f₂) (hg : g₁ =ᶠ[l] g₂) :
  is_O_with c₁ f₁ g₁ l ↔ is_O_with c₂ f₂ g₂ l :=
begin
  unfold is_O_with,
  subst c₂,
  apply filter.eventually_congr,
  filter_upwards [hf, hg],
  assume x e₁ e₂,
  rw [e₁, e₂]
end

theorem is_O_with.congr' {c₁ c₂} {f₁ f₂ : α → E} {g₁ g₂ : α → F} {l : filter α}
  (hc : c₁ = c₂) (hf : f₁ =ᶠ[l] f₂) (hg : g₁ =ᶠ[l] g₂) :
  is_O_with c₁ f₁ g₁ l → is_O_with c₂ f₂ g₂ l :=
(is_O_with_congr hc hf hg).mp

theorem is_O_with.congr {c₁ c₂} {f₁ f₂ : α → E} {g₁ g₂ : α → F} {l : filter α}
  (hc : c₁ = c₂) (hf : ∀ x, f₁ x = f₂ x) (hg : ∀ x, g₁ x = g₂ x) :
  is_O_with c₁ f₁ g₁ l → is_O_with c₂ f₂ g₂ l :=
λ h, h.congr' hc (univ_mem_sets' hf) (univ_mem_sets' hg)

theorem is_O_with.congr_left {f₁ f₂ : α → E} {l : filter α} (hf : ∀ x, f₁ x = f₂ x) :
  is_O_with c f₁ g l → is_O_with c f₂ g l :=
is_O_with.congr rfl hf (λ _, rfl)

theorem is_O_with.congr_right {g₁ g₂ : α → F} {l : filter α} (hg : ∀ x, g₁ x = g₂ x) :
  is_O_with c f g₁ l → is_O_with c f g₂ l :=
is_O_with.congr rfl (λ _, rfl) hg

theorem is_O_with.congr_const {c₁ c₂} {l : filter α} (hc : c₁ = c₂) :
  is_O_with c₁ f g l → is_O_with c₂ f g l :=
is_O_with.congr hc (λ _, rfl) (λ _, rfl)

theorem is_O_congr {f₁ f₂ : α → E} {g₁ g₂ : α → F} {l : filter α}
    (hf : f₁ =ᶠ[l] f₂) (hg : g₁ =ᶠ[l] g₂) :
  is_O f₁ g₁ l ↔ is_O f₂ g₂ l :=
by { unfold is_O, exact exists_congr (λ c, is_O_with_congr rfl hf hg) }

theorem is_O.congr' {f₁ f₂ : α → E} {g₁ g₂ : α → F} {l : filter α}
    (hf : f₁ =ᶠ[l] f₂) (hg : g₁ =ᶠ[l] g₂) :
  is_O f₁ g₁ l → is_O f₂ g₂ l :=
(is_O_congr hf hg).mp

theorem is_O.congr {f₁ f₂ : α → E} {g₁ g₂ : α → F} {l : filter α}
    (hf : ∀ x, f₁ x = f₂ x) (hg : ∀ x, g₁ x = g₂ x) :
  is_O f₁ g₁ l → is_O f₂ g₂ l :=
λ h, h.congr' (univ_mem_sets' hf) (univ_mem_sets' hg)

theorem is_O.congr_left {f₁ f₂ : α → E} {l : filter α} (hf : ∀ x, f₁ x = f₂ x) :
  is_O f₁ g l → is_O f₂ g l :=
is_O.congr hf (λ _, rfl)

theorem is_O.congr_right {g₁ g₂ : α → E} {l : filter α} (hg : ∀ x, g₁ x = g₂ x) :
  is_O f g₁ l → is_O f g₂ l :=
is_O.congr (λ _, rfl) hg

theorem is_o_congr {f₁ f₂ : α → E} {g₁ g₂ : α → F} {l : filter α}
    (hf : f₁ =ᶠ[l] f₂) (hg : g₁ =ᶠ[l] g₂) :
  is_o f₁ g₁ l ↔ is_o f₂ g₂ l :=
by { unfold is_o, exact ball_congr (λ c hc, is_O_with_congr (eq.refl c) hf hg) }

theorem is_o.congr' {f₁ f₂ : α → E} {g₁ g₂ : α → F} {l : filter α}
    (hf : f₁ =ᶠ[l] f₂) (hg : g₁ =ᶠ[l] g₂) :
  is_o f₁ g₁ l → is_o f₂ g₂ l :=
(is_o_congr hf hg).mp

theorem is_o.congr {f₁ f₂ : α → E} {g₁ g₂ : α → F} {l : filter α}
    (hf : ∀ x, f₁ x = f₂ x) (hg : ∀ x, g₁ x = g₂ x) :
  is_o f₁ g₁ l → is_o f₂ g₂ l :=
λ h, h.congr' (univ_mem_sets' hf) (univ_mem_sets' hg)

theorem is_o.congr_left {f₁ f₂ : α → E} {l : filter α} (hf : ∀ x, f₁ x = f₂ x) :
  is_o f₁ g l → is_o f₂ g l :=
is_o.congr hf (λ _, rfl)

theorem is_o.congr_right {g₁ g₂ : α → E} {l : filter α} (hg : ∀ x, g₁ x = g₂ x) :
  is_o f g₁ l → is_o f g₂ l :=
is_o.congr (λ _, rfl) hg

/-! ### Filter operations and transitivity -/

theorem is_O_with.comp_tendsto (hcfg : is_O_with c f g l)
  {k : β → α} {l' : filter β} (hk : tendsto k l' l):
  is_O_with c (f ∘ k) (g ∘ k) l' :=
is_O_with.of_bound $ hk hcfg.bound

theorem is_O.comp_tendsto (hfg : is_O f g l) {k : β → α} {l' : filter β} (hk : tendsto k l' l) :
  is_O (f ∘ k) (g ∘ k) l' :=
is_O_iff_is_O_with.2 $ hfg.is_O_with.imp (λ c h, h.comp_tendsto hk)

theorem is_o.comp_tendsto (hfg : is_o f g l) {k : β → α} {l' : filter β} (hk : tendsto k l' l) :
  is_o (f ∘ k) (g ∘ k) l' :=
is_o.of_is_O_with $ λ c cpos, (hfg.forall_is_O_with cpos).comp_tendsto hk

@[simp] theorem is_O_with_map {k : β → α} {l : filter β} :
  is_O_with c f g (map k l) ↔ is_O_with c (f ∘ k) (g ∘ k) l :=
by { unfold is_O_with, exact mem_map }

@[simp] theorem is_O_map {k : β → α} {l : filter β} :
  is_O f g (map k l) ↔ is_O (f ∘ k) (g ∘ k) l :=
by simp only [is_O, is_O_with_map]

@[simp] theorem is_o_map {k : β → α} {l : filter β} :
  is_o f g (map k l) ↔ is_o (f ∘ k) (g ∘ k) l :=
by simp only [is_o, is_O_with_map]

theorem is_O_with.mono (h : is_O_with c f g l') (hl : l ≤ l') : is_O_with c f g l :=
is_O_with.of_bound $ hl h.bound

theorem is_O.mono (h : is_O f g l') (hl : l ≤ l') : is_O f g l :=
is_O_iff_is_O_with.2 $ h.is_O_with.imp (λ c h, h.mono hl)

theorem is_o.mono (h : is_o f g l') (hl : l ≤ l') : is_o f g l :=
is_o.of_is_O_with $ λ c cpos, (h.forall_is_O_with cpos).mono hl

theorem is_O_with.trans (hfg : is_O_with c f g l) (hgk : is_O_with c' g k l) (hc : 0 ≤ c) :
  is_O_with (c * c') f k l :=
begin
  unfold is_O_with at *,
  filter_upwards [hfg, hgk],
  assume x hx hx',
  calc ∥f x∥ ≤ c * ∥g x∥ : hx
  ... ≤ c * (c' * ∥k x∥) : mul_le_mul_of_nonneg_left hx' hc
  ... = c * c' * ∥k x∥ : (mul_assoc _ _ _).symm
end

theorem is_O.trans (hfg : is_O f g' l) (hgk : is_O g' k l) : is_O f k l :=
let ⟨c, cnonneg, hc⟩ := hfg.exists_nonneg, ⟨c', hc'⟩ := hgk.is_O_with in
(hc.trans hc' cnonneg).is_O

theorem is_o.trans_is_O_with (hfg : is_o f g l) (hgk : is_O_with c g k l) (hc : 0 < c) :
  is_o f k l :=
begin
  unfold is_o at *,
  intros c' c'pos,
  have : 0 < c' / c, from div_pos c'pos hc,
  exact ((hfg this).trans hgk (le_of_lt this)).congr_const (div_mul_cancel _ (ne_of_gt hc))
end

theorem is_o.trans_is_O (hfg : is_o f g l) (hgk : is_O g k' l) : is_o f k' l :=
let ⟨c, cpos, hc⟩ := hgk.exists_pos in hfg.trans_is_O_with hc cpos

theorem is_O_with.trans_is_o (hfg : is_O_with c f g l) (hgk : is_o g k l) (hc : 0 < c) :
  is_o f k l :=
begin
  unfold is_o at *,
  intros c' c'pos,
  have : 0 < c' / c, from div_pos c'pos hc,
  exact (hfg.trans (hgk this) (le_of_lt hc)).congr_const (mul_div_cancel' _ (ne_of_gt hc))
end

theorem is_O.trans_is_o (hfg : is_O f g' l) (hgk : is_o g' k l) : is_o f k l :=
let ⟨c, cpos, hc⟩ := hfg.exists_pos in hc.trans_is_o hgk cpos

theorem is_o.trans (hfg : is_o f g l) (hgk : is_o g k' l) : is_o f k' l :=
hfg.trans_is_O hgk.is_O

theorem is_o.trans' (hfg : is_o f g' l) (hgk : is_o g' k l) : is_o f k l :=
hfg.is_O.trans_is_o hgk

section

variable (l)

theorem is_O_with_of_le' (hfg : ∀ x, ∥f x∥ ≤ c * ∥g x∥) : is_O_with c f g l :=
is_O_with.of_bound $ univ_mem_sets' hfg

theorem is_O_with_of_le (hfg : ∀ x, ∥f x∥ ≤ ∥g x∥) : is_O_with 1 f g l :=
is_O_with_of_le' l $ λ x, by { rw one_mul, exact hfg x }

theorem is_O_of_le' (hfg : ∀ x, ∥f x∥ ≤ c * ∥g x∥) : is_O f g l :=
(is_O_with_of_le' l hfg).is_O

theorem is_O_of_le (hfg : ∀ x, ∥f x∥ ≤ ∥g x∥) : is_O f g l :=
(is_O_with_of_le l hfg).is_O

end

theorem is_O_with_refl (f : α → E) (l : filter α) : is_O_with 1 f f l :=
is_O_with_of_le l $ λ _, le_refl _

theorem is_O_refl (f : α → E) (l : filter α) : is_O f f l := (is_O_with_refl f l).is_O

theorem is_O_with.trans_le (hfg : is_O_with c f g l) (hgk : ∀ x, ∥g x∥ ≤ ∥k x∥) (hc : 0 ≤ c) :
  is_O_with c f k l :=
(hfg.trans (is_O_with_of_le l hgk) hc).congr_const $ mul_one c

theorem is_O.trans_le (hfg : is_O f g' l) (hgk : ∀ x, ∥g' x∥ ≤ ∥k x∥) :
  is_O f k l :=
hfg.trans (is_O_of_le l hgk)

theorem is_o.trans_le (hfg : is_o f g l) (hgk : ∀ x, ∥g x∥ ≤ ∥k x∥) :
  is_o f k l :=
hfg.trans_is_O_with (is_O_with_of_le _ hgk) zero_lt_one

section bot

variables (c f g)

@[simp] theorem is_O_with_bot : is_O_with c f g ⊥ := is_O_with.of_bound $ trivial

@[simp] theorem is_O_bot : is_O f g ⊥ := (is_O_with_bot 1 f g).is_O

@[simp] theorem is_o_bot : is_o f g ⊥ := is_o.of_is_O_with $ λ c _, is_O_with_bot c f g

end bot

theorem is_O_with.join (h : is_O_with c f g l) (h' : is_O_with c f g l') :
  is_O_with c f g (l ⊔ l') :=
is_O_with.of_bound $ mem_sup_sets.2 ⟨h.bound, h'.bound⟩

theorem is_O_with.join' (h : is_O_with c f g' l) (h' : is_O_with c' f g' l') :
  is_O_with (max c c') f g' (l ⊔ l') :=
is_O_with.of_bound $
mem_sup_sets.2 ⟨(h.weaken $ le_max_left c c').bound, (h'.weaken $ le_max_right c c').bound⟩

theorem is_O.join (h : is_O f g' l) (h' : is_O f g' l') : is_O f g' (l ⊔ l') :=
let ⟨c, hc⟩ := h.is_O_with, ⟨c', hc'⟩ := h'.is_O_with in (hc.join' hc').is_O

theorem is_o.join (h : is_o f g l) (h' : is_o f g l') :
  is_o f g (l ⊔ l') :=
is_o.of_is_O_with $
λ c cpos, (h.forall_is_O_with cpos).join (h'.forall_is_O_with cpos)

/-! ### Simplification : norm -/

@[simp] theorem is_O_with_norm_right : is_O_with c f (λ x, ∥g' x∥) l ↔ is_O_with c f g' l :=
by simp only [is_O_with, norm_norm]

alias is_O_with_norm_right ↔ asymptotics.is_O_with.of_norm_right asymptotics.is_O_with.norm_right

@[simp] theorem is_O_norm_right : is_O f (λ x, ∥g' x∥) l ↔ is_O f g' l :=
by { unfold is_O, exact exists_congr (λ _, is_O_with_norm_right) }

alias is_O_norm_right ↔ asymptotics.is_O.of_norm_right asymptotics.is_O.norm_right

@[simp] theorem is_o_norm_right : is_o f (λ x, ∥g' x∥) l ↔ is_o f g' l :=
by { unfold is_o, exact forall_congr (λ _, forall_congr $ λ _, is_O_with_norm_right) }

alias is_o_norm_right ↔ asymptotics.is_o.of_norm_right asymptotics.is_o.norm_right

@[simp] theorem is_O_with_norm_left : is_O_with c (λ x, ∥f' x∥) g l ↔ is_O_with c f' g l :=
by simp only [is_O_with, norm_norm]

alias is_O_with_norm_left ↔ asymptotics.is_O_with.of_norm_left asymptotics.is_O_with.norm_left

@[simp] theorem is_O_norm_left : is_O (λ x, ∥f' x∥) g l ↔ is_O f' g l :=
by { unfold is_O, exact exists_congr (λ _, is_O_with_norm_left) }

alias is_O_norm_left ↔ asymptotics.is_O.of_norm_left asymptotics.is_O.norm_left

@[simp] theorem is_o_norm_left : is_o (λ x, ∥f' x∥) g l ↔ is_o f' g l :=
by { unfold is_o,  exact forall_congr (λ _, forall_congr $ λ _, is_O_with_norm_left) }

alias is_o_norm_left ↔ asymptotics.is_o.of_norm_left asymptotics.is_o.norm_left

theorem is_O_with_norm_norm :
  is_O_with c (λ x, ∥f' x∥) (λ x, ∥g' x∥) l ↔ is_O_with c f' g' l :=
is_O_with_norm_left.trans is_O_with_norm_right

alias is_O_with_norm_norm ↔ asymptotics.is_O_with.of_norm_norm asymptotics.is_O_with.norm_norm

theorem is_O_norm_norm :
  is_O (λ x, ∥f' x∥) (λ x, ∥g' x∥) l ↔ is_O f' g' l :=
is_O_norm_left.trans is_O_norm_right

alias is_O_norm_norm ↔ asymptotics.is_O.of_norm_norm asymptotics.is_O.norm_norm

theorem is_o_norm_norm :
  is_o (λ x, ∥f' x∥) (λ x, ∥g' x∥) l ↔ is_o f' g' l :=
is_o_norm_left.trans is_o_norm_right

alias is_o_norm_norm ↔ asymptotics.is_o.of_norm_norm asymptotics.is_o.norm_norm

/-! ### Simplification: negate -/

@[simp] theorem is_O_with_neg_right : is_O_with c f (λ x, -(g' x)) l ↔ is_O_with c f g' l :=
by simp only [is_O_with, norm_neg]

alias is_O_with_neg_right ↔ asymptotics.is_O_with.of_neg_right asymptotics.is_O_with.neg_right

@[simp] theorem is_O_neg_right : is_O f (λ x, -(g' x)) l ↔ is_O f g' l :=
by { unfold is_O, exact exists_congr (λ _, is_O_with_neg_right) }

alias is_O_neg_right ↔ asymptotics.is_O.of_neg_right asymptotics.is_O.neg_right

@[simp] theorem is_o_neg_right : is_o f (λ x, -(g' x)) l ↔ is_o f g' l :=
by { unfold is_o, exact forall_congr (λ _, (forall_congr (λ _, is_O_with_neg_right))) }

alias is_o_neg_right ↔ asymptotics.is_o.of_neg_right asymptotics.is_o.neg_right

@[simp] theorem is_O_with_neg_left : is_O_with c (λ x, -(f' x)) g l ↔ is_O_with c f' g l :=
by simp only [is_O_with, norm_neg]

alias is_O_with_neg_left ↔ asymptotics.is_O_with.of_neg_left asymptotics.is_O_with.neg_left

@[simp] theorem is_O_neg_left : is_O (λ x, -(f' x)) g l ↔ is_O f' g l :=
by { unfold is_O, exact exists_congr (λ _, is_O_with_neg_left) }

alias is_O_neg_left ↔ asymptotics.is_O.of_neg_left asymptotics.is_O.neg_left

@[simp] theorem is_o_neg_left : is_o (λ x, -(f' x)) g l ↔ is_o f' g l :=
by { unfold is_o, exact forall_congr (λ _, (forall_congr (λ _, is_O_with_neg_left))) }

alias is_o_neg_left ↔ asymptotics.is_o.of_neg_right asymptotics.is_o.neg_left

/-! ### Product of functions (right) -/

lemma is_O_with_fst_prod : is_O_with 1 f' (λ x, (f' x, g' x)) l :=
is_O_with_of_le l $ λ x, le_max_left _ _

lemma is_O_with_snd_prod : is_O_with 1 g' (λ x, (f' x, g' x)) l :=
is_O_with_of_le l $ λ x, le_max_right _ _

lemma is_O_fst_prod : is_O f' (λ x, (f' x, g' x)) l := is_O_with_fst_prod.is_O

lemma is_O_snd_prod : is_O g' (λ x, (f' x, g' x)) l := is_O_with_snd_prod.is_O

lemma is_O_fst_prod' {f' : α → E' × F'} : is_O (λ x, (f' x).1) f' l :=
by simpa [is_O, is_O_with] using is_O_fst_prod

lemma is_O_snd_prod' {f' : α → E' × F'} : is_O (λ x, (f' x).2) f' l :=
by simpa [is_O, is_O_with] using is_O_snd_prod

section

variables (f' k')

lemma is_O.prod_rightl (h : is_O f g' l) : is_O f (λx, (g' x, k' x)) l :=
let ⟨c, cnonneg, hc⟩ := h.exists_nonneg in (hc.prod_rightl k' cnonneg).is_O

lemma is_o.prod_rightl (h : is_o f g' l) : is_o f (λ x, (g' x, k' x)) l :=
is_o.of_is_O_with $
λ c cpos, (h.forall_is_O_with cpos).prod_rightl k' (le_of_lt cpos)

lemma is_O_with.prod_rightr (h : is_O_with c f g' l) (hc : 0 ≤ c) :
  is_O_with c f (λ x, (f' x, g' x)) l :=
(h.trans is_O_with_snd_prod hc).congr_const (mul_one c)

lemma is_O.prod_rightr (h : is_O f g' l) : is_O f (λx, (f' x, g' x)) l :=
let ⟨c, cnonneg, hc⟩ := h.exists_nonneg in (hc.prod_rightr f' cnonneg).is_O

lemma is_o.prod_rightr (h : is_o f g' l) : is_o f (λx, (f' x, g' x)) l :=
is_o.of_is_O_with $
λ c cpos, (h.forall_is_O_with cpos).prod_rightr f' (le_of_lt cpos)

end

lemma is_O_with.prod_left_same (hf : is_O_with c f' k' l) (hg : is_O_with c g' k' l) :
  is_O_with c (λ x, (f' x, g' x)) k' l :=
by rw is_O_with_iff at *; filter_upwards [hf, hg] λ x, max_le

lemma is_O_with.prod_left (hf : is_O_with c f' k' l) (hg : is_O_with c' g' k' l) :
  is_O_with (max c c') (λ x, (f' x, g' x)) k' l :=
(hf.weaken $ le_max_left c c').prod_left_same (hg.weaken $ le_max_right c c')

lemma is_O_with.prod_left_fst (h : is_O_with c (λ x, (f' x, g' x)) k' l) :
  is_O_with c f' k' l :=
(is_O_with_fst_prod.trans h zero_le_one).congr_const $ one_mul c

lemma is_O_with.prod_left_snd (h : is_O_with c (λ x, (f' x, g' x)) k' l) :
  is_O_with c g' k' l :=
(is_O_with_snd_prod.trans h zero_le_one).congr_const $ one_mul c

lemma is_O_with_prod_left :
   is_O_with c (λ x, (f' x, g' x)) k' l ↔ is_O_with c f' k' l ∧ is_O_with c g' k' l :=
⟨λ h, ⟨h.prod_left_fst, h.prod_left_snd⟩, λ h, h.1.prod_left_same h.2⟩

lemma is_O.prod_left (hf : is_O f' k' l) (hg : is_O g' k' l) : is_O (λ x, (f' x, g' x)) k' l :=
let ⟨c, hf⟩ := hf.is_O_with, ⟨c', hg⟩ := hg.is_O_with in (hf.prod_left hg).is_O

lemma is_O.prod_left_fst (h : is_O (λ x, (f' x, g' x)) k' l) : is_O f' k' l :=
is_O_fst_prod.trans h

lemma is_O.prod_left_snd (h : is_O (λ x, (f' x, g' x)) k' l) : is_O g' k' l :=
is_O_snd_prod.trans h

@[simp] lemma is_O_prod_left :
  is_O (λ x, (f' x, g' x)) k' l ↔ is_O f' k' l ∧ is_O g' k' l :=
⟨λ h, ⟨h.prod_left_fst, h.prod_left_snd⟩, λ h, h.1.prod_left h.2⟩

lemma is_o.prod_left (hf : is_o f' k' l) (hg : is_o g' k' l) : is_o (λ x, (f' x, g' x)) k' l :=
is_o.of_is_O_with $
λ c hc, (hf.forall_is_O_with hc).prod_left_same (hg.forall_is_O_with hc)

lemma is_o.prod_left_fst (h : is_o (λ x, (f' x, g' x)) k' l) : is_o f' k' l :=
is_O_fst_prod.trans_is_o h

lemma is_o.prod_left_snd (h : is_o (λ x, (f' x, g' x)) k' l) : is_o g' k' l :=
is_O_snd_prod.trans_is_o h

@[simp] lemma is_o_prod_left :
  is_o (λ x, (f' x, g' x)) k' l ↔ is_o f' k' l ∧ is_o g' k' l :=
⟨λ h, ⟨h.prod_left_fst, h.prod_left_snd⟩, λ h, h.1.prod_left h.2⟩

lemma is_O_with.eq_zero_imp (h : is_O_with c f' g' l) : ∀ᶠ x in l, g' x = 0 → f' x = 0 :=
eventually.mono h.bound $ λ x hx hg, norm_le_zero_iff.1 $ by simpa [hg] using hx

lemma is_O.eq_zero_imp (h : is_O f' g' l) : ∀ᶠ x in l, g' x = 0 → f' x = 0 :=
let ⟨C, hC⟩ := h.is_O_with in hC.eq_zero_imp

/-! ### Addition and subtraction -/

section add_sub

variables {c₁ c₂ : ℝ} {f₁ f₂ : α → E'}

theorem is_O_with.add (h₁ : is_O_with c₁ f₁ g l) (h₂ : is_O_with c₂ f₂ g l) :
  is_O_with (c₁ + c₂) (λ x, f₁ x + f₂ x) g l :=
by rw is_O_with at *; filter_upwards [h₁, h₂] λ x hx₁ hx₂,
calc ∥f₁ x + f₂ x∥ ≤ c₁ * ∥g x∥ + c₂ * ∥g x∥ : norm_add_le_of_le hx₁ hx₂
               ... = (c₁ + c₂) * ∥g x∥       : (add_mul _ _ _).symm

theorem is_O.add (h₁ : is_O f₁ g l) (h₂ : is_O f₂ g l) : is_O (λ x, f₁ x + f₂ x) g l :=
let ⟨c₁, hc₁⟩ := h₁.is_O_with, ⟨c₂, hc₂⟩ := h₂.is_O_with in (hc₁.add hc₂).is_O

theorem is_o.add (h₁ : is_o f₁ g l) (h₂ : is_o f₂ g l) : is_o (λ x, f₁ x + f₂ x) g l :=
is_o.of_is_O_with $ λ c cpos, ((h₁.forall_is_O_with $ half_pos cpos).add
  (h₂.forall_is_O_with $ half_pos cpos)).congr_const (add_halves c)

theorem is_o.add_add {g₁ g₂ : α → F'} (h₁ : is_o f₁ g₁ l) (h₂ : is_o f₂ g₂ l) :
  is_o (λ x, f₁ x + f₂ x) (λ x, ∥g₁ x∥ + ∥g₂ x∥) l :=
by refine (h₁.trans_le $ λ x, _).add (h₂.trans_le _);
  simp [real.norm_eq_abs, abs_of_nonneg, add_nonneg]

theorem is_O.add_is_o (h₁ : is_O f₁ g l) (h₂ : is_o f₂ g l) : is_O (λ x, f₁ x + f₂ x) g l :=
h₁.add h₂.is_O

theorem is_o.add_is_O (h₁ : is_o f₁ g l) (h₂ : is_O f₂ g l) : is_O (λ x, f₁ x + f₂ x) g l :=
h₁.is_O.add h₂

theorem is_O_with.add_is_o (h₁ : is_O_with c₁ f₁ g l) (h₂ : is_o f₂ g l) (hc : c₁ < c₂) :
  is_O_with c₂ (λx, f₁ x + f₂ x) g l :=
(h₁.add (h₂.forall_is_O_with (sub_pos.2 hc))).congr_const (add_sub_cancel'_right _ _)

theorem is_o.add_is_O_with (h₁ : is_o f₁ g l) (h₂ : is_O_with c₁ f₂ g l) (hc : c₁ < c₂) :
  is_O_with c₂ (λx, f₁ x + f₂ x) g l :=
(h₂.add_is_o h₁ hc).congr_left $ λ _, add_comm _ _

theorem is_O_with.sub (h₁ : is_O_with c₁ f₁ g l) (h₂ : is_O_with c₂ f₂ g l) :
  is_O_with (c₁ + c₂) (λ x, f₁ x - f₂ x) g l :=
by simpa only [sub_eq_add_neg] using h₁.add h₂.neg_left

theorem is_O_with.sub_is_o (h₁ : is_O_with c₁ f₁ g l) (h₂ : is_o f₂ g l) (hc : c₁ < c₂) :
  is_O_with c₂ (λ x, f₁ x - f₂ x) g l :=
by simpa only [sub_eq_add_neg] using h₁.add_is_o h₂.neg_left hc

theorem is_O.sub (h₁ : is_O f₁ g l) (h₂ : is_O f₂ g l) : is_O (λ x, f₁ x - f₂ x) g l :=
by simpa only [sub_eq_add_neg] using h₁.add h₂.neg_left

theorem is_o.sub (h₁ : is_o f₁ g l) (h₂ : is_o f₂ g l) : is_o (λ x, f₁ x - f₂ x) g l :=
by simpa only [sub_eq_add_neg] using h₁.add h₂.neg_left

end add_sub

/-! ### Lemmas about `is_O (f₁ - f₂) g l` / `is_o (f₁ - f₂) g l` treated as a binary relation -/

section is_oO_as_rel

variables {f₁ f₂ f₃ : α → E'}

theorem is_O_with.symm (h : is_O_with c (λ x, f₁ x - f₂ x) g l) :
  is_O_with c (λ x, f₂ x - f₁ x) g l :=
h.neg_left.congr_left $ λ x, neg_sub _ _

theorem is_O_with_comm :
  is_O_with c (λ x, f₁ x - f₂ x) g l ↔ is_O_with c (λ x, f₂ x - f₁ x) g l :=
⟨is_O_with.symm, is_O_with.symm⟩

theorem is_O.symm (h : is_O (λ x, f₁ x - f₂ x) g l) : is_O (λ x, f₂ x - f₁ x) g l :=
h.neg_left.congr_left $ λ x, neg_sub _ _

theorem is_O_comm : is_O (λ x, f₁ x - f₂ x) g l ↔ is_O (λ x, f₂ x - f₁ x) g l :=
⟨is_O.symm, is_O.symm⟩

theorem is_o.symm (h : is_o (λ x, f₁ x - f₂ x) g l) : is_o (λ x, f₂ x - f₁ x) g l :=
by simpa only [neg_sub] using h.neg_left

theorem is_o_comm : is_o (λ x, f₁ x - f₂ x) g l ↔ is_o (λ x, f₂ x - f₁ x) g l :=
⟨is_o.symm, is_o.symm⟩

theorem is_O_with.triangle (h₁ : is_O_with c (λ x, f₁ x - f₂ x) g l)
  (h₂ : is_O_with c' (λ x, f₂ x - f₃ x) g l) :
  is_O_with (c + c') (λ x, f₁ x - f₃ x) g l :=
(h₁.add h₂).congr_left $ λ x, sub_add_sub_cancel _ _ _

theorem is_O.triangle (h₁ : is_O (λ x, f₁ x - f₂ x) g l) (h₂ : is_O (λ x, f₂ x - f₃ x) g l) :
  is_O (λ x, f₁ x - f₃ x) g l :=
(h₁.add h₂).congr_left $ λ x, sub_add_sub_cancel _ _ _

theorem is_o.triangle (h₁ : is_o (λ x, f₁ x - f₂ x) g l) (h₂ : is_o (λ x, f₂ x - f₃ x) g l) :
  is_o (λ x, f₁ x - f₃ x) g l :=
(h₁.add h₂).congr_left $ λ x, sub_add_sub_cancel _ _ _

theorem is_O.congr_of_sub (h : is_O (λ x, f₁ x - f₂ x) g l) :
  is_O f₁ g l ↔ is_O f₂ g l :=
⟨λ h', (h'.sub h).congr_left (λ x, sub_sub_cancel _ _),
 λ h', (h.add h').congr_left (λ x, sub_add_cancel _ _)⟩

theorem is_o.congr_of_sub (h : is_o (λ x, f₁ x - f₂ x) g l) :
  is_o f₁ g l ↔ is_o f₂ g l :=
⟨λ h', (h'.sub h).congr_left (λ x, sub_sub_cancel _ _),
 λ h', (h.add h').congr_left (λ x, sub_add_cancel _ _)⟩

end is_oO_as_rel

/-! ### Zero, one, and other constants -/

section zero_const

variables (g g' l)

theorem is_o_zero : is_o (λ x, (0 : E')) g' l :=
is_o.of_bound $ λ c hc, univ_mem_sets' $ λ x,
by simpa using mul_nonneg (le_of_lt hc) (norm_nonneg $ g' x)

theorem is_O_with_zero (hc : 0 ≤ c) : is_O_with c (λ x, (0 : E')) g' l :=
is_O_with.of_bound $ univ_mem_sets' $ λ x, by simpa using mul_nonneg hc (norm_nonneg $ g' x)

theorem is_O_with_zero' : is_O_with 0 (λ x, (0 : E')) g l :=
is_O_with.of_bound $ univ_mem_sets' $ λ x, by simp

theorem is_O_zero : is_O (λ x, (0 : E')) g l :=
is_O_iff_is_O_with.2 ⟨0, is_O_with_zero' _ _⟩

theorem is_O_refl_left : is_O (λ x, f' x - f' x) g' l :=
(is_O_zero g' l).congr_left $ λ x, (sub_self _).symm

theorem is_o_refl_left : is_o (λ x, f' x - f' x) g' l :=
(is_o_zero g' l).congr_left $ λ x, (sub_self _).symm

variables {g g' l}

@[simp] theorem is_O_with_zero_right_iff :
  is_O_with c f' (λ x, (0 : F')) l ↔ ∀ᶠ x in l, f' x = 0 :=
by simp only [is_O_with, exists_prop, true_and, norm_zero, mul_zero, norm_le_zero_iff]

@[simp] theorem is_O_zero_right_iff : is_O f' (λ x, (0 : F')) l ↔ ∀ᶠ x in l, f' x = 0 :=
⟨λ h, let ⟨c, hc⟩ := h.is_O_with in is_O_with_zero_right_iff.1 hc,
  λ h, (is_O_with_zero_right_iff.2 h : is_O_with 1 _ _ _).is_O⟩

@[simp] theorem is_o_zero_right_iff :
  is_o f' (λ x, (0 : F')) l ↔ ∀ᶠ x in l, f' x = 0 :=
⟨λ h, is_O_zero_right_iff.1 h.is_O,
  λ h, is_o.of_is_O_with $ λ c hc, is_O_with_zero_right_iff.2 h⟩

theorem is_O_with_const_const (c : E) {c' : F'} (hc' : c' ≠ 0) (l : filter α) :
  is_O_with (∥c∥ / ∥c'∥) (λ x : α, c) (λ x, c') l :=
begin
  unfold is_O_with,
  apply univ_mem_sets',
  intro x,
  rw [mem_set_of_eq, div_mul_cancel],
  rwa [ne.def, norm_eq_zero]
end

theorem is_O_const_const (c : E) {c' : F'} (hc' : c' ≠ 0) (l : filter α) :
  is_O (λ x : α, c) (λ x, c') l :=
(is_O_with_const_const c hc' l).is_O

end zero_const

@[simp] lemma is_O_with_top : is_O_with c f g ⊤ ↔ ∀ x, ∥f x∥ ≤ c * ∥g x∥ := by rw is_O_with; refl

@[simp] lemma is_O_top : is_O f g ⊤ ↔ ∃ C, ∀ x, ∥f x∥ ≤ C * ∥g x∥ := by rw is_O_iff; refl

@[simp] lemma is_o_top : is_o f' g' ⊤ ↔ ∀ x, f' x = 0 :=
begin
  refine ⟨_, λ h, (is_o_zero g' ⊤).congr (λ x, (h x).symm) (λ x, rfl)⟩,
  simp only [is_o_iff, eventually_top],
  refine λ h x, norm_le_zero_iff.1 _,
  have : tendsto (λ c : ℝ, c * ∥g' x∥) (𝓝[Ioi 0] 0) (𝓝 0) :=
    ((continuous_id.mul continuous_const).tendsto' _ _ (zero_mul _)).mono_left inf_le_left,
  exact le_of_tendsto_of_tendsto tendsto_const_nhds this
    (eventually_nhds_within_iff.2 $ eventually_of_forall $ λ c hc, h hc x)
end

@[simp] lemma is_O_with_principal {s : set α} :
  is_O_with c f g (𝓟 s) ↔ ∀ x ∈ s, ∥f x∥ ≤ c * ∥g x∥ :=
by rw is_O_with; refl

lemma is_O_principal {s : set α} :
  is_O f g (𝓟 s) ↔ ∃ c, ∀ x ∈ s, ∥f x∥ ≤ c * ∥g x∥ :=
by rw is_O_iff; refl

theorem is_O_with_const_one (c : E) (l : filter α) : is_O_with ∥c∥ (λ x : α, c) (λ x, (1 : 𝕜)) l :=
begin
  refine (is_O_with_const_const c _ l).congr_const _,
  { rw [norm_one, div_one] },
  { exact one_ne_zero }
end

theorem is_O_const_one (c : E) (l : filter α) : is_O (λ x : α, c) (λ x, (1 : 𝕜)) l :=
(is_O_with_const_one c l).is_O

section

variable (𝕜)

theorem is_o_const_iff_is_o_one {c : F'} (hc : c ≠ 0) :
  is_o f (λ x, c) l ↔ is_o f (λ x, (1:𝕜)) l :=
⟨λ h, h.trans_is_O $ is_O_const_one c l, λ h, h.trans_is_O $ is_O_const_const _ hc _⟩

end

theorem is_o_const_iff {c : F'} (hc : c ≠ 0) :
  is_o f' (λ x, c) l ↔ tendsto f' l (𝓝 0) :=
(is_o_const_iff_is_o_one ℝ hc).trans
begin
  clear hc c,
  simp only [is_o, is_O_with, norm_one, mul_one, metric.nhds_basis_closed_ball.tendsto_right_iff,
    metric.mem_closed_ball, dist_zero_right]
end

lemma is_o_id_const {c : F'} (hc : c ≠ 0) :
  is_o (λ (x : E'), x) (λ x, c) (𝓝 0) :=
(is_o_const_iff hc).mpr (continuous_id.tendsto 0)

theorem is_O_const_of_tendsto {y : E'} (h : tendsto f' l (𝓝 y)) {c : F'} (hc : c ≠ 0) :
  is_O f' (λ x, c) l :=
begin
  refine is_O.trans _ (is_O_const_const (∥y∥ + 1) hc l),
  refine is_O.of_bound 1 _,
  simp only [is_O_with, one_mul],
  have : tendsto (λx, ∥f' x∥) l (𝓝 ∥y∥), from (continuous_norm.tendsto _).comp h,
  have Iy : ∥y∥ < ∥∥y∥ + 1∥, from lt_of_lt_of_le (lt_add_one _) (le_abs_self _),
  exact this (ge_mem_nhds Iy)
end

section

variable (𝕜)

theorem is_o_one_iff : is_o f' (λ x, (1 : 𝕜)) l ↔ tendsto f' l (𝓝 0) :=
is_o_const_iff one_ne_zero

theorem is_O_one_of_tendsto {y : E'} (h : tendsto f' l (𝓝 y)) :
  is_O f' (λ x, (1:𝕜)) l :=
is_O_const_of_tendsto h one_ne_zero

theorem is_O.trans_tendsto_nhds (hfg : is_O f g' l) {y : F'} (hg : tendsto g' l (𝓝 y)) :
  is_O f (λ x, (1:𝕜)) l :=
hfg.trans $ is_O_one_of_tendsto 𝕜 hg

end

theorem is_O.trans_tendsto (hfg : is_O f' g' l) (hg : tendsto g' l (𝓝 0)) :
  tendsto f' l (𝓝 0) :=
(is_o_one_iff ℝ).1 $ hfg.trans_is_o $ (is_o_one_iff ℝ).2 hg

theorem is_o.trans_tendsto (hfg : is_o f' g' l) (hg : tendsto g' l (𝓝 0)) :
  tendsto f' l (𝓝 0) :=
hfg.is_O.trans_tendsto hg

/-! ### Multiplication by a constant -/

theorem is_O_with_const_mul_self (c : R) (f : α → R) (l : filter α) :
  is_O_with ∥c∥ (λ x, c * f x) f l :=
is_O_with_of_le' _ $ λ x, norm_mul_le _ _

theorem is_O_const_mul_self (c : R) (f : α → R) (l : filter α) :
  is_O (λ x, c * f x) f l :=
(is_O_with_const_mul_self c f l).is_O

theorem is_O_with.const_mul_left {f : α → R} (h : is_O_with c f g l) (c' : R) :
  is_O_with (∥c'∥ * c) (λ x, c' * f x) g l :=
(is_O_with_const_mul_self c' f l).trans h (norm_nonneg c')

theorem is_O.const_mul_left {f : α → R} (h : is_O f g l) (c' : R) :
  is_O (λ x, c' * f x) g l :=
let ⟨c, hc⟩ := h.is_O_with in (hc.const_mul_left c').is_O

theorem is_O_with_self_const_mul' (u : units R) (f : α → R) (l : filter α) :
  is_O_with ∥(↑u⁻¹:R)∥ f (λ x, ↑u * f x) l :=
(is_O_with_const_mul_self ↑u⁻¹ _ l).congr_left $ λ x, u.inv_mul_cancel_left (f x)

theorem is_O_with_self_const_mul (c : 𝕜) (hc : c ≠ 0) (f : α → 𝕜) (l : filter α) :
  is_O_with ∥c∥⁻¹ f (λ x, c * f x) l :=
(is_O_with_self_const_mul' (units.mk0 c hc) f l).congr_const $
  normed_field.norm_inv c

theorem is_O_self_const_mul' {c : R} (hc : is_unit c) (f : α → R) (l : filter α) :
  is_O f (λ x, c * f x) l :=
let ⟨u, hu⟩ := hc in hu ▸ (is_O_with_self_const_mul' u f l).is_O

theorem is_O_self_const_mul (c : 𝕜) (hc : c ≠ 0) (f : α → 𝕜) (l : filter α) :
  is_O f (λ x, c * f x) l :=
is_O_self_const_mul' (is_unit.mk0 c hc) f l

theorem is_O_const_mul_left_iff' {f : α → R} {c : R} (hc : is_unit c) :
  is_O (λ x, c * f x) g l ↔ is_O f g l :=
⟨(is_O_self_const_mul' hc f l).trans, λ h, h.const_mul_left c⟩

theorem is_O_const_mul_left_iff {f : α → 𝕜} {c : 𝕜} (hc : c ≠ 0) :
  is_O (λ x, c * f x) g l ↔ is_O f g l :=
is_O_const_mul_left_iff' $ is_unit.mk0 c hc

theorem is_o.const_mul_left {f : α → R} (h : is_o f g l) (c : R) :
  is_o (λ x, c * f x) g l :=
(is_O_const_mul_self c f l).trans_is_o h

theorem is_o_const_mul_left_iff' {f : α → R} {c : R} (hc : is_unit c) :
  is_o (λ x, c * f x) g l ↔ is_o f g l :=
⟨(is_O_self_const_mul' hc f l).trans_is_o, λ h, h.const_mul_left c⟩

theorem is_o_const_mul_left_iff {f : α → 𝕜} {c : 𝕜} (hc : c ≠ 0) :
  is_o (λ x, c * f x) g l ↔ is_o f g l :=
is_o_const_mul_left_iff' $ is_unit.mk0 c hc

theorem is_O_with.of_const_mul_right {g : α → R} {c : R} (hc' : 0 ≤ c')
  (h : is_O_with c' f (λ x, c * g x) l) :
  is_O_with (c' * ∥c∥) f g l :=
h.trans (is_O_with_const_mul_self c g l) hc'

theorem is_O.of_const_mul_right {g : α → R} {c : R}
  (h : is_O f (λ x, c * g x) l) :
  is_O f g l :=
let ⟨c, cnonneg, hc⟩ := h.exists_nonneg in (hc.of_const_mul_right cnonneg).is_O

theorem is_O_with.const_mul_right' {g : α → R} {u : units R} {c' : ℝ} (hc' : 0 ≤ c')
  (h : is_O_with c' f g l) :
  is_O_with (c' * ∥(↑u⁻¹:R)∥) f (λ x, ↑u * g x) l :=
h.trans (is_O_with_self_const_mul' _ _ _) hc'

theorem is_O_with.const_mul_right {g : α → 𝕜} {c : 𝕜} (hc : c ≠ 0)
  {c' : ℝ} (hc' : 0 ≤ c') (h : is_O_with c' f g l) :
  is_O_with (c' * ∥c∥⁻¹) f (λ x, c * g x) l :=
h.trans (is_O_with_self_const_mul c hc g l) hc'

theorem is_O.const_mul_right' {g : α → R} {c : R} (hc : is_unit c) (h : is_O f g l) :
  is_O f (λ x, c * g x) l :=
h.trans (is_O_self_const_mul' hc g l)

theorem is_O.const_mul_right {g : α → 𝕜} {c : 𝕜} (hc : c ≠ 0) (h : is_O f g l) :
  is_O f (λ x, c * g x) l :=
h.const_mul_right' $ is_unit.mk0 c hc

theorem is_O_const_mul_right_iff' {g : α → R} {c : R} (hc : is_unit c) :
  is_O f (λ x, c * g x) l ↔ is_O f g l :=
⟨λ h, h.of_const_mul_right, λ h, h.const_mul_right' hc⟩

theorem is_O_const_mul_right_iff {g : α → 𝕜} {c : 𝕜} (hc : c ≠ 0) :
  is_O f (λ x, c * g x) l ↔ is_O f g l :=
is_O_const_mul_right_iff' $ is_unit.mk0 c hc

theorem is_o.of_const_mul_right {g : α → R} {c : R} (h : is_o f (λ x, c * g x) l) :
  is_o f g l :=
h.trans_is_O (is_O_const_mul_self c g l)

theorem is_o.const_mul_right' {g : α → R} {c : R} (hc : is_unit c) (h : is_o f g l) :
  is_o f (λ x, c * g x) l :=
h.trans_is_O (is_O_self_const_mul' hc g l)

theorem is_o.const_mul_right {g : α → 𝕜} {c : 𝕜} (hc : c ≠ 0) (h : is_o f g l) :
  is_o f (λ x, c * g x) l :=
h.const_mul_right' $ is_unit.mk0 c hc

theorem is_o_const_mul_right_iff' {g : α → R} {c : R} (hc : is_unit c) :
  is_o f (λ x, c * g x) l ↔ is_o f g l :=
⟨λ h, h.of_const_mul_right, λ h, h.const_mul_right' hc⟩

theorem is_o_const_mul_right_iff {g : α → 𝕜} {c : 𝕜} (hc : c ≠ 0) :
  is_o f (λ x, c * g x) l ↔ is_o f g l :=
is_o_const_mul_right_iff' $ is_unit.mk0 c hc

/-! ### Multiplication -/

theorem is_O_with.mul {f₁ f₂ : α → R} {g₁ g₂ : α → 𝕜} {c₁ c₂ : ℝ}
  (h₁ : is_O_with c₁ f₁ g₁ l) (h₂ : is_O_with c₂ f₂ g₂ l) :
  is_O_with (c₁ * c₂) (λ x, f₁ x * f₂ x) (λ x, g₁ x * g₂ x) l :=
begin
  unfold is_O_with at *,
  filter_upwards [h₁, h₂],
  intros x hx₁ hx₂,
  apply le_trans (norm_mul_le _ _),
  convert mul_le_mul hx₁ hx₂ (norm_nonneg _) (le_trans (norm_nonneg _) hx₁) using 1,
  rw normed_field.norm_mul,
  ac_refl
end

theorem is_O.mul {f₁ f₂ : α → R} {g₁ g₂ : α → 𝕜}
  (h₁ : is_O f₁ g₁ l) (h₂ : is_O f₂ g₂ l) :
  is_O (λ x, f₁ x * f₂ x) (λ x, g₁ x * g₂ x) l :=
let ⟨c, hc⟩ := h₁.is_O_with, ⟨c', hc'⟩ := h₂.is_O_with in (hc.mul hc').is_O

theorem is_O.mul_is_o {f₁ f₂ : α → R} {g₁ g₂ : α → 𝕜}
  (h₁ : is_O f₁ g₁ l) (h₂ : is_o f₂ g₂ l) :
  is_o (λ x, f₁ x * f₂ x) (λ x, g₁ x * g₂ x) l :=
begin
  unfold is_o at *,
  intros c cpos,
  rcases h₁.exists_pos with ⟨c', c'pos, hc'⟩,
  exact (hc'.mul (h₂ (div_pos cpos c'pos))).congr_const (mul_div_cancel' _ (ne_of_gt c'pos))
end

theorem is_o.mul_is_O {f₁ f₂ : α → R} {g₁ g₂ : α → 𝕜}
  (h₁ : is_o f₁ g₁ l) (h₂ : is_O f₂ g₂ l) :
  is_o (λ x, f₁ x * f₂ x) (λ x, g₁ x * g₂ x) l :=
begin
  unfold is_o at *,
  intros c cpos,
  rcases h₂.exists_pos with ⟨c', c'pos, hc'⟩,
  exact ((h₁ (div_pos cpos c'pos)).mul hc').congr_const (div_mul_cancel _ (ne_of_gt c'pos))
end

theorem is_o.mul {f₁ f₂ : α → R} {g₁ g₂ : α → 𝕜} (h₁ : is_o f₁ g₁ l) (h₂ : is_o f₂ g₂ l) :
  is_o (λ x, f₁ x * f₂ x) (λ x, g₁ x * g₂ x) l :=
h₁.mul_is_O h₂.is_O

theorem is_O_with.pow' {f : α → R} {g : α → 𝕜} (h : is_O_with c f g l) :
  ∀ n : ℕ, is_O_with (nat.cases_on n ∥(1 : R)∥ (λ n, c ^ (n + 1))) (λ x, f x ^ n) (λ x, g x ^ n) l
| 0 := by simpa using is_O_with_const_const (1 : R) (@one_ne_zero 𝕜 _ _) l
| 1 := by simpa
| (n + 2) := by simpa [pow_succ] using h.mul (is_O_with.pow' (n + 1))

theorem is_O_with.pow [norm_one_class R] {f : α → R} {g : α → 𝕜} (h : is_O_with c f g l) :
  ∀ n : ℕ, is_O_with (c ^ n) (λ x, f x ^ n) (λ x, g x ^ n) l
| 0 := by simpa using h.pow' 0
| (n + 1) := h.pow' (n + 1)

theorem is_O.pow {f : α → R} {g : α → 𝕜} (h : is_O f g l) (n : ℕ) :
  is_O (λ x, f x ^ n) (λ x, g x ^ n) l :=
let ⟨C, hC⟩ := h.is_O_with in is_O_iff_is_O_with.2 ⟨_, hC.pow' n⟩

theorem is_o.pow {f : α → R} {g : α → 𝕜} (h : is_o f g l) {n : ℕ} (hn : 0 < n) :
  is_o (λ x, f x ^ n) (λ x, g x ^ n) l :=
begin
  cases n, exact hn.false.elim, clear hn,
  induction n with n ihn, { simpa only [pow_one] },
  convert h.mul ihn; simp [pow_succ]
end

/-! ### Scalar multiplication -/

section smul_const
variables [normed_space 𝕜 E']

theorem is_O_with.const_smul_left (h : is_O_with c f' g l) (c' : 𝕜) :
  is_O_with (∥c'∥ * c) (λ x, c' • f' x) g l :=
by refine ((h.norm_left.const_mul_left (∥c'∥)).congr _ _ (λ _, rfl)).of_norm_left;
    intros; simp only [norm_norm, norm_smul]

theorem is_O_const_smul_left_iff {c : 𝕜} (hc : c ≠ 0) :
  is_O (λ x, c • f' x) g l ↔ is_O f' g l :=
begin
  have cne0 : ∥c∥ ≠ 0, from mt norm_eq_zero.mp hc,
  rw [←is_O_norm_left], simp only [norm_smul],
  rw [is_O_const_mul_left_iff cne0, is_O_norm_left],
end

theorem is_o_const_smul_left (h : is_o f' g l) (c : 𝕜) :
  is_o (λ x, c • f' x) g l :=
begin
  refine ((h.norm_left.const_mul_left (∥c∥)).congr_left _).of_norm_left,
  exact λ x, (norm_smul _ _).symm
end

theorem is_o_const_smul_left_iff {c : 𝕜} (hc : c ≠ 0) :
  is_o (λ x, c • f' x) g l ↔ is_o f' g l :=
begin
  have cne0 : ∥c∥ ≠ 0, from mt norm_eq_zero.mp hc,
  rw [←is_o_norm_left], simp only [norm_smul],
  rw [is_o_const_mul_left_iff cne0, is_o_norm_left]
end

theorem is_O_const_smul_right {c : 𝕜} (hc : c ≠ 0) :
  is_O f (λ x, c • f' x) l ↔ is_O f f' l :=
begin
  have cne0 : ∥c∥ ≠ 0, from mt norm_eq_zero.mp hc,
  rw [←is_O_norm_right], simp only [norm_smul],
  rw [is_O_const_mul_right_iff cne0, is_O_norm_right]
end

theorem is_o_const_smul_right {c : 𝕜} (hc : c ≠ 0) :
  is_o f (λ x, c • f' x) l ↔ is_o f f' l :=
begin
  have cne0 : ∥c∥ ≠ 0, from mt norm_eq_zero.mp hc,
  rw [←is_o_norm_right], simp only [norm_smul],
  rw [is_o_const_mul_right_iff cne0, is_o_norm_right]
end

end smul_const

section smul

variables [normed_space 𝕜 E'] [normed_space 𝕜 F']

theorem is_O_with.smul {k₁ k₂ : α → 𝕜} (h₁ : is_O_with c k₁ k₂ l) (h₂ : is_O_with c' f' g' l) :
  is_O_with (c * c') (λ x, k₁ x • f' x) (λ x, k₂ x • g' x) l :=
by refine ((h₁.norm_norm.mul h₂.norm_norm).congr rfl _ _).of_norm_norm;
  by intros; simp only [norm_smul]

theorem is_O.smul {k₁ k₂ : α → 𝕜} (h₁ : is_O k₁ k₂ l) (h₂ : is_O f' g' l) :
  is_O (λ x, k₁ x • f' x) (λ x, k₂ x • g' x) l :=
by refine ((h₁.norm_norm.mul h₂.norm_norm).congr _ _).of_norm_norm;
  by intros; simp only [norm_smul]

theorem is_O.smul_is_o {k₁ k₂ : α → 𝕜} (h₁ : is_O k₁ k₂ l) (h₂ : is_o f' g' l) :
  is_o (λ x, k₁ x • f' x) (λ x, k₂ x • g' x) l :=
by refine ((h₁.norm_norm.mul_is_o h₂.norm_norm).congr _ _).of_norm_norm;
  by intros; simp only [norm_smul]

theorem is_o.smul_is_O {k₁ k₂ : α → 𝕜} (h₁ : is_o k₁ k₂ l) (h₂ : is_O f' g' l) :
  is_o (λ x, k₁ x • f' x) (λ x, k₂ x • g' x) l :=
by refine ((h₁.norm_norm.mul_is_O h₂.norm_norm).congr _ _).of_norm_norm;
  by intros; simp only [norm_smul]

theorem is_o.smul {k₁ k₂ : α → 𝕜} (h₁ : is_o k₁ k₂ l) (h₂ : is_o f' g' l) :
  is_o (λ x, k₁ x • f' x) (λ x, k₂ x • g' x) l :=
by refine ((h₁.norm_norm.mul h₂.norm_norm).congr _ _).of_norm_norm;
  by intros; simp only [norm_smul]

end smul

/-! ### Sum -/

section sum

variables {ι : Type*} {A : ι → α → E'} {C : ι → ℝ} {s : finset ι}

theorem is_O_with.sum (h : ∀ i ∈ s, is_O_with (C i) (A i) g l) :
  is_O_with (∑ i in s, C i) (λ x, ∑ i in s, A i x) g l :=
begin
  induction s using finset.induction_on with i s is IH,
  { simp only [is_O_with_zero', finset.sum_empty, forall_true_iff] },
  { simp only [is, finset.sum_insert, not_false_iff],
    exact (h _ (finset.mem_insert_self i s)).add (IH (λ j hj, h _ (finset.mem_insert_of_mem hj))) }
end

theorem is_O.sum (h : ∀ i ∈ s, is_O (A i) g l) :
  is_O (λ x, ∑ i in s, A i x) g l :=
begin
  induction s using finset.induction_on with i s is IH,
  { simp only [is_O_zero, finset.sum_empty, forall_true_iff] },
  { simp only [is, finset.sum_insert, not_false_iff],
    exact (h _ (finset.mem_insert_self i s)).add (IH (λ j hj, h _ (finset.mem_insert_of_mem hj))) }
end

theorem is_o.sum (h : ∀ i ∈ s, is_o (A i) g' l) :
  is_o (λ x, ∑ i in s, A i x) g' l :=
begin
  induction s using finset.induction_on with i s is IH,
  { simp only [is_o_zero, finset.sum_empty, forall_true_iff] },
  { simp only [is, finset.sum_insert, not_false_iff],
    exact (h _ (finset.mem_insert_self i s)).add (IH (λ j hj, h _ (finset.mem_insert_of_mem hj))) }
end

end sum

/-! ### Relation between `f = o(g)` and `f / g → 0` -/

theorem is_o.tendsto_0 {f g : α → 𝕜} {l : filter α} (h : is_o f g l) :
  tendsto (λ x, f x / (g x)) l (𝓝 0) :=
have eq₁ : is_o (λ x, f x / g x) (λ x, g x / g x) l,
  by simpa only [div_eq_mul_inv] using h.mul_is_O (is_O_refl _ _),
have eq₂ : is_O (λ x, g x / g x) (λ x, (1 : 𝕜)) l,
  from is_O_of_le _ (λ x, by by_cases h : ∥g x∥ = 0; simp [h, zero_le_one]),
(is_o_one_iff 𝕜).mp (eq₁.trans_is_O eq₂)

theorem is_o_iff_tendsto' {f g : α → 𝕜} {l : filter α}
    (hgf : ∀ᶠ x in l, g x = 0 → f x = 0) :
  is_o f g l ↔ tendsto (λ x, f x / (g x)) l (𝓝 0) :=
iff.intro is_o.tendsto_0 $ λ h,
  (((is_o_one_iff _).mpr h).mul_is_O (is_O_refl g l)).congr'
    (hgf.mono $ λ x, div_mul_cancel_of_imp) (eventually_of_forall $ λ x, one_mul _)

theorem is_o_iff_tendsto {f g : α → 𝕜} {l : filter α}
    (hgf : ∀ x, g x = 0 → f x = 0) :
  is_o f g l ↔ tendsto (λ x, f x / (g x)) l (𝓝 0) :=
⟨λ h, h.tendsto_0, (is_o_iff_tendsto' (eventually_of_forall hgf)).2⟩

alias is_o_iff_tendsto' ↔ _ asymptotics.is_o_of_tendsto'
alias is_o_iff_tendsto ↔ _ asymptotics.is_o_of_tendsto

/-!
### Eventually (u / v) * v = u

If `u` and `v` are linked by an `is_O_with` relation, then we
eventually have `(u / v) * v = u`, even if `v` vanishes.
-/

section eventually_mul_div_cancel

variables {u v : α → 𝕜}

lemma is_O_with.eventually_mul_div_cancel (h : is_O_with c u v l) :
  (u / v) * v =ᶠ[l] u :=
eventually.mono h.bound (λ y hy, div_mul_cancel_of_imp $ λ hv, by simpa [hv] using hy)

/-- If `u = O(v)` along `l`, then `(u / v) * v = u` eventually at `l`. -/
lemma is_O.eventually_mul_div_cancel (h : is_O u v l) :  (u / v) * v =ᶠ[l] u :=
let ⟨c, hc⟩ := h.is_O_with in hc.eventually_mul_div_cancel

/-- If `u = o(v)` along `l`, then `(u / v) * v = u` eventually at `l`. -/
lemma is_o.eventually_mul_div_cancel (h : is_o u v l) : (u / v) * v =ᶠ[l] u :=
(h.forall_is_O_with zero_lt_one).eventually_mul_div_cancel

end eventually_mul_div_cancel

/-! ### Equivalent definitions of the form `∃ φ, u =ᶠ[l] φ * v` in a `normed_field`. -/

section exists_mul_eq

variables {u v : α → 𝕜}

/-- If `∥φ∥` is eventually bounded by `c`, and `u =ᶠ[l] φ * v`, then we have `is_O_with c u v l`.
    This does not require any assumptions on `c`, which is why we keep this version along with
    `is_O_with_iff_exists_eq_mul`. -/
lemma is_O_with_of_eq_mul (φ : α → 𝕜) (hφ : ∀ᶠ x in l, ∥φ x∥ ≤ c) (h : u =ᶠ[l] φ * v) :
  is_O_with c u v l :=
begin
  unfold is_O_with,
  refine h.symm.rw (λ x a, ∥a∥ ≤ c * ∥v x∥) (hφ.mono $ λ x hx, _),
  simp only [normed_field.norm_mul, pi.mul_apply],
  exact mul_le_mul_of_nonneg_right hx (norm_nonneg _)
end

lemma is_O_with_iff_exists_eq_mul (hc : 0 ≤ c) :
  is_O_with c u v l ↔ ∃ (φ : α → 𝕜) (hφ : ∀ᶠ x in l, ∥φ x∥ ≤ c), u =ᶠ[l] φ * v :=
begin
  split,
  { intro h,
    use (λ x, u x / v x),
    refine ⟨eventually.mono h.bound (λ y hy, _), h.eventually_mul_div_cancel.symm⟩,
    simpa using div_le_of_nonneg_of_le_mul (norm_nonneg _) hc hy },
  { rintros ⟨φ, hφ, h⟩,
    exact is_O_with_of_eq_mul φ hφ h }
end

lemma is_O_with.exists_eq_mul (h : is_O_with c u v l) (hc : 0 ≤ c) :
  ∃ (φ : α → 𝕜) (hφ : ∀ᶠ x in l, ∥φ x∥ ≤ c), u =ᶠ[l] φ * v :=
(is_O_with_iff_exists_eq_mul hc).mp h

lemma is_O_iff_exists_eq_mul :
  is_O u v l ↔ ∃ (φ : α → 𝕜) (hφ : l.is_bounded_under (≤) (norm ∘ φ)), u =ᶠ[l] φ * v :=
begin
  split,
  { rintros h,
    rcases h.exists_nonneg with ⟨c, hnnc, hc⟩,
    rcases hc.exists_eq_mul hnnc with ⟨φ, hφ, huvφ⟩,
    exact ⟨φ, ⟨c, hφ⟩, huvφ⟩ },
  { rintros ⟨φ, ⟨c, hφ⟩, huvφ⟩,
    exact is_O_iff_is_O_with.2 ⟨c, is_O_with_of_eq_mul φ hφ huvφ⟩ }
end

alias is_O_iff_exists_eq_mul ↔ asymptotics.is_O.exists_eq_mul _

lemma is_o_iff_exists_eq_mul :
  is_o u v l ↔ ∃ (φ : α → 𝕜) (hφ : tendsto φ l (𝓝 0)), u =ᶠ[l] φ * v :=
begin
  split,
  { exact λ h, ⟨λ x, u x / v x, h.tendsto_0, h.eventually_mul_div_cancel.symm⟩ },
  { unfold is_o, rintros ⟨φ, hφ, huvφ⟩ c hpos,
    rw normed_group.tendsto_nhds_zero at hφ,
    exact is_O_with_of_eq_mul _ ((hφ c hpos).mono $ λ x, le_of_lt)  huvφ }
end

alias is_o_iff_exists_eq_mul ↔ asymptotics.is_o.exists_eq_mul _

end exists_mul_eq

/-! ### Miscellanous lemmas -/

theorem div_is_bounded_under_of_is_O {α : Type*} {l : filter α}
  {f g : α → 𝕜} (h : is_O f g l) :
  is_bounded_under (≤) l (λ x, ∥f x / g x∥) :=
begin
  obtain ⟨c, hc⟩ := is_O_iff.mp h,
  refine ⟨max c 0, eventually_map.2 (filter.mem_sets_of_superset hc (λ x hx, _))⟩,
  simp only [mem_set_of_eq, normed_field.norm_div] at ⊢ hx,
  by_cases hgx : g x = 0,
  { rw [hgx, norm_zero, div_zero, le_max_iff],
    exact or.inr le_rfl },
  { exact le_max_iff.2 (or.inl ((div_le_iff (norm_pos_iff.2 hgx)).2 hx)) }
end

theorem is_O_iff_div_is_bounded_under {α : Type*} {l : filter α}
  {f g : α → 𝕜} (hgf : ∀ᶠ x in l, g x = 0 → f x = 0) :
  is_O f g l ↔ is_bounded_under (≤) l (λ x, ∥f x / g x∥) :=
begin
  refine ⟨div_is_bounded_under_of_is_O, λ h, _⟩,
  obtain ⟨c, hc⟩ := h,
  rw filter.eventually_iff at hgf hc,
  simp only [mem_set_of_eq, mem_map, normed_field.norm_div] at hc,
  refine is_O_iff.2 ⟨c, filter.eventually_of_mem (inter_mem_sets hgf hc) (λ x hx, _)⟩,
  by_cases hgx : g x = 0,
  { simp [hx.1 hgx, hgx] },
  { refine (div_le_iff (norm_pos_iff.2 hgx)).mp hx.2 },
end

theorem is_O_of_div_tendsto_nhds {α : Type*} {l : filter α}
  {f g : α → 𝕜} (hgf : ∀ᶠ x in l, g x = 0 → f x = 0)
  (c : 𝕜) (H : filter.tendsto (f / g) l (𝓝 c)) :
  is_O f g l :=
(is_O_iff_div_is_bounded_under hgf).2 $ is_bounded_under_of_tendsto H

lemma is_o.tendsto_zero_of_tendsto {α E 𝕜 : Type*} [normed_group E] [normed_field 𝕜] {u : α → E}
  {v : α → 𝕜} {l : filter α} {y : 𝕜} (huv : is_o u v l) (hv : tendsto v l (𝓝 y)) :
  tendsto u l (𝓝 0) :=
begin
  suffices h : is_o u (λ x, (1 : 𝕜)) l,
  { rwa is_o_one_iff at h },
  exact huv.trans_is_O (is_O_one_of_tendsto 𝕜 hv),
end

theorem is_o_pow_pow {m n : ℕ} (h : m < n) :
  is_o (λ(x : 𝕜), x^n) (λx, x^m) (𝓝 0) :=
begin
  let p := n - m,
  have nmp : n = m + p := (nat.add_sub_cancel' (le_of_lt h)).symm,
  have : (λ(x : 𝕜), x^m) = (λx, x^m * 1), by simp only [mul_one],
  simp only [this, pow_add, nmp],
  refine is_O.mul_is_o (is_O_refl _ _) ((is_o_one_iff _).2 _),
  convert (continuous_pow p).tendsto (0 : 𝕜),
  exact (zero_pow (nat.sub_pos_of_lt h)).symm
end

theorem is_o_norm_pow_norm_pow {m n : ℕ} (h : m < n) :
  is_o (λ(x : E'), ∥x∥^n) (λx, ∥x∥^m) (𝓝 (0 : E')) :=
(is_o_pow_pow h).comp_tendsto tendsto_norm_zero

theorem is_o_pow_id {n : ℕ} (h : 1 < n) :
  is_o (λ(x : 𝕜), x^n) (λx, x) (𝓝 0) :=
by { convert is_o_pow_pow h, simp only [pow_one] }

theorem is_o_norm_pow_id {n : ℕ} (h : 1 < n) :
  is_o (λ(x : E'), ∥x∥^n) (λx, x) (𝓝 0) :=
by simpa only [pow_one, is_o_norm_right] using @is_o_norm_pow_norm_pow E' _ _ _ h

theorem is_O_with.right_le_sub_of_lt_1 {f₁ f₂ : α → E'} (h : is_O_with c f₁ f₂ l) (hc : c < 1) :
  is_O_with (1 / (1 - c)) f₂ (λx, f₂ x - f₁ x) l :=
is_O_with.of_bound $ mem_sets_of_superset h.bound $ λ x hx,
begin
  simp only [mem_set_of_eq] at hx ⊢,
  rw [mul_comm, one_div, ← div_eq_mul_inv, le_div_iff, mul_sub, mul_one, mul_comm],
  { exact le_trans (sub_le_sub_left hx _) (norm_sub_norm_le _ _) },
  { exact sub_pos.2 hc }
end

theorem is_O_with.right_le_add_of_lt_1 {f₁ f₂ : α → E'} (h : is_O_with c f₁ f₂ l) (hc : c < 1) :
  is_O_with (1 / (1 - c)) f₂ (λx, f₁ x + f₂ x) l :=
(h.neg_right.right_le_sub_of_lt_1 hc).neg_right.of_neg_left.congr rfl (λ x, rfl)
  (λ x, by rw [neg_sub, sub_neg_eq_add])

theorem is_o.right_is_O_sub {f₁ f₂ : α → E'} (h : is_o f₁ f₂ l) :
  is_O f₂ (λx, f₂ x - f₁ x) l :=
((h.def' one_half_pos).right_le_sub_of_lt_1 one_half_lt_one).is_O

theorem is_o.right_is_O_add {f₁ f₂ : α → E'} (h : is_o f₁ f₂ l) :
  is_O f₂ (λx, f₁ x + f₂ x) l :=
((h.def' one_half_pos).right_le_add_of_lt_1 one_half_lt_one).is_O

/-- If `f x = O(g x)` along `cofinite`, then there exists a positive constant `C` such that
`∥f x∥ ≤ C * ∥g x∥` whenever `g x ≠ 0`. -/
theorem bound_of_is_O_cofinite (h : is_O f g' cofinite) :
  ∃ C > 0, ∀ ⦃x⦄, g' x ≠ 0 → ∥f x∥ ≤ C * ∥g' x∥ :=
begin
  rcases h.exists_pos with ⟨C, C₀, hC⟩,
  rw [is_O_with, eventually_cofinite] at hC,
  rcases (hC.to_finset.image (λ x, ∥f x∥ / ∥g' x∥)).exists_le with ⟨C', hC'⟩,
  have : ∀ x, C * ∥g' x∥ < ∥f x∥ → ∥f x∥ / ∥g' x∥ ≤ C', by simpa using hC',
  refine ⟨max C C', lt_max_iff.2 (or.inl C₀), λ x h₀, _⟩,
  rw [max_mul_of_nonneg _ _ (norm_nonneg _), le_max_iff, or_iff_not_imp_left, not_le],
  exact λ hx, (div_le_iff (norm_pos_iff.2 h₀)).1 (this _ hx)
end

theorem is_O_cofinite_iff (h : ∀ x, g' x = 0 → f' x = 0) :
  is_O f' g' cofinite ↔ ∃ C, ∀ x, ∥f' x∥ ≤ C * ∥g' x∥ :=
⟨λ h', let ⟨C, C₀, hC⟩ := bound_of_is_O_cofinite h' in
  ⟨C, λ x, if hx : g' x = 0 then by simp [h _ hx, hx] else hC hx⟩,
  λ h, (is_O_top.2 h).mono le_top⟩

theorem bound_of_is_O_nat_at_top {f : ℕ → E} {g' : ℕ → E'} (h : is_O f g' at_top) :
  ∃ C > 0, ∀ ⦃x⦄, g' x ≠ 0 → ∥f x∥ ≤ C * ∥g' x∥ :=
bound_of_is_O_cofinite $ by rwa nat.cofinite_eq_at_top

theorem is_O_nat_at_top_iff {f : ℕ → E'} {g : ℕ → F'} (h : ∀ x, g x = 0 → f x = 0) :
  is_O f g at_top ↔ ∃ C, ∀ x, ∥f x∥ ≤ C * ∥g x∥ :=
by rw [← nat.cofinite_eq_at_top, is_O_cofinite_iff h]

theorem is_O_one_nat_at_top_iff {f : ℕ → E'} :
  is_O f (λ n, 1 : ℕ → ℝ) at_top ↔ ∃ C, ∀ n, ∥f n∥ ≤ C :=
iff.trans (is_O_nat_at_top_iff (λ n h, (one_ne_zero h).elim)) $
  by simp only [norm_one, mul_one]

theorem is_O_with_pi {ι : Type*} [fintype ι] {E' : ι → Type*} [Π i, normed_group (E' i)]
  {f : α → Π i, E' i} {C : ℝ} (hC : 0 ≤ C) :
  is_O_with C f g' l ↔ ∀ i, is_O_with C (λ x, f x i) g' l :=
have ∀ x, 0 ≤ C * ∥g' x∥, from λ x, mul_nonneg hC (norm_nonneg _),
by simp only [is_O_with_iff, pi_norm_le_iff (this _), eventually_all]

@[simp] theorem is_O_pi {ι : Type*} [fintype ι] {E' : ι → Type*} [Π i, normed_group (E' i)]
  {f : α → Π i, E' i} :
  is_O f g' l ↔ ∀ i, is_O (λ x, f x i) g' l :=
begin
  simp only [is_O_iff_eventually_is_O_with, ← eventually_all],
  exact eventually_congr (eventually_at_top.2 ⟨0, λ c, is_O_with_pi⟩)
end

@[simp] theorem is_o_pi {ι : Type*} [fintype ι] {E' : ι → Type*} [Π i, normed_group (E' i)]
  {f : α → Π i, E' i} :
  is_o f g' l ↔ ∀ i, is_o (λ x, f x i) g' l :=
begin
  simp only [is_o, is_O_with_pi, le_of_lt] { contextual := tt },
  exact ⟨λ h i c hc, h hc i, λ h c hc i, h i hc⟩
end

end asymptotics

open asymptotics

lemma summable_of_is_O {ι E} [normed_group E] [complete_space E] {f : ι → E} (g : ι → ℝ)
  (hg : summable g) (h : is_O f g cofinite) : summable f :=
let ⟨C, hC⟩ := h.is_O_with in
summable_of_norm_bounded_eventually (λ x, C * ∥g x∥) (hg.abs.mul_left _) hC.bound

lemma summable_of_is_O_nat {E} [normed_group E] [complete_space E] {f : ℕ → E} (g : ℕ → ℝ)
  (hg : summable g) (h : is_O f g at_top) : summable f :=
summable_of_is_O g hg $ nat.cofinite_eq_at_top.symm ▸ h

namespace local_homeomorph

variables {α : Type*} {β : Type*} [topological_space α] [topological_space β]

variables {E : Type*} [has_norm E] {F : Type*} [has_norm F]

/-- Transfer `is_O_with` over a `local_homeomorph`. -/
lemma is_O_with_congr (e : local_homeomorph α β) {b : β} (hb : b ∈ e.target)
  {f : β → E} {g : β → F} {C : ℝ} :
  is_O_with C f g (𝓝 b) ↔ is_O_with C (f ∘ e) (g ∘ e) (𝓝 (e.symm b)) :=
⟨λ h, h.comp_tendsto $
  by { convert e.continuous_at (e.map_target hb), exact (e.right_inv hb).symm },
  λ h, (h.comp_tendsto (e.continuous_at_symm hb)).congr' rfl
    ((e.eventually_right_inverse hb).mono $ λ x hx, congr_arg f hx)
    ((e.eventually_right_inverse hb).mono $ λ x hx, congr_arg g hx)⟩

/-- Transfer `is_O` over a `local_homeomorph`. -/
lemma is_O_congr (e : local_homeomorph α β) {b : β} (hb : b ∈ e.target) {f : β → E} {g : β → F} :
  is_O f g (𝓝 b) ↔ is_O (f ∘ e) (g ∘ e) (𝓝 (e.symm b)) :=
by { unfold is_O, exact exists_congr (λ C, e.is_O_with_congr hb) }

/-- Transfer `is_o` over a `local_homeomorph`. -/
lemma is_o_congr (e : local_homeomorph α β) {b : β} (hb : b ∈ e.target) {f : β → E} {g : β → F} :
  is_o f g (𝓝 b) ↔ is_o (f ∘ e) (g ∘ e) (𝓝 (e.symm b)) :=
by { unfold is_o, exact (forall_congr $ λ c, forall_congr $ λ hc, e.is_O_with_congr hb) }

end local_homeomorph

namespace homeomorph

variables {α : Type*} {β : Type*} [topological_space α] [topological_space β]

variables {E : Type*} [has_norm E] {F : Type*} [has_norm F]

open asymptotics

/-- Transfer `is_O_with` over a `homeomorph`. -/
lemma is_O_with_congr (e : α ≃ₜ β) {b : β} {f : β → E} {g : β → F} {C : ℝ} :
  is_O_with C f g (𝓝 b) ↔ is_O_with C (f ∘ e) (g ∘ e) (𝓝 (e.symm b)) :=
e.to_local_homeomorph.is_O_with_congr trivial

/-- Transfer `is_O` over a `homeomorph`. -/
lemma is_O_congr (e : α ≃ₜ β) {b : β} {f : β → E} {g : β → F} :
  is_O f g (𝓝 b) ↔ is_O (f ∘ e) (g ∘ e) (𝓝 (e.symm b)) :=
by { unfold is_O, exact exists_congr (λ C, e.is_O_with_congr) }

/-- Transfer `is_o` over a `homeomorph`. -/
lemma is_o_congr (e : α ≃ₜ β) {b : β} {f : β → E} {g : β → F} :
  is_o f g (𝓝 b) ↔ is_o (f ∘ e) (g ∘ e) (𝓝 (e.symm b)) :=
by { unfold is_o, exact forall_congr (λ c, forall_congr (λ hc, e.is_O_with_congr)) }

end homeomorph


import tactic --hide

/-

#Logical statements

In Logic, a *statement* is a sentence that is either true of false, for example:

```md
1. The sum of two odd numbers is an even number
2. 36 is divisible by 3
3. x > 2
```

One of the goals of this game is to show you how to work with logical statements on a computer,
specifically using a computer language called Lean. In Lean, logical staments live in a
place called `Prop` (which is short for "Propositions"). For example, if we write  <mark style ="background-color :   #ebdef0 ">`P : Prop`</mark>
we mean "`P` is a logical statement."  Think of the "`:`" symbol as saying `P` lives in `Prop`
(in Maths this would be denoted by `P ∈ Prop`, to mean `P` is an element of `Prop`).

#Truth Values

The *truth value* of a logical statement (i.e. some `P : Prop` is whether the statement is true or
false. For example, the truth value of the statement: "For all integers $x ∈ ℤ$, $0 ≤ x^2$"
is *true*, while "For all integers $x ∈ ℤ$, $0 ≤ x^3$" is *false*.

In Lean we assign truth values as follows: We first think of a proposition $P$ as a set with
*at most* one element. If this set is empty, then we say `P` is *false*. Otherwise, if this set has
an element, lets call it `p`, then this statement is *true* and `p` is the proof. We will usually
use upper case letters for logical statements, and lower case for their proofs.

To recap, we write `P : Prop` to mean `P` is a
proposition and we write  <mark style ="background-color : #ebdef0 ">`p : P`</mark>  to mean "`p` is a
proof of `P`".

The aim of this game is to learn how to prove logical statements, which we call *lemmas* through a
series of manipulations which we call *tactics*.

Lets start with a simple example of how a lemma looks like in Lean.

This is where the window on the right will come into play. If you delete the *sorry* below,
on the right you will see the following state:

```
P : Prop,
p : P,
⊢ P
```

Here, the first two lines are our assumptions, where the first line `P : Prop` says "$P$ is a
logical statement" and the second line `p : P` says `p` is the proof of `P`.

The thing after the `⊢` symbol is what we need to prove to win the level (i.e. prove the lemma).

In this game our "moves" or, as we call them in Lean, *tactics*, are what will allow us to
manipulate our statements and produce proofs.

The first tactic we will use is called *exact*. It is used when you need to prove a
statement $P$ and you have the proof of it. In the example above we have `p : P` and we need to
prove `P`, then typing <mark style ="background-color :   #ebdef0 ">`exact p,`</mark>  will prove it.

**Note:  We end each line with a comma `,` to tell Lean that this is the end of our move.**

Try it for yourself now and see what happens!

-/

/- Lemma
If $P$ is a logical stament and $p$ is a proof of $P$ then $P$ is true.
-/
lemma example_one (P : Prop) (p : P) : P :=
begin
  exact p,

end

/- Tactic : exact
## Summary
If the goal is `⊢ P` then `exact p,` will close the goal if
and only if `p` is a proof of `P` (i.e. `p : P`)
## Details
Say $P$, $Q$ and $R$ are logical staments
and your lemma looks like this:
```
p : P,
h : P → Q,
j : Q → R
⊢ R
```
Then you can solve the level by using `exact j(h(p)),`
-/


/-
In this unit, we implement our own version of the Boolean algebra
already built into Lean. We will define our own version of Lean's
bool type, and our own versions of its and, or, and not operators.
-/

/-
In doing this, we will encounter several important concepts in
Lean, which are also shared by many other languages. They include
the following:

- namespaces
- inductive type definitions
- sets, product sets, tuples, binary relations, and functions
- type judgments, type declarations, and type inference
- function types and function values
- functional programing: case analysis via pattern matching
-/

-- NAMESPACES

/-
Picking good names for concepts is important in every field.
It's  especially important in computer science and software
development because we want complex "code," often involving
hundreds, thousands, or even millions of identifiers (the
CS term for a name to which a meaning is given in a program),
to be humanly understandable.

For example, rather than using a cryptic identifier, such as
"n", to refer to the "next node" to be processed in a loop,
one might instead use the identifier "next_node". It makes
no difference to the machine, but it makes a big difference
to the programmer coming to the code for the first time or
returning to it after some time has passed. The names that
we pick in this way can serve to document the intended
interpretation of the otherwise cryptic logic of the code.

A problem arises, of course, when two programmers, working
on two different parts of a larger program, decide to use
the same name for different concepts. One can imagine two
programmers both deciding to use "next_node" to refer to
the next object to be processed in two unrelated pieces of
code. When those pieces of code are brought together, the
result could be that the name becomes ambiguous: with two
different meanings.

A general solution to this problem employed by many
different programming languages is to define meanings
for identifiers within different namespaces. A namespace
is just a prefix that it implicitly prepended to each
name defined within the namespace.

If you've programmed in Python, Java, or most any other
industrial programming language, then you've seen the
use of namespaces.

In this class, we will enclose all of our definitions
within a namespace called edu.virginia.cs.dm. The idea
is that (1) the namespace explains the context in which
names are defined, i.e., the discrete math (dm) class
at UVa; and (2) no one else seems likely to pick the
same namespace. The next line of code tells Lean that
all names between here at the "end edu.virginia.cs.dm"
command at the end of the file are defined within the
edu.virginia.cs.dm namespace.
-/

namespace edu.virginia.cs.dm

/-
What this means is that every name, such as dm_bool,
below is implicitly prefixed with edu.virginia.cs.dm.
The "fully qualified" name for dm_bool as defined below
is thus edu.virginia.cs.dm.dm_bool.
-/

/-
With that ancillary detail out of the way we can
now turn to the main content of this chapter, giving
our own implementation of Boolean algebra.
-/

/-
**** INDUCTIVE DEFINITIONS OF TYPES: CASE STUDY OF bool ****
-/

/-
Our implementation comprises (1) a definition of the "carrier
set" of values of the Boolean type, and (2) a definition of a
set of operations (functions) closed on this set.

We first specify the "carrier set" of values of Boolean
algebra. The values are typically called true and false
(with different names in different natural languages).
The set of values of Boolean algebra can thus be written
as { true, false }. We call this "display notation".

A data type defines nothing other than a set of values,
namely the values that are specified as having that type
and no other values whatsoever.

Here's how we will represent the carrier set of Boolean
algebra as a data type in the Lean language. We explain
this code below.
-/

inductive dm_bool : Type
| dm_ff : dm_bool
| dm_tt : dm_bool

/-
To avoid any possibility of confusion between the
built-in bool type, and its tt and ff values (even
though we're in our own namespace here), we will
define our own Boolean algebra implementation to
be like the built-in version but with all names
prefixed with an "dm".
-/

/-
In Lean, as in many other functional languages, we
define types inductively. We specify the name of the
type and then we give a list of constructors that
specify the values of the type being defined.

The Lean keyword "inductive" introduces an inductive
definition. The name of the type being defined is
dm_bool, which is declared to be a type. (Technically
it is declared to be a value of type, Type, which,
as you might guess, is a type! Yep, types are values
too, in Lean: values of type Type.)

Finally a list of constructors is given, each starting
with a vertical bar, followed by the name of the
constructor and, at least in the simple case here,
a statement saying that the given constructor *is* a
value of the type being defined.

We have thus just defined a type called dm_bool with
two values, and no other values, namely dm_tt and dm_ff.
Our *intended* interpretation is that dm_bool represents
the carrier set of Boolean algebra, dm_tt represents the
true value of Boolean algebram and dm_ff represents
the false value.

This definition precisely mirrors the definition of
the built-in Boolean type in Lean, which is specified
like this:

inductive bool : Type
| ff : bool
| tt : bool

-/

/-
Our inductive type definition has actually defined meanings
for three different names: dm_bool, dm_tt, and dm_ff. The name, or
identifier, dm_bool, now refers to a type with two values, dm_tt
and dm_ff. Each dm_tt and dm_ff refer to two different constructors
that in effect define the two different values of the dm_bool
type.

We can use Lean's #check command to see that these names are
defined, and Lean will tell us the type of thing that each
name means. We start by using the fully qualified name of
our dm_bool type. Hover your mouse pointer over the two
parts of this command to see what it tells you.
-/

#check edu.virginia.cs.dm.dm_bool

/-
Because we are within the edu.virginia.cs.dm namespace here,
and because there is no ambiguity, we can leave off the whole
edu.virginia.cs.dm prefix. Note that when you hover over dm_bool
in the following #check command, it does tell you the fully
qualified name of dm_bool.
-/

#check dm_bool

/-
Importantly, Lean tells you that the type of dm_bool is Type.
That's it's way of saying "dm_bool is a type!"
-/

/-
You might think that you can also use the #check command
to check to see what's the type of dm_tt, and indeed we can.
However, we can't just do "#check dm_tt" because the name,
dm_tt, is defined in a namespace created by the dm_bool type.
Uncomment the following code and see what error you get.
-/

-- #check dm_tt

/-
Here's what you need to do instead.
-/

#check dm_bool.dm_tt

/-
This is equivalent to the following.
-/

#check edu.virginia.cs.dm.dm_bool.dm_tt

/-
What we see is that the name, dm_bool, as defined in the
current namespace, is "visible" in this namespace, but
the names defined in its namespace are still hidden away.

If you want to be able to use the names dm_tt and dm_ff
without qualifiers, you have to "open" the namespace
in which they are defined, i.e., the dm_bool namespace.
-/

open dm_bool

/-
Ah, now it all just works. That's it on namespaces.
-/

#check dm_tt
#check dm_ff

/-
We've thus got the first component of our algebra defined: the
carrier set of Boolean values! To complete our implementation,
we need to define a set of operations on this set: operations
such as "and", "or", and "not", which should already be familiar
from CS1 (abh: CS 1xxx?). Take a few minutes to recall exactly how they work in
a language you already know (e..g, Python).
-/

/-
EXERCISES:

* In the language you used for CS1, assign the Boolean
value, true, however it is written in your language, to
a variable called X. Assign the Boolean variable, false,
to Y. Assign to a variable, Z, the value of the negation
of ("not") X. Assign to W the disjunction ("or") of X and
Y. Finally print the value of the conjunction of W and
true. What value is produced? Every useful programming
languages provides a built-in implementation of Boolean
algebra. Now you're building your own.
-/

-- MATHEMATICAL FUNCTIONS AND PURE FUNCTIONAL PROGRAMS

/-
As you now recall from CS1, "and", "or", and "not" are really
just functions that take and return Boolean values. The "not"
function takes a single bool value as an argument ("not" is a
unary function) and returns its opposite bool value. The "and"
and "or" operations each take two bool values (they are binary
operations) and return bool values.
-/

/-
We now complete a definition of a limited version Boolean algebra
by defining several unary functions and binary operations over the
Booleans. A unary operation (aka function) takes one argument. A
binary operation takes two. Let's start with unary operations.
-/

/- THE UNARY OPERATIONS OF BOOLEAN ALGEBRA -/

/-
A unary operation takes one argument and on the basis of its value
alone returns a result. Boolean operations take and return Boolean
values, here implemented (represented, if you will) as the value of
our type, bool.

The functions of an algebra are "closed" on the carrier set of that
algebra. What this means is that each such functions yields a result
in that set when given any argument values from that set. We can also
call such a function "total." A "partial function" is not necessarily
total, and a "strictly partial" function is definitely not total. An
example of a partial function on the real numbers is division. It is
not defined when the second argument (the denominator) is zero.

We are interested in this section only in total unary functions on
the Booleans. What this means is, first, that for each Boolean value
there must be a corresponding Boolean result, and, second, to be a
*function*, there can be no more than one result. That is, there is
exactly one result for each argument value.
-/

/-
As a mathematical object, such a function is a set of pairs with
exactly one pair having each Boolean value in the first position.
For example, the set, { (tt, ff), (ff, tt) } is such a function.
The set { (tt, tt), (tt, ff) } is not, for two different reasons.
First, it is not a function at all, because it has two pairs with
the same first element, and so is not single-valued. Second, it
doesn't have a pair with ff as a first element and so is not total.
-/

/-
We can graphically depict a total unary function on the Booleans
as a table with two rows and two columns. The first entry in each
row indicates the argument to the function, and the second entry,
the corresponding result. Every such table will have the same first
column, listing each and every possible argument value. Different
functions will then be defined by the corresponding entries in the
second column. Such a table looks like this, where underscores are
placeholders for return values.

 Arg   Ret
+----+----+
| tt | __ |
+----+----|
| ff | __ |
+----+----+
-/

/-
One of the important concepts in discrete mathematics is that of
"counting" the number of objects of some particular kind. Here the
question is, how many unary functions are there on the Booleans?
The answer is equal to the number of different ways in which that
second column can be completed. For example, the co-called identity
function on the Booleans is the function where the return result
is always the same as the argument value. Here's the table.

 Arg   Ret
+----+----+
| tt | tt |
+----+----|
| ff | ff |
+----+----+

Of course such a table is just another way of representing the set
of pairs, { (tt, tt), (ff, ff) }, which is the right way to think of
the identity function as a mathematical object. If we wanted to give
this function a name, we might call it id_bool, with a prefix, id,
suggesting the identity function, and the suffix, _bool, suggesting
that this is the identity function for the bool type.

If you were writing out the algebra in ordinary paper-and-pencil
mathematics,  you'd write this function as id_bool(b) = b, where
b is any Boolean value. You can imagine the corresponding identity
functions for any other type. E.g., for the natural numbers, you
could define id_nat as id_nat(n) = n, where n is any natural number.
-/

/-
We can now put all of these ideas together to write a pure
functional program that implements this function. We will call
this program id_bool. If we apply this resulting program to a
Boolean-valued argument value, b, which would be done by writing
the expression, "id_bool b", the result that is returned will be
just b itself.

As we've now seen, a Boolean function can be represented in at
least three ways:

- as a set of pairs, such as { (tt, tt), (ff, ff) }
- in the form of a table, namely a truth table
- using an equation, such as "id_bool(b) = b"

Simple pure functional programs are generally written in the
equational form. Here's the code for the id_bool function.
-/

def id_bool (b: dm_bool) : dm_bool := b

/-
The "def" keyword introduces a new definition -- a binding of a name,
or "identifier", here id_bool, to a value, here a definition of the
identity function that takes a bool argument and returns that same
value as the result. (Yes, function bodies are values, too.)

You can thus pronounce this code as follows: "we define id_bool to
be a function that takes one argument of type bool, bound to the
identifier b,  and that also returns a value of type bool, namely
that obtained by evaluating the expression, "b", in the context of
the prevailing binding of the identifier, b, to the value of the
bool argument to which the function is applied in any given case.
-/

/-
EXERCISE: Write pure functional programs called false_bool and
true_bool, respectively, each of which takes a bool argument and
that always returns false or true, respectively.
-/

-- TYPE INFERENCE

/-
There's a shorter way to write the same function: we can leave out
the explicit return type (the bool after the colon) because Lean
can infer what it must be by analyzing the type of the expression
that defines the return result. The argument, b, is declared to
be of type bool, so it is clear that type obtained by evaluating
the expression, b, defining the return result, is also of type bool.
Here's another version of the same definition,  with a "prime" mark
to make the name unique, exhibiting the use of type inference.

-/

def id_bool' (b: dm_bool) := b

/-
EXERCISE: Use type inference to write shorter versions of
you true_bool and false_bool programs.
-/

-- FUNCTION APPLICATION EXPRESSIONS

/-
A function application expression is an expression written
as a function name (or more generally as an expression that
reduces to a function value)) followed by an expression that
reduces to a value that is taken to be the argument to the
function.

The simplest form of a function application expression is
just a function name applied to a so-called "literal value"
of the required type. In the function application expression,
"id_bool tt", you see first a function name, the "variable",
id_bool, followed by the literal expression, tt.

Here's an example in which id_bool is
applied to the literal value, tt. By hovering over the
#reduce command, you can see the value to which this
function application expression is reduced.
-/

#reduce id_bool dm_tt

/-
EXERCISES: Write and reduce expressions in which you apply
your true_bool and false_bool programs to each of the two
bool values, thereby exhaustively testing each program for
correctness.
-/

-- EVALUATION OF FUNCTION APPLICATION EXPRESSIONS

/-
Reducing a function application expression is a very simple
process. First you evaluate the function expression, then you
evaluate the argument expression, then you apply the resulting
function to the resulting value. Let's do this in steps.

First, the function expression is given by the identifier,
id_bool. To obtain the actual function, Lean looks up its
definition and finds a function that takes a bool as a value
and that returns that same bool value as a result.

abh: there are a lot of references to "tt" where we clearly
mean "dm_tt"

Second, the identifier, tt, is a literal expression for the
tt value/constructor of the bool type.

Finally, the function is applied to this argument value.
This is done by substituting the argument value for the
argument variable wherever it appears in the body of the
function and by then evaluating the resulting expression.

The body of the function in this case is just "b". So the
value, tt, is substituted for "b". Finally this expression
is evaluated, once again producing the value, tt, and that
is the result of the function application!
-/

/-
EXERCISE: We previously confirmed that the definition of
tt is ambiguous in the current environment. So why was it
okay here to write tt without qualifiers in the expression,
id_bool tt?
-/

/-
EXERCISE: Explain in precise, concise English exactly
how your true_bool program is evaluated when applied to
the argument given by the literal expression, tt.
-/

-- FUNCTION TYPES

-- abh: There's some redundancy between dm_bool.lean and functions.lean

/-
Functions also have types. We can check the type of id_boolean
using the #check  command. Hover your mouse over the #check.
Lean reports that the type of this function is boolean → boolean.
That is how, in type theory, we write the type of a function
that takes an argument of type bool and that returns a result
of that same type.
-/
#check id_bool

/-
EXERCISE: First mentally determine the types of your false_bool
and true_bool functions, and then use #check commands to test
your predictions.
-/

-- TESTING FUNCTION IMPLEMENTATIONS FOR CORRECTNESS

/-
Whenever we write programs that are meant to compute values
of functions for given arguments, the question arises, did
we represent/implement the intended function correctly?

An important observation is that the question presumes
that we have a definition of a function to be implemented
against which the correctness of the implementation can
be evaluated.

Consider our implementation of id_bool? Against what
specification should its correctness be determined?
Here the best answers are that we can evaluate the
correctness of id_bool against either the truth table
or the equivalent set-theoretic definition. The tuples
in the definition of the function, (tt, tt) and (ff, ff),
tell us what result to *expect* for each argument value:
expect tt when given tt as an argument and expect ff
when given ff as an argument.
-/

/-
The process of software *testing* is one in which a
program is evaluated for one or more argument values
and the actual results are compared with the expected
results to identify any discrepancies. A single pair,
(argument value, expected result) is called a *test
case*. The tuples in our set-theoretic definition
of id_bool can thus serve as test cases. Consider
the tuple, (tt, tt). Viewing it as a test case tells
us that we should expect that "id_bool tt = tt."
-/

-- PROPOSITIONS AND PROOFS

/-
A claim like this---an assertion that a certain state
of affairs holds in a given situation, here the assertion
that if we evaluate id_bool with argument tt the result
will be tt---is what in logic we call a proposition.

A proposition is a truth claim. A proposition can thus
be true, or false, or, in some logics the truth of a
given proposition can be indeterminate. In any case,
establishing the truth of a proposition requires what
we call a proof.
-/

/-
The real power of Lean is that in addition to letting
us write programs, it also let us write propositions
and proofs, and it checks that proofs are correct.
Here, the proposition for which we want a proof is
the proposition, id_bool tt = tt. We can write and
prove this proposition in Lean as follows.
-/

theorem id_bool_correct_for_tt: id_bool dm_tt = dm_tt :=
    rfl
/-
We introduce a proposition and its proof with the
keyword, theorem. Technically theorem is just the
same as definition: it's a way to say we're about
to define a value but we intend for that value to
be a proof of some proposition

Following this keyword we give a name to the proof
that we intend to define: id_bool_correct_for_tt.

Next, after the colon comes the proposition itself:
here, id_bool tt = tt.

Next comes a :=. Finally we write the proof: here
the cryptic term, rfl.
-/

/-
PROOF BY SIMPLIFICATION AND THE REFLEXIVE PROPERTY
OF EQUALITY
-/

/-
Proofs come in many forms. As a mathematician,
you learn what forms of proofs work for what
kinds of propositions. Here we have propositions
that two expressions reduce to the same value.
To construct such a proof, you first reduce each
expression to a value. The expression id_bool dm_tt
reduces to tt. The expression dm_tt reduces to dm_tt
(it's already reduced to a value). Then what
you have is the proposition, dm_tt = dm_tt. But
that is true by the reflexive property of
equality: for any value x, no matter what it
is, x = x. So dm_tt = dm_tt. The proposition is
thus proved.
-/

/-
You thus read "rfl" as saying what a mathematician
would pronounce as, "by simplification of expressions
and the reflexive property of equality."

The fact that Lean accepts rfl as a proof (value)
provides a very strong mechanized check on the
correctness of the proof.
-/

/-
EXERCISE: Use Lean to state and prove the proposition
that id_bool dm_ff = dm_ff.
-/

/-
EXERCISE: Write a similar definition, bad_proof_1,
asserting that there is a proof of the proposition,
id_bool dm_tt = dm_ff. Does Lean accept the proof? What
error messages does Lean report?
-/

-- YOUR WORK HERE

/-
The red under rfl indicates a "type mismatch", stating
that rfl was expected to be a proof that something
(here given the arbitrary name m_2) is equal to itself,
but that (in so many words) the things asserted to be
equal are not equal.

The red under bad_proof states, in effect, that the
name, bad_proof, was expected to be bound to a proof
of some proposition, but it is not so bound (due to
the preceding error).

A proposition that is false has no proof. To see that
rfl cannot be a proof of id_bool dm_tt = dm_ff, observe that
id_bool dm_tt reduces to dm_tt, so the proposition reduces
to dm_tt = dm_ff, but dm_tt and dm_ff are not equal, and so rfl
is not a valid proof: it can only be used to prove that
two values are equal to themselves. In the logic of
Lean, different constructors *always* build different
values -- values that are never equal to each other --
so dm_ff cannot be equal to dm_tt. The proposed proof, rfl,
is thus rejected.
-/

/-
EXERCISE: Give a proof, call it one_equals_one, for
the proposition, 1 = 1.
-/

-- YOUR WORK HERE

/-
EXERCISE: Attempt to give a proof, using rfl, of the
proposition that 1 = 2. What happens. Investigate and
briefly explain in plain English the meanings of the
error messages that are reported.
-/

/- UNIVERSAL QUANTIFICATION AND PROOF BY CASE ANALYSIS -/

/-
Testing that a program is correct for one input, which is
what a test case asserts, does not prove that it is correct
for all possible inputs, unless there is only one possible
input, in which case the function is pretty much useless.
The kind of propositions that we really want to prove are
ones that claim= a program is correct *for all* possible
inputs.
-/

theorem id_bool_correct:
  ∀ b: dm_bool, id_bool b = b
    | dm_tt := rfl
    | dm_ff := rfl

/-
We once again give our proof a name that reflects the
proposition that it proves: here, id_bool_correct. We
are claiming that the function is correct for every
possible input. On the second lines is the proposition
itself. The "universal quantifier", ∀, pronounced as
"for all", or "for every", or "for any". It is followed
by a variable, b, and its type, bool. So far we can thus
pronounce the proposition as saying "for any value, b,
of type bool." Then comes a comma followed by the rest
of the proposition: namely, the claim that, for any
such b, id_bool b = b. The "b" in this part of the
proposition is the b "bound" in the quantifier part
of the expression. The whole proposition thus covers
all possible cases, and reads, "for any boolean value,
b, id_bool b is equal to b."

We can't use rfl directly as a proof, because the
form of the proposition is not a simple assertion
of equality of the values of two expressions. It is
instead a "universally quantified" proposition

The remainder is the code instead gives a "proof by
case analysis." We show that for each possible value
of b considered in turn, the claim that id_bool b = b
is true.

Because there are only two values of type bool, there
are two cases: one where b is tt, and one where b is
ff.

Each case starts with a vertical bar, followed by the
case (the value of b) being considered. Then comes a
:=, and followed by a proof for that case.

Consider the first case, in which b is bound to tt.
In this case, making this substitution in the "body"
of the proposition gives us id_bool tt = tt. For
this proposition, we already have a proof! It's rfl.
The same holds true for the second case.

As we've now given a proof for each individual case,
we've given a proof "for all" cases, showing that
the overall quantified proposition is true. Proving
universally quantified propositions about software
correctness is called formal verification, and is a
state of the art approach to producing ultra-high
quality code. Such a high standard of correctness
is not always necessary or practical, but when
lives or nations depend on correctness of code, it
is a "gold standard" approach to software quality.
-/

/-
EXERCISE: In a similar style, state and prove the
proposition that every natural number, n, is equal
to itself. Call your proof nat_refl. You can get
the ∀ character (the universal quantifier, for all)
in Lean by typing \forall followed by a space.

NOTE: You don't know how to write such a proof
yet, so just write "sorry" instead. This tells
Lean to accept the proposition as being true
even though you haven't yet given a proof (and
even if it's actually not even true). You are
saying, "accept the proposition without proof",
or "accept it as an "axiom." An axiom is any
proposition that is accepted without a proof.

This is really just an exercise that asks you
to write a proposition in Lean using a ∀.
-/

-- theorem nat_refl: ∀ n: nat, n = n := sorry

/-
EXERCISE: State a proposition and give a proof
in Lean for the proposition stating that for
*every* possible argument, b, to false_bool,
false_bool b = false. Similarly prove that
your implementation of true_bool is correct
with respect to our understanding of how it
should behave.
-/

/-
Exercise: How many test cases do we need to "prove" that
the function works correctly for all possible inputs of
type boolean. (Hint: how many such inputs are there?)
Write any additional test cases need to prove that our
definition of the identity function works as we expect
it to.
-/

/-
Proof by case analysis often works well when you want to
prove that something is true for every element in a finite
set of elements. It isn't an appropriate proof strategy when
the set of values to be considered is infinite, as it would
be impossible to test every individual case. For example,
you can't prove that a functional program that takes a
natural number as an argument is correct by giving a proof
for each natural number in turn because there is an infinity
of such values. Another proof strategy would be need. It
goes by the name of "proof by induction." More on that later!
-/

-- THE MAJOR FUNCTIONS OF BOOLEAN ALGEBRA

/-
So far we have implemented three of the four unary functions
of Boolean algebra. The remaining one is usually called "not"
or "negation." We will give it the name, negb. Given one of
the two Boolean values as an argument, negb returns the other.

The set-theoretic definition is negb = { (tt, ff), (ff, tt) }.
The truth table depicts this definition graphically.

abh: the table below has switched from tt/ff to T/F

 Arg Ret
+---+---+
| T | F |
+---+---|
| F | T |
+---+---+
-/

/-
We don't yet have the tools needed to implement this function.
The tool we need is called pattern matching. It's just a form
of case analysis! Here's the code.
-/

def negb (b: dm_bool): dm_bool :=
  match b with
    | dm_tt := dm_ff
    | dm_ff := dm_tt
  end

/-
We define negb to be a function that takes a Boolean value and
returns a Boolean value. That is, the type of this function is,
like the others we've defined so far, bool → bool. The thing that
is different about this function is that we have to inspect the
argument value to determining what result to return. We do that by
case analysis! What the body of this function says is "match the
value of b with each of its possible cases. THe first case is tt,
and in this case (after the :=), the return value is given by
the expression ff. Similarly, for the case, ff, the result is tt.
There are no more cases, and so this function has given a result
value "for every" possible value of b. This is thus a definition
of a total function by case analysis!

(You get the → symbol in Lean by typing \to. EXERCISE: Try it.)
-/

/-
EXERCISE: Write a second implementation of id_bool, call it
id_bool', using case analysis. Prove by case analysis that it
is correct with respect to its expected behavior.
-/

-- FUNCTION TYPES

/-
In Lean, every function is required to be total. That is, a
function must define how to construct a return value of the
specified type "for every" value of its argument type. The
four unary functions we've seen so far all do this. For every
value of type dm_bool, each of them returns a value of type dm_bool.
We've seen that you can write this type as dm_bool → dm_bool, but
another way to write it is, ∀ b: dm_bool, dm_bool. This just says,
for every value of type bool you can give me, I promise to
give you back a value of type dm_bool. Hover over the #check
that follows. You will see that we have just found two ways
to write the same function type!
-/

#check ∀ b: dm_bool, dm_bool

/-
EXERCISE: Use a universal quantifier to write and check the
type of functions from natural numbers to dm_bools. An example
of such a function would be one that took any natural number
and returned dm_tt if and only it it was even, and dm_ff otherwise.
-/

#check ∀ n: nat, dm_bool

/-
THE BINARY OPERATORS OF BOOLEAN ALGEBRA
-/

/-
Binary operations of an algebra are functions that take two
arguments of a given type (such as dm_bool) and that return a
value of that same type. The conjunction, aka and, operation
in Boolean algebra is an example. It takes two Boolean values
as arguments and returns a Boolean result. If both arguments
are true, its result is true, otherwise it is false. This
behavior is reflected in  the following set-theoretic and
truth table specifications. We will call the function xandb.

xandb = { ((T, T), T), ((T, F), F), ((F, T), F), ((F, F), F) }

Note that we again give the function as a set of argument/result
pairs, but now each argument is itself a pair of values.

A truth table view:

    xandb
+---+---+---+
| T | T | T |
+---+---|---+
| T | F | F |
+---+---+---+
| F | T | F |
+---+---+---+
| F | F | F |
+---+---+---+

abh: An alternative truth table view
(lower-case inputs, upper-case result):

        arg 1
      +---+---+
      | t | f |
a +---+---|---+
r | t | T | F |
g +---+---+---+
  | f | F | F |
2 +---+---+---+

-/

/-
We already have all the tools but for one to implement this
function. The one tool we need is pattern matching (i.e., case
analysis) for *pairs* of argument values. This following code
shows how to do this. Instead of matching on one value, b, we
now match on the comma-separated pair of values, b1, b2; and
each case now corresponds to a possible pair of argument values.
There are four possible pairs of Boolean values, and so there
are four cases to consider. Note how the visual organization
of the code reflects the truth table contents (and thus the
equivalent set-theoretic definition nearly directly.)
-/

def and_boolean' (b1 b2: dm_bool): dm_bool :=
    match b1, b2 with
        | dm_tt, dm_tt := dm_tt
        | dm_tt, dm_ff := dm_ff
        | dm_ff, dm_tt := dm_ff
        | dm_ff, dm_ff := dm_ff
    end

/-
It's often the case (ha ha) that in a case analysis, one
case stands out and several others or all the rest can be
considered at once. To define "xandb" all we really have to
say is "if b1 and b2 are both true, the result is true, and
in any other case the result is false." In Lean and in many
other functional programming language, we can write cases
analysis using "wildcards" (here underscores) that match
any values not considered in previous cases.
-/

def and_boolean (b1 b2: dm_bool): dm_bool :=
    match b1, b2 with
        | dm_tt, dm_tt := dm_tt
        | _, _ := dm_ff
    end

/-
EXERCISES:

1. Using case analysis, write definitions of the following binary
functions on booleans in the form of (a) sets, using display notation,
(b) truth tables, (3) functional programs. When writing functional
programs, use wildcards where possible to shorten your definitions.

* or (call it dm_orb)-- false if both arguments are false, otherwise true
* xor (dm_orb) -- true if either not both both arguments are true
* xnand (dm_nandb) -- the negation of the conjunction of the arguments
* ximplies (dm_implb) -- false if b1 is true and b2 is false otherwise true
* xnor (dm_norb)-- the negation of the "disjunction" of its arguments

2. By the method of case analysis, prove that your "or" and "xor" programs are
correct with respect to your truth table definitions, i.e., that they produce the
outputs specified by the truth tables for the given inputs.

3. How many binary functions on booleans are there? Justify your answer. Hint:
think about the truth tables. The set of possible arguments is always the set of
pairs of booleans. How many different ways can these arguments be associated with
boolean results?

4. Write a second definition of nandb, call it nandb', that instead of
using pattern matching applies a combination of andb and negb functions.
Surround function application expressions with parentheses to specify
groupings when they might otherwise be misinterpreted, e.g., vvv (www x y)
in the case where the argument to vvv is meant to be the result of
evaluating www x y.
-/

/-
FORMAL AND INFORMAL PROOFS.
-/

/-
Our formal proofs are very precise and their correctness is assured by Lean's
automated proof checking mechanism. By contrast, most working mathematicians
write informal proofs. These are still highly precise, but they are written in
structured English (or another natural language) rather than in what amounts
to code. The major benefit of informal proofs is that they are easier for most
people to understand. The downside is that mistakes in proofs can, and often
do, go undetected. One benefit of formal proofs is that they are checked for
correctness by a computer, and, when verified can be accepted as correct with
very high levels of confidence. Another benefit of learning how to write such
proofs is that the relationships between forms of propositions (e.g., equality
claims or universally quantified claims) and the forms of the corresponding
proofs becomes very clear. Learning how to write both informal and formal
propositions and proofs is an important goal of this class.
-/

/-
As an example, let's recast our formal proof of the correctness of our
program for id_bool as an informal proof. Here's how it might be written.

"The goal is to show that for every value, b, id_bool = b. We do this by
case analysis. There are two cases: b = dm_tt, and b = dm_ff, respectively. We
first consider the case where b = dm_tt. In this case, the proposition to be
proved is that id_bool dm_tt = dm_tt. We prove this by simplification of the left
hand side to tt by applying the definition of id_bool. What's left to prove
is that dm_tt = dm_tt, and this is done trivially by appealing to the reflexive
property of equality. The proof of the second case is by the same strategy
of simplification and reflexivity of equality."
-/

/-
EXERCISE: Write both a formal and a corresponding informal proof of the
correctness of your andb function.
-/

/-
BOOLEAN ALGEBRA AS AN ALGEBRA
-/

/-
We have now gotten to the point where we can make sense of the term, boolean algebra.
Boolean algebra is an algebra, which is to say it is a particular set of values and
a particular collection of operations closed on that set. We have represented the set
of values as a type, namely our dm_bool type. We have represented the operations as
pure functional programs taking and returning values of this type.

Moreover, by defining this set and its operations in a namespace, we've grouped
the values and operations on them in a meaningful way.

+ --------------+
|  csdm.dm_bool |  DATA
+---------------+
|  csdm.dm_negb |
|  csdm.dm_andb | OPERATIONS
|  csdm.dm_orb  |
|     ...       |
+---------------+

Such a structure, comprising a data type and a collection of operations on it is
also known, in the computer science field, as an "abstract data type (ADT)". When
computer scientists talk about "types", sometimes they mean inductively defined
types, such as the bool type (separate from operations), and sometimes (actually
more often) then mean abstract data types. Abstract data types, i.e., algebras!,
are fundamental building blocks of software. It took a while for us to build up
our bool ADT, but now that we're done, you can step back and now view our data
type definition and our definitions of a collection of associated operations as
a coherent whole, an abstract data type that implements the algebra of Boolean
truth values.
-/

-- USING AN ABSTRCT DATA TYPE

/-
The last question we address in this chapter is how to use such an abstract
data type implementation. To this end, please open and shift your attention
to the file csdm_bool_test.lean. It will show you how to import and use type
and function definitions in another file: this file in this case.
-/

/-
SUMMARY

- boolean algebra
  * inductive definition of the type of booleans
  * functions on booleans
  ** set theoretic, truth table, and pure functional representations
  ** unary functions on booleans
  ** binary functions on booleans
- types and values
- inductive definitions
- tuple values and tuple types
- relations and functions (set theoretic)
- functional programs: their types and values
- propositions
  * about equality of terms
  * universally quantified propositions
- an application: propositions and proofs of program correctness
- proof strategies:
  * by simplification and reflexivity of equality
  * by exhaustive case analysis
- formal and informal proofs
- algebras
-/

end edu.virginia.cs.dm


-- Logical Equivalance

variables p q : Prop

theorem and_swap : p ∧ q ↔ q ∧ p :=
iff.intro
  (assume h : p ∧ q,
    show q ∧ p, from and.intro h.right h.left)
  (assume h : q ∧ p,
    show p ∧ q, from ⟨ h.right, h.left ⟩)

#check and_swap p q

variable h : p ∧ q
example : q ∧ p := iff.mp (and_swap p q) h

theorem and_swap' : p ∧ q ↔ q ∧ p :=
⟨λ h : p ∧ q, ⟨ h.right, h.left ⟩ , λ h : q ∧ p, ⟨ h.right, h.left ⟩ ⟩

example (h : p ∧ q) : q ∧ p := (and_swap' p q).mp h