#   This file is part of UnitSystems.jl
#   It is licensed under the MIT license
#   UnitSystems Copyright (C) 2020 Michael Reed
#       _           _                         _
#      | |         | |                       | |
#   ___| |__   __ _| | ___ __ __ ___   ____ _| | __ _
#  / __| '_ \ / _` | |/ / '__/ _` \ \ / / _` | |/ _` |
# | (__| | | | (_| |   <| | | (_| |\ V / (_| | | (_| |
#  \___|_| |_|\__,_|_|\_\_|  \__,_| \_/ \__,_|_|\__,_|
#
#   https://github.com/chakravala
#   https://crucialflow.com

@doc """
    μₑᵤ, μₚᵤ, μₚₑ, αinv, αG, ΩΛ

Physical measured dimensionless `Coupling` values with uncertainty are the electron to proton mass ratio `μₑᵤ`, proton to atomic mass ratio `μₚᵤ`, proton to electron mass ratio `μₚₑ`, inverted fine structure constant `αinv`, and the gravitaional coupling constant `αG`.

```Julia
julia> μₑᵤ # electronunit(Universe)
$μₑᵤ

julia> μₚᵤ # protonunit(Universe)
$μₚᵤ

julia> μₚₑ # protonelectron(Universe)
$μₚₑ

julia> αinv # 1/finestructure(Universe)
$αinv

julia> αG # coupling(Universe)
$αG

julia> ΩΛ # darkenergydensity(Universe)
$ΩΛ
```
""" Universe, μₑᵤ, μₚᵤ, μₚₑ, αinv, αG, meu, mpu, mpe, ainv, aG, ΩΛ, electronunit, protonunit, protonelectron, finestructure, coupling, darkenergydensity

@doc """
$(unitext(:turn,"2π/angle(U)"))

Complete rotation `angle` of revolution from a full circle.
```Julia
julia> turn(MetricEngineering)
$(turn(MetricEngineering))
```
""" turn

@doc """
$(unitext(:spat,"4π/solidangle(U)"))

Complete spherical `solidangle` around point from a full sphere.
```Julia
julia> spat(MetricEngineering)
$(spat(MetricEngineering))
```
""" spat

@doc """
$(unitext(:luminousefficacy,"Kcd*power(U)\nluminousefficacy(U::UnitSystem{𝟏}) = 𝟏"))

Luminous efficacy of monochromatic radiation `Kcd` of frequency 540 THz (lm⋅W⁻¹).
```Julia
julia> luminousefficacy(Metric) # lm⋅W⁻¹
$(luminousefficacy(Metric))

julia> luminousefficacy(CODATA) # lm⋅W⁻¹
$(luminousefficacy(CODATA))

julia> luminousefficacy(Conventional) # lm⋅W⁻¹
$(luminousefficacy(Conventional))

julia> luminousefficacy(International) # lm⋅W⁻¹
$(luminousefficacy(International))

julia> luminousefficacy(British) # lm⋅s³⋅slug⋅ft⁻²
$(luminousefficacy(British))
```
""" luminousefficacy, Kcd

@doc """
$(unitext(:molarmass,"avogadro(U)*electronmass(U)/electronunit(U)"))

Molar mass constant `Mᵤ` is the ratio of the `molarmass` and `relativemass` of a chemical.
```Julia
julia> molarmass(CGS) # g⋅mol⁻¹
$(molarmass(CGS))

julia> molarmass(Metric) # kg⋅mol⁻¹
$(molarmass(Metric))

julia> molarmass(SI2019) # kg⋅mol⁻¹
$(molarmass(SI2019))

julia> molarmass(International) # kg⋅mol⁻¹
$(molarmass(International))
```
""" molarmass, Mᵤ, Mu

@doc """
$(unitext(:avogadro, "molargas(x)/boltzmann(x) # Mᵤ/atomicmass(x)"))

Avogadro `NA` is `molarmass(x)/atomicmass(x)` number of atoms in a 12 g sample of C₁₂.
```Julia
julia> avogadro(SI2019) # mol⁻¹
$(avogadro(SI2019))

julia> avogadro(Metric) # mol⁻¹
$(avogadro(Metric))

julia> avogadro(CODATA) # mol⁻¹
$(avogadro(CODATA))

julia> avogadro(Conventional) # mol⁻¹
$(avogadro(Conventional))

julia> avogadro(English) # lb-mol⁻¹
$(avogadro(English))

julia> avogadro(British) # slug-mol⁻¹
$(avogadro(British))
```
""" avogadro, NA

@doc """
$(unitext(:planckreduced,"planck(x)/turn(x)"))

Reduced Planck constant `ħ` is a Planck per radian (J⋅s⋅rad⁻¹ or ft⋅lb⋅s⋅rad⁻¹).

```Julia
julia> planckreduced(SI2019) # J⋅s⋅rad⁻¹
$(planckreduced(SI2019))

julia> planckreduced(SI2019)*lightspeed(SI2019) # J⋅m⋅rad⁻¹
$(planckreduced(SI2019)*lightspeed(SI2019))

julia> planckreduced(CODATA) # J⋅s⋅rad⁻¹
$(planckreduced(CODATA))

julia> planckreduced(Conventional) # J⋅s⋅rad⁻¹
$(planckreduced(Conventional))

julia> planckreduced(SI2019)/elementarycharge(SI2019) # eV⋅s⋅rad⁻¹
$(planckreduced(SI2019)/elementarycharge(SI2019))

julia> planckreduced(SI2019)*lightspeed(SI2019)/elementarycharge(SI2019) # eV⋅m⋅rad⁻¹
$(planckreduced(SI2019)*lightspeed(SI2019)/elementarycharge(SI2019))

julia> planckreduced(British) # ft⋅lb⋅s⋅rad⁻¹
$(planckreduced(British))
```
""" planckreduced, ħ

@doc """
$(unitext(:planck,"turn(x)*planckreduced(x)"))

Planck constant `𝘩` is energy per electromagnetic frequency (J⋅s or ft⋅lb⋅s).

```Julia
julia> planck(SI2019) # J⋅s
$(planck(SI2019))

julia> planck(SI2019)*lightspeed(SI2019) # J⋅m
$(planck(SI2019)*lightspeed(SI2019))

julia> planck(CODATA) # J⋅s
$(planck(CODATA))

julia> planck(Conventional) # J⋅s
$(planck(Conventional))

julia> planck(SI2019)/elementarycharge(SI2019) # eV⋅s
$(planck(SI2019)/elementarycharge(SI2019))

julia> planck(SI2019)*lightspeed(SI2019)/elementarycharge(SI2019) # eV⋅m
$(planck(SI2019)*lightspeed(SI2019)/elementarycharge(SI2019))

julia> planck(British) # ft⋅lb⋅s
$(planck(British))
```
""" planck, 𝘩, hh

@doc """
$(unitext(:boltzmann,"molargas(x)/avogadro(x)"))

Boltzmann constant `kB` is the entropy amount of a unit number microstate permutation.

```Julia
julia> boltzmann(SI2019) # J⋅K⁻¹
$(boltzmann(SI2019))

julia> boltzmann(Metric) # J⋅K⁻¹
$(boltzmann(Metric))

julia> boltzmann(SI2019)/elementarycharge(SI2019) # eV⋅K⁻¹
$(boltzmann(SI2019)/elementarycharge(SI2019))

julia> boltzmann(SI2019)/planck(SI2019) # Hz⋅K⁻¹
$(boltzmann(SI2019)/planck(SI2019))

julia> boltzmann(CGS) # erg⋅K⁻¹
$(boltzmann(CGS))

julia> boltzmann(SI2019)/calorie(SI2019) # calᵢₜ⋅K⁻¹
$(boltzmann(SI2019)/calorie(SI2019))

julia> boltzmann(SI2019)*°R/calorie(SI2019) # calᵢₜ⋅°R⁻¹
$(boltzmann(SI2019)*°R/calorie(SI2019))

julia> boltzmann(Brtish) # ft⋅lb⋅°R⁻¹
$(boltzmann(British))

julia> boltzmann(SI2019)/planck(SI2019)/lightspeed(SI2019) # m⁻¹⋅K⁻¹
$(boltzmann(SI2019)/planck(SI2019)/lightspeed(SI2019))

julia> avogadro(SI2019)*boltzmann(SI2019)/calorie(SI2019) # calᵢₜ⋅mol⁻¹⋅K⁻¹
$(avogadro(SI2019)*boltzmann(SI2019)/calorie(SI2019))

julia> dB(boltzmann(SI2019)) # dB(W⋅K⁻¹⋅Hz⁻¹)
$(dB(boltzmann(SI2019)))
```
""" boltzmann, kB

@doc """
$(unitext(:lightspeed,"𝟏/sqrt(vacuumpermeability(U)*vacuumpermittivity(U))/lorentz(U)"))

Speed of light in a vacuum `𝘤` for massless particles (m⋅s⁻¹ or ft⋅s⁻¹).

```Julia
julia> lightspeed(Metric) # m⋅s⁻¹
$(lightspeed(Metric))

julia> lightspeed(English) # ft⋅s⁻¹
$(lightspeed(English))

julia> lightspeed(IAU) # au⋅D⁻¹
$(lightspeed(IAU))
```
""" lightspeed, 𝘤, cc

@doc """
$(unitext(:vacuumpermeability,"𝟏/vacuumpermittivity(U)/(lightspeed(U)*lorentz(U))^2"))

Magnetic permeability in a classical vacuum defined as `μ₀` in SI units (H⋅m⁻¹, kg⋅m²⋅C⁻²).

```Julia
julia> vacuumpermeability(Metric) # H⋅m⁻¹
$(vacuumpermeability(Metric))

julia> vacuumpermeability(Conventional) # H⋅m⁻¹
$(vacuumpermeability(Conventional))

julia> vacuumpermeability(CODATA) # H⋅m⁻¹
$(vacuumpermeability(CODATA))

julia> vacuumpermeability(SI2019) # H⋅m⁻¹
$(vacuumpermeability(SI2019))

julia> vacuumpermeability(International) # H⋅m⁻¹
$(vacuumpermeability(International))

julia> vacuumpermeability(EMU) # abH⋅cm⁻¹
$(vacuumpermeability(EMU))

julia> vacuumpermeability(ESU) # statH⋅cm⁻¹
$(vacuumpermeability(ESU))
```
""" vacuumpermeability, μ₀, m0

@doc """
$(unitext(:lorentz,"spat(U)*biotsavart(U)/vacuumpermeability(U)/rationalization(U)"))

Electromagnetic proportionality constant `αL` for the Lorentz's law force (dimensionless).

```Julia
julia> lorentz(Metric)
$(lorentz(Metric))

julia> lorentz(LorentzHeaviside)
$(lorentz(LorentzHeaviside))

julia> lorentz(Gauss)
$(lorentz(Gauss))
```
""" lorentz, αL, aL, C

@doc """
$(unitext(:rationalization,"spat(U)*biotsavart(U)/vacuumpermeability(U)/lorentz(U)"))

Constant of magnetization and polarization density or `spat(U)*coulomb(U)*permittivity(U)`.

```Julia
julia> rationalization(Metric)
$(rationalization(Metric))

julia> rationalization(Gauss)
$(rationalization(Gauss))
```
""" rationalization, Λ

@doc """
$(unitext(:electronmass,"protonmass(U)/protonelectron(U) # αinv^2*R∞*2𝘩/𝘤"))

Electron rest mass `mₑ` of subatomic particle with `-𝘦` elementary charge  (kg or slugs).
```Julia
julia> electronmass(Metric) # kg
$(electronmass(Metric))

julia> electronmass(CODATA) # kg
$(electronmass(CODATA))

julia> electronmass(Conventional) # kg
$(electronmass(Conventional))

julia> electronmass(International) # kg
$(electronmass(International))

julia> electronmass(Metric)/atomicmass(Metric) # Da
$μₑᵤ

julia> electronmass(Metric)*lightspeed(Metric)^2 # J
$(electronmass(Metric)*lightspeed(Metric)^2)

julia> electronmass(SI2019)*lightspeed(SI2019)^2/elementarycharge(SI2019) # eV⋅𝘤⁻²
$(electronmass(SI2019)*lightspeed(SI2019)^2/elementarycharge(SI2019))

julia> electronmass(English) # lb
$(electronmass(English))
```
""" electronmass, mₑ, me

@doc """
$(unitext(:atomicmass,"Mᵤ/avogadro(U) # $(molarmass(SI2019)) ≈ 0.001-3.5e-13"))

Atomic mass unit `mᵤ` of 1/12 of the C₁₂ carbon-12 atom's mass  (kg or slugs).
```Julia
julia> atomicmass(Metric) # kg
$(atomicmass(Metric))

julia> atomicmass(Metric)/electronmass(Metric) # mₑ
$(atomicmass(Metric)/electronmass(Metric))

julia> atomicmass(Metric)*lightspeed(Metric)^2 # J
$(atomicmass(Metric)*lightspeed(Metric)^2)

julia> atomicmass(SI2019)*lightspeed(SI2019)^2/elementarycharge(SI2019) # eV⋅𝘤⁻²
$(atomicmass(SI2019)*lightspeed(SI2019)^2/elementarycharge(SI2019))

julia> atomicmass(British) # lb
$(atomicmass(British))
```
""" atomicmass, mᵤ, mu

@doc """
$(unitext(:protonmass,"protonunit(U)*atomicmass(U)"))

Proton mass `mₚ` of subatomic particle with `+𝘦` elementary charge  (kg or mass).
```Julia
julia> protonmass(Metric) # kg
$(protonmass(Metric))

julia> protonmass(SI2019)*lightspeed(SI2019)^2/elementarycharge(SI2019) # eV⋅𝘤⁻²
$(protonmass(SI2019)*lightspeed(SI2019)^2/elementarycharge(SI2019))

julia> protonmass(Metric)/atomicmass(Metric) # mᵤ
$(protonmass(Metric)/atomicmass(Metric))

julia> protonmass(Metric)/electronmass(Metric) # mₑ
$(protonmass(Metric)/electronmass(Metric))
```
""" protonmass, mₚ, mp

@doc """
$(unitext(:planckmass,"electronmass(U)/sqrt(coupling(U))"))

Planck mass factor `mP` from the gravitational coupling constant `αG` (kg or slugs).
```Julia
juila> planckmass(Metric)*lightspeed(Metric)^2/elementarycharge(Metric) # eV⋅𝘤⁻²
$(planckmass(Metric)*lightspeed(Metric)^2/elementarycharge(Metric))

juila> planckmass(Metric) # kg
$(planckmass(Metric))

juila> planckmass(Metric)/atomicmass(Metric) # mᵤ
$(planckmass(Metric)/atomicmass(Metric))

juila> planckmass(Metric)*lightspeed(Metric)^2/elementarycharge(Metric)/sqrt(𝟐^2*τ) # eV⋅𝘤⁻²
$(planckmass(Metric)*lightspeed(Metric)^2/elementarycharge(Metric)/sqrt(𝟐^2*τ))

juila> planckmass(Metric)/sqrt(𝟐^2*τ) # kg
$(planckmass(Metric)/sqrt(𝟐^2*τ))
```
""" planckmass, mP

@doc """
$(unitext(:gravitation,"lightspeed(U)*planckreduced(U)/planckmass(U)^2"))

Universal gravitational constant `G` of Newton's law (m³⋅kg⁻¹⋅s⁻² or ft³⋅slug⁻¹⋅s⁻²).
```Julia
juila> gravitation(Metric) # m³⋅kg⁻¹⋅s⁻²
$(gravitation(Metric))

julia> gravitation(English) # ft³⋅lbm⁻¹⋅s⁻²
$(gravitation(English))

julia> gravitation(PlanckGauss)
$(gravitation(PlanckGauss))
```
""" gravitation, G, GG

@doc """
$(unitext(:einstein,"𝟐*spat(U)*gravitation(U)/lightspeed(U)^4"))

Einstein's gravitational constant from the Einstein field equations (s⋅²⋅m⁻¹⋅kg⁻¹).
```Julia
julia> einstein(Metric) # s²⋅m⁻¹⋅kg⁻¹
$(einstein(Metric))

julia> einstein(IAU) # day²⋅au⁻¹⋅M☉⁻¹
$(einstein(IAU))
```
""" einstein, κ

#=@doc """
$(unitext(:einstein2,"𝟐*spat(U)*gravitation(U)/lightspeed(U)^2"))

Einstein's gravitational constant from the Einstein field equations (m⋅kg⁻¹).
```Julia
julia> einstein2(Metric) # m⋅kg⁻¹
$(einstein2(Metric))

julia> einstein2(IAU) # au⋅M☉⁻¹
$(einstein2(IAU))
```
""" einstein2=#

@doc """
$(unitext(:gravity,"# mass*acceleration/force"))

Gravitational force reference used in technical engineering units (kg⋅m⋅N⁻¹⋅s⁻²).
```Julia
julia> gravity(Metric)
$(gravity(Metric))

julia> gravity(MetricEngineering) # m⋅kg⋅N⁻¹⋅s⁻²
$(gravity(MetricEngineering))

julia> gravity(English) # ft⋅lbm⋅lbf⁻¹⋅s⁻²
$(gravity(English))
```
""" gravity

@doc """
$(unitext(:molargas,"boltzmann(x)*avogadro(x)"))

Universal gas constant `Rᵤ` is factored into specific `gasconstant(x)*molarmass(x)` values.

```Julia
julia> molargas(SI2019) # J⋅K⁻¹⋅mol⁻¹
$(molargas(SI2019))

julia> molargas(English)/𝟐^4/𝟑^2 # psi⋅ft³⋅°R⁻¹⋅lb-mol⁻¹
$(molargas(English)/𝟐^4/𝟑^2)

julia> molargas(English)/atmosphere(English) # atm⋅ft³⋅R⁻¹⋅lb-mol⁻¹
$(molargas(English)/atmosphere(English))

julia> molargas(English)/thermalunit(English) # BTU⋅°R⁻¹⋅lb-mol⁻¹
$(molargas(English)/thermalunit(English))

julia> molargas(Metric)/atmosphere(Metric) # atm⋅m³⋅K⁻¹⋅mol⁻¹
$(molargas(Metric)/atmosphere(Metric))

julia> molargas(Metric)/torr(Metric) # m³⋅torr⋅K⁻¹⋅mol⁻¹
$(molargas(Metric)/torr(Metric))

julia> molargas(English)/torr(English) # ft³⋅torr⋅°R⁻¹⋅lb-mol⁻¹
$(molargas(English)/torr(English))

julia> molargas(CGS) # erg⋅K⁻¹⋅mol⁻¹
$(molargas(CGS))

julia> molargas(English) # ft⋅lb⋅°R⁻¹⋅lb-mol⁻¹
$(molargas(English))

julia> molargas(British) # ft⋅lb⋅°R⁻¹⋅slug-mol⁻¹
$(molargas(British))

julia> molargas(SI1976) # J⋅K⁻¹⋅mol⁻¹ (US1976 Standard Atmosphere)
$(molargas(SI1976))
```
""" molargas, Rᵤ, Ru

@doc """
$(unitext(:stefan,"π^4/𝟐*spat(U)*boltzmann(U)^4/(15planck(U)^3*lightspeed(U)^2)"))

Stefan-Boltzmann proportionality `σ` of black body radiation (W⋅m⁻²⋅K⁻⁴ or ?⋅ft⁻²⋅°R⁻⁴).

```Julia
julia> stefan(SI2019) # W⋅m⁻²⋅K⁻⁴
$(stefan(SI2019))

julia> stefan(Metric) # W⋅m⁻²⋅K⁻⁴
$(stefan(Metric))

julia> stefan(Conventional) # W⋅m⁻²⋅K⁻⁴
$(stefan(Conventional))

julia> stefan(CODATA) # W⋅m⁻²⋅K⁻⁴
$(stefan(CODATA))

julia> stefan(Metric)*day(Metric)/(calorie(Metric)*100^2) # cal⋅cm⁻²⋅day⁻¹⋅K⁻⁴
$(stefan(Metric)*day(Metric)/calorie(Metric))

julia> stefan(English) # lb⋅s⁻¹⋅ft⁻³⋅°R⁻⁴
$(stefan(English))
```
""" stefan, σ, SB

@doc """
$(unitext(:radiationdensity,"𝟐^2*stefan(U)/lightspeed(U)"))

Raditation density constant of black body radiation (J⋅m⁻³⋅K⁻⁴ or lb⋅ft⁻²⋅°R⁻⁴).

```Julia
julia> radiationdensity(Metric) # J⋅m⁻³⋅K⁻⁴
$(radiationdensity(Metric))

julia> radiationdensity(SI2019) # J⋅m⁻³⋅K⁻⁴
$(radiationdensity(SI2019))

julia> radiationdensity(Conventional) # J⋅m⁻³⋅K⁻⁴
$(radiationdensity(Conventional))

julia> radiationdensity(CODATA) # J⋅m⁻³⋅K⁻⁴
$(radiationdensity(CODATA))

julia> radiationdensity(International) # J⋅m⁻³⋅K⁻⁴
$(radiationdensity(International))
```
""" radiationdensity

@doc """
$(unitext(:vacuumpermittivity,"𝟏/vacuumpermeability(U)/(lightspeed(U)*lorentz(U))^2"))

Dielectric permittivity constant `ε₀` of a classical vacuum (C²⋅N⁻¹⋅m⁻²).

```Julia
julia> vacuumpermittivity(Metric) # F⋅m⁻¹
$(vacuumpermittivity(Metric))

julia> vacuumpermittivity(Conventional) # F⋅m⁻¹
$(vacuumpermittivity(Conventional))

julia> vacuumpermittivity(CODATA) # F⋅m⁻¹
$(vacuumpermittivity(CODATA))

julia> vacuumpermittivity(SI2019) # F⋅m⁻¹
$(vacuumpermittivity(SI2019))

julia> vacuumpermittivity(International) # F⋅m⁻¹
$(vacuumpermittivity(International))

julia> vacuumpermittivity(EMU) # abF⋅cm⁻¹
$(vacuumpermittivity(EMU))

julia> vacuumpermittivity(ESU) # statF⋅cm⁻¹
$(vacuumpermittivity(ESU))

julia> vacuumpermittivity(SI2019)/elementarycharge(SI2019) # 𝘦²⋅eV⁻¹⋅m⁻¹
$(vacuumpermittivity(SI2019)/elementarycharge(SI2019))
```
""" vacuumpermittivity, ε₀, ϵ₀, e0

@doc """
$(unitext(:electrostatic,"rationalization(U)/spat(U)/vacuumpermittivity(U)"))

Electrostatic proportionality constant `kₑ` for the Coulomb's law force (N⋅m²⋅C⁻²).

```Julia
julia> electrostatic(Metric) # N⋅m²⋅C⁻²
$(electrostatic(Metric))

julia> electrostatic(CODATA) # N·m²⋅C⁻²
$(electrostatic(CODATA))

julia> electrostatic(SI2019) # N·m²⋅C⁻²
$(electrostatic(SI2019))

julia> electrostatic(Conventional) # N·m²⋅C⁻²
$(electrostatic(Conventional))

julia> electrostatic(International) # N·m²⋅C⁻²
$(electrostatic(International))

julia> electrostatic(EMU) # dyn⋅cm²⋅abC⁻²
$(electrostatic(EMU))

julia> electrostatic(ESU) # dyn⋅cm²⋅statC⁻²
$(electrostatic(ESU))

julia> electrostatic(HLU) # dyn⋅cm²⋅hlC⁻²
$(electrostatic(HLU))
```
""" electrostatic, kₑ, ke

@doc """
$(unitext(:biotsavart,"vacuumpermeability(U)*lorentz(U)*rationalization(U)/spat(U)"))

Magnetostatic proportionality constant `αB` for the Biot-Savart's law (H/m).

```Julia
julia> biotsavart(Metric) # H⋅m⁻¹
$(biotsavart(Metric))

julia> biotsavart(CODATA) # H⋅m⁻¹
$(biotsavart(CODATA))

julia> biotsavart(SI2019) # H⋅m⁻¹
$(biotsavart(SI2019))

julia> biotsavart(Conventional) # H⋅m⁻¹
$(biotsavart(Conventional))

julia> biotsavart(International) # H⋅m⁻¹
$(biotsavart(International))

julia> biotsavart(InternationalMean) # H⋅m⁻¹
$(biotsavart(InternationalMean))

julia> biotsavart(EMU) # abH⋅cm⁻¹
$(biotsavart(EMU))

julia> biotsavart(ESU) # statH⋅cm⁻¹
$(biotsavart(ESU))

julia> biotsavart(Gauss) # abH⋅cm⁻¹
$(biotsavart(Gauss))

julia> biotsavart(HLU) # hlH⋅cm⁻¹
$(biotsavart(HLU))
```
""" biotsavart, αB, aB

@doc """
$(unitext(:magnetostatic,"lorentz(U)*biotsavart(U) # electrostatic(U)/lightspeed(U)^2"))

Magnetic proportionality constant `kₘ` for the Ampere's law force (N·s²⋅C⁻²).

```Julia
julia> magnetostatic(Metric) # H⋅m⁻¹
$(magnetostatic(Metric))

julia> magnetostatic(CODATA) # H⋅m⁻¹
$(magnetostatic(CODATA))

julia> magnetostatic(SI2019) # H⋅m⁻¹
$(magnetostatic(SI2019))

julia> magnetostatic(Conventional) # H⋅m⁻¹
$(magnetostatic(Conventional))

julia> magnetostatic(International) # H⋅m⁻¹
$(magnetostatic(International))

julia> magnetostatic(EMU) # abH⋅m⁻¹
$(magnetostatic(EMU))

julia> magnetostatic(ESU) # statH⋅m⁻¹
$(magnetostatic(ESU))

julia> magnetostatic(HLU) # hlH⋅m⁻¹
$(magnetostatic(HLU))
```
""" magnetostatic, kₘ, km

@doc """
$(unitext(:vacuumimpedance,"vacuumpermeability(U)*lightspeed(U)*rationalization(U)*lorentz(U)^2"))

Vacuum impedance of free space `Z₀` is magnitude ratio of electric to magnetic field (Ω).
```Julia
julia> vacuumimpedance(Metric) # Ω
$(vacuumimpedance(Metric))

julia> vacuumimpedance(Conventional) # Ω
$(vacuumimpedance(Conventional))

julia> vacuumimpedance(CODATA) # Ω
$(vacuumimpedance(CODATA))

julia> vacuumimpedance(SI2019) # Ω
$(vacuumimpedance(SI2019))

julia> vacuumimpedance(International) # Ω
$(vacuumimpedance(International))

julia> vacuumimpedance(InternationalMean) # Ω
$(vacuumimpedance(InternationalMean))

julia> 120π # 3e8*μ₀ # Ω
$(120π)

julia> vacuumimpedance(EMU) # abΩ
$(vacuumimpedance(EMU))

julia> vacuumimpedance(ESU) # statΩ
$(vacuumimpedance(ESU))

julia> vacuumimpedance(HLU) # hlΩ
$(vacuumimpedance(HLU))
```
""" vacuumimpedance, Z₀, Z0

@doc """
$(unitext(:elementarycharge,"√(𝟐*planck(U)*finestructure(U)/vacuumimpedance(U))"))

Quantized elementary charge `𝘦` of a proton or electron `2/(klitzing(U)*josephson(U))` (C).
```Julia
julia> elementarycharge(SI2019) # C
$(elementarycharge(SI2019))

julia> elementarycharge(Metric) # C
$(elementarycharge(Metric))

julia> elementarycharge(CODATA) # C
$(elementarycharge(CODATA))

julia> elementarycharge(Conventional) # C
$(elementarycharge(Conventional))

julia> elementarycharge(International) # C
$(elementarycharge(International))

julia> elementarycharge(EMU) # abC
$(elementarycharge(EMU))

julia> elementarycharge(ESU) # statC
$(elementarycharge(ESU))

julia> elementarycharge(Planck) # sqrt(4π/αinv)
$(elementarycharge(Planck))
```
""" elementarycharge, 𝘦, ee

@doc """
$(unitext(:faraday,"elementarycharge(U)*avogadro(U)"))

Electric charge per mole of electrons `𝔉` based on elementary charge (C⋅mol⁻¹).
```Julia
julia> faraday(SI2019) # C⋅mol⁻¹
$(faraday(SI2019))

julia> faraday(Metric) # C⋅mol⁻¹
$(faraday(Metric))

julia> faraday(CODATA) # C⋅mol⁻¹
$(faraday(CODATA))

julia> faraday(Conventional) # C⋅mol⁻¹
$(faraday(Conventional))

julia> faraday(International) # C⋅mol⁻¹
$(faraday(International))

julia> faraday(InternationalMean) # C⋅mol⁻¹
$(faraday(InternationalMean))

julia> faraday(EMU) # abC⋅mol⁻¹
$(faraday(EMU))

julia> faraday(ESU) # statC⋅mol⁻¹
$(faraday(ESU))

julia> faraday(Metric)/kilocalorie(Metric) # kcal⋅(V-g-e)⁻¹
$(faraday(Metric)/kilocalorie(Metric))

julia> faraday(Metric)/3600 # A⋅h⋅mol⁻¹
$(faraday(Metric)/HOUR)
```
""" faraday, 𝔉, FF

@doc """
$(unitext(:josephson,"𝟐*elementarycharge(U)*lorentz(U)/planck(U) # 𝟏/magneticfluxquantum(U)"))

Josephson constant `KJ` relating potential difference to irradiation frequency (Hz⋅V⁻¹).
```Julia
julia> josephson(SI2019) # Hz⋅V⁻¹
$(josephson(SI2019))

julia> josephson(Metric) # Hz⋅V⁻¹
$(josephson(Metric))

julia> josephson(Conventional) # Hz⋅V⁻¹
$(josephson(Conventional))

julia> josephson(CODATA) # Hz⋅V⁻¹
$(josephson(CODATA))

julia> josephson(International) # Hz⋅V⁻¹
$(josephson(International))

julia> josephson(EMU) # Hz⋅abV⁻¹
$(josephson(EMU))

julia> josephson(ESU) # Hz⋅statV⁻¹
$(josephson(ESU))
```
""" josephson, KJ

@doc """
$(unitext(:magneticfluxquantum,"planck(U)/𝟐/elementarycharge(U)/lorentz(U)"))

Magnetic flux quantum `Φ₀` is `𝟏/josephson(U)` (Wb).
```Julia
julia> magneticfluxquantum(SI2019) # Wb
$(magneticfluxquantum(SI2019))

julia> magneticfluxquantum(Metric) # Wb
$(magneticfluxquantum(Metric))

julia> magneticfluxquantum(Conventional) # Wb
$(magneticfluxquantum(Conventional))

julia> magneticfluxquantum(International) # Wb
$(magneticfluxquantum(International))

julia> magneticfluxquantum(InternationalMean) # Wb
$(magneticfluxquantum(InternationalMean))

julia> magneticfluxquantum(EMU) # Mx
$(magneticfluxquantum(EMU))

julia> magneticfluxquantum(ESU) # statWb
$(magneticfluxquantum(ESU))
```
""" magneticfluxquantum, Φ₀

@doc """
$(unitext(:klitzing,"planck(U)/elementarycharge(U)^2"))

Quantized Hall resistance `RK` (Ω).
```Julia
julia> klitzing(SI2019) # Ω
$(klitzing(SI2019))

julia> klitzing(Metric) # Ω
$(klitzing(Metric))

julia> klitzing(Conventional) # Ω
$(klitzing(Conventional))

julia> klitzing(International) # Ω
$(klitzing(International))

julia> klitzing(CODATA) # Ω
$(klitzing(CODATA))

julia> klitzing(EMU) # abΩ
$(klitzing(EMU))

julia> klitzing(ESU) # statΩ
$(klitzing(ESU))
```
""" klitzing, RK

@doc """
$(unitext(:conductancequantum,"𝟐*elementarycharge(U)^2/planck(U) # 2/klitzing(U)"))

Conductance quantum `G₀` is a quantized unit of electrical conductance (S).
```Julia
julia> conductancequantum(SI2019) # S
$(conductancequantum(SI2019))

julia> conductancequantum(Metric) # S
$(conductancequantum(Metric))

julia> conductancequantum(Conventional) # S
$(conductancequantum(Conventional))

julia> conductancequantum(CODATA) # S
$(conductancequantum(CODATA))

julia> conductancequantum(International) # S
$(conductancequantum(International))

julia> conductancequantum(InternationalMean) # S
$(conductancequantum(InternationalMean))

julia> conductancequantum(EMU) # abS
$(conductancequantum(EMU))

julia> conductancequantum(ESU) # statS
$(conductancequantum(ESU))
```
""" conductancequantum, G₀, G0

@doc """
$(unitext(:hartree,"electronmass(U)/gravity(U)*(lightspeed(U)*finestructure(U))^2"))

Hartree electric potential energy `Eₕ` of the hydrogen atom at ground state is `2R∞*𝘩*𝘤` (J).
```Julia
julia> hartree(SI2019)/elementarycharge(SI2019) # eV
$(hartree(SI2019)/elementarycharge(SI2019))

julia> hartree(Metric) # J
$(hartree(Metric))

julia> hartree(CGS) # erg
$(hartree(CGS))

julia> hartree(Metric)*avogadro(Metric)/kilo # kJ⋅mol⁻¹
$(hartree(Metric)*avogadro(Metric)/kilo)

julia> hartree(Metric)*avogadro(Metric)/kilocalorie(Metric) # kcal⋅mol⁻¹
$(hartree(Metric)*avogadro(Metric)/kilocalorie(Metric))

julia> 𝟐*centi*rydberg(Metric) # Eₕ/𝘩/𝘤/100 cm⁻¹
$(hartree(Metric)/planck(Metric)/lightspeed(Metric)/(𝟐*𝟓)^2)

julia> hartree(Metric)/planck(Metric) # Hz
$(hartree(Metric)/planck(Metric))

julia> hartree(Metric)/boltzmann(Metric) # K
$(hartree(Metric)/boltzmann(Metric))
```
In a Gaussian unit system where `4π*ε₀ == 1` the Hartree energy is `𝘦^2/a₀`.
""" hartree, Eₕ, Eh

@doc """
$(unitext(:rydberg,"hartree(U)/2planck(U)/lightspeed(U) # Eₕ/2𝘩/𝘤"))

Rydberg constant `R∞` is lowest energy photon capable of ionizing atom at ground state (m⁻¹).
```Julia
julia> rydberg(Metric) # m⁻¹
$(rydberg(Metric))
```
The Rydberg constant for hydrogen `RH` is `R∞*mₚ/(mₑ+mₚ)` (m⁻¹).
```Julia
julia> rydberg(Metric)*protonmass(Metric)/(electronmass(Metric)+protonmass(Metric)) # m⁻¹
$(rydberg(Metric)*protonmass(Metric)/(electronmass(Metric)+protonmass(Metric)))
```
Rydberg unit of photon energy `Ry` is `𝘩*𝘤*R∞` or `Eₕ/2` (J).
```Julia
julia> hartree(Metric)/2 # J
$(hartree(Metric)/𝟐)

julia> hartree(SI2019)/𝟐/elementarycharge(SI2019) # eV
$(hartree(SI2019)/𝟐/elementarycharge(SI2019))
```
Rydberg photon frequency `𝘤*R∞` or `Eₕ/2𝘩` (Hz).
```Julia
julia> lightspeed(Metric)*rydberg(Metric) # Hz
$(lightspeed(Metric)*rydberg(Metric))
```
Rydberg wavelength `1/R∞` (m).
```Julia
julia> 𝟏/rydberg(Metric) # m
$(𝟏/rydberg(Metric))

julia> 𝟏/rydberg(Metric)/τ # m⋅rad⁻¹
$(𝟏/rydberg(Metric)/τ)
```
Precision measurements of the Rydberg constants are within a relative standard uncertainty of under 2 parts in 10¹², and is chosen to constrain values of other physical constants.
""" rydberg, R∞, RH, Ry

@doc """
$(unitext(:bohr,"planckreduced(U)*gravity(U)/electronmass(U)/lightspeed(U)/finestructure(U)"))

Bohr radius of the hydrogen atom in its ground state `a₀` (m).
```Julia
julia> bohr(Metric) # m
$(bohr(Metric))

julia> bohr(IPS) # in
$(bohr(IPS))

julia> bohr(Hartree) # a₀
$(bohr(Hartree))
```
""" bohr, a₀, a0
#julia> bohr(Metric)/length(PlanckGauss) # ℓP
#$(bohr(Metric)/length(PlanckGauss))

#=@doc """
$(unitext(:bohrreduced,"bohr(U)*(1+1/protonelectron(U))"))

Reduced Bohr radius including the effect of reduced mass in hydrogen atom (m).
```Julia
julia> bohrreduced(Metric) # m
$(bohrreduced(Metric))

julia> bohrreduced(Metric)/bohr(Metric) # a₀
$(bohrreduced(Metric)/bohr(Metric))
```
""" bohrreduced=#

@doc """
$(unitext(:electronradius,"finestructure(U)*planckreduced(U)*gravity(U)/electronmass(U)/lightspeed(U)"))

Classical electron radius or Lorentz radius or Thomson scattering length (m).
```Julia
julia> electronradius(Metric) # m
$(electronradius(Metric))

julia> electronradius(CODATA) # m
$(electronradius(CODATA))

julia> electronradius(Conventional) # m
$(electronradius(Conventional))
```
""" electronradius, rₑ, re

@doc """
$(unitext(:magneton,"elementarycharge(U)*planckreduced(U)*lorentz(U)/2electronmass(U)"))

Bohr magneton `μB` natural unit for expressing magnetic moment of electron (J⋅T⁻¹).
```Julia
julia> magneton(SI2019) # J⋅T⁻¹
$(magneton(SI2019))

julia> magneton(Metric) # J⋅T⁻¹
$(magneton(Metric))

julia> magneton(CODATA) # J⋅T⁻¹
$(magneton(CODATA))

julia> magneton(Conventional) # J⋅T⁻¹
$(magneton(Conventional))

julia> magneton(International) # J⋅T⁻¹
$(magneton(International))

julia> magneton(ESU) # statA⋅cm²
$(magneton(ESU))

julia> magneton(SI2019)/elementarycharge(SI2019) # eV⋅T⁻¹
$(magneton(SI2019)/elementarycharge(SI2019))

julia> magneton(Hartree) # 𝘤⋅ħ⋅mₑ⁻¹
$(magneton(Hartree))
```
""" magneton, μB

@doc """
$(unitext(:wienwavelength,"planck(U)*lightspeed(U)/boltzmann(U)/(𝟓+W₀(-𝟓*exp(-𝟓)))"))

Wien wavelength displacement law constant based on Lambert `W₀` evaluation (m⋅K or ft⋅°R).
```Julia
julia> wienwavelength(Metric) # m⋅K
$(wienwavelength(Metric))

julia> wienwavelength(SI2019) # m⋅K
$(wienwavelength(SI2019))

julia> wienwavelength(Conventional) # m⋅K
$(wienwavelength(Conventional))

julia> wienwavelength(CODATA) # m⋅K
$(wienwavelength(CODATA))

julia> wienwavelength(English) # ft⋅°R
$(wienwavelength(English))
```
""" wienwavelength

@doc """
$(unitext(:wienfrequency,"(𝟑+W₀(-𝟑*exp(-𝟑)))*boltzmann(U)/planck(U)"))

Wien frequency radiation law constant based on Lambert `W₀` evaluation (Hz⋅K⁻¹).
```Julia
julia> wienfrequency(Metric) # Hz⋅K⁻¹
$(wienfrequency(Metric))

julia> wienfrequency(SI2019) # Hz⋅K⁻¹
$(wienfrequency(SI2019))

julia> wienfrequency(Conventional) # Hz⋅K⁻¹
$(wienfrequency(Conventional))

julia> wienfrequency(CODATA) # Hz⋅K⁻¹
$(wienfrequency(CODATA))

julia> wienfrequency(English) # Hz⋅°R⁻¹
$(wienfrequency(English))
```
""" wienfrequency

@doc """
$(unitext(:loschmidt,"atmosphere(U)/boltzmann(U)/temperature(T₀,SI2019,U)"))

Number of molecules (number density) of an ideal gas in a unit volume (m⁻³ or ft⁻³).
```Julia
julia> loschmidt(SI2019) # m⁻³
$(loschmidt(SI2019))

julia> loschmidt(Metric,atm,T₀) # m⁻³
$(loschmidt(Metric,atm,T₀))

julia> loschmidt(Conventional,atm,T₀) # m⁻³
$(loschmidt(Conventional,atm,T₀))

julia> loschmidt(CODATA,atm,T₀) # m⁻³
$(loschmidt(CODATA,atm,T₀))

julia> loschmidt(SI1976,atm,T₀) # m⁻³
$(loschmidt(SI1976,atm,T₀))

julia> loschmidt(English) # ft⁻³
$(loschmidt(English))

julia> loschmidt(IAU) # au⁻³
$(loschmidt(IAU))
```
""" loschmidt

@doc """
$(unitext(:amagat,"loschmidt(U)/avogadro(U)"))

Number of moles of an ideal gas in a unit volume (mol⋅m⁻³ or lb-mol⋅ft⁻³).
```Julia
julia> amagat(Metric) # mol⋅m⁻³
$(amagat(Metric))

julia> amagat(SI2019) # mol⋅m⁻³
$(amagat(SI2019))

julia> amagat(English) # slug-mol⋅ft⁻³
$(amagat(English))
```
""" amagat

@doc """
$(unitext(:hyperfine,"frequency($ΔνCs,U)"))

Unperturbed groundstate hyperfine transition frequency `ΔνCs` of caesium-133 atom (Hz).
```Julia
julia> hyperfine(Metric) # Hz
$(hyperfine(Metric))
```
""" hyperfine, ΔνCs

@doc """
$(unitext(:hubble,"time(U,Hubble)"))

Hubble universe expansion frequency parameter.
```Julia
julia> hubble(Metric)
$(hubble(Metric))

julia> hubble(Hubble)
$(hubble(Hubble))

julia> hubble(Cosmological)
$(hubble(Cosmological))

julia> 𝟏/hubble(Metric)/year(Metric)
$(𝟏/hubble(Metric)/year(Metric))
```
""" hubble

@doc """
$(unitext(:cosmological,"𝟑*darkenergydensity(U)*(hubble(U)/lightspeed(U))^2"))

Cosmological constant from Einstein's controversial theory expanded on by Hubble.
```Julia
julia> cosmological(Metric)
$(cosmological(Metric))

julia> cosmological(Hubble)
$(cosmological(Hubble))

julia> cosmological(Cosmological)
$(cosmological(Cosmological))
```
""" cosmological

@doc """
$(unitext(:solarmass,"mass($(GM☉/G),U)"))

Solar `mass` estimated from gravitational constant estimates (kg or slug).
```Julia
julia> solarmass(Metric) # kg
$(solarmass(Metric))

julia> solarmass(British) # slug
$(solarmass(British))

julia> solarmass(English) # lb
$(solarmass(English))

julia> solarmass(IAUE) # ME
$(solarmass(IAUE))

julia> solarmass(IAUJ) # MJ
$(solarmass(IAUJ))
```
""" solarmass, mₛ

@doc """
$(unitext(:earthmass,"mass($(GME/G),U)"))

Earth `mass` estimated from gravitational constant estimates (kg or slug).
```Julia
julia> earthmass(Metric) # kg
$(earthmass(Metric))

julia> earthmass(British) # slug
$(earthmass(British))

julia> earthmass(English) # lb
$(earthmass(English))

julia> earthmass(IAU) # M☉
$(earthmass(IAU))

julia> earthmass(IAUJ) # MJ
$(earthmass(IAUJ))
```
""" earthmass

@doc """
$(unitext(:jupitermass,"mass($(GMJ/G),U)"))

Jupiter `mass` estimated from gravitational constant estimates (kg or slug).
```Julia
julia> jupitermass(Metric) # kg
$(jupitermass(Metric))

julia> jupitermass(British) # slug
$(jupitermass(British))

julia> jupitermass(English) # lb
$(jupitermass(English))

julia> jupitermass(IAU) # M☉
$(jupitermass(IAU))

julia> jupitermass(IAUE) # ME
$(jupitermass(IAUE))
```
""" jupitermass

@doc """
$(unitext(:lunarmass,"earthmass(U)/μE☾"))

Lunar `mass` estimated from `μE☾` Earth-Moon mass ratio (kg or slug).
```Julia
julia> lunarmass(Metric) # kg
$(lunarmass(Metric))

julia> lunarmass(British) # slug
$(lunarmass(British))

julia> lunarmass(English) # lb
$(lunarmass(English))

julia> lunarmass(IAU) # M☉
$(lunarmass(IAU))

julia> lunarmass(IAUE) # ME
$(lunarmass(IAUE))

julia> lunarmass(IAUJ) # MJ
$(lunarmass(IAUJ))
```
""" lunarmass

@doc """
$(unitext(:gaussgravitation,"sqrt(gravitation(U)*solarmass(U)/astronomicalunit(U)^3)"))

Gaussian  gravitational constant `k` of Newton's laws (Hz or rad⋅D⁻¹).
```Julia
julia> gaussgravitation(Metric)
$(gaussgravitation(Metric))

juila> gaussgravitation(MPH)
$(gaussgravitation(MPH))

julia> gaussgravitation(IAU)
$(gaussgravitation(IAU))
```
""" gaussgravitation, k, kG

@doc """
$(unitext(:gaussianyear,"turn(U)/gaussgravitation(U)"))

Orbit `time` defined by `gaussgravitation` constant `kG` for neglible `mass` satellite.
```Julia
julia> gaussianyear(Metric) # s
$(gaussianyear(Metric))

julia> gaussianyear(MPH) # h
$(gaussianyear(MPH))

julia> gaussianyear(IAU) # D
$(gaussianyear(IAU))
```
""" gaussianyear

@doc """
$(unitext(:siderealyear,"gaussianyear(U)/√(𝟏+earthmass(IAU)+lunarmass(IAU))"))

Orbit `time` defined by `gaussgravitation` constant `kG` and the Earth-Moon system `mass`.
```Julia
julia> siderealyear(Metric) # s
$(siderealyear(Metric))

julia> siderealyear(MPH) # h
$(siderealyear(MPH))

julia> siderealyear(IAU) # D
$(siderealyear(IAU))
```
""" siderealyear

@doc """
$(unitext(:jovianyear,"τ*day(U)*√(jupiterdistance(U)^3/solarmass(U)/gravitation(U))/√(𝟏+jupitermass(IAU))"))

Orbit `time` defined by `jupiterdistance` and the Sun-Jupiter system `mass`.
```Julia
julia> jovianyear(Metric) # s
$(jovianyear(Metric))

julia> jovianyear(MPH) # h
$(jovianyear(MPH))

julia> jovianyear(IAU) # D
$(jovianyear(IAU))
```
""" jovianyear

@doc """
$(unitext(:gaussianmonth,"τ*sqrt(lunardistance(U)^3/earthmass(U)/gravitation(U))"))

Orbit `time` defined by `lunardistance` and `earthmass` for neglible `mass` satellite.
```Julia
julia> gaussianmonth(Metric) # s
$(gaussianmonth(Metric))

julia> gaussianmonth(MPH) # h
$(gaussianmonth(MPH))

julia> gaussianmonth(IAU) # D
$(gaussianmonth(IAU))
```
""" gaussianmonth

@doc """
$(unitext(:siderealmonth,"gaussianmonth(U)/√(𝟏+lunarmass(IAU))"))

Orbit `time` defined by standard `lunardistance` and the Earth-Moon system `mass`.
```Julia
julia> siderealmonth(Metric) # s
$(siderealmonth(Metric))

julia> siderealmonth(MPH) # h
$(siderealmonth(MPH))

julia> siderealmonth(IAU) # D
$(siderealmonth(IAU))
```
""" siderealmonth

@doc """
$(unitext(:synodicmonth,"𝟏/(𝟏/siderealmonth(U)-𝟏/siderealyear(U))"))

Orbit `time` defined by `siderealmonth` and `siderealyear` of the Sun-Earth-Moon system.
```Julia
julia> synodicmonth(Metric) # s
$(synodicmonth(Metric))

julia> synodicmonth(MPH) # h
$(synodicmonth(MPH))

julia> synodicmonth(IAU) # D
$(synodicmonth(IAU))
```
""" synodicmonth

@doc """
$(unitext(:earthradius,"sqrt(earthmass(U)*gravitation(U)/gforce(U))"))

Approximate `length` of standard Earth two-body radius consistent with units (m or ft).
```Julia
julia> earthradius(KKH) # km
$(earthradius(KKH))

julia> earthradius(Nautical) # nm
$(earthradius(Nautical))

julia> earthradius(IAU) # au
$(earthradius(IAU))
```
""" earthradius

@doc """
$(unitext(:greatcircle,"τ*earthradius(U)"))

Approximate `length` of standard Earth two-body circle consistent with units (m or ft).
```Julia
julia> greatcircle(KKH) # km
$(greatcircle(KKH))

julia> greatcircle(Nautical) # nm
$(greatcircle(Nautical))

julia> greatcircle(IAU) # au
$(greatcircle(IAU))
```
""" greatcircle

@doc """
    sackurtetrode(U::UnitSystem,P=atm,T=𝟏,m=mᵤ) = log(kB*T/P*sqrt(m*kB*T/τ/ħ^2)^3)+5/2
    dimensionless : [𝟙], [𝟙], [𝟙], [𝟙], [𝟙]

Ideal gas entropy density for pressure `P`, temperature `T`, atomicmass `m` (dimensionless).
```Julia
julia> sackurtetrode(Metric)
$(sackurtetrode(Metric))

julia> sackurtetrode(SI2019)
$(sackurtetrode(SI2019))

julia> sackurtetrode(Conventional)
$(sackurtetrode(Conventional))

julia> sackurtetrode(CODATA)
$(sackurtetrode(CODATA))

julia> sackurtetrode(SI2019,𝟏𝟎^5)
$(sackurtetrode(SI2019,𝟏𝟎^5))
```
""" sackurtetrode

@doc """
    mechanicalheat(U::UnitSystem) = molargas(U)/molargas(Metric)*calorie(Metric)
    energy : [FL], [FL], [ML²T⁻²], [ML²T⁻²], [ML²T⁻²]

Heat to raise 1 `mass` unit of water by 1 `temperature` unit, or $(normal(molargas(Metric)/calorie(Metric))) `mechanicalheat` per `molaramount` per `temperature` units (J or ft⋅lb).
```Julia
julia> mechanicalheat(Metric) # J
$(mechanicalheat(Metric))

julia> mechanicalheat(English) # ft⋅lb
$(mechanicalheat(English))

julia> mechanicalheat(British) # ft⋅lb
$(mechanicalheat(British))
```
""" mechanicalheat

include("derivedocs.jl")


#using LIBLINEAR, UnicodePlots
#include("utils.jl")
#include("main.jl")

# USAGE: copy and paste the following line to work with this script
# include("utils.jl"); include("main.jl"); using Revise, LIBLINEAR, UnicodePlots; includet("inspect-models.jl")
#inspect("_entropy_pan20-en.params", "PAN20/en.train.json", "PAN20/en.test.json")

function vocab(config, dataname)
    D = loadjson(dataname, textkey="text", labelkey="klass")
    textconfig = config.textconfig
    #textconfig = copy(config.textconfig, nlist=[3], qlist=[])
    config = MicroTC_Config(textconfig, config.textmodel, config.cls)
    c = MicroTC(config, D.corpus, D.labels, tok=Tokenizer(config.textconfig))
    Dict(decode(c.tok, k) => v.weight for (k, v) in c.textmodel.tokens)
end

function topk(vec, k=length(vec))
    s = sort!(collect(vec), by=p->p[end], rev=true)
    if k !== length(vec)
        s[1:k]
    else
        s
    end
end

function prune(vec, k::Integer)
    Dict(topk(vec, k))
end

function prune(vec, minw::Real)
    Dict(k => w for (k, w) in vec if w >= minw)
end

function merge(A, B)
    if length(B) < length(A)
        A, B = B, A
    end

    Dict(k => sqrt(v * B[k]) for (k, v) in A if haskey(B, k))
end

function plotweights(vec, k=0)
    L = sort!(collect(vec), by=p->p[end], rev=true)
    if k > 0
        L = L[1:k]
    end
    lineplot(last.(L))
end

function inspect(paramsfile, trainfile, testfile)
    P = loadparams(paramsfile)
    vtrain = vocab(P[1][1], trainfile)
    vtest = vocab(P[1][1], testfile)
    vtrain, vtest
end


using POMDPs
#using RLESUtils, Observers
import Base: getindex, setindex!, haskey

# State node in the search tree
type StateNode
  n::Array{Int64,1} # number of visits at the node for each action
  Q::Array{Reward,1} # estimated value for each action
  action_map::Vector{Action} # for converting action idxs to action types
  nextstates::Vector{Nullable{UInt64}} #deterministic state transition means 1-1 mapping between actions and sp's
  explored::Bool #true if subtree is fully explored
end
function StateNode(nA, space::AbstractSpace, q0::Float64=0.0, explored::Bool=false)
  n = zeros(Int32, nA)
  Q = fill(q0, nA) #zeros(Reward, nA)
  # creates the action map
  am = Action[]
  for a in iterator(space)
    push!(am, a)
  end
  nextstates = [Nullable{UInt64}() for i = 1:nA]
  StateNode(n, Q, am, nextstates, explored)
end

# MCTS solver type
type MCTSSolver <: POMDPs.Solver
  n_iterations::Int64	# number of iterations during each action() call
  depth::Int64 # the max depth of the tree
  exploration_constant::Float64 # constant balancing exploration and exploitation
  maxmod::Bool
  rng::AbstractRNG # random number generator
  tree::Dict{UInt64, StateNode} # the search tree
end
# solver constructor
function MCTSSolver(;n_iterations::Int64=100,
                    depth::Int64=10,
                    exploration_constant::Float64=1.0,
                    maxmod::Bool=false, #use the max modification when updating q
                    rng = MersenneTwister(1))
  tree = Dict{UInt64, StateNode}() #key is hash(State)
  return MCTSSolver(n_iterations, depth, exploration_constant, maxmod, rng, tree)
end

# MCTS policy type
type MCTSPolicy <: POMDPs.Policy
  mcts::MCTSSolver # containts the solver parameters
  mdp::POMDP # model
  action_space::AbstractSpace # pre-allocated for rollout
  state::State # pre-allocated for sampling
  action::Action # pre-allocated for sampling
  distribution::AbstractDistribution # pre-allocated for memory efficiency
  rollout_policy::Policy # used in the rollout evaluation
  q0::Float64
  best_reward::Reward
  best_state::State
  best_at_eval::Int64
  totalevals::Int64
end
# policy constructor
function MCTSPolicy(mcts::MCTSSolver, mdp::POMDP,
                    rollout_policy=RandomPolicy(mdp, mcts.rng); # random policy is default
                    q0::Float64=0.0,
                    best_at_eval::Int64=0,
                    totalevals::Int64=0)
  # pre-allocate action space
  as = actions(mdp)
  # pre-allocate the state distrbution
  d = create_transition_distribution(mdp)
  s = create_state(mdp)
  a = create_action(mdp)
  best_reward = q0
  best_state = create_state(mdp)
  return MCTSPolicy(mcts, mdp, as, s, a, d, rollout_policy, q0, best_reward, best_state, best_at_eval, totalevals)
end

# for convenience - no computation is done in solve
function POMDPs.solve(solver::MCTSSolver, mdp::POMDP, policy::MCTSPolicy)
  policy
end

# retuns an approximately optimal action
function POMDPs.action(policy::MCTSPolicy, state::State)
  n_iterations = policy.mcts.n_iterations
  depth = policy.mcts.depth
  tree = policy.mcts.tree
  # build the tree
  for n = 1:n_iterations
    isexplored(tree, state) && break #can't do better, exit early

    #@notify_observer(policy.observer, "iteration", [n])
    simulate(policy, state, depth)
  end
  snode = tree[state]
  # find the index of action with highest q val
  i = indmax(snode.Q)
  #@notify_observer(policy.observer, "Qs", [snode.Q])
  #@notify_observer(policy.observer, "best_i", [i])
  #@notify_observer(policy.observer, "tree", [tree, state])
  # use map to conver index to mdp action
  return snode.action_map[i]
end

# runs a simulation from the passed in state to the specified depth
function POMDPs.simulate(policy::MCTSPolicy, state::State, depth::Int64)
  # model parameters
  mdp = policy.mdp
  na = n_actions(mdp, state)
  discount_factor = discount(mdp)
  sp = policy.state
  rng = policy.mcts.rng

  # solver parameters
  n_iterations = policy.mcts.n_iterations
  tree = policy.mcts.tree
  exploration_constant = policy.mcts.exploration_constant

  # if unexplored state add to the tree
  isnew = false
  if !haskey(tree, state)
    as = actions(mdp, state, policy.action_space)
    tree[state] = StateNode(na, as, policy.q0)
    isnew = true
  end

  snode = tree[state]

  if isterminal(mdp, state) #terminal states don't have corresponding snodes?
    snode.explored = true
    r = reward(mdp, state)
    policy.totalevals += 1 #expr eval call
    #@notify_observer(policy.observer, "terminal_reward", [r, state])
    if r > policy.best_reward
      policy.best_reward = r
      copy!(policy.best_state, state)
      policy.best_at_eval = policy.totalevals
    end
    return r
  end

  if isnew #new but not terminal
    return rollout(policy, depth, state)
  end

  # don't choose fully-explored subtrees
  best_i = -1
  best_val = -realmax(Float64)
  for i = 1:na
    if !isexplored(tree, snode.nextstates[i])
      #selection criteria
      val =  snode.Q[i] + exploration_constant * real(sqrt(complex(log(sum(snode.n)) / snode.n[i])))

      if best_i < 0 || val > best_val #accept the first unexplored one or accept if multiple ties for best
        best_val = val
        best_i = i
      end
    end
  end
  i = best_i
  a = snode.action_map[i]
  r = reward(mdp, state, a)
  # transition to a new state
  d = policy.distribution
  d = transition(mdp, state, a, d)
  sp = rand!(rng, sp, d)

  #add this transition to snode
  if !isnull(snode.nextstates[i])
    @assert get(snode.nextstates[i]) == hash(sp)
  else
    snode.nextstates[i] = Nullable{UInt64}(hash(sp))
  end

  # update the Q and n values
  q = r + discount_factor * simulate(policy, sp, depth - 1)
  snode.n[i] += 1 # increase number of node visits by one

  if policy.mcts.maxmod #maxmod
    snode.Q[i] = max(snode.Q[i], q)
  else
    snode.Q[i] += ((q - snode.Q[i]) / (snode.n[i])) # moving average of Q value
  end

  #if all children are fully-explored, mark this node as fully-explored
  snode.explored = isexplored(tree, snode.nextstates)

  return q
end

# recursive rollout to specified depth, returns the accumulated discounted reward
function rollout(policy::MCTSPolicy, depth::Int64, state::State)
  mdp = policy.mdp
  # finish when depth is zero or reach terminal state
  if isterminal(mdp, state)
    r = reward(mdp, state)
    policy.totalevals += 1 #expr eval call
    if r > policy.best_reward
      policy.best_reward = r
      copy!(policy.best_state, state)
      policy.best_at_eval = policy.totalevals
    end
    return r
  end
  d = policy.distribution
  discount_factor = discount(mdp)
  rng = policy.mcts.rng
  sp = policy.state
  # follow the rollout policy
  a = action(policy.rollout_policy, state)
  # compute reward
  r = reward(mdp, state, a)
  # sample the next state
  d = transition(mdp, state, a, d)
  sp = rand!(rng, sp, d)
  return r + discount_factor * rollout(policy, depth - 1, sp)
end

#override to use the hash instead
Base.getindex{V}(D::Dict{UInt64,V}, key::State) = D[hash(key)]
Base.getindex{V}(D::Dict{UInt64,V}, key::Nullable{UInt64}) = D[get(key)]
Base.setindex!{V}(D::Dict{UInt64,V}, value::V, key::State) = D[hash(key)] = value
Base.setindex!{V}(D::Dict{UInt64,V}, value::V, key::Nullable{UInt64}) = D[get(key)] = value
Base.haskey{V}(D::Dict{UInt64,V}, key::State) = haskey(D, hash(key))
Base.haskey{V}(D::Dict{UInt64,V}, key::Nullable{UInt64}) = haskey(D, get(key))

isexplored(tree::Dict{UInt64,StateNode}, s::State) = haskey(tree, s) && tree[s].explored
isexplored(tree::Dict{UInt64,StateNode}, h::Nullable{UInt64}) = !isnull(h) && tree[get(h)].explored

function isexplored(tree::Dict{UInt64,StateNode}, nextstates::Vector{Nullable{UInt64}})
  explored = true
  for h in nextstates
    if !isexplored(tree, h)
      explored = false
      break
    end
  end
  return explored
end


# by lazarusA # HIDE
# using GLMakie # HIDE
using CairoMakie, TimeSeries, MarketData, Dates
CairoMakie.activate!() # HIDE
let
    # dummy data
    dates = Date(2018, 1, 1):Day(1):Date(2019, 12, 31)
    ta = TimeArray(dates, rand(length(dates)))
    vals = 0.5*values(ta)
    tempo = string.(timestamp(ta))
    lentime = length(tempo)
    slice_dates = range(1,lentime, step = lentime÷11)

    fig = Figure(resolution = (700, 450), font =:sans)
    ax = Axis(fig, xlabel = "Date", ylabel = "value")
    line1 = lines!(ax,1:lentime, vals, color = :black, linewidth = 0.85)
    ax.xticks = (slice_dates, tempo[slice_dates])
    ax.xticklabelrotation = π/4
    #ax.xticklabelalign = (:center, :center)
    fig[1,1] = ax
    fig
    save(joinpath(@__DIR__, "output", "timeSeries.png"), fig, px_per_unit = 2) # HIDE
end

using Pkg # HIDE
Pkg.status(["CairoMakie","TimeSeries","MarketData","Dates"]) # HIDE


## TYPES AND CONSTRUCTOR

mutable struct DeterministicTunedModel{T,M<:Deterministic} <: MLJBase.Deterministic
    model::M
    tuning::T  # tuning strategy
    resampling # resampling strategy
    measure
    weights::Union{Nothing,Vector{<:Real}}
    operation
    range
    selection_heuristic
    train_best::Bool
    repeats::Int
    n::Union{Int,Nothing}
    acceleration::AbstractResource
    acceleration_resampling::AbstractResource
    check_measure::Bool
    cache::Bool
end

mutable struct ProbabilisticTunedModel{T,M<:Probabilistic} <: MLJBase.Probabilistic
    model::M
    tuning::T  # tuning strategy
    resampling # resampling strategy
    measure
    weights::Union{Nothing,AbstractVector{<:Real}}
    operation
    range
    selection_heuristic
    train_best::Bool
    repeats::Int
    n::Union{Int,Nothing}
    acceleration::AbstractResource
    acceleration_resampling::AbstractResource
    check_measure::Bool
    cache::Bool
end

const EitherTunedModel{T,M} =
    Union{DeterministicTunedModel{T,M},ProbabilisticTunedModel{T,M}}

#todo update:
"""
    tuned_model = TunedModel(; model=nothing,
                             tuning=Grid(),
                             resampling=Holdout(),
                             measure=nothing,
                             weights=nothing,
                             repeats=1,
                             operation=predict,
                             range=nothing,
                             selection_heuristic=NaiveSelection(),
                             n=default_n(tuning, range),
                             train_best=true,
                             acceleration=default_resource(),
                             acceleration_resampling=CPU1(),
                             check_measure=true,
                             cache=true)

Construct a model wrapper for hyperparameter optimization of a
supervised learner.

Calling `fit!(mach)` on a machine `mach=machine(tuned_model, X, y)` or
`mach=machine(tuned_model, X, y, w)` will:

- Instigate a search, over clones of `model`, with the hyperparameter
  mutations specified by `range`, for a model optimizing the specified
  `measure`, using performance evaluations carried out using the
  specified `tuning` strategy and `resampling` strategy.

- Fit an internal machine, based on the optimal model
  `fitted_params(mach).best_model`, wrapping the optimal `model`
  object in *all* the provided data `X`, `y`(, `w`). Calling
  `predict(mach, Xnew)` then returns predictions on `Xnew` of this
  internal machine. The final train can be supressed by setting
  `train_best=false`.

The `range` objects supported depend on the `tuning` strategy
specified. Query the `strategy` docstring for details. To optimize
over an explicit list `v` of models of the same type, use
`strategy=Explicit()` and specify `model=v[1]` and `range=v`.

The number of models searched is specified by `n`. If unspecified,
then `MLJTuning.default_n(tuning, range)` is used. When `n` is
increased and `fit!(mach)` called again, the old search history is
re-instated and the search continues where it left off.

If `measure` supports weights (`supports_weights(measure) == true`)
then any `weights` specified will be passed to the measure. If more
than one `measure` is specified, then only the first is optimized
(unless `strategy` is multi-objective) but the performance against
every measure specified will be computed and reported in
`report(mach).best_performance` and other relevant attributes of the
generated report.

Specify `repeats > 1` for repeated resampling per model
evaluation. See [`evaluate!`](@ref) options for details.

*Important.* If a custom `measure` is used, and the measure is
a score, rather than a loss, be sure to check that
`MLJ.orientation(measure) == :score` to ensure maximization of the
measure, rather than minimization. Override an incorrect value with
`MLJ.orientation(::typeof(measure)) = :score`.

In the case of two-parameter tuning, a Plots.jl plot of performance
estimates is returned by `plot(mach)` or `heatmap(mach)`.

Once a tuning machine `mach` has bee trained as above, then
`fitted_params(mach)` has these keys/values:

key                 | value
--------------------|--------------------------------------------------
`best_model`        | optimal model instance
`best_fitted_params`| learned parameters of the optimal model

The named tuple `report(mach)` includes these keys/values:

key                 | value
--------------------|--------------------------------------------------
`best_model`        | optimal model instance
`best_history_entry`| corresponding entry in the history, including performance estimate
`best_report`       | report generated by fitting the optimal model to all data
`history`           | tuning strategy-specific history of all evaluations

plus other key/value pairs specific to the `tuning` strategy.

### Summary of key-word arguments

- `model`: `Supervised` model prototype that is cloned and mutated to
  generate models for evaluation

- `tuning=Grid()`: tuning strategy to be applied (eg, `RandomSearch()`)

- `resampling=Holdout()`: resampling strategy (eg, `Holdout()`, `CV()`),
  `StratifiedCV()`) to be applied in performance evaluations

- `measure`: measure or measures to be applied in performance
  evaluations; only the first used in optimization (unless the
  strategy is multi-objective) but all reported to the history

- `weights`: sample weights to be passed the measure(s) in performance
  evaluations, if supported.

- `repeats=1`: for generating train/test sets multiple times in
  resampling; see [`evaluate!`](@ref) for details

- `operation=predict`: operation to be applied to each fitted model;
  usually `predict` but `predict_mean`, `predict_median` or
  `predict_mode` can be used for `Probabilistic` models, if
  the specified measures are `Deterministic`

- `range`: range object; tuning strategy documentation describes
  supported types

- `selection_heuristic`: the rule determining how the best model is
  decided. According to the default heuristic,
  `NaiveSelection()`, `measure` (or the first
  element of `measure`) is evaluated for each resample and these
  per-fold measurements are aggregrated. The model with the lowest
  (resp. highest) aggregate is chosen if the measure is a `:loss`
  (resp. a `:score`).

- `n`: number of iterations (ie, models to be evaluated); set by
  tuning strategy if left unspecified

- `train_best=true`: whether to train the optimal model

- `acceleration=default_resource()`: mode of parallelization for
  tuning strategies that support this

- `acceleration_resampling=CPU1()`: mode of parallelization for
  resampling

- `check_measure=true`: whether to check `measure` is compatible with the
  specified `model` and `operation`)

- `cache=true`: whether to cache model-specific representations of
  user-suplied data; set to `false` to conserve memory. Speed gains
  likely limited to the case `resampling isa Holdout`.

"""
function TunedModel(; model=nothing,
                    tuning=Grid(),
                    resampling=MLJBase.Holdout(),
                    measures=nothing,
                    measure=measures,
                    weights=nothing,
                    operation=predict,
                    ranges=nothing,
                    range=ranges,
                    selection_heuristic=NaiveSelection(),
                    train_best=true,
                    repeats=1,
                    n=nothing,
                    acceleration=default_resource(),
                    acceleration_resampling=CPU1(),
                    check_measure=true,
                    cache=true)

    range === nothing && error("You need to specify `range=...`.")
    model == nothing && error("You need to specify model=... .\n"*
                              "If `tuning=Explicit()`, any model in the "*
                              "range will do. ")

    if model isa Deterministic
        tuned_model = DeterministicTunedModel(model, tuning, resampling,
                                              measure, weights, operation,
                                              range, selection_heuristic,
                                              train_best, repeats, n,
                                              acceleration,
                                              acceleration_resampling,
                                              check_measure,
                                              cache)
    elseif model isa Probabilistic
        tuned_model = ProbabilisticTunedModel(model, tuning, resampling,
                                              measure, weights, operation,
                                              range, selection_heuristic,
                                              train_best, repeats, n,
                                              acceleration,
                                              acceleration_resampling,
                                              check_measure,
                                              cache)
    else
        error("Only `Deterministic` and `Probabilistic` "*
              "model types supported.")
    end

    message = clean!(tuned_model)
    isempty(message) || @info message

    return tuned_model

end

function MLJBase.clean!(tuned_model::EitherTunedModel)
    message = ""
    if tuned_model.measure === nothing
        tuned_model.measure = default_measure(tuned_model.model)
        if tuned_model.measure === nothing
            error("Unable to deduce a default measure for specified model. "*
                  "You must specify `measure=...`. ")
        else
            message *= "No measure specified. "*
            "Setting measure=$(tuned_model.measure). "
        end
    end

    if !supports_heuristic(tuned_model.tuning, tuned_model.selection_heuristic)
        message *= "`selection_heuristic=$(tuned_model.selection_heuristic)` "*
        "is not supported by $(tuned_model.tuning). Resetting to "*
        "`NaiveSelectionment()`."
        tuned_model.selection_heuristic = NaiveSelection()
    end

    if (tuned_model.acceleration isa CPUProcesses &&
        tuned_model.acceleration_resampling isa CPUProcesses)
        message *=
        "The combination acceleration=$(tuned_model.acceleration) and"*
        " acceleration_resampling=$(tuned_model.acceleration_resampling) is"*
        "  not generally optimal. You may want to consider setting"*
        " `acceleration = CPUProcesses()` and"*
        " `acceleration_resampling = CPUThreads()`."
    end

    if (tuned_model.acceleration isa CPUThreads &&
        tuned_model.acceleration_resampling isa CPUProcesses)
        message *=
        "The combination acceleration=$(tuned_model.acceleration) and"*
        " acceleration_resampling=$(tuned_model.acceleration_resampling) isn't"*
        " supported. \n Resetting to"*
        " `acceleration = CPUProcesses()` and"*
        " `acceleration_resampling = CPUThreads()`."

        tuned_model.acceleration = CPUProcesses()
        tuned_model.acceleration_resampling = CPUThreads()
    end

    tuned_model.acceleration =
        _process_accel_settings(tuned_model.acceleration)

    return message
end

## FIT AND UPDATE METHODS

# A *metamodel* is either a `Model` instance, `model`, or a tuple
# `(model, s)`, where `s` is extra data associated with `model` that
# the tuning strategy implementation wants available to the `result`
# method for recording in the history.

_first(m::MLJBase.Model) = m
_last(m::MLJBase.Model) = nothing
_first(m::Tuple{Model,Any}) = first(m)
_last(m::Tuple{Model,Any}) = last(m)

# returns a (model, result) pair for the history:
function event(metamodel,
               resampling_machine,
               verbosity,
               tuning,
               history,
               state)
    model = _first(metamodel)
    metadata = _last(metamodel)
    resampling_machine.model.model = model
    verb = (verbosity >= 2 ? verbosity - 3 : verbosity - 1)
    fit!(resampling_machine, verbosity=verb)
    E = evaluate(resampling_machine)
    entry0 = (model       = model,
              measure     = E.measure,
              measurement = E.measurement,
              per_fold    = E.per_fold,
              metadata    = metadata)
    entry = merge(entry0, extras(tuning, history, state, E))
    if verbosity > 2
        println("hyperparameters: $(params(model))")
    end

    if verbosity > 1
        println("measurement: $(E.measurement[1])")
    end
    return entry
end

function assemble_events(metamodels,
                         resampling_machine,
                         verbosity,
                         tuning,
                         history,
                         state,
                         acceleration::CPU1)

     n_metamodels = length(metamodels)

     p = Progress(n_metamodels,
         dt = 0,
         desc = "Evaluating over $(n_metamodels) metamodels: ",
         barglyphs = BarGlyphs("[=> ]"),
         barlen = 25,
         color = :yellow)

    verbosity !=1 || update!(p,0)

    entries = map(metamodels) do m
        r = event(m, resampling_machine, verbosity, tuning, history, state)
        verbosity < 1 || begin
                  p.counter += 1
                  ProgressMeter.updateProgress!(p)
                end
        r
      end

    return entries
end

function assemble_events(metamodels,
                         resampling_machine,
                         verbosity,
                         tuning,
                         history,
                         state,
                         acceleration::CPUProcesses)

    n_metamodels = length(metamodels)

    entries = @sync begin
        channel = RemoteChannel(()->Channel{Bool}(min(1000, n_metamodels)), 1)
        p = Progress(n_metamodels,
                     dt = 0,
                     desc = "Evaluating over $n_metamodels metamodels: ",
                     barglyphs = BarGlyphs("[=> ]"),
                     barlen = 25,
                     color = :yellow)

        # printing the progress bar
        verbosity < 1 || begin
            update!(p,0)
            @async while take!(channel)
                p.counter +=1
                ProgressMeter.updateProgress!(p)
            end
        end

        ret = @distributed vcat for m in metamodels
            r = event(m, resampling_machine, verbosity, tuning, history, state)
            verbosity < 1 || begin
                put!(channel, true)
            end
            r
        end
        verbosity < 1 || put!(channel, false)
        ret
    end

    return entries
end

@static if VERSION >= v"1.3.0-DEV.573"
# one machine for each thread; cycle through available threads:
function assemble_events(metamodels,
                         resampling_machine,
                         verbosity,
                         tuning,
                         history,
                         state,
                         acceleration::CPUThreads)

    if Threads.nthreads() == 1
        return assemble_events(metamodels,
                         resampling_machine,
                         verbosity,
                         tuning,
                         history,
                         state,
                         CPU1())
   end

    n_metamodels = length(metamodels)
    ntasks = acceleration.settings
    partitions = chunks(1:n_metamodels, ntasks)
    #tasks = Vector{Task}(undef, length(partitions))
    entries = Vector(undef, length(partitions))
    p = Progress(n_metamodels,
         dt = 0,
         desc = "Evaluating over $(n_metamodels) metamodels: ",
         barglyphs = BarGlyphs("[=> ]"),
         barlen = 25,
         color = :yellow)
    ch = Channel{Bool}(min(1000, length(partitions)) )

    @sync begin
        # printing the progress bar
        verbosity < 1 || begin
            update!(p,0)
            @async while take!(ch)
                p.counter +=1
                ProgressMeter.updateProgress!(p)
            end
        end
        # One resampling_machine per task
         machs = [resampling_machine,
                 [machine(Resampler(
                     model= resampling_machine.model.model,
                     resampling    = resampling_machine.model.resampling,
                     measure       = resampling_machine.model.measure,
                     weights       = resampling_machine.model.weights,
                     operation     = resampling_machine.model.operation,
                     check_measure = resampling_machine.model.check_measure,
                     repeats       = resampling_machine.model.repeats,
                     acceleration  = resampling_machine.model.acceleration,
                     cache         = resampling_machine.model.cache),
                          resampling_machine.args...; cache=false) for
                  _ in 2:length(partitions)]...]

        @sync for (i, parts) in enumerate(partitions)
            Threads.@spawn begin
                entries[i] =  map(metamodels[parts]) do m
                    r = event(m, machs[i],
                              verbosity, tuning, history, state)
                    verbosity < 1 || put!(ch, true)
                    r
                end
            end
        end
        verbosity < 1 || put!(ch, false)
    end
    reduce(vcat, entries)
end

end # of if VERSION ...

# history is intialized to `nothing` because it's type is not known.
_vcat(history, Δhistory) = vcat(history, Δhistory)
_vcat(history::Nothing, Δhistory) = Δhistory
_length(history) = length(history)
_length(::Nothing) = 0

# builds on an existing `history` until the length is `n` or the model
# supply is exhausted (method shared by `fit` and `update`). Returns
# the bigger history:
function build(history,
               n,
               tuning,
               model,
               state,
               verbosity,
               acceleration,
               resampling_machine)
    j = _length(history)
    models_exhausted = false
    while j < n && !models_exhausted
        metamodels, state  = models(tuning,
                                    model,
                                    history,
                                    state,
                                    n - j,
                                    verbosity)
        Δj = _length(metamodels)
        Δj == 0 && (models_exhausted = true)
        shortfall = n - Δj
        if models_exhausted && shortfall > 0 && verbosity > -1
            @info "Only $j (of $n) models evaluated.\n"*
            "Model supply exhausted. "
        end
        Δj == 0 && break
        shortfall < 0 && (metamodels = metamodels[1:n - j])
        j += Δj

        Δhistory = assemble_events(metamodels,
                                   resampling_machine,
                                   verbosity,
                                   tuning,
                                   history,
                                   state,
                                   acceleration)
        history = _vcat(history, Δhistory)
    end
    return history, state
end

# given complete history, pick out best model, fit it on all data and
# generate report and cache (meta_state):
function finalize(tuned_model, history, state, verbosity, rm, data...)
    model = tuned_model.model
    tuning = tuned_model.tuning

    user_history = map(history) do entry
        delete(entry, :metadata)
    end

    entry =  best(tuned_model.selection_heuristic, history)
    best_model = entry.model
    best_history_entry = delete(entry, :metadata)
    fitresult = machine(best_model, data...)

    report0 = (best_model         = best_model,
               best_history_entry = best_history_entry,
               history            = user_history)

    if tuned_model.train_best
        fit!(fitresult, verbosity=verbosity - 1)
        report1 = merge(report0, (best_report=MLJBase.report(fitresult),))
    else
        report1 = merge(report0, (best_report=missing,))
    end

    report = merge(report1, tuning_report(tuning, history, state))
    meta_state = (history, deepcopy(tuned_model), state, rm)

    return fitresult, meta_state, report
end

function MLJBase.fit(tuned_model::EitherTunedModel{T,M},
                     verbosity::Integer, data...) where {T,M}
    tuning = tuned_model.tuning
    model = tuned_model.model
    range = tuned_model.range
    n = tuned_model.n === nothing ?
        default_n(tuning, range) : tuned_model.n

    verbosity < 1 || @info "Attempting to evaluate $n models."

    acceleration = tuned_model.acceleration

    state = setup(tuning, model, range, tuned_model.n, verbosity)

    # instantiate resampler (`model` to be replaced with mutated
    # clones during iteration below):
    resampler = Resampler(model=model,
                          resampling    = tuned_model.resampling,
                          measure       = tuned_model.measure,
                          weights       = tuned_model.weights,
                          operation     = tuned_model.operation,
                          check_measure = tuned_model.check_measure,
                          repeats       = tuned_model.repeats,
                          acceleration  = tuned_model.acceleration_resampling,
                          cache         = tuned_model.cache)
    resampling_machine = machine(resampler, data...; cache=false)
    history, state = build(nothing, n, tuning, model, state,
                           verbosity, acceleration, resampling_machine)

    rm = resampling_machine
    return finalize(tuned_model, history, state, verbosity, rm, data...)

end

function MLJBase.update(tuned_model::EitherTunedModel,
                        verbosity::Integer,
                        old_fitresult, old_meta_state, data...)

    history, old_tuned_model, state, resampling_machine = old_meta_state
    acceleration = tuned_model.acceleration

    tuning = tuned_model.tuning
    range = tuned_model.range
    model = tuned_model.model

    # exclamation points are for values actually used rather than
    # stored:
    n! = tuned_model.n === nothing ?
        default_n(tuning, range) : tuned_model.n

    old_n! = old_tuned_model.n === nothing ?
        default_n(tuning, range) : old_tuned_model.n

    if MLJBase.is_same_except(tuned_model, old_tuned_model, :n) &&
        n! >= old_n!

        verbosity < 1 || @info "Attempting to add $(n! - old_n!) models "*
        "to search, bringing total to $n!. "

        history, state = build(history, n!, tuning, model, state,
                               verbosity, acceleration, resampling_machine)

        rm = resampling_machine
        return finalize(tuned_model, history, state, verbosity, rm, data...)
    else
        return  fit(tuned_model, verbosity, data...)
    end
end

MLJBase.predict(tuned_model::EitherTunedModel, fitresult, Xnew) =
    predict(fitresult, Xnew)

function MLJBase.fitted_params(tuned_model::EitherTunedModel, fitresult)
    if tuned_model.train_best
        return (best_model=fitresult.model,
                best_fitted_params=fitted_params(fitresult))
    else
        return (best_model=fitresult.model,
                best_fitted_params=missing)
    end
end

## SUPPORT FOR MLJ ITERATION API

MLJBase.iteration_parameter(::Type{<:EitherTunedModel}) = :n
MLJBase.supports_training_losses(::Type{<:EitherTunedModel}) = true

function MLJBase.training_losses(tuned_model::EitherTunedModel, _report)
    _losses = MLJTuning.losses(tuned_model.selection_heuristic, _report.history)
    MLJTuning._length(_losses) == 0 && return nothing

    ret = similar(_losses)
    lowest = first(_losses)
    for i in eachindex(_losses)
        current = _losses[i]
        lowest = min(current, lowest)
        ret[i] = lowest
    end
    return ret
end

## METADATA

MLJBase.is_wrapper(::Type{<:EitherTunedModel}) = true
MLJBase.supports_weights(::Type{<:EitherTunedModel{<:Any,M}}) where M =
    MLJBase.supports_weights(M)
MLJBase.load_path(::Type{<:ProbabilisticTunedModel}) =
    "MLJTuning.ProbabilisticTunedModel"
MLJBase.load_path(::Type{<:DeterministicTunedModel}) =
    "MLJTuning.DeterministicTunedModel"
MLJBase.package_name(::Type{<:EitherTunedModel}) = "MLJTuning"
MLJBase.package_uuid(::Type{<:EitherTunedModel}) =
    "03970b2e-30c4-11ea-3135-d1576263f10f"
MLJBase.package_url(::Type{<:EitherTunedModel}) =
    "https://github.com/alan-turing-institute/MLJTuning.jl"
MLJBase.package_license(::Type{<:EitherTunedModel}) = "MIT"
MLJBase.is_pure_julia(::Type{<:EitherTunedModel{T,M}}) where {T,M} =
    MLJBase.is_pure_julia(M)
MLJBase.input_scitype(::Type{<:EitherTunedModel{T,M}}) where {T,M} =
    MLJBase.input_scitype(M)
MLJBase.target_scitype(::Type{<:EitherTunedModel{T,M}}) where {T,M} =
    MLJBase.target_scitype(M)


using Images
using ImageView

function float2img(a::AbstractArray{T, 3}) where {T <: Real}
    img = permutedims(a, (3, 1, 2))
    img = colorview(RGB, img)
    return img
end

function img2float(img::Array{RGB{T}}) where T
    a = channelview(img)
    a = rawview(a)
    a = permutedims(a, (2,3,1))
    a = a / 255
    return a
end

img2float(img::Array{Gray{T}}) where T = img2float(RGB.(img))

function load_image(path::AbstractString, transform=nothing)
    img = load(path)
    if transform != nothing
        img = transform(img)
    end
    return img2float(img)
end

function view_image(a::AbstractArray{<:Real, 3})
    img = float2img(a)
    imshow(img)
end

struct ImageFolder
    dirpath::AbstractString
    paths::Vector{String}
    labels::Vector{Int}
    classes::Vector{String}
    transform::Function
end

function ImageFolder(dirpath::AbstractString; transform::Function=identity)
    paths = String[]
    classes = String[]
    labels = Int[]
    for subdir in readdir(dirpath)
        subdir_path = joinpath(dirpath, subdir)
        for filename in readdir(subdir_path)
            path = joinpath(subdir_path, filename)
            push!(paths, path)
            if isempty(classes) || classes[end] != subdir
                push!(classes, subdir)
            end
            push!(labels, length(classes))
        end
    end
    return ImageFolder(dirpath, paths, labels, classes, transform)
end

Base.show(io::IO, dataset::ImageFolder) = print(io, "ImageFolder(\"$(dataset.dirpath)\")")
Base.length(dataset::ImageFolder) = length(dataset.labels)

function Base.getindex(dataset::ImageFolder, idxs)
    labels = dataset.labels[idxs]
    imgs = Vector{Array}(undef, length(idxs))
    # Threads.@threads - causes concurrency violation exception from FileIO
    for (i, idx) in collect(enumerate(idxs))
        img = load(dataset.paths[idx])
        img = dataset.transform(img)
        arr = img2float(img)
        imgs[i] = arr
    end
    @assert(all(sz -> sz == size(imgs[1]), [size(img) for img in imgs]),
            "Cannot concat images into a batch since they have different size. " *
            "Use `transform` keyword argument to ImageFolder constructor to resize " *
            "all images to the same size.")
    imgs4d = cat([reshape(img, size(img)..., 1) for img in imgs]...; dims=4)
    return imgs4d, labels
end

function Avalon.getbatch(dataset::ImageFolder, i::Int, sz::Int)
    start = (i-1)*sz + 1
    finish = min(i*sz, length(dataset))
    start > finish && return nothing
    return dataset[start:finish]
end


module PedCar

#=
Computationally efficient implementation of an autonomous driving scenario modeled as an MDP
This problem involves three agents:
    - an ego vehicle
    - an other car
    - a pedestrian
=#

using StaticArrays
using GridInterpolations
using POMDPs
using POMDPModelTools
using ParticleFilters
using AutomotiveDrivingModels
using AutoViz
using AutomotivePOMDPs
#using AutomotiveSensors
using Parameters
using Random
using LinearAlgebra

export
    PedCarMDP,
    PedCarMDPState,
    PedCarMDPAction,
    init_car_grid,
    init_ped_grid,
    init_l_grid,
    init_v_grid,
    car_transition,
    car_interpolation,
    car_reset,
    ped_transition,
    ped_interpolation,
    ped_reset,
    ego_transition,
    crash,
    ind2state,
    init_transition!,
    init_collision_checker!,
    animate_states,
    state2scene,
    get_mdp_state

include("mdp_type.jl")
include("spaces.jl")
include("interpolation.jl")
include("driver_models_helpers.jl")
include("transition.jl")
include("helpers.jl")
include("reward.jl")
include("render_helpers.jl")

#export
#    init_car_belief,
#    init_ped_belief,
#    ParticleOverlay,
#    CarState,
#    CarStateEstimator,
#    PedStateEstimator

#include("belief_updater.jl")

end # module


struct AsynchronousExecution{T <: AbstractFloat,
                            A <: AbstractVector,
                            S <: LQSolver} <: AbstractExecution
    subworkers::Vector{SubWorker{T,A,S}}
    work::Vector{Work}
    progressqueue::ProgressQueue{T}
    x̄::Vector{RunningAverage{A}}
    δ::Vector{RunningAverage{T}}
    decisions::Decisions{A}
    r::IteratedValue{T}
    active_workers::Vector{Future}
    # Bookkeeping
    subobjectives::Vector{A}
    finished::Vector{Int}
    # Parameters
    κ::T

    function AsynchronousExecution(κ::T, ::Type{T}, ::Type{A}, ::Type{S}) where {T <: AbstractFloat, A <: AbstractVector, S <: LQSolver}
        return new{T,A,S}(Vector{SubWorker{T,A,S}}(undef, nworkers()),
                          Vector{Work}(undef,nworkers()),
                          RemoteChannel(() -> Channel{Progress{T}}(4*nworkers())),
                          Vector{RunningAverage{A}}(undef,nworkers()),
                          Vector{RunningAverage{T}}(undef,nworkers()),
                          RemoteChannel(() -> IterationChannel(Dict{Int,A}())),
                          RemoteChannel(() -> IterationChannel(Dict{Int,T}())),
                          Vector{Future}(undef,nworkers()),
                          Vector{A}(),
                          Vector{Int}(),
                          κ)
    end
end

function init_subproblems!(ph::AbstractProgressiveHedgingSolver, subsolver::QPSolver, execution::AsynchronousExecution{T,A,S}) where {T <: AbstractFloat, A <: AbstractVector, S <: LQSolver}
    # Create subproblems on worker processes
    m = ph.stochasticprogram
    @sync begin
        for w in workers()
            execution.subworkers[w-1] = RemoteChannel(() -> Channel{Vector{SubProblem{T,A,S}}}(1), w)
            @async load_worker!(scenarioproblems(m), m, w, execution.subworkers[w-1], subsolver)
        end
    end
    @sync begin
        # Continue preparation
        for w in workers()
            execution.work[w-1] = RemoteChannel(() -> Channel{Int}(round(Int,10/execution.κ)), w)
            @async execution.x̄[w-1] = remotecall_fetch((sw, xdim)->begin
                subproblems = fetch(sw)
                if length(subproblems) > 0
                    x̄ = sum([s.π*s.x for s in subproblems])
                    return RemoteChannel(()->RunningAverageChannel(x̄, [s.x for s in subproblems]), myid())
                else
                    return RemoteChannel(()->RunningAverageChannel(zeros(T,xdim), Vector{A}()), myid())
                end
            end, w, execution.subworkers[w-1], decision_length(m))
            @async execution.δ[w-1] = remotecall_fetch((sw, xdim)->RemoteChannel(()->RunningAverageChannel(zero(T), fill(zero(T),length(fetch(sw))))), w, execution.subworkers[w-1], decision_length(m))
            put!(execution.work[w-1], 1)
        end
        # Prepare memory
        push!(execution.subobjectives, zeros(nscenarios(ph)))
        push!(execution.finished, 0)
        log_val = ph.parameters.log
        ph.parameters.log = false
        log!(ph)
        ph.parameters.log = log_val
    end
    update_iterate!(ph)
    # Init δ₂
    @sync begin
        for w in workers()
            @async remotecall_fetch((sw,ξ,δ)->begin
                for (i,s) ∈ enumerate(fetch(sw))
                    take!(δ, i)
                    put!(δ, i, norm(s.x - ξ, 2)^2, s.π)
                end
            end, w, execution.subworkers[w-1], ph.ξ, execution.δ[w-1])
        end
    end
    return ph
end

function iterate!(ph::AbstractProgressiveHedgingSolver, execution::AsynchronousExecution{T}) where T <: AbstractFloat
    wait(execution.progressqueue)
    while isready(execution.progressqueue)
        # Add new cuts from subworkers
        t::Int, i::Int, Q::T = take!(execution.progressqueue)
        if Q == Inf
            @warn "Subproblem $(i) is infeasible, aborting procedure."
            return :Infeasible
        end
        execution.subobjectives[t][i] = Q
        execution.finished[t] += 1
        if execution.finished[t] == nscenarios(ph)
            # Update objective
            ph.Q_history[t] = current_objective_value(ph, execution.subobjectives[t])
            ph.data.Q = ph.Q_history[t]
        end
    end
    # Project and generate new iterate
    t = ph.data.iterations
    if execution.finished[t] >= execution.κ*nscenarios(ph)
        # Get dual gap
        update_dual_gap!(ph)
        # Update progress
        @unpack δ₁, δ₂ = ph.data
        ph.dual_gaps[t] = δ₂
        ph.data.δ = sqrt(δ₁ + δ₂)/(1e-10+norm(ph.ξ,2))
        # Check if optimal
        if check_optimality(ph)
            # Optimal, tell workers to stop
            map((w,aw)->!isready(aw) && put!(w,t), execution.work, execution.active_workers)
            map((w,aw)->!isready(aw) && put!(w,-1), execution.work, execution.active_workers)
            # Final log
            log!(ph)
            return :Optimal
        end
        # Update penalty (if applicable)
        update_penalty!(ph)
        # Update iterate
        update_iterate!(ph)
        # Send new work to workers
        put!(execution.decisions, t+1, ph.ξ)
        put!(execution.r, t+1, penalty(ph))
        map((w,aw)->!isready(aw) && put!(w,t+1), execution.work, execution.active_workers)
        # Prepare memory for next iteration
        push!(execution.subobjectives, zeros(nscenarios(ph)))
        push!(execution.finished, 0)
        # Log progress
        log!(ph)
    end
    # Just return a valid status for this iteration
    return :Valid
end

function init_workers!(ph::AbstractProgressiveHedgingSolver, execution::AsynchronousExecution)
    # Load initial decision
    put!(execution.decisions, 1, ph.ξ)
    put!(execution.r, 1, penalty(ph))
    for w in workers()
        execution.active_workers[w-1] = remotecall(work_on_subproblems!,
                                                   w,
                                                   execution.subworkers[w-1],
                                                   execution.work[w-1],
                                                   execution.progressqueue,
                                                   execution.x̄[w-1],
                                                   execution.δ[w-1],
                                                   execution.decisions,
                                                   execution.r)
    end
    return nothing
end

function close_workers!(ph::AbstractProgressiveHedgingSolver, execution::AsynchronousExecution)
    map(wait, execution.active_workers)
end

function resolve_subproblems!(ph::AbstractProgressiveHedgingSolver, execution::AsynchronousExecution)
    return nothing
end

function update_iterate!(ph::AbstractProgressiveHedgingSolver, execution::AsynchronousExecution)
    ξ_prev = copy(ph.ξ)
    ph.ξ[:] = sum(fetch.(execution.x̄))
    # Update δ₁
    ph.data.δ₁ = norm(ph.ξ-ξ_prev, 2)^2
    return nothing
end

function update_subproblems!(ph::AbstractProgressiveHedgingSolver, execution::AsynchronousExecution)
    return nothing
end

function update_dual_gap!(ph::AbstractProgressiveHedgingSolver, execution::AsynchronousExecution)
    ph.data.δ₂ = sum(fetch.(execution.δ))
    return nothing
end

function calculate_objective_value(ph::AbstractProgressiveHedgingSolver, execution::AsynchronousExecution)
    return calculate_objective_value(ph, execution.subworkers)
end

function
    fill_submodels!(ph::AbstractProgressiveHedgingSolver, scenarioproblems, execution::AsynchronousExecution)
    return fill_submodels!(ph, scenarioproblems, execution.subworkers)
end

# API
# ------------------------------------------------------------
"""
    Asynchronous

...
# Parameters
- `κ::Real = 0.5`: Amount of subproblems, relative to the total number of scenarios, required to generate a new iterate in master procedure.

...
"""
struct Asynchronous <: Execution
    κ::Float64

    function Asynchronous(; κ::AbstractFloat = 0.5)
        return new(κ)
    end
end

function (execution::Asynchronous)(::Type{T}, ::Type{A}, ::Type{S}) where {T <: AbstractFloat, A <: AbstractVector, S <: LQSolver}
    return AsynchronousExecution(execution.κ, T, A, S)
end

function str(::Asynchronous)
    return "asynchronous execution"
end